diff --git a/packages/client/src/tiptap/core/extensions/scroll-into-view.ts b/packages/client/src/tiptap/core/extensions/scroll-into-view.ts index 4991ebc8..28f2cb0d 100644 --- a/packages/client/src/tiptap/core/extensions/scroll-into-view.ts +++ b/packages/client/src/tiptap/core/extensions/scroll-into-view.ts @@ -1,4 +1,5 @@ import { Editor, Extension } from '@tiptap/core'; +import { throttle } from 'helpers/throttle'; import { Plugin, PluginKey, Transaction } from 'prosemirror-state'; export const scrollIntoViewPluginKey = new PluginKey('scrollIntoViewPlugin'); @@ -8,7 +9,7 @@ type TransactionWithScroll = Transaction & { scrolledIntoView: boolean }; interface IScrollIntoViewOptions { /** * - * 将 markdown 转换为 html + * 滚动编辑器 */ onScroll: (editor: Editor) => void; } @@ -24,6 +25,9 @@ export const ScrollIntoView = Extension.create({ addProseMirrorPlugins() { const { editor } = this; + + const onScroll = this.options.onScroll ? throttle(this.options.onScroll, 200) : (editor) => {}; + return [ new Plugin({ key: scrollIntoViewPluginKey, @@ -38,7 +42,7 @@ export const ScrollIntoView = Extension.create({ tr.getMeta('scrollIntoView') !== false && tr.getMeta('addToHistory') !== false ) { - this.options.onScroll(editor); + onScroll(editor); return newState.tr.scrollIntoView(); } }, diff --git a/packages/client/src/tiptap/core/extensions/title.ts b/packages/client/src/tiptap/core/extensions/title.ts index 02c75b47..a92aaea7 100644 --- a/packages/client/src/tiptap/core/extensions/title.ts +++ b/packages/client/src/tiptap/core/extensions/title.ts @@ -2,7 +2,7 @@ import { mergeAttributes, Node } from '@tiptap/core'; import { ReactNodeViewRenderer } from '@tiptap/react'; import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'; import { Decoration, DecorationSet } from 'prosemirror-view'; -import { getDatasetAttribute, isInTitle } from 'tiptap/prose-utils'; +import { getDatasetAttribute, isInTitle, nodeAttrsToDataset } from 'tiptap/prose-utils'; import { TitleWrapper } from '../wrappers/title'; @@ -21,11 +21,15 @@ declare module '@tiptap/core' { export const TitleExtensionName = 'title'; +const TitlePluginKey = new PluginKey(TitleExtensionName); + export const Title = Node.create({ name: TitleExtensionName, content: 'inline*', group: 'block', - selectable: true, + defining: true, + isolating: true, + showGapCursor: true, addOptions() { return { @@ -52,8 +56,19 @@ export const Title = Node.create({ ]; }, - renderHTML({ HTMLAttributes }) { - return ['h1', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + renderHTML({ HTMLAttributes, node }) { + const { cover } = node.attrs; + return [ + 'h1', + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, nodeAttrsToDataset(node)), + [ + 'img', + { + src: cover, + }, + ], + ['div', 0], + ]; }, addNodeView() { @@ -62,38 +77,11 @@ export const Title = Node.create({ addProseMirrorPlugins() { const { editor } = this; + let shouldSelectTitleNode = true; return [ new Plugin({ - key: new PluginKey(this.name), - props: { - handleKeyDown(view, evt) { - const { state, dispatch } = view; - - if (isInTitle(view.state) && evt.code === 'Enter') { - evt.preventDefault(); - - const paragraph = state.schema.nodes.paragraph; - - if (!paragraph) { - return; - } - - const $head = state.selection.$head; - const titleNode = $head.node($head.depth); - const endPos = ((titleNode.firstChild && titleNode.firstChild.nodeSize) || 0) + 1; - - dispatch(state.tr.insert(endPos, paragraph.create())); - - const newState = view.state; - const next = new TextSelection(newState.doc.resolve(endPos + 2)); - dispatch(newState.tr.setSelection(next)); - return true; - } - }, - }, - }), - new Plugin({ + key: TitlePluginKey, props: { decorations: (state) => { const { doc } = state; @@ -111,6 +99,73 @@ export const Title = Node.create({ return DecorationSet.create(doc, decorations); }, + handleClick() { + shouldSelectTitleNode = false; + return; + }, + handleDOMEvents: { + click() { + shouldSelectTitleNode = false; + return; + }, + mousedown() { + shouldSelectTitleNode = false; + return; + }, + pointerdown() { + shouldSelectTitleNode = false; + return; + }, + touchstart() { + shouldSelectTitleNode = false; + return; + }, + }, + handleKeyDown(view, evt) { + const { state, dispatch } = view; + shouldSelectTitleNode = false; + + if (isInTitle(view.state) && evt.code === 'Enter') { + evt.preventDefault(); + + const paragraph = state.schema.nodes.paragraph; + + if (!paragraph) { + return true; + } + + const $head = state.selection.$head; + const titleNode = $head.node($head.depth); + + const endPos = ((titleNode.firstChild && titleNode.firstChild.nodeSize) || 0) + 1; + + dispatch(state.tr.insert(endPos, paragraph.create())); + + const newState = view.state; + const next = new TextSelection(newState.doc.resolve(endPos + 2)); + dispatch(newState.tr.setSelection(next)); + + return true; + } + }, + }, + appendTransaction: (transactions, oldState, newState) => { + if (!editor.isEditable) return; + + if (!shouldSelectTitleNode) return; + + const tr = newState.tr; + + const firstNode = newState?.doc?.content?.content?.[0]; + + if (firstNode && firstNode.type.name === this.name && firstNode.nodeSize === 2) { + const selection = new TextSelection(newState.tr.doc.resolve(firstNode?.attrs?.cover ? 1 : 0)); + tr.setSelection(selection).scrollIntoView(); + tr.setMeta('addToHistory', false); + return tr; + } + + return; }, }), ]; diff --git a/packages/client/src/tiptap/core/index.tsx b/packages/client/src/tiptap/core/index.tsx index ec6459cd..538ea1c1 100644 --- a/packages/client/src/tiptap/core/index.tsx +++ b/packages/client/src/tiptap/core/index.tsx @@ -2,6 +2,7 @@ import { EditorOptions } from '@tiptap/core'; import { Editor as BuiltInEditor } from '@tiptap/react'; import { EditorContent, NodeViewContent, NodeViewWrapper } from '@tiptap/react'; import { EventEmitter } from 'helpers/event-emitter'; +import { throttle } from 'helpers/throttle'; import { DependencyList, useEffect, useState } from 'react'; function useForceUpdate() { @@ -36,6 +37,15 @@ export const useEditor = (options: Partial = {}, deps: Dependency setEditor(instance); + if (options.editable) { + instance.on( + 'update', + throttle(() => { + instance.chain().focus().scrollIntoView().run(); + }, 200) + ); + } + instance.on('transaction', () => { requestAnimationFrame(() => { requestAnimationFrame(() => { diff --git a/packages/client/src/tiptap/editor/collaboration/collaboration/index.module.scss b/packages/client/src/tiptap/editor/collaboration/collaboration/index.module.scss index 1d46b9d0..1da0c864 100644 --- a/packages/client/src/tiptap/editor/collaboration/collaboration/index.module.scss +++ b/packages/client/src/tiptap/editor/collaboration/collaboration/index.module.scss @@ -50,6 +50,7 @@ flex: 1; justify-content: center; flex-wrap: nowrap; + scroll-behavior: smooth; .contentWrap { width: 100%; diff --git a/packages/client/src/tiptap/editor/collaboration/kit.ts b/packages/client/src/tiptap/editor/collaboration/kit.ts index fa761d43..1c236848 100644 --- a/packages/client/src/tiptap/editor/collaboration/kit.ts +++ b/packages/client/src/tiptap/editor/collaboration/kit.ts @@ -1,4 +1,5 @@ import { Toast } from '@douyinfe/semi-ui'; +import scrollIntoView from 'scroll-into-view-if-needed'; // 自定义节点扩展 import { Attachment } from 'tiptap/core/extensions/attachment'; import { BackgroundColor } from 'tiptap/core/extensions/background-color'; diff --git a/packages/client/src/tiptap/prose-utils/node.ts b/packages/client/src/tiptap/prose-utils/node.ts index 7400f8d6..6dc8e975 100644 --- a/packages/client/src/tiptap/prose-utils/node.ts +++ b/packages/client/src/tiptap/prose-utils/node.ts @@ -61,6 +61,7 @@ export function isInCodeBlock(state: EditorState): boolean { } export function isInTitle(state: EditorState): boolean { + if (state?.selection?.$head?.pos === 0) return true; return isInCustomNode(state, 'title'); } diff --git a/packages/constants/lib/index.js b/packages/constants/lib/index.js index c069d354..eafc1d0d 100644 --- a/packages/constants/lib/index.js +++ b/packages/constants/lib/index.js @@ -16,17 +16,8 @@ exports.EMPTY_DOCUMNENT = { content: JSON.stringify({ "default": { type: 'doc', - content: [{ type: 'title', content: [{ type: 'text', text: '未命名文档' }] }] + content: [{ type: 'title', content: [{ type: 'text', text: '' }] }] } }), - state: Buffer.from(new Uint8Array([ - 1, 14, 204, 224, 154, 225, 13, 0, 7, 1, 7, 100, 101, 102, 97, 117, 108, 116, 3, 5, 116, 105, 116, 108, 101, 1, 0, - 204, 224, 154, 225, 13, 0, 1, 0, 1, 135, 204, 224, 154, 225, 13, 0, 3, 9, 112, 97, 114, 97, 103, 114, 97, 112, - 104, 40, 0, 204, 224, 154, 225, 13, 3, 6, 105, 110, 100, 101, 110, 116, 1, 125, 0, 40, 0, 204, 224, 154, 225, 13, - 3, 9, 116, 101, 120, 116, 65, 108, 105, 103, 110, 1, 119, 4, 108, 101, 102, 116, 0, 4, 71, 204, 224, 154, 225, 13, - 1, 6, 1, 0, 204, 224, 154, 225, 13, 10, 3, 132, 204, 224, 154, 225, 13, 13, 3, 230, 156, 170, 129, 204, 224, 154, - 225, 13, 14, 6, 132, 204, 224, 154, 225, 13, 20, 6, 229, 145, 189, 229, 144, 141, 129, 204, 224, 154, 225, 13, 22, - 5, 132, 204, 224, 154, 225, 13, 27, 6, 230, 150, 135, 230, 161, 163, 1, 204, 224, 154, 225, 13, 5, 1, 2, 6, 4, 11, - 3, 15, 6, 23, 5, - ])) + state: Buffer.from(new Uint8Array([])) }; diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 40639593..e68afae3 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -17,19 +17,8 @@ export const EMPTY_DOCUMNENT = { content: JSON.stringify({ default: { type: 'doc', - content: [{ type: 'title', content: [{ type: 'text', text: '未命名文档' }] }], + content: [{ type: 'title', content: [{ type: 'text', text: '' }] }], }, }), - state: Buffer.from( - new Uint8Array([ - 1, 14, 204, 224, 154, 225, 13, 0, 7, 1, 7, 100, 101, 102, 97, 117, 108, 116, 3, 5, 116, 105, 116, 108, 101, 1, 0, - 204, 224, 154, 225, 13, 0, 1, 0, 1, 135, 204, 224, 154, 225, 13, 0, 3, 9, 112, 97, 114, 97, 103, 114, 97, 112, - 104, 40, 0, 204, 224, 154, 225, 13, 3, 6, 105, 110, 100, 101, 110, 116, 1, 125, 0, 40, 0, 204, 224, 154, 225, 13, - 3, 9, 116, 101, 120, 116, 65, 108, 105, 103, 110, 1, 119, 4, 108, 101, 102, 116, 0, 4, 71, 204, 224, 154, 225, 13, - 1, 6, 1, 0, 204, 224, 154, 225, 13, 10, 3, 132, 204, 224, 154, 225, 13, 13, 3, 230, 156, 170, 129, 204, 224, 154, - 225, 13, 14, 6, 132, 204, 224, 154, 225, 13, 20, 6, 229, 145, 189, 229, 144, 141, 129, 204, 224, 154, 225, 13, 22, - 5, 132, 204, 224, 154, 225, 13, 27, 6, 230, 150, 135, 230, 161, 163, 1, 204, 224, 154, 225, 13, 5, 1, 2, 6, 4, 11, - 3, 15, 6, 23, 5, - ]) - ), + state: Buffer.from(new Uint8Array([])), };