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;
-// }