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

View File

@@ -18,7 +18,7 @@ import {
import { resetInertElement } from "./content/dom/Style.js";
import { createRoot, createEmptyRoot } from "./content/dom/Root.js";
import { createParagraph } from "./content/dom/Paragraph.js";
import { createEmptyInline, createInline } from "./content/dom/Inline.js";
import { createEmptyTextSpan, createTextSpan } from "./content/dom/TextSpan.js";
import { isLineBreak } from "./content/dom/LineBreak.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
* can infer root, paragraph and inline declarations.
* CSS Style declaration for the current text span. From here we
* can infer root, paragraph and text span declarations.
*
* @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 {Object.<string,*>|CSSStyleDeclaration} styles
* @returns {HTMLSpanElement}
*/
createInlineFromString(text, styles) {
createTextSpanFromString(text, styles) {
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
* @returns {HTMLSpanElement}
*/
createInline(...args) {
return createInline(...args);
createTextSpan(...args) {
return createTextSpan(...args);
}
/**

View File

@@ -29,14 +29,14 @@ describe("TextEditor", () => {
const textEditor = new TextEditor(document.createElement("div"));
textEditor.root = textEditor.createRoot([
textEditor.createParagraph([
textEditor.createInlineFromString("Hello, World!"),
textEditor.createTextSpanFromString("Hello, World!"),
]),
textEditor.createParagraph([textEditor.createInlineFromString("")]),
textEditor.createParagraph([textEditor.createTextSpanFromString("")]),
textEditor.createParagraph([
textEditor.createInlineFromString("¡Hola, Mundo!"),
textEditor.createTextSpanFromString("¡Hola, Mundo!"),
]),
textEditor.createParagraph([
textEditor.createInlineFromString("Hallo, Welt!"),
textEditor.createTextSpanFromString("Hallo, Welt!"),
]),
]);
expect(textEditor).toBeInstanceOf(TextEditor);
@@ -76,7 +76,7 @@ describe("TextEditor", () => {
const textEditor = new TextEditor(textEditorElement);
textEditor.root = textEditor.createRoot([
textEditor.createParagraph([
textEditor.createInlineFromString("Hello, World!"),
textEditor.createTextSpanFromString("Hello, World!"),
]),
]);
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
// 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.
if (selectionController.isTextFocus && selectionController.focusOffset > 0) {
return selectionController.removeBackwardText();
@@ -41,11 +41,11 @@ export function deleteContentBackward(event, editor, selectionController) {
) {
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
// remove the next paragraph.
} else if (
selectionController.isInlineFocus ||
selectionController.isTextSpanFocus ||
selectionController.isLineBreakFocus
) {
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
// 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.
if (selectionController.isTextFocus
&& selectionController.focusAtEnd) {
@@ -41,11 +41,11 @@ export function deleteContentForward(event, editor, selectionController) {
) {
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
// remove the next paragraph.
} else if (
(selectionController.isInlineFocus ||
(selectionController.isTextSpanFocus ||
selectionController.isLineBreakFocus) &&
editor.numParagraphs > 1
) {

View File

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

View File

@@ -6,7 +6,7 @@
* Copyright (c) KALEIDOS INC
*/
import { createInline, isLikeInline } from "./Inline.js";
import { createTextSpan, isLikeTextSpan } from "./TextSpan.js";
import {
createEmptyParagraph,
createParagraph,
@@ -21,11 +21,11 @@ const DEFAULT_FILLS = '[["^ ","~:fill-color", "#000000","~:fill-opacity", 1]]';
/**
* 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}
*/
function isContentFragmentFromDocumentInline(document) {
function isContentFragmentFromDocumentTextSpan(document) {
const nodeIterator = document.createNodeIterator(
document.documentElement,
NodeFilter.SHOW_ELEMENT,
@@ -37,7 +37,7 @@ function isContentFragmentFromDocumentInline(document) {
continue;
}
if (!isLikeInline(currentNode)) return false;
if (!isLikeTextSpan(currentNode)) return false;
currentNode = nodeIterator.nextNode();
}
@@ -75,29 +75,29 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) {
currentParagraph = createParagraph(undefined, currentStyle);
}
}
const inline = createInline(new Text(currentNode.nodeValue), currentStyle);
const fontSize = inline.style.getPropertyValue("font-size");
const textSpan = createTextSpan(new Text(currentNode.nodeValue), currentStyle);
const fontSize = textSpan.style.getPropertyValue("font-size");
if (!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) {
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) {
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) {
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();
}
@@ -107,9 +107,9 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) {
}
if (fragment.children.length === 1) {
const isContentInline = isContentFragmentFromDocumentInline(document);
if (isContentInline) {
currentParagraph.dataset.inline = "force";
const isContentTextSpan = isContentFragmentFromDocumentTextSpan(document);
if (isContentTextSpan) {
currentParagraph.dataset.textSpan = "force";
}
}
@@ -149,7 +149,7 @@ export function mapContentFragmentFromString(string, styleDefaults) {
} else {
fragment.appendChild(
createParagraph(
[createInline(new Text(line), styleDefaults)],
[createTextSpan(new Text(line), styleDefaults)],
styleDefaults,
),
);

View File

@@ -24,7 +24,7 @@ describe("Content", () => {
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 contentFragment = mapContentFragmentFromHTML(
"<div>Hello,<br/><span> World!</span><br/></div>",

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
* Copyright (c) KALEIDOS INC
*/
import { isInline } from "./Inline.js";
import { isTextSpan } from "./TextSpan.js";
import { isLineBreak } from "./LineBreak.js";
import { isParagraph } from "./Paragraph.js";
import { isEditor } from "./Editor.js";
@@ -58,7 +58,7 @@ export function getTextNodeLength(node) {
*/
export function getClosestTextNode(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 (isRoot(node)) return node.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 TextNodeIterator from "./TextNodeIterator.js";
import { createInline } from "./Inline.js";
import { createTextSpan } from "./TextSpan.js";
import { createParagraph } from "./Paragraph.js";
import { createRoot } from "./Root.js";
import { createLineBreak } from "./LineBreak.js";
@@ -21,16 +21,16 @@ describe("TextNodeIterator", () => {
test("Create a new TextNodeIterator and iterate only over text nodes", () => {
const rootNode = createRoot([
createParagraph([
createInline(new Text("Hello, ")),
createInline(new Text("World!")),
createInline(new Text("Whatever")),
createTextSpan(new Text("Hello, ")),
createTextSpan(new Text("World!")),
createTextSpan(new Text("Whatever")),
]),
createParagraph([createInline(createLineBreak())]),
createParagraph([createTextSpan(createLineBreak())]),
createParagraph([
createInline(new Text("This is a ")),
createInline(new Text("test")),
createTextSpan(new Text("This is a ")),
createTextSpan(new Text("test")),
]),
createParagraph([createInline(new Text("Hi!"))]),
createParagraph([createTextSpan(new Text("Hi!"))]),
]);
const textNodeIterator = new TextNodeIterator(rootNode);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { createRoot } from "../editor/content/dom/Root.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";
export class TextEditorMock extends EventTarget {
@@ -68,7 +68,7 @@ export class TextEditorMock extends EventTarget {
*/
static createTextEditorMockEmpty() {
const root = createRoot([
createParagraph([createInline(createLineBreak())]),
createParagraph([createTextSpan(createLineBreak())]),
]);
return this.createTextEditorMockWithRoot(root);
}
@@ -76,7 +76,7 @@ export class TextEditorMock extends EventTarget {
/**
* 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.
*
* @param {string} text
@@ -86,21 +86,21 @@ export class TextEditorMock extends EventTarget {
return this.createTextEditorMockWithParagraphs([
createParagraph([
text.length === 0
? createEmptyInline()
: createInline(new Text(text))
? createEmptyTextSpan()
: createTextSpan(new Text(text))
]),
]);
}
/**
* Creates a TextEditor mock with some inlines and
* Creates a TextEditor mock with some textSpans and
* only one paragraph.
*
* @param {Array<HTMLSpanElement>} inlines
* @param {Array<HTMLSpanElement>} textSpans
* @returns
*/
static createTextEditorMockWithParagraph(inlines) {
return this.createTextEditorMockWithParagraphs([createParagraph(inlines)]);
static createTextEditorMockWithParagraph(textSpans) {
return this.createTextEditorMockWithParagraphs([createParagraph(textSpans)]);
}
#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> =
std::collections::HashMap::new();
for leaf in paragraph.children().iter() {
let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds);
for span in paragraph.children().iter() {
let text_paint: skia_safe::Handle<_> = merge_fills(span.fills(), *bounds);
let stroke_paints = get_text_stroke_paints(
stroke,
bounds,
@@ -43,7 +43,7 @@ pub fn stroke_paragraph_builder_group_from_text(
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() {
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)
});
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 =
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.add_text(&text);
}
@@ -342,7 +342,7 @@ fn render_text_decoration(
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
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() {
let text_style = &style_metric.text_style;
let style_end = style_metrics

View File

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

View File

@@ -37,7 +37,7 @@ impl TextPaths {
let start = line_metrics.start_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 mut offset_x = 0.0;
@@ -56,23 +56,23 @@ impl TextPaths {
.map(|(i, _)| i)
.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 blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
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(
leaf_text,
span_text,
&font,
blob_offset_x,
blob_offset_y,
style_metric,
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;
paths.push((text_path, paint));
}
@@ -87,7 +87,7 @@ impl TextPaths {
fn generate_text_path(
&self,
leaf_text: &str,
span_text: &str,
font: &skia::Font,
blob_offset_x: f32,
blob_offset_y: f32,
@@ -99,10 +99,10 @@ impl TextPaths {
// This is used to avoid rendering empty paths, but we can
// revisit this logic later
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 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 font_metrics = style_metric.font_metrics;
@@ -165,13 +165,13 @@ impl TextPaths {
}
fn get_text_blob_path(
leaf_text: &str,
span_text: &str,
font: &skia::Font,
blob_offset_x: f32,
blob_offset_y: f32,
) -> Option<(skia::Path, skia::Rect)> {
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 emoji_font = state.render_state.fonts().get_emoji_font(font.size());
let use_font = emoji_font.as_ref().unwrap_or(font);

View File

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