🔧 Rename textleafs and inlines to keep coherence between render and editor

This commit is contained in:
Aitor Moreno
2025-10-22 11:33:03 +02:00
parent 140290cd60
commit eb088c31c1
26 changed files with 665 additions and 657 deletions

View File

@@ -16,7 +16,7 @@
[app.render-wasm.wasm :as wasm])) [app.render-wasm.wasm :as wasm]))
(def ^:const PARAGRAPH-ATTR-U8-SIZE 44) (def ^:const PARAGRAPH-ATTR-U8-SIZE 44)
(def ^:const LEAF-ATTR-U8-SIZE 60) (def ^:const SPAN-ATTR-U8-SIZE 60)
(def ^:const MAX-TEXT-FILLS types.fills.impl/MAX-FILLS) (def ^:const MAX-TEXT-FILLS types.fills.impl/MAX-FILLS)
(defn- encode-text (defn- encode-text
@@ -25,7 +25,7 @@
(let [encoder (js/TextEncoder.)] (let [encoder (js/TextEncoder.)]
(.encode encoder text))) (.encode encoder text)))
(defn- write-leaf-fills (defn- write-span-fills
[offset dview fills] [offset dview fills]
(let [new-ofset (reduce (fn [offset fill] (let [new-ofset (reduce (fn [offset fill]
(let [opacity (get fill :fill-opacity 1.0) (let [opacity (get fill :fill-opacity 1.0)
@@ -76,20 +76,20 @@
(defn- write-leaves (defn- write-leaves
[offset dview leaves paragraph] [offset dview leaves paragraph]
(reduce (fn [offset leaf] (reduce (fn [offset span]
(let [font-style (sr/translate-font-style (get leaf :font-style)) (let [font-style (sr/translate-font-style (get span :font-style))
font-size (get leaf :font-size) font-size (get span :font-size)
letter-spacing (get leaf :letter-spacing) letter-spacing (get span :letter-spacing)
font-weight (get leaf :font-weight) font-weight (get span :font-weight)
font-id (f/normalize-font-id (get leaf :font-id)) font-id (f/normalize-font-id (get span :font-id))
font-family (hash (get leaf :font-family)) font-family (hash (get span :font-family))
text-buffer (encode-text (get leaf :text)) text-buffer (encode-text (get span :text))
text-length (mem/size text-buffer) text-length (mem/size text-buffer)
fills (take MAX-TEXT-FILLS (get leaf :fills)) fills (take MAX-TEXT-FILLS (get span :fills))
font-variant-id font-variant-id
(get leaf :font-variant-id) (get span :font-variant-id)
font-variant-id font-variant-id
(if (uuid? font-variant-id) (if (uuid? font-variant-id)
@@ -97,17 +97,17 @@
uuid/zero) uuid/zero)
text-decoration text-decoration
(or (sr/translate-text-decoration (:text-decoration leaf)) (or (sr/translate-text-decoration (:text-decoration span))
(sr/translate-text-decoration (:text-decoration paragraph)) (sr/translate-text-decoration (:text-decoration paragraph))
(sr/translate-text-decoration "none")) (sr/translate-text-decoration "none"))
text-transform text-transform
(or (sr/translate-text-transform (:text-transform leaf)) (or (sr/translate-text-transform (:text-transform span))
(sr/translate-text-transform (:text-transform paragraph)) (sr/translate-text-transform (:text-transform paragraph))
(sr/translate-text-transform "none")) (sr/translate-text-transform "none"))
text-direction text-direction
(or (sr/translate-text-direction (:text-direction leaf)) (or (sr/translate-text-direction (:text-direction span))
(sr/translate-text-direction (:text-direction paragraph)) (sr/translate-text-direction (:text-direction paragraph))
(sr/translate-text-direction "ltr"))] (sr/translate-text-direction "ltr"))]
@@ -127,20 +127,20 @@
(mem/write-i32 dview text-length) (mem/write-i32 dview text-length)
(mem/write-i32 dview (count fills)) (mem/write-i32 dview (count fills))
(mem/assert-written offset LEAF-ATTR-U8-SIZE) (mem/assert-written offset SPAN-ATTR-U8-SIZE)
(write-leaf-fills dview fills)))) (write-span-fills dview fills))))
offset offset
leaves)) leaves))
(defn write-shape-text (defn write-shape-text
;; buffer has the following format: ;; buffer has the following format:
;; [<num-leaves> <paragraph_attributes> <leaves_attributes> <text>] ;; [<num-leaves> <paragraph_attributes> <leaves_attributes> <text>]
[leaves paragraph text] [spans paragraph text]
(let [num-leaves (count leaves) (let [num-spans (count spans)
fills-size (* types.fills.impl/FILL-U8-SIZE MAX-TEXT-FILLS) fills-size (* types.fills.impl/FILL-U8-SIZE MAX-TEXT-FILLS)
metadata-size (+ PARAGRAPH-ATTR-U8-SIZE metadata-size (+ PARAGRAPH-ATTR-U8-SIZE
(* num-leaves (+ LEAF-ATTR-U8-SIZE fills-size))) (* num-spans (+ SPAN-ATTR-U8-SIZE fills-size)))
text-buffer (encode-text text) text-buffer (encode-text text)
text-size (mem/size text-buffer) text-size (mem/size text-buffer)
@@ -151,9 +151,9 @@
offset (mem/alloc total-size)] offset (mem/alloc total-size)]
(-> offset (-> offset
(mem/write-u32 dview num-leaves) (mem/write-u32 dview num-spans)
(write-paragraph dview paragraph) (write-paragraph dview paragraph)
(write-leaves dview leaves paragraph) (write-leaves dview spans paragraph)
(mem/write-buffer heapu8 text-buffer)) (mem/write-buffer heapu8 text-buffer))
(h/call wasm/internal-module "_set_shape_text_content"))) (h/call wasm/internal-module "_set_shape_text_content")))

View File

@@ -18,7 +18,7 @@ import {
import { resetInertElement } from "./content/dom/Style.js"; import { resetInertElement } from "./content/dom/Style.js";
import { createRoot, createEmptyRoot } from "./content/dom/Root.js"; import { createRoot, createEmptyRoot } from "./content/dom/Root.js";
import { createParagraph } from "./content/dom/Paragraph.js"; import { createParagraph } from "./content/dom/Paragraph.js";
import { createEmptyInline, createInline } from "./content/dom/Inline.js"; import { createEmptyTextSpan, createTextSpan } from "./content/dom/TextSpan.js";
import { isLineBreak } from "./content/dom/LineBreak.js"; import { isLineBreak } from "./content/dom/LineBreak.js";
import LayoutType from "./layout/LayoutType.js"; import LayoutType from "./layout/LayoutType.js";
@@ -428,8 +428,8 @@ export class TextEditor extends EventTarget {
} }
/** /**
* CSS Style declaration for the current inline. From here we * CSS Style declaration for the current text span. From here we
* can infer root, paragraph and inline declarations. * can infer root, paragraph and text span declarations.
* *
* @type {CSSStyleDeclaration} * @type {CSSStyleDeclaration}
*/ */
@@ -472,27 +472,27 @@ export class TextEditor extends EventTarget {
} }
/** /**
* Creates a new inline from a string. * Creates a new text span from a string.
* *
* @param {string} text * @param {string} text
* @param {Object.<string,*>|CSSStyleDeclaration} styles * @param {Object.<string,*>|CSSStyleDeclaration} styles
* @returns {HTMLSpanElement} * @returns {HTMLSpanElement}
*/ */
createInlineFromString(text, styles) { createTextSpanFromString(text, styles) {
if (text === "") { if (text === "") {
return createEmptyInline(styles); return createEmptyTextSpan(styles);
} }
return createInline(new Text(text), styles); return createTextSpan(new Text(text), styles);
} }
/** /**
* Creates a new inline. * Creates a new text span.
* *
* @param {...any} args * @param {...any} args
* @returns {HTMLSpanElement} * @returns {HTMLSpanElement}
*/ */
createInline(...args) { createTextSpan(...args) {
return createInline(...args); return createTextSpan(...args);
} }
/** /**

View File

@@ -29,14 +29,14 @@ describe("TextEditor", () => {
const textEditor = new TextEditor(document.createElement("div")); const textEditor = new TextEditor(document.createElement("div"));
textEditor.root = textEditor.createRoot([ textEditor.root = textEditor.createRoot([
textEditor.createParagraph([ textEditor.createParagraph([
textEditor.createInlineFromString("Hello, World!"), textEditor.createTextSpanFromString("Hello, World!"),
]), ]),
textEditor.createParagraph([textEditor.createInlineFromString("")]), textEditor.createParagraph([textEditor.createTextSpanFromString("")]),
textEditor.createParagraph([ textEditor.createParagraph([
textEditor.createInlineFromString("¡Hola, Mundo!"), textEditor.createTextSpanFromString("¡Hola, Mundo!"),
]), ]),
textEditor.createParagraph([ textEditor.createParagraph([
textEditor.createInlineFromString("Hallo, Welt!"), textEditor.createTextSpanFromString("Hallo, Welt!"),
]), ]),
]); ]);
expect(textEditor).toBeInstanceOf(TextEditor); expect(textEditor).toBeInstanceOf(TextEditor);
@@ -76,7 +76,7 @@ describe("TextEditor", () => {
const textEditor = new TextEditor(textEditorElement); const textEditor = new TextEditor(textEditorElement);
textEditor.root = textEditor.createRoot([ textEditor.root = textEditor.createRoot([
textEditor.createParagraph([ textEditor.createParagraph([
textEditor.createInlineFromString("Hello, World!"), textEditor.createTextSpanFromString("Hello, World!"),
]), ]),
]); ]);
expect(textEditor).toBeInstanceOf(TextEditor); expect(textEditor).toBeInstanceOf(TextEditor);

View File

@@ -27,7 +27,7 @@ export function deleteContentBackward(event, editor, selectionController) {
} }
// If we're in a text node and the offset is // If we're in a text node and the offset is
// greater than 0 (not at the start of the inline) // greater than 0 (not at the start of the text span)
// we simple remove a character from the text. // we simple remove a character from the text.
if (selectionController.isTextFocus && selectionController.focusOffset > 0) { if (selectionController.isTextFocus && selectionController.focusOffset > 0) {
return selectionController.removeBackwardText(); return selectionController.removeBackwardText();
@@ -41,11 +41,11 @@ export function deleteContentBackward(event, editor, selectionController) {
) { ) {
return selectionController.mergeBackwardParagraph(); return selectionController.mergeBackwardParagraph();
// If we're at an inline or a line break paragraph // If we're at an text span or a line break paragraph
// and there's more than one paragraph, then we should // and there's more than one paragraph, then we should
// remove the next paragraph. // remove the next paragraph.
} else if ( } else if (
selectionController.isInlineFocus || selectionController.isTextSpanFocus ||
selectionController.isLineBreakFocus selectionController.isLineBreakFocus
) { ) {
return selectionController.removeBackwardParagraph(); return selectionController.removeBackwardParagraph();

View File

@@ -26,7 +26,7 @@ export function deleteContentForward(event, editor, selectionController) {
} }
// If we're in a text node and the offset is // If we're in a text node and the offset is
// greater than 0 (not at the start of the inline) // greater than 0 (not at the start of the text span)
// we simple remove a character from the text. // we simple remove a character from the text.
if (selectionController.isTextFocus if (selectionController.isTextFocus
&& selectionController.focusAtEnd) { && selectionController.focusAtEnd) {
@@ -41,11 +41,11 @@ export function deleteContentForward(event, editor, selectionController) {
) { ) {
return selectionController.removeForwardText(); return selectionController.removeForwardText();
// If we're at an inline or a line break paragraph // If we're at a text span or a line break paragraph
// and there's more than one paragraph, then we should // and there's more than one paragraph, then we should
// remove the next paragraph. // remove the next paragraph.
} else if ( } else if (
(selectionController.isInlineFocus || (selectionController.isTextSpanFocus ||
selectionController.isLineBreakFocus) && selectionController.isLineBreakFocus) &&
editor.numParagraphs > 1 editor.numParagraphs > 1
) { ) {

View File

@@ -25,8 +25,8 @@ export function insertText(event, editor, selectionController) {
} else { } else {
if (selectionController.isMultiParagraph) { if (selectionController.isMultiParagraph) {
return selectionController.replaceParagraphs(event.data); return selectionController.replaceParagraphs(event.data);
} else if (selectionController.isMultiInline) { } else if (selectionController.isMultiTextSpan) {
return selectionController.replaceInlines(event.data); return selectionController.replaceTextSpans(event.data);
} else if (selectionController.isTextSame) { } else if (selectionController.isTextSame) {
return selectionController.replaceText(event.data); return selectionController.replaceText(event.data);
} }

View File

@@ -6,7 +6,7 @@
* Copyright (c) KALEIDOS INC * Copyright (c) KALEIDOS INC
*/ */
import { createInline, isLikeInline } from "./Inline.js"; import { createTextSpan, isLikeTextSpan } from "./TextSpan.js";
import { import {
createEmptyParagraph, createEmptyParagraph,
createParagraph, createParagraph,
@@ -21,11 +21,11 @@ const DEFAULT_FILLS = '[["^ ","~:fill-color", "#000000","~:fill-opacity", 1]]';
/** /**
* Returns if the content fragment should be treated as * Returns if the content fragment should be treated as
* inline content and not a paragraphed one. * text span content and not a paragraphed one.
* *
* @returns {boolean} * @returns {boolean}
*/ */
function isContentFragmentFromDocumentInline(document) { function isContentFragmentFromDocumentTextSpan(document) {
const nodeIterator = document.createNodeIterator( const nodeIterator = document.createNodeIterator(
document.documentElement, document.documentElement,
NodeFilter.SHOW_ELEMENT, NodeFilter.SHOW_ELEMENT,
@@ -37,7 +37,7 @@ function isContentFragmentFromDocumentInline(document) {
continue; continue;
} }
if (!isLikeInline(currentNode)) return false; if (!isLikeTextSpan(currentNode)) return false;
currentNode = nodeIterator.nextNode(); currentNode = nodeIterator.nextNode();
} }
@@ -75,29 +75,29 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) {
currentParagraph = createParagraph(undefined, currentStyle); currentParagraph = createParagraph(undefined, currentStyle);
} }
} }
const inline = createInline(new Text(currentNode.nodeValue), currentStyle); const textSpan = createTextSpan(new Text(currentNode.nodeValue), currentStyle);
const fontSize = inline.style.getPropertyValue("font-size"); const fontSize = textSpan.style.getPropertyValue("font-size");
if (!fontSize) { if (!fontSize) {
console.warn("font-size", fontSize); console.warn("font-size", fontSize);
inline.style.setProperty("font-size", styleDefaults?.getPropertyValue("font-size") ?? DEFAULT_FONT_SIZE); textSpan.style.setProperty("font-size", styleDefaults?.getPropertyValue("font-size") ?? DEFAULT_FONT_SIZE);
} }
const fontFamily = inline.style.getPropertyValue("font-family"); const fontFamily = textSpan.style.getPropertyValue("font-family");
if (!fontFamily) { if (!fontFamily) {
console.warn("font-family", fontFamily); console.warn("font-family", fontFamily);
inline.style.setProperty("font-family", styleDefaults?.getPropertyValue("font-family") ?? DEFAULT_FONT_FAMILY); textSpan.style.setProperty("font-family", styleDefaults?.getPropertyValue("font-family") ?? DEFAULT_FONT_FAMILY);
} }
const fontWeight = inline.style.getPropertyValue("font-weight"); const fontWeight = textSpan.style.getPropertyValue("font-weight");
if (!fontWeight) { if (!fontWeight) {
console.warn("font-weight", fontWeight); console.warn("font-weight", fontWeight);
inline.style.setProperty("font-weight", styleDefaults?.getPropertyValue("font-weight") ?? DEFAULT_FONT_WEIGHT) textSpan.style.setProperty("font-weight", styleDefaults?.getPropertyValue("font-weight") ?? DEFAULT_FONT_WEIGHT)
} }
const fills = inline.style.getPropertyValue('--fills'); const fills = textSpan.style.getPropertyValue('--fills');
if (!fills) { if (!fills) {
console.warn("fills", fills); console.warn("fills", fills);
inline.style.setProperty("--fills", styleDefaults?.getPropertyValue("--fills") ?? DEFAULT_FILLS); textSpan.style.setProperty("--fills", styleDefaults?.getPropertyValue("--fills") ?? DEFAULT_FILLS);
} }
currentParagraph.appendChild(inline); currentParagraph.appendChild(textSpan);
currentNode = nodeIterator.nextNode(); currentNode = nodeIterator.nextNode();
} }
@@ -107,9 +107,9 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) {
} }
if (fragment.children.length === 1) { if (fragment.children.length === 1) {
const isContentInline = isContentFragmentFromDocumentInline(document); const isContentTextSpan = isContentFragmentFromDocumentTextSpan(document);
if (isContentInline) { if (isContentTextSpan) {
currentParagraph.dataset.inline = "force"; currentParagraph.dataset.textSpan = "force";
} }
} }
@@ -149,7 +149,7 @@ export function mapContentFragmentFromString(string, styleDefaults) {
} else { } else {
fragment.appendChild( fragment.appendChild(
createParagraph( createParagraph(
[createInline(new Text(line), styleDefaults)], [createTextSpan(new Text(line), styleDefaults)],
styleDefaults, styleDefaults,
), ),
); );

View File

@@ -24,7 +24,7 @@ describe("Content", () => {
expect(contentFragment.textContent).toBe("Hello, World!"); expect(contentFragment.textContent).toBe("Hello, World!");
}); });
test("mapContentFragmentFromHTML should return a valid content for the editor (multiple inlines)", () => { test("mapContentFragmentFromHTML should return a valid content for the editor (multiple text spans)", () => {
const inertElement = document.createElement("div"); const inertElement = document.createElement("div");
const contentFragment = mapContentFragmentFromHTML( const contentFragment = mapContentFragmentFromHTML(
"<div>Hello,<br/><span> World!</span><br/></div>", "<div>Hello,<br/><span> World!</span><br/></div>",

View File

@@ -1,123 +1,123 @@
import { describe, test, expect } from "vitest"; import { describe, test, expect } from "vitest";
import { import {
createEmptyInline, createEmptyTextSpan,
createInline, createTextSpan,
getInline, getTextSpan,
getInlineLength, getTextSpanLength,
isInline, isTextSpan,
isInlineEnd, isTextSpanEnd,
isInlineStart, isTextSpanStart,
isLikeInline, isLikeTextSpan,
splitInline, splitTextSpan,
TAG, TAG,
TYPE, TYPE,
} from "./Inline.js"; } from "./TextSpan.js";
import { createLineBreak } from "./LineBreak.js"; import { createLineBreak } from "./LineBreak.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe("Inline", () => { describe("TextSpan", () => {
test("createInline should throw when passed an invalid child", () => { test("createTextSpan should throw when passed an invalid child", () => {
expect(() => createInline("Hello, World!")).toThrowError( expect(() => createTextSpan("Hello, World!")).toThrowError(
"Invalid inline child", "Invalid textSpan child",
); );
}); });
test("createInline creates a new inline element with a <br> inside", () => { test("createTextSpan creates a new textSpan element with a <br> inside", () => {
const inline = createInline(createLineBreak()); const textSpan = createTextSpan(createLineBreak());
expect(inline).toBeInstanceOf(HTMLSpanElement); expect(textSpan).toBeInstanceOf(HTMLSpanElement);
expect(inline.dataset.itype).toBe(TYPE); expect(textSpan.dataset.itype).toBe(TYPE);
expect(inline.nodeName).toBe(TAG); expect(textSpan.nodeName).toBe(TAG);
expect(inline.textContent).toBe(""); expect(textSpan.textContent).toBe("");
expect(inline.firstChild).toBeInstanceOf(HTMLBRElement); expect(textSpan.firstChild).toBeInstanceOf(HTMLBRElement);
}); });
test("createInline creates a new inline element with a text inside", () => { test("createTextSpan creates a new textSpan element with a text inside", () => {
const inline = createInline(new Text("Hello, World!")); const textSpan = createTextSpan(new Text("Hello, World!"));
expect(inline).toBeInstanceOf(HTMLSpanElement); expect(textSpan).toBeInstanceOf(HTMLSpanElement);
expect(inline.dataset.itype).toBe(TYPE); expect(textSpan.dataset.itype).toBe(TYPE);
expect(inline.nodeName).toBe(TAG); expect(textSpan.nodeName).toBe(TAG);
expect(inline.textContent).toBe("Hello, World!"); expect(textSpan.textContent).toBe("Hello, World!");
expect(inline.firstChild).toBeInstanceOf(Text); expect(textSpan.firstChild).toBeInstanceOf(Text);
}); });
test("createEmptyInline creates a new empty inline element with a <br> inside", () => { test("createEmptyTextSpan creates a new empty textSpan element with a <br> inside", () => {
const emptyInline = createEmptyInline(); const emptyTextSpan = createEmptyTextSpan();
expect(emptyInline).toBeInstanceOf(HTMLSpanElement); expect(emptyTextSpan).toBeInstanceOf(HTMLSpanElement);
expect(emptyInline.dataset.itype).toBe(TYPE); expect(emptyTextSpan.dataset.itype).toBe(TYPE);
expect(emptyInline.nodeName).toBe(TAG); expect(emptyTextSpan.nodeName).toBe(TAG);
expect(emptyInline.textContent).toBe(""); expect(emptyTextSpan.textContent).toBe("");
expect(emptyInline.firstChild).toBeInstanceOf(HTMLBRElement); expect(emptyTextSpan.firstChild).toBeInstanceOf(HTMLBRElement);
}); });
test("isInline should return true on elements that are inlines", () => { test("isTextSpan should return true on elements that are text spans", () => {
const inline = createInline(new Text("Hello, World!")); const textSpan = createTextSpan(new Text("Hello, World!"));
expect(isInline(inline)).toBe(true); expect(isTextSpan(textSpan)).toBe(true);
const a = document.createElement("a"); const a = document.createElement("a");
expect(isInline(a)).toBe(false); expect(isTextSpan(a)).toBe(false);
const b = null; const b = null;
expect(isInline(b)).toBe(false); expect(isTextSpan(b)).toBe(false);
const c = document.createElement("span"); const c = document.createElement("span");
expect(isInline(c)).toBe(false); expect(isTextSpan(c)).toBe(false);
}); });
test("isLikeInline should return true on elements that have inline behavior by default", () => { test("isLikeTextSpan should return true on elements that have textSpan behavior by default", () => {
expect(isLikeInline(Infinity)).toBe(false); expect(isLikeTextSpan(Infinity)).toBe(false);
expect(isLikeInline(null)).toBe(false); expect(isLikeTextSpan(null)).toBe(false);
expect(isLikeInline(document.createElement("A"))).toBe(true); expect(isLikeTextSpan(document.createElement("A"))).toBe(true);
}); });
// FIXME: Should throw? // FIXME: Should throw?
test("isInlineStart returns false when passed node is not an inline", () => { test("isTextSpanStart returns false when passed node is not an textSpan", () => {
const inline = document.createElement("div"); const textSpan = document.createElement("div");
expect(isInlineStart(inline, 0)).toBe(false); expect(isTextSpanStart(textSpan, 0)).toBe(false);
expect(isInlineStart(inline, "Hello, World!".length)).toBe(false); expect(isTextSpanStart(textSpan, "Hello, World!".length)).toBe(false);
}); });
test("isInlineStart returns if we're at the start of an inline", () => { test("isTextSpanStart returns if we're at the start of an textSpan", () => {
const inline = createInline(new Text("Hello, World!")); const textSpan = createTextSpan(new Text("Hello, World!"));
expect(isInlineStart(inline, 0)).toBe(true); expect(isTextSpanStart(textSpan, 0)).toBe(true);
expect(isInlineStart(inline, "Hello, World!".length)).toBe(false); expect(isTextSpanStart(textSpan, "Hello, World!".length)).toBe(false);
}); });
// FIXME: Should throw? // FIXME: Should throw?
test("isInlineEnd returns false when passed node is not an inline", () => { test("isTextSpanEnd returns false when passed node is not an textSpan", () => {
const inline = document.createElement("div"); const textSpan = document.createElement("div");
expect(isInlineEnd(inline, 0)).toBe(false); expect(isTextSpanEnd(textSpan, 0)).toBe(false);
expect(isInlineEnd(inline, "Hello, World!".length)).toBe(false); expect(isTextSpanEnd(textSpan, "Hello, World!".length)).toBe(false);
}); });
test("isInlineEnd returns if we're in the end of an inline", () => { test("isTextSpanEnd returns if we're in the end of an textSpan", () => {
const inline = createInline(new Text("Hello, World!")); const textSpan = createTextSpan(new Text("Hello, World!"));
expect(isInlineEnd(inline, 0)).toBe(false); expect(isTextSpanEnd(textSpan, 0)).toBe(false);
expect(isInlineEnd(inline, "Hello, World!".length)).toBe(true); expect(isTextSpanEnd(textSpan, "Hello, World!".length)).toBe(true);
}); });
test("getInline ", () => { test("getTextSpan ", () => {
expect(getInline(null)).toBe(null); expect(getTextSpan(null)).toBe(null);
}); });
test("getInlineLength throws when the passed node is not an inline", () => { test("getTextSpanLength throws when the passed node is not an textSpan", () => {
const inline = document.createElement("div"); const textSpan = document.createElement("div");
expect(() => getInlineLength(inline)).toThrowError("Invalid inline"); expect(() => getTextSpanLength(textSpan)).toThrowError("Invalid textSpan");
}); });
test("getInlineLength returns the length of the inline content", () => { test("getTextSpanLength returns the length of the textSpan content", () => {
const inline = createInline(new Text("Hello, World!")); const textSpan = createTextSpan(new Text("Hello, World!"));
expect(getInlineLength(inline)).toBe(13); expect(getTextSpanLength(textSpan)).toBe(13);
}); });
test("getInlineLength should return 0 when the inline content is a <br>", () => { test("getTextSpanLength should return 0 when the textSpan content is a <br>", () => {
const emptyInline = createEmptyInline(); const emptyTextSpan = createEmptyTextSpan();
expect(getInlineLength(emptyInline)).toBe(0); expect(getTextSpanLength(emptyTextSpan)).toBe(0);
}); });
test("splitInline returns a new inline from the splitted inline", () => { test("splitTextSpan returns a new textSpan from the splitted textSpan", () => {
const inline = createInline(new Text("Hello, World!")); const textSpan = createTextSpan(new Text("Hello, World!"));
const newInline = splitInline(inline, 5); const newTextSpan = splitTextSpan(textSpan, 5);
expect(newInline).toBeInstanceOf(HTMLSpanElement); expect(newTextSpan).toBeInstanceOf(HTMLSpanElement);
expect(newInline.firstChild).toBeInstanceOf(Text); expect(newTextSpan.firstChild).toBeInstanceOf(Text);
expect(newInline.textContent).toBe(", World!"); expect(newTextSpan.textContent).toBe(", World!");
expect(newInline.dataset.itype).toBe(TYPE); expect(newTextSpan.dataset.itype).toBe(TYPE);
expect(newInline.nodeName).toBe(TAG); expect(newTextSpan.nodeName).toBe(TAG);
}); });
}); });

View File

@@ -14,15 +14,15 @@ import {
isOffsetAtEnd, isOffsetAtEnd,
} from "./Element.js"; } from "./Element.js";
import { import {
isInline, isTextSpan,
isLikeInline, isLikeTextSpan,
getInline, getTextSpan,
getInlinesFrom, getTextSpansFrom,
createInline, createTextSpan,
createEmptyInline, createEmptyTextSpan,
isInlineEnd, isTextSpanEnd,
splitInline, splitTextSpan,
} from "./Inline.js"; } from "./TextSpan.js";
import { createLineBreak, isLineBreak } from "./LineBreak.js"; import { createLineBreak, isLineBreak } from "./LineBreak.js";
import { setStyles } from "./Style.js"; import { setStyles } from "./Style.js";
@@ -50,7 +50,7 @@ export const STYLES = [
/** /**
* FIXME: This is a fix for Chrome that removes the * FIXME: This is a fix for Chrome that removes the
* current inline when the last character is deleted * current text span when the last character is deleted
* in `insertCompositionText`. * in `insertCompositionText`.
* *
* @param {*} node * @param {*} node
@@ -60,7 +60,7 @@ export function fixParagraph(node) {
return; return;
} }
const br = createLineBreak(); const br = createLineBreak();
node.replaceChildren(createInline(br)); node.replaceChildren(createTextSpan(br));
return br; return br;
} }
@@ -74,7 +74,7 @@ export function fixParagraph(node) {
* @returns {boolean} * @returns {boolean}
*/ */
export function isLikeParagraph(element) { export function isLikeParagraph(element) {
return !isLikeInline(element); return !isLikeTextSpan(element);
} }
/** /**
@@ -85,9 +85,9 @@ export function isLikeParagraph(element) {
*/ */
export function isEmptyParagraph(element) { export function isEmptyParagraph(element) {
if (!isParagraph(element)) throw new TypeError("Invalid paragraph"); if (!isParagraph(element)) throw new TypeError("Invalid paragraph");
const inline = element.firstChild; const textSpan = element.firstChild;
if (!isInline(inline)) throw new TypeError("Invalid inline"); if (!isTextSpan(textSpan)) throw new TypeError("Invalid text span");
return isLineBreak(inline.firstChild); return isLineBreak(textSpan.firstChild);
} }
/** /**
@@ -106,20 +106,20 @@ export function isParagraph(node) {
/** /**
* Creates a new paragraph. * Creates a new paragraph.
* *
* @param {Array<HTMLDivElement>} inlines * @param {Array<HTMLDivElement>} textSpans
* @param {Object.<string, *>|CSSStyleDeclaration} styles * @param {Object.<string, *>|CSSStyleDeclaration} styles
* @param {Object.<string, *>} [attrs] * @param {Object.<string, *>} [attrs]
* @returns {HTMLDivElement} * @returns {HTMLDivElement}
*/ */
export function createParagraph(inlines, styles, attrs) { export function createParagraph(textSpans, styles, attrs) {
if (inlines && (!Array.isArray(inlines) || !inlines.every(isInline))) if (textSpans && (!Array.isArray(textSpans) || !textSpans.every(isTextSpan)))
throw new TypeError("Invalid paragraph children"); throw new TypeError("Invalid paragraph children");
return createElement(TAG, { return createElement(TAG, {
attributes: { id: createRandomId(), ...attrs }, attributes: { id: createRandomId(), ...attrs },
data: { itype: TYPE }, data: { itype: TYPE },
styles: styles, styles: styles,
allowedStyles: STYLES, allowedStyles: STYLES,
children: inlines, children: textSpans,
}); });
} }
@@ -130,7 +130,7 @@ export function createParagraph(inlines, styles, attrs) {
* @returns {HTMLDivElement} * @returns {HTMLDivElement}
*/ */
export function createEmptyParagraph(styles) { export function createEmptyParagraph(styles) {
return createParagraph([createEmptyInline(styles)], styles); return createParagraph([createEmptyTextSpan(styles)], styles);
} }
/** /**
@@ -177,11 +177,11 @@ export function getParagraph(node) {
export function isParagraphStart(node, offset) { export function isParagraphStart(node, offset) {
const paragraph = getParagraph(node); const paragraph = getParagraph(node);
if (!paragraph) throw new Error("Can't find the paragraph"); if (!paragraph) throw new Error("Can't find the paragraph");
const inline = getInline(node); const textSpan = getTextSpan(node);
if (!inline) throw new Error("Can't find the inline"); if (!textSpan) throw new Error("Can't find the text span");
return ( return (
paragraph.firstElementChild === inline && paragraph.firstElementChild === textSpan &&
isOffsetAtStart(inline.firstChild, offset) isOffsetAtStart(textSpan.firstChild, offset)
); );
} }
@@ -196,11 +196,11 @@ export function isParagraphStart(node, offset) {
export function isParagraphEnd(node, offset) { export function isParagraphEnd(node, offset) {
const paragraph = getParagraph(node); const paragraph = getParagraph(node);
if (!paragraph) throw new Error("Cannot find the paragraph"); if (!paragraph) throw new Error("Cannot find the paragraph");
const inline = getInline(node); const textSpan = getTextSpan(node);
if (!inline) throw new Error("Cannot find the inline"); if (!textSpan) throw new Error("Cannot find the text span");
return ( return (
paragraph.lastElementChild === inline && paragraph.lastElementChild === textSpan &&
isOffsetAtEnd(inline.firstChild, offset) isOffsetAtEnd(textSpan.firstChild, offset)
); );
} }
@@ -208,17 +208,17 @@ export function isParagraphEnd(node, offset) {
* Splits a paragraph. * Splits a paragraph.
* *
* @param {HTMLDivElement} paragraph * @param {HTMLDivElement} paragraph
* @param {HTMLSpanElement} inline * @param {HTMLSpanElement} textSpan
* @param {number} offset * @param {number} offset
*/ */
export function splitParagraph(paragraph, inline, offset) { export function splitParagraph(paragraph, textSpan, offset) {
const style = paragraph.style; const style = paragraph.style;
if (isInlineEnd(inline, offset)) { if (isTextSpanEnd(textSpan, offset)) {
const newParagraph = createParagraph(getInlinesFrom(inline), style); const newParagraph = createParagraph(getTextSpansFrom(textSpan), style);
return newParagraph; return newParagraph;
} }
const newInline = splitInline(inline, offset); const newTextSpan = splitTextSpan(textSpan, offset);
const newParagraph = createParagraph([newInline], style); const newParagraph = createParagraph([newTextSpan], style);
return newParagraph; return newParagraph;
} }
@@ -231,11 +231,11 @@ export function splitParagraph(paragraph, inline, offset) {
export function splitParagraphAtNode(paragraph, startIndex) { export function splitParagraphAtNode(paragraph, startIndex) {
const style = paragraph.style; const style = paragraph.style;
const newParagraph = createParagraph(null, style); const newParagraph = createParagraph(null, style);
const newInlines = []; const newTextSpans = [];
for (let index = startIndex; index < paragraph.children.length; index++) { for (let index = startIndex; index < paragraph.children.length; index++) {
newInlines.push(paragraph.children.item(index)); newTextSpans.push(paragraph.children.item(index));
} }
newParagraph.append(...newInlines); newParagraph.append(...newTextSpans);
return newParagraph; return newParagraph;
} }

View File

@@ -13,7 +13,7 @@ import {
splitParagraphAtNode, splitParagraphAtNode,
isEmptyParagraph, isEmptyParagraph,
} from "./Paragraph.js"; } from "./Paragraph.js";
import { createInline, isInline } from "./Inline.js"; import { createTextSpan, isTextSpan } from "./TextSpan.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe("Paragraph", () => { describe("Paragraph", () => {
@@ -28,7 +28,7 @@ describe("Paragraph", () => {
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement); expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
expect(emptyParagraph.nodeName).toBe(TAG); expect(emptyParagraph.nodeName).toBe(TAG);
expect(emptyParagraph.dataset.itype).toBe(TYPE); expect(emptyParagraph.dataset.itype).toBe(TYPE);
expect(isInline(emptyParagraph.firstChild)).toBe(true); expect(isTextSpan(emptyParagraph.firstChild)).toBe(true);
}); });
test("isParagraph should return true when the passed node is a paragraph", () => { test("isParagraph should return true when the passed node is a paragraph", () => {
@@ -37,7 +37,7 @@ describe("Paragraph", () => {
expect(isParagraph(document.createElement("h1"))).toBe(false); expect(isParagraph(document.createElement("h1"))).toBe(false);
expect(isParagraph(createEmptyParagraph())).toBe(true); expect(isParagraph(createEmptyParagraph())).toBe(true);
expect( expect(
isParagraph(createParagraph([createInline(new Text("Hello, World!"))])), isParagraph(createParagraph([createTextSpan(new Text("Hello, World!"))])),
).toBe(true); ).toBe(true);
}); });
@@ -62,8 +62,8 @@ describe("Paragraph", () => {
test("getParagraph should return the closest paragraph of the passed node", () => { test("getParagraph should return the closest paragraph of the passed node", () => {
const text = new Text("Hello, World!"); const text = new Text("Hello, World!");
const inline = createInline(text); const textSpan = createTextSpan(text);
const paragraph = createParagraph([inline]); const paragraph = createParagraph([textSpan]);
expect(getParagraph(text)).toBe(paragraph); expect(getParagraph(text)).toBe(paragraph);
}); });
@@ -81,7 +81,7 @@ describe("Paragraph", () => {
test("isParagraphStart should return true on a paragraph", () => { test("isParagraphStart should return true on a paragraph", () => {
const paragraph = createParagraph([ const paragraph = createParagraph([
createInline(new Text("Hello, World!")), createTextSpan(new Text("Hello, World!")),
]); ]);
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true); expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true);
}); });
@@ -93,15 +93,15 @@ describe("Paragraph", () => {
test("isParagraphEnd should return true on a paragraph", () => { test("isParagraphEnd should return true on a paragraph", () => {
const paragraph = createParagraph([ const paragraph = createParagraph([
createInline(new Text("Hello, World!")), createTextSpan(new Text("Hello, World!")),
]); ]);
expect(isParagraphEnd(paragraph.firstChild.firstChild, 13)).toBe(true); expect(isParagraphEnd(paragraph.firstChild.firstChild, 13)).toBe(true);
}); });
test("splitParagraph should split a paragraph", () => { test("splitParagraph should split a paragraph", () => {
const inline = createInline(new Text("Hello, World!")); const textSpan = createTextSpan(new Text("Hello, World!"));
const paragraph = createParagraph([inline]); const paragraph = createParagraph([textSpan]);
const newParagraph = splitParagraph(paragraph, inline, 6); const newParagraph = splitParagraph(paragraph, textSpan, 6);
expect(newParagraph).toBeInstanceOf(HTMLDivElement); expect(newParagraph).toBeInstanceOf(HTMLDivElement);
expect(newParagraph.nodeName).toBe(TAG); expect(newParagraph.nodeName).toBe(TAG);
expect(newParagraph.dataset.itype).toBe(TYPE); expect(newParagraph.dataset.itype).toBe(TYPE);
@@ -109,10 +109,10 @@ describe("Paragraph", () => {
}); });
test("splitParagraphAtNode should split a paragraph at a specified node", () => { test("splitParagraphAtNode should split a paragraph at a specified node", () => {
const helloInline = createInline(new Text("Hello, ")); const helloTextSpan = createTextSpan(new Text("Hello, "));
const worldInline = createInline(new Text("World")); const worldTextSpan = createTextSpan(new Text("World"));
const exclInline = createInline(new Text("!")); const exclTextSpan = createTextSpan(new Text("!"));
const paragraph = createParagraph([helloInline, worldInline, exclInline]); const paragraph = createParagraph([helloTextSpan, worldTextSpan, exclTextSpan]);
const newParagraph = splitParagraphAtNode(paragraph, 1); const newParagraph = splitParagraphAtNode(paragraph, 1);
expect(newParagraph).toBeInstanceOf(HTMLDivElement); expect(newParagraph).toBeInstanceOf(HTMLDivElement);
expect(newParagraph.nodeName).toBe(TAG); expect(newParagraph.nodeName).toBe(TAG);
@@ -121,7 +121,7 @@ describe("Paragraph", () => {
expect(newParagraph.textContent).toBe("World!"); expect(newParagraph.textContent).toBe("World!");
}); });
test("isLikeParagraph should return true if the element it's not an inline element", () => { test("isLikeParagraph should return true if the element it's not an text span element", () => {
const span = document.createElement("span"); const span = document.createElement("span");
const a = document.createElement("a"); const a = document.createElement("a");
const br = document.createElement("br"); const br = document.createElement("br");
@@ -149,23 +149,23 @@ describe("Paragraph", () => {
paragraph.dataset.itype = "paragraph"; paragraph.dataset.itype = "paragraph";
paragraph.appendChild(document.createElement("svg")); paragraph.appendChild(document.createElement("svg"));
isEmptyParagraph(paragraph); isEmptyParagraph(paragraph);
}).toThrowError("Invalid inline"); }).toThrowError("Invalid text span");
const lineBreak = document.createElement("br"); const lineBreak = document.createElement("br");
const emptyInline = document.createElement("span"); const emptyTextSpan = document.createElement("span");
emptyInline.dataset.itype = "inline"; emptyTextSpan.dataset.itype = "span";
emptyInline.appendChild(lineBreak); emptyTextSpan.appendChild(lineBreak);
const emptyParagraph = document.createElement("div"); const emptyParagraph = document.createElement("div");
emptyParagraph.dataset.itype = "paragraph"; emptyParagraph.dataset.itype = "paragraph";
emptyParagraph.appendChild(emptyInline); emptyParagraph.appendChild(emptyTextSpan);
expect(isEmptyParagraph(emptyParagraph)).toBe(true); expect(isEmptyParagraph(emptyParagraph)).toBe(true);
const nonEmptyInline = document.createElement("span"); const nonEmptyTextSpan = document.createElement("span");
nonEmptyInline.dataset.itype = "inline"; nonEmptyTextSpan.dataset.itype = "span";
nonEmptyInline.appendChild(new Text("Not empty!")); nonEmptyTextSpan.appendChild(new Text("Not empty!"));
const nonEmptyParagraph = document.createElement("div"); const nonEmptyParagraph = document.createElement("div");
nonEmptyParagraph.dataset.itype = "paragraph"; nonEmptyParagraph.dataset.itype = "paragraph";
nonEmptyParagraph.appendChild(nonEmptyInline); nonEmptyParagraph.appendChild(nonEmptyTextSpan);
expect(isEmptyParagraph(nonEmptyParagraph)).toBe(false); expect(isEmptyParagraph(nonEmptyParagraph)).toBe(false);
}); });
}); });

View File

@@ -6,7 +6,7 @@
* Copyright (c) KALEIDOS INC * Copyright (c) KALEIDOS INC
*/ */
import { isInline } from "./Inline.js"; import { isTextSpan } from "./TextSpan.js";
import { isLineBreak } from "./LineBreak.js"; import { isLineBreak } from "./LineBreak.js";
import { isParagraph } from "./Paragraph.js"; import { isParagraph } from "./Paragraph.js";
import { isEditor } from "./Editor.js"; import { isEditor } from "./Editor.js";
@@ -58,7 +58,7 @@ export function getTextNodeLength(node) {
*/ */
export function getClosestTextNode(node) { export function getClosestTextNode(node) {
if (isTextNode(node)) return node; if (isTextNode(node)) return node;
if (isInline(node)) return node.firstChild; if (isTextSpan(node)) return node.firstChild;
if (isParagraph(node)) return node.firstChild.firstChild; if (isParagraph(node)) return node.firstChild.firstChild;
if (isRoot(node)) return node.firstChild.firstChild.firstChild; if (isRoot(node)) return node.firstChild.firstChild.firstChild;
if (isEditor(node)) return node.firstChild.firstChild.firstChild.firstChild; if (isEditor(node)) return node.firstChild.firstChild.firstChild.firstChild;

View File

@@ -1,6 +1,6 @@
import { describe, test, expect } from "vitest"; import { describe, test, expect } from "vitest";
import TextNodeIterator from "./TextNodeIterator.js"; import TextNodeIterator from "./TextNodeIterator.js";
import { createInline } from "./Inline.js"; import { createTextSpan } from "./TextSpan.js";
import { createParagraph } from "./Paragraph.js"; import { createParagraph } from "./Paragraph.js";
import { createRoot } from "./Root.js"; import { createRoot } from "./Root.js";
import { createLineBreak } from "./LineBreak.js"; import { createLineBreak } from "./LineBreak.js";
@@ -21,16 +21,16 @@ describe("TextNodeIterator", () => {
test("Create a new TextNodeIterator and iterate only over text nodes", () => { test("Create a new TextNodeIterator and iterate only over text nodes", () => {
const rootNode = createRoot([ const rootNode = createRoot([
createParagraph([ createParagraph([
createInline(new Text("Hello, ")), createTextSpan(new Text("Hello, ")),
createInline(new Text("World!")), createTextSpan(new Text("World!")),
createInline(new Text("Whatever")), createTextSpan(new Text("Whatever")),
]), ]),
createParagraph([createInline(createLineBreak())]), createParagraph([createTextSpan(createLineBreak())]),
createParagraph([ createParagraph([
createInline(new Text("This is a ")), createTextSpan(new Text("This is a ")),
createInline(new Text("test")), createTextSpan(new Text("test")),
]), ]),
createParagraph([createInline(new Text("Hi!"))]), createParagraph([createTextSpan(new Text("Hi!"))]),
]); ]);
const textNodeIterator = new TextNodeIterator(rootNode); const textNodeIterator = new TextNodeIterator(rootNode);

View File

@@ -17,7 +17,7 @@ import { setStyles, mergeStyles } from "./Style.js";
import { createRandomId } from "./Element.js"; import { createRandomId } from "./Element.js";
export const TAG = "SPAN"; export const TAG = "SPAN";
export const TYPE = "inline"; export const TYPE = "span";
export const QUERY = `[data-itype="${TYPE}"]`; export const QUERY = `[data-itype="${TYPE}"]`;
export const STYLES = [ export const STYLES = [
["--typography-ref-id"], ["--typography-ref-id"],
@@ -37,12 +37,12 @@ export const STYLES = [
]; ];
/** /**
* Returns true if passed node is an inline. * Returns true if passed node is a text span.
* *
* @param {Node} node * @param {Node} node
* @returns {boolean} * @returns {boolean}
*/ */
export function isInline(node) { export function isTextSpan(node) {
if (!node) return false; if (!node) return false;
if (!isElement(node, TAG)) return false; if (!isElement(node, TAG)) return false;
if (node.dataset.itype !== TYPE) return false; if (node.dataset.itype !== TYPE) return false;
@@ -50,13 +50,13 @@ export function isInline(node) {
} }
/** /**
* Returns true if the passed node "behaves" like an * Returns true if the passed node "behaves" like a
* inline. * text span.
* *
* @param {Node} element * @param {Node} element
* @returns {boolean} * @returns {boolean}
*/ */
export function isLikeInline(element) { export function isLikeTextSpan(element) {
return element return element
? [ ? [
"A", "A",
@@ -97,26 +97,26 @@ export function isLikeInline(element) {
} }
/** /**
* Creates a new Inline * Creates a new TextSpan
* *
* @param {Text|HTMLBRElement} text * @param {Text|HTMLBRElement} text
* @param {Object.<string, *>|CSSStyleDeclaration} styles * @param {Object.<string, *>|CSSStyleDeclaration} styles
* @param {Object.<string, *>} [attrs] * @param {Object.<string, *>} [attrs]
* @returns {HTMLSpanElement} * @returns {HTMLSpanElement}
*/ */
export function createInline(textOrLineBreak, styles, attrs) { export function createTextSpan(textOrLineBreak, styles, attrs) {
if ( if (
!(textOrLineBreak instanceof HTMLBRElement) && !(textOrLineBreak instanceof HTMLBRElement) &&
!(textOrLineBreak instanceof Text) !(textOrLineBreak instanceof Text)
) { ) {
throw new TypeError("Invalid inline child"); throw new TypeError("Invalid text span child");
} }
if ( if (
textOrLineBreak instanceof Text && textOrLineBreak instanceof Text &&
textOrLineBreak.nodeValue.length === 0 textOrLineBreak.nodeValue.length === 0
) { ) {
console.trace("nodeValue", textOrLineBreak.nodeValue); console.trace("nodeValue", textOrLineBreak.nodeValue);
throw new TypeError("Invalid inline child, cannot be an empty text"); throw new TypeError("Invalid text span child, cannot be an empty text");
} }
return createElement(TAG, { return createElement(TAG, {
attributes: { id: createRandomId(), ...attrs }, attributes: { id: createRandomId(), ...attrs },
@@ -128,34 +128,42 @@ export function createInline(textOrLineBreak, styles, attrs) {
} }
/** /**
* Creates a new inline from an older inline. This only * Creates a new text span from an older text span. This only
* merges styles from the older inline to the new inline. * merges styles from the older text span to the new text span.
* *
* @param {HTMLSpanElement} inline * @param {HTMLSpanElement} textSpan
* @param {Object.<string, *>} textOrLineBreak * @param {Object.<string, *>} textOrLineBreak
* @param {Object.<string, *>|CSSStyleDeclaration} styles * @param {Object.<string, *>|CSSStyleDeclaration} styles
* @param {Object.<string, *>} [attrs] * @param {Object.<string, *>} [attrs]
* @returns {HTMLSpanElement} * @returns {HTMLSpanElement}
*/ */
export function createInlineFrom(inline, textOrLineBreak, styles, attrs) { export function createTextSpanFrom(textSpan, textOrLineBreak, styles, attrs) {
return createInline( return createTextSpan(
textOrLineBreak, textOrLineBreak,
mergeStyles(STYLES, inline.style, styles), mergeStyles(STYLES, textSpan.style, styles),
attrs, attrs,
); );
} }
/** /**
* Creates a new empty inline. * Creates a new empty text span.
* *
* @param {Object.<string,*>|CSSStyleDeclaration} styles * @param {Object.<string,*>|CSSStyleDeclaration} styles
* @returns {HTMLSpanElement} * @returns {HTMLSpanElement}
*/ */
export function createEmptyInline(styles) { export function createEmptyTextSpan(styles) {
return createInline(createLineBreak(), styles); return createTextSpan(createLineBreak(), styles);
} }
export function createVoidInline(styles) { /**
* Creates a new text span with an empty text. The difference between
* this and the createEmptyTextSpan is that createEmptyTextSpan creates
* a text span with a <br> inside.
*
* @param {Object.<string,*>|CSSStyleDeclaration} styles
* @returns {HTMLSpanElement}
*/
export function createVoidTextSpan(styles) {
return createElement(TAG, { return createElement(TAG, {
attributes: { id: createRandomId() }, attributes: { id: createRandomId() },
data: { itype: TYPE }, data: { itype: TYPE },
@@ -166,116 +174,116 @@ export function createVoidInline(styles) {
} }
/** /**
* Sets the inline styles. * Sets the text span styles.
* *
* @param {HTMLSpanElement} element * @param {HTMLSpanElement} element
* @param {Object.<string,*>|CSSStyleDeclaration} styles * @param {Object.<string,*>|CSSStyleDeclaration} styles
* @returns {HTMLSpanElement} * @returns {HTMLSpanElement}
*/ */
export function setInlineStyles(element, styles) { export function setTextSpanStyles(element, styles) {
return setStyles(element, STYLES, styles); return setStyles(element, STYLES, styles);
} }
/** /**
* Gets the closest inline from a node. * Gets the closest text span from a node.
* *
* @param {Node} node * @param {Node} node
* @returns {HTMLElement|null} * @returns {HTMLElement|null}
*/ */
export function getInline(node) { export function getTextSpan(node) {
if (!node) return null; // FIXME: Should throw? if (!node) return null; // FIXME: Should throw?
if (isInline(node)) return node; if (isTextSpan(node)) return node;
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
const inline = node?.parentElement; const textSpan = node?.parentElement;
if (!inline) return null; if (!textSpan) return null;
if (!isInline(inline)) return null; if (!isTextSpan(textSpan)) return null;
return inline; return textSpan;
} }
return node.closest(QUERY); return node.closest(QUERY);
} }
/** /**
* Returns true if we are at the start offset * Returns true if we are at the start offset
* of an inline. * of a text span.
* *
* NOTE: Only the first inline returns this as true * NOTE: Only the first text span returns this as true
* *
* @param {TextNode|HTMLBRElement} node * @param {TextNode|HTMLBRElement} node
* @param {number} offset * @param {number} offset
* @returns {boolean} * @returns {boolean}
*/ */
export function isInlineStart(node, offset) { export function isTextSpanStart(node, offset) {
const inline = getInline(node); const textSpan = getTextSpan(node);
if (!inline) return false; if (!textSpan) return false;
return isOffsetAtStart(inline, offset); return isOffsetAtStart(textSpan, offset);
} }
/** /**
* Returns true if we are at the end offset * Returns true if we are at the end offset
* of an inline. * of a text span.
* *
* @param {TextNode|HTMLBRElement} node * @param {TextNode|HTMLBRElement} node
* @param {number} offset * @param {number} offset
* @returns {boolean} * @returns {boolean}
*/ */
export function isInlineEnd(node, offset) { export function isTextSpanEnd(node, offset) {
const inline = getInline(node); const textSpan = getTextSpan(node);
if (!inline) return false; if (!textSpan) return false;
return isOffsetAtEnd(inline.firstChild, offset); return isOffsetAtEnd(textSpan.firstChild, offset);
} }
/** /**
* Splits an inline. * Splits a text span.
* *
* @param {HTMLSpanElement} inline * @param {HTMLSpanElement} textSpan
* @param {number} offset * @param {number} offset
*/ */
export function splitInline(inline, offset) { export function splitTextSpan(textSpan, offset) {
const textNode = inline.firstChild; const textNode = textSpan.firstChild;
const style = inline.style; const style = textSpan.style;
const newTextNode = textNode.splitText(offset); const newTextNode = textNode.splitText(offset);
return createInline(newTextNode, style); return createTextSpan(newTextNode, style);
} }
/** /**
* Returns all the inlines of a paragraph starting at * Returns all the text spans of a paragraph starting at
* the specified inline. * the specified text span.
* *
* @param {HTMLSpanElement} startInline * @param {HTMLSpanElement} startTextSpan
* @returns {Array<HTMLSpanElement>} * @returns {Array<HTMLSpanElement>}
*/ */
export function getInlinesFrom(startInline) { export function getTextSpansFrom(startTextSpan) {
const inlines = []; const textSpans = [];
let currentInline = startInline; let currentTextSpan = startTextSpan;
let index = 0; let index = 0;
while (currentInline) { while (currentTextSpan) {
if (index > 0) inlines.push(currentInline); if (index > 0) textSpans.push(currentTextSpan);
currentInline = currentInline.nextElementSibling; currentTextSpan = currentTextSpan.nextElementSibling;
index++; index++;
} }
return inlines; return textSpans;
} }
/** /**
* Returns the length of an inline. * Returns the length of a text span.
* *
* @param {HTMLElement} inline * @param {HTMLElement} textSpan
* @returns {number} * @returns {number}
*/ */
export function getInlineLength(inline) { export function getTextSpanLength(textSpan) {
if (!isInline(inline)) throw new Error("Invalid inline"); if (!isTextSpan(textSpan)) throw new Error("Invalid text span");
if (isLineBreak(inline.firstChild)) return 0; if (isLineBreak(textSpan.firstChild)) return 0;
return inline.firstChild.nodeValue.length; return textSpan.firstChild.nodeValue.length;
} }
/** /**
* Merges two inlines. * Merges two text spans.
* *
* @param {HTMLSpanElement} a * @param {HTMLSpanElement} a
* @param {HTMLSpanElement} b * @param {HTMLSpanElement} b
* @returns {HTMLSpanElement} * @returns {HTMLSpanElement}
*/ */
export function mergeInlines(a, b) { export function mergeTextSpans(a, b) {
a.append(...b.childNodes); a.append(...b.childNodes);
b.remove(); b.remove();
// We need to normalize Text nodes. // We need to normalize Text nodes.

View File

@@ -8,18 +8,18 @@
import { createLineBreak, isLineBreak } from "../content/dom/LineBreak.js"; import { createLineBreak, isLineBreak } from "../content/dom/LineBreak.js";
import { import {
createInline, createTextSpan,
createInlineFrom, createTextSpanFrom,
getInline, getTextSpan,
getInlineLength, getTextSpanLength,
isInline, isTextSpan,
isInlineStart, isTextSpanStart,
isInlineEnd, isTextSpanEnd,
setInlineStyles, setTextSpanStyles,
splitInline, splitTextSpan,
createEmptyInline, createEmptyTextSpan,
createVoidInline, createVoidTextSpan,
} from "../content/dom/Inline.js"; } from "../content/dom/TextSpan.js";
import { import {
createEmptyParagraph, createEmptyParagraph,
isEmptyParagraph, isEmptyParagraph,
@@ -62,9 +62,9 @@ import SafeGuard from "./SafeGuard.js";
/** /**
* SelectionController uses the same concepts used by the Selection API but extending it to support * SelectionController uses the same concepts used by the Selection API but extending it to support
* our own internal model based on paragraphs (in drafconst textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ * our own internal model based on paragraphs (in drafconst textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, "))]), createParagraph([createTextSpan(new Text("Hello, "))]),
createEmptyParagraph(), createEmptyParagraph(),
createParagraph([createInline(new Text("World!"))]), createParagraph([createTextSpan(new Text("World!"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -88,12 +88,12 @@ import SafeGuard from "./SafeGuard.js";
HTMLSpanElement HTMLSpanElement
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline" "span"
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, "); expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
expect(textEditorMock.root.lastChild.textContent).toBe("World!"); expect(textEditorMock.root.lastChild.textContent).toBe("World!");
t.js they were called blocks) and inlines. t.js they were called blocks) and text spans.
*/ */
export class SelectionController extends EventTarget { export class SelectionController extends EventTarget {
/** /**
@@ -228,7 +228,7 @@ export class SelectionController extends EventTarget {
} }
/** /**
* Styles of the current inline. * Styles of the current text span.
* *
* @type {CSSStyleDeclaration} * @type {CSSStyleDeclaration}
*/ */
@@ -266,18 +266,18 @@ export class SelectionController extends EventTarget {
} }
/** /**
* Updates current styles based on the currently selected inline. * Updates current styles based on the currently selected text span.
* *
* @param {HTMLSpanElement} inline * @param {HTMLSpanElement} textSpan
* @returns {SelectionController} * @returns {SelectionController}
*/ */
#updateCurrentStyle(inline) { #updateCurrentStyle(textSpan) {
this.#applyDefaultStylesToCurrentStyle(); this.#applyDefaultStylesToCurrentStyle();
const root = inline.parentElement.parentElement; const root = textSpan.parentElement.parentElement;
this.#applyStylesToCurrentStyle(root); this.#applyStylesToCurrentStyle(root);
const paragraph = inline.parentElement; const paragraph = textSpan.parentElement;
this.#applyStylesToCurrentStyle(paragraph); this.#applyStylesToCurrentStyle(paragraph);
this.#applyStylesToCurrentStyle(inline); this.#applyStylesToCurrentStyle(textSpan);
return this; return this;
} }
@@ -334,7 +334,7 @@ export class SelectionController extends EventTarget {
} }
// If focus node changed, we need to retrieve all the // If focus node changed, we need to retrieve all the
// styles of the current inline and dispatch an event // styles of the current text span and dispatch an event
// to notify that the styles have changed. // to notify that the styles have changed.
if (focusNodeChanges) { if (focusNodeChanges) {
this.#notifyStyleChange(); this.#notifyStyleChange();
@@ -355,19 +355,19 @@ export class SelectionController extends EventTarget {
* Notifies that the styles have changed. * Notifies that the styles have changed.
*/ */
#notifyStyleChange() { #notifyStyleChange() {
const inline = this.focusInline; const textSpan = this.focusTextSpan;
if (inline) { if (textSpan) {
this.#updateCurrentStyle(inline); this.#updateCurrentStyle(textSpan);
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("stylechange", { new CustomEvent("stylechange", {
detail: this.#currentStyle, detail: this.#currentStyle,
}), }),
); );
} else { } else {
const firstInline = const firstTextSpan =
this.#textEditor.root?.firstElementChild?.firstElementChild; this.#textEditor.root?.firstElementChild?.firstElementChild;
if (firstInline) { if (firstTextSpan) {
this.#updateCurrentStyle(firstInline); this.#updateCurrentStyle(firstTextSpan);
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("stylechange", { new CustomEvent("stylechange", {
detail: this.#currentStyle, detail: this.#currentStyle,
@@ -766,13 +766,13 @@ export class SelectionController extends EventTarget {
} }
/** /**
* Returns the inline in the focus node * Returns the text span in the focus node
* of the current selection. * of the current selection.
* *
* @type {HTMLElement|null} * @type {HTMLElement|null}
*/ */
get focusInline() { get focusTextSpan() {
return getInline(this.focusNode); return getTextSpan(this.focusNode);
} }
/** /**
@@ -786,13 +786,13 @@ export class SelectionController extends EventTarget {
} }
/** /**
* Returns the current inline in the anchor * Returns the current text span in the anchor
* node of the current selection. * node of the current selection.
* *
* @type {HTMLElement|null} * @type {HTMLElement|null}
*/ */
get anchorInline() { get anchorTextSpan() {
return getInline(this.anchorNode); return getTextSpan(this.anchorNode);
} }
/** /**
@@ -829,14 +829,14 @@ export class SelectionController extends EventTarget {
} }
/** /**
* Start inline of the current page. * Start text span of the current page.
* *
* @type {HTMLElement|null} * @type {HTMLElement|null}
*/ */
get startInline() { get startTextSpan() {
const startContainer = this.startContainer; const startContainer = this.startContainer;
if (!startContainer) return null; if (!startContainer) return null;
return getInline(startContainer); return getTextSpan(startContainer);
} }
/** /**
@@ -876,15 +876,15 @@ export class SelectionController extends EventTarget {
} }
/** /**
* Inline element of the `endContainer` of * TextSpan element of the `endContainer` of
* the current range. * the current range.
* *
* @type {HTMLElement|null} * @type {HTMLElement|null}
*/ */
get endInline() { get endTextSpan() {
const endContainer = this.endContainer; const endContainer = this.endContainer;
if (!endContainer) return null; if (!endContainer) return null;
return getInline(endContainer); return getTextSpan(endContainer);
} }
/** /**
@@ -919,21 +919,21 @@ export class SelectionController extends EventTarget {
} }
/** /**
* Is true if the current focus node is a inline. * Is true if the current focus node is a text span.
* *
* @type {boolean} * @type {boolean}
*/ */
get isInlineFocus() { get isTextSpanFocus() {
return isInline(this.focusNode); return isTextSpan(this.focusNode);
} }
/** /**
* Is true if the current anchor node is a inline. * Is true if the current anchor node is a text span.
* *
* @type {boolean} * @type {boolean}
*/ */
get isInlineAnchor() { get isTextSpanAnchor() {
return isInline(this.anchorNode); return isTextSpan(this.anchorNode);
} }
/** /**
@@ -962,7 +962,7 @@ export class SelectionController extends EventTarget {
get isLineBreakFocus() { get isLineBreakFocus() {
return ( return (
isLineBreak(this.focusNode) || isLineBreak(this.focusNode) ||
(isInline(this.focusNode) && isLineBreak(this.focusNode.firstChild)) (isTextSpan(this.focusNode) && isLineBreak(this.focusNode.firstChild))
); );
} }
@@ -996,35 +996,35 @@ export class SelectionController extends EventTarget {
/** /**
* Indicates that we have selected multiple * Indicates that we have selected multiple
* inline elements. * text span elements.
* *
* @type {boolean} * @type {boolean}
*/ */
get isMultiInline() { get isMultiTextSpan() {
return this.isMulti && this.focusInline !== this.anchorInline; return this.isMulti && this.focusTextSpan !== this.anchorTextSpan;
} }
/** /**
* Indicates that the caret (only the caret) * Indicates that the caret (only the caret)
* is at the start of an inline. * is at the start of an text span.
* *
* @type {boolean} * @type {boolean}
*/ */
get isInlineStart() { get isTextSpanStart() {
if (!this.isCollapsed) return false; if (!this.isCollapsed) return false;
return isInlineStart(this.focusNode, this.focusOffset); return isTextSpanStart(this.focusNode, this.focusOffset);
} }
/** /**
* Indicates that the caret (only the caret) * Indicates that the caret (only the caret)
* is at the end of an inline. This value doesn't * is at the end of an text span. This value doesn't
* matter when dealing with selections. * matter when dealing with selections.
* *
* @type {boolean} * @type {boolean}
*/ */
get isInlineEnd() { get isTextSpanEnd() {
if (!this.isCollapsed) return false; if (!this.isCollapsed) return false;
return isInlineEnd(this.focusNode, this.focusOffset); return isTextSpanEnd(this.focusNode, this.focusOffset);
} }
/** /**
@@ -1055,18 +1055,18 @@ export class SelectionController extends EventTarget {
insertPaste(fragment) { insertPaste(fragment) {
if ( if (
fragment.children.length === 1 && fragment.children.length === 1 &&
fragment.firstElementChild?.dataset?.inline === "force" fragment.firstElementChild?.dataset?.textSpan === "force"
) { ) {
const collapseNode = fragment.firstElementChild.firstChild; const collapseNode = fragment.firstElementChild.firstChild;
if (this.isInlineStart) { if (this.isTextSpanStart) {
this.focusInline.before(...fragment.firstElementChild.children); this.focusTextSpan.before(...fragment.firstElementChild.children);
} else if (this.isInlineEnd) { } else if (this.isTextSpanEnd) {
this.focusInline.after(...fragment.firstElementChild.children); this.focusTextSpan.after(...fragment.firstElementChild.children);
} else { } else {
const newInline = splitInline(this.focusInline, this.focusOffset); const newTextSpan = splitTextSpan(this.focusTextSpan, this.focusOffset);
this.focusInline.after( this.focusTextSpan.after(
...fragment.firstElementChild.children, ...fragment.firstElementChild.children,
newInline, newTextSpan,
); );
} }
return this.collapse(collapseNode, collapseNode.nodeValue.length); return this.collapse(collapseNode, collapseNode.nodeValue.length);
@@ -1085,7 +1085,7 @@ export class SelectionController extends EventTarget {
} else { } else {
const newParagraph = splitParagraph( const newParagraph = splitParagraph(
this.focusParagraph, this.focusParagraph,
this.focusInline, this.focusTextSpan,
this.focusOffset, this.focusOffset,
); );
this.focusParagraph.after(fragment, newParagraph); this.focusParagraph.after(fragment, newParagraph);
@@ -1110,7 +1110,7 @@ export class SelectionController extends EventTarget {
*/ */
replaceLineBreak(text) { replaceLineBreak(text) {
const newText = new Text(text); const newText = new Text(text);
this.focusInline.replaceChildren(newText); this.focusTextSpan.replaceChildren(newText);
this.collapse(newText, text.length); this.collapse(newText, text.length);
} }
@@ -1131,23 +1131,23 @@ export class SelectionController extends EventTarget {
const paragraph = this.focusParagraph; const paragraph = this.focusParagraph;
if (!paragraph) throw new Error("Cannot find paragraph"); if (!paragraph) throw new Error("Cannot find paragraph");
const inline = this.focusInline; const textSpan = this.focusTextSpan;
if (!inline) throw new Error("Cannot find inline"); if (!textSpan) throw new Error("Cannot find text span");
const nextTextNode = this.#textNodeIterator.nextNode(); const nextTextNode = this.#textNodeIterator.nextNode();
if (this.focusNode.nodeValue === "") { if (this.focusNode.nodeValue === "") {
this.focusNode.remove(); this.focusNode.remove();
} }
if (paragraph.childNodes.length === 1 && inline.childNodes.length === 0) { if (paragraph.childNodes.length === 1 && textSpan.childNodes.length === 0) {
const lineBreak = createLineBreak(); const lineBreak = createLineBreak();
inline.appendChild(lineBreak); textSpan.appendChild(lineBreak);
return this.collapse(lineBreak, 0); return this.collapse(lineBreak, 0);
} else if ( } else if (
paragraph.childNodes.length > 1 && paragraph.childNodes.length > 1 &&
inline.childNodes.length === 0 textSpan.childNodes.length === 0
) { ) {
inline.remove(); textSpan.remove();
return this.collapse(nextTextNode, 0); return this.collapse(nextTextNode, 0);
} }
return this.collapse(this.focusNode, this.focusOffset); return this.collapse(this.focusNode, this.focusOffset);
@@ -1177,23 +1177,23 @@ export class SelectionController extends EventTarget {
const paragraph = this.focusParagraph; const paragraph = this.focusParagraph;
if (!paragraph) throw new Error("Cannot find paragraph"); if (!paragraph) throw new Error("Cannot find paragraph");
const inline = this.focusInline; const textSpan = this.focusTextSpan;
if (!inline) throw new Error("Cannot find inline"); if (!textSpan) throw new Error("Cannot find text span");
const previousTextNode = this.#textNodeIterator.previousNode(); const previousTextNode = this.#textNodeIterator.previousNode();
if (this.focusNode.nodeValue === "") { if (this.focusNode.nodeValue === "") {
this.focusNode.remove(); this.focusNode.remove();
} }
if (paragraph.children.length === 1 && inline.childNodes.length === 0) { if (paragraph.children.length === 1 && textSpan.childNodes.length === 0) {
const lineBreak = createLineBreak(); const lineBreak = createLineBreak();
inline.appendChild(lineBreak); textSpan.appendChild(lineBreak);
return this.collapse(lineBreak, 0); return this.collapse(lineBreak, 0);
} else if ( } else if (
paragraph.children.length > 1 && paragraph.children.length > 1 &&
inline.childNodes.length === 0 textSpan.childNodes.length === 0
) { ) {
inline.remove(); textSpan.remove();
return this.collapse( return this.collapse(
previousTextNode, previousTextNode,
getTextNodeLength(previousTextNode), getTextNodeLength(previousTextNode),
@@ -1214,7 +1214,7 @@ export class SelectionController extends EventTarget {
this.focusOffset, this.focusOffset,
newText, newText,
); );
this.#mutations.update(this.focusInline); this.#mutations.update(this.focusTextSpan);
return this.collapse(this.focusNode, this.focusOffset + newText.length); return this.collapse(this.focusNode, this.focusOffset + newText.length);
} }
@@ -1259,36 +1259,36 @@ export class SelectionController extends EventTarget {
this.focusNode.replaceWith(new Text(newText)); this.focusNode.replaceWith(new Text(newText));
} else if (this.isRootFocus) { } else if (this.isRootFocus) {
const newTextNode = new Text(newText); const newTextNode = new Text(newText);
const newInline = createInline(newTextNode, this.#currentStyle); const newTextSpan = createTextSpan(newTextNode, this.#currentStyle);
const newParagraph = createParagraph([newInline], this.#currentStyle); const newParagraph = createParagraph([newTextSpan], this.#currentStyle);
this.focusNode.replaceChildren(newParagraph); this.focusNode.replaceChildren(newParagraph);
return this.collapse(newTextNode, newText.length + 1); return this.collapse(newTextNode, newText.length + 1);
} else { } else {
throw new Error("Unknown node type"); throw new Error("Unknown node type");
} }
this.#mutations.update(this.focusInline); this.#mutations.update(this.focusTextSpan);
return this.collapse(this.focusNode, startOffset + newText.length); return this.collapse(this.focusNode, startOffset + newText.length);
} }
/** /**
* Replaces the selected inlines with new text. * Replaces the selected text spans with new text.
* *
* @param {string} newText * @param {string} newText
*/ */
replaceInlines(newText) { replaceTextSpans(newText) {
const currentParagraph = this.focusParagraph; const currentParagraph = this.focusParagraph;
// This is the special (and fast) case where we're // This is the special (and fast) case where we're
// removing everything inside a paragraph. // removing everything inside a paragraph.
if ( if (
this.startInline === currentParagraph.firstChild && this.startTextSpan === currentParagraph.firstChild &&
this.startOffset === 0 && this.startOffset === 0 &&
this.endInline === currentParagraph.lastChild && this.endTextSpan === currentParagraph.lastChild &&
this.endOffset === currentParagraph.lastChild.textContent.length this.endOffset === currentParagraph.lastChild.textContent.length
) { ) {
const newTextNode = new Text(newText); const newTextNode = new Text(newText);
currentParagraph.replaceChildren( currentParagraph.replaceChildren(
createInline(newTextNode, this.anchorInline.style), createTextSpan(newTextNode, this.anchorTextSpan.style),
); );
return this.collapse(newTextNode, newTextNode.nodeValue.length); return this.collapse(newTextNode, newTextNode.nodeValue.length);
} }
@@ -1304,10 +1304,10 @@ export class SelectionController extends EventTarget {
); );
*/ */
// FIXME: I'm not sure if we should merge inlines when they share the same styles. // FIXME: I'm not sure if we should merge text spans when they share the same styles.
// For example: if we have > 2 inlines and the start inline and the end inline // For example: if we have > 2 text spans and the start text span and the end text span
// share the same styles, maybe we should merge them? // share the same styles, maybe we should merge them?
// mergeInlines(startInline, endInline); // mergeTextSpans(startTextSpan, endTextSpan);
return this.collapse(this.focusNode, this.focusOffset + newText.length); return this.collapse(this.focusNode, this.focusOffset + newText.length);
} }
@@ -1368,7 +1368,7 @@ export class SelectionController extends EventTarget {
const currentParagraph = this.focusParagraph; const currentParagraph = this.focusParagraph;
const newParagraph = splitParagraph( const newParagraph = splitParagraph(
this.focusParagraph, this.focusParagraph,
this.focusInline, this.focusTextSpan,
this.#focusOffset, this.#focusOffset,
); );
this.focusParagraph.after(newParagraph); this.focusParagraph.after(newParagraph);
@@ -1395,13 +1395,13 @@ export class SelectionController extends EventTarget {
*/ */
replaceWithParagraph() { replaceWithParagraph() {
const currentParagraph = this.focusParagraph; const currentParagraph = this.focusParagraph;
const currentInline = this.focusInline; const currentTextSpan = this.focusTextSpan;
this.removeSelected(); this.removeSelected();
const newParagraph = splitParagraph( const newParagraph = splitParagraph(
currentParagraph, currentParagraph,
currentInline, currentTextSpan,
this.focusOffset, this.focusOffset,
); );
currentParagraph.after(newParagraph); currentParagraph.after(newParagraph);
@@ -1422,15 +1422,15 @@ export class SelectionController extends EventTarget {
} }
const paragraphToBeRemoved = this.focusParagraph; const paragraphToBeRemoved = this.focusParagraph;
paragraphToBeRemoved.remove(); paragraphToBeRemoved.remove();
const previousInline = const previousTextSpan =
previousParagraph.children.length > 1 previousParagraph.children.length > 1
? previousParagraph.lastElementChild ? previousParagraph.lastElementChild
: previousParagraph.firstChild; : previousParagraph.firstChild;
const previousOffset = isLineBreak(previousInline.firstChild) const previousOffset = isLineBreak(previousTextSpan.firstChild)
? 0 ? 0
: previousInline.firstChild.nodeValue.length; : previousTextSpan.firstChild.nodeValue.length;
this.#mutations.remove(paragraphToBeRemoved); this.#mutations.remove(paragraphToBeRemoved);
return this.collapse(previousInline.firstChild, previousOffset); return this.collapse(previousTextSpan.firstChild, previousOffset);
} }
/** /**
@@ -1442,18 +1442,18 @@ export class SelectionController extends EventTarget {
if (!previousParagraph) { if (!previousParagraph) {
return; return;
} }
let previousInline = previousParagraph.lastChild; let previousTextSpan = previousParagraph.lastChild;
const previousOffset = getInlineLength(previousInline); const previousOffset = getTextSpanLength(previousTextSpan);
if (isEmptyParagraph(previousParagraph)) { if (isEmptyParagraph(previousParagraph)) {
previousParagraph.replaceChildren(...currentParagraph.children); previousParagraph.replaceChildren(...currentParagraph.children);
previousInline = previousParagraph.firstChild; previousTextSpan = previousParagraph.firstChild;
currentParagraph.remove(); currentParagraph.remove();
} else { } else {
mergeParagraphs(previousParagraph, currentParagraph); mergeParagraphs(previousParagraph, currentParagraph);
} }
this.#mutations.remove(currentParagraph); this.#mutations.remove(currentParagraph);
this.#mutations.update(previousParagraph); this.#mutations.update(previousParagraph);
return this.collapse(previousInline.firstChild, previousOffset); return this.collapse(previousTextSpan.firstChild, previousOffset);
} }
/** /**
@@ -1482,24 +1482,24 @@ export class SelectionController extends EventTarget {
} }
const paragraphToBeRemoved = this.focusParagraph; const paragraphToBeRemoved = this.focusParagraph;
paragraphToBeRemoved.remove(); paragraphToBeRemoved.remove();
const nextInline = nextParagraph.firstChild; const nextTextSpan = nextParagraph.firstChild;
const nextOffset = this.focusOffset; const nextOffset = this.focusOffset;
this.#mutations.remove(paragraphToBeRemoved); this.#mutations.remove(paragraphToBeRemoved);
return this.collapse(nextInline.firstChild, nextOffset); return this.collapse(nextTextSpan.firstChild, nextOffset);
} }
/** /**
* Cleans up all the affected paragraphs. * Cleans up all the affected paragraphs.
* *
* @param {Set<HTMLDivElement>} affectedParagraphs * @param {Set<HTMLDivElement>} affectedParagraphs
* @param {Set<HTMLSpanElement>} affectedInlines * @param {Set<HTMLSpanElement>} affectedTextSpans
*/ */
cleanUp(affectedParagraphs, affectedInlines) { cleanUp(affectedParagraphs, affectedTextSpans) {
// Remove empty inlines // Remove empty text spans
for (const inline of affectedInlines) { for (const textSpan of affectedTextSpans) {
if (inline.textContent === "") { if (textSpan.textContent === "") {
inline.remove(); textSpan.remove();
this.#mutations.remove(inline); this.#mutations.remove(textSpan);
} }
} }
@@ -1520,7 +1520,7 @@ export class SelectionController extends EventTarget {
removeSelected(options) { removeSelected(options) {
if (this.isCollapsed) return; if (this.isCollapsed) return;
const affectedInlines = new Set(); const affectedTextSpans = new Set();
const affectedParagraphs = new Set(); const affectedParagraphs = new Set();
const startNode = getClosestTextNode(this.#range.startContainer); const startNode = getClosestTextNode(this.#range.startContainer);
@@ -1541,9 +1541,9 @@ export class SelectionController extends EventTarget {
this.#textNodeIterator.currentNode = startNode; this.#textNodeIterator.currentNode = startNode;
nextNode = this.#textNodeIterator.nextNode(); nextNode = this.#textNodeIterator.nextNode();
const inline = getInline(startNode); const textSpan = getTextSpan(startNode);
const paragraph = getParagraph(startNode); const paragraph = getParagraph(startNode);
affectedInlines.add(inline); affectedTextSpans.add(textSpan);
affectedParagraphs.add(paragraph); affectedParagraphs.add(paragraph);
const newNodeValue = removeSlice( const newNodeValue = removeSlice(
@@ -1553,7 +1553,7 @@ export class SelectionController extends EventTarget {
); );
if (newNodeValue === "") { if (newNodeValue === "") {
const lineBreak = createLineBreak(); const lineBreak = createLineBreak();
inline.replaceChildren(lineBreak); textSpan.replaceChildren(lineBreak);
return this.collapse(lineBreak, 0); return this.collapse(lineBreak, 0);
} }
startNode.nodeValue = newNodeValue; startNode.nodeValue = newNodeValue;
@@ -1567,9 +1567,9 @@ export class SelectionController extends EventTarget {
// Select initial node. // Select initial node.
this.#textNodeIterator.currentNode = startNode; this.#textNodeIterator.currentNode = startNode;
const startInline = getInline(startNode); const startTextSpan = getTextSpan(startNode);
const startParagraph = getParagraph(startNode); const startParagraph = getParagraph(startNode);
const endInline = getInline(endNode); const endTextSpan = getTextSpan(endNode);
const endParagraph = getParagraph(endNode); const endParagraph = getParagraph(endNode);
SafeGuard.start(); SafeGuard.start();
@@ -1578,11 +1578,11 @@ export class SelectionController extends EventTarget {
const { currentNode } = this.#textNodeIterator; const { currentNode } = this.#textNodeIterator;
// We retrieve the inline and paragraph of the // We retrieve the textSpan and paragraph of the
// current node. // current node.
const inline = getInline(currentNode); const textSpan = getTextSpan(currentNode);
const paragraph = getParagraph(currentNode); const paragraph = getParagraph(currentNode);
affectedInlines.add(inline); affectedTextSpans.add(textSpan);
affectedParagraphs.add(paragraph); affectedParagraphs.add(paragraph);
let shouldRemoveNodeCompletely = false; let shouldRemoveNodeCompletely = false;
@@ -1619,8 +1619,8 @@ export class SelectionController extends EventTarget {
continue; continue;
} }
if (inline.childNodes.length === 0) { if (textSpan.childNodes.length === 0) {
inline.remove(); textSpan.remove();
} }
if (paragraph !== startParagraph && paragraph.children.length === 0) { if (paragraph !== startParagraph && paragraph.children.length === 0) {
@@ -1636,44 +1636,44 @@ export class SelectionController extends EventTarget {
if (startParagraph !== endParagraph) { if (startParagraph !== endParagraph) {
const mergedParagraph = mergeParagraphs(startParagraph, endParagraph); const mergedParagraph = mergeParagraphs(startParagraph, endParagraph);
if (mergedParagraph.children.length === 0) { if (mergedParagraph.children.length === 0) {
const newEmptyInline = createEmptyInline(this.#currentStyle); const newEmptyTextSpan = createEmptyTextSpan(this.#currentStyle);
mergedParagraph.appendChild(newEmptyInline); mergedParagraph.appendChild(newEmptyTextSpan);
return this.collapse(newEmptyInline.firstChild, 0); return this.collapse(newEmptyTextSpan.firstChild, 0);
} }
} }
if ( if (
startInline.childNodes.length === 0 && startTextSpan.childNodes.length === 0 &&
endInline.childNodes.length > 0 endTextSpan.childNodes.length > 0
) { ) {
startInline.remove(); startTextSpan.remove();
return this.collapse(endNode, 0); return this.collapse(endNode, 0);
} else if ( } else if (
startInline.childNodes.length > 0 && startTextSpan.childNodes.length > 0 &&
endInline.childNodes.length === 0 endTextSpan.childNodes.length === 0
) { ) {
endInline.remove(); endTextSpan.remove();
return this.collapse(startNode, startOffset); return this.collapse(startNode, startOffset);
} else if ( } else if (
startInline.childNodes.length === 0 && startTextSpan.childNodes.length === 0 &&
endInline.childNodes.length === 0 endTextSpan.childNodes.length === 0
) { ) {
const previousInline = startInline.previousElementSibling; const previousTextSpan = startTextSpan.previousElementSibling;
const nextInline = endInline.nextElementSibling; const nextTextSpan = endTextSpan.nextElementSibling;
startInline.remove(); startTextSpan.remove();
endInline.remove(); endTextSpan.remove();
if (previousInline) { if (previousTextSpan) {
return this.collapse( return this.collapse(
previousInline.firstChild, previousTextSpan.firstChild,
previousInline.firstChild.nodeValue.length, previousTextSpan.firstChild.nodeValue.length,
); );
} }
if (nextInline) { if (nextTextSpan) {
return this.collapse(nextInline.firstChild, 0); return this.collapse(nextTextSpan.firstChild, 0);
} }
const newEmptyInline = createEmptyInline(this.#currentStyle); const newEmptyTextSpan = createEmptyTextSpan(this.#currentStyle);
startParagraph.appendChild(newEmptyInline); startParagraph.appendChild(newEmptyTextSpan);
return this.collapse(newEmptyInline.firstChild, 0); return this.collapse(newEmptyTextSpan.firstChild, 0);
} }
return this.collapse(startNode, startOffset); return this.collapse(startNode, startOffset);
@@ -1701,30 +1701,30 @@ export class SelectionController extends EventTarget {
// The styles are applied to the node completely. // The styles are applied to the node completely.
if (startOffset === 0 && endOffset === endNode.nodeValue.length) { if (startOffset === 0 && endOffset === endNode.nodeValue.length) {
const paragraph = this.startParagraph; const paragraph = this.startParagraph;
const inline = this.startInline; const textSpan = this.startTextSpan;
setParagraphStyles(paragraph, newStyles); setParagraphStyles(paragraph, newStyles);
setInlineStyles(inline, newStyles); setTextSpanStyles(textSpan, newStyles);
// The styles are applied to a part of the node. // The styles are applied to a part of the node.
} else if (startOffset !== endOffset) { } else if (startOffset !== endOffset) {
const paragraph = this.startParagraph; const paragraph = this.startParagraph;
setParagraphStyles(paragraph, newStyles); setParagraphStyles(paragraph, newStyles);
const inline = this.startInline; const textSpan = this.startTextSpan;
const midText = startNode.splitText(startOffset); const midText = startNode.splitText(startOffset);
const endText = midText.splitText(endOffset - startOffset); const endText = midText.splitText(endOffset - startOffset);
const midInline = createInlineFrom(inline, midText, newStyles); const midTextSpan = createTextSpanFrom(textSpan, midText, newStyles);
inline.after(midInline); textSpan.after(midTextSpan);
if (endText.length > 0) { if (endText.length > 0) {
const endInline = createInline(endText, inline.style); const endTextSpan = createTextSpan(endText, textSpan.style);
midInline.after(endInline); midTextSpan.after(endTextSpan);
} }
// NOTE: This is necessary because sometimes // NOTE: This is necessary because sometimes
// inlines are splitted from the beginning // text spans are splitted from the beginning
// to a mid offset and then the starting node // to a mid offset and then the starting node
// remains empty. // remains empty.
if (inline.firstChild.nodeValue === "") { if (textSpan.firstChild.nodeValue === "") {
inline.remove(); textSpan.remove();
} }
// FIXME: This can change focus <-> anchor order. // FIXME: This can change focus <-> anchor order.
@@ -1735,9 +1735,9 @@ export class SelectionController extends EventTarget {
this.startOffset === this.endOffset && this.startOffset === this.endOffset &&
this.endOffset === endNode.nodeValue.length this.endOffset === endNode.nodeValue.length
) { ) {
const newInline = createVoidInline(newStyles); const newTextSpan = createVoidTextSpan(newStyles);
this.endInline.after(newInline); this.endTextSpan.after(newTextSpan);
this.setSelection(newInline.firstChild, 0, newInline.firstChild, 0); this.setSelection(newTextSpan.firstChild, 0, newTextSpan.firstChild, 0);
} }
// The styles are applied to the paragraph // The styles are applied to the paragraph
else { else {
@@ -1758,17 +1758,17 @@ export class SelectionController extends EventTarget {
const paragraph = getParagraph(this.#textNodeIterator.currentNode); const paragraph = getParagraph(this.#textNodeIterator.currentNode);
setParagraphStyles(paragraph, newStyles); setParagraphStyles(paragraph, newStyles);
const inline = getInline(this.#textNodeIterator.currentNode); const textSpan = getTextSpan(this.#textNodeIterator.currentNode);
// If we're at the start node and offset is greater than 0 // If we're at the start node and offset is greater than 0
// then we should split the inline and apply styles to that // then we should split the text span and apply styles to that
// new inline. // new text span.
if ( if (
this.#textNodeIterator.currentNode === startNode && this.#textNodeIterator.currentNode === startNode &&
startOffset > 0 startOffset > 0
) { ) {
const newInline = splitInline(inline, startOffset); const newTextSpan = splitTextSpan(textSpan, startOffset);
setInlineStyles(newInline, newStyles); setTextSpanStyles(newTextSpan, newStyles);
inline.after(newInline); textSpan.after(newTextSpan);
// If we're at the start node and offset is equal to 0 // If we're at the start node and offset is equal to 0
// or current node is different to start node and // or current node is different to start node and
// different to end node or we're at the end node // different to end node or we're at the end node
@@ -1781,16 +1781,16 @@ export class SelectionController extends EventTarget {
(this.#textNodeIterator.currentNode === endNode && (this.#textNodeIterator.currentNode === endNode &&
endOffset === endNode.nodeValue.length) endOffset === endNode.nodeValue.length)
) { ) {
setInlineStyles(inline, newStyles); setTextSpanStyles(textSpan, newStyles);
// If we're at end node // If we're at end node
} else if ( } else if (
this.#textNodeIterator.currentNode === endNode && this.#textNodeIterator.currentNode === endNode &&
endOffset < endNode.nodeValue.length endOffset < endNode.nodeValue.length
) { ) {
const newInline = splitInline(inline, endOffset); const newTextSpan = splitTextSpan(textSpan, endOffset);
setInlineStyles(inline, newStyles); setTextSpanStyles(textSpan, newStyles);
inline.after(newInline); textSpan.after(newTextSpan);
} }
// We've reached the final node so we can return safely. // We've reached the final node so we can return safely.

View File

@@ -3,7 +3,7 @@ import {
createEmptyParagraph, createEmptyParagraph,
createParagraph, createParagraph,
} from "../content/dom/Paragraph.js"; } from "../content/dom/Paragraph.js";
import { createInline } from "../content/dom/Inline.js"; import { createTextSpan } from "../content/dom/TextSpan.js";
import { createLineBreak } from "../content/dom/LineBreak.js"; import { createLineBreak } from "../content/dom/LineBreak.js";
import { TextEditorMock } from "../../test/TextEditorMock.js"; import { TextEditorMock } from "../../test/TextEditorMock.js";
import { SelectionController } from "./SelectionController.js"; import { SelectionController } from "./SelectionController.js";
@@ -206,7 +206,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -235,7 +235,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -256,7 +256,7 @@ describe("SelectionController", () => {
selection, selection,
); );
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0); focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
const paragraph = createParagraph([createInline(new Text("Hello"))]); const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.append(paragraph); fragment.append(paragraph);
@@ -269,7 +269,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -293,7 +293,7 @@ describe("SelectionController", () => {
selection, selection,
); );
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length); focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length);
const paragraph = createParagraph([createInline(new Text("ipsum "))]); const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.append(paragraph); fragment.append(paragraph);
@@ -306,7 +306,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Lorem ipsum dolor"); expect(textEditorMock.root.textContent).toBe("Lorem ipsum dolor");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -337,7 +337,7 @@ describe("SelectionController", () => {
root.firstChild.firstChild.firstChild, root.firstChild.firstChild.firstChild,
"Hello".length, "Hello".length,
); );
const paragraph = createParagraph([createInline(new Text(", World!"))]); const paragraph = createParagraph([createTextSpan(new Text(", World!"))]);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.append(paragraph); fragment.append(paragraph);
@@ -350,7 +350,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -364,7 +364,7 @@ describe("SelectionController", () => {
).toBe(", World!"); ).toBe(", World!");
}); });
test("`insertPaste` should insert an inline from a pasted fragment (at start)", () => { test("`insertPaste` should insert a text span from a pasted fragment (at start)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithText(", World!"); const textEditorMock = TextEditorMock.createTextEditorMockWithText(", World!");
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -378,8 +378,8 @@ describe("SelectionController", () => {
root.firstChild.firstChild.firstChild, root.firstChild.firstChild.firstChild,
0, 0,
); );
const paragraph = createParagraph([createInline(new Text("Hello"))]); const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
paragraph.dataset.inline = "force"; paragraph.dataset.textSpan = "force";
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.append(paragraph); fragment.append(paragraph);
@@ -392,7 +392,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -406,7 +406,7 @@ describe("SelectionController", () => {
).toBe(", World!"); ).toBe(", World!");
}); });
test("`insertPaste` should insert an inline from a pasted fragment (at middle)", () => { test("`insertPaste` should insert an text span from a pasted fragment (at middle)", () => {
const textEditorMock = const textEditorMock =
TextEditorMock.createTextEditorMockWithText("Lorem dolor"); TextEditorMock.createTextEditorMockWithText("Lorem dolor");
const root = textEditorMock.root; const root = textEditorMock.root;
@@ -416,8 +416,8 @@ describe("SelectionController", () => {
selection, selection,
); );
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length); focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length);
const paragraph = createParagraph([createInline(new Text("ipsum "))]); const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
paragraph.dataset.inline = "force"; paragraph.dataset.textSpan = "force";
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.append(paragraph); fragment.append(paragraph);
@@ -430,7 +430,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Lorem ipsum dolor"); expect(textEditorMock.root.textContent).toBe("Lorem ipsum dolor");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -447,7 +447,7 @@ describe("SelectionController", () => {
).toBe("dolor"); ).toBe("dolor");
}); });
test("`insertPaste` should insert an inline from a pasted fragment (at end)", () => { test("`insertPaste` should insert an text span from a pasted fragment (at end)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello"); const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -462,9 +462,9 @@ describe("SelectionController", () => {
"Hello".length, "Hello".length,
); );
const paragraph = createParagraph([ const paragraph = createParagraph([
createInline(new Text(", World!")) createTextSpan(new Text(", World!"))
]); ]);
paragraph.dataset.inline = "force"; paragraph.dataset.textSpan = "force";
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
fragment.append(paragraph); fragment.append(paragraph);
@@ -477,7 +477,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -515,7 +515,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World"); expect(textEditorMock.root.textContent).toBe("Hello, World");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -549,15 +549,15 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe(""); expect(textEditorMock.root.textContent).toBe("");
}); });
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => { test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, "))]), createParagraph([createTextSpan(new Text("Hello, "))]),
createParagraph([createInline(new Text("World!"))]), createParagraph([createTextSpan(new Text("World!"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -581,16 +581,16 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
}); });
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => { test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, "))]), createParagraph([createTextSpan(new Text("Hello, "))]),
createEmptyParagraph(), createEmptyParagraph(),
createParagraph([createInline(new Text("World!"))]), createParagraph([createTextSpan(new Text("World!"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -614,7 +614,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, "); expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
@@ -623,8 +623,8 @@ describe("SelectionController", () => {
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => { test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, "))]), createParagraph([createTextSpan(new Text("Hello, "))]),
createParagraph([createInline(new Text("World!"))]), createParagraph([createTextSpan(new Text("World!"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -648,16 +648,16 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
}); });
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => { test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, "))]), createParagraph([createTextSpan(new Text("Hello, "))]),
createEmptyParagraph(), createEmptyParagraph(),
createParagraph([createInline(new Text("World!"))]), createParagraph([createTextSpan(new Text("World!"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -681,7 +681,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, "); expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
@@ -707,7 +707,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("ello, World!"); expect(textEditorMock.root.textContent).toBe("ello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -744,7 +744,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, Mundo!"); expect(textEditorMock.root.textContent).toBe("Hello, Mundo!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -755,10 +755,10 @@ describe("SelectionController", () => {
); );
}); });
test("`replaceInlines` should replace the selected text in multiple inlines (2 completelly selected)", () => { test("`replaceTextSpans` should replace the selected text in multiple text spans (2 completelly selected)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
createInline(new Text("Hello, ")), createTextSpan(new Text("Hello, ")),
createInline(new Text("World!")), createTextSpan(new Text("World!")),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -774,7 +774,7 @@ describe("SelectionController", () => {
root.firstChild.lastChild.firstChild, root.firstChild.lastChild.firstChild,
"World!".length, "World!".length,
); );
selectionController.replaceInlines("Mundo"); selectionController.replaceTextSpans("Mundo");
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.dataset.itype).toBe("root");
@@ -785,7 +785,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Mundo"); expect(textEditorMock.root.textContent).toBe("Mundo");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -796,10 +796,10 @@ describe("SelectionController", () => {
); );
}); });
test("`replaceInlines` should replace the selected text in multiple inlines (2 partially selected)", () => { test("`replaceTextSpans` should replace the selected text in multiple text spans (2 partially selected)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
createInline(new Text("Hello, ")), createTextSpan(new Text("Hello, ")),
createInline(new Text("World!")), createTextSpan(new Text("World!")),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -815,7 +815,7 @@ describe("SelectionController", () => {
root.firstChild.lastChild.firstChild, root.firstChild.lastChild.firstChild,
"World!".length - 3, "World!".length - 3,
); );
selectionController.replaceInlines("Mundo"); selectionController.replaceTextSpans("Mundo");
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.dataset.itype).toBe("root");
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
@@ -825,7 +825,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("HeMundold!"); expect(textEditorMock.root.textContent).toBe("HeMundold!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -842,10 +842,10 @@ describe("SelectionController", () => {
); );
}); });
test("`replaceInlines` should replace the selected text in multiple inlines (1 partially selected, 1 completelly selected)", () => { test("`replaceTextSpans` should replace the selected text in multiple text spans (1 partially selected, 1 completelly selected)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
createInline(new Text("Hello, ")), createTextSpan(new Text("Hello, ")),
createInline(new Text("World!")), createTextSpan(new Text("World!")),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -861,7 +861,7 @@ describe("SelectionController", () => {
root.firstChild.lastChild.firstChild, root.firstChild.lastChild.firstChild,
"World!".length, "World!".length,
); );
selectionController.replaceInlines("Mundo"); selectionController.replaceTextSpans("Mundo");
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.dataset.itype).toBe("root");
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
@@ -871,7 +871,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("HeMundo"); expect(textEditorMock.root.textContent).toBe("HeMundo");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -882,10 +882,10 @@ describe("SelectionController", () => {
); );
}); });
test("`replaceInlines` should replace the selected text in multiple inlines (1 completelly selected, 1 partially selected)", () => { test("`replaceTextSpans` should replace the selected text in multiple text spans (1 completelly selected, 1 partially selected)", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
createInline(new Text("Hello, ")), createTextSpan(new Text("Hello, ")),
createInline(new Text("World!")), createTextSpan(new Text("World!")),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -901,7 +901,7 @@ describe("SelectionController", () => {
root.firstChild.lastChild.firstChild, root.firstChild.lastChild.firstChild,
"World!".length - 3, "World!".length - 3,
); );
selectionController.replaceInlines("Mundo"); selectionController.replaceTextSpans("Mundo");
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
expect(textEditorMock.root.dataset.itype).toBe("root"); expect(textEditorMock.root.dataset.itype).toBe("root");
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement); expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
@@ -911,7 +911,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Mundold!"); expect(textEditorMock.root.textContent).toBe("Mundold!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -949,7 +949,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, !"); expect(textEditorMock.root.textContent).toBe("Hello, !");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -960,10 +960,10 @@ describe("SelectionController", () => {
); );
}); });
test("`removeSelected` multiple inlines", () => { test("`removeSelected` multiple text spans", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
createInline(new Text("Hello, ")), createTextSpan(new Text("Hello, ")),
createInline(new Text("World!")), createTextSpan(new Text("World!")),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -989,7 +989,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe(""); expect(textEditorMock.root.textContent).toBe("");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -999,9 +999,9 @@ describe("SelectionController", () => {
test("`removeSelected` multiple paragraphs", () => { test("`removeSelected` multiple paragraphs", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, "))]), createParagraph([createTextSpan(new Text("Hello, "))]),
createParagraph([createInline(createLineBreak())]), createParagraph([createTextSpan(createLineBreak())]),
createParagraph([createInline(new Text("World!"))]), createParagraph([createTextSpan(new Text("World!"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -1028,7 +1028,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -1047,9 +1047,9 @@ describe("SelectionController", () => {
test("`removeSelected` and `removeBackwardParagraph`", () => { test("`removeSelected` and `removeBackwardParagraph`", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createTextSpan(new Text("Hello, World!"))]),
createParagraph([createInline(createLineBreak())]), createParagraph([createTextSpan(createLineBreak())]),
createParagraph([createInline(new Text("This is a test"))]), createParagraph([createTextSpan(new Text("This is a test"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -1077,7 +1077,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -1090,9 +1090,9 @@ describe("SelectionController", () => {
test("`removeSelected` and `removeForwardParagraph`", () => { test("`removeSelected` and `removeForwardParagraph`", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createTextSpan(new Text("Hello, World!"))]),
createParagraph([createInline(createLineBreak())]), createParagraph([createTextSpan(createLineBreak())]),
createParagraph([createInline(new Text("This is a test"))]), createParagraph([createTextSpan(new Text("This is a test"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -1120,7 +1120,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("This is a test"); expect(textEditorMock.root.textContent).toBe("This is a test");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -1133,9 +1133,9 @@ describe("SelectionController", () => {
test("performing a `removeSelected` after a `removeSelected` should do nothing", () => { test("performing a `removeSelected` after a `removeSelected` should do nothing", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createTextSpan(new Text("Hello, World!"))]),
createParagraph([createInline(createLineBreak())]), createParagraph([createTextSpan(createLineBreak())]),
createParagraph([createInline(new Text("This is a test"))]), createParagraph([createTextSpan(new Text("This is a test"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -1166,7 +1166,7 @@ describe("SelectionController", () => {
HTMLSpanElement, HTMLSpanElement,
); );
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("This is a test"); expect(textEditorMock.root.textContent).toBe("This is a test");
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf( expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
@@ -1179,9 +1179,9 @@ describe("SelectionController", () => {
test("`removeSelected` removes everything", () => { test("`removeSelected` removes everything", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createTextSpan(new Text("Hello, World!"))]),
createParagraph([createInline(createLineBreak())]), createParagraph([createTextSpan(createLineBreak())]),
createParagraph([createInline(new Text("This is a test"))]), createParagraph([createTextSpan(new Text("This is a test"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -1212,9 +1212,9 @@ describe("SelectionController", () => {
test("`removeSelected` removes everything and insert text", () => { test("`removeSelected` removes everything and insert text", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createInline(new Text("Hello, World!"))]), createParagraph([createTextSpan(new Text("Hello, World!"))]),
createParagraph([createInline(createLineBreak())]), createParagraph([createTextSpan(createLineBreak())]),
createParagraph([createInline(new Text("This is a test"))]), createParagraph([createTextSpan(new Text("This is a test"))]),
]); ]);
const root = textEditorMock.root; const root = textEditorMock.root;
const selection = document.getSelection(); const selection = document.getSelection();
@@ -1277,7 +1277,7 @@ describe("SelectionController", () => {
); );
expect(textEditorMock.root.firstChild.children.length).toBe(3); expect(textEditorMock.root.firstChild.children.length).toBe(3);
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe( expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.textContent).toBe("Hello, World!"); expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe( expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
@@ -1291,12 +1291,12 @@ describe("SelectionController", () => {
); );
}); });
test("`applyStyles` to inlines", () => { test("`applyStyles` to text spans", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
createInline(new Text("Hello, "), { createTextSpan(new Text("Hello, "), {
"font-style": "italic", "font-style": "italic",
}), }),
createInline(new Text("World!"), { createTextSpan(new Text("World!"), {
"font-style": "oblique", "font-style": "oblique",
}), }),
]); ]);
@@ -1328,25 +1328,25 @@ describe("SelectionController", () => {
); );
expect(textEditorMock.root.firstChild.children.length).toBe(4); expect(textEditorMock.root.firstChild.children.length).toBe(4);
expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe( expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe( expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
"He", "He",
); );
expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe( expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe( expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe(
"llo, ", "llo, ",
); );
expect(textEditorMock.root.firstChild.children.item(2).dataset.itype).toBe( expect(textEditorMock.root.firstChild.children.item(2).dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.firstChild.children.item(2).textContent).toBe( expect(textEditorMock.root.firstChild.children.item(2).textContent).toBe(
"Wor", "Wor",
); );
expect(textEditorMock.root.firstChild.children.item(3).dataset.itype).toBe( expect(textEditorMock.root.firstChild.children.item(3).dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.firstChild.children.item(3).textContent).toBe( expect(textEditorMock.root.firstChild.children.item(3).textContent).toBe(
"ld!", "ld!",
@@ -1356,12 +1356,12 @@ describe("SelectionController", () => {
test("`applyStyles` to paragraphs", () => { test("`applyStyles` to paragraphs", () => {
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([ const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([ createParagraph([
createInline(new Text("Hello, "), { createTextSpan(new Text("Hello, "), {
"font-style": "italic", "font-style": "italic",
}), }),
]), ]),
createParagraph([ createParagraph([
createInline(new Text("World!"), { createTextSpan(new Text("World!"), {
"font-style": "oblique", "font-style": "oblique",
}), }),
]), ]),
@@ -1394,26 +1394,26 @@ describe("SelectionController", () => {
); );
expect(textEditorMock.root.firstChild.children.length).toBe(2); expect(textEditorMock.root.firstChild.children.length).toBe(2);
expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe( expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe( expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
"He", "He",
); );
expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe( expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe( expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe(
"llo, ", "llo, ",
); );
expect(textEditorMock.root.lastChild.children.length).toBe(2); expect(textEditorMock.root.lastChild.children.length).toBe(2);
expect(textEditorMock.root.lastChild.children.item(0).dataset.itype).toBe( expect(textEditorMock.root.lastChild.children.item(0).dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.lastChild.children.item(0).textContent).toBe( expect(textEditorMock.root.lastChild.children.item(0).textContent).toBe(
"Wor", "Wor",
); );
expect(textEditorMock.root.lastChild.children.item(1).dataset.itype).toBe( expect(textEditorMock.root.lastChild.children.item(1).dataset.itype).toBe(
"inline", "span",
); );
expect(textEditorMock.root.lastChild.children.item(1).textContent).toBe( expect(textEditorMock.root.lastChild.children.item(1).textContent).toBe(
"ld!", "ld!",

View File

@@ -36,15 +36,15 @@ export class SelectionControllerDebug {
update(selectionController) { update(selectionController) {
this.#elements.direction.value = selectionController.direction; this.#elements.direction.value = selectionController.direction;
this.#elements.multiElement.checked = selectionController.isMulti; this.#elements.multiElement.checked = selectionController.isMulti;
this.#elements.multiInlineElement.checked = this.#elements.multiTextSpanElement.checked =
selectionController.isMultiInline; selectionController.isMultiTextSpan;
this.#elements.multiParagraphElement.checked = this.#elements.multiParagraphElement.checked =
selectionController.isMultiParagraph; selectionController.isMultiParagraph;
this.#elements.isParagraphStart.checked = this.#elements.isParagraphStart.checked =
selectionController.isParagraphStart; selectionController.isParagraphStart;
this.#elements.isParagraphEnd.checked = selectionController.isParagraphEnd; this.#elements.isParagraphEnd.checked = selectionController.isParagraphEnd;
this.#elements.isInlineStart.checked = selectionController.isInlineStart; this.#elements.isTextSpanStart.checked = selectionController.isTextSpanStart;
this.#elements.isInlineEnd.checked = selectionController.isInlineEnd; this.#elements.isTextSpanEnd.checked = selectionController.isTextSpanEnd;
this.#elements.isTextAnchor.checked = selectionController.isTextAnchor; this.#elements.isTextAnchor.checked = selectionController.isTextAnchor;
this.#elements.isTextFocus.checked = selectionController.isTextFocus; this.#elements.isTextFocus.checked = selectionController.isTextFocus;
this.#elements.focusNode.value = this.getNodeDescription( this.#elements.focusNode.value = this.getNodeDescription(
@@ -57,11 +57,11 @@ export class SelectionControllerDebug {
selectionController.anchorOffset selectionController.anchorOffset
); );
this.#elements.anchorOffset.value = selectionController.anchorOffset; this.#elements.anchorOffset.value = selectionController.anchorOffset;
this.#elements.focusInline.value = this.getNodeDescription( this.#elements.focusTextSpan.value = this.getNodeDescription(
selectionController.focusInline selectionController.focusTextSpan
); );
this.#elements.anchorInline.value = this.getNodeDescription( this.#elements.anchorTextSpan.value = this.getNodeDescription(
selectionController.anchorInline selectionController.anchorTextSpan
); );
this.#elements.focusParagraph.value = this.getNodeDescription( this.#elements.focusParagraph.value = this.getNodeDescription(
selectionController.focusParagraph selectionController.focusParagraph

View File

@@ -129,8 +129,8 @@
<input id="focus-offset" readonly type="number"> <input id="focus-offset" readonly type="number">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="focus-inline">Focus Inline</label> <label for="focus-textspan">Focus TextSpan</label>
<input id="focus-inline" readonly type="text" /> <input id="focus-textspan" readonly type="text" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="focus-paragraph">Focus Paragraph</label> <label for="focus-paragraph">Focus Paragraph</label>
@@ -145,8 +145,8 @@
<input id="anchor-offset" readonly type="number"> <input id="anchor-offset" readonly type="number">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="anchor-inline">Anchor Inline</label> <label for="anchor-textspan">Anchor TextSpan</label>
<input id="anchor-inline" readonly type="text" /> <input id="anchor-textspan" readonly type="text" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="anchor-paragraph">Anchor Paragraph</label> <label for="anchor-paragraph">Anchor Paragraph</label>
@@ -173,8 +173,8 @@
<input id="multi" readonly type="checkbox" /> <input id="multi" readonly type="checkbox" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="multi-inline">Multi inline?</label> <label for="multi-textspan">Multi text span?</label>
<input id="multi-inline" readonly type="checkbox" /> <input id="multi-textspan" readonly type="checkbox" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="multi-paragraph">Multi paragraph?</label> <label for="multi-paragraph">Multi paragraph?</label>
@@ -197,12 +197,12 @@
<input id="is-paragraph-end" readonly type="checkbox" /> <input id="is-paragraph-end" readonly type="checkbox" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="is-inline-start">Is inline start?</label> <label for="is-textspan-start">Is text span start?</label>
<input id="is-inline-start" readonly type="checkbox" /> <input id="is-textspan-start" readonly type="checkbox" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="is-inline-end">Is inline end?</label> <label for="is-textspan-end">Is text span end?</label>
<input id="is-inline-end" readonly type="checkbox"> <input id="is-textspan-end" readonly type="checkbox">
</div> </div>
</fieldset> </fieldset>
</form> </form>

View File

@@ -6,7 +6,7 @@ import { UUID } from "./playground/uuid.js";
import { Rect, Point } from "./playground/geom.js"; import { Rect, Point } from "./playground/geom.js";
import { WASMModuleWrapper } from "./playground/wasm.js"; import { WASMModuleWrapper } from "./playground/wasm.js";
import { FontManager } from "./playground/font.js"; import { FontManager } from "./playground/font.js";
import { TextContent, TextParagraph, TextLeaf } from "./playground/text.js"; import { TextContent, TextParagraph, TextSpan } from "./playground/text.js";
import { Viewport } from "./playground/viewport.js"; import { Viewport } from "./playground/viewport.js";
import { Fill } from "./playground/fill.js"; import { Fill } from "./playground/fill.js";
import { Shape } from "./playground/shape.js"; import { Shape } from "./playground/shape.js";
@@ -80,21 +80,21 @@ class TextEditorPlayground {
debug: new SelectionControllerDebug({ debug: new SelectionControllerDebug({
direction: document.getElementById("direction"), direction: document.getElementById("direction"),
multiElement: document.getElementById("multi"), multiElement: document.getElementById("multi"),
multiInlineElement: document.getElementById("multi-inline"), multiTextSpanElement: document.getElementById("multi-textspan"),
multiParagraphElement: document.getElementById("multi-paragraph"), multiParagraphElement: document.getElementById("multi-paragraph"),
isParagraphStart: document.getElementById("is-paragraph-start"), isParagraphStart: document.getElementById("is-paragraph-start"),
isParagraphEnd: document.getElementById("is-paragraph-end"), isParagraphEnd: document.getElementById("is-paragraph-end"),
isInlineStart: document.getElementById("is-inline-start"), isTextSpanStart: document.getElementById("is-textspan-start"),
isInlineEnd: document.getElementById("is-inline-end"), isTextSpanEnd: document.getElementById("is-textspan-end"),
isTextAnchor: document.getElementById("is-text-anchor"), isTextAnchor: document.getElementById("is-text-anchor"),
isTextFocus: document.getElementById("is-text-focus"), isTextFocus: document.getElementById("is-text-focus"),
focusNode: document.getElementById("focus-node"), focusNode: document.getElementById("focus-node"),
focusOffset: document.getElementById("focus-offset"), focusOffset: document.getElementById("focus-offset"),
focusInline: document.getElementById("focus-inline"), focusTextSpan: document.getElementById("focus-textspan"),
focusParagraph: document.getElementById("focus-paragraph"), focusParagraph: document.getElementById("focus-paragraph"),
anchorNode: document.getElementById("anchor-node"), anchorNode: document.getElementById("anchor-node"),
anchorOffset: document.getElementById("anchor-offset"), anchorOffset: document.getElementById("anchor-offset"),
anchorInline: document.getElementById("anchor-inline"), anchorTextSpan: document.getElementById("anchor-textspan"),
anchorParagraph: document.getElementById("anchor-paragraph"), anchorParagraph: document.getElementById("anchor-paragraph"),
startContainer: document.getElementById("start-container"), startContainer: document.getElementById("start-container"),
startOffset: document.getElementById("start-offset"), startOffset: document.getElementById("start-offset"),
@@ -496,7 +496,7 @@ class TextEditorPlayground {
leaf.fills.forEach((fill, index) => { leaf.fills.forEach((fill, index) => {
const fillOffset = const fillOffset =
offset + TextLeaf.BYTE_LENGTH + index * Fill.BYTE_LENGTH; offset + TextSpan.BYTE_LENGTH + index * Fill.BYTE_LENGTH;
if (fill.type === Fill.Type.SOLID) { if (fill.type === Fill.Type.SOLID) {
view.setUint8(fillOffset + 0, fill.type, true); view.setUint8(fillOffset + 0, fill.type, true);
view.setUint32(fillOffset + 4, fill.solid.color.argb32, true); view.setUint32(fillOffset + 4, fill.solid.color.argb32, true);

View File

@@ -37,11 +37,11 @@ export const TextTransform = {
fromStyle, fromStyle,
}; };
export class TextLeaf { export class TextSpan {
static BYTE_LENGTH = 60; static BYTE_LENGTH = 60;
static fromDOM(leafElement, fontManager) { static fromDOM(spanElement, fontManager) {
const elementStyle = leafElement.style; //window.getComputedStyle(leafElement); const elementStyle = spanElement.style; //window.getComputedStyle(leafElement);
const fontSize = parseFloat( const fontSize = parseFloat(
elementStyle.getPropertyValue("font-size"), elementStyle.getPropertyValue("font-size"),
); );
@@ -77,7 +77,7 @@ export class TextLeaf {
if (!font) { if (!font) {
throw new Error(`Invalid font "${fontFamily}"`) throw new Error(`Invalid font "${fontFamily}"`)
} }
return new TextLeaf({ return new TextSpan({
fontId: font.id, // leafElement.style.getPropertyValue("--font-id"), fontId: font.id, // leafElement.style.getPropertyValue("--font-id"),
fontFamilyHash: 0, fontFamilyHash: 0,
fontVariantId: UUID.ZERO, // leafElement.style.getPropertyValue("--font-variant-id"), fontVariantId: UUID.ZERO, // leafElement.style.getPropertyValue("--font-variant-id"),
@@ -88,7 +88,7 @@ export class TextLeaf {
textDecoration, textDecoration,
textTransform, textTransform,
textDirection, textDirection,
text: leafElement.textContent, text: spanElement.textContent,
}); });
} }
@@ -134,7 +134,7 @@ export class TextLeaf {
} }
get leafByteLength() { get leafByteLength() {
return this.fills.length * Fill.BYTE_LENGTH + TextLeaf.BYTE_LENGTH; return this.fills.length * Fill.BYTE_LENGTH + TextSpan.BYTE_LENGTH;
} }
} }
@@ -162,7 +162,7 @@ export class TextParagraph {
paragraphElement.style.getPropertyValue("letter-spacing"), paragraphElement.style.getPropertyValue("letter-spacing"),
), ),
leaves: Array.from(paragraphElement.children, (leafElement) => leaves: Array.from(paragraphElement.children, (leafElement) =>
TextLeaf.fromDOM(leafElement, fontManager), TextSpan.fromDOM(leafElement, fontManager),
), ),
}); });
} }
@@ -189,7 +189,7 @@ export class TextParagraph {
this.#leaves = init?.leaves ?? []; this.#leaves = init?.leaves ?? [];
if ( if (
!Array.isArray(this.#leaves) || !Array.isArray(this.#leaves) ||
!this.#leaves.every((leaf) => leaf instanceof TextLeaf) !this.#leaves.every((leaf) => leaf instanceof TextSpan)
) { ) {
throw new TypeError("Invalid text leaves"); throw new TypeError("Invalid text leaves");
} }

View File

@@ -1,6 +1,6 @@
import { createRoot } from "../editor/content/dom/Root.js"; import { createRoot } from "../editor/content/dom/Root.js";
import { createParagraph } from "../editor/content/dom/Paragraph.js"; import { createParagraph } from "../editor/content/dom/Paragraph.js";
import { createEmptyInline, createInline } from "../editor/content/dom/Inline.js"; import { createEmptyTextSpan, createTextSpan } from "../editor/content/dom/TextSpan.js";
import { createLineBreak } from "../editor/content/dom/LineBreak.js"; import { createLineBreak } from "../editor/content/dom/LineBreak.js";
export class TextEditorMock extends EventTarget { export class TextEditorMock extends EventTarget {
@@ -68,7 +68,7 @@ export class TextEditorMock extends EventTarget {
*/ */
static createTextEditorMockEmpty() { static createTextEditorMockEmpty() {
const root = createRoot([ const root = createRoot([
createParagraph([createInline(createLineBreak())]), createParagraph([createTextSpan(createLineBreak())]),
]); ]);
return this.createTextEditorMockWithRoot(root); return this.createTextEditorMockWithRoot(root);
} }
@@ -76,7 +76,7 @@ export class TextEditorMock extends EventTarget {
/** /**
* Creates a TextEditor mock with some text. * Creates a TextEditor mock with some text.
* *
* NOTE: If the text is empty an empty inline will be * NOTE: If the text is empty an empty text span will be
* created. * created.
* *
* @param {string} text * @param {string} text
@@ -86,21 +86,21 @@ export class TextEditorMock extends EventTarget {
return this.createTextEditorMockWithParagraphs([ return this.createTextEditorMockWithParagraphs([
createParagraph([ createParagraph([
text.length === 0 text.length === 0
? createEmptyInline() ? createEmptyTextSpan()
: createInline(new Text(text)) : createTextSpan(new Text(text))
]), ]),
]); ]);
} }
/** /**
* Creates a TextEditor mock with some inlines and * Creates a TextEditor mock with some textSpans and
* only one paragraph. * only one paragraph.
* *
* @param {Array<HTMLSpanElement>} inlines * @param {Array<HTMLSpanElement>} textSpans
* @returns * @returns
*/ */
static createTextEditorMockWithParagraph(inlines) { static createTextEditorMockWithParagraph(textSpans) {
return this.createTextEditorMockWithParagraphs([createParagraph(inlines)]); return this.createTextEditorMockWithParagraphs([createParagraph(textSpans)]);
} }
#element = null; #element = null;

View File

@@ -33,8 +33,8 @@ pub fn stroke_paragraph_builder_group_from_text(
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> = let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
std::collections::HashMap::new(); std::collections::HashMap::new();
for leaf in paragraph.children().iter() { for span in paragraph.children().iter() {
let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds); let text_paint: skia_safe::Handle<_> = merge_fills(span.fills(), *bounds);
let stroke_paints = get_text_stroke_paints( let stroke_paints = get_text_stroke_paints(
stroke, stroke,
bounds, bounds,
@@ -43,7 +43,7 @@ pub fn stroke_paragraph_builder_group_from_text(
remove_stroke_alpha, remove_stroke_alpha,
); );
let text: String = leaf.apply_text_transform(); let text: String = span.apply_text_transform();
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() { for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| { let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| {
@@ -51,9 +51,9 @@ pub fn stroke_paragraph_builder_group_from_text(
ParagraphBuilder::new(&paragraph_style, fonts) ParagraphBuilder::new(&paragraph_style, fonts)
}); });
let stroke_paint = stroke_paint.clone(); let stroke_paint = stroke_paint.clone();
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent(); let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
let stroke_style = let stroke_style =
leaf.to_stroke_style(&stroke_paint, fallback_fonts, remove_alpha); span.to_stroke_style(&stroke_paint, fallback_fonts, remove_alpha);
builder.push_style(&stroke_style); builder.push_style(&stroke_style);
builder.add_text(&text); builder.add_text(&text);
} }
@@ -342,7 +342,7 @@ fn render_text_decoration(
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) = let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
calculate_decoration_metrics(&style_metrics, line_baseline); calculate_decoration_metrics(&style_metrics, line_baseline);
// Draw decorations per segment (text leaf) // Draw decorations per segment (text span)
for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() { for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() {
let text_style = &style_metric.text_style; let text_style = &style_metric.text_style;
let style_end = style_metrics let style_end = style_metrics

View File

@@ -98,7 +98,7 @@ impl TextContentSize {
pub struct TextPositionWithAffinity { pub struct TextPositionWithAffinity {
pub position_with_affinity: PositionWithAffinity, pub position_with_affinity: PositionWithAffinity,
pub paragraph: i32, pub paragraph: i32,
pub leaf: i32, pub span: i32,
pub offset: i32, pub offset: i32,
} }
@@ -106,13 +106,13 @@ impl TextPositionWithAffinity {
pub fn new( pub fn new(
position_with_affinity: PositionWithAffinity, position_with_affinity: PositionWithAffinity,
paragraph: i32, paragraph: i32,
leaf: i32, span: i32,
offset: i32, offset: i32,
) -> Self { ) -> Self {
Self { Self {
position_with_affinity, position_with_affinity,
paragraph, paragraph,
leaf, span,
offset, offset,
} }
} }
@@ -289,7 +289,7 @@ impl TextContent {
let layout_paragraphs = self.layout.paragraphs.iter().flatten(); let layout_paragraphs = self.layout.paragraphs.iter().flatten();
let mut paragraph_index: i32 = -1; let mut paragraph_index: i32 = -1;
let mut leaf_index: i32 = -1; let mut span_index: i32 = -1;
for layout_paragraph in layout_paragraphs { for layout_paragraph in layout_paragraphs {
paragraph_index += 1; paragraph_index += 1;
let start_y = offset_y; let start_y = offset_y;
@@ -303,17 +303,17 @@ impl TextContent {
if let Some(paragraph) = self.paragraphs().get(paragraph_index as usize) { if let Some(paragraph) = self.paragraphs().get(paragraph_index as usize) {
// Computed position keeps the current position in terms // Computed position keeps the current position in terms
// of number of characters of text. This is used to know // of number of characters of text. This is used to know
// in which leaf we are. // in which span we are.
let mut computed_position = 0; let mut computed_position = 0;
let mut leaf_offset = 0; let mut span_offset = 0;
for leaf in paragraph.children() { for span in paragraph.children() {
leaf_index += 1; span_index += 1;
let length = leaf.text.len(); let length = span.text.len();
let start_position = computed_position; let start_position = computed_position;
let end_position = computed_position + length; let end_position = computed_position + length;
let current_position = position_with_affinity.position as usize; let current_position = position_with_affinity.position as usize;
if start_position <= current_position && end_position >= current_position { if start_position <= current_position && end_position >= current_position {
leaf_offset = position_with_affinity.position - start_position as i32; span_offset = position_with_affinity.position - start_position as i32;
break; break;
} }
computed_position += length; computed_position += length;
@@ -321,8 +321,8 @@ impl TextContent {
return Some(TextPositionWithAffinity::new( return Some(TextPositionWithAffinity::new(
position_with_affinity, position_with_affinity,
paragraph_index, paragraph_index,
leaf_index, span_index,
leaf_offset, span_offset,
)); ));
} }
} }
@@ -344,10 +344,10 @@ impl TextContent {
for paragraph in self.paragraphs() { for paragraph in self.paragraphs() {
let paragraph_style = paragraph.paragraph_to_style(); let paragraph_style = paragraph.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts); let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
for leaf in paragraph.children() { for span in paragraph.children() {
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent(); let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
let text_style = leaf.to_style(&self.bounds(), fallback_fonts, remove_alpha); let text_style = span.to_style(&self.bounds(), fallback_fonts, remove_alpha);
let text = leaf.apply_text_transform(); let text = span.apply_text_transform();
builder.push_style(&text_style); builder.push_style(&text_style);
builder.add_text(&text); builder.add_text(&text);
} }
@@ -519,7 +519,7 @@ pub struct Paragraph {
letter_spacing: f32, letter_spacing: f32,
typography_ref_file: Uuid, typography_ref_file: Uuid,
typography_ref_id: Uuid, typography_ref_id: Uuid,
children: Vec<TextLeaf>, children: Vec<TextSpan>,
} }
impl Default for Paragraph { impl Default for Paragraph {
@@ -549,7 +549,7 @@ impl Paragraph {
letter_spacing: f32, letter_spacing: f32,
typography_ref_file: Uuid, typography_ref_file: Uuid,
typography_ref_id: Uuid, typography_ref_id: Uuid,
children: Vec<TextLeaf>, children: Vec<TextSpan>,
) -> Self { ) -> Self {
Self { Self {
text_align, text_align,
@@ -565,17 +565,17 @@ impl Paragraph {
} }
#[allow(dead_code)] #[allow(dead_code)]
fn set_children(&mut self, children: Vec<TextLeaf>) { fn set_children(&mut self, children: Vec<TextSpan>) {
self.children = children; self.children = children;
} }
pub fn children(&self) -> &[TextLeaf] { pub fn children(&self) -> &[TextSpan] {
&self.children &self.children
} }
#[allow(dead_code)] #[allow(dead_code)]
fn add_leaf(&mut self, leaf: TextLeaf) { fn add_span(&mut self, span: TextSpan) {
self.children.push(leaf); self.children.push(span);
} }
// FIXME: move serialization to wasm module // FIXME: move serialization to wasm module
@@ -622,7 +622,7 @@ impl Paragraph {
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct TextLeaf { pub struct TextSpan {
text: String, text: String,
font_family: FontFamily, font_family: FontFamily,
font_size: f32, font_size: f32,
@@ -635,7 +635,7 @@ pub struct TextLeaf {
fills: Vec<shapes::Fill>, fills: Vec<shapes::Fill>,
} }
impl TextLeaf { impl TextSpan {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
text: String, text: String,

View File

@@ -37,7 +37,7 @@ impl TextPaths {
let start = line_metrics.start_index; let start = line_metrics.start_index;
let end = line_metrics.end_index; let end = line_metrics.end_index;
// 3. Get styles present in line for each text leaf // 3. Get styles present in line for each text span
let style_metrics = line_metrics.get_style_metrics(start..end); let style_metrics = line_metrics.get_style_metrics(start..end);
let mut offset_x = 0.0; let mut offset_x = 0.0;
@@ -56,23 +56,23 @@ impl TextPaths {
.map(|(i, _)| i) .map(|(i, _)| i)
.unwrap_or(text.len()); .unwrap_or(text.len());
let leaf_text = &text[start_byte..end_byte]; let span_text = &text[start_byte..end_byte];
let font = skia_paragraph.get_font_at(*start_index); let font = skia_paragraph.get_font_at(*start_index);
let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x; let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
let blob_offset_y = line_offset_y; let blob_offset_y = line_offset_y;
// 4. Get the path for each text leaf // 4. Get the path for each text span
if let Some((text_path, paint)) = self.generate_text_path( if let Some((text_path, paint)) = self.generate_text_path(
leaf_text, span_text,
&font, &font,
blob_offset_x, blob_offset_x,
blob_offset_y, blob_offset_y,
style_metric, style_metric,
antialias, antialias,
) { ) {
let text_width = font.measure_text(leaf_text, None).0; let text_width = font.measure_text(span_text, None).0;
offset_x += text_width; offset_x += text_width;
paths.push((text_path, paint)); paths.push((text_path, paint));
} }
@@ -87,7 +87,7 @@ impl TextPaths {
fn generate_text_path( fn generate_text_path(
&self, &self,
leaf_text: &str, span_text: &str,
font: &skia::Font, font: &skia::Font,
blob_offset_x: f32, blob_offset_x: f32,
blob_offset_y: f32, blob_offset_y: f32,
@@ -99,10 +99,10 @@ impl TextPaths {
// This is used to avoid rendering empty paths, but we can // This is used to avoid rendering empty paths, but we can
// revisit this logic later // revisit this logic later
if let Some((text_blob_path, text_blob_bounds)) = if let Some((text_blob_path, text_blob_bounds)) =
Self::get_text_blob_path(leaf_text, font, blob_offset_x, blob_offset_y) Self::get_text_blob_path(span_text, font, blob_offset_x, blob_offset_y)
{ {
let mut text_path = text_blob_path.clone(); let mut text_path = text_blob_path.clone();
let text_width = font.measure_text(leaf_text, None).0; let text_width = font.measure_text(span_text, None).0;
let decoration = style_metric.text_style.decoration(); let decoration = style_metric.text_style.decoration();
let font_metrics = style_metric.font_metrics; let font_metrics = style_metric.font_metrics;
@@ -165,13 +165,13 @@ impl TextPaths {
} }
fn get_text_blob_path( fn get_text_blob_path(
leaf_text: &str, span_text: &str,
font: &skia::Font, font: &skia::Font,
blob_offset_x: f32, blob_offset_x: f32,
blob_offset_y: f32, blob_offset_y: f32,
) -> Option<(skia::Path, skia::Rect)> { ) -> Option<(skia::Path, skia::Rect)> {
with_state_mut!(state, { with_state_mut!(state, {
let utf16_text = leaf_text.encode_utf16().collect::<Vec<u16>>(); let utf16_text = span_text.encode_utf16().collect::<Vec<u16>>();
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) }; let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
let emoji_font = state.render_state.fonts().get_emoji_font(font.size()); let emoji_font = state.render_state.fonts().get_emoji_font(font.size());
let use_font = emoji_font.as_ref().unwrap_or(font); let use_font = emoji_font.as_ref().unwrap_or(font);

View File

@@ -3,21 +3,21 @@
use crate::shapes::TextPositionWithAffinity; use crate::shapes::TextPositionWithAffinity;
/// TODO: Now this is just a tuple with 2 i32 working /// TODO: Now this is just a tuple with 2 i32 working
/// as indices (paragraph and leaf). /// as indices (paragraph and span).
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
pub struct TextNodePosition { pub struct TextNodePosition {
pub paragraph: i32, pub paragraph: i32,
pub leaf: i32, pub span: i32,
} }
impl TextNodePosition { impl TextNodePosition {
pub fn new(paragraph: i32, leaf: i32) -> Self { pub fn new(paragraph: i32, span: i32) -> Self {
Self { paragraph, leaf } Self { paragraph, span }
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn is_invalid(&self) -> bool { pub fn is_invalid(&self) -> bool {
self.paragraph < 0 || self.leaf < 0 self.paragraph < 0 || self.span < 0
} }
} }
@@ -95,7 +95,7 @@ impl TextEditorState {
self.selection.set( self.selection.set(
Some(TextNodePosition::new( Some(TextNodePosition::new(
text_position_with_affinity.paragraph, text_position_with_affinity.paragraph,
text_position_with_affinity.leaf, text_position_with_affinity.span,
)), )),
text_position_with_affinity.offset, text_position_with_affinity.offset,
); );

View File

@@ -9,7 +9,7 @@ use crate::shapes::{
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet}; use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
use crate::{with_current_shape_mut, with_state_mut, with_state_mut_current_shape, STATE}; use crate::{with_current_shape_mut, with_state_mut, with_state_mut_current_shape, STATE};
const RAW_LEAF_DATA_SIZE: usize = std::mem::size_of::<RawTextLeaf>(); const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>(); const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
const MAX_TEXT_FILLS: usize = 8; const MAX_TEXT_FILLS: usize = 8;
@@ -94,7 +94,7 @@ impl From<RawTextTransform> for Option<TextTransform> {
#[repr(align(4))] #[repr(align(4))]
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct RawParagraphData { pub struct RawParagraphData {
leaf_count: u32, span_count: u32,
text_align: RawTextAlign, text_align: RawTextAlign,
text_direction: RawTextDirection, text_direction: RawTextDirection,
text_decoration: RawTextDecoration, text_decoration: RawTextDecoration,
@@ -124,7 +124,7 @@ impl TryFrom<&[u8]> for RawParagraphData {
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct RawTextLeaf { pub struct RawTextSpan {
font_style: RawFontStyle, font_style: RawFontStyle,
text_decoration: RawTextDecoration, text_decoration: RawTextDecoration,
text_transform: RawTextTransform, text_transform: RawTextTransform,
@@ -140,25 +140,25 @@ pub struct RawTextLeaf {
fills: [RawFillData; MAX_TEXT_FILLS], fills: [RawFillData; MAX_TEXT_FILLS],
} }
impl From<[u8; RAW_LEAF_DATA_SIZE]> for RawTextLeaf { impl From<[u8; RAW_SPAN_DATA_SIZE]> for RawTextSpan {
fn from(bytes: [u8; RAW_LEAF_DATA_SIZE]) -> Self { fn from(bytes: [u8; RAW_SPAN_DATA_SIZE]) -> Self {
unsafe { std::mem::transmute(bytes) } unsafe { std::mem::transmute(bytes) }
} }
} }
impl TryFrom<&[u8]> for RawTextLeaf { impl TryFrom<&[u8]> for RawTextSpan {
type Error = String; type Error = String;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let data: [u8; RAW_LEAF_DATA_SIZE] = bytes let data: [u8; RAW_SPAN_DATA_SIZE] = bytes
.get(0..RAW_LEAF_DATA_SIZE) .get(0..RAW_SPAN_DATA_SIZE)
.and_then(|slice| slice.try_into().ok()) .and_then(|slice| slice.try_into().ok())
.ok_or("Invalid text leaf data".to_string())?; .ok_or("Invalid text span data".to_string())?;
Ok(RawTextLeaf::from(data)) Ok(RawTextSpan::from(data))
} }
} }
impl From<RawTextLeaf> for shapes::TextLeaf { impl From<RawTextSpan> for shapes::TextSpan {
fn from(value: RawTextLeaf) -> Self { fn from(value: RawTextSpan) -> Self {
let text = String::default(); let text = String::default();
let font_family = shapes::FontFamily::new( let font_family = shapes::FontFamily::new(
@@ -193,7 +193,7 @@ impl From<RawTextLeaf> for shapes::TextLeaf {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RawParagraph { pub struct RawParagraph {
attrs: RawParagraphData, attrs: RawParagraphData,
leaves: Vec<RawTextLeaf>, leaves: Vec<RawTextSpan>,
text_buffer: Vec<u8>, text_buffer: Vec<u8>,
} }
@@ -204,12 +204,12 @@ impl TryFrom<&Vec<u8>> for RawParagraph {
fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> { fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
let attrs = RawParagraphData::try_from(&bytes[..RAW_PARAGRAPH_DATA_SIZE])?; let attrs = RawParagraphData::try_from(&bytes[..RAW_PARAGRAPH_DATA_SIZE])?;
let mut offset = RAW_PARAGRAPH_DATA_SIZE; let mut offset = RAW_PARAGRAPH_DATA_SIZE;
let mut raw_text_leaves: Vec<RawTextLeaf> = Vec::new(); let mut raw_text_leaves: Vec<RawTextSpan> = Vec::new();
for _ in 0..attrs.leaf_count { for _ in 0..attrs.span_count {
let text_leaf = RawTextLeaf::try_from(&bytes[offset..(offset + RAW_LEAF_DATA_SIZE)])?; let text_span = RawTextSpan::try_from(&bytes[offset..(offset + RAW_SPAN_DATA_SIZE)])?;
offset += RAW_LEAF_DATA_SIZE; offset += RAW_SPAN_DATA_SIZE;
raw_text_leaves.push(text_leaf); raw_text_leaves.push(text_span);
} }
let text_buffer = &bytes[offset..]; let text_buffer = &bytes[offset..];
@@ -230,16 +230,16 @@ impl From<RawParagraph> for shapes::Paragraph {
let mut leaves = vec![]; let mut leaves = vec![];
let mut offset = 0; let mut offset = 0;
for raw_leaf in value.leaves.into_iter() { for raw_span in value.leaves.into_iter() {
let delta = raw_leaf.text_length as usize; let delta = raw_span.text_length as usize;
let text_buffer = &value.text_buffer[offset..offset + delta]; let text_buffer = &value.text_buffer[offset..offset + delta];
let mut leaf = shapes::TextLeaf::from(raw_leaf); let mut span = shapes::TextSpan::from(raw_span);
if !text_buffer.is_empty() { if !text_buffer.is_empty() {
leaf.set_text(String::from_utf8_lossy(text_buffer).to_string()); span.set_text(String::from_utf8_lossy(text_buffer).to_string());
} }
leaves.push(leaf); leaves.push(span);
offset += delta; offset += delta;
} }