diff --git a/packages/text-editor/package.json b/packages/text-editor/package.json index 3632356349..5bd7131896 100644 --- a/packages/text-editor/package.json +++ b/packages/text-editor/package.json @@ -82,8 +82,8 @@ "@tiptap/extension-code": "^2.1.12", "@tiptap/extension-bubble-menu": "^2.1.12", "@tiptap/extension-underline": "^2.1.12", - "@hocuspocus/provider": "^2.5.0", "@tiptap/extension-list-keymap": "^2.1.12", + "@hocuspocus/provider": "^2.5.0", "slugify": "^1.6.6" } } diff --git a/packages/text-editor/src/Completion.ts b/packages/text-editor/src/Completion.ts index 47af741a1a..12250e8c28 100644 --- a/packages/text-editor/src/Completion.ts +++ b/packages/text-editor/src/Completion.ts @@ -97,9 +97,9 @@ export const Completion = Node.create({ addAttributes () { return { - id: getDataAttribute('id', null), - label: getDataAttribute('label', null), - objectclass: getDataAttribute('objectclass', null) + id: getDataAttribute('id'), + label: getDataAttribute('label'), + objectclass: getDataAttribute('objectclass') } }, diff --git a/packages/text-editor/src/components/extension/todo.ts b/packages/text-editor/src/components/extension/todo.ts new file mode 100644 index 0000000000..1847ef730b --- /dev/null +++ b/packages/text-editor/src/components/extension/todo.ts @@ -0,0 +1,35 @@ +import { TaskItem } from '@tiptap/extension-task-item' +import { TaskList } from '@tiptap/extension-task-list' + +import { getDataAttribute } from '../../utils' + +export const TodoItemExtension = TaskItem.extend({ + name: 'todoItem', + + addOptions () { + return { + nested: false, + HTMLAttributes: {}, + taskListTypeName: 'todoList' + } + }, + + addAttributes () { + return { + ...this.parent?.(), + todoid: getDataAttribute('todoid', { default: null, keepOnSplit: false }), + userid: getDataAttribute('userid', { default: null, keepOnSplit: false }) + } + } +}) + +export const TodoListExtension = TaskList.extend({ + name: 'todoList', + + addOptions () { + return { + itemTypeName: 'todoItem', + HTMLAttributes: {} + } + } +}) diff --git a/packages/text-editor/src/components/extensions.ts b/packages/text-editor/src/components/extensions.ts index f90c475593..21ea4b8d57 100644 --- a/packages/text-editor/src/components/extensions.ts +++ b/packages/text-editor/src/components/extensions.ts @@ -19,8 +19,8 @@ import Link from '@tiptap/extension-link' import Typography from '@tiptap/extension-typography' import { CompletionOptions } from '../Completion' import MentionList from './MentionList.svelte' -import { SvelteRenderer } from './SvelteRenderer' import { NodeUuidExtension } from './extension/nodeUuid' +import { SvelteRenderer } from './node-view' export const tableExtensions = [ Table.configure({ @@ -164,9 +164,12 @@ export const completionConfig: Partial = { return { onStart: (props: any) => { component = new SvelteRenderer(MentionList, { - ...props, - close: () => { - component.destroy() + element: document.body, + props: { + ...props, + close: () => { + component.destroy() + } } }) }, diff --git a/packages/text-editor/src/components/node-view/NodeViewContent.svelte b/packages/text-editor/src/components/node-view/NodeViewContent.svelte new file mode 100644 index 0000000000..ad4c77ed1f --- /dev/null +++ b/packages/text-editor/src/components/node-view/NodeViewContent.svelte @@ -0,0 +1,33 @@ + + + + + + diff --git a/packages/text-editor/src/components/node-view/NodeViewWrapper.svelte b/packages/text-editor/src/components/node-view/NodeViewWrapper.svelte new file mode 100644 index 0000000000..2cc18a456e --- /dev/null +++ b/packages/text-editor/src/components/node-view/NodeViewWrapper.svelte @@ -0,0 +1,42 @@ + + + + + + diff --git a/packages/text-editor/src/components/node-view/context.ts b/packages/text-editor/src/components/node-view/context.ts new file mode 100644 index 0000000000..6a2886869e --- /dev/null +++ b/packages/text-editor/src/components/node-view/context.ts @@ -0,0 +1,33 @@ +// +// 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 { getContext } from 'svelte' + +const key = 'tiptap-node-view-context' + +export interface TiptapNodeViewContext { + onDragStart: (event: DragEvent) => void + onContentElement: (element: HTMLElement) => void +} + +export function getNodeViewContext (): TiptapNodeViewContext { + return getContext(key) +} + +export function createNodeViewContext (value: TiptapNodeViewContext): Map { + const context = new Map() + context.set(key, value) + return context +} diff --git a/packages/text-editor/src/components/node-view/index.ts b/packages/text-editor/src/components/node-view/index.ts new file mode 100644 index 0000000000..f7439e6814 --- /dev/null +++ b/packages/text-editor/src/components/node-view/index.ts @@ -0,0 +1,24 @@ +// +// 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. +// + +export { default as NodeViewContent } from './NodeViewContent.svelte' +export { default as NodeViewWrapper } from './NodeViewWrapper.svelte' +export { + default as SvelteNodeViewRenderer, + SvelteNodeViewComponent, + SvelteNodeViewProps as NodeViewProps, + SvelteNodeViewRendererOptions +} from './svelte-node-view-renderer' +export { SvelteRenderer, SvelteRendererComponent, SvelteRendererOptions } from './svelte-renderer' diff --git a/packages/text-editor/src/components/node-view/svelte-node-view-renderer.ts b/packages/text-editor/src/components/node-view/svelte-node-view-renderer.ts new file mode 100644 index 0000000000..5d1ce2fb6e --- /dev/null +++ b/packages/text-editor/src/components/node-view/svelte-node-view-renderer.ts @@ -0,0 +1,155 @@ +// +// 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 { + DecorationWithType, + Editor, + NodeView, + NodeViewProps, + NodeViewRenderer, + NodeViewRendererOptions +} from '@tiptap/core' +import type { Node as ProseMirrorNode } from '@tiptap/pm/model' +import type { ComponentType, SvelteComponent } from 'svelte' + +import { createNodeViewContext } from './context' +import { SvelteRenderer } from './svelte-renderer' + +export interface SvelteNodeViewRendererOptions extends NodeViewRendererOptions { + update?: (node: ProseMirrorNode, decorations: DecorationWithType[]) => boolean + contentAs?: string + contentDOMElementAs?: string + componentProps?: Record +} + +export type SvelteNodeViewProps = NodeViewProps & { + [key: string]: any +} + +export type SvelteNodeViewComponent = typeof SvelteComponent | ComponentType + +/** + * Svelte NodeView renderer, inspired by React and Vue implementation by Tiptap + * https://tiptap.dev/guide/node-views/react/ + */ +class SvelteNodeView extends NodeView { + renderer!: SvelteRenderer + + contentDOMElement!: HTMLElement | null + + override mount (): void { + const props: SvelteNodeViewProps = { + editor: this.editor, + node: this.node, + decorations: this.decorations, + selected: false, + extension: this.extension, + getPos: () => this.getPos(), + updateAttributes: (attributes = {}) => this.updateAttributes(attributes), + deleteNode: () => this.deleteNode(), + ...(this.options.componentProps ?? {}) + } + + if (this.node.isLeaf) { + this.contentDOMElement = null + } else if (this.options.contentDOMElementAs !== undefined) { + this.contentDOMElement = document.createElement(this.options.contentDOMElementAs) + } else { + this.contentDOMElement = document.createElement(this.node.isInline ? 'span' : 'div') + } + + if (this.contentDOMElement !== null) { + // For some reason the whiteSpace prop is not inherited properly in Chrome and Safari + // With this fix it seems to work fine + // See: https://github.com/ueberdosis/tiptap/issues/1197 + this.contentDOMElement.style.whiteSpace = 'inherit' + } + + const contentAs = this.options.contentAs ?? (this.node.isInline ? 'span' : 'div') + + const target = document.createElement(contentAs) + target.classList.add(`node-${this.node.type.name}`) + + const context = createNodeViewContext({ + onDragStart: this.onDragStart.bind(this), + onContentElement: (element) => { + if (this.contentDOMElement !== null && !element.contains(this.contentDOMElement)) { + element.appendChild(this.contentDOMElement) + } + } + }) + + this.renderer = new SvelteRenderer(this.component, { element: target, props, context }) + } + + override get dom (): HTMLElement { + if (this.renderer.element.firstElementChild?.hasAttribute('data-node-view-wrapper') === false) { + throw Error('Please use the NodeViewWrapper component for your node view.') + } + + return this.renderer.element + } + + override get contentDOM (): HTMLElement | null { + if (this.node.isLeaf) { + return null + } + + return this.contentDOMElement + } + + update (node: ProseMirrorNode, decorations: DecorationWithType[]): boolean { + if (typeof this.options.update === 'function') { + return this.options.update(node, decorations) + } + + if (node.type !== this.node.type) { + return false + } + + if (node === this.node && this.decorations === decorations) { + return true + } + + this.node = node + this.decorations = decorations + + this.renderer.updateProps({ node, decorations }) + + return true + } + + selectNode (): void { + this.renderer.updateProps({ selected: true }) + } + + deselectNode (): void { + this.renderer.updateProps({ selected: false }) + } + + destroy (): void { + this.renderer.destroy() + this.contentDOMElement = null + } +} + +const SvelteNodeViewRenderer = ( + component: SvelteNodeViewComponent, + options: Partial +): NodeViewRenderer => { + return (props) => new SvelteNodeView(component, props, options) +} + +export default SvelteNodeViewRenderer diff --git a/packages/text-editor/src/components/SvelteRenderer.ts b/packages/text-editor/src/components/node-view/svelte-renderer.ts similarity index 63% rename from packages/text-editor/src/components/SvelteRenderer.ts rename to packages/text-editor/src/components/node-view/svelte-renderer.ts index 5ee87dd944..fd22530696 100644 --- a/packages/text-editor/src/components/SvelteRenderer.ts +++ b/packages/text-editor/src/components/node-view/svelte-renderer.ts @@ -1,6 +1,6 @@ // // Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. +// Copyright © 2021, 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 @@ -14,14 +14,27 @@ // limitations under the License. // -import { ComponentType, SvelteComponent } from 'svelte' +import type { ComponentType, SvelteComponent } from 'svelte' + +export type SvelteRendererComponent = typeof SvelteComponent | ComponentType + +export interface SvelteRendererOptions { + element: HTMLElement + props?: any + context?: any +} export class SvelteRenderer { private readonly component: SvelteComponent + element: HTMLElement - constructor (comp: typeof SvelteComponent | ComponentType, props: any) { - const options = { target: document.body, props } - this.component = new (comp as any)(options) + constructor (component: SvelteRendererComponent, { element, props, context }: SvelteRendererOptions) { + this.element = element + this.element.classList.add('svelte-renderer') + + const options = { target: element, props, context } + const Component = component + this.component = new Component(options) } updateProps (props: Record): void { diff --git a/packages/text-editor/src/index.ts b/packages/text-editor/src/index.ts index 8c65298926..d0e0b90ab7 100644 --- a/packages/text-editor/src/index.ts +++ b/packages/text-editor/src/index.ts @@ -1,6 +1,6 @@ // // Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021 Hardcore Engineering Inc. +// Copyright © 2021, 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 @@ -30,6 +30,7 @@ export { default as TextEditor } from './components/TextEditor.svelte' export { default as TextEditorStyleToolbar } from './components/TextEditorStyleToolbar.svelte' export { default as AttachIcon } from './components/icons/Attach.svelte' export { default as TableOfContents } from './components/toc/TableOfContents.svelte' +export * from './components/node-view' export { default } from './plugin' export * from './types' export * from './utils' @@ -58,5 +59,6 @@ export { type InlineStyleToolbarStorage } from './components/extension/inlineStyleToolbar' export { ImageExtension, type ImageOptions } from './components/extension/imageExt' +export { TodoItemExtension, TodoListExtension } from './components/extension/todo' export { textEditorId } diff --git a/packages/text-editor/src/utils.ts b/packages/text-editor/src/utils.ts index b82293a9e4..1e60f57d5b 100644 --- a/packages/text-editor/src/utils.ts +++ b/packages/text-editor/src/utils.ts @@ -78,11 +78,14 @@ export function copyDocumentContent ( provider.copyContent(documentId, snapshotId) } -export function getDataAttribute (name: string, def?: unknown | null): Partial { +export function getDataAttribute ( + name: string, + options?: Omit +): Partial { const dataName = `data-${name}` return { - default: def, + default: null, parseHTML: (element) => element.getAttribute(dataName), renderHTML: (attributes) => { // eslint-disable-next-line @@ -93,6 +96,7 @@ export function getDataAttribute (name: string, def?: unknown | null): Partial { +export function getDataAttribute ( + name: string, + options?: Omit +): Partial { const dataName = `data-${name}` return { - default: def, + default: null, parseHTML: (element) => element.getAttribute(dataName), renderHTML: (attributes) => { // eslint-disable-next-line @@ -33,6 +36,7 @@ export function getDataAttribute (name: string, def?: unknown | null): Partial