Skip to content

Commit

Permalink
UBER-395: Allow to drop images into description (#3382)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
  • Loading branch information
haiodo authored Jun 7, 2023
1 parent 3c5648c commit aa09fa6
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 42 deletions.
3 changes: 2 additions & 1 deletion packages/presentation/src/components/Card.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
export let accentHeader: boolean = false
export let gap: string | undefined = undefined
export let width: 'large' | 'medium' | 'small' | 'x-small' | 'menu' = 'large'
export let noFade = false
const dispatch = createEventDispatcher()
Expand Down Expand Up @@ -87,7 +88,7 @@
</div>
{/if}
<div class="antiCard-content">
<Scroller padding={$$slots.pool ? '.5rem 1.5rem' : '.5rem 1.5rem 1.5rem'} {gap}>
<Scroller padding={$$slots.pool ? '.5rem 1.5rem' : '.5rem 1.5rem 1.5rem'} {gap} {noFade}>
<slot />
</Scroller>
</div>
Expand Down
27 changes: 26 additions & 1 deletion packages/text-editor/src/components/StyledTextBox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import { Completion } from '../Completion'
import textEditorPlugin from '../plugin'
import StyledTextEditor from './StyledTextEditor.svelte'
import { completionConfig, imagePlugin } from './extensions'
import { completionConfig } from './extensions'
import { ImageRef, FileAttachFunction } from './imageExt'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
export let label: IntlString | undefined = undefined
export let content: string
Expand All @@ -38,6 +40,8 @@
export let enableBackReferences: boolean = false
export let isScrollable: boolean = true
export let attachFile: FileAttachFunction | undefined = undefined
const Mode = {
View: 1,
Edit: 2
Expand Down Expand Up @@ -129,6 +133,27 @@
dispatch('open-document', { event, _id, _class })
}
})
const attachments = new Map<string, ProseMirrorNode>()
const imagePlugin = ImageRef.configure({
inline: false,
HTMLAttributes: {},
attachFile,
reportNode: (id, node) => {
attachments.set(id, node)
}
})
/**
* @public
*/
export function removeAttachment (id: string): void {
const nde = attachments.get(id)
if (nde !== undefined) {
textEditor.removeNode(nde)
}
}
</script>

<!-- svelte-ignore a11y-click-events-have-key-events -->
Expand Down
9 changes: 9 additions & 0 deletions packages/text-editor/src/components/StyledTextEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import LinkPopup from './LinkPopup.svelte'
import StyleButton from './StyleButton.svelte'
import TextEditor from './TextEditor.svelte'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
const dispatch = createEventDispatcher()
Expand Down Expand Up @@ -450,6 +451,13 @@
: buttonSize === 'medium'
? 'h-5 max-h-5'
: 'h-4 max-h-4'
/**
* @public
*/
export function removeNode (nde: ProseMirrorNode): void {
textEditor.removeNode(nde)
}
</script>

<!-- svelte-ignore a11y-click-events-have-key-events -->
Expand Down Expand Up @@ -587,6 +595,7 @@
bind:content
{placeholder}
{extensions}
bind:this={textEditor}
bind:isEmpty
on:value
on:content={(ev) => {
Expand Down
19 changes: 18 additions & 1 deletion packages/text-editor/src/components/TextEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
<script lang="ts">
import { IntlString, translate } from '@hcengineering/platform'
import type { FocusPosition } from '@tiptap/core'
import { FocusPosition } from '@tiptap/core'
import { AnyExtension, Editor, Extension, HTMLContent } from '@tiptap/core'
import { Level } from '@tiptap/extension-heading'
import Placeholder from '@tiptap/extension-placeholder'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import textEditorPlugin from '../plugin'
import { FormatMode } from '../types'
import { defaultExtensions } from './extensions'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
export let content: string = ''
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
Expand Down Expand Up @@ -236,6 +237,22 @@
editor.destroy()
}
})
/**
* @public
*/
export function removeNode (nde: ProseMirrorNode): void {
const deleteOp = (n: ProseMirrorNode, pos: number) => {
if (nde === n) {
// const pos = editor.view.posAtDOM(nde, 0)
editor.view.dispatch(editor.view.state.tr.delete(pos, pos + 1))
}
n.descendants(deleteOp)
}
editor.view.state.doc.descendants((n, pos) => {
deleteOp(n, pos)
})
}
</script>

<div class="select-text" style="width: 100%;" bind:this={element} />
Expand Down
12 changes: 3 additions & 9 deletions packages/text-editor/src/components/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,16 @@ import Heading, { Level } from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import StarterKit from '@tiptap/starter-kit'

import TipTapCodeBlock from '@tiptap/extension-code-block'
import Code from '@tiptap/extension-code'
import TipTapCodeBlock from '@tiptap/extension-code-block'
import Gapcursor from '@tiptap/extension-gapcursor'

import { AnyExtension } from '@tiptap/core'
import Link from '@tiptap/extension-link'
import Typography from '@tiptap/extension-typography'
import { CompletionOptions } from '../Completion'
import MentionList from './MentionList.svelte'
import { SvelteRenderer } from './SvelteRenderer'
import { ImageRef } from './imageExt'
import Typography from '@tiptap/extension-typography'
import { AnyExtension } from '@tiptap/core'

export const tableExtensions = [
Table.configure({
Expand Down Expand Up @@ -176,8 +175,3 @@ export const completionConfig: Partial<CompletionOptions> = {
}
}
}

/**
* @public
*/
export const imagePlugin = ImageRef.configure({ inline: false, HTMLAttributes: {} })
125 changes: 96 additions & 29 deletions packages/text-editor/src/components/imageExt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ import { Node, createNodeFromContent, mergeAttributes, nodeInputRule } from '@ti
import { Plugin, PluginKey } from 'prosemirror-state'
import plugin from '../plugin'

import { Node as ProseMirrorNode } from '@tiptap/pm/model'

/**
* @public
*/
export type FileAttachFunction = (file: File) => Promise<{ file: string, type: string } | undefined>

/**
* @public
*/
export interface ImageOptions {
inline: boolean
HTMLAttributes: Record<string, any>

showPreview?: (event: MouseEvent, fileId: string) => void
attachFile?: FileAttachFunction

reportNode?: (id: string, node: ProseMirrorNode) => void
}

declare module '@tiptap/core' {
Expand All @@ -23,8 +35,14 @@ declare module '@tiptap/core' {
}
}

/**
* @public
*/
export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/

/**
* @public
*/
export const ImageRef = Node.create<ImageOptions>({
name: 'image',

Expand Down Expand Up @@ -79,15 +97,16 @@ export const ImageRef = Node.create<ImageOptions>({
]
},

renderHTML ({ HTMLAttributes }) {
renderHTML ({ node, HTMLAttributes }) {
const merged = mergeAttributes(
{
'data-type': this.name
},
this.options.HTMLAttributes,
HTMLAttributes
)
merged.src = getFileUrl(merged['file-id'], 'full')
const id = merged['file-id']
merged.src = getFileUrl(id, 'full')
let width: IconSize | undefined
switch (merged.width) {
case '32px':
Expand All @@ -105,11 +124,11 @@ export const ImageRef = Node.create<ImageOptions>({
break
}
if (width !== undefined) {
merged.src = getFileUrl(merged['file-id'], width)
merged.srcset =
getFileUrl(merged['file-id'], width) + ' 1x,' + getFileUrl(merged['file-id'], getIconSize2x(width)) + ' 2x'
merged.src = getFileUrl(id, width)
merged.srcset = getFileUrl(id, width) + ' 1x,' + getFileUrl(id, getIconSize2x(width)) + ' 2x'
}
merged.class = 'textEditorImage'
this.options.reportNode?.(id, node)
return ['img', merged]
},

Expand Down Expand Up @@ -140,6 +159,7 @@ export const ImageRef = Node.create<ImageOptions>({
]
},
addProseMirrorPlugins () {
const opt = this.options
return [
new Plugin({
key: new PluginKey('handle-image-paste'),
Expand All @@ -149,10 +169,9 @@ export const ImageRef = Node.create<ImageOptions>({
.split('\r\n')
.filter((it) => !it.startsWith('#'))
let result = false
const pos = view.posAtCoords({ left: event.x, top: event.y })
for (const uri of uris) {
if (uri !== '') {
const pos = view.posAtCoords({ left: event.x, top: event.y })

const url = new URL(uri)
if (url.hostname !== location.hostname) {
return
Expand All @@ -164,7 +183,7 @@ export const ImageRef = Node.create<ImageOptions>({
return
}
const content = createNodeFromContent(
`<img data-type='image' width='25%' file-id='${_file}'></img>`,
`<img data-type='image' width='75%' file-id='${_file}'></img>`,
view.state.schema,
{
parseOptions: {
Expand All @@ -178,6 +197,36 @@ export const ImageRef = Node.create<ImageOptions>({
result = true
}
}
if (result) {
return result
}

const files = event.dataTransfer?.files
if (files !== undefined && opt.attachFile !== undefined) {
event.preventDefault()
event.stopPropagation()
for (let i = 0; i < files.length; i++) {
const file = files.item(i)
if (file != null) {
void opt.attachFile(file).then((id) => {
if (id !== undefined) {
if (id.type.includes('image')) {
const content = createNodeFromContent(
`<img data-type='image' width='75%' file-id='${id.file}'></img>`,
view.state.schema,
{
parseOptions: {
preserveWhitespace: 'full'
}
}
)
view.dispatch(view.state.tr.insert(pos?.pos ?? 0, content))
}
}
})
}
}
}
return result
},
handleClick: (view, pos, event) => {
Expand All @@ -199,37 +248,55 @@ export const ImageRef = Node.create<ImageOptions>({
action: async (props, event) => {},
component: Menu,
props: {
actions: ['32px', '64px', '128px', '256px', '512px', '25%', '50%', '100%', plugin.string.Unset].map(
(it) => {
return {
label: it === plugin.string.Unset ? it : getEmbeddedLabel(it),
action: async () => {
view.dispatch(
view.state.tr.setNodeAttribute(pos, 'width', it === plugin.string.Unset ? null : it)
)
}
actions: [
'32px',
'64px',
'128px',
'256px',
'512px',
'25%',
'50%',
'75%',
'100%',
plugin.string.Unset
].map((it) => {
return {
label: it === plugin.string.Unset ? it : getEmbeddedLabel(it),
action: async () => {
view.dispatch(
view.state.tr.setNodeAttribute(pos, 'width', it === plugin.string.Unset ? null : it)
)
}
}
)
})
}
},
{
label: plugin.string.Height,
action: async (props, event) => {},
component: Menu,
props: {
actions: ['32px', '64px', '128px', '256px', '512px', '25%', '50%', '100%', plugin.string.Unset].map(
(it) => {
return {
label: it === plugin.string.Unset ? it : getEmbeddedLabel(it),
action: async () => {
view.dispatch(
view.state.tr.setNodeAttribute(pos, 'height', it === plugin.string.Unset ? null : it)
)
}
actions: [
'32px',
'64px',
'128px',
'256px',
'512px',
'25%',
'50%',
'75%',
'100%',
plugin.string.Unset
].map((it) => {
return {
label: it === plugin.string.Unset ? it : getEmbeddedLabel(it),
action: async () => {
view.dispatch(
view.state.tr.setNodeAttribute(pos, 'height', it === plugin.string.Unset ? null : it)
)
}
}
)
})
}
}
]
Expand Down
Loading

0 comments on commit aa09fa6

Please sign in to comment.