72 lines
2.7 KiB
TypeScript
72 lines
2.7 KiB
TypeScript
import type { Editor } from "@tiptap/core";
|
|
import { GapCursor } from "@tiptap/pm/gapcursor";
|
|
import type { Selection } from "@tiptap/pm/state";
|
|
import { NodeSelection, Selection as ProseMirrorSelection } from "@tiptap/pm/state";
|
|
import { IMAGE_GALLERY_NODE_TYPE } from "@edgeever/shared";
|
|
|
|
export type ResourceInsertionTarget = number | { from: number; to: number };
|
|
|
|
/**
|
|
* Capture where a pasted or dropped resource belongs before its async upload.
|
|
* A selected block node (most commonly the previously inserted image) is not
|
|
* user-selected text to replace, so the next resource belongs after it.
|
|
*/
|
|
export const getResourceInsertionTarget = (selection: Selection): ResourceInsertionTarget => {
|
|
if (selection instanceof NodeSelection) {
|
|
for (let depth = selection.$from.depth; depth > 0; depth -= 1) {
|
|
if (selection.$from.node(depth).type.name === IMAGE_GALLERY_NODE_TYPE) {
|
|
return selection.$from.after(depth);
|
|
}
|
|
}
|
|
return selection.to;
|
|
}
|
|
|
|
return { from: selection.from, to: selection.to };
|
|
};
|
|
|
|
export const clampResourceInsertionTarget = (
|
|
target: ResourceInsertionTarget,
|
|
documentSize: number,
|
|
): ResourceInsertionTarget => {
|
|
if (typeof target === "number") {
|
|
return Math.max(0, Math.min(target, documentSize));
|
|
}
|
|
|
|
const from = Math.max(0, Math.min(target.from, documentSize));
|
|
const to = Math.max(from, Math.min(target.to, documentSize));
|
|
return { from, to };
|
|
};
|
|
|
|
export const shouldSelectInsertedResources = (
|
|
interactionVersionAtRequest: number,
|
|
currentInteractionVersion: number,
|
|
) => interactionVersionAtRequest === currentInteractionVersion;
|
|
|
|
/** Clear a selected image/block when the user clicks the empty editor canvas. */
|
|
export const clearNodeSelectionAtDocumentEnd = (editor: Editor) => {
|
|
if (!(editor.state.selection instanceof NodeSelection)) {
|
|
return false;
|
|
}
|
|
|
|
const documentEnd = editor.state.doc.content.size;
|
|
const resolvedEnd = editor.state.doc.resolve(documentEnd);
|
|
// prosemirror-gapcursor exposes valid() at runtime, but its published
|
|
// declaration currently omits the static method.
|
|
const isValidGap = (GapCursor as typeof GapCursor & {
|
|
valid: (position: typeof resolvedEnd) => boolean;
|
|
}).valid(resolvedEnd);
|
|
const nextSelection = isValidGap
|
|
? new GapCursor(resolvedEnd)
|
|
: ProseMirrorSelection.atEnd(editor.state.doc);
|
|
|
|
// A document containing only a selectable block resolves atEnd() back to
|
|
// that node. GapCursor handles the normal image-only case; avoid claiming
|
|
// success for any schema where no non-node selection is available.
|
|
if (nextSelection instanceof NodeSelection) {
|
|
return false;
|
|
}
|
|
|
|
editor.view.dispatch(editor.state.tr.setSelection(nextSelection));
|
|
return true;
|
|
};
|