Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EZQMS-266: Commenting on document #3759

Merged
merged 2 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 8 additions & 79 deletions packages/text-editor/src/components/CollaboratorEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,15 @@
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import Placeholder from '@tiptap/extension-placeholder'
import BubbleMenu from '@tiptap/extension-bubble-menu'
import { generateId, getCurrentAccount, Markup } from '@hcengineering/core'
import { getCurrentAccount, Markup } from '@hcengineering/core'
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 { CollaborationIds, TextFormatCategory, TextNodeAction } from '../types'

import { calculateDecorations } from './diff/decorations'
import { defaultExtensions } from './extensions'
import { NodeHighlightExtension, NodeHighlightType } from './extension/nodeHighlight'
import TextEditorStyleToolbar from './TextEditorStyleToolbar.svelte'

import StyleButton from './StyleButton.svelte'
Expand All @@ -60,9 +58,9 @@
export let autoOverflow = false
export let initialContent: string | undefined = undefined
export let textNodeActions: TextNodeAction[] = []
export let extensions: AnyExtension[] = []
export let isNodeHighlightModeOn: boolean = false
export let onNodeHighlightType: (uuid: string) => NodeHighlightType = () => NodeHighlightType.WARNING
export let onExtensions: () => AnyExtension[] = () => []

let element: HTMLElement

const ydoc = (getContext(CollaborationIds.Doc) as Y.Doc | undefined) ?? new Y.Doc()
const contextProvider = getContext(CollaborationIds.Provider) as WebsocketProvider | undefined
Expand All @@ -84,10 +82,6 @@

const currentUser = getCurrentAccount()

let currentTextNodeAction: TextNodeAction | undefined | null
let selectedNodeUuid: string | null | undefined
let textNodeActionMenuElement: HTMLElement
let element: HTMLElement
let editor: Editor

let placeHolderStr: string = ''
Expand Down Expand Up @@ -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({
addProseMirrorPlugins () {
return [
Expand Down Expand Up @@ -231,34 +213,7 @@
}
}),
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
...onExtensions()
],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
Expand Down Expand Up @@ -296,24 +251,7 @@
let showDiff = true
</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>
<slot />
{#if visible}
<div class="ref-container" class:autoOverflow>
{#if isFormatting && !readonly}
Expand All @@ -336,7 +274,7 @@
needFocus = true
}}
on:action={(event) => {
currentTextNodeAction = textNodeActions.find((action) => action.id === event.detail)
dispatch('action', { action: event.detail, editor })
needFocus = true
}}
/>
Expand Down Expand Up @@ -517,13 +455,4 @@
position: sticky;
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>
125 changes: 67 additions & 58 deletions packages/text-editor/src/components/extension/nodeHighlight.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,97 @@
import { Extension, getMarkRange, mergeAttributes } from '@tiptap/core'
import { Plugin, TextSelection } from 'prosemirror-state'
import { Extension, Range, getMarkRange, mergeAttributes } from '@tiptap/core'
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
import { NodeUuidExtension, NodeUuidOptions } from './nodeUuid'

export enum NodeHighlightType {
WARNING = 'warning',
SUCCESS = 'success',
ERROR = 'error'
}
interface NodeHighlightExtensionOptions extends NodeUuidOptions {
export interface NodeHighlightExtensionOptions extends NodeUuidOptions {
getNodeHighlightType: (uuid: string) => NodeHighlightType | undefined | null
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
*/
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
export const NodeHighlightExtension: Extension<NodeHighlightExtensionOptions> =
Extension.create<NodeHighlightExtensionOptions>({
addProseMirrorPlugins () {
const options = this.options
const plugins = [
...(this.parent?.() ?? []),
new Plugin({
key: new PluginKey('handle-node-highlight-click-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])
const range = getMarkRange(doc.resolve(pos), schema.marks[NodeUuidExtension.name])

if (range === null || range === undefined) {
return false
}
if (!isRange(range)) {
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)))

return true
return true
}
}
}
})
]
})
]

return plugins
},
return plugins
},

addExtensions () {
const options = this.options
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 } = {}
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 (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'
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)
return mergeAttributes(attrs, classAttrs)
}
}
}
}
}
})
]
}
})
})
]
}
})
56 changes: 42 additions & 14 deletions packages/text-editor/src/components/extension/nodeUuid.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import { Mark, mergeAttributes } from '@tiptap/core'
import { Mark, getMarkAttributes, mergeAttributes } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'

const NAME = 'node-uuid'

export interface NodeUuidOptions {
HTMLAttributes: Record<string, any>
onNodeSelected?: (uuid: string | null) => any
onNodeSelected?: (uuid: string | null) => void
onNodeClicked?: (uuid: string) => void
}

declare module '@tiptap/core' {
interface Commands<ReturnType> {
[NAME]: {
/**
* Add uuid mark
*/
setUuid: (uuid: string) => ReturnType
/**
* Unset uuid mark
*/
unsetUuid: () => ReturnType
}
export interface NodeUuidCommands<ReturnType> {
[NAME]: {
/**
* Add uuid mark
*/
setUuid: (uuid: string) => ReturnType
/**
* Unset uuid mark
*/
unsetUuid: () => ReturnType
}
}

declare module '@tiptap/core' {
interface Commands<ReturnType> extends NodeUuidCommands<ReturnType> {}
}

export interface NodeUuidStorage {
activeNodeUuid: string | null
}
Expand Down Expand Up @@ -67,6 +71,30 @@ export const NodeUuidExtension = Mark.create<NodeUuidOptions, NodeUuidStorage>({
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 () {
return {
setUuid:
Expand Down
2 changes: 2 additions & 0 deletions packages/text-editor/src/components/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Typography from '@tiptap/extension-typography'
import { CompletionOptions } from '../Completion'
import MentionList from './MentionList.svelte'
import { SvelteRenderer } from './SvelteRenderer'
import { NodeUuidExtension } from './extension/nodeUuid'

export const tableExtensions = [
Table.configure({
Expand Down Expand Up @@ -76,6 +77,7 @@ export const defaultExtensions: AnyExtension[] = [
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
NodeUuidExtension,
...tableExtensions
// ...taskListExtensions // Disable since tasks are not working properly now.
]
Expand Down
Loading