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}