EZQMS-266: Commenting on document (#3759)

Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
Anna No 2023-09-29 14:41:52 +07:00 committed by GitHub
parent ca6f8426b2
commit 5f0ba95cee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 141 additions and 158 deletions

View File

@ -24,17 +24,15 @@
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 { getCurrentAccount, Markup } from '@hcengineering/core'
import { generateId, getCurrentAccount, Markup } from '@hcengineering/core'
import { IntlString, translate } from '@hcengineering/platform' import { IntlString, translate } from '@hcengineering/platform'
import { Component, getPlatformColorForText, IconObjects, IconSize, themeStore } from '@hcengineering/ui' import { getPlatformColorForText, IconObjects, IconSize, themeStore } from '@hcengineering/ui'
import textEditorPlugin from '../plugin' import textEditorPlugin from '../plugin'
import { CollaborationIds, TextFormatCategory, TextNodeAction } from '../types' import { CollaborationIds, TextFormatCategory, TextNodeAction } from '../types'
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 StyleButton from './StyleButton.svelte'
@ -60,9 +58,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 textNodeActions: TextNodeAction[] = []
export let extensions: AnyExtension[] = [] export let onExtensions: () => AnyExtension[] = () => []
export let isNodeHighlightModeOn: boolean = false
export let onNodeHighlightType: (uuid: string) => NodeHighlightType = () => NodeHighlightType.WARNING let element: HTMLElement
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
@ -84,10 +82,6 @@
const currentUser = getCurrentAccount() const currentUser = getCurrentAccount()
let currentTextNodeAction: TextNodeAction | undefined | null
let selectedNodeUuid: string | null | undefined
let textNodeActionMenuElement: HTMLElement
let element: HTMLElement
let editor: Editor let editor: Editor
let placeHolderStr: string = '' let placeHolderStr: string = ''
@ -177,18 +171,6 @@
} }
} }
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 [
@ -231,34 +213,7 @@
} }
}), }),
DecorationExtension, DecorationExtension,
NodeHighlightExtension.configure({ ...onExtensions()
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
], ],
onTransaction: () => { onTransaction: () => {
// force re-render so `editor.isActive` works as expected // force re-render so `editor.isActive` works as expected
@ -296,24 +251,7 @@
let showDiff = true let showDiff = true
</script> </script>
<div class="actionPanel" bind:this={textNodeActionMenuElement}> <slot />
{#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}
@ -336,7 +274,7 @@
needFocus = true needFocus = true
}} }}
on:action={(event) => { on:action={(event) => {
currentTextNodeAction = textNodeActions.find((action) => action.id === event.detail) dispatch('action', { action: event.detail, editor })
needFocus = true needFocus = true
}} }}
/> />
@ -517,13 +455,4 @@
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>

View File

@ -1,5 +1,5 @@
import { Extension, getMarkRange, mergeAttributes } from '@tiptap/core' import { Extension, Range, getMarkRange, mergeAttributes } from '@tiptap/core'
import { Plugin, TextSelection } from 'prosemirror-state' import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
import { NodeUuidExtension, NodeUuidOptions } from './nodeUuid' import { NodeUuidExtension, NodeUuidOptions } from './nodeUuid'
export enum NodeHighlightType { export enum NodeHighlightType {
@ -7,19 +7,27 @@ export enum NodeHighlightType {
SUCCESS = 'success', SUCCESS = 'success',
ERROR = 'error' ERROR = 'error'
} }
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
} }
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
function isRange (range: Range | undefined | null | void): range is Range {
return range !== null && range !== undefined
}
/** /**
* Extension allows to highlight nodes based on uuid * Extension allows to highlight nodes based on uuid
*/ */
export const NodeHighlightExtension = Extension.create<NodeHighlightExtensionOptions>({ export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions> =
Extension.create<NodeHighlightExtensionOptions>({
addProseMirrorPlugins () { addProseMirrorPlugins () {
const options = this.options const options = this.options
const plugins = [ const plugins = [
...(this.parent?.() ?? []),
new Plugin({ new Plugin({
key: new PluginKey('handle-node-highlight-click-plugin'),
props: { props: {
handleClick (view, pos) { handleClick (view, pos) {
if (!options.isHighlightModeOn()) { if (!options.isHighlightModeOn()) {
@ -29,11 +37,12 @@ export const NodeHighlightExtension = Extension.create<NodeHighlightExtensionOpt
const range = getMarkRange(doc.resolve(pos), schema.marks[NodeUuidExtension.name]) const range = getMarkRange(doc.resolve(pos), schema.marks[NodeUuidExtension.name])
if (range === null || range === undefined) { if (!isRange(range)) {
return false return false
} }
const [$start, $end] = [doc.resolve(range.from), doc.resolve(range.to)] const { from, to } = range
const [$start, $end] = [doc.resolve(from), doc.resolve(to)]
view.dispatch(tr.setSelection(new TextSelection($start, $end))) view.dispatch(tr.setSelection(new TextSelection($start, $end)))

View File

@ -1,14 +1,15 @@
import { Mark, mergeAttributes } from '@tiptap/core' import { Mark, getMarkAttributes, mergeAttributes } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
const NAME = 'node-uuid' const NAME = 'node-uuid'
export interface NodeUuidOptions { export interface NodeUuidOptions {
HTMLAttributes: Record<string, any> HTMLAttributes: Record<string, any>
onNodeSelected?: (uuid: string | null) => any onNodeSelected?: (uuid: string | null) => void
onNodeClicked?: (uuid: string) => void
} }
declare module '@tiptap/core' { export interface NodeUuidCommands<ReturnType> {
interface Commands<ReturnType> {
[NAME]: { [NAME]: {
/** /**
* Add uuid mark * Add uuid mark
@ -20,6 +21,9 @@ declare module '@tiptap/core' {
unsetUuid: () => ReturnType unsetUuid: () => ReturnType
} }
} }
declare module '@tiptap/core' {
interface Commands<ReturnType> extends NodeUuidCommands<ReturnType> {}
} }
export interface NodeUuidStorage { export interface NodeUuidStorage {
@ -67,6 +71,30 @@ export const NodeUuidExtension = Mark.create<NodeUuidOptions, NodeUuidStorage>({
return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
}, },
addProseMirrorPlugins () {
const options = this.options
const plugins = [
...(this.parent?.() ?? []),
new Plugin({
key: new PluginKey('handle-node-uuid-click-plugin'),
props: {
handleClick (view) {
const { schema } = view.state
const attrs = getMarkAttributes(view.state, schema.marks[NAME])
const nodeUuid = attrs?.[NAME]
if (nodeUuid !== null || nodeUuid !== undefined) {
options.onNodeClicked?.(nodeUuid)
}
}
}
})
]
return plugins
},
addCommands () { addCommands () {
return { return {
setUuid: setUuid:

View File

@ -19,6 +19,7 @@ import Typography from '@tiptap/extension-typography'
import { CompletionOptions } from '../Completion' import { CompletionOptions } from '../Completion'
import MentionList from './MentionList.svelte' import MentionList from './MentionList.svelte'
import { SvelteRenderer } from './SvelteRenderer' import { SvelteRenderer } from './SvelteRenderer'
import { NodeUuidExtension } from './extension/nodeUuid'
export const tableExtensions = [ export const tableExtensions = [
Table.configure({ Table.configure({
@ -76,6 +77,7 @@ export const defaultExtensions: AnyExtension[] = [
openOnClick: true, openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' } HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}), }),
NodeUuidExtension,
...tableExtensions ...tableExtensions
// ...taskListExtensions // Disable since tasks are not working properly now. // ...taskListExtensions // Disable since tasks are not working properly now.
] ]

View File

@ -31,6 +31,13 @@ export * from './types'
export { default as Collaboration } from './components/Collaboration.svelte' export { default as Collaboration } from './components/Collaboration.svelte'
export { default as StyleButton } from './components/StyleButton.svelte' export { default as StyleButton } from './components/StyleButton.svelte'
export {
NodeHighlightExtension,
NodeHighlightExtensionOptions,
NodeHighlightType
} from './components/extension/nodeHighlight'
export { NodeUuidCommands, NodeUuidExtension, NodeUuidOptions, NodeUuidStorage } from './components/extension/nodeUuid'
addStringsLoader(textEditorId, async (lang: string) => { addStringsLoader(textEditorId, async (lang: string) => {
return await import(`../lang/${lang}.json`) return await import(`../lang/${lang}.json`)
}) })

View File

@ -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 { AnyComponent, AnySvelteComponent } from '@hcengineering/ui' import type { AnySvelteComponent } from '@hcengineering/ui'
/** /**
* @public * @public
@ -61,5 +61,4 @@ export interface TextNodeAction {
id: string id: string
label?: IntlString label?: IntlString
icon: Asset | AnySvelteComponent icon: Asset | AnySvelteComponent
panel: AnyComponent
} }

View File

@ -14,6 +14,14 @@
.text-editor-highlighted-node-success { .text-editor-highlighted-node-success {
background-color: var(--theme-won-color); background-color: var(--theme-won-color);
} }
.text-editor-popup {
background-color: var(--theme-comp-header-color);
border-radius: 0.5rem;
box-shadow: var(--theme-popup-shadow);
z-index: 1;
}
.proseH1 { .proseH1 {
margin-block-start: 1.25rem; margin-block-start: 1.25rem;
margin-block-end: 1.25rem; margin-block-end: 1.25rem;

View File

@ -76,5 +76,6 @@
{noUnderline} {noUnderline}
{...props} {...props}
on:accent-color on:accent-color
on:close
/> />
{/if} {/if}