Files
outline/shared/editor/commands/splitHeading.ts
2023-05-24 19:24:05 -07:00

95 lines
2.9 KiB
TypeScript

import { NodeType } from "prosemirror-model";
import { Command, TextSelection } from "prosemirror-state";
import { findBlockNodes } from "../queries/findChildren";
import findCollapsedNodes from "../queries/findCollapsedNodes";
export default function splitHeading(type: NodeType): Command {
return (state, dispatch): boolean => {
const { $from, from, $to, to } = state.selection;
// check we're in a matching heading node
if ($from.parent.type !== type) {
return false;
}
// is the selection at the beginning of the node
const startPos = $from.before() + 1;
if (startPos === from) {
const collapsedNodes = findCollapsedNodes(state.doc);
const allBlocks = findBlockNodes(state.doc);
const previousBlock = allBlocks
.filter((a) => a.pos + a.node.nodeSize < startPos)
.pop();
const previousBlockIsCollapsed = !!collapsedNodes.find(
(a) => a.pos === previousBlock?.pos
);
if (previousBlockIsCollapsed) {
// Insert a new heading directly before this one
const transaction = state.tr.insert(
$from.before(),
type.create({ ...$from.parent.attrs, collapsed: false })
);
// Move the selection into the new heading node and make sure it's on screen
dispatch?.(
transaction
.setSelection(
TextSelection.near(transaction.doc.resolve($from.before()))
)
.scrollIntoView()
);
return true;
}
return false;
}
// If the heading isn't collapsed standard behavior applies
if (!$from.parent.attrs.collapsed) {
return false;
}
// is the selection at the end of the node. If not standard node-splitting
// behavior applies
const endPos = $to.after() - 1;
if (endPos === to) {
// Find the next visible block after this one. It takes into account nested
// collapsed headings and reaching the end of the document
const collapsedNodes = findCollapsedNodes(state.doc);
const allBlocks = findBlockNodes(state.doc);
const visibleBlocks = allBlocks.filter(
(a) => !collapsedNodes.find((b) => b.pos === a.pos)
);
const nextVisibleBlock = visibleBlocks.find((a) => a.pos > from);
const pos = nextVisibleBlock
? nextVisibleBlock.pos
: state.doc.content.size;
// Insert a new heading directly before the next visible block
const transaction = state.tr.insert(
pos,
type.create({ ...$from.parent.attrs, collapsed: false })
);
// Move the selection into the new heading node and make sure it's on screen
dispatch?.(
transaction
.setSelection(
TextSelection.near(
transaction.doc.resolve(
Math.min(pos + 1, transaction.doc.content.size)
)
)
)
.scrollIntoView()
);
return true;
}
return false;
};
}