diff --git a/packages/text-editor/src/components/CollaboratorEditor.svelte b/packages/text-editor/src/components/CollaboratorEditor.svelte index 78bd7c03ce..3cd3337574 100644 --- a/packages/text-editor/src/components/CollaboratorEditor.svelte +++ b/packages/text-editor/src/components/CollaboratorEditor.svelte @@ -15,31 +15,31 @@ // --> +
+ {#if !!currentTextNodeAction} + { + currentTextNodeAction = undefined + }} + /> + {/if} +
{#if visible}
{#if isFormatting && !readonly} @@ -246,9 +330,14 @@ TextFormatCategory.Table ]} formatButtonSize={buttonSize} + {textNodeActions} on:focus={() => { needFocus = true }} + on:action={(event) => { + currentTextNodeAction = textNodeActions.find((action) => action.id === event.detail) + needFocus = true + }} />
@@ -422,8 +511,18 @@ box-shadow: var(--button-shadow); z-index: 1; } + .ref-container:focus-within .formatPanel { position: sticky; top: 1.25rem; } + + .actionPanel { + margin: -0.5rem -0.25rem 0.5rem; + padding: 0.375rem; + background-color: var(--theme-comp-header-color); + border-radius: 0.5rem; + box-shadow: var(--theme-popup-shadow); + z-index: 1; + } diff --git a/packages/text-editor/src/components/TextEditor.svelte b/packages/text-editor/src/components/TextEditor.svelte index eedc93121f..1924eb43fd 100644 --- a/packages/text-editor/src/components/TextEditor.svelte +++ b/packages/text-editor/src/components/TextEditor.svelte @@ -176,8 +176,6 @@ }, onSelectionUpdate: () => { showContextMenu = false - - dispatch('selection-update') } }) }) diff --git a/packages/text-editor/src/components/TextEditorStyleToolbar.svelte b/packages/text-editor/src/components/TextEditorStyleToolbar.svelte index a2297bf631..aa759d2387 100644 --- a/packages/text-editor/src/components/TextEditorStyleToolbar.svelte +++ b/packages/text-editor/src/components/TextEditorStyleToolbar.svelte @@ -20,7 +20,7 @@ import { Editor } from '@tiptap/core' import { Level } from '@tiptap/extension-heading' import textEditorPlugin from '../plugin' - import { TextFormatCategory } from '../types' + import { TextFormatCategory, TextNodeAction } from '../types' import { mInsertTable } from './extensions' import Bold from './icons/Bold.svelte' import Code from './icons/Code.svelte' @@ -48,6 +48,7 @@ export let formatButtonSize: IconSize = 'small' export let textEditor: Editor export let textFormatCategories: TextFormatCategory[] = [] + export let textNodeActions: TextNodeAction[] = [] const dispatch = createEventDispatcher() @@ -318,4 +319,19 @@
{/if} {/each} + {#if textFormatCategories.length > 0 && textNodeActions.length > 0} +
+ {/if} + {#each textNodeActions as action} + { + dispatch('action', action.id) + }} + /> + {/each} {/if} diff --git a/packages/text-editor/src/components/extension/nodeHighlight.ts b/packages/text-editor/src/components/extension/nodeHighlight.ts new file mode 100644 index 0000000000..5e930225cc --- /dev/null +++ b/packages/text-editor/src/components/extension/nodeHighlight.ts @@ -0,0 +1,88 @@ +import { Extension, getMarkRange, mergeAttributes } from '@tiptap/core' +import { Plugin, TextSelection } from 'prosemirror-state' +import { NodeUuidExtension, NodeUuidOptions } from './nodeUuid' + +export enum NodeHighlightType { + WARNING = 'warning', + SUCCESS = 'success', + ERROR = 'error' +} +interface NodeHighlightExtensionOptions extends NodeUuidOptions { + getNodeHighlightType: (uuid: string) => NodeHighlightType | undefined | null + isHighlightModeOn: () => boolean +} + +/** + * Extension allows to highlight nodes based on uuid + */ +export const NodeHighlightExtension = Extension.create({ + addProseMirrorPlugins () { + const options = this.options + const plugins = [ + new Plugin({ + props: { + handleClick (view, pos) { + if (!options.isHighlightModeOn()) { + return + } + const { schema, doc, tr } = view.state + + const range = getMarkRange(doc.resolve(pos), schema.marks[NodeUuidExtension.name]) + + if (range === null || range === undefined) { + return false + } + + const [$start, $end] = [doc.resolve(range.from), doc.resolve(range.to)] + + view.dispatch(tr.setSelection(new TextSelection($start, $end))) + + return true + } + } + }) + ] + + return plugins + }, + + addExtensions () { + const options = this.options + + return [ + NodeUuidExtension.extend({ + addOptions () { + return { + ...this.parent?.(), + ...options + } + }, + addAttributes () { + return { + [NodeUuidExtension.name]: { + renderHTML: (attrs) => { + // get uuid from parent mark (NodeUuidExtension) attributes + const uuid = attrs[NodeUuidExtension.name] + const classAttrs: { class?: string } = {} + + if (options.isHighlightModeOn()) { + const type = options.getNodeHighlightType(uuid) + + if (type === NodeHighlightType.ERROR) { + classAttrs.class = 'text-editor-highlighted-node-error' + } else if (type === NodeHighlightType.WARNING) { + classAttrs.class = 'text-editor-highlighted-node-warning' + } else if (type === NodeHighlightType.SUCCESS) { + classAttrs.class = 'text-editor-highlighted-node-success' + } + } + + return mergeAttributes(attrs, classAttrs) + } + } + } + } + }) + ] + } +}) diff --git a/packages/text-editor/src/components/extension/nodeUuid.ts b/packages/text-editor/src/components/extension/nodeUuid.ts new file mode 100644 index 0000000000..7fae0b6a61 --- /dev/null +++ b/packages/text-editor/src/components/extension/nodeUuid.ts @@ -0,0 +1,105 @@ +import { Mark, mergeAttributes } from '@tiptap/core' + +const NAME = 'node-uuid' + +export interface NodeUuidOptions { + HTMLAttributes: Record + onNodeSelected?: (uuid: string | null) => any +} + +declare module '@tiptap/core' { + interface Commands { + [NAME]: { + /** + * Add uuid mark + */ + setUuid: (uuid: string) => ReturnType + /** + * Unset uuid mark + */ + unsetUuid: () => ReturnType + } + } +} + +export interface NodeUuidStorage { + activeNodeUuid: string | null +} + +/** + * This mark allows to add node uuid to the selected text + * Creates span node with attribute node-uuid + */ +export const NodeUuidExtension = Mark.create({ + name: NAME, + addOptions () { + return { + HTMLAttributes: {} + } + }, + + addAttributes () { + return { + [NAME]: { + default: null, + parseHTML: (el) => (el as HTMLSpanElement).getAttribute(NAME) + } + } + }, + + parseHTML () { + return [ + { + tag: `span[${NAME}]`, + getAttrs: (el) => { + const value = (el as HTMLSpanElement).getAttribute(NAME)?.trim() + if (value === null || value === undefined || value.length === 0) { + return false + } + + return null + } + } + ] + }, + + renderHTML ({ HTMLAttributes }) { + return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, + + addCommands () { + return { + setUuid: + (uuid: string) => + ({ commands }) => + commands.setMark(this.name, { [NAME]: uuid }), + unsetUuid: + () => + ({ commands }) => + commands.unsetMark(this.name) + } + }, + + addStorage () { + return { + activeNodeUuid: null + } + }, + + onSelectionUpdate () { + const { $head } = this.editor.state.selection + + const marks = $head.marks() + this.storage.activeNodeUuid = null + if (marks.length > 0) { + const nodeUuidMark = this.editor.schema.marks[NAME] + const activeNodeUuidMark = marks.find((mark) => mark.type === nodeUuidMark) + + if (activeNodeUuidMark !== undefined && activeNodeUuidMark !== null) { + this.storage.activeNodeUuid = activeNodeUuidMark.attrs[NAME] + } + } + + this.options.onNodeSelected?.(this.storage.activeNodeUuid) + } +}) diff --git a/packages/text-editor/src/types.ts b/packages/text-editor/src/types.ts index 424e18536b..88a8b9a8c0 100644 --- a/packages/text-editor/src/types.ts +++ b/packages/text-editor/src/types.ts @@ -1,6 +1,6 @@ import { Asset, IntlString, Resource } from '@hcengineering/platform' import { Doc } from '@hcengineering/core' -import type { AnySvelteComponent } from '@hcengineering/ui' +import type { AnyComponent, AnySvelteComponent } from '@hcengineering/ui' /** * @public @@ -56,3 +56,10 @@ export interface RefAction { fill?: string disabled?: boolean } + +export interface TextNodeAction { + id: string + label?: IntlString + icon: Asset | AnySvelteComponent + panel: AnyComponent +} diff --git a/packages/theme/styles/_text-editor.scss b/packages/theme/styles/_text-editor.scss index b6e0b264e7..5f39f38369 100644 --- a/packages/theme/styles/_text-editor.scss +++ b/packages/theme/styles/_text-editor.scss @@ -3,6 +3,17 @@ object-fit: contain; } +.text-editor-highlighted-node-warning { + background-color: var(--theme-warning-color); +} + +.text-editor-highlighted-node-error { + background-color: var(--theme-error-color); +} + +.text-editor-highlighted-node-success { + background-color: var(--theme-won-color); +} .proseH1 { margin-block-start: 1.25rem; margin-block-end: 1.25rem; @@ -19,9 +30,3 @@ // line-height: 1.75rem; color: green; } - -// need to override editor's bubble max-width -// due to https://github.com/atomiks/tippyjs/issues/451 -// .tippy-box { -// max-width: 30rem !important; -// }