diff --git a/packages/text-editor/lang/en.json b/packages/text-editor/lang/en.json index dee6659821b..8a5ccc79119 100644 --- a/packages/text-editor/lang/en.json +++ b/packages/text-editor/lang/en.json @@ -37,6 +37,9 @@ "CategoryColumn": "Columns", "Table": "Table", "InsertTable": "Insert table", - "TableOptions": "Customize table" + "TableOptions": "Customize table", + "Width": "Width", + "Height": "Height", + "Unset": "Unset" } } diff --git a/packages/text-editor/lang/ru.json b/packages/text-editor/lang/ru.json index 12b539bf788..416cf30d99b 100644 --- a/packages/text-editor/lang/ru.json +++ b/packages/text-editor/lang/ru.json @@ -37,6 +37,9 @@ "CategoryColumn": "Колонки", "Table": "Таблица", "InsertTable": "Добавить таблицу", - "TableOptions": "Настроить таблицу" + "TableOptions": "Настроить таблицу", + "Width": "Щирина", + "Height": "Высота", + "Unset": "Убрать" } } diff --git a/packages/text-editor/src/components/StyledTextBox.svelte b/packages/text-editor/src/components/StyledTextBox.svelte index 712b9e15446..ce2b826e215 100644 --- a/packages/text-editor/src/components/StyledTextBox.svelte +++ b/packages/text-editor/src/components/StyledTextBox.svelte @@ -16,7 +16,7 @@ import { Completion } from '../Completion' import textEditorPlugin from '../plugin' import StyledTextEditor from './StyledTextEditor.svelte' - import { completionConfig } from './extensions' + import { completionConfig, imagePlugin } from './extensions' export let label: IntlString | undefined = undefined export let content: string @@ -161,7 +161,7 @@ {enableFormatting} {autofocus} {isScrollable} - extensions={enableBackReferences ? [completionPlugin] : []} + extensions={enableBackReferences ? [completionPlugin, imagePlugin] : [imagePlugin]} bind:content={rawValue} bind:this={textEditor} on:attach diff --git a/packages/text-editor/src/components/extensions.ts b/packages/text-editor/src/components/extensions.ts index 2ecd523320e..e0d33e4decd 100644 --- a/packages/text-editor/src/components/extensions.ts +++ b/packages/text-editor/src/components/extensions.ts @@ -17,6 +17,7 @@ import Link from '@tiptap/extension-link' import { CompletionOptions } from '../Completion' import MentionList from './MentionList.svelte' import { SvelteRenderer } from './SvelteRenderer' +import { ImageRef } from './imageExt' export const tableExtensions = [ Table.configure({ @@ -166,3 +167,8 @@ 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 new file mode 100644 index 00000000000..644c66824cc --- /dev/null +++ b/packages/text-editor/src/components/imageExt.ts @@ -0,0 +1,225 @@ +import { getEmbeddedLabel } from '@hcengineering/platform' +import { getFileUrl } from '@hcengineering/presentation' +import { Action, Menu, getEventPositionElement, showPopup } from '@hcengineering/ui' +import { Node, createNodeFromContent, mergeAttributes, nodeInputRule } from '@tiptap/core' +import { Plugin, PluginKey } from 'prosemirror-state' +import plugin from '../plugin' + +export interface ImageOptions { + inline: boolean + HTMLAttributes: Record + + showPreview?: (event: MouseEvent, fileId: string) => void +} + +declare module '@tiptap/core' { + interface Commands { + image: { + /** + * Add an image + */ + setImage: (options: { src: string, alt?: string, title?: string }) => ReturnType + } + } +} + +export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/ + +export const ImageRef = Node.create({ + name: 'image', + + addOptions () { + return { + inline: false, + HTMLAttributes: {} + } + }, + + inline () { + return this.options.inline + }, + + group () { + return this.options.inline ? 'inline' : 'block' + }, + + draggable: true, + selectable: true, + + addAttributes () { + return { + fileid: { + default: null, + parseHTML: (element) => element.getAttribute('file-id'), + renderHTML: (attributes) => { + // eslint-disable-next-line + if (!attributes.fileid) { + return {} + } + + return { + 'file-id': attributes.fileid + } + } + }, + width: { + default: null + }, + height: { + default: null + } + } + }, + + parseHTML () { + return [ + { + tag: `img[data-type="${this.name}"]` + } + ] + }, + + renderHTML ({ HTMLAttributes }) { + const merged = mergeAttributes( + { + 'data-type': this.name + }, + this.options.HTMLAttributes, + HTMLAttributes + ) + merged.src = getFileUrl(merged['file-id'], 'full') + merged.class = 'textEditorImage' + return ['img', merged] + }, + + addCommands () { + return { + setImage: + (options) => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options + }) + } + } + }, + + addInputRules () { + return [ + nodeInputRule({ + find: inputRegex, + type: this.type, + getAttributes: (match) => { + const [, , alt, src, title] = match + + return { src, alt, title } + } + }) + ] + }, + addProseMirrorPlugins () { + return [ + new Plugin({ + key: new PluginKey('handle-image-paste'), + props: { + handleDrop (view, event, slice) { + const uris = (event.dataTransfer?.getData('text/uri-list') ?? '') + .split('\r\n') + .filter((it) => !it.startsWith('#')) + let result = false + 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 + } + + const _file = (url.searchParams.get('file') ?? '').split('/').join('') + + if (_file.trim().length === 0) { + return + } + const content = createNodeFromContent( + ``, + view.state.schema, + { + parseOptions: { + preserveWhitespace: 'full' + } + } + ) + event.preventDefault() + event.stopPropagation() + view.dispatch(view.state.tr.insert(pos?.pos ?? 0, content)) + result = true + } + } + return result + }, + handleClick: (view, pos, event) => { + if (event.button !== 0) { + return false + } + + const node = event.target as unknown as HTMLElement + if (node != null) { + const fileId = (node as any).attributes['file-id']?.value + if (fileId === undefined) { + return false + } + const pos = view.posAtDOM(node, 0) + + const actions: Action[] = [ + { + label: plugin.string.Width, + 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) + ) + } + } + } + ) + } + }, + { + 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) + ) + } + } + } + ) + } + } + ] + event.preventDefault() + event.stopPropagation() + showPopup(Menu, { actions }, getEventPositionElement(event)) + } + return false + } + } + }) + ] + } +}) diff --git a/packages/text-editor/src/plugin.ts b/packages/text-editor/src/plugin.ts index 2dd049cdf9f..d3243d67d21 100644 --- a/packages/text-editor/src/plugin.ts +++ b/packages/text-editor/src/plugin.ts @@ -64,6 +64,9 @@ export default plugin(textEditorId, { CategoryRow: '' as IntlString, CategoryColumn: '' as IntlString, Table: '' as IntlString, - TableOptions: '' as IntlString + TableOptions: '' as IntlString, + Width: '' as IntlString, + Height: '' as IntlString, + Unset: '' as IntlString } }) diff --git a/packages/theme/styles/_text-editor.scss b/packages/theme/styles/_text-editor.scss new file mode 100644 index 00000000000..05389aa15ae --- /dev/null +++ b/packages/theme/styles/_text-editor.scss @@ -0,0 +1,4 @@ +.textEditorImage { + cursor: pointer; + object-fit: contain; +} \ No newline at end of file diff --git a/packages/theme/styles/global.scss b/packages/theme/styles/global.scss index 60217cfc5b0..f238620ea5f 100644 --- a/packages/theme/styles/global.scss +++ b/packages/theme/styles/global.scss @@ -23,6 +23,7 @@ @import "./panel.scss"; @import "./prose.scss"; @import "./button.scss"; +@import "./_text-editor.scss"; @font-face { font-family: 'IBM Plex Sans'; diff --git a/packages/ui/src/components/DropdownLabelsPopup.svelte b/packages/ui/src/components/DropdownLabelsPopup.svelte index 093da6b5c7a..be8b57e3d2a 100644 --- a/packages/ui/src/components/DropdownLabelsPopup.svelte +++ b/packages/ui/src/components/DropdownLabelsPopup.svelte @@ -29,6 +29,7 @@ export let items: DropdownTextItem[] export let selected: DropdownTextItem['id'] | DropdownTextItem['id'][] | undefined = undefined export let multiselect: boolean = false + export let enableSearch = true let search: string = '' const dispatch = createEventDispatcher() @@ -91,18 +92,20 @@ dispatch('changeContent') }} > -
- -
+ {#if enableSearch} +
+ +
+ {/if}
diff --git a/plugins/attachment-resources/src/components/AccordionEditor.svelte b/plugins/attachment-resources/src/components/AccordionEditor.svelte index a9bf20ceff5..425d1231bdd 100644 --- a/plugins/attachment-resources/src/components/AccordionEditor.svelte +++ b/plugins/attachment-resources/src/components/AccordionEditor.svelte @@ -115,7 +115,7 @@ bind:this={attachments[i]} alwaysEdit showButtons - fakeAttach={withoutAttach ? 'hidden' : i < items.length - 1 ? 'fake' : 'normal'} + enableAttachments={!withoutAttach} bind:content={item.content} placeholder={textEditorPlugin.string.EditorPlaceholder} {objectId} diff --git a/plugins/attachment-resources/src/components/AttachmentPresenter.svelte b/plugins/attachment-resources/src/components/AttachmentPresenter.svelte index 5b8b1a2ad2d..aea3a24cdec 100644 --- a/plugins/attachment-resources/src/components/AttachmentPresenter.svelte +++ b/plugins/attachment-resources/src/components/AttachmentPresenter.svelte @@ -83,7 +83,7 @@ class="flex-center icon" class:svg={value.type === 'image/svg+xml'} class:image={isImage(value.type)} - style:background-image={isImage(value.type) ? `url(${getFileUrl(value.file)})` : 'none'} + style:background-image={isImage(value.type) ? `url(${getFileUrl(value.file, 'large')})` : 'none'} > {#if !isImage(value.type)}{iconLabel(value.name)}{/if}
diff --git a/plugins/attachment-resources/src/components/AttachmentPreview.svelte b/plugins/attachment-resources/src/components/AttachmentPreview.svelte index 5973ab4b0fc..065e8a8fa5b 100644 --- a/plugins/attachment-resources/src/components/AttachmentPreview.svelte +++ b/plugins/attachment-resources/src/components/AttachmentPreview.svelte @@ -47,7 +47,7 @@ dispatch('open', popupInfo.id) }} > - {value.name} + {value.name}
diff --git a/plugins/attachment-resources/src/components/AttachmentStyleBoxEditor.svelte b/plugins/attachment-resources/src/components/AttachmentStyleBoxEditor.svelte index 47c18edf8c4..8cccbd30849 100644 --- a/plugins/attachment-resources/src/components/AttachmentStyleBoxEditor.svelte +++ b/plugins/attachment-resources/src/components/AttachmentStyleBoxEditor.svelte @@ -83,7 +83,7 @@ {focusIndex} enableBackReferences={true} bind:this={descriptionBox} - useAttachmentPreview={true} + useAttachmentPreview={false} objectId={object._id} _class={object._class} space={object.space} diff --git a/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte b/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte index b485818f1ef..32a3fd677a2 100644 --- a/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte +++ b/plugins/attachment-resources/src/components/AttachmentStyledBox.svelte @@ -38,11 +38,11 @@ export let formatButtonSize: IconSize = 'small' export let maxHeight: 'max' | 'card' | 'limited' | string = 'max' export let focusable: boolean = false - export let fakeAttach: 'fake' | 'hidden' | 'normal' = 'normal' export let refContainer: HTMLElement | undefined = undefined export let shouldSaveDraft: boolean = false export let useAttachmentPreview = false export let focusIndex: number | undefined = -1 + export let enableAttachments: boolean = true export let enableBackReferences: boolean = false let draftKey = objectId ? `${objectId}_attachments` : undefined @@ -334,12 +334,11 @@
(fakeAttach === 'normal' ? pasteAction(ev) : undefined)} + on:paste={(ev) => pasteAction(ev)} on:dragover|preventDefault={() => {}} on:dragleave={() => {}} on:drop|preventDefault|stopPropagation={(ev) => { - if (fakeAttach === 'fake') dispatch('attach', { action: 'drop', event: ev }) - else fileDrop(ev) + fileDrop(ev) }} >
@@ -350,7 +349,7 @@ {placeholder} {alwaysEdit} {showButtons} - hideAttachments={fakeAttach === 'hidden'} + hideAttachments={!enableAttachments} {buttonSize} {formatButtonSize} {maxHeight} @@ -364,12 +363,11 @@ on:open-document on:open-document on:attach={() => { - if (fakeAttach === 'fake') dispatch('attach', { action: 'add' }) - else if (fakeAttach === 'normal') attach() + attach() }} />
- {#if attachments.size && fakeAttach === 'normal'} + {#if attachments.size && enableAttachments}
{#each Array.from(attachments.values()) as attachment, index}
diff --git a/plugins/tracker-resources/src/components/CreateIssue.svelte b/plugins/tracker-resources/src/components/CreateIssue.svelte index a64b1b9a899..fa51f7acab2 100644 --- a/plugins/tracker-resources/src/components/CreateIssue.svelte +++ b/plugins/tracker-resources/src/components/CreateIssue.svelte @@ -597,6 +597,7 @@ showButtons={false} kind={'indented'} enableBackReferences={true} + enableAttachments={false} bind:content={object.description} placeholder={tracker.string.IssueDescriptionPlaceholder} on:changeSize={() => dispatch('changeContent')}