Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UBER-395: Allow to drop images into description #3382

Merged
merged 1 commit into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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