🐛 Fix text editor select all functionality and inner paste corner cases

This commit is contained in:
Elena Torro
2025-11-17 15:03:42 +01:00
parent 30413dbc66
commit 368f4cfe81
5 changed files with 272 additions and 16 deletions

View File

@@ -39,6 +39,8 @@ import {
replaceWith,
insertInto,
removeSlice,
findPreviousWordBoundary,
removeWordBackward,
} from "../content/Text.js";
import {
getTextNodeLength,
@@ -502,7 +504,51 @@ export class SelectionController extends EventTarget {
if (this.#textEditor.isEmpty) {
return this;
}
this.#selection.selectAllChildren(this.#textEditor.root);
// Find the first and last text nodes to create a proper text selection
// instead of selecting container elements
const root = this.#textEditor.root;
const firstParagraph = root.firstElementChild;
const lastParagraph = root.lastElementChild;
if (!firstParagraph || !lastParagraph) {
return this;
}
const firstTextSpan = firstParagraph.firstElementChild;
const lastTextSpan = lastParagraph.lastElementChild;
if (!firstTextSpan || !lastTextSpan) {
return this;
}
const firstTextNode = firstTextSpan.firstChild;
const lastTextNode = lastTextSpan.lastChild;
if (!firstTextNode || !lastTextNode) {
return this;
}
// Create a range from first text node to last text node
const range = document.createRange();
range.setStart(firstTextNode, 0);
range.setEnd(lastTextNode, lastTextNode.nodeValue?.length || 0);
this.#selection.removeAllRanges();
this.#selection.addRange(range);
// Ensure internal state is synchronized
this.#focusNode = this.#selection.focusNode;
this.#focusOffset = this.#selection.focusOffset;
this.#anchorNode = this.#selection.anchorNode;
this.#anchorOffset = this.#selection.anchorOffset;
this.#range = range;
this.#ranges.clear();
this.#ranges.add(range);
// Notify style changes
this.#notifyStyleChange();
return this;
}
@@ -1095,12 +1141,14 @@ export class SelectionController extends EventTarget {
this.focusParagraph.after(fragment);
mergeParagraphs(a, b);
} else {
const newParagraph = splitParagraph(
this.focusParagraph,
this.focusTextSpan,
this.focusOffset,
);
this.focusParagraph.after(fragment, newParagraph);
if (this.isTextSpanStart) {
this.focusTextSpan.before(...fragment.firstElementChild.children);
} else if (this.isTextSpanEnd) {
this.focusTextSpan.after(...fragment.firstElementChild.children);
} else {
const newTextSpan = splitTextSpan(this.focusTextSpan, this.focusOffset);
this.focusTextSpan.after(...fragment.firstElementChild.children, newTextSpan);
}
}
if (isLineBreak(collapseNode)) {
return this.collapse(collapseNode, 0);
@@ -1218,6 +1266,79 @@ export class SelectionController extends EventTarget {
return this.collapse(this.focusNode, this.focusOffset - 1);
}
/**
* Removes word backward from the current caret position.
*/
removeWordBackward() {
if (!this.isCollapsed) {
return this.removeSelected();
}
this.#textNodeIterator.currentNode = this.focusNode;
const originalNodeValue = this.focusNode.nodeValue || "";
const wordStart = findPreviousWordBoundary(originalNodeValue, this.focusOffset);
// Start node
if (wordStart === this.focusOffset && this.focusOffset === 0) {
if (this.focusTextSpan.previousElementSibling) {
const prevTextSpan = this.focusTextSpan.previousElementSibling;
const prevTextNode = prevTextSpan.lastChild;
if (prevTextNode && prevTextNode.nodeType === Node.TEXT_NODE) {
this.collapse(prevTextNode, prevTextNode.nodeValue.length);
return this.removeWordBackward();
}
} else if (this.focusParagraph.previousElementSibling) {
// Move to previous paragraph
const prevParagraph = this.focusParagraph.previousElementSibling;
const prevTextSpan = prevParagraph.lastElementChild;
const prevTextNode = prevTextSpan?.lastChild;
if (prevTextNode && prevTextNode.nodeType === Node.TEXT_NODE) {
this.collapse(prevTextNode, prevTextNode.nodeValue.length);
return this.removeWordBackward();
} else {
return this.mergeBackwardParagraph();
}
}
return this;
}
const removedData = removeWordBackward(originalNodeValue, this.focusOffset);
if (this.focusNode.nodeValue !== removedData) {
this.focusNode.nodeValue = removedData;
this.#mutations.update(this.focusTextSpan);
}
const paragraph = this.focusParagraph;
if (!paragraph) throw new Error("Cannot find paragraph");
const textSpan = this.focusTextSpan;
if (!textSpan) throw new Error("Cannot find text span");
// If the text node is empty, handle cleanup
if (this.focusNode.nodeValue === "") {
const previousTextNode = this.#textNodeIterator.previousNode();
this.focusNode.remove();
if (paragraph.children.length === 1 && textSpan.childNodes.length === 0) {
const lineBreak = createLineBreak();
textSpan.appendChild(lineBreak);
return this.collapse(lineBreak, 0);
} else if (
paragraph.children.length > 1 &&
textSpan.childNodes.length === 0
) {
textSpan.remove();
return this.collapse(
previousTextNode,
getTextNodeLength(previousTextNode),
);
}
}
return this.collapse(this.focusNode, wordStart);
}
/**
* Inserts some text in the caret position.
*