mirror of
https://github.com/penpot/penpot.git
synced 2026-05-05 16:18:35 +00:00
Merge pull request #6854 from penpot/ladybenko-11522-fix-missing-font
🐛 Fix missing font when pasting text
This commit is contained in:
commit
bd15ef4618
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"}]}])
|
||||
|
||||
|
||||
@ -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 =))
|
||||
|
||||
|
||||
@ -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)))))
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -198,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}))
|
||||
|
||||
@ -12,7 +12,11 @@ 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 { 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";
|
||||
@ -173,11 +177,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 +203,7 @@ export class TextEditor extends EventTarget {
|
||||
if (rects) {
|
||||
const rect = this.#selectionImposterElement.getBoundingClientRect();
|
||||
this.#selectionImposterElement.replaceChildren(
|
||||
createSelectionImposterFromClientRects(rect, rects)
|
||||
createSelectionImposterFromClientRects(rect, rects),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -258,7 +262,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.currentStyle,
|
||||
}),
|
||||
);
|
||||
|
||||
clipboard.copy(e, this, this.#selectionController);
|
||||
};
|
||||
|
||||
/**
|
||||
* Event called before the DOM is modified.
|
||||
@ -304,7 +316,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 +346,7 @@ export class TextEditor extends EventTarget {
|
||||
type: type,
|
||||
mutations: mutations,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -492,7 +507,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 +517,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);
|
||||
resetInertElement();
|
||||
return root;
|
||||
}
|
||||
|
||||
@ -517,7 +533,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 +554,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) {
|
||||
|
||||
@ -16,4 +16,4 @@
|
||||
* @param {ClipboardEvent} event
|
||||
* @param {TextEditor} editor
|
||||
*/
|
||||
export function copy(event, editor) {}
|
||||
export function copy(event, editor, selectionController) {}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
@ -78,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();
|
||||
@ -110,7 +109,7 @@ export function mapContentFragmentFromHTML(html, styleDefaults) {
|
||||
return mapContentFragmentFromDocument(
|
||||
htmlDocument,
|
||||
htmlDocument.documentElement,
|
||||
styleDefaults
|
||||
styleDefaults,
|
||||
);
|
||||
}
|
||||
|
||||
@ -129,9 +128,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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,12 +37,12 @@ export function createElement(tag, 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) {
|
||||
|
||||
@ -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`.
|
||||
*
|
||||
@ -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');
|
||||
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);
|
||||
@ -110,10 +120,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 +138,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.
|
||||
@ -162,6 +169,11 @@ export function normalizeStyles(node, styleDefaults = getStyleDefaultsDeclaratio
|
||||
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.
|
||||
@ -173,7 +185,7 @@ export function normalizeStyles(node, styleDefaults = getStyleDefaultsDeclaratio
|
||||
parseFloat(lineHeight) / parseFloat(fontSize),
|
||||
);
|
||||
}
|
||||
return styleDeclaration
|
||||
return styleDeclaration;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -237,7 +249,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 +303,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 +332,7 @@ export function setStyles(element, allowedStyles, styleObjectOrDeclaration) {
|
||||
return setStylesFromDeclaration(
|
||||
element,
|
||||
allowedStyles,
|
||||
styleObjectOrDeclaration
|
||||
styleObjectOrDeclaration,
|
||||
);
|
||||
}
|
||||
return setStylesFromObject(element, allowedStyles, styleObjectOrDeclaration);
|
||||
@ -354,7 +370,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;
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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")) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user