mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-19 23:00:13 +00:00
EZQMS-266: Commenting on document (#3759)
Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
parent
ca6f8426b2
commit
5f0ba95cee
@ -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>
|
||||||
|
@ -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)))
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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.
|
||||||
]
|
]
|
||||||
|
@ -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`)
|
||||||
})
|
})
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -76,5 +76,6 @@
|
|||||||
{noUnderline}
|
{noUnderline}
|
||||||
{...props}
|
{...props}
|
||||||
on:accent-color
|
on:accent-color
|
||||||
|
on:close
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
Loading…
Reference in New Issue
Block a user