mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 21:50:34 +03:00
EZQMS-359 Add update highlight command to nodeHighlight plugin (#3997)
Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
parent
af5c79c928
commit
89bfabb37d
@ -44,34 +44,16 @@
|
||||
"@tiptap/core": "^2.1.12",
|
||||
"@tiptap/pm": "^2.1.12",
|
||||
"@tiptap/starter-kit": "^2.1.12",
|
||||
"@tiptap/suggestion": "^2.1.12",
|
||||
"@tiptap/extension-highlight": "^2.1.12",
|
||||
"@tiptap/extension-placeholder": "^2.1.12",
|
||||
"@tiptap/extension-mention": "^2.1.12",
|
||||
"@tiptap/extension-typography": "^2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.12",
|
||||
"@tiptap/suggestion": "^2.1.12",
|
||||
"@tiptap/extension-task-list": "^2.1.12",
|
||||
"@tiptap/extension-task-item": "^2.1.12",
|
||||
"@tiptap/extension-collaboration": "^2.1.12",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.1.12",
|
||||
"@tiptap/prosemirror-tables": "^1.1.4",
|
||||
"prosemirror-gapcursor": "^1.3.2",
|
||||
"prosemirror-dropcursor": "^1.8.1",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-transform": "^1.7.3",
|
||||
"prosemirror-schema-list": "^1.3.0",
|
||||
"prosemirror-commands": "^1.5.2",
|
||||
"yjs": "^13.5.52",
|
||||
"y-websocket": "^1.5.0",
|
||||
"y-prosemirror": "^1.2.1",
|
||||
"prosemirror-changeset": "^2.2.1",
|
||||
"prosemirror-model": "^1.19.2",
|
||||
"prosemirror-view": "^1.31.4",
|
||||
"prosemirror-history": "^1.3.2",
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"rfc6902": "^5.0.1",
|
||||
"diff": "^5.1.0",
|
||||
"@tiptap/extension-code-block": "^2.1.12",
|
||||
"@tiptap/extension-gapcursor": "^2.1.12",
|
||||
"@tiptap/extension-heading": "^2.1.12",
|
||||
@ -84,7 +66,12 @@
|
||||
"@tiptap/extension-underline": "^2.1.12",
|
||||
"@tiptap/extension-list-keymap": "^2.1.12",
|
||||
"@hocuspocus/provider": "^2.5.0",
|
||||
"slugify": "^1.6.6",
|
||||
"prosemirror-codemark": "^0.4.2"
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
"yjs": "^13.5.52",
|
||||
"y-websocket": "^1.5.0",
|
||||
"y-prosemirror": "^1.2.1",
|
||||
"rfc6902": "^5.0.1",
|
||||
"diff": "^5.1.0",
|
||||
"slugify": "^1.6.6"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Extension, Mark, mergeAttributes } from '@tiptap/core'
|
||||
import { ChangeSet } from 'prosemirror-changeset'
|
||||
import { Plugin } from 'prosemirror-state'
|
||||
import { ChangeSet } from '@tiptap/pm/changeset'
|
||||
import { Plugin } from '@tiptap/pm/state'
|
||||
|
||||
export interface ChangeHighlightOptions {
|
||||
multicolor: boolean
|
||||
|
38
packages/text-editor/src/commands.ts
Normal file
38
packages/text-editor/src/commands.ts
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { TextEditorCommand, TextEditorCommandHandler } from './types'
|
||||
|
||||
export function textEditorCommandHandler (editor: Editor | undefined): TextEditorCommandHandler | undefined {
|
||||
return editor !== undefined ? new TextEditorCommandHandlerImpl(editor) : undefined
|
||||
}
|
||||
|
||||
class TextEditorCommandHandlerImpl implements TextEditorCommandHandler {
|
||||
constructor (private readonly editor: Editor) {}
|
||||
|
||||
chain (...commands: TextEditorCommand[]): boolean {
|
||||
let chain = this.editor.chain()
|
||||
|
||||
for (const command of commands) {
|
||||
chain = chain.command(({ editor, commands }) => command({ editor, commands }))
|
||||
}
|
||||
|
||||
return chain.run()
|
||||
}
|
||||
|
||||
command (command: TextEditorCommand): boolean {
|
||||
return this.editor.commands.command(({ editor, commands }) => command({ editor, commands }))
|
||||
}
|
||||
}
|
@ -16,20 +16,18 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Editor, Extension, mergeAttributes } from '@tiptap/core'
|
||||
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { DecorationSet } from '@tiptap/pm/view'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { IconObjects, IconSize } from '@hcengineering/ui'
|
||||
import StyleButton from './StyleButton.svelte'
|
||||
|
||||
import { DecorationSet } from 'prosemirror-view'
|
||||
import textEditorPlugin from '../plugin'
|
||||
|
||||
import { calculateDecorations } from './diff/decorations'
|
||||
import { defaultExtensions } from './extensions'
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { defaultExtensions } from './extensions'
|
||||
import StyleButton from './StyleButton.svelte'
|
||||
|
||||
export let content: Markup
|
||||
export let buttonSize: IconSize = 'small'
|
||||
|
@ -15,8 +15,8 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
||||
import { DecorationSet } from 'prosemirror-view'
|
||||
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state'
|
||||
import { DecorationSet } from '@tiptap/pm/view'
|
||||
import { getContext, createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import * as Y from 'yjs'
|
||||
import {
|
||||
@ -33,12 +33,13 @@
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import { getCurrentAccount, Markup } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { getPlatformColorForText, IconObjects, IconSize, Loading, registerFocus, themeStore } from '@hcengineering/ui'
|
||||
import { IconObjects, IconSize, Loading, getPlatformColorForText, registerFocus, themeStore } from '@hcengineering/ui'
|
||||
|
||||
import { Completion } from '../Completion'
|
||||
import { textEditorCommandHandler } from '../commands'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import { TiptapCollabProvider } from '../provider'
|
||||
import { CollaborationIds, TextFormatCategory, TextNodeAction } from '../types'
|
||||
import { CollaborationIds, TextEditorCommandHandler, TextFormatCategory, TextNodeAction } from '../types'
|
||||
import { copyDocumentContent, copyDocumentField } from '../utils'
|
||||
|
||||
import { calculateDecorations } from './diff/decorations'
|
||||
@ -112,6 +113,12 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: handler = textEditorCommandHandler(editor)
|
||||
|
||||
export function commands (): TextEditorCommandHandler | undefined {
|
||||
return handler
|
||||
}
|
||||
|
||||
export function getHTML (): string | undefined {
|
||||
if (editor) {
|
||||
return editor.getHTML()
|
||||
|
@ -15,12 +15,12 @@
|
||||
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { ChangeSet } from 'prosemirror-changeset'
|
||||
import { DOMParser, Node, Schema } from 'prosemirror-model'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
import { recreateTransform } from './recreate'
|
||||
import { ChangeSet } from '@tiptap/pm/changeset'
|
||||
import { DOMParser, Node, Schema } from '@tiptap/pm/model'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
import { yDocToProsemirrorJSON } from 'y-prosemirror'
|
||||
import { Doc, applyUpdate } from 'yjs'
|
||||
import { recreateTransform } from './recreate'
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -17,8 +17,8 @@
|
||||
//
|
||||
|
||||
import { Change, diffWordsWithSpace } from 'diff'
|
||||
import { Node, Schema } from 'prosemirror-model'
|
||||
import { ReplaceStep, Step, Transform } from 'prosemirror-transform'
|
||||
import { Node, Schema } from '@tiptap/pm/model'
|
||||
import { ReplaceStep, Step, Transform } from '@tiptap/pm/transform'
|
||||
import { applyPatch, createPatch, Operation } from 'rfc6902'
|
||||
import { Pointer } from 'rfc6902/pointer'
|
||||
import { diffArraysPM } from './diff'
|
||||
|
@ -13,6 +13,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { DecorationAttrs } from 'prosemirror-view'
|
||||
import { DecorationAttrs } from '@tiptap/pm/view'
|
||||
|
||||
export const noSelectionRender = (_user: Record<string, any>): DecorationAttrs => ({})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Extension, isTextSelection } from '@tiptap/core'
|
||||
import { BubbleMenuOptions } from '@tiptap/extension-bubble-menu'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { InlinePopupExtension } from './inlinePopup'
|
||||
|
||||
export type InlineStyleToolbarOptions = BubbleMenuOptions & {
|
||||
|
@ -1,13 +1,21 @@
|
||||
import { Extension, Range, getMarkRange, mergeAttributes } from '@tiptap/core'
|
||||
import { Command, CommandProps, Extension, Range, getMarkRange, mergeAttributes } from '@tiptap/core'
|
||||
import { Node as ProseMirrorNode, MarkType } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state'
|
||||
import { AddMarkStep, RemoveMarkStep } from '@tiptap/pm/transform'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
import { NodeUuidExtension, NodeUuidOptions, NodeUuidStorage, findNodeUuidMark } from './nodeUuid'
|
||||
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
import { TextEditorCommand } from '../../types'
|
||||
|
||||
export enum NodeHighlightType {
|
||||
WARNING = 'warning',
|
||||
ADD = 'add',
|
||||
DELETE = 'delete'
|
||||
}
|
||||
|
||||
export function highlightUpdateCommand (): TextEditorCommand {
|
||||
return ({ commands }) => commands.updateHighlight()
|
||||
}
|
||||
|
||||
export interface NodeHighlightExtensionOptions extends NodeUuidOptions {
|
||||
getNodeHighlightType: (uuid: string) => NodeHighlightType | undefined | null
|
||||
isHighlightModeOn: () => boolean
|
||||
@ -41,14 +49,34 @@ const generateAttributes = (uuid: string, options: NodeHighlightExtensionOptions
|
||||
return classAttrs
|
||||
}
|
||||
|
||||
const NodeHighlight = 'node-highlight'
|
||||
|
||||
const NodeHighlightMeta = 'node-highlight'
|
||||
|
||||
export interface NodeHighlightCommands<ReturnType> {
|
||||
[NodeHighlight]: {
|
||||
/**
|
||||
* Force all nodes to be re-rendered
|
||||
*/
|
||||
updateHighlight: () => ReturnType
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> extends NodeHighlightCommands<ReturnType> {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension allows to highlight nodes based on uuid
|
||||
*/
|
||||
export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions, NodeUuidStorage> =
|
||||
Extension.create<NodeHighlightExtensionOptions>({
|
||||
name: NodeHighlight,
|
||||
|
||||
addStorage (): NodeUuidStorage {
|
||||
return { activeNodeUuid: null }
|
||||
},
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
const options = this.options
|
||||
const storage: NodeUuidStorage = this.storage
|
||||
@ -56,7 +84,7 @@ export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions, No
|
||||
const plugins = [
|
||||
...(this.parent?.() ?? []),
|
||||
new Plugin({
|
||||
key: new PluginKey('handle-node-highlight-click-plugin'),
|
||||
key: new PluginKey('node-highlight-click-plugin'),
|
||||
props: {
|
||||
handleClick (view, pos) {
|
||||
if (!options.isHighlightModeOn() || options.isAutoSelect?.() !== true) {
|
||||
@ -79,45 +107,65 @@ export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions, No
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
new Plugin({
|
||||
key: new PluginKey('node-highlight-click-decorations-plugin'),
|
||||
props: {
|
||||
decorations (state) {
|
||||
if (!options.isHighlightModeOn()) {
|
||||
return undefined
|
||||
}
|
||||
const decorations: Decoration[] = []
|
||||
key: new PluginKey('node-highlight-decoration-plugin'),
|
||||
state: {
|
||||
init (_config, state): DecorationSet {
|
||||
const { doc, schema } = state
|
||||
doc.descendants((node, pos) => {
|
||||
const nodeUuidMark = findNodeUuidMark(node)
|
||||
const markType = schema.marks[NodeUuidExtension.name]
|
||||
return createDecorations(doc, markType, storage, options)
|
||||
},
|
||||
|
||||
if (nodeUuidMark !== null && nodeUuidMark !== undefined) {
|
||||
const nodeUuid = nodeUuidMark.attrs[NodeUuidExtension.name]
|
||||
const attributes = generateAttributes(nodeUuid, options)
|
||||
if (attributes === null || attributes === undefined) {
|
||||
return
|
||||
apply (tr, decorations, oldState, newState) {
|
||||
const markType = newState.schema.marks[NodeUuidExtension.name]
|
||||
|
||||
if (tr.getMeta(NodeHighlightMeta) !== undefined) {
|
||||
return createDecorations(tr.doc, markType, storage, options)
|
||||
}
|
||||
|
||||
if (!tr.docChanged) {
|
||||
return decorations.map(tr.mapping, tr.doc)
|
||||
}
|
||||
|
||||
// update all decorations when transaction has mark changes
|
||||
if (
|
||||
tr.steps.some(
|
||||
(step) =>
|
||||
(step instanceof AddMarkStep && step.mark.type === markType) ||
|
||||
(step instanceof RemoveMarkStep && step.mark.type === markType)
|
||||
)
|
||||
) {
|
||||
return createDecorations(tr.doc, markType, storage, options)
|
||||
}
|
||||
|
||||
// update all decorations when changed content has mark changes
|
||||
let hasMarkChanges = false
|
||||
tr.mapping.maps.forEach((map, index) => {
|
||||
if (hasMarkChanges) return
|
||||
|
||||
map.forEach((oldStart, oldEnd, newStart, newEnd) => {
|
||||
const oldDoc = tr.docs[index]
|
||||
const newDoc = tr.docs[index + 1] ?? tr.doc
|
||||
|
||||
if (
|
||||
oldDoc.rangeHasMark(oldStart, oldEnd, markType) !== newDoc.rangeHasMark(newStart, newEnd, markType)
|
||||
) {
|
||||
hasMarkChanges = 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 (!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)
|
||||
if (hasMarkChanges) {
|
||||
return createDecorations(tr.doc, markType, storage, options)
|
||||
}
|
||||
|
||||
return decorations.map(tr.mapping, tr.doc)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return this.getState(state)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -125,6 +173,20 @@ export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions, No
|
||||
|
||||
return plugins
|
||||
},
|
||||
|
||||
addCommands () {
|
||||
const result: NodeHighlightCommands<Command>[typeof NodeHighlight] = {
|
||||
updateHighlight:
|
||||
() =>
|
||||
({ view: { dispatch, state } }: CommandProps) => {
|
||||
dispatch(state.tr.setMeta(NodeHighlightMeta, ''))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
|
||||
addExtensions () {
|
||||
const options: NodeHighlightExtensionOptions = this.options
|
||||
const storage: NodeUuidStorage = this.storage
|
||||
@ -144,3 +206,43 @@ export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions, No
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const createDecorations = (
|
||||
doc: ProseMirrorNode,
|
||||
markType: MarkType,
|
||||
storage: NodeUuidStorage,
|
||||
options: NodeHighlightExtensionOptions
|
||||
): DecorationSet => {
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
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), markType)
|
||||
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.create(doc, decorations)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Command, CommandProps, Mark, getMarkType, getMarksBetween, mergeAttributes } from '@tiptap/core'
|
||||
import { Node, Mark as ProseMirrorMark } from 'prosemirror-model'
|
||||
import { EditorState, Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { Node, Mark as ProseMirrorMark } from '@tiptap/pm/model'
|
||||
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
const NAME = 'node-uuid'
|
||||
|
||||
|
@ -45,7 +45,8 @@ export {
|
||||
export {
|
||||
NodeHighlightExtension,
|
||||
NodeHighlightType,
|
||||
type NodeHighlightExtensionOptions
|
||||
type NodeHighlightExtensionOptions,
|
||||
highlightUpdateCommand
|
||||
} from './components/extension/nodeHighlight'
|
||||
export {
|
||||
NodeUuidExtension,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import type { AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { Editor, SingleCommands } from '@tiptap/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -72,3 +73,24 @@ export interface Heading {
|
||||
level: number
|
||||
title: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface TextEditorCommandProps {
|
||||
editor: Editor
|
||||
commands: SingleCommands
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type TextEditorCommand = (props: TextEditorCommandProps) => boolean
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface TextEditorCommandHandler {
|
||||
command: (command: TextEditorCommand) => boolean
|
||||
chain: (...commands: TextEditorCommand[]) => boolean
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user