From f21996252b697ca7450660b039aab0e9d01c5b67 Mon Sep 17 00:00:00 2001 From: Victor Ilyushchenko Date: Thu, 5 Jun 2025 07:05:21 +0300 Subject: [PATCH] Generalized implementation of the text editor toolbar (#9147) Signed-off-by: Victor Ilyushchenko --- models/controlled-documents/src/index.ts | 1 + models/text-editor/src/index.ts | 46 +- packages/text/src/nodes/image.ts | 88 +-- .../ui/src/components/PopupInstance.svelte | 2 + .../src/components/EditDocPanel.svelte | 7 +- .../components/document/EditDocContent.svelte | 3 + .../components/CollaborativeTextEditor.svelte | 33 +- .../src/components/TextActionButton.svelte | 1 + .../src/components/TextEditor.svelte | 24 +- .../src/components/TextEditorToolbar.svelte | 155 ---- .../extension/embed/EmbedToolbarHead.svelte | 54 ++ .../src/components/extension/embed/embed.ts | 528 +++---------- .../extension/embed/providers/drive.ts | 3 +- .../extension/embed/providers/youtube.ts | 2 + .../src/components/extension/imageExt.ts | 266 ++++--- .../src/components/extension/inlinePopup.ts | 17 - .../src/components/extension/inlineToolbar.ts | 65 -- .../src/components/extension/reference.ts | 2 + .../extension/table/TableNodeView.svelte | 39 +- .../extension/table/TableToolbar.svelte | 90 --- .../src/components/extension/table/table.ts | 75 +- .../EditorToolbar.svelte} | 91 +-- .../components/extension/toolbar/toolbar.ts | 700 ++++++++++++++++++ plugins/text-editor-resources/src/index.ts | 5 +- .../src/kits/editor-kit.ts | 106 +-- plugins/text-editor/src/types.ts | 4 +- .../tests/documents/documents-content.spec.ts | 3 +- .../model/documents/document-content-page.ts | 10 +- 28 files changed, 1307 insertions(+), 1113 deletions(-) delete mode 100644 plugins/text-editor-resources/src/components/TextEditorToolbar.svelte create mode 100644 plugins/text-editor-resources/src/components/extension/embed/EmbedToolbarHead.svelte delete mode 100644 plugins/text-editor-resources/src/components/extension/inlinePopup.ts delete mode 100644 plugins/text-editor-resources/src/components/extension/inlineToolbar.ts delete mode 100644 plugins/text-editor-resources/src/components/extension/table/TableToolbar.svelte rename plugins/text-editor-resources/src/components/extension/{embed/EmbedToolbar.svelte => toolbar/EditorToolbar.svelte} (61%) create mode 100644 plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts diff --git a/models/controlled-documents/src/index.ts b/models/controlled-documents/src/index.ts index b6bf1e1190..93bf5e9b40 100644 --- a/models/controlled-documents/src/index.ts +++ b/models/controlled-documents/src/index.ts @@ -1122,6 +1122,7 @@ export function defineSearch (builder: Builder): void { export function defineTextActions (builder: Builder): void { // Comment category builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { + tags: ['text'], action: documents.function.Comment, icon: chunter.icon.Chunter, visibilityTester: documents.function.IsCommentVisible, diff --git a/models/text-editor/src/index.ts b/models/text-editor/src/index.ts index 93f85ac061..71c595414d 100644 --- a/models/text-editor/src/index.ts +++ b/models/text-editor/src/index.ts @@ -14,30 +14,28 @@ // import { DOMAIN_MODEL } from '@hcengineering/core' -import { type Builder, Model } from '@hcengineering/model' +import { Model, type Builder } from '@hcengineering/model' import core, { TDoc } from '@hcengineering/model-core' import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform' import { + type ActiveDescriptor, type ExtensionCreator, - type TextEditorExtensionFactory, type RefInputAction, type RefInputActionItem, - type TextEditorAction, + type TextActionActiveFunction, type TextActionFunction, type TextActionVisibleFunction, - type TextActionActiveFunction, - type ActiveDescriptor, - type TogglerDescriptor, - type TextEditorActionKind + type TextEditorAction, + type TextEditorExtensionFactory, + type TogglerDescriptor } from '@hcengineering/text-editor' +import view from '@hcengineering/view' +import textEditor from './plugin' // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { EditorKitOptions } from '@hcengineering/text-editor-resources/src/kits/editor-kit' -import textEditor from './plugin' -import view from '@hcengineering/view' - +export { textEditorId } from '@hcengineering/text-editor' export { textEditorOperation } from './migration' export { default } from './plugin' -export { textEditorId } from '@hcengineering/text-editor' export type { RefInputAction, RefInputActionItem } @Model(textEditor.class.RefInputActionItem, core.class.Doc, DOMAIN_MODEL) @@ -57,7 +55,7 @@ export class TTextEditorExtensionFactory extends TDoc implements TextEditorExten @Model(textEditor.class.TextEditorAction, core.class.Doc, DOMAIN_MODEL) export class TTextEditorAction extends TDoc implements TextEditorAction { - kind?: TextEditorActionKind + tags?: string[] action!: TogglerDescriptor | Resource visibilityTester?: Resource icon!: Asset @@ -127,7 +125,7 @@ function createImageAlignmentAction (builder: Builder, align: 'center' | 'left' } builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'image', + tags: ['image'], action: { command: 'setImageAlignment', params: { @@ -171,7 +169,7 @@ function createTextAlignmentAction (builder: Builder, align: 'center' | 'left' | } builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'text', + tags: ['text'], action: { command: 'setTextAlign', params: align @@ -367,7 +365,7 @@ export function createModel (builder: Builder): void { // Table cell category builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'table', + tags: ['table', 'tableCell'], action: textEditor.function.SetBackgroundColor, icon: textEditor.icon.Brush, visibilityTester: textEditor.function.IsTableToolbarContext, @@ -378,7 +376,7 @@ export function createModel (builder: Builder): void { // Table category builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'table', + tags: ['table'], action: textEditor.function.SelectTable, icon: textEditor.icon.SelectTable, visibilityTester: textEditor.function.IsTableToolbarContext, @@ -388,7 +386,7 @@ export function createModel (builder: Builder): void { }) builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'table', + tags: ['table'], action: textEditor.function.OpenTableOptions, icon: textEditor.icon.TableProps, visibilityTester: textEditor.function.IsTableToolbarContext, @@ -404,7 +402,7 @@ export function createModel (builder: Builder): void { // Image view category builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'image', + tags: ['image'], action: textEditor.function.OpenImage, icon: textEditor.icon.ScaleOut, label: textEditor.string.ViewImage, @@ -413,7 +411,7 @@ export function createModel (builder: Builder): void { }) builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'image', + tags: ['image'], action: textEditor.function.ExpandImage, icon: textEditor.icon.Expand, label: textEditor.string.ViewOriginal, @@ -422,7 +420,7 @@ export function createModel (builder: Builder): void { }) builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'image', + tags: ['image'], action: textEditor.function.DownloadImage, icon: textEditor.icon.Download, label: textEditor.string.Download, @@ -431,7 +429,7 @@ export function createModel (builder: Builder): void { }) builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'image', + tags: ['image'], action: textEditor.function.MoreImageActions, visibilityTester: textEditor.function.IsEditable, icon: textEditor.icon.MoreH, @@ -465,7 +463,7 @@ export function createModel (builder: Builder): void { }) builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'preview', + tags: ['embed'], action: textEditor.function.CopyPreviewLinkAction, icon: view.icon.Copy, visibilityTester: textEditor.function.ShouldShowCopyPreviewLinkAction, @@ -475,7 +473,7 @@ export function createModel (builder: Builder): void { }) builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'preview', + tags: ['embed'], action: textEditor.function.ConvertToLinkPreviewAction, icon: textEditor.icon.LinkPreview, visibilityTester: textEditor.function.ShouldShowConvertToLinkPreviewAction, @@ -486,7 +484,7 @@ export function createModel (builder: Builder): void { }) builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - kind: 'preview', + tags: ['embed'], action: textEditor.function.ConvertToEmbedPreviewAction, icon: textEditor.icon.EmbedPreview, visibilityTester: textEditor.function.ShouldShowConvertToEmbedPreviewAction, diff --git a/packages/text/src/nodes/image.ts b/packages/text/src/nodes/image.ts index 83ad9f4164..ae29f14042 100644 --- a/packages/text/src/nodes/image.ts +++ b/packages/text/src/nodes/image.ts @@ -16,6 +16,39 @@ import type { Blob, Ref } from '@hcengineering/core' import { Node, mergeAttributes } from '@tiptap/core' import { getDataAttribute } from './utils' +/** + * @public + */ +export type ImageAlignment = 'center' | 'left' | 'right' + +export interface ImageAlignmentOptions { + align?: ImageAlignment +} + +export interface ImageSizeOptions { + height?: number | string + width?: number | string +} + +declare module '@tiptap/core' { + export interface Commands { + image: { + /** + * Add an image + */ + setImage: (options: { src: string, alt?: string, title?: string }) => ReturnType + /** + * Set image alignment + */ + setImageAlignment: (options: ImageAlignmentOptions) => ReturnType + /** + * Set image size + */ + setImageSize: (options: ImageSizeOptions) => ReturnType + } + } +} + /** * @public */ @@ -110,60 +143,5 @@ export const ImageNode = Node.create({ } return ['div', divAttributes, ['img', imgAttributes]] - }, - addNodeView () { - return ({ node, HTMLAttributes }) => { - const container = document.createElement('div') - const imgElement = document.createElement('img') - container.append(imgElement) - const divAttributes = { - class: 'text-editor-image-container', - 'data-type': this.name, - 'data-align': node.attrs.align - } - - for (const [k, v] of Object.entries(divAttributes)) { - if (v !== null) { - container.setAttribute(k, v) - } - } - - const imgAttributes = mergeAttributes( - { - 'data-type': this.name - }, - this.options.HTMLAttributes, - HTMLAttributes - ) - for (const [k, v] of Object.entries(imgAttributes)) { - if (k !== 'src' && k !== 'srcset' && v !== null) { - imgElement.setAttribute(k, v) - } - } - const fileId = imgAttributes['file-id'] - if (fileId != null) { - const setBrokenImg = setTimeout(() => { - imgElement.src = this.options.loadingImgSrc ?? `platform://platform/files/workspace/?file=${fileId}` - }, 500) - if (fileId != null) { - void this.options.getBlobRef(fileId).then((val) => { - clearTimeout(setBrokenImg) - imgElement.src = val.src - imgElement.srcset = val.srcset - }) - } - } else { - if (imgAttributes.srcset != null) { - imgElement.srcset = imgAttributes.srcset - } - if (imgAttributes.src != null) { - imgElement.src = imgAttributes.src - } - } - - return { - dom: container - } - } } }) diff --git a/packages/ui/src/components/PopupInstance.svelte b/packages/ui/src/components/PopupInstance.svelte index c20b1a1e5d..8f80e08c8d 100644 --- a/packages/ui/src/components/PopupInstance.svelte +++ b/packages/ui/src/components/PopupInstance.svelte @@ -296,6 +296,8 @@ style:min-width={options?.props?.minWidth} style:min-height={options?.props?.minHeight} style:transform={options?.props?.transform} + data-block-editor-blur="true" + data-block-cursor-update="true" use:resizeObserver={(element) => { clientWidth = element.clientWidth clientHeight = element.clientHeight diff --git a/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte b/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte index 0e1a62af30..8d692546e4 100644 --- a/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte +++ b/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte @@ -128,11 +128,15 @@ let tabs: Tab[] + let content: HTMLElement + $: tabs = [ { label: documentRes.string.ContentTab, component: EditDocContent, - props: {} + props: { + boundary: content + } }, { label: documentRes.string.ReasonAndImpact, @@ -307,6 +311,7 @@ {#if $controlledDocument !== null && attribute !== undefined} !showToolbar - }, - image: { - toolbar: { - element: imageToolbarElement, - boundary, - appendTo: () => boundary ?? element, - isHidden: () => !showToolbar - } + popupContainer: editorPopupContainer }, mermaid: { ...mermaidOptions, @@ -488,10 +478,6 @@ drawingBoard: { getSavedBoard }, - embed: { - boundary: boundary ?? element, - popupContainer: editorPopupContainer - }, ...kitOptions }), ...optionalExtensions, @@ -585,23 +571,6 @@ {/if} - - - -
- - -