mirror of
https://github.com/docmost/docmost.git
synced 2026-07-15 06:56:47 +10:00
134 lines
4.0 KiB
TypeScript
134 lines
4.0 KiB
TypeScript
import { Node, mergeAttributes } from '@tiptap/core';
|
|
import { EditorState, NodeSelection, Plugin } from '@tiptap/pm/state';
|
|
|
|
export interface BaseEmbedOptions {
|
|
HTMLAttributes: Record<string, any>;
|
|
}
|
|
|
|
declare module '@tiptap/core' {
|
|
interface Commands<ReturnType> {
|
|
baseEmbed: {
|
|
insertBaseEmbed: (attrs: {
|
|
pageId: string | null;
|
|
pendingKey?: string | null;
|
|
}) => ReturnType;
|
|
};
|
|
}
|
|
}
|
|
|
|
export const BaseEmbed = Node.create<BaseEmbedOptions>({
|
|
name: 'baseEmbed',
|
|
group: 'block',
|
|
atom: true,
|
|
selectable: true,
|
|
draggable: true,
|
|
|
|
addOptions() {
|
|
return { HTMLAttributes: {} };
|
|
},
|
|
|
|
// prosemirror-dropcursor draws a block-boundary indicator on every
|
|
// `dragover` it sees. Pragmatic-dnd (used for column / choice reorder
|
|
// inside the embed) fires native `dragstart`/`dragover`, which bubble
|
|
// up to the editor and trigger dropcursor — visible as a stray blue
|
|
// line above or below the embed during an internal drag. The cursor
|
|
// event lands over the atom node, so dropcursor consults
|
|
// `disableDropCursor` on this node spec; returning true suppresses
|
|
// the indicator while still letting pragmatic-dnd handle the drag.
|
|
extendNodeSchema(extension) {
|
|
return extension.name === 'baseEmbed'
|
|
? { disableDropCursor: true }
|
|
: {};
|
|
},
|
|
|
|
addAttributes() {
|
|
return {
|
|
pageId: {
|
|
default: null,
|
|
parseHTML: (el) => el.getAttribute('data-page-id'),
|
|
renderHTML: (attrs) =>
|
|
attrs.pageId ? { 'data-page-id': attrs.pageId } : {},
|
|
},
|
|
// Transient marker set when the slash command inserts the embed
|
|
// before the server has assigned a pageId. The view renders a
|
|
// skeleton in this state. Cleared once the API responds and the
|
|
// real pageId is patched in. Not serialized — embeds saved with
|
|
// a pendingKey would orphan if the page were closed mid-request.
|
|
pendingKey: {
|
|
default: null,
|
|
parseHTML: () => null,
|
|
renderHTML: () => ({}),
|
|
},
|
|
};
|
|
},
|
|
|
|
parseHTML() {
|
|
return [{ tag: 'div[data-type="base-embed"]' }];
|
|
},
|
|
|
|
renderHTML({ HTMLAttributes }) {
|
|
return [
|
|
'div',
|
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
'data-type': 'base-embed',
|
|
}),
|
|
];
|
|
},
|
|
|
|
addCommands() {
|
|
return {
|
|
insertBaseEmbed:
|
|
(attrs) =>
|
|
({ commands }) =>
|
|
commands.insertContent({
|
|
type: this.name,
|
|
attrs,
|
|
}),
|
|
};
|
|
},
|
|
|
|
addKeyboardShortcuts() {
|
|
// Block Backspace / Delete when the base embed itself is the
|
|
// current selection — the "click on the embed and hit delete"
|
|
// accidental-delete path. Returning true tells TipTap we've
|
|
// handled the key, preventing the default removal. Range
|
|
// selections covering the node and programmatic deletes still
|
|
// work normally.
|
|
const isThisNodeSelected = (): boolean => {
|
|
const { selection } = this.editor.state;
|
|
return (
|
|
selection instanceof NodeSelection &&
|
|
selection.node.type.name === this.name
|
|
);
|
|
};
|
|
return {
|
|
Backspace: () => isThisNodeSelected(),
|
|
Delete: () => isThisNodeSelected(),
|
|
};
|
|
},
|
|
|
|
addProseMirrorPlugins() {
|
|
// Same idea as the Backspace/Delete shortcuts above, but for the
|
|
// other accidental-delete path: when the embed is the selection,
|
|
// a typed character or paste would replace the whole node. These
|
|
// hooks return true (handled, no-op) so the node stays put. The
|
|
// user can still press an arrow key to deselect and then type.
|
|
const nodeName = this.name;
|
|
const isThisNodeSelected = (state: EditorState): boolean => {
|
|
const { selection } = state;
|
|
return (
|
|
selection instanceof NodeSelection &&
|
|
selection.node.type.name === nodeName
|
|
);
|
|
};
|
|
return [
|
|
new Plugin({
|
|
props: {
|
|
handleTextInput: (view) => isThisNodeSelected(view.state),
|
|
handlePaste: (view) => isThisNodeSelected(view.state),
|
|
},
|
|
}),
|
|
];
|
|
},
|
|
});
|