Files
docmost/packages/editor-ext/src/lib/trailing-node.ts
T
2026-08-11 01:30:26 +01:00

95 lines
2.8 KiB
TypeScript

import { Extension } from '@tiptap/core'
import { PluginKey, Plugin } from '@tiptap/pm/state';
export interface TrailingNodeExtensionOptions {
node: string,
notAfter: string[],
}
function nodeEqualsType({ types, node }: { types: any, node: any }) {
if (!node) return false
return (Array.isArray(types) && types.includes(node.type)) || node.type === types
}
// footnotes must stay the last doc child, so the trailing node goes before it
function lastNodeBeforeFootnotes(doc: any) {
const lastChild = doc.lastChild
if (lastChild?.type.name === 'footnotes') {
return doc.childCount > 1 ? doc.child(doc.childCount - 2) : null
}
return lastChild
}
// @ts-ignore
/**
* Extension based on:
* - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js
* - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts
*/
export const TrailingNode = Extension.create<TrailingNodeExtensionOptions>({
name: 'trailingNode',
addOptions() {
return {
node: 'paragraph',
notAfter: [
'paragraph',
],
};
},
addProseMirrorPlugins() {
const plugin = new PluginKey(this.name)
const disabledNodes = Object.entries(this.editor.schema.nodes)
.map(([, value]) => value)
.filter(node => this.options.notAfter.includes(node.name))
return [
new Plugin({
key: plugin,
appendTransaction: (_, __, state) => {
const { doc, tr, schema } = state;
const shouldInsertNodeAtEnd = plugin.getState(state);
const type = schema.nodes[this.options.node]
if (!shouldInsertNodeAtEnd) {
return;
}
const lastChild = doc.lastChild
const endPosition = lastChild?.type.name === 'footnotes'
? doc.content.size - lastChild.nodeSize
: doc.content.size
return tr.insert(endPosition, type.create());
},
state: {
init: (_, state) => {
try {
const lastNode = lastNodeBeforeFootnotes(state.tr.doc)
return !nodeEqualsType({ node: lastNode, types: disabledNodes })
} catch (err){
console.log(err)
}
return true;
},
apply: (tr, value) => {
if (!tr.docChanged) {
return value
}
// Ignore transactions from UniqueID extension to prevent infinite loops
// when UniqueID adds IDs to newly inserted trailing nodes
if (tr.getMeta('__uniqueIDTransaction')) {
return value
}
const lastNode = lastNodeBeforeFootnotes(tr.doc)
return !nodeEqualsType({ node: lastNode, types: disabledNodes })
},
},
}),
]
}
})