From 6edc29dce213c83425b6c645583a1dfbb50f3482 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 28 Oct 2025 10:59:49 +0100 Subject: [PATCH] :bug: Fix text selection --- frontend/text-editor/package.json | 1 + .../editor/controllers/SelectionController.js | 74 +++++++++++++++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/frontend/text-editor/package.json b/frontend/text-editor/package.json index b31fc91b7f..40ffaf6472 100644 --- a/frontend/text-editor/package.json +++ b/frontend/text-editor/package.json @@ -9,6 +9,7 @@ "coverage": "vitest run --coverage", "wasm:update": "cp ../resources/public/js/render_wasm.wasm ./src/wasm/render_wasm.wasm && cp ../resources/public/js/render_wasm.js ./src/wasm/render_wasm.js", "test": "vitest --run", + "fmt:js": "yarn run prettier -c src/**/*.js", "test:watch": "vitest", "test:watch:ui": "vitest --ui", "test:watch:e2e": "vitest --browser" diff --git a/frontend/text-editor/src/editor/controllers/SelectionController.js b/frontend/text-editor/src/editor/controllers/SelectionController.js index 7c2ff0edd8..4dc8979542 100644 --- a/frontend/text-editor/src/editor/controllers/SelectionController.js +++ b/frontend/text-editor/src/editor/controllers/SelectionController.js @@ -1263,6 +1263,7 @@ export class SelectionController extends EventTarget { replaceText(newText) { const startOffset = Math.min(this.anchorOffset, this.focusOffset); const endOffset = Math.max(this.anchorOffset, this.focusOffset); + if (this.isTextFocus) { this.focusNode.nodeValue = replaceWith( this.focusNode.nodeValue, @@ -1279,7 +1280,11 @@ export class SelectionController extends EventTarget { this.focusNode.replaceChildren(newParagraph); return this.collapse(newTextNode, newText.length + 1); } else { - throw new Error("Unknown node type"); + const newTextNode = new Text(newText); + const newTextSpan = createTextSpan(newTextNode, this.#currentStyle); + const newParagraph = createParagraph([newTextSpan], this.#currentStyle); + this.#textEditor.root.replaceChildren(newParagraph); + return this.collapse(newTextNode, newText.length + 1); } this.#mutations.update(this.focusTextSpan); return this.collapse(this.focusNode, startOffset + newText.length); @@ -1538,15 +1543,15 @@ export class SelectionController extends EventTarget { const affectedTextSpans = new Set(); const affectedParagraphs = new Set(); - const startNode = getClosestTextNode(this.#range.startContainer); - const endNode = getClosestTextNode(this.#range.endContainer); - - const startOffset = this.#range.startOffset; - const endOffset = this.#range.endOffset; - let previousNode = null; let nextNode = null; + let { startNode, endNode, startOffset, endOffset } = this.getRanges(); + + if (this.shouldHandleCompleteDeletion(startNode, endNode)) { + return this.handleCompleteContentDeletion(); + } + // This is the simplest case, when the startNode and // the endNode are the same and they're textNodes. if (startNode === endNode) { @@ -1612,7 +1617,8 @@ export class SelectionController extends EventTarget { } else if (currentNode === endNode) { if ( isLineBreak(endNode) || - (isTextNode(endNode) && endOffset === (endNode.nodeValue?.length || 0)) + (isTextNode(endNode) && + endOffset === (endNode.nodeValue?.length || 0)) ) { // We should remove this node completely. shouldRemoveNodeCompletely = true; @@ -1694,6 +1700,58 @@ export class SelectionController extends EventTarget { return this.collapse(startNode, startOffset); } + getRanges() { + let startNode = getClosestTextNode(this.#range.startContainer); + let endNode = getClosestTextNode(this.#range.endContainer); + + let startOffset = this.#range.startOffset; + let endOffset = this.#range.startOffset + this.#range.toString().length; + + return { startNode, endNode, startOffset, endOffset }; + } + + shouldHandleCompleteDeletion(startNode, endNode) { + const root = this.#textEditor.root; + return ( + (startNode && + endNode && + this.#range.toString() === (root.textContent || "") && + root.textContent.length > 0) || + !startNode || + !endNode + ); + } + + /** + * Handles complete content deletion when all content is selected. + * @returns {SelectionController} + */ + handleCompleteContentDeletion() { + const root = this.#textEditor.root; + const firstParagraph = root.firstElementChild; + + const newTextSpan = createEmptyTextSpan(this.#currentStyle); + if (!newTextSpan.firstChild) { + newTextSpan.appendChild(new Text("")); + } + + if (firstParagraph) { + firstParagraph.replaceChildren(newTextSpan); + let currentParagraph = firstParagraph.nextElementSibling; + while (currentParagraph) { + const nextParagraph = currentParagraph.nextElementSibling; + currentParagraph.remove(); + currentParagraph = nextParagraph; + } + } else { + const newParagraph = createEmptyParagraph(); + newParagraph.appendChild(newTextSpan); + root.replaceChildren(newParagraph); + } + + return this.collapse(newTextSpan.firstChild, 0); + } + /** * Applies styles from the startNode to the endNode. *