diff --git a/.changeset/swift-keys-collect.md b/.changeset/swift-keys-collect.md new file mode 100644 index 000000000..d9a6ebf45 --- /dev/null +++ b/.changeset/swift-keys-collect.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +Improve handling of selections with `updateAttributes`. Should no longer modify parent nodes of the same type. diff --git a/packages/core/src/commands/updateAttributes.ts b/packages/core/src/commands/updateAttributes.ts index d6993b6d6..f01fb1a75 100644 --- a/packages/core/src/commands/updateAttributes.ts +++ b/packages/core/src/commands/updateAttributes.ts @@ -1,4 +1,7 @@ -import { MarkType, NodeType } from '@tiptap/pm/model' +import { + Mark, MarkType, Node, NodeType, +} from '@tiptap/pm/model' +import { SelectionRange } from '@tiptap/pm/state' import { getMarkType } from '../helpers/getMarkType.js' import { getNodeType } from '../helpers/getNodeType.js' @@ -30,6 +33,7 @@ declare module '@tiptap/core' { } export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, attributes = {}) => ({ tr, state, dispatch }) => { + let nodeType: NodeType | null = null let markType: MarkType | null = null @@ -51,24 +55,80 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at } if (dispatch) { - tr.selection.ranges.forEach(range => { + tr.selection.ranges.forEach((range: SelectionRange) => { + const from = range.$from.pos const to = range.$to.pos - state.doc.nodesBetween(from, to, (node, pos) => { - if (nodeType && nodeType === node.type) { - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, + let lastPos: number | undefined + let lastNode: Node | undefined + let trimmedFrom: number + let trimmedTo: number + + if (tr.selection.empty) { + state.doc.nodesBetween(from, to, (node: Node, pos: number) => { + + if (nodeType && nodeType === node.type) { + trimmedFrom = Math.max(pos, from) + trimmedTo = Math.min(pos + node.nodeSize, to) + lastPos = pos + lastNode = node + } + }) + } else { + state.doc.nodesBetween(from, to, (node: Node, pos: number) => { + + if (pos < from && nodeType && nodeType === node.type) { + trimmedFrom = Math.max(pos, from) + trimmedTo = Math.min(pos + node.nodeSize, to) + lastPos = pos + lastNode = node + } + + if (pos >= from && pos <= to) { + + if (nodeType && nodeType === node.type) { + tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + ...attributes, + }) + } + + if (markType && node.marks.length) { + node.marks.forEach((mark: Mark) => { + + if (markType === mark.type) { + const trimmedFrom2 = Math.max(pos, from) + const trimmedTo2 = Math.min(pos + node.nodeSize, to) + + tr.addMark( + trimmedFrom2, + trimmedTo2, + markType.create({ + ...mark.attrs, + ...attributes, + }), + ) + } + }) + } + } + }) + } + + if (lastNode) { + + if (lastPos !== undefined) { + tr.setNodeMarkup(lastPos, undefined, { + ...lastNode.attrs, ...attributes, }) } - if (markType && node.marks.length) { - node.marks.forEach(mark => { - if (markType === mark.type) { - const trimmedFrom = Math.max(pos, from) - const trimmedTo = Math.min(pos + node.nodeSize, to) + if (markType && lastNode.marks.length) { + lastNode.marks.forEach((mark: Mark) => { + if (markType === mark.type) { tr.addMark( trimmedFrom, trimmedTo, @@ -80,7 +140,7 @@ export const updateAttributes: RawCommands['updateAttributes'] = (typeOrName, at } }) } - }) + } }) }