From e35cc67de643e8baa39c8ec203384cf41de42004 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sat, 13 Apr 2024 22:50:21 +0700 Subject: [PATCH 1/6] fix: NodeViewRenderer --- packages/svelte/package.json | 1 + packages/svelte/src/lib/Editor.svelte | 2 + .../svelte/src/lib/adapter/Counter.svelte | 26 ++++ .../lib/node-view/SvelteNodeViewRenderer.ts | 124 +++++++++++------- .../src/lib/node-view/SvelteRenderer.ts | 38 ++---- .../lib/plugins/codeBlock/CodeBlock.svelte | 26 ++-- .../lib/plugins/codeBlock/Languages.svelte | 8 +- .../src/lib/plugins/codeBlock/codeBlock.ts | 42 ++++-- .../src/lib/plugins/figure/Figure.svelte | 47 ++++--- .../svelte/src/lib/plugins/figure/image.ts | 4 +- .../src/lib/plugins/image/EmbedTab.svelte | 8 +- .../src/lib/plugins/image/Placeholder.svelte | 8 +- .../src/lib/plugins/image/SelectImage.svelte | 10 +- packages/svelte/src/routes/+page.svelte | 3 + pnpm-lock.yaml | 20 +++ 15 files changed, 226 insertions(+), 141 deletions(-) create mode 100644 packages/svelte/src/lib/adapter/Counter.svelte diff --git a/packages/svelte/package.json b/packages/svelte/package.json index cf8c787..48b7bd2 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -37,6 +37,7 @@ "@floating-ui/dom": "^1.5.3", "@melt-ui/svelte": "^0.61.1", "@nextlint/core": "^1.4.0", + "@prosemirror-adapter/svelte": "^0.2.6", "@svelte-put/clickoutside": "^3.0.0", "@svelte-put/lockscroll": "^1.0.1", "@tiptap/core": "^2.1.13", diff --git a/packages/svelte/src/lib/Editor.svelte b/packages/svelte/src/lib/Editor.svelte index e5d527a..f8d6b3c 100644 --- a/packages/svelte/src/lib/Editor.svelte +++ b/packages/svelte/src/lib/Editor.svelte @@ -25,10 +25,12 @@ Dropcursor, type DropcursorOptions } from '@tiptap/extension-dropcursor'; + import {useProsemirrorAdapterProvider} from '@prosemirror-adapter/svelte'; import BubbleMenu from './components/BubbleMenu/BubbleMenu.svelte'; import {BubbleMenuExtension} from './plugins/bubbleMenu/bubbleMenu'; + useProsemirrorAdapterProvider(); export let content: Content; export let placeholder = "Press 'space' GPT support, type '/' for help"; export let onChange: (editor: Editor) => void; diff --git a/packages/svelte/src/lib/adapter/Counter.svelte b/packages/svelte/src/lib/adapter/Counter.svelte new file mode 100644 index 0000000..c8f2456 --- /dev/null +++ b/packages/svelte/src/lib/adapter/Counter.svelte @@ -0,0 +1,26 @@ + + + + +
+ + diff --git a/packages/svelte/src/lib/node-view/SvelteNodeViewRenderer.ts b/packages/svelte/src/lib/node-view/SvelteNodeViewRenderer.ts index f42973d..f63d3fb 100644 --- a/packages/svelte/src/lib/node-view/SvelteNodeViewRenderer.ts +++ b/packages/svelte/src/lib/node-view/SvelteNodeViewRenderer.ts @@ -1,5 +1,9 @@ -import type {NodeView as ProseMirrorNodeView} from '@tiptap/pm/view'; -import type {Node as PMNode} from '@tiptap/pm/model'; +//Inspire by Prosemirror Adapter by Mirone. +import type { + Decoration, + NodeView as ProseMirrorNodeView +} from '@tiptap/pm/view'; +import type {Attrs, Node as PMNode} from '@tiptap/pm/model'; import type {ComponentType} from 'svelte'; import { @@ -10,16 +14,34 @@ import { type NodeViewRendererOptions as TNodeViewRendererOptions } from '@tiptap/core'; -import {get, writable, type Writable} from 'svelte/store'; +import {writable, type Writable} from 'svelte/store'; import {SvelteRenderer} from './SvelteRenderer'; +export interface NodeViewContext { + // immutable + editor: Editor; + contentDOM: (node: HTMLElement) => void; + getPos: () => number | undefined; + updateAttributes: (attrs: Attrs) => void; + deleteNode: () => void; + + // changes between updates + node: Writable; + selected: Writable; + decorations: Writable; +} + +//@ts-expect-error skip export class SvelteNodeView extends NodeView implements ProseMirrorNodeView { - renderer!: SvelteRenderer; store!: Writable; + props: NodeViewRendererProps; + context: NodeViewContext; + renderer: SvelteRenderer; + #contentDOM: HTMLElement | null = null; constructor( readonly options: NodeViewRendererOptions, @@ -29,25 +51,24 @@ export class SvelteNodeView stopEvent: options.stopEvent || null, ignoreMutation: options.ignoreMutation || null }); - this.store = writable({ + this.props = props; + this.context = { + //immutable 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.renderer = new SvelteRenderer( - { - component: options.component, - contentAs: options.contentAs, - domAs: options.domAs + getPos: this.getPos, + updateAttributes: (attrs = {}) => this.updateAttributes(attrs), + deleteNode: this.deleteNode, + contentDOM: (contentElement: HTMLElement) => { + this.#contentDOM = contentElement; }, - this.store - ); + + //change between updates + node: writable(this.node), + selected: writable(false), + decorations: writable(this.decorations) + }; + + this.renderer = new SvelteRenderer(options, this.context); } override get dom() { @@ -56,7 +77,7 @@ export class SvelteNodeView override get contentDOM() { if (this.node.isLeaf) return null; - return this.renderer.contentElement || null; + return this.#contentDOM || null; } deleteNode = () => { @@ -65,38 +86,43 @@ export class SvelteNodeView }; update(node: PMNode) { - this.store.update(store => { - store.node = node; - return store; - }); - return true; + if (this.shouldUpdate(node)) { + this.context.node.set(node); + return true; + } + return false; + } + selectNode() { + this.context.selected.set(true); } - selectNode = () => { - this.store.update(store => { - store.selected = true; - return store; - }); - }; - - deselectNode = () => { - this.store.update(store => { - store.selected = false; - return store; - }); - }; + deselectNode() { + this.context.selected.set(false); + } - toggleNodeSelection = () => { - if (get(this.store).selected) { - this.deselectNode(); - return; - } - return this.selectNode(); + shouldUpdate = (node: PMNode) => { + if (node.type !== this.node.type) return false; + if (node.sameMarkup(this.node) && node.content.eq(this.node.content)) + return false; + return true; }; - destroy() { this.renderer.destroy(); } + ignoreMutation(mutation: MutationRecord) { + if (!this.dom || !this.contentDOM) return true; + + if (this.node.isLeaf || this.node.isAtom) return true; + + if ((mutation.type as unknown) === 'selection') return false; + + if (this.contentDOM === mutation.target && mutation.type === 'attributes') + return true; + + if (this.contentDOM.contains(mutation.target)) return false; + + return true; + } } export interface NodeViewRendererOptions @@ -106,5 +132,7 @@ export interface NodeViewRendererOptions domAs?: string; } export const SvelteNodeViewRenderer = (options: NodeViewRendererOptions) => { - return (props: NodeViewRendererProps) => new SvelteNodeView(options, props); + return (props: NodeViewRendererProps) => { + return new SvelteNodeView(options, props); + }; }; diff --git a/packages/svelte/src/lib/node-view/SvelteRenderer.ts b/packages/svelte/src/lib/node-view/SvelteRenderer.ts index c983ea8..c6d7913 100644 --- a/packages/svelte/src/lib/node-view/SvelteRenderer.ts +++ b/packages/svelte/src/lib/node-view/SvelteRenderer.ts @@ -1,11 +1,12 @@ import {getContext, type ComponentType, type SvelteComponent} from 'svelte'; import type {NodeViewProps} from '@tiptap/core'; -import {get, type Writable} from 'svelte/store'; +import type {Writable} from 'svelte/store'; +import type {NodeViewContext} from './SvelteNodeViewRenderer'; export interface SvelteNodeViewContext { props?: Writable; - contentRef?: (element: HTMLElement) => void; + contentDOM?: (element: HTMLElement) => void; } export interface SvelteRenderOptions { @@ -19,30 +20,19 @@ export class SvelteRenderer { contentElement?: HTMLElement; component: SvelteComponent; - context: SvelteNodeViewContext = {}; - - constructor(opts: SvelteRenderOptions, props: Writable) { - const {component: Component, domAs, contentAs} = opts; - this.context = { - props, - contentRef: (element: HTMLElement) => { - element.setAttribute('data-node-view-content', 'true'); - element.style.whiteSpace = 'inherit'; - this.contentElement = element; - } - }; + + constructor( + opts: SvelteRenderOptions, + readonly context: NodeViewContext + ) { + const {component: Component, domAs} = opts; // Create dom node this.element = document.createElement(domAs || 'div'); this.element.setAttribute('data-node-view-root', 'true'); - this.component = new Component({ target: this.element, - props: { - as: domAs, - contentAs - }, - context: new Map(Object.entries(this.context)) + context: new Map(Object.entries(context)) }); } @@ -53,8 +43,6 @@ export class SvelteRenderer { } } -export const useNodeViewProps = () => - getContext>('props'); - -export const useContentRef = () => - getContext<(element: HTMLElement) => void>('contentRef'); +export const useNodeViewContext = ( + ctxKey: K +) => getContext(ctxKey); diff --git a/packages/svelte/src/lib/plugins/codeBlock/CodeBlock.svelte b/packages/svelte/src/lib/plugins/codeBlock/CodeBlock.svelte index 8e66f3f..2f72bea 100644 --- a/packages/svelte/src/lib/plugins/codeBlock/CodeBlock.svelte +++ b/packages/svelte/src/lib/plugins/codeBlock/CodeBlock.svelte @@ -1,5 +1,5 @@ -
- {@html highlightCode} -
+ + +
-  
-
+ class="absolute top-0 left-0 block inset-0 outline-nonerounded-md overflow-y-hidden" + use:contentRef> diff --git a/packages/svelte/src/lib/plugins/codeBlock/Languages.svelte b/packages/svelte/src/lib/plugins/codeBlock/Languages.svelte index e0fd5a8..e47e82d 100644 --- a/packages/svelte/src/lib/plugins/codeBlock/Languages.svelte +++ b/packages/svelte/src/lib/plugins/codeBlock/Languages.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/src/lib/plugins/codeBlock/codeBlock.ts b/packages/svelte/src/lib/plugins/codeBlock/codeBlock.ts index 9914f14..859bfe1 100644 --- a/packages/svelte/src/lib/plugins/codeBlock/codeBlock.ts +++ b/packages/svelte/src/lib/plugins/codeBlock/codeBlock.ts @@ -1,10 +1,13 @@ import {CodeBlock} from '@tiptap/extension-code-block'; import type {BundledLanguage, BundledTheme} from 'shiki'; -import {SvelteNodeViewRenderer} from '$lib/node-view'; - import SvelteCodeBlock from './CodeBlock.svelte'; import {PluginKey, Plugin} from '@tiptap/pm/state'; +import {mergeAttributes, type NodeViewRendererProps} from '@tiptap/core'; +import type {SvelteComponent} from 'svelte'; +import {SvelteNodeView} from '@prosemirror-adapter/svelte'; + +import type {NodeView} from '@tiptap/pm/view'; export type NextlintCodeBlockAttrs = { lang: BundledLanguage; @@ -24,22 +27,22 @@ export const NextlintCodeBlock = CodeBlock.extend({ lang: { default: this.options.langs[0], parseHTML: html => { - return html.getAttribute('code-block-lang'); + return html.getAttribute('data-lang'); }, renderHTML: attrs => { return { - 'code-block-lang': attrs.lang + 'data-lang': attrs.lang }; } }, theme: { default: this.options.themes[0], parseHTML: html => { - return html.getAttribute('code-block-theme'); + return html.getAttribute('data-theme'); }, renderHTML: attrs => { return { - 'code-block-theme': attrs.theme + 'data-theme': attrs.theme }; } } @@ -77,11 +80,28 @@ export const NextlintCodeBlock = CodeBlock.extend({ }, addNodeView() { - return SvelteNodeViewRenderer({ - component: SvelteCodeBlock, - domAs: 'code-block', - contentAs: 'pre' - }); + return props => { + const svelteNodeView = new SvelteNodeView({ + node: props.node, + //@ts-expect-error skip + getPos: props.getPos, + decorations: props.decorations, + view: this.editor.view, + options: { + component: SvelteCodeBlock, + as: 'code-block', + contentAs: 'code', + stopEvent() { + return true; + }, + selectNode() { + console.log('nodeSeeclted'); + } + } + }); + svelteNodeView.render(); + return svelteNodeView; + }; }, addProseMirrorPlugins() { diff --git a/packages/svelte/src/lib/plugins/figure/Figure.svelte b/packages/svelte/src/lib/plugins/figure/Figure.svelte index 27d5104..6d16575 100644 --- a/packages/svelte/src/lib/plugins/figure/Figure.svelte +++ b/packages/svelte/src/lib/plugins/figure/Figure.svelte @@ -1,31 +1,28 @@
- {#if selected} + {#if $selected}
updateAttributes({fit: 'contain'})} class={cn( - attrs.fit === 'contain' ? 'bg-accent' : 'bg-background', + attrs?.fit === 'contain' ? 'bg-accent' : 'bg-background', 'p-[6px] rounded-md hover:bg-secondary' )} > @@ -44,7 +41,7 @@ aria-label="Fit View" on:mousedown={() => updateAttributes({fit: 'cover'})} class={cn( - attrs.fit === 'cover' ? 'bg-accent' : 'bg-background', + attrs?.fit === 'cover' ? 'bg-accent' : 'bg-background', 'p-[6px] rounded-md hover:bg-secondary' )} > @@ -54,7 +51,7 @@ @@ -62,18 +59,20 @@
{/if} {attrs.alt} -
- {node.textContent || 'description...'} -
+