Compare commits

...

2 Commits

Author SHA1 Message Date
Tom Moor baf7c1a6cd Add inclusive logic for isNodeActive 2025-03-30 10:22:11 -04:00
Tom Moor f254ce0b84 fix: Paste over any inline code prevents pasting nodes
closes #8825
2025-03-29 23:24:19 -04:00
4 changed files with 56 additions and 15 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ export default class PasteHandler extends Extension {
// If the users selection is currently in a code block then paste
// as plain text, ignore all formatting and HTML content.
if (isInCode(state)) {
if (isInCode(state, { inclusive: true })) {
event.preventDefault();
view.dispatch(state.tr.insertText(text));
return true;
+17 -3
View File
@@ -7,6 +7,8 @@ type Options = {
onlyBlock?: boolean;
/** Only check if the selection is inside a code mark. */
onlyMark?: boolean;
/** If true then code must contain entire selection */
inclusive?: boolean;
};
/**
@@ -20,17 +22,29 @@ export function isInCode(state: EditorState, options?: Options): boolean {
const { nodes, marks } = state.schema;
if (!options?.onlyMark) {
if (nodes.code_block && isNodeActive(nodes.code_block)(state)) {
if (
nodes.code_block &&
isNodeActive(nodes.code_block, undefined, {
inclusive: options?.inclusive,
})(state)
) {
return true;
}
if (nodes.code_fence && isNodeActive(nodes.code_fence)(state)) {
if (
nodes.code_fence &&
isNodeActive(nodes.code_fence, undefined, {
inclusive: options?.inclusive,
})(state)
) {
return true;
}
}
if (!options?.onlyBlock) {
if (marks.code_inline) {
return isMarkActive(marks.code_inline)(state);
return isMarkActive(marks.code_inline, undefined, {
inclusive: options?.inclusive,
})(state);
}
}
+4 -1
View File
@@ -6,6 +6,8 @@ import { getMarksBetween } from "./getMarksBetween";
type Options = {
/** Only return match if the range and attrs is exact */
exact?: boolean;
/** If true then mark must contain entire selection */
inclusive?: boolean;
};
/**
@@ -40,7 +42,8 @@ export const isMarkActive =
Object.keys(attrs).every(
(key) => mark.attrs[key] === attrs[key]
)) &&
(!options?.exact || (start === from && end === to))
(!options?.exact || (start === from && end === to)) &&
(!options?.inclusive || (start <= from && end >= to))
);
}
+34 -10
View File
@@ -3,31 +3,55 @@ import { EditorState } from "prosemirror-state";
import { Primitive } from "utility-types";
import { findParentNode } from "./findParentNode";
type Options = {
/** Only return match if the range and attrs is exact */
exact?: boolean;
/** If true then node must contain entire selection */
inclusive?: boolean;
};
/**
* Checks if a node is active in the current selection or not.
*
* @param type The node type to check.
* @param attrs The attributes to check.
* @param options The options to use.
* @returns A function that checks if a node is active in the current selection or not.
*/
export const isNodeActive =
(type: NodeType, attrs: Record<string, Primitive> = {}) =>
(state: EditorState) => {
(type: NodeType, attrs?: Record<string, Primitive>, options?: Options) =>
(state: EditorState): boolean => {
if (!type) {
return false;
}
const nodeAfter = state.selection.$from.nodeAfter;
let node = nodeAfter?.type === type ? nodeAfter : undefined;
const { from, to } = state.selection;
const nodeWithPos = findParentNode(
(node) =>
node.type === type &&
(!attrs ||
Object.keys(attrs).every((key) => node.attrs[key] === attrs[key]))
)(state.selection);
if (!node) {
const parent = findParentNode((n) => n.type === type)(state.selection);
node = parent?.node;
if (!nodeWithPos) {
return false;
}
if (!Object.keys(attrs).length || !node) {
return !!node;
if (options?.inclusive) {
// Check if the node's position contains the entire selection
return (
nodeWithPos.pos <= from &&
nodeWithPos.pos + nodeWithPos.node.nodeSize >= to
);
}
return node.hasMarkup(type, { ...node.attrs, ...attrs });
if (options?.exact) {
// Check if node's range exactly matches selection
return (
nodeWithPos.pos === from &&
nodeWithPos.pos + nodeWithPos.node.nodeSize === to
);
}
return true;
};