mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-20 15:20:18 +00:00
Collaborator Text Editor: add node highlight extension for inline comments feature (#3660)
* Collaborator Text Editor: add node highlight extension for inline comments feature Signed-off-by: Anna No <anna.no@xored.com> * fix dependencies Signed-off-by: Anna No <anna.no@xored.com> * use generateId instead of uuid Signed-off-by: Anna No <anna.no@xored.com> --------- Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
parent
a52b806e5c
commit
7c2aa9f282
@ -15,31 +15,31 @@
|
|||||||
//
|
//
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { IntlString, translate } from '@hcengineering/platform'
|
import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state'
|
||||||
|
import { DecorationSet } from 'prosemirror-view'
|
||||||
import { Editor, Extension, HTMLContent } from '@tiptap/core'
|
import { getContext, createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
|
import { WebsocketProvider } from 'y-websocket'
|
||||||
|
import * as Y from 'yjs'
|
||||||
|
import { Editor, Extension, HTMLContent, getMarkRange } from '@tiptap/core'
|
||||||
import Collaboration from '@tiptap/extension-collaboration'
|
import Collaboration from '@tiptap/extension-collaboration'
|
||||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||||
import Placeholder from '@tiptap/extension-placeholder'
|
import Placeholder from '@tiptap/extension-placeholder'
|
||||||
|
import BubbleMenu from '@tiptap/extension-bubble-menu'
|
||||||
|
import { generateId, getCurrentAccount, Markup } from '@hcengineering/core'
|
||||||
|
import { IntlString, translate } from '@hcengineering/platform'
|
||||||
|
import { Component, getPlatformColorForText, IconObjects, IconSize, themeStore } from '@hcengineering/ui'
|
||||||
|
|
||||||
import { Plugin, PluginKey, Transaction } from 'prosemirror-state'
|
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
|
||||||
|
|
||||||
import { getCurrentAccount, Markup } from '@hcengineering/core'
|
|
||||||
import { getPlatformColorForText, IconObjects, IconSize, themeStore } from '@hcengineering/ui'
|
|
||||||
import { WebsocketProvider } from 'y-websocket'
|
|
||||||
import * as Y from 'yjs'
|
|
||||||
import StyleButton from './StyleButton.svelte'
|
|
||||||
|
|
||||||
import { DecorationSet } from 'prosemirror-view'
|
|
||||||
import textEditorPlugin from '../plugin'
|
import textEditorPlugin from '../plugin'
|
||||||
import { CollaborationIds, TextFormatCategory } from '../types'
|
import { CollaborationIds, TextFormatCategory, TextNodeAction } from '../types'
|
||||||
|
|
||||||
import { getContext } from 'svelte'
|
|
||||||
import { calculateDecorations } from './diff/decorations'
|
import { calculateDecorations } from './diff/decorations'
|
||||||
import { defaultExtensions } from './extensions'
|
import { defaultExtensions } from './extensions'
|
||||||
|
import { NodeHighlightExtension, NodeHighlightType } from './extension/nodeHighlight'
|
||||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||||
|
|
||||||
|
import StyleButton from './StyleButton.svelte'
|
||||||
|
import { NodeUuidExtension } from './extension/nodeUuid'
|
||||||
|
|
||||||
export let documentId: string
|
export let documentId: string
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let visible = true
|
export let visible = true
|
||||||
@ -59,6 +59,9 @@
|
|||||||
|
|
||||||
export let autoOverflow = false
|
export let autoOverflow = false
|
||||||
export let initialContent: string | undefined = undefined
|
export let initialContent: string | undefined = undefined
|
||||||
|
export let textNodeActions: TextNodeAction[] = []
|
||||||
|
export let isNodeHighlightModeOn: boolean = false
|
||||||
|
export let onNodeHighlightType: (uuid: string) => NodeHighlightType = () => NodeHighlightType.WARNING
|
||||||
|
|
||||||
const ydoc = (getContext(CollaborationIds.Doc) as Y.Doc | undefined) ?? new Y.Doc()
|
const ydoc = (getContext(CollaborationIds.Doc) as Y.Doc | undefined) ?? new Y.Doc()
|
||||||
const contextProvider = getContext(CollaborationIds.Provider) as WebsocketProvider | undefined
|
const contextProvider = getContext(CollaborationIds.Provider) as WebsocketProvider | undefined
|
||||||
@ -80,6 +83,9 @@
|
|||||||
|
|
||||||
const currentUser = getCurrentAccount()
|
const currentUser = getCurrentAccount()
|
||||||
|
|
||||||
|
let currentTextNodeAction: TextNodeAction | undefined | null
|
||||||
|
let selectedNodeUuid: string | null | undefined
|
||||||
|
let textNodeActionMenuElement: HTMLElement
|
||||||
let element: HTMLElement
|
let element: HTMLElement
|
||||||
let editor: Editor
|
let editor: Editor
|
||||||
|
|
||||||
@ -91,21 +97,46 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export function clear (): void {
|
|
||||||
editor.commands.clearContent(false)
|
|
||||||
}
|
|
||||||
export function insertText (text: string): void {
|
|
||||||
editor.commands.insertContent(text as HTMLContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHTML (): string | undefined {
|
export function getHTML (): string | undefined {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
return editor.getHTML()
|
return editor.getHTML()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkIsSelectionEmpty () {
|
export function selectNode (uuid: string) {
|
||||||
return editor.view.state.selection.empty
|
if (!editor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { doc, schema, tr } = editor.view.state
|
||||||
|
let foundNode = false
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (foundNode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeUuidMark = node.marks.find(
|
||||||
|
(mark) => mark.type.name === NodeUuidExtension.name && mark.attrs[NodeUuidExtension.name] === uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!nodeUuidMark) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foundNode = true
|
||||||
|
|
||||||
|
// the first pos does not contain the mark, so we need to add 1 (pos + 1) to get the correct range
|
||||||
|
const range = getMarkRange(doc.resolve(pos + 1), schema.marks[NodeUuidExtension.name])
|
||||||
|
|
||||||
|
if (!range) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const [$start, $end] = [doc.resolve(range.from), doc.resolve(range.to)]
|
||||||
|
|
||||||
|
editor.view.dispatch(tr.setSelection(new TextSelection($start, $end)))
|
||||||
|
needFocus = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let needFocus = false
|
let needFocus = false
|
||||||
@ -145,6 +176,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNodeUuid = () => {
|
||||||
|
if (editor.view.state.selection.empty) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!selectedNodeUuid) {
|
||||||
|
selectedNodeUuid = generateId()
|
||||||
|
editor.chain().setUuid(selectedNodeUuid!).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedNodeUuid
|
||||||
|
}
|
||||||
|
|
||||||
const DecorationExtension = Extension.create({
|
const DecorationExtension = Extension.create({
|
||||||
addProseMirrorPlugins () {
|
addProseMirrorPlugins () {
|
||||||
return [
|
return [
|
||||||
@ -170,7 +213,6 @@
|
|||||||
ph.then(() => {
|
ph.then(() => {
|
||||||
editor = new Editor({
|
editor = new Editor({
|
||||||
element,
|
element,
|
||||||
// content: 'Hello world<br/> This is simple text<br/>Some more text<br/>Yahoo <br/>Cool <br/><br/> Done',
|
|
||||||
editable: true,
|
editable: true,
|
||||||
extensions: [
|
extensions: [
|
||||||
...defaultExtensions,
|
...defaultExtensions,
|
||||||
@ -187,7 +229,34 @@
|
|||||||
color: getPlatformColorForText(currentUser.email, $themeStore.dark)
|
color: getPlatformColorForText(currentUser.email, $themeStore.dark)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
DecorationExtension
|
DecorationExtension,
|
||||||
|
NodeHighlightExtension.configure({
|
||||||
|
isHighlightModeOn: () => isNodeHighlightModeOn,
|
||||||
|
getNodeHighlightType: onNodeHighlightType,
|
||||||
|
onNodeSelected: (uuid: string | null) => {
|
||||||
|
if (selectedNodeUuid !== uuid) {
|
||||||
|
selectedNodeUuid = uuid
|
||||||
|
dispatch('node-selected', selectedNodeUuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'text-node-action-menu',
|
||||||
|
element: textNodeActionMenuElement,
|
||||||
|
tippyOptions: {
|
||||||
|
maxWidth: '38rem',
|
||||||
|
onClickOutside: () => {
|
||||||
|
currentTextNodeAction = undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldShow: (editor) => {
|
||||||
|
if (!editor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!currentTextNodeAction
|
||||||
|
}
|
||||||
|
})
|
||||||
// ...extensions
|
// ...extensions
|
||||||
],
|
],
|
||||||
onTransaction: () => {
|
onTransaction: () => {
|
||||||
@ -203,14 +272,11 @@
|
|||||||
},
|
},
|
||||||
onUpdate: (op: { editor: Editor; transaction: Transaction }) => {
|
onUpdate: (op: { editor: Editor; transaction: Transaction }) => {
|
||||||
dispatch('content', editor.getHTML())
|
dispatch('content', editor.getHTML())
|
||||||
},
|
|
||||||
onSelectionUpdate: () => {
|
|
||||||
dispatch('selection-update')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (initialContent) {
|
if (initialContent) {
|
||||||
insertText(initialContent)
|
editor.commands.insertContent(initialContent as HTMLContent)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -229,6 +295,24 @@
|
|||||||
let showDiff = true
|
let showDiff = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="actionPanel" bind:this={textNodeActionMenuElement}>
|
||||||
|
{#if !!currentTextNodeAction}
|
||||||
|
<Component
|
||||||
|
is={currentTextNodeAction.panel}
|
||||||
|
props={{
|
||||||
|
documentId,
|
||||||
|
field,
|
||||||
|
editor,
|
||||||
|
action: currentTextNodeAction,
|
||||||
|
disabled: editor.view.state.selection.empty,
|
||||||
|
onNodeUuid: getNodeUuid
|
||||||
|
}}
|
||||||
|
on:close={() => {
|
||||||
|
currentTextNodeAction = undefined
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div class="ref-container" class:autoOverflow>
|
<div class="ref-container" class:autoOverflow>
|
||||||
{#if isFormatting && !readonly}
|
{#if isFormatting && !readonly}
|
||||||
@ -246,9 +330,14 @@
|
|||||||
TextFormatCategory.Table
|
TextFormatCategory.Table
|
||||||
]}
|
]}
|
||||||
formatButtonSize={buttonSize}
|
formatButtonSize={buttonSize}
|
||||||
|
{textNodeActions}
|
||||||
on:focus={() => {
|
on:focus={() => {
|
||||||
needFocus = true
|
needFocus = true
|
||||||
}}
|
}}
|
||||||
|
on:action={(event) => {
|
||||||
|
currentTextNodeAction = textNodeActions.find((action) => action.id === event.detail)
|
||||||
|
needFocus = true
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@ -422,8 +511,18 @@
|
|||||||
box-shadow: var(--button-shadow);
|
box-shadow: var(--button-shadow);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ref-container:focus-within .formatPanel {
|
.ref-container:focus-within .formatPanel {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 1.25rem;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -176,8 +176,6 @@
|
|||||||
},
|
},
|
||||||
onSelectionUpdate: () => {
|
onSelectionUpdate: () => {
|
||||||
showContextMenu = false
|
showContextMenu = false
|
||||||
|
|
||||||
dispatch('selection-update')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
import { Editor } from '@tiptap/core'
|
import { Editor } from '@tiptap/core'
|
||||||
import { Level } from '@tiptap/extension-heading'
|
import { Level } from '@tiptap/extension-heading'
|
||||||
import textEditorPlugin from '../plugin'
|
import textEditorPlugin from '../plugin'
|
||||||
import { TextFormatCategory } from '../types'
|
import { TextFormatCategory, TextNodeAction } from '../types'
|
||||||
import { mInsertTable } from './extensions'
|
import { mInsertTable } from './extensions'
|
||||||
import Bold from './icons/Bold.svelte'
|
import Bold from './icons/Bold.svelte'
|
||||||
import Code from './icons/Code.svelte'
|
import Code from './icons/Code.svelte'
|
||||||
@ -48,6 +48,7 @@
|
|||||||
export let formatButtonSize: IconSize = 'small'
|
export let formatButtonSize: IconSize = 'small'
|
||||||
export let textEditor: Editor
|
export let textEditor: Editor
|
||||||
export let textFormatCategories: TextFormatCategory[] = []
|
export let textFormatCategories: TextFormatCategory[] = []
|
||||||
|
export let textNodeActions: TextNodeAction[] = []
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
@ -318,4 +319,19 @@
|
|||||||
<div class="buttons-divider" />
|
<div class="buttons-divider" />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if textFormatCategories.length > 0 && textNodeActions.length > 0}
|
||||||
|
<div class="buttons-divider" />
|
||||||
|
{/if}
|
||||||
|
{#each textNodeActions as action}
|
||||||
|
<StyleButton
|
||||||
|
icon={action.icon}
|
||||||
|
size={formatButtonSize}
|
||||||
|
selected={false}
|
||||||
|
disabled={textEditor.view.state.selection.empty}
|
||||||
|
showTooltip={{ label: action.label }}
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('action', action.id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -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<NodeHighlightExtensionOptions>({
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
105
packages/text-editor/src/components/extension/nodeUuid.ts
Normal file
105
packages/text-editor/src/components/extension/nodeUuid.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { Mark, mergeAttributes } from '@tiptap/core'
|
||||||
|
|
||||||
|
const NAME = 'node-uuid'
|
||||||
|
|
||||||
|
export interface NodeUuidOptions {
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
onNodeSelected?: (uuid: string | null) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
[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<NodeUuidOptions, NodeUuidStorage>({
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
@ -1,6 +1,6 @@
|
|||||||
import { Asset, IntlString, Resource } from '@hcengineering/platform'
|
import { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||||
import { Doc } from '@hcengineering/core'
|
import { Doc } from '@hcengineering/core'
|
||||||
import type { AnySvelteComponent } from '@hcengineering/ui'
|
import type { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -56,3 +56,10 @@ export interface RefAction {
|
|||||||
fill?: string
|
fill?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TextNodeAction {
|
||||||
|
id: string
|
||||||
|
label?: IntlString
|
||||||
|
icon: Asset | AnySvelteComponent
|
||||||
|
panel: AnyComponent
|
||||||
|
}
|
||||||
|
@ -3,6 +3,17 @@
|
|||||||
object-fit: contain;
|
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 {
|
.proseH1 {
|
||||||
margin-block-start: 1.25rem;
|
margin-block-start: 1.25rem;
|
||||||
margin-block-end: 1.25rem;
|
margin-block-end: 1.25rem;
|
||||||
@ -19,9 +30,3 @@
|
|||||||
// line-height: 1.75rem;
|
// line-height: 1.75rem;
|
||||||
color: green;
|
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;
|
|
||||||
// }
|
|
||||||
|
Loading…
Reference in New Issue
Block a user