From af5b942e0538dbb1df7d55e1086299cf59369c6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 23 Jul 2025 13:15:15 +0200 Subject: [PATCH] :bug: Fix copy/paste not working on follow up pastes --- frontend/src/app/plugins/fonts.cljs | 2 +- frontend/src/app/render_wasm/api/fonts.cljs | 4 +- frontend/text-editor/src/editor/TextEditor.js | 4 +- .../src/editor/content/dom/Content.js | 2 + .../src/editor/content/dom/Style.js | 23 ++- .../editor/controllers/SelectionController.js | 150 ++++++++++-------- frontend/text-editor/src/main.js | 24 +-- 7 files changed, 121 insertions(+), 88 deletions(-) diff --git a/frontend/src/app/plugins/fonts.cljs b/frontend/src/app/plugins/fonts.cljs index b36c7731dd..77602816f6 100644 --- a/frontend/src/app/plugins/fonts.cljs +++ b/frontend/src/app/plugins/fonts.cljs @@ -66,7 +66,7 @@ :font-family family :font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant)) :font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant)) - :font-weight (d/nilv (obj/get variant "fontWeight") (:wegith default-variant))}] + :font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}] (st/emit! (dwt/update-attrs id values))))) :applyToRange diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs index 0872039b73..1ebef59ab3 100644 --- a/frontend/src/app/render_wasm/api/fonts.cljs +++ b/frontend/src/app/render_wasm/api/fonts.cljs @@ -224,8 +224,8 @@ (defn add-emoji-font [fonts] - (conj fonts {:font-id "gfont-noto-color-emoji" - :font-variant-id "regular" + (conj fonts {:font-id " gfont-noto-color-emoji " + :font-variant-id " regular " :style 0 :weight 400 :is-emoji true})) diff --git a/frontend/text-editor/src/editor/TextEditor.js b/frontend/text-editor/src/editor/TextEditor.js index 0d37fb9eea..09fddb1d59 100644 --- a/frontend/text-editor/src/editor/TextEditor.js +++ b/frontend/text-editor/src/editor/TextEditor.js @@ -16,6 +16,7 @@ import { mapContentFragmentFromHTML, mapContentFragmentFromString, } from "./content/dom/Content.js"; +import { resetInertElement } from "./content/dom/Style.js"; import { createRoot, createEmptyRoot } from "./content/dom/Root.js"; import { createParagraph } from "./content/dom/Paragraph.js"; import { createEmptyInline, createInline } from "./content/dom/Inline.js"; @@ -264,7 +265,7 @@ export class TextEditor extends EventTarget { #onCopy = (e) => { this.dispatchEvent( new CustomEvent("clipboardchange", { - detail: this.#selectionController.currentStyle, + detail: this.currentStyle, }), ); @@ -520,6 +521,7 @@ export function createRootFromHTML(html, style) { const fragment = mapContentFragmentFromHTML(html, style); const root = createRoot([], style); root.replaceChildren(fragment); + resetInertElement(); return root; } diff --git a/frontend/text-editor/src/editor/content/dom/Content.js b/frontend/text-editor/src/editor/content/dom/Content.js index d95d9e2a71..8b3f1e8e69 100644 --- a/frontend/text-editor/src/editor/content/dom/Content.js +++ b/frontend/text-editor/src/editor/content/dom/Content.js @@ -75,6 +75,8 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) { if (!fontSize) console.warn("font-size", fontSize); const fontFamily = inline.style.getPropertyValue("font-family"); if (!fontFamily) console.warn("font-family", fontFamily); + const fontWeight = inline.style.getPropertyValue("font-weight"); + if (!fontWeight) console.warn("font-weight", fontWeight); currentParagraph.appendChild(inline); currentNode = nodeIterator.nextNode(); diff --git a/frontend/text-editor/src/editor/content/dom/Style.js b/frontend/text-editor/src/editor/content/dom/Style.js index 19a4a6a977..7b6bca9162 100644 --- a/frontend/text-editor/src/editor/content/dom/Style.js +++ b/frontend/text-editor/src/editor/content/dom/Style.js @@ -10,7 +10,7 @@ import { getFills } from "./Color.js"; const DEFAULT_FONT_SIZE = "16px"; const DEFAULT_LINE_HEIGHT = "1.2"; - +const DEFAULT_FONT_WEIGHT = "400"; /** * Merges two style declarations. `source` -> `target`. * @@ -55,7 +55,7 @@ let inertElement = null; * Resets the style declaration of the inert * element. */ -function resetInertElement() { +export function resetInertElement() { if (!inertElement) throw new Error("Invalid inert element"); resetStyleDeclaration(inertElement.style); return inertElement; @@ -94,11 +94,21 @@ function getStyleDefaultsDeclaration() { * @returns {CSSStyleDeclaration} */ export function getComputedStyle(element) { + if (typeof window !== "undefined" && window.getComputedStyle) { + const inertElement = getInertElement(); + const computedStyle = window.getComputedStyle(element); + inertElement.style = computedStyle; + + return inertElement.style; + } + return getComputedStylePolyfill(element); +} + +export function getComputedStylePolyfill(element) { const inertElement = getInertElement(); + let currentElement = element; while (currentElement) { - // This is better but it doesn't work in JSDOM. - // for (const styleName of currentElement.style) { for (let index = 0; index < currentElement.style.length; index++) { const styleName = currentElement.style.item(index); const currentValue = inertElement.style.getPropertyValue(styleName); @@ -159,6 +169,11 @@ export function normalizeStyles( styleDeclaration.setProperty("font-size", DEFAULT_FONT_SIZE); } + const fontWeight = styleDeclaration.getPropertyValue("font-weight"); + if (!fontWeight || fontWeight === "0") { + styleDeclaration.setProperty("font-weight", DEFAULT_FONT_WEIGHT); + } + const lineHeight = styleDeclaration.getPropertyValue("line-height"); if (!lineHeight || lineHeight === "" || !lineHeight.endsWith("px")) { // TODO: PodrĂ­amos convertir unidades en decimales. diff --git a/frontend/text-editor/src/editor/controllers/SelectionController.js b/frontend/text-editor/src/editor/controllers/SelectionController.js index dd01048552..695f8d789c 100644 --- a/frontend/text-editor/src/editor/controllers/SelectionController.js +++ b/frontend/text-editor/src/editor/controllers/SelectionController.js @@ -39,7 +39,11 @@ import { insertInto, removeSlice, } from "../content/Text.js"; -import { getTextNodeLength, getClosestTextNode, isTextNode } from "../content/dom/TextNode.js"; +import { + getTextNodeLength, + getClosestTextNode, + isTextNode, +} from "../content/dom/TextNode.js"; import TextNodeIterator from "../content/dom/TextNodeIterator.js"; import TextEditor from "../TextEditor.js"; import CommandMutations from "../commands/CommandMutations.js"; @@ -240,7 +244,7 @@ export class SelectionController extends EventTarget { for (const [name, value] of Object.entries(this.#styleDefaults)) { this.#currentStyle.setProperty( name, - value + (name === "font-size" ? "px" : "") + value + (name === "font-size" ? "px" : ""), ); } } @@ -356,10 +360,11 @@ export class SelectionController extends EventTarget { this.dispatchEvent( new CustomEvent("stylechange", { detail: this.#currentStyle, - }) + }), ); } else { - const firstInline = this.#textEditor.root?.firstElementChild?.firstElementChild; + const firstInline = + this.#textEditor.root?.firstElementChild?.firstElementChild; if (firstInline) { this.#updateCurrentStyle(firstInline); this.dispatchEvent( @@ -452,13 +457,16 @@ export class SelectionController extends EventTarget { if (this.#savedSelection.anchorNode && this.#savedSelection.focusNode) { if (this.#savedSelection.anchorNode === this.#savedSelection.focusNode) { - this.#selection.setPosition(this.#savedSelection.focusNode, this.#savedSelection.focusOffset); + this.#selection.setPosition( + this.#savedSelection.focusNode, + this.#savedSelection.focusOffset, + ); } else { this.#selection.setBaseAndExtent( this.#savedSelection.anchorNode, this.#savedSelection.anchorOffset, this.#savedSelection.focusNode, - this.#savedSelection.focusOffset + this.#savedSelection.focusOffset, ); } } @@ -491,7 +499,7 @@ export class SelectionController extends EventTarget { */ selectAll() { if (this.#textEditor.isEmpty) { - return this + return this; } this.#selection.selectAllChildren(this.#textEditor.root); return this; @@ -516,16 +524,12 @@ export class SelectionController extends EventTarget { * @param {number} offset */ collapse(node, offset) { - const nodeOffset = (node.nodeType === Node.TEXT_NODE && offset >= node.nodeValue.length) - ? node.nodeValue.length - : offset + const nodeOffset = + node.nodeType === Node.TEXT_NODE && offset >= node.nodeValue.length + ? node.nodeValue.length + : offset; - return this.setSelection( - node, - nodeOffset, - node, - nodeOffset - ); + return this.setSelection(node, nodeOffset, node, nodeOffset); } /** @@ -536,12 +540,17 @@ export class SelectionController extends EventTarget { * @param {Node} [focusNode=anchorNode] * @param {number} [focusOffset=anchorOffset] */ - setSelection(anchorNode, anchorOffset, focusNode = anchorNode, focusOffset = anchorOffset) { + setSelection( + anchorNode, + anchorOffset, + focusNode = anchorNode, + focusOffset = anchorOffset, + ) { if (!anchorNode.isConnected) { - throw new Error('Invalid anchorNode') + throw new Error("Invalid anchorNode"); } if (!focusNode.isConnected) { - throw new Error('Invalid focusNode') + throw new Error("Invalid focusNode"); } if (this.#savedSelection) { this.#savedSelection.isCollapsed = @@ -578,7 +587,7 @@ export class SelectionController extends EventTarget { anchorNode, anchorOffset, focusNode, - focusOffset + focusOffset, ); } } @@ -711,8 +720,7 @@ export class SelectionController extends EventTarget { if (this.#savedSelection) { return this.#savedSelection.focusNode; } - if (!this.#focusNode) - console.trace("focusNode", this.#focusNode); + if (!this.#focusNode) console.trace("focusNode", this.#focusNode); return this.#focusNode; } @@ -963,7 +971,7 @@ export class SelectionController extends EventTarget { * @type {boolean} */ get isRootFocus() { - return isRoot(this.focusNode) + return isRoot(this.focusNode); } /** @@ -1044,27 +1052,25 @@ export class SelectionController extends EventTarget { * @param {DocumentFragment} fragment */ insertPaste(fragment) { - if (fragment.children.length === 1 - && fragment.firstElementChild?.dataset?.inline === "force" + if ( + fragment.children.length === 1 && + fragment.firstElementChild?.dataset?.inline === "force" ) { - const collapseNode = fragment.lastElementChild.firstChild + const collapseNode = fragment.lastElementChild.firstChild; if (this.isInlineStart) { - this.focusInline.before(...fragment.firstElementChild.children) + this.focusInline.before(...fragment.firstElementChild.children); } else if (this.isInlineEnd) { this.focusInline.after(...fragment.firstElementChild.children); } else { - const newInline = splitInline( - this.focusInline, - this.focusOffset - ) - this.focusInline.after(...fragment.firstElementChild.children, newInline) + const newInline = splitInline(this.focusInline, this.focusOffset); + this.focusInline.after( + ...fragment.firstElementChild.children, + newInline, + ); } - return this.collapse( - collapseNode, - collapseNode.nodeValue.length - ); + return this.collapse(collapseNode, collapseNode.nodeValue.length); } - const collapseNode = fragment.lastElementChild.lastElementChild.firstChild + const collapseNode = fragment.lastElementChild.lastElementChild.firstChild; if (this.isParagraphStart) { const a = fragment.lastElementChild; const b = this.focusParagraph; @@ -1079,7 +1085,7 @@ export class SelectionController extends EventTarget { const newParagraph = splitParagraph( this.focusParagraph, this.focusInline, - this.focusOffset + this.focusOffset, ); this.focusParagraph.after(fragment, newParagraph); } @@ -1115,7 +1121,7 @@ export class SelectionController extends EventTarget { const removedData = removeForward( this.focusNode.nodeValue, - this.focusOffset + this.focusOffset, ); if (this.focusNode.nodeValue !== removedData) { @@ -1155,7 +1161,7 @@ export class SelectionController extends EventTarget { // Remove the character from the string. const removedData = removeBackward( this.focusNode.nodeValue, - this.focusOffset + this.focusOffset, ); if (this.focusNode.nodeValue !== removedData) { @@ -1187,7 +1193,10 @@ export class SelectionController extends EventTarget { inline.childNodes.length === 0 ) { inline.remove(); - return this.collapse(previousTextNode, getTextNodeLength(previousTextNode)); + return this.collapse( + previousTextNode, + getTextNodeLength(previousTextNode), + ); } return this.collapse(this.focusNode, this.focusOffset - 1); @@ -1202,7 +1211,7 @@ export class SelectionController extends EventTarget { this.focusNode.nodeValue = insertInto( this.focusNode.nodeValue, this.focusOffset, - newText + newText, ); this.#mutations.update(this.focusInline); return this.collapse(this.focusNode, this.focusOffset + newText.length); @@ -1219,14 +1228,14 @@ export class SelectionController extends EventTarget { this.focusNode.nodeValue = insertInto( this.focusNode.nodeValue, this.focusOffset, - newText + newText, ); } else if (this.isLineBreakFocus) { const textNode = new Text(newText); this.focusNode.replaceWith(textNode); this.collapse(textNode, newText.length); } else { - throw new Error('Unknown node type'); + throw new Error("Unknown node type"); } } @@ -1243,22 +1252,18 @@ export class SelectionController extends EventTarget { this.focusNode.nodeValue, startOffset, endOffset, - newText + newText, ); } else if (this.isLineBreakFocus) { this.focusNode.replaceWith(new Text(newText)); } else if (this.isRootFocus) { const newTextNode = new Text(newText); const newInline = createInline(newTextNode, this.#currentStyle); - const newParagraph = createParagraph([ - newInline - ], this.#currentStyle) - this.focusNode.replaceChildren( - newParagraph - ); + const newParagraph = createParagraph([newInline], this.#currentStyle); + this.focusNode.replaceChildren(newParagraph); return this.collapse(newTextNode, newText.length + 1); } else { - throw new Error('Unknown node type'); + throw new Error("Unknown node type"); } this.#mutations.update(this.focusInline); return this.collapse(this.focusNode, startOffset + newText.length); @@ -1282,7 +1287,7 @@ export class SelectionController extends EventTarget { ) { const newTextNode = new Text(newText); currentParagraph.replaceChildren( - createInline(newTextNode, this.anchorInline.style) + createInline(newTextNode, this.anchorInline.style), ); return this.collapse(newTextNode, newTextNode.nodeValue.length); } @@ -1363,7 +1368,7 @@ export class SelectionController extends EventTarget { const newParagraph = splitParagraph( this.focusParagraph, this.focusInline, - this.#focusOffset + this.#focusOffset, ); this.focusParagraph.after(newParagraph); this.#mutations.update(currentParagraph); @@ -1396,7 +1401,7 @@ export class SelectionController extends EventTarget { const newParagraph = splitParagraph( currentParagraph, currentInline, - this.focusOffset + this.focusOffset, ); currentParagraph.after(newParagraph); @@ -1542,7 +1547,7 @@ export class SelectionController extends EventTarget { const newNodeValue = removeSlice( startNode.nodeValue, startOffset, - endOffset + endOffset, ); if (newNodeValue === "") { const lineBreak = createLineBreak(); @@ -1588,9 +1593,10 @@ export class SelectionController extends EventTarget { currentNode.nodeValue = currentNode.nodeValue.slice(0, startOffset); } } else if (currentNode === endNode) { - if (isLineBreak(endNode) - || (isTextNode(endNode) - && endOffset === endNode.nodeValue.length)) { + if ( + isLineBreak(endNode) || + (isTextNode(endNode) && endOffset === endNode.nodeValue.length) + ) { // We should remove this node completely. shouldRemoveNodeCompletely = true; } else { @@ -1623,7 +1629,6 @@ export class SelectionController extends EventTarget { if (currentNode === endNode) { break; } - } while (this.#textNodeIterator.currentNode); if (startParagraph !== endParagraph) { @@ -1635,22 +1640,31 @@ export class SelectionController extends EventTarget { } } - if (startInline.childNodes.length === 0 - && endInline.childNodes.length > 0) { + if ( + startInline.childNodes.length === 0 && + endInline.childNodes.length > 0 + ) { startInline.remove(); return this.collapse(endNode, 0); - } else if (startInline.childNodes.length > 0 - && endInline.childNodes.length === 0) { + } else if ( + startInline.childNodes.length > 0 && + endInline.childNodes.length === 0 + ) { endInline.remove(); return this.collapse(startNode, startOffset); - } else if (startInline.childNodes.length === 0 - && endInline.childNodes.length === 0) { + } else if ( + startInline.childNodes.length === 0 && + endInline.childNodes.length === 0 + ) { const previousInline = startInline.previousElementSibling; const nextInline = endInline.nextElementSibling; startInline.remove(); endInline.remove(); if (previousInline) { - return this.collapse(previousInline.firstChild, previousInline.firstChild.nodeValue.length); + return this.collapse( + previousInline.firstChild, + previousInline.firstChild.nodeValue.length, + ); } if (nextInline) { return this.collapse(nextInline.firstChild, 0); @@ -1790,7 +1804,7 @@ export class SelectionController extends EventTarget { this.startOffset, this.endContainer, this.endOffset, - newStyles + newStyles, ); } diff --git a/frontend/text-editor/src/main.js b/frontend/text-editor/src/main.js index dbcbc91e4a..1f58fc01be 100644 --- a/frontend/text-editor/src/main.js +++ b/frontend/text-editor/src/main.js @@ -17,7 +17,7 @@ const debug = searchParams.has("debug") : []; const textEditorSelectionImposterElement = document.getElementById( - "text-editor-selection-imposter" + "text-editor-selection-imposter", ); const textEditorElement = document.querySelector(".text-editor-content"); @@ -25,18 +25,18 @@ const textEditor = new TextEditor(textEditorElement, { styleDefaults: { "font-family": "sourcesanspro", "font-size": "14", - "font-weight": "400", + "font-weight": "500", "font-style": "normal", "line-height": "1.2", "letter-spacing": "0", - "direction": "ltr", + direction: "ltr", "text-align": "left", "text-transform": "none", "text-decoration": "none", "--typography-ref-id": '["~#\'",null]', "--typography-ref-file": '["~#\'",null]', "--font-id": '["~#\'","sourcesanspro"]', - "--fills": '[["^ ","~:fill-color","#000000","~:fill-opacity",1]]' + "--fills": '[["^ ","~:fill-color","#000000","~:fill-opacity",1]]', }, selectionImposterElement: textEditorSelectionImposterElement, debug: new SelectionControllerDebug({ @@ -87,7 +87,7 @@ function onDirectionChange(e) { } if (e.target.checked) { textEditor.applyStylesToSelection({ - "direction": e.target.value + direction: e.target.value, }); } } @@ -101,7 +101,7 @@ function onTextAlignChange(e) { } if (e.target.checked) { textEditor.applyStylesToSelection({ - "text-align": e.target.value + "text-align": e.target.value, }); } } @@ -143,18 +143,18 @@ lineHeightElement.addEventListener("change", (e) => { console.log(e); } textEditor.applyStylesToSelection({ - "line-height": e.target.value - }) -}) + "line-height": e.target.value, + }); +}); letterSpacingElement.addEventListener("change", (e) => { if (debug.includes("events")) { console.log(e); } textEditor.applyStylesToSelection({ - "letter-spacing": e.target.value - }) -}) + "letter-spacing": e.target.value, + }); +}); fontStyleElement.addEventListener("change", (e) => { if (debug.includes("events")) {