mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-23 08:48:01 +00:00
QMS: Update inline comments extensions (#3814)
* update documents inline comments extensions Signed-off-by: Anna No <anna.no@xored.com> * update qms documents inline comments Signed-off-by: Anna No <anna.no@xored.com> * qms: update document inline comments extensions Signed-off-by: Anna No <anna.no@xored.com> * qms: move highlight to prose mirror decorations Signed-off-by: Anna No <anna.no@xored.com> * fix formatting issues Signed-off-by: Anna No <anna.no@xored.com> * fix formatting issues Signed-off-by: Anna No <anna.no@xored.com> * fix formatting issues Signed-off-by: Anna No <anna.no@xored.com> * fix formatting issues Signed-off-by: Anna No <anna.no@xored.com> --------- Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
parent
8805a588bc
commit
4e08b75040
@ -35,7 +35,7 @@
|
|||||||
import { calculateDecorations } from './diff/decorations'
|
import { calculateDecorations } from './diff/decorations'
|
||||||
import { defaultEditorAttributes } from './editor/editorProps'
|
import { defaultEditorAttributes } from './editor/editorProps'
|
||||||
import { completionConfig, defaultExtensions } from './extensions'
|
import { completionConfig, defaultExtensions } from './extensions'
|
||||||
import { InlineStyleToolbar } from './extension/inlineStyleToolbar'
|
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||||
import { NodeUuidExtension } from './extension/nodeUuid'
|
import { NodeUuidExtension } from './extension/nodeUuid'
|
||||||
import StyleButton from './StyleButton.svelte'
|
import StyleButton from './StyleButton.svelte'
|
||||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||||
@ -47,7 +47,6 @@
|
|||||||
export let token: string
|
export let token: string
|
||||||
export let collaboratorURL: string
|
export let collaboratorURL: string
|
||||||
|
|
||||||
export let isFormatting = true
|
|
||||||
export let buttonSize: IconSize = 'small'
|
export let buttonSize: IconSize = 'small'
|
||||||
export let focusable: boolean = false
|
export let focusable: boolean = false
|
||||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||||
@ -90,7 +89,6 @@
|
|||||||
|
|
||||||
let editor: Editor
|
let editor: Editor
|
||||||
let inlineToolbar: HTMLElement
|
let inlineToolbar: HTMLElement
|
||||||
let showInlineToolbar = false
|
|
||||||
|
|
||||||
let placeHolderStr: string = ''
|
let placeHolderStr: string = ''
|
||||||
|
|
||||||
@ -136,7 +134,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [$start, $end] = [doc.resolve(range.from), doc.resolve(range.to)]
|
const [$start, $end] = [doc.resolve(range.from), doc.resolve(range.to)]
|
||||||
|
|
||||||
editor.view.dispatch(tr.setSelection(new TextSelection($start, $end)))
|
editor.view.dispatch(tr.setSelection(new TextSelection($start, $end)))
|
||||||
needFocus = true
|
needFocus = true
|
||||||
})
|
})
|
||||||
@ -146,6 +143,22 @@
|
|||||||
provider.copyContent(documentId, snapshotId)
|
provider.copyContent(documentId, snapshotId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unregisterPlugin (nameOrPluginKey: string | PluginKey) {
|
||||||
|
if (!editor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.unregisterPlugin(nameOrPluginKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerPlugin (plugin: Plugin) {
|
||||||
|
if (!editor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.registerPlugin(plugin)
|
||||||
|
}
|
||||||
|
|
||||||
let needFocus = false
|
let needFocus = false
|
||||||
|
|
||||||
let focused = false
|
let focused = false
|
||||||
@ -201,6 +214,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
$: updateEditor(editor, field, comparedVersion)
|
$: updateEditor(editor, field, comparedVersion)
|
||||||
|
$: if (editor) dispatch('editor', editor)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
ph.then(() => {
|
ph.then(() => {
|
||||||
@ -211,10 +225,10 @@
|
|||||||
extensions: [
|
extensions: [
|
||||||
...defaultExtensions,
|
...defaultExtensions,
|
||||||
Placeholder.configure({ placeholder: placeHolderStr }),
|
Placeholder.configure({ placeholder: placeHolderStr }),
|
||||||
InlineStyleToolbar.configure({
|
InlineStyleToolbarExtension.configure({
|
||||||
element: inlineToolbar,
|
element: inlineToolbar,
|
||||||
getEditorElement: () => element,
|
isSupported: () => !readonly,
|
||||||
isShown: () => !readonly && showInlineToolbar
|
isSelectionOnly: () => false
|
||||||
}),
|
}),
|
||||||
Collaboration.configure({
|
Collaboration.configure({
|
||||||
document: ydoc,
|
document: ydoc,
|
||||||
@ -247,22 +261,14 @@
|
|||||||
onFocus: () => {
|
onFocus: () => {
|
||||||
focused = true
|
focused = true
|
||||||
},
|
},
|
||||||
onUpdate: ({ editor, transaction }) => {
|
onUpdate: ({ transaction }) => {
|
||||||
showInlineToolbar = false
|
|
||||||
|
|
||||||
// ignore non-document changes
|
// ignore non-document changes
|
||||||
if (!transaction.docChanged) return
|
if (!transaction.docChanged) return
|
||||||
|
|
||||||
// TODO this is heavy and should be replaced with more lightweight event
|
|
||||||
dispatch('content', editor.getHTML())
|
|
||||||
|
|
||||||
// ignore non-local changes
|
// ignore non-local changes
|
||||||
if (isChangeOrigin(transaction)) return
|
if (isChangeOrigin(transaction)) return
|
||||||
|
|
||||||
dispatch('update')
|
dispatch('update')
|
||||||
},
|
|
||||||
onSelectionUpdate: () => {
|
|
||||||
showInlineToolbar = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -283,16 +289,11 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function onEditorClick () {
|
|
||||||
if (!editor.isEmpty) {
|
|
||||||
showInlineToolbar = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let showDiff = true
|
let showDiff = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot {editor} />
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
{#if comparedVersion !== undefined || $$slots.tools}
|
{#if comparedVersion !== undefined || $$slots.tools}
|
||||||
<div class="ref-container" class:autoOverflow>
|
<div class="ref-container" class:autoOverflow>
|
||||||
@ -336,7 +337,7 @@
|
|||||||
needFocus = true
|
needFocus = true
|
||||||
}}
|
}}
|
||||||
on:action={(event) => {
|
on:action={(event) => {
|
||||||
dispatch('action', { action: event.detail, editor })
|
dispatch('action', event.detail)
|
||||||
needFocus = true
|
needFocus = true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -344,7 +345,7 @@
|
|||||||
|
|
||||||
<div class="ref-container" class:autoOverflow>
|
<div class="ref-container" class:autoOverflow>
|
||||||
<div class="textInput" class:focusable>
|
<div class="textInput" class:focusable>
|
||||||
<div class="select-text" style="width: 100%;" on:mousedown={onEditorClick} bind:this={element} />
|
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
import { themeStore } from '@hcengineering/ui'
|
import { themeStore } from '@hcengineering/ui'
|
||||||
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'
|
||||||
import { TextFormatCategory } from '../types'
|
import { TextFormatCategory } from '../types'
|
||||||
import { InlineStyleToolbar } from './extension/inlineStyleToolbar'
|
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||||
import { defaultEditorAttributes } from './editor/editorProps'
|
import { defaultEditorAttributes } from './editor/editorProps'
|
||||||
|
|
||||||
export let content: string = ''
|
export let content: string = ''
|
||||||
@ -77,7 +77,6 @@
|
|||||||
let needFocus = false
|
let needFocus = false
|
||||||
let focused = false
|
let focused = false
|
||||||
let posFocus: FocusPosition | undefined = undefined
|
let posFocus: FocusPosition | undefined = undefined
|
||||||
let showContextMenu = false
|
|
||||||
let textEditorToolbar: HTMLElement
|
let textEditorToolbar: HTMLElement
|
||||||
|
|
||||||
export function focus (position?: FocusPosition): void {
|
export function focus (position?: FocusPosition): void {
|
||||||
@ -137,10 +136,10 @@
|
|||||||
...(supportSubmit ? [Handle] : []), // order important
|
...(supportSubmit ? [Handle] : []), // order important
|
||||||
Placeholder.configure({ placeholder: placeHolderStr }),
|
Placeholder.configure({ placeholder: placeHolderStr }),
|
||||||
...extensions,
|
...extensions,
|
||||||
InlineStyleToolbar.configure({
|
InlineStyleToolbarExtension.configure({
|
||||||
element: textEditorToolbar,
|
element: textEditorToolbar,
|
||||||
getEditorElement: () => element,
|
isSupported: () => true,
|
||||||
isShown: () => showContextMenu
|
isSelectionOnly: () => false
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
parseOptions: {
|
parseOptions: {
|
||||||
@ -160,12 +159,8 @@
|
|||||||
},
|
},
|
||||||
onUpdate: () => {
|
onUpdate: () => {
|
||||||
content = editor.getHTML()
|
content = editor.getHTML()
|
||||||
showContextMenu = false
|
|
||||||
dispatch('value', content)
|
dispatch('value', content)
|
||||||
dispatch('update', content)
|
dispatch('update', content)
|
||||||
},
|
|
||||||
onSelectionUpdate: () => {
|
|
||||||
showContextMenu = false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -177,12 +172,6 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function onEditorClick () {
|
|
||||||
if (!editor.isEmpty) {
|
|
||||||
showContextMenu = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -209,7 +198,7 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="select-text" style="width: 100%;" on:mousedown={onEditorClick} bind:this={element} />
|
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.formatPanel {
|
.formatPanel {
|
||||||
|
@ -330,7 +330,7 @@
|
|||||||
disabled={textEditor.view.state.selection.empty}
|
disabled={textEditor.view.state.selection.empty}
|
||||||
showTooltip={{ label: action.label }}
|
showTooltip={{ label: action.label }}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dispatch('action', action.id)
|
dispatch('action', { action: action.id, editor: textEditor })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
16
packages/text-editor/src/components/extension/inlinePopup.ts
Normal file
16
packages/text-editor/src/components/extension/inlinePopup.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Extension } from '@tiptap/core'
|
||||||
|
import BubbleMenu, { BubbleMenuOptions } from '@tiptap/extension-bubble-menu'
|
||||||
|
|
||||||
|
export const InlinePopupExtension: Extension<BubbleMenuOptions> = BubbleMenu.extend({
|
||||||
|
addOptions () {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
pluginKey: 'inline-popup',
|
||||||
|
element: null,
|
||||||
|
tippyOptions: {
|
||||||
|
maxWidth: '38rem',
|
||||||
|
appendTo: () => document.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -1,48 +1,86 @@
|
|||||||
import { Extension, isTextSelection } from '@tiptap/core'
|
import { Editor, Extension, isTextSelection } from '@tiptap/core'
|
||||||
import BubbleMenu, { BubbleMenuOptions } from '@tiptap/extension-bubble-menu'
|
import { BubbleMenuOptions } from '@tiptap/extension-bubble-menu'
|
||||||
|
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||||
|
import { InlinePopupExtension } from './inlinePopup'
|
||||||
|
|
||||||
type InlineStyleToolbarOptions = BubbleMenuOptions & {
|
export type InlineStyleToolbarOptions = BubbleMenuOptions & {
|
||||||
getEditorElement: () => HTMLElement | null | undefined
|
isSupported: () => boolean
|
||||||
isShown?: () => boolean
|
isSelectionOnly?: () => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InlineStyleToolbar = Extension.create<InlineStyleToolbarOptions>({
|
export interface InlineStyleToolbarStorage {
|
||||||
defaultOptions: {
|
isShown: boolean
|
||||||
pluginKey: 'inline-style-toolbar',
|
}
|
||||||
element: null,
|
|
||||||
tippyOptions: {
|
const handleFocus = (editor: Editor, options: InlineStyleToolbarOptions, storage: InlineStyleToolbarStorage): void => {
|
||||||
maxWidth: '38rem',
|
if (!options.isSupported()) {
|
||||||
appendTo: () => document.body
|
return
|
||||||
},
|
}
|
||||||
getEditorElement: () => null
|
|
||||||
|
if (editor.isEmpty) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.isSelectionOnly?.() === true && editor.view.state.selection.empty) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.isShown = true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InlineStyleToolbarExtension = Extension.create<InlineStyleToolbarOptions, InlineStyleToolbarStorage>({
|
||||||
|
pluginKey: new PluginKey('inline-style-toolbar'),
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
const options = this.options
|
||||||
|
const storage = this.storage
|
||||||
|
const editor = this.editor
|
||||||
|
|
||||||
|
const plugins = [
|
||||||
|
...(this.parent?.() ?? []),
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('inline-style-toolbar-click-plugin'),
|
||||||
|
props: {
|
||||||
|
handleClick () {
|
||||||
|
handleFocus(editor, options, storage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
},
|
||||||
|
addStorage () {
|
||||||
|
return {
|
||||||
|
isShown: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
addExtensions () {
|
addExtensions () {
|
||||||
const options: InlineStyleToolbarOptions = this.options
|
const options: InlineStyleToolbarOptions = this.options
|
||||||
|
|
||||||
return [
|
return [
|
||||||
BubbleMenu.configure({
|
InlinePopupExtension.configure({
|
||||||
...options,
|
...options,
|
||||||
// to override shouldShow behaviour a little
|
|
||||||
// I need to copypaste original function and make a little change
|
|
||||||
// with showContextMenu falg
|
|
||||||
shouldShow: ({ editor, view, state, oldState, from, to }) => {
|
shouldShow: ({ editor, view, state, oldState, from, to }) => {
|
||||||
const editorElement = options.getEditorElement()
|
if (!this.options.isSupported()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editor.isDestroyed || !editor.isEditable) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.storage.isShown) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// For some reason shouldShow might be called after dismount and
|
// For some reason shouldShow might be called after dismount and
|
||||||
// after destroing the editor. We should handle this just no to have
|
// after destroing the editor. We should handle this just no to have
|
||||||
// any errors in runtime
|
// any errors in runtime
|
||||||
|
const editorElement = editor.view.dom
|
||||||
if (editorElement === null || editorElement === undefined) {
|
if (editorElement === null || editorElement === undefined) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!editor.isEditable) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isShown = options.isShown?.() ?? false
|
|
||||||
if (isShown) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// When clicking on a element inside the bubble menu the editor "blur" event
|
// When clicking on a element inside the bubble menu the editor "blur" event
|
||||||
// is called and the bubble menu item is focussed. In this case we should
|
// is called and the bubble menu item is focussed. In this case we should
|
||||||
// consider the menu as part of the editor and keep showing the menu
|
// consider the menu as part of the editor and keep showing the menu
|
||||||
@ -67,5 +105,14 @@ export const InlineStyleToolbar = Extension.create<InlineStyleToolbarOptions>({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
onFocus () {
|
||||||
|
handleFocus(this.editor, this.options, this.storage)
|
||||||
|
},
|
||||||
|
onSelectionUpdate () {
|
||||||
|
this.storage.isShown = false
|
||||||
|
},
|
||||||
|
onUpdate () {
|
||||||
|
this.storage.isShown = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { Extension, Range, getMarkRange, mergeAttributes } from '@tiptap/core'
|
import { Extension, Range, getMarkRange, mergeAttributes } from '@tiptap/core'
|
||||||
|
import { NodeUuidExtension, NodeUuidOptions, NodeUuidStorage, findNodeUuidMark } from './nodeUuid'
|
||||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
||||||
import { NodeUuidExtension, NodeUuidOptions } from './nodeUuid'
|
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||||
|
|
||||||
export enum NodeHighlightType {
|
export enum NodeHighlightType {
|
||||||
INFO = 'info',
|
WARNING = 'warning',
|
||||||
ADD = 'add',
|
ADD = 'add',
|
||||||
DELETE = 'delete'
|
DELETE = 'delete'
|
||||||
}
|
}
|
||||||
export interface NodeHighlightExtensionOptions extends NodeUuidOptions {
|
export interface NodeHighlightExtensionOptions extends NodeUuidOptions {
|
||||||
getNodeHighlightType: (uuid: string) => NodeHighlightType | undefined | null
|
getNodeHighlightType: (uuid: string) => NodeHighlightType | undefined | null
|
||||||
isHighlightModeOn: () => boolean
|
isHighlightModeOn: () => boolean
|
||||||
|
isAutoSelect?: () => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||||
@ -17,20 +19,47 @@ function isRange (range: Range | undefined | null | void): range is Range {
|
|||||||
return range !== null && range !== undefined
|
return range !== null && range !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generateAttributes = (uuid: string, options: NodeHighlightExtensionOptions): Record<string, any> | undefined => {
|
||||||
|
if (!options.isHighlightModeOn()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = options.getNodeHighlightType(uuid)
|
||||||
|
if (type === null || type === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const classAttrs: { class?: string } = {}
|
||||||
|
|
||||||
|
if (type === NodeHighlightType.WARNING) {
|
||||||
|
classAttrs.class = 'text-editor-highlighted-node-warning'
|
||||||
|
} else if (type === NodeHighlightType.ADD) {
|
||||||
|
classAttrs.class = 'text-editor-highlighted-node-add'
|
||||||
|
} else if (type === NodeHighlightType.DELETE) {
|
||||||
|
classAttrs.class = 'text-editor-highlighted-node-delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
return classAttrs
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension allows to highlight nodes based on uuid
|
* Extension allows to highlight nodes based on uuid
|
||||||
*/
|
*/
|
||||||
export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions> =
|
export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions, NodeUuidStorage> =
|
||||||
Extension.create<NodeHighlightExtensionOptions>({
|
Extension.create<NodeHighlightExtensionOptions>({
|
||||||
|
addStorage (): NodeUuidStorage {
|
||||||
|
return { activeNodeUuid: null }
|
||||||
|
},
|
||||||
addProseMirrorPlugins () {
|
addProseMirrorPlugins () {
|
||||||
const options = this.options
|
const options = this.options
|
||||||
|
const storage: NodeUuidStorage = this.storage
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
...(this.parent?.() ?? []),
|
...(this.parent?.() ?? []),
|
||||||
new Plugin({
|
new Plugin({
|
||||||
key: new PluginKey('handle-node-highlight-click-plugin'),
|
key: new PluginKey('handle-node-highlight-click-plugin'),
|
||||||
props: {
|
props: {
|
||||||
handleClick (view, pos) {
|
handleClick (view, pos) {
|
||||||
if (!options.isHighlightModeOn()) {
|
if (!options.isHighlightModeOn() || options.isAutoSelect?.() !== true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { schema, doc, tr } = view.state
|
const { schema, doc, tr } = view.state
|
||||||
@ -49,45 +78,65 @@ export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions> =
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('node-highlight-click-decorations-plugin'),
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
if (!options.isHighlightModeOn()) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
const { doc, schema } = state
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
const nodeUuidMark = findNodeUuidMark(node)
|
||||||
|
|
||||||
|
if (nodeUuidMark !== null && nodeUuidMark !== undefined) {
|
||||||
|
const nodeUuid = nodeUuidMark.attrs[NodeUuidExtension.name]
|
||||||
|
const attributes = generateAttributes(nodeUuid, options)
|
||||||
|
if (attributes === null || attributes === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (!isRange(range)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decorations.push(
|
||||||
|
Decoration.inline(
|
||||||
|
range.from,
|
||||||
|
range.to,
|
||||||
|
mergeAttributes(
|
||||||
|
attributes,
|
||||||
|
nodeUuid === storage.activeNodeUuid ? { class: 'text-editor-highlighted-node-selected' } : {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return DecorationSet.empty.add(doc, decorations)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
return plugins
|
return plugins
|
||||||
},
|
},
|
||||||
|
|
||||||
addExtensions () {
|
addExtensions () {
|
||||||
const options = this.options
|
const options: NodeHighlightExtensionOptions = this.options
|
||||||
|
const storage: NodeUuidStorage = this.storage
|
||||||
return [
|
return [
|
||||||
NodeUuidExtension.extend({
|
NodeUuidExtension.extend({
|
||||||
addOptions () {
|
addOptions (): NodeUuidOptions {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
...options
|
...options,
|
||||||
}
|
onNodeSelected: (uuid: string) => {
|
||||||
},
|
storage.activeNodeUuid = uuid
|
||||||
addAttributes () {
|
options.onNodeSelected?.(uuid)
|
||||||
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.INFO) {
|
|
||||||
classAttrs.class = 'text-editor-highlighted-node-info'
|
|
||||||
} else if (type === NodeHighlightType.ADD) {
|
|
||||||
classAttrs.class = 'text-editor-highlighted-node-add'
|
|
||||||
} else if (type === NodeHighlightType.DELETE) {
|
|
||||||
classAttrs.class = 'text-editor-highlighted-node-delete'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeAttributes(attrs, classAttrs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Mark, getMarkAttributes, mergeAttributes } from '@tiptap/core'
|
import { Command, CommandProps, Mark, getMarkAttributes, getMarkType, mergeAttributes } from '@tiptap/core'
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
import { Node, Mark as ProseMirrorMark } from 'prosemirror-model'
|
||||||
|
import { EditorState, Plugin, PluginKey } from 'prosemirror-state'
|
||||||
|
|
||||||
const NAME = 'node-uuid'
|
const NAME = 'node-uuid'
|
||||||
|
|
||||||
@ -14,11 +15,11 @@ export interface NodeUuidCommands<ReturnType> {
|
|||||||
/**
|
/**
|
||||||
* Add uuid mark
|
* Add uuid mark
|
||||||
*/
|
*/
|
||||||
setUuid: (uuid: string) => ReturnType
|
setNodeUuid: (uuid: string) => ReturnType
|
||||||
/**
|
/**
|
||||||
* Unset uuid mark
|
* Unset uuid mark
|
||||||
*/
|
*/
|
||||||
unsetUuid: () => ReturnType
|
unsetNodeUuid: () => ReturnType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,8 +31,32 @@ export interface NodeUuidStorage {
|
|||||||
activeNodeUuid: string | null
|
activeNodeUuid: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const findSelectionNodeUuidMark = (state: EditorState): ProseMirrorMark | undefined => {
|
||||||
|
if (state.selection === null || state.selection === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodeUuidMark: ProseMirrorMark | undefined
|
||||||
|
state.doc.nodesBetween(state.selection.from, state.selection.to, (node) => {
|
||||||
|
if (nodeUuidMark !== null || nodeUuidMark !== undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
nodeUuidMark = findNodeUuidMark(node)
|
||||||
|
})
|
||||||
|
|
||||||
|
return nodeUuidMark
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findNodeUuidMark = (node: Node): ProseMirrorMark | undefined => {
|
||||||
|
if (node === null || node === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.marks.find((mark) => mark.type.name === NAME && mark.attrs[NAME])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This mark allows to add node uuid to the selected text
|
* This extension allows to add node uuid to the selected text
|
||||||
* Creates span node with attribute node-uuid
|
* Creates span node with attribute node-uuid
|
||||||
*/
|
*/
|
||||||
export const NodeUuidExtension = Mark.create<NodeUuidOptions, NodeUuidStorage>({
|
export const NodeUuidExtension = Mark.create<NodeUuidOptions, NodeUuidStorage>({
|
||||||
@ -73,20 +98,23 @@ export const NodeUuidExtension = Mark.create<NodeUuidOptions, NodeUuidStorage>({
|
|||||||
|
|
||||||
addProseMirrorPlugins () {
|
addProseMirrorPlugins () {
|
||||||
const options = this.options
|
const options = this.options
|
||||||
|
const storage: NodeUuidStorage = this.storage
|
||||||
const plugins = [
|
const plugins = [
|
||||||
...(this.parent?.() ?? []),
|
...(this.parent?.() ?? []),
|
||||||
new Plugin({
|
new Plugin({
|
||||||
key: new PluginKey('handle-node-uuid-click-plugin'),
|
key: new PluginKey('handle-node-uuid-click-plugin'),
|
||||||
props: {
|
props: {
|
||||||
handleClick (view) {
|
handleClick (view) {
|
||||||
const { schema } = view.state
|
const attrs = getMarkAttributes(view.state, view.state.schema.marks[NAME])
|
||||||
|
|
||||||
const attrs = getMarkAttributes(view.state, schema.marks[NAME])
|
|
||||||
const nodeUuid = attrs?.[NAME]
|
const nodeUuid = attrs?.[NAME]
|
||||||
|
|
||||||
if (nodeUuid !== null || nodeUuid !== undefined) {
|
if (nodeUuid !== null || nodeUuid !== undefined) {
|
||||||
options.onNodeClicked?.(nodeUuid)
|
options.onNodeClicked?.(nodeUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (storage.activeNodeUuid !== nodeUuid) {
|
||||||
|
storage.activeNodeUuid = nodeUuid
|
||||||
|
options.onNodeSelected?.(storage.activeNodeUuid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -96,16 +124,27 @@ export const NodeUuidExtension = Mark.create<NodeUuidOptions, NodeUuidStorage>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
addCommands () {
|
addCommands () {
|
||||||
return {
|
const result: NodeUuidCommands<Command>[typeof NAME] = {
|
||||||
setUuid:
|
setNodeUuid:
|
||||||
(uuid: string) =>
|
(uuid: string) =>
|
||||||
({ commands }) =>
|
({ commands, state }: CommandProps) => {
|
||||||
commands.setMark(this.name, { [NAME]: uuid }),
|
const { doc, selection } = state
|
||||||
unsetUuid:
|
if (selection.empty) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (doc.rangeHasMark(selection.from, selection.to, getMarkType(NAME, state.schema))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands.setMark(this.name, { [NAME]: uuid })
|
||||||
|
},
|
||||||
|
unsetNodeUuid:
|
||||||
() =>
|
() =>
|
||||||
({ commands }) =>
|
({ commands }: CommandProps) =>
|
||||||
commands.unsetMark(this.name)
|
commands.unsetMark(this.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
|
|
||||||
addStorage () {
|
addStorage () {
|
||||||
@ -115,19 +154,13 @@ export const NodeUuidExtension = Mark.create<NodeUuidOptions, NodeUuidStorage>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onSelectionUpdate () {
|
onSelectionUpdate () {
|
||||||
const { $head } = this.editor.state.selection
|
const activeNodeUuidMark = findSelectionNodeUuidMark(this.editor.state)
|
||||||
|
const activeNodeUuid =
|
||||||
|
activeNodeUuidMark !== null && activeNodeUuidMark !== undefined ? activeNodeUuidMark.attrs[NAME] : null
|
||||||
|
|
||||||
const marks = $head.marks()
|
if (this.storage.activeNodeUuid !== activeNodeUuid) {
|
||||||
this.storage.activeNodeUuid = null
|
this.storage.activeNodeUuid = activeNodeUuid
|
||||||
if (marks.length > 0) {
|
this.options.onNodeSelected?.(this.storage.activeNodeUuid)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -27,6 +27,7 @@ export { default as StyledTextArea } from './components/StyledTextArea.svelte'
|
|||||||
export { default as StyledTextBox } from './components/StyledTextBox.svelte'
|
export { default as StyledTextBox } from './components/StyledTextBox.svelte'
|
||||||
export { default as StyledTextEditor } from './components/StyledTextEditor.svelte'
|
export { default as StyledTextEditor } from './components/StyledTextEditor.svelte'
|
||||||
export { default as TextEditor } from './components/TextEditor.svelte'
|
export { default as TextEditor } from './components/TextEditor.svelte'
|
||||||
|
export { default as TextEditorStyleToolbar } from './components/TextEditorStyleToolbar.svelte'
|
||||||
export { default } from './plugin'
|
export { default } from './plugin'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
@ -41,5 +42,11 @@ export {
|
|||||||
NodeHighlightType
|
NodeHighlightType
|
||||||
} from './components/extension/nodeHighlight'
|
} from './components/extension/nodeHighlight'
|
||||||
export { NodeUuidCommands, NodeUuidExtension, NodeUuidOptions, NodeUuidStorage } from './components/extension/nodeUuid'
|
export { NodeUuidCommands, NodeUuidExtension, NodeUuidOptions, NodeUuidStorage } from './components/extension/nodeUuid'
|
||||||
|
export { InlinePopupExtension } from './components/extension/inlinePopup'
|
||||||
|
export {
|
||||||
|
InlineStyleToolbarExtension,
|
||||||
|
InlineStyleToolbarOptions,
|
||||||
|
InlineStyleToolbarStorage
|
||||||
|
} from './components/extension/inlineStyleToolbar'
|
||||||
|
|
||||||
export { textEditorId }
|
export { textEditorId }
|
||||||
|
@ -73,8 +73,9 @@
|
|||||||
--highlight-red-hover: #ff967e;
|
--highlight-red-hover: #ff967e;
|
||||||
--highlight-red-press: #f96f50bd;
|
--highlight-red-press: #f96f50bd;
|
||||||
|
|
||||||
--text-editor-highlighted-node-info-background-color: #F2D7AE;
|
--text-editor-highlighted-node-warning-active-background-color: #F2D7AE;
|
||||||
--text-editor-highlighted-node-info-border-color: #DE9B35;
|
--text-editor-highlighted-node-warning-background-color: #F8EBD7;
|
||||||
|
--text-editor-highlighted-node-warning-border-color: #DE9B35;
|
||||||
|
|
||||||
--text-editor-highlighted-node-add-background-color: #DAEDDC;
|
--text-editor-highlighted-node-add-background-color: #DAEDDC;
|
||||||
--text-editor-highlighted-node-add-font-color: #1C4220;
|
--text-editor-highlighted-node-add-font-color: #1C4220;
|
||||||
|
@ -108,9 +108,13 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-editor-highlighted-node-info {
|
.text-editor-highlighted-node-warning {
|
||||||
background-color: var(--text-editor-highlighted-node-info-background-color);
|
background-color: var(--text-editor-highlighted-node-warning-background-color);
|
||||||
border-bottom: 0.0625rem solid var(--text-editor-highlighted-node-info-border-color);
|
border-bottom: 0.0625rem solid var(--text-editor-highlighted-node-warning-border-color);
|
||||||
|
|
||||||
|
&.text-editor-highlighted-node-selected, &:hover {
|
||||||
|
background-color: var(--text-editor-highlighted-node-warning-active-background-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-editor-highlighted-node-delete {
|
.text-editor-highlighted-node-delete {
|
||||||
|
Loading…
Reference in New Issue
Block a user