From a242962113685990c37035cde8ce3e9a6b179a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Fri, 4 Jul 2025 15:52:13 +0200 Subject: [PATCH 1/3] :bug: Fix missing font when pasting text (editor v1) --- frontend/src/app/main/data/workspace.cljs | 7 +++ .../app/main/data/workspace/clipboard.cljs | 4 +- frontend/src/app/main/refs.cljs | 3 ++ .../ui/workspace/shapes/text/v2_editor.cljs | 9 +++- frontend/text-editor/src/editor/TextEditor.js | 41 ++++++++++++------ .../text-editor/src/editor/clipboard/copy.js | 2 +- .../text-editor/src/editor/clipboard/paste.js | 15 +++++-- .../src/editor/content/dom/Content.js | 14 +++--- .../src/editor/content/dom/Element.js | 5 ++- .../src/editor/content/dom/Style.js | 43 +++++++++++-------- 10 files changed, 95 insertions(+), 48 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index f8a927684d..a59b27270e 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1295,6 +1295,13 @@ (js/console.log "Copies no ref" (count copies-no-ref) (clj->js copies-no-ref)) (js/console.log "Childs no ref" (count childs-no-ref) (clj->js childs-no-ref)))))) +(defn set-clipboard-style + [style] + (ptk/reify ::set-clipboard-style + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-global :clipboard-style] style)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Exports ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 9cf3ff1ccb..9cc7a10ce1 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -42,6 +42,7 @@ [app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.undo :as dwu] [app.main.errors] + [app.main.refs :as refs] [app.main.repo :as rp] [app.main.router :as rt] [app.main.streams :as ms] @@ -950,7 +951,8 @@ (ptk/reify ::paste-html-text ptk/WatchEvent (watch [_ state _] - (let [root (dwtxt/create-root-from-html html) + (let [style (deref refs/workspace-clipboard-style) + root (dwtxt/create-root-from-html html style) content (tc/dom->cljs root)] (when (types.text/valid-content? content) (let [id (uuid/next) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 9b27c73c11..73b41500e2 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -402,6 +402,9 @@ [frame-id] (l/derived #(get % frame-id) workspace-frame-modifiers =)) +(def workspace-clipboard-style + (l/derived :clipboard-style workspace-global)) + (defn select-bool-children [id] (l/derived #(dsh/select-bool-children % id) st/state =)) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs index 8a42dde614..e1dad90b26 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs @@ -116,7 +116,12 @@ on-change (fn [] (when-let [content (content/dom->cljs (dwt/get-editor-root instance))] - (st/emit! (dwt/v2-update-text-shape-content shape-id content :update-name? true))))] + (st/emit! (dwt/v2-update-text-shape-content shape-id content :update-name? true)))) + + on-clipboard-change + (fn [event] + (let [style (.-detail event)] + (st/emit! (dw/set-clipboard-style style))))] (.addEventListener ^js global/document "keyup" on-key-up) (.addEventListener ^js instance "blur" on-blur) @@ -124,6 +129,7 @@ (.addEventListener ^js instance "needslayout" on-needs-layout) (.addEventListener ^js instance "stylechange" on-style-change) (.addEventListener ^js instance "change" on-change) + (.addEventListener ^js instance "clipboardchange" on-clipboard-change) (st/emit! (dwt/update-editor instance)) (when (some? content) @@ -138,6 +144,7 @@ (.removeEventListener ^js instance "needslayout" on-needs-layout) (.removeEventListener ^js instance "stylechange" on-style-change) (.removeEventListener ^js instance "change" on-change) + (.removeEventListener ^js instance "clipboardchange" on-clipboard-change) (dwt/dispose! instance) (st/emit! (dwt/update-editor nil))))) diff --git a/frontend/text-editor/src/editor/TextEditor.js b/frontend/text-editor/src/editor/TextEditor.js index 795731b9a4..277ba0eced 100644 --- a/frontend/text-editor/src/editor/TextEditor.js +++ b/frontend/text-editor/src/editor/TextEditor.js @@ -12,7 +12,10 @@ import ChangeController from "./controllers/ChangeController.js"; import SelectionController from "./controllers/SelectionController.js"; import { createSelectionImposterFromClientRects } from "./selection/Imposter.js"; import { addEventListeners, removeEventListeners } from "./Event.js"; -import { mapContentFragmentFromHTML, mapContentFragmentFromString } from "./content/dom/Content.js"; +import { + mapContentFragmentFromHTML, + mapContentFragmentFromString, +} from "./content/dom/Content.js"; import { createRoot, createEmptyRoot } from "./content/dom/Root.js"; import { createParagraph } from "./content/dom/Paragraph.js"; import { createEmptyInline, createInline } from "./content/dom/Inline.js"; @@ -173,11 +176,11 @@ export class TextEditor extends EventTarget { this.#selectionController = new SelectionController( this, document.getSelection(), - options + options, ); this.#selectionController.addEventListener( "stylechange", - this.#onStyleChange + this.#onStyleChange, ); addEventListeners(this.#element, this.#events, { capture: true, @@ -199,7 +202,7 @@ export class TextEditor extends EventTarget { if (rects) { const rect = this.#selectionImposterElement.getBoundingClientRect(); this.#selectionImposterElement.replaceChildren( - createSelectionImposterFromClientRects(rect, rects) + createSelectionImposterFromClientRects(rect, rects), ); } } @@ -258,7 +261,15 @@ export class TextEditor extends EventTarget { * * @param {ClipboardEvent} e */ - #onCopy = (e) => clipboard.copy(e, this, this.#selectionController); + #onCopy = (e) => { + this.dispatchEvent( + new CustomEvent("clipboardchange", { + detail: this.#selectionController.currentStyle, + }), + ); + + clipboard.copy(e, this, this.#selectionController); + }; /** * Event called before the DOM is modified. @@ -304,7 +315,10 @@ export class TextEditor extends EventTarget { return; } - if (e.inputType === "insertCompositionText" && this.#fixInsertCompositionText) { + if ( + e.inputType === "insertCompositionText" && + this.#fixInsertCompositionText + ) { e.preventDefault(); this.#fixInsertCompositionText = false; if (e.data) { @@ -331,7 +345,7 @@ export class TextEditor extends EventTarget { type: type, mutations: mutations, }, - }) + }), ); } @@ -492,7 +506,7 @@ export class TextEditor extends EventTarget { this.#changeController = null; this.#selectionController.removeEventListener( "stylechange", - this.#onStyleChange + this.#onStyleChange, ); this.#selectionController.dispose(); this.#selectionController = null; @@ -502,10 +516,11 @@ export class TextEditor extends EventTarget { } } -export function createRootFromHTML(html) { - const fragment = mapContentFragmentFromHTML(html); - const root = createRoot([]); +export function createRootFromHTML(html, style) { + const fragment = mapContentFragmentFromHTML(html, style); + const root = createRoot([], style); root.replaceChildren(fragment); + console.log("ROOT", root); return root; } @@ -517,7 +532,7 @@ export function createRootFromString(string) { } export function isEditor(instance) { - return (instance instanceof TextEditor); + return instance instanceof TextEditor; } /* Convenience function based API for Text Editor */ @@ -538,7 +553,7 @@ export function setRoot(instance, root) { } export function create(element, options) { - return new TextEditor(element, {...options}); + return new TextEditor(element, { ...options }); } export function getCurrentStyle(instance) { diff --git a/frontend/text-editor/src/editor/clipboard/copy.js b/frontend/text-editor/src/editor/clipboard/copy.js index 49ed9f9ec3..374d301965 100644 --- a/frontend/text-editor/src/editor/clipboard/copy.js +++ b/frontend/text-editor/src/editor/clipboard/copy.js @@ -16,4 +16,4 @@ * @param {ClipboardEvent} event * @param {TextEditor} editor */ -export function copy(event, editor) {} +export function copy(event, editor, selectionController) {} diff --git a/frontend/text-editor/src/editor/clipboard/paste.js b/frontend/text-editor/src/editor/clipboard/paste.js index 2b3525afc0..af472888bd 100644 --- a/frontend/text-editor/src/editor/clipboard/paste.js +++ b/frontend/text-editor/src/editor/clipboard/paste.js @@ -6,7 +6,10 @@ * Copyright (c) KALEIDOS INC */ -import { mapContentFragmentFromHTML, mapContentFragmentFromString } from "../content/dom/Content.js"; +import { + mapContentFragmentFromHTML, + mapContentFragmentFromString, +} from "../content/dom/Content.js"; /** * When the user pastes some HTML, what we do is generate @@ -27,10 +30,16 @@ export function paste(event, editor, selectionController) { let fragment = null; if (event.clipboardData.types.includes("text/html")) { const html = event.clipboardData.getData("text/html"); - fragment = mapContentFragmentFromHTML(html, selectionController.currentStyle); + fragment = mapContentFragmentFromHTML( + html, + selectionController.currentStyle, + ); } else if (event.clipboardData.types.includes("text/plain")) { const plain = event.clipboardData.getData("text/plain"); - fragment = mapContentFragmentFromString(plain, selectionController.currentStyle); + fragment = mapContentFragmentFromString( + plain, + selectionController.currentStyle, + ); } if (!fragment) { diff --git a/frontend/text-editor/src/editor/content/dom/Content.js b/frontend/text-editor/src/editor/content/dom/Content.js index 9c6c8efccc..d95d9e2a71 100644 --- a/frontend/text-editor/src/editor/content/dom/Content.js +++ b/frontend/text-editor/src/editor/content/dom/Content.js @@ -48,10 +48,7 @@ function isContentFragmentFromDocumentInline(document) { * @returns {DocumentFragment} */ export function mapContentFragmentFromDocument(document, root, styleDefaults) { - const nodeIterator = document.createNodeIterator( - root, - NodeFilter.SHOW_TEXT - ); + const nodeIterator = document.createNodeIterator(root, NodeFilter.SHOW_TEXT); const fragment = document.createDocumentFragment(); let currentParagraph = null; @@ -110,7 +107,7 @@ export function mapContentFragmentFromHTML(html, styleDefaults) { return mapContentFragmentFromDocument( htmlDocument, htmlDocument.documentElement, - styleDefaults + styleDefaults, ); } @@ -129,9 +126,10 @@ export function mapContentFragmentFromString(string, styleDefaults) { fragment.appendChild(createEmptyParagraph(styleDefaults)); } else { fragment.appendChild( - createParagraph([ - createInline(new Text(line), styleDefaults) - ], styleDefaults) + createParagraph( + [createInline(new Text(line), styleDefaults)], + styleDefaults, + ), ); } } diff --git a/frontend/text-editor/src/editor/content/dom/Element.js b/frontend/text-editor/src/editor/content/dom/Element.js index d118801468..a270e076f8 100644 --- a/frontend/text-editor/src/editor/content/dom/Element.js +++ b/frontend/text-editor/src/editor/content/dom/Element.js @@ -34,15 +34,16 @@ export function createRandomId() { * @returns {HTMLElement} */ export function createElement(tag, options) { + console.log("createElement", options); const element = document.createElement(tag); if (options?.attributes) { Object.entries(options.attributes).forEach(([name, value]) => - element.setAttribute(name, value) + element.setAttribute(name, value), ); } if (options?.data) { Object.entries(options.data).forEach( - ([name, value]) => (element.dataset[name] = value) + ([name, value]) => (element.dataset[name] = value), ); } if (options?.styles && options?.allowedStyles) { diff --git a/frontend/text-editor/src/editor/content/dom/Style.js b/frontend/text-editor/src/editor/content/dom/Style.js index d194a13363..19a4a6a977 100644 --- a/frontend/text-editor/src/editor/content/dom/Style.js +++ b/frontend/text-editor/src/editor/content/dom/Style.js @@ -26,7 +26,7 @@ export function mergeStyleDeclarations(target, source) { const styleValue = source.getPropertyValue(styleName); target.setProperty(styleName, styleValue); } - return target + return target; } /** @@ -40,7 +40,7 @@ function resetStyleDeclaration(styleDeclaration) { const styleName = styleDeclaration.item(index); styleDeclaration.removeProperty(styleName); } - return styleDeclaration + return styleDeclaration; } /** @@ -49,14 +49,14 @@ function resetStyleDeclaration(styleDeclaration) { * * @type {HTMLDivElement|null} */ -let inertElement = null +let inertElement = null; /** * Resets the style declaration of the inert * element. */ function resetInertElement() { - if (!inertElement) throw new Error('Invalid inert element'); + if (!inertElement) throw new Error("Invalid inert element"); resetStyleDeclaration(inertElement.style); return inertElement; } @@ -110,10 +110,7 @@ export function getComputedStyle(element) { } } else { const newValue = currentElement.style.getPropertyValue(styleName); - inertElement.style.setProperty( - styleName, - newValue - ); + inertElement.style.setProperty(styleName, newValue); } } currentElement = currentElement.parentElement; @@ -131,12 +128,12 @@ export function getComputedStyle(element) { * @param {CSSStyleDeclaration} [styleDefaults] * @returns {CSSStyleDeclaration} */ -export function normalizeStyles(node, styleDefaults = getStyleDefaultsDeclaration()) { +export function normalizeStyles( + node, + styleDefaults = getStyleDefaultsDeclaration(), +) { const computedStyle = getComputedStyle(node.parentElement); - const styleDeclaration = mergeStyleDeclarations( - styleDefaults, - computedStyle - ); + const styleDeclaration = mergeStyleDeclarations(styleDefaults, computedStyle); // If there's a color property, we should convert it to // a --fills CSS variable property. @@ -173,7 +170,7 @@ export function normalizeStyles(node, styleDefaults = getStyleDefaultsDeclaratio parseFloat(lineHeight) / parseFloat(fontSize), ); } - return styleDeclaration + return styleDeclaration; } /** @@ -237,7 +234,7 @@ export function getStyleFromDeclaration(style, styleName, styleUnit) { if (styleName === "font-size") { return getStyleFontSize(styleValueAsNumber, styleValue); } else if (styleName === "line-height") { - return styleValue + return styleValue; } if (Number.isNaN(styleValueAsNumber)) { return styleValue; @@ -291,10 +288,14 @@ export function setStylesFromObject(element, allowedStyles, styleObject) { export function setStylesFromDeclaration( element, allowedStyles, - styleDeclaration + styleDeclaration, ) { for (const [styleName, styleUnit] of allowedStyles) { - const styleValue = getStyleFromDeclaration(styleDeclaration, styleName, styleUnit); + const styleValue = getStyleFromDeclaration( + styleDeclaration, + styleName, + styleUnit, + ); if (styleValue) { setStyle(element, styleName, styleValue, styleUnit); } @@ -316,7 +317,7 @@ export function setStyles(element, allowedStyles, styleObjectOrDeclaration) { return setStylesFromDeclaration( element, allowedStyles, - styleObjectOrDeclaration + styleObjectOrDeclaration, ); } return setStylesFromObject(element, allowedStyles, styleObjectOrDeclaration); @@ -354,7 +355,11 @@ export function mergeStyles(allowedStyles, styleDeclaration, newStyles) { if (styleName in newStyles) { mergedStyles[styleName] = newStyles[styleName]; } else { - mergedStyles[styleName] = getStyleFromDeclaration(styleDeclaration, styleName, styleUnit); + mergedStyles[styleName] = getStyleFromDeclaration( + styleDeclaration, + styleName, + styleUnit, + ); } } return mergedStyles; From 098fd9fb0f2adc21b72f5c8d13fe2a12f1892026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Mon, 7 Jul 2025 18:08:53 +0200 Subject: [PATCH 2/3] :bug: Fix not picking up font style / variant in new renderer --- CHANGES.md | 1 + frontend/src/app/main/fonts.cljs | 4 +-- frontend/src/app/render_wasm/api/fonts.cljs | 28 ++++++++++++++++++- frontend/text-editor/src/editor/TextEditor.js | 1 - .../src/editor/content/dom/Element.js | 1 - 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 577cd2b3be..433534c025 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ ### :bug: Bugs fixed - Fix problem with booleans selection [Taiga #11627](https://tree.taiga.io/project/penpot/issue/11627) +- Fix missing font when copy&paste a chunk of text [Taiga #11522](https://tree.taiga.io/project/penpot/issue/11522) ## 2.9.0 (Unreleased) diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index d72b3880cc..7d42607905 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -39,8 +39,8 @@ {:id "300italic" :name "300 (italic)" :weight "300" :style "italic" :suffix "lightitalic" :ttf-url "sourcesanspro-lightitalic.ttf"} {:id "regular" :name "regular" :weight "400" :style "normal" :ttf-url "sourcesanspro-regular.ttf"} {:id "italic" :name "italic" :weight "400" :style "italic" :ttf-url "sourcesanspro-italic.ttf"} - {:id "bold" :name "bold" :weight "bold" :style "normal" :ttf-url "sourcesanspro-bold.ttf"} - {:id "bolditalic" :name "bold (italic)" :weight "bold" :style "italic" :ttf-url "sourcesanspro-bolditalic.ttf"} + {:id "bold" :name "bold" :weight "700" :style "normal" :ttf-url "sourcesanspro-bold.ttf"} + {:id "bolditalic" :name "bold (italic)" :weight "700" :style "italic" :ttf-url "sourcesanspro-bolditalic.ttf"} {:id "black" :name "black" :weight "900" :style "normal" :ttf-url "sourcesanspro-black.ttf"} {:id "blackitalic" :name "black (italic)" :weight "900" :style "italic" :ttf-url "sourcesanspro-blackitalic.ttf"}]}]) diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs index deed99ccf4..0872039b73 100644 --- a/frontend/src/app/render_wasm/api/fonts.cljs +++ b/frontend/src/app/render_wasm/api/fonts.cljs @@ -169,7 +169,31 @@ (defn serialize-font-weight [font-weight] - (js/Number font-weight)) + (if (number? font-weight) + font-weight + (let [font-weight-str (str font-weight)] + (cond + (re-matches #"\d+" font-weight-str) + (js/Number font-weight-str) + + (str/includes? font-weight-str "bold") + 700 + (str/includes? font-weight-str "black") + 900 + (str/includes? font-weight-str "extrabold") + 800 + (str/includes? font-weight-str "extralight") + 200 + (str/includes? font-weight-str "light") + 300 + (str/includes? font-weight-str "medium") + 500 + (str/includes? font-weight-str "semibold") + 600 + (str/includes? font-weight-str "thin") + 100 + :else + 400)))) (defn store-font [shape-id font] @@ -182,6 +206,7 @@ weight (serialize-font-weight raw-weight) style (serialize-font-style (cond (str/includes? font-variant-id "italic") "italic" + (str/includes? raw-weight "italic") "italic" :else "normal")) asset-id (font-id->asset-id font-id font-variant-id) font-data {:wasm-id wasm-id @@ -189,6 +214,7 @@ :font-variant-id font-variant-id :style style :weight weight}] + (store-font-id shape-id font-data asset-id emoji? fallback?))) (defn store-fonts diff --git a/frontend/text-editor/src/editor/TextEditor.js b/frontend/text-editor/src/editor/TextEditor.js index 277ba0eced..0d37fb9eea 100644 --- a/frontend/text-editor/src/editor/TextEditor.js +++ b/frontend/text-editor/src/editor/TextEditor.js @@ -520,7 +520,6 @@ export function createRootFromHTML(html, style) { const fragment = mapContentFragmentFromHTML(html, style); const root = createRoot([], style); root.replaceChildren(fragment); - console.log("ROOT", root); return root; } diff --git a/frontend/text-editor/src/editor/content/dom/Element.js b/frontend/text-editor/src/editor/content/dom/Element.js index a270e076f8..e188270758 100644 --- a/frontend/text-editor/src/editor/content/dom/Element.js +++ b/frontend/text-editor/src/editor/content/dom/Element.js @@ -34,7 +34,6 @@ export function createRandomId() { * @returns {HTMLElement} */ export function createElement(tag, options) { - console.log("createElement", options); const element = document.createElement(tag); if (options?.attributes) { Object.entries(options.attributes).forEach(([name, value]) => 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 3/3] :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")) {