diff --git a/packages/presentation/src/components/Card.svelte b/packages/presentation/src/components/Card.svelte index 18ff3688819..3de22638c54 100644 --- a/packages/presentation/src/components/Card.svelte +++ b/packages/presentation/src/components/Card.svelte @@ -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() @@ -87,7 +88,7 @@ {/if}
- +
diff --git a/packages/text-editor/src/components/StyledTextBox.svelte b/packages/text-editor/src/components/StyledTextBox.svelte index c0296f677cd..0e6ca20ca4e 100644 --- a/packages/text-editor/src/components/StyledTextBox.svelte +++ b/packages/text-editor/src/components/StyledTextBox.svelte @@ -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 @@ -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 @@ -129,6 +133,27 @@ dispatch('open-document', { event, _id, _class }) } }) + + const attachments = new Map() + + 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) + } + } diff --git a/packages/text-editor/src/components/StyledTextEditor.svelte b/packages/text-editor/src/components/StyledTextEditor.svelte index 11a52e35575..0ff2d1b765f 100644 --- a/packages/text-editor/src/components/StyledTextEditor.svelte +++ b/packages/text-editor/src/components/StyledTextEditor.svelte @@ -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() @@ -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) + } @@ -587,6 +595,7 @@ bind:content {placeholder} {extensions} + bind:this={textEditor} bind:isEmpty on:value on:content={(ev) => { diff --git a/packages/text-editor/src/components/TextEditor.svelte b/packages/text-editor/src/components/TextEditor.svelte index 8ebb86e7060..45968943f76 100644 --- a/packages/text-editor/src/components/TextEditor.svelte +++ b/packages/text-editor/src/components/TextEditor.svelte @@ -16,7 +16,7 @@
diff --git a/packages/text-editor/src/components/extensions.ts b/packages/text-editor/src/components/extensions.ts index 05d1fd2a566..cf5a88b44da 100644 --- a/packages/text-editor/src/components/extensions.ts +++ b/packages/text-editor/src/components/extensions.ts @@ -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({ @@ -176,8 +175,3 @@ export const completionConfig: Partial = { } } } - -/** - * @public - */ -export const imagePlugin = ImageRef.configure({ inline: false, HTMLAttributes: {} }) diff --git a/packages/text-editor/src/components/imageExt.ts b/packages/text-editor/src/components/imageExt.ts index 4830803c898..ba22a2080c5 100644 --- a/packages/text-editor/src/components/imageExt.ts +++ b/packages/text-editor/src/components/imageExt.ts @@ -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 - showPreview?: (event: MouseEvent, fileId: string) => void + attachFile?: FileAttachFunction + + reportNode?: (id: string, node: ProseMirrorNode) => void } declare module '@tiptap/core' { @@ -23,8 +35,14 @@ declare module '@tiptap/core' { } } +/** + * @public + */ export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/ +/** + * @public + */ export const ImageRef = Node.create({ name: 'image', @@ -79,7 +97,7 @@ export const ImageRef = Node.create({ ] }, - renderHTML ({ HTMLAttributes }) { + renderHTML ({ node, HTMLAttributes }) { const merged = mergeAttributes( { 'data-type': this.name @@ -87,7 +105,8 @@ export const ImageRef = Node.create({ 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': @@ -105,11 +124,11 @@ export const ImageRef = Node.create({ 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] }, @@ -140,6 +159,7 @@ export const ImageRef = Node.create({ ] }, addProseMirrorPlugins () { + const opt = this.options return [ new Plugin({ key: new PluginKey('handle-image-paste'), @@ -149,10 +169,9 @@ export const ImageRef = Node.create({ .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 @@ -164,7 +183,7 @@ export const ImageRef = Node.create({ return } const content = createNodeFromContent( - ``, + ``, view.state.schema, { parseOptions: { @@ -178,6 +197,36 @@ export const ImageRef = Node.create({ 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( + ``, + view.state.schema, + { + parseOptions: { + preserveWhitespace: 'full' + } + } + ) + view.dispatch(view.state.tr.insert(pos?.pos ?? 0, content)) + } + } + }) + } + } + } return result }, handleClick: (view, pos, event) => { @@ -199,18 +248,27 @@ export const ImageRef = Node.create({ 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) + ) } } - ) + }) } }, { @@ -218,18 +276,27 @@ export const ImageRef = Node.create({ 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) + ) } } - ) + }) } } ] diff --git a/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte b/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte index 946aa6ce316..7968718c15e 100644 --- a/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte +++ b/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte @@ -138,7 +138,7 @@ } } - async function createAttachment (file: File) { + async function createAttachment (file: File): Promise<{ file: string; type: string } | undefined> { if (space === undefined || objectId === undefined || _class === undefined) return try { const uuid = await uploadFile(file) @@ -164,6 +164,7 @@ saveDraft() dispatch('attach', { action: 'saved', value: attachments.size }) dispatch('attached', _id) + return { file: uuid, type: file.type } } catch (err: any) { setPlatformStatus(unknownError(err)) } @@ -199,6 +200,7 @@ removedAttachments.add(attachment) attachments.delete(attachment._id) attachments = attachments + refInput.removeAttachment(attachment.file) saveDraft() dispatch('detached', attachment._id) } @@ -367,6 +369,9 @@ on:attach={() => { attach() }} + attachFile={async (file) => { + return createAttachment(file) + }} />
{#if attachments.size && enableAttachments} diff --git a/plugins/tracker-resources/src/components/CreateIssue.svelte b/plugins/tracker-resources/src/components/CreateIssue.svelte index 230f05e7747..2cbf2f59254 100644 --- a/plugins/tracker-resources/src/components/CreateIssue.svelte +++ b/plugins/tracker-resources/src/components/CreateIssue.svelte @@ -533,6 +533,7 @@ onCancel={showConfirmationDialog} hideAttachments={attachments.size === 0} hideSubheader={!parentIssue} + noFade={true} on:changeContent > @@ -611,6 +612,7 @@ alwaysEdit showButtons={false} kind={'indented'} + isScrollable={false} enableBackReferences={true} enableAttachments={false} bind:content={object.description}