tiptap: ensure only one title node, do not create paragraph if next sibling is exists

This commit is contained in:
fantasticit 2022-08-19 10:36:05 +08:00
parent 1b99a0876b
commit a7e787d758
1 changed files with 63 additions and 33 deletions

View File

@ -2,7 +2,7 @@ import { mergeAttributes, Node } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react'; import { ReactNodeViewRenderer } from '@tiptap/react';
import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'; import { Plugin, PluginKey, TextSelection } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view'; import { Decoration, DecorationSet } from 'prosemirror-view';
import { getDatasetAttribute, isInTitle, nodeAttrsToDataset } from 'tiptap/prose-utils'; import { getDatasetAttribute, getNodeAtPos, isInTitle, nodeAttrsToDataset } from 'tiptap/prose-utils';
import { TitleWrapper } from '../wrappers/title'; import { TitleWrapper } from '../wrappers/title';
@ -78,15 +78,29 @@ export const Title = Node.create<TitleOptions>({
addProseMirrorPlugins() { addProseMirrorPlugins() {
const { editor } = this; const { editor } = this;
let shouldSelectTitleNode = true; let shouldSelectTitleNode = true;
let editorView;
const closeSelectTitleNode = () => {
shouldSelectTitleNode = false;
return;
};
return [ return [
new Plugin({ new Plugin({
key: TitlePluginKey, key: TitlePluginKey,
view: (view) => {
editorView = view;
return {
update(view) {
editorView = view;
},
};
},
props: { props: {
decorations: (state) => { decorations: (state) => {
const { doc } = state; const { doc } = state;
const decorations = []; const decorations = [];
doc.descendants((node, pos) => { doc.descendants((node, pos) => {
if (node.type.name !== this.name) return; if (node.type.name !== this.name) return;
@ -96,34 +110,22 @@ export const Title = Node.create<TitleOptions>({
}) })
); );
}); });
return DecorationSet.create(doc, decorations); return DecorationSet.create(doc, decorations);
}, },
handleClick() { handleClick() {
shouldSelectTitleNode = false; closeSelectTitleNode();
return; return;
}, },
handleDOMEvents: { handleDOMEvents: {
click() { click: closeSelectTitleNode,
shouldSelectTitleNode = false; mousedown: closeSelectTitleNode,
return; pointerdown: closeSelectTitleNode,
}, touchstart: closeSelectTitleNode,
mousedown() {
shouldSelectTitleNode = false;
return;
},
pointerdown() {
shouldSelectTitleNode = false;
return;
},
touchstart() {
shouldSelectTitleNode = false;
return;
},
}, },
handleKeyDown(view, evt) { handleKeyDown(view, evt) {
const { state, dispatch } = view; const { state, dispatch } = view;
shouldSelectTitleNode = false;
closeSelectTitleNode();
if (isInTitle(view.state) && evt.code === 'Enter') { if (isInTitle(view.state) && evt.code === 'Enter') {
evt.preventDefault(); evt.preventDefault();
@ -136,10 +138,13 @@ export const Title = Node.create<TitleOptions>({
const $head = state.selection.$head; const $head = state.selection.$head;
const titleNode = $head.node($head.depth); const titleNode = $head.node($head.depth);
const endPos = ((titleNode.firstChild && titleNode.firstChild.nodeSize) || 0) + 1; const endPos = ((titleNode.firstChild && titleNode.firstChild.nodeSize) || 0) + 1;
const nextNode = getNodeAtPos(state, endPos + 2);
if (!nextNode) {
dispatch(state.tr.insert(endPos, paragraph.create())); dispatch(state.tr.insert(endPos, paragraph.create()));
}
const newState = view.state; const newState = view.state;
const next = new TextSelection(newState.doc.resolve(endPos + 2)); const next = new TextSelection(newState.doc.resolve(endPos + 2));
@ -152,20 +157,45 @@ export const Title = Node.create<TitleOptions>({
appendTransaction: (transactions, oldState, newState) => { appendTransaction: (transactions, oldState, newState) => {
if (!editor.isEditable) return; if (!editor.isEditable) return;
if (!shouldSelectTitleNode) return;
const tr = newState.tr; const tr = newState.tr;
let shouldReturnTr = false;
if (shouldSelectTitleNode) {
const firstNode = newState?.doc?.content?.content?.[0]; const firstNode = newState?.doc?.content?.content?.[0];
if (firstNode && firstNode.type.name === this.name && firstNode.nodeSize === 2) { if (firstNode && firstNode.type.name === this.name && firstNode.nodeSize === 2) {
const selection = new TextSelection(newState.tr.doc.resolve(firstNode?.attrs?.cover ? 1 : 0)); const selection = new TextSelection(newState.tr.doc.resolve(firstNode?.attrs?.cover ? 1 : 0));
tr.setSelection(selection).scrollIntoView(); tr.setSelection(selection).scrollIntoView();
tr.setMeta('addToHistory', false); tr.setMeta('addToHistory', false);
return tr; shouldReturnTr = true;
}
} }
return; const newTitleNodes = (newState.tr.doc.content.content || []).filter((item) => item.type.name === this.name);
if (newTitleNodes.length > 1) {
const oldTitleNode = (oldState.tr.doc.content.content || []).filter((item) => item.type.name === this.name);
const otherNewNodes = (newState.tr.doc.content.content || []).filter(
(item) => item.type.name !== this.name
);
const fixedDoc = {
...newState.tr.doc.toJSON(),
content: [].concat(
((oldTitleNode && oldTitleNode[0]) || newTitleNodes[0]).toJSON(),
otherNewNodes.map((node) => node.toJSON())
),
};
tr.replaceWith(0, newState.doc.content.size, newState.schema.nodeFromJSON(fixedDoc));
if (tr.docChanged) {
shouldReturnTr = true;
tr.setMeta('addToHistory', false);
}
}
return shouldReturnTr ? tr : undefined;
}, },
}), }),
]; ];