mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
🔧 Rename textleafs and inlines to keep coherence between render and editor
This commit is contained in:
@@ -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")))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(¶graph_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
|
||||
|
||||
@@ -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(¶graph_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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user