mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +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]))
|
[app.render-wasm.wasm :as wasm]))
|
||||||
|
|
||||||
(def ^:const PARAGRAPH-ATTR-U8-SIZE 44)
|
(def ^:const PARAGRAPH-ATTR-U8-SIZE 44)
|
||||||
(def ^:const LEAF-ATTR-U8-SIZE 60)
|
(def ^:const SPAN-ATTR-U8-SIZE 60)
|
||||||
(def ^:const MAX-TEXT-FILLS types.fills.impl/MAX-FILLS)
|
(def ^:const MAX-TEXT-FILLS types.fills.impl/MAX-FILLS)
|
||||||
|
|
||||||
(defn- encode-text
|
(defn- encode-text
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
(let [encoder (js/TextEncoder.)]
|
(let [encoder (js/TextEncoder.)]
|
||||||
(.encode encoder text)))
|
(.encode encoder text)))
|
||||||
|
|
||||||
(defn- write-leaf-fills
|
(defn- write-span-fills
|
||||||
[offset dview fills]
|
[offset dview fills]
|
||||||
(let [new-ofset (reduce (fn [offset fill]
|
(let [new-ofset (reduce (fn [offset fill]
|
||||||
(let [opacity (get fill :fill-opacity 1.0)
|
(let [opacity (get fill :fill-opacity 1.0)
|
||||||
@@ -76,20 +76,20 @@
|
|||||||
|
|
||||||
(defn- write-leaves
|
(defn- write-leaves
|
||||||
[offset dview leaves paragraph]
|
[offset dview leaves paragraph]
|
||||||
(reduce (fn [offset leaf]
|
(reduce (fn [offset span]
|
||||||
(let [font-style (sr/translate-font-style (get leaf :font-style))
|
(let [font-style (sr/translate-font-style (get span :font-style))
|
||||||
font-size (get leaf :font-size)
|
font-size (get span :font-size)
|
||||||
letter-spacing (get leaf :letter-spacing)
|
letter-spacing (get span :letter-spacing)
|
||||||
font-weight (get leaf :font-weight)
|
font-weight (get span :font-weight)
|
||||||
font-id (f/normalize-font-id (get leaf :font-id))
|
font-id (f/normalize-font-id (get span :font-id))
|
||||||
font-family (hash (get leaf :font-family))
|
font-family (hash (get span :font-family))
|
||||||
|
|
||||||
text-buffer (encode-text (get leaf :text))
|
text-buffer (encode-text (get span :text))
|
||||||
text-length (mem/size text-buffer)
|
text-length (mem/size text-buffer)
|
||||||
fills (take MAX-TEXT-FILLS (get leaf :fills))
|
fills (take MAX-TEXT-FILLS (get span :fills))
|
||||||
|
|
||||||
font-variant-id
|
font-variant-id
|
||||||
(get leaf :font-variant-id)
|
(get span :font-variant-id)
|
||||||
|
|
||||||
font-variant-id
|
font-variant-id
|
||||||
(if (uuid? font-variant-id)
|
(if (uuid? font-variant-id)
|
||||||
@@ -97,17 +97,17 @@
|
|||||||
uuid/zero)
|
uuid/zero)
|
||||||
|
|
||||||
text-decoration
|
text-decoration
|
||||||
(or (sr/translate-text-decoration (:text-decoration leaf))
|
(or (sr/translate-text-decoration (:text-decoration span))
|
||||||
(sr/translate-text-decoration (:text-decoration paragraph))
|
(sr/translate-text-decoration (:text-decoration paragraph))
|
||||||
(sr/translate-text-decoration "none"))
|
(sr/translate-text-decoration "none"))
|
||||||
|
|
||||||
text-transform
|
text-transform
|
||||||
(or (sr/translate-text-transform (:text-transform leaf))
|
(or (sr/translate-text-transform (:text-transform span))
|
||||||
(sr/translate-text-transform (:text-transform paragraph))
|
(sr/translate-text-transform (:text-transform paragraph))
|
||||||
(sr/translate-text-transform "none"))
|
(sr/translate-text-transform "none"))
|
||||||
|
|
||||||
text-direction
|
text-direction
|
||||||
(or (sr/translate-text-direction (:text-direction leaf))
|
(or (sr/translate-text-direction (:text-direction span))
|
||||||
(sr/translate-text-direction (:text-direction paragraph))
|
(sr/translate-text-direction (:text-direction paragraph))
|
||||||
(sr/translate-text-direction "ltr"))]
|
(sr/translate-text-direction "ltr"))]
|
||||||
|
|
||||||
@@ -127,20 +127,20 @@
|
|||||||
|
|
||||||
(mem/write-i32 dview text-length)
|
(mem/write-i32 dview text-length)
|
||||||
(mem/write-i32 dview (count fills))
|
(mem/write-i32 dview (count fills))
|
||||||
(mem/assert-written offset LEAF-ATTR-U8-SIZE)
|
(mem/assert-written offset SPAN-ATTR-U8-SIZE)
|
||||||
|
|
||||||
(write-leaf-fills dview fills))))
|
(write-span-fills dview fills))))
|
||||||
offset
|
offset
|
||||||
leaves))
|
leaves))
|
||||||
|
|
||||||
(defn write-shape-text
|
(defn write-shape-text
|
||||||
;; buffer has the following format:
|
;; buffer has the following format:
|
||||||
;; [<num-leaves> <paragraph_attributes> <leaves_attributes> <text>]
|
;; [<num-leaves> <paragraph_attributes> <leaves_attributes> <text>]
|
||||||
[leaves paragraph text]
|
[spans paragraph text]
|
||||||
(let [num-leaves (count leaves)
|
(let [num-spans (count spans)
|
||||||
fills-size (* types.fills.impl/FILL-U8-SIZE MAX-TEXT-FILLS)
|
fills-size (* types.fills.impl/FILL-U8-SIZE MAX-TEXT-FILLS)
|
||||||
metadata-size (+ PARAGRAPH-ATTR-U8-SIZE
|
metadata-size (+ PARAGRAPH-ATTR-U8-SIZE
|
||||||
(* num-leaves (+ LEAF-ATTR-U8-SIZE fills-size)))
|
(* num-spans (+ SPAN-ATTR-U8-SIZE fills-size)))
|
||||||
|
|
||||||
text-buffer (encode-text text)
|
text-buffer (encode-text text)
|
||||||
text-size (mem/size text-buffer)
|
text-size (mem/size text-buffer)
|
||||||
@@ -151,9 +151,9 @@
|
|||||||
offset (mem/alloc total-size)]
|
offset (mem/alloc total-size)]
|
||||||
|
|
||||||
(-> offset
|
(-> offset
|
||||||
(mem/write-u32 dview num-leaves)
|
(mem/write-u32 dview num-spans)
|
||||||
(write-paragraph dview paragraph)
|
(write-paragraph dview paragraph)
|
||||||
(write-leaves dview leaves paragraph)
|
(write-leaves dview spans paragraph)
|
||||||
(mem/write-buffer heapu8 text-buffer))
|
(mem/write-buffer heapu8 text-buffer))
|
||||||
|
|
||||||
(h/call wasm/internal-module "_set_shape_text_content")))
|
(h/call wasm/internal-module "_set_shape_text_content")))
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { resetInertElement } from "./content/dom/Style.js";
|
import { resetInertElement } from "./content/dom/Style.js";
|
||||||
import { createRoot, createEmptyRoot } from "./content/dom/Root.js";
|
import { createRoot, createEmptyRoot } from "./content/dom/Root.js";
|
||||||
import { createParagraph } from "./content/dom/Paragraph.js";
|
import { createParagraph } from "./content/dom/Paragraph.js";
|
||||||
import { createEmptyInline, createInline } from "./content/dom/Inline.js";
|
import { createEmptyTextSpan, createTextSpan } from "./content/dom/TextSpan.js";
|
||||||
import { isLineBreak } from "./content/dom/LineBreak.js";
|
import { isLineBreak } from "./content/dom/LineBreak.js";
|
||||||
import LayoutType from "./layout/LayoutType.js";
|
import LayoutType from "./layout/LayoutType.js";
|
||||||
|
|
||||||
@@ -428,8 +428,8 @@ export class TextEditor extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSS Style declaration for the current inline. From here we
|
* CSS Style declaration for the current text span. From here we
|
||||||
* can infer root, paragraph and inline declarations.
|
* can infer root, paragraph and text span declarations.
|
||||||
*
|
*
|
||||||
* @type {CSSStyleDeclaration}
|
* @type {CSSStyleDeclaration}
|
||||||
*/
|
*/
|
||||||
@@ -472,27 +472,27 @@ export class TextEditor extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new inline from a string.
|
* Creates a new text span from a string.
|
||||||
*
|
*
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
||||||
* @returns {HTMLSpanElement}
|
* @returns {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
createInlineFromString(text, styles) {
|
createTextSpanFromString(text, styles) {
|
||||||
if (text === "") {
|
if (text === "") {
|
||||||
return createEmptyInline(styles);
|
return createEmptyTextSpan(styles);
|
||||||
}
|
}
|
||||||
return createInline(new Text(text), styles);
|
return createTextSpan(new Text(text), styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new inline.
|
* Creates a new text span.
|
||||||
*
|
*
|
||||||
* @param {...any} args
|
* @param {...any} args
|
||||||
* @returns {HTMLSpanElement}
|
* @returns {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
createInline(...args) {
|
createTextSpan(...args) {
|
||||||
return createInline(...args);
|
return createTextSpan(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ describe("TextEditor", () => {
|
|||||||
const textEditor = new TextEditor(document.createElement("div"));
|
const textEditor = new TextEditor(document.createElement("div"));
|
||||||
textEditor.root = textEditor.createRoot([
|
textEditor.root = textEditor.createRoot([
|
||||||
textEditor.createParagraph([
|
textEditor.createParagraph([
|
||||||
textEditor.createInlineFromString("Hello, World!"),
|
textEditor.createTextSpanFromString("Hello, World!"),
|
||||||
]),
|
]),
|
||||||
textEditor.createParagraph([textEditor.createInlineFromString("")]),
|
textEditor.createParagraph([textEditor.createTextSpanFromString("")]),
|
||||||
textEditor.createParagraph([
|
textEditor.createParagraph([
|
||||||
textEditor.createInlineFromString("¡Hola, Mundo!"),
|
textEditor.createTextSpanFromString("¡Hola, Mundo!"),
|
||||||
]),
|
]),
|
||||||
textEditor.createParagraph([
|
textEditor.createParagraph([
|
||||||
textEditor.createInlineFromString("Hallo, Welt!"),
|
textEditor.createTextSpanFromString("Hallo, Welt!"),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||||
@@ -76,7 +76,7 @@ describe("TextEditor", () => {
|
|||||||
const textEditor = new TextEditor(textEditorElement);
|
const textEditor = new TextEditor(textEditorElement);
|
||||||
textEditor.root = textEditor.createRoot([
|
textEditor.root = textEditor.createRoot([
|
||||||
textEditor.createParagraph([
|
textEditor.createParagraph([
|
||||||
textEditor.createInlineFromString("Hello, World!"),
|
textEditor.createTextSpanFromString("Hello, World!"),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function deleteContentBackward(event, editor, selectionController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we're in a text node and the offset is
|
// If we're in a text node and the offset is
|
||||||
// greater than 0 (not at the start of the inline)
|
// greater than 0 (not at the start of the text span)
|
||||||
// we simple remove a character from the text.
|
// we simple remove a character from the text.
|
||||||
if (selectionController.isTextFocus && selectionController.focusOffset > 0) {
|
if (selectionController.isTextFocus && selectionController.focusOffset > 0) {
|
||||||
return selectionController.removeBackwardText();
|
return selectionController.removeBackwardText();
|
||||||
@@ -41,11 +41,11 @@ export function deleteContentBackward(event, editor, selectionController) {
|
|||||||
) {
|
) {
|
||||||
return selectionController.mergeBackwardParagraph();
|
return selectionController.mergeBackwardParagraph();
|
||||||
|
|
||||||
// If we're at an inline or a line break paragraph
|
// If we're at an text span or a line break paragraph
|
||||||
// and there's more than one paragraph, then we should
|
// and there's more than one paragraph, then we should
|
||||||
// remove the next paragraph.
|
// remove the next paragraph.
|
||||||
} else if (
|
} else if (
|
||||||
selectionController.isInlineFocus ||
|
selectionController.isTextSpanFocus ||
|
||||||
selectionController.isLineBreakFocus
|
selectionController.isLineBreakFocus
|
||||||
) {
|
) {
|
||||||
return selectionController.removeBackwardParagraph();
|
return selectionController.removeBackwardParagraph();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function deleteContentForward(event, editor, selectionController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we're in a text node and the offset is
|
// If we're in a text node and the offset is
|
||||||
// greater than 0 (not at the start of the inline)
|
// greater than 0 (not at the start of the text span)
|
||||||
// we simple remove a character from the text.
|
// we simple remove a character from the text.
|
||||||
if (selectionController.isTextFocus
|
if (selectionController.isTextFocus
|
||||||
&& selectionController.focusAtEnd) {
|
&& selectionController.focusAtEnd) {
|
||||||
@@ -41,11 +41,11 @@ export function deleteContentForward(event, editor, selectionController) {
|
|||||||
) {
|
) {
|
||||||
return selectionController.removeForwardText();
|
return selectionController.removeForwardText();
|
||||||
|
|
||||||
// If we're at an inline or a line break paragraph
|
// If we're at a text span or a line break paragraph
|
||||||
// and there's more than one paragraph, then we should
|
// and there's more than one paragraph, then we should
|
||||||
// remove the next paragraph.
|
// remove the next paragraph.
|
||||||
} else if (
|
} else if (
|
||||||
(selectionController.isInlineFocus ||
|
(selectionController.isTextSpanFocus ||
|
||||||
selectionController.isLineBreakFocus) &&
|
selectionController.isLineBreakFocus) &&
|
||||||
editor.numParagraphs > 1
|
editor.numParagraphs > 1
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export function insertText(event, editor, selectionController) {
|
|||||||
} else {
|
} else {
|
||||||
if (selectionController.isMultiParagraph) {
|
if (selectionController.isMultiParagraph) {
|
||||||
return selectionController.replaceParagraphs(event.data);
|
return selectionController.replaceParagraphs(event.data);
|
||||||
} else if (selectionController.isMultiInline) {
|
} else if (selectionController.isMultiTextSpan) {
|
||||||
return selectionController.replaceInlines(event.data);
|
return selectionController.replaceTextSpans(event.data);
|
||||||
} else if (selectionController.isTextSame) {
|
} else if (selectionController.isTextSame) {
|
||||||
return selectionController.replaceText(event.data);
|
return selectionController.replaceText(event.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Copyright (c) KALEIDOS INC
|
* Copyright (c) KALEIDOS INC
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createInline, isLikeInline } from "./Inline.js";
|
import { createTextSpan, isLikeTextSpan } from "./TextSpan.js";
|
||||||
import {
|
import {
|
||||||
createEmptyParagraph,
|
createEmptyParagraph,
|
||||||
createParagraph,
|
createParagraph,
|
||||||
@@ -21,11 +21,11 @@ const DEFAULT_FILLS = '[["^ ","~:fill-color", "#000000","~:fill-opacity", 1]]';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the content fragment should be treated as
|
* Returns if the content fragment should be treated as
|
||||||
* inline content and not a paragraphed one.
|
* text span content and not a paragraphed one.
|
||||||
*
|
*
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function isContentFragmentFromDocumentInline(document) {
|
function isContentFragmentFromDocumentTextSpan(document) {
|
||||||
const nodeIterator = document.createNodeIterator(
|
const nodeIterator = document.createNodeIterator(
|
||||||
document.documentElement,
|
document.documentElement,
|
||||||
NodeFilter.SHOW_ELEMENT,
|
NodeFilter.SHOW_ELEMENT,
|
||||||
@@ -37,7 +37,7 @@ function isContentFragmentFromDocumentInline(document) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLikeInline(currentNode)) return false;
|
if (!isLikeTextSpan(currentNode)) return false;
|
||||||
|
|
||||||
currentNode = nodeIterator.nextNode();
|
currentNode = nodeIterator.nextNode();
|
||||||
}
|
}
|
||||||
@@ -75,29 +75,29 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) {
|
|||||||
currentParagraph = createParagraph(undefined, currentStyle);
|
currentParagraph = createParagraph(undefined, currentStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const inline = createInline(new Text(currentNode.nodeValue), currentStyle);
|
const textSpan = createTextSpan(new Text(currentNode.nodeValue), currentStyle);
|
||||||
const fontSize = inline.style.getPropertyValue("font-size");
|
const fontSize = textSpan.style.getPropertyValue("font-size");
|
||||||
if (!fontSize) {
|
if (!fontSize) {
|
||||||
console.warn("font-size", fontSize);
|
console.warn("font-size", fontSize);
|
||||||
inline.style.setProperty("font-size", styleDefaults?.getPropertyValue("font-size") ?? DEFAULT_FONT_SIZE);
|
textSpan.style.setProperty("font-size", styleDefaults?.getPropertyValue("font-size") ?? DEFAULT_FONT_SIZE);
|
||||||
}
|
}
|
||||||
const fontFamily = inline.style.getPropertyValue("font-family");
|
const fontFamily = textSpan.style.getPropertyValue("font-family");
|
||||||
if (!fontFamily) {
|
if (!fontFamily) {
|
||||||
console.warn("font-family", fontFamily);
|
console.warn("font-family", fontFamily);
|
||||||
inline.style.setProperty("font-family", styleDefaults?.getPropertyValue("font-family") ?? DEFAULT_FONT_FAMILY);
|
textSpan.style.setProperty("font-family", styleDefaults?.getPropertyValue("font-family") ?? DEFAULT_FONT_FAMILY);
|
||||||
}
|
}
|
||||||
const fontWeight = inline.style.getPropertyValue("font-weight");
|
const fontWeight = textSpan.style.getPropertyValue("font-weight");
|
||||||
if (!fontWeight) {
|
if (!fontWeight) {
|
||||||
console.warn("font-weight", fontWeight);
|
console.warn("font-weight", fontWeight);
|
||||||
inline.style.setProperty("font-weight", styleDefaults?.getPropertyValue("font-weight") ?? DEFAULT_FONT_WEIGHT)
|
textSpan.style.setProperty("font-weight", styleDefaults?.getPropertyValue("font-weight") ?? DEFAULT_FONT_WEIGHT)
|
||||||
}
|
}
|
||||||
const fills = inline.style.getPropertyValue('--fills');
|
const fills = textSpan.style.getPropertyValue('--fills');
|
||||||
if (!fills) {
|
if (!fills) {
|
||||||
console.warn("fills", fills);
|
console.warn("fills", fills);
|
||||||
inline.style.setProperty("--fills", styleDefaults?.getPropertyValue("--fills") ?? DEFAULT_FILLS);
|
textSpan.style.setProperty("--fills", styleDefaults?.getPropertyValue("--fills") ?? DEFAULT_FILLS);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentParagraph.appendChild(inline);
|
currentParagraph.appendChild(textSpan);
|
||||||
|
|
||||||
currentNode = nodeIterator.nextNode();
|
currentNode = nodeIterator.nextNode();
|
||||||
}
|
}
|
||||||
@@ -107,9 +107,9 @@ export function mapContentFragmentFromDocument(document, root, styleDefaults) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fragment.children.length === 1) {
|
if (fragment.children.length === 1) {
|
||||||
const isContentInline = isContentFragmentFromDocumentInline(document);
|
const isContentTextSpan = isContentFragmentFromDocumentTextSpan(document);
|
||||||
if (isContentInline) {
|
if (isContentTextSpan) {
|
||||||
currentParagraph.dataset.inline = "force";
|
currentParagraph.dataset.textSpan = "force";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ export function mapContentFragmentFromString(string, styleDefaults) {
|
|||||||
} else {
|
} else {
|
||||||
fragment.appendChild(
|
fragment.appendChild(
|
||||||
createParagraph(
|
createParagraph(
|
||||||
[createInline(new Text(line), styleDefaults)],
|
[createTextSpan(new Text(line), styleDefaults)],
|
||||||
styleDefaults,
|
styleDefaults,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ describe("Content", () => {
|
|||||||
expect(contentFragment.textContent).toBe("Hello, World!");
|
expect(contentFragment.textContent).toBe("Hello, World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mapContentFragmentFromHTML should return a valid content for the editor (multiple inlines)", () => {
|
test("mapContentFragmentFromHTML should return a valid content for the editor (multiple text spans)", () => {
|
||||||
const inertElement = document.createElement("div");
|
const inertElement = document.createElement("div");
|
||||||
const contentFragment = mapContentFragmentFromHTML(
|
const contentFragment = mapContentFragmentFromHTML(
|
||||||
"<div>Hello,<br/><span> World!</span><br/></div>",
|
"<div>Hello,<br/><span> World!</span><br/></div>",
|
||||||
|
|||||||
@@ -1,123 +1,123 @@
|
|||||||
import { describe, test, expect } from "vitest";
|
import { describe, test, expect } from "vitest";
|
||||||
import {
|
import {
|
||||||
createEmptyInline,
|
createEmptyTextSpan,
|
||||||
createInline,
|
createTextSpan,
|
||||||
getInline,
|
getTextSpan,
|
||||||
getInlineLength,
|
getTextSpanLength,
|
||||||
isInline,
|
isTextSpan,
|
||||||
isInlineEnd,
|
isTextSpanEnd,
|
||||||
isInlineStart,
|
isTextSpanStart,
|
||||||
isLikeInline,
|
isLikeTextSpan,
|
||||||
splitInline,
|
splitTextSpan,
|
||||||
TAG,
|
TAG,
|
||||||
TYPE,
|
TYPE,
|
||||||
} from "./Inline.js";
|
} from "./TextSpan.js";
|
||||||
import { createLineBreak } from "./LineBreak.js";
|
import { createLineBreak } from "./LineBreak.js";
|
||||||
|
|
||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
describe("Inline", () => {
|
describe("TextSpan", () => {
|
||||||
test("createInline should throw when passed an invalid child", () => {
|
test("createTextSpan should throw when passed an invalid child", () => {
|
||||||
expect(() => createInline("Hello, World!")).toThrowError(
|
expect(() => createTextSpan("Hello, World!")).toThrowError(
|
||||||
"Invalid inline child",
|
"Invalid textSpan child",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("createInline creates a new inline element with a <br> inside", () => {
|
test("createTextSpan creates a new textSpan element with a <br> inside", () => {
|
||||||
const inline = createInline(createLineBreak());
|
const textSpan = createTextSpan(createLineBreak());
|
||||||
expect(inline).toBeInstanceOf(HTMLSpanElement);
|
expect(textSpan).toBeInstanceOf(HTMLSpanElement);
|
||||||
expect(inline.dataset.itype).toBe(TYPE);
|
expect(textSpan.dataset.itype).toBe(TYPE);
|
||||||
expect(inline.nodeName).toBe(TAG);
|
expect(textSpan.nodeName).toBe(TAG);
|
||||||
expect(inline.textContent).toBe("");
|
expect(textSpan.textContent).toBe("");
|
||||||
expect(inline.firstChild).toBeInstanceOf(HTMLBRElement);
|
expect(textSpan.firstChild).toBeInstanceOf(HTMLBRElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("createInline creates a new inline element with a text inside", () => {
|
test("createTextSpan creates a new textSpan element with a text inside", () => {
|
||||||
const inline = createInline(new Text("Hello, World!"));
|
const textSpan = createTextSpan(new Text("Hello, World!"));
|
||||||
expect(inline).toBeInstanceOf(HTMLSpanElement);
|
expect(textSpan).toBeInstanceOf(HTMLSpanElement);
|
||||||
expect(inline.dataset.itype).toBe(TYPE);
|
expect(textSpan.dataset.itype).toBe(TYPE);
|
||||||
expect(inline.nodeName).toBe(TAG);
|
expect(textSpan.nodeName).toBe(TAG);
|
||||||
expect(inline.textContent).toBe("Hello, World!");
|
expect(textSpan.textContent).toBe("Hello, World!");
|
||||||
expect(inline.firstChild).toBeInstanceOf(Text);
|
expect(textSpan.firstChild).toBeInstanceOf(Text);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("createEmptyInline creates a new empty inline element with a <br> inside", () => {
|
test("createEmptyTextSpan creates a new empty textSpan element with a <br> inside", () => {
|
||||||
const emptyInline = createEmptyInline();
|
const emptyTextSpan = createEmptyTextSpan();
|
||||||
expect(emptyInline).toBeInstanceOf(HTMLSpanElement);
|
expect(emptyTextSpan).toBeInstanceOf(HTMLSpanElement);
|
||||||
expect(emptyInline.dataset.itype).toBe(TYPE);
|
expect(emptyTextSpan.dataset.itype).toBe(TYPE);
|
||||||
expect(emptyInline.nodeName).toBe(TAG);
|
expect(emptyTextSpan.nodeName).toBe(TAG);
|
||||||
expect(emptyInline.textContent).toBe("");
|
expect(emptyTextSpan.textContent).toBe("");
|
||||||
expect(emptyInline.firstChild).toBeInstanceOf(HTMLBRElement);
|
expect(emptyTextSpan.firstChild).toBeInstanceOf(HTMLBRElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isInline should return true on elements that are inlines", () => {
|
test("isTextSpan should return true on elements that are text spans", () => {
|
||||||
const inline = createInline(new Text("Hello, World!"));
|
const textSpan = createTextSpan(new Text("Hello, World!"));
|
||||||
expect(isInline(inline)).toBe(true);
|
expect(isTextSpan(textSpan)).toBe(true);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
expect(isInline(a)).toBe(false);
|
expect(isTextSpan(a)).toBe(false);
|
||||||
const b = null;
|
const b = null;
|
||||||
expect(isInline(b)).toBe(false);
|
expect(isTextSpan(b)).toBe(false);
|
||||||
const c = document.createElement("span");
|
const c = document.createElement("span");
|
||||||
expect(isInline(c)).toBe(false);
|
expect(isTextSpan(c)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isLikeInline should return true on elements that have inline behavior by default", () => {
|
test("isLikeTextSpan should return true on elements that have textSpan behavior by default", () => {
|
||||||
expect(isLikeInline(Infinity)).toBe(false);
|
expect(isLikeTextSpan(Infinity)).toBe(false);
|
||||||
expect(isLikeInline(null)).toBe(false);
|
expect(isLikeTextSpan(null)).toBe(false);
|
||||||
expect(isLikeInline(document.createElement("A"))).toBe(true);
|
expect(isLikeTextSpan(document.createElement("A"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME: Should throw?
|
// FIXME: Should throw?
|
||||||
test("isInlineStart returns false when passed node is not an inline", () => {
|
test("isTextSpanStart returns false when passed node is not an textSpan", () => {
|
||||||
const inline = document.createElement("div");
|
const textSpan = document.createElement("div");
|
||||||
expect(isInlineStart(inline, 0)).toBe(false);
|
expect(isTextSpanStart(textSpan, 0)).toBe(false);
|
||||||
expect(isInlineStart(inline, "Hello, World!".length)).toBe(false);
|
expect(isTextSpanStart(textSpan, "Hello, World!".length)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isInlineStart returns if we're at the start of an inline", () => {
|
test("isTextSpanStart returns if we're at the start of an textSpan", () => {
|
||||||
const inline = createInline(new Text("Hello, World!"));
|
const textSpan = createTextSpan(new Text("Hello, World!"));
|
||||||
expect(isInlineStart(inline, 0)).toBe(true);
|
expect(isTextSpanStart(textSpan, 0)).toBe(true);
|
||||||
expect(isInlineStart(inline, "Hello, World!".length)).toBe(false);
|
expect(isTextSpanStart(textSpan, "Hello, World!".length)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME: Should throw?
|
// FIXME: Should throw?
|
||||||
test("isInlineEnd returns false when passed node is not an inline", () => {
|
test("isTextSpanEnd returns false when passed node is not an textSpan", () => {
|
||||||
const inline = document.createElement("div");
|
const textSpan = document.createElement("div");
|
||||||
expect(isInlineEnd(inline, 0)).toBe(false);
|
expect(isTextSpanEnd(textSpan, 0)).toBe(false);
|
||||||
expect(isInlineEnd(inline, "Hello, World!".length)).toBe(false);
|
expect(isTextSpanEnd(textSpan, "Hello, World!".length)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isInlineEnd returns if we're in the end of an inline", () => {
|
test("isTextSpanEnd returns if we're in the end of an textSpan", () => {
|
||||||
const inline = createInline(new Text("Hello, World!"));
|
const textSpan = createTextSpan(new Text("Hello, World!"));
|
||||||
expect(isInlineEnd(inline, 0)).toBe(false);
|
expect(isTextSpanEnd(textSpan, 0)).toBe(false);
|
||||||
expect(isInlineEnd(inline, "Hello, World!".length)).toBe(true);
|
expect(isTextSpanEnd(textSpan, "Hello, World!".length)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getInline ", () => {
|
test("getTextSpan ", () => {
|
||||||
expect(getInline(null)).toBe(null);
|
expect(getTextSpan(null)).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getInlineLength throws when the passed node is not an inline", () => {
|
test("getTextSpanLength throws when the passed node is not an textSpan", () => {
|
||||||
const inline = document.createElement("div");
|
const textSpan = document.createElement("div");
|
||||||
expect(() => getInlineLength(inline)).toThrowError("Invalid inline");
|
expect(() => getTextSpanLength(textSpan)).toThrowError("Invalid textSpan");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getInlineLength returns the length of the inline content", () => {
|
test("getTextSpanLength returns the length of the textSpan content", () => {
|
||||||
const inline = createInline(new Text("Hello, World!"));
|
const textSpan = createTextSpan(new Text("Hello, World!"));
|
||||||
expect(getInlineLength(inline)).toBe(13);
|
expect(getTextSpanLength(textSpan)).toBe(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getInlineLength should return 0 when the inline content is a <br>", () => {
|
test("getTextSpanLength should return 0 when the textSpan content is a <br>", () => {
|
||||||
const emptyInline = createEmptyInline();
|
const emptyTextSpan = createEmptyTextSpan();
|
||||||
expect(getInlineLength(emptyInline)).toBe(0);
|
expect(getTextSpanLength(emptyTextSpan)).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("splitInline returns a new inline from the splitted inline", () => {
|
test("splitTextSpan returns a new textSpan from the splitted textSpan", () => {
|
||||||
const inline = createInline(new Text("Hello, World!"));
|
const textSpan = createTextSpan(new Text("Hello, World!"));
|
||||||
const newInline = splitInline(inline, 5);
|
const newTextSpan = splitTextSpan(textSpan, 5);
|
||||||
expect(newInline).toBeInstanceOf(HTMLSpanElement);
|
expect(newTextSpan).toBeInstanceOf(HTMLSpanElement);
|
||||||
expect(newInline.firstChild).toBeInstanceOf(Text);
|
expect(newTextSpan.firstChild).toBeInstanceOf(Text);
|
||||||
expect(newInline.textContent).toBe(", World!");
|
expect(newTextSpan.textContent).toBe(", World!");
|
||||||
expect(newInline.dataset.itype).toBe(TYPE);
|
expect(newTextSpan.dataset.itype).toBe(TYPE);
|
||||||
expect(newInline.nodeName).toBe(TAG);
|
expect(newTextSpan.nodeName).toBe(TAG);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ import {
|
|||||||
isOffsetAtEnd,
|
isOffsetAtEnd,
|
||||||
} from "./Element.js";
|
} from "./Element.js";
|
||||||
import {
|
import {
|
||||||
isInline,
|
isTextSpan,
|
||||||
isLikeInline,
|
isLikeTextSpan,
|
||||||
getInline,
|
getTextSpan,
|
||||||
getInlinesFrom,
|
getTextSpansFrom,
|
||||||
createInline,
|
createTextSpan,
|
||||||
createEmptyInline,
|
createEmptyTextSpan,
|
||||||
isInlineEnd,
|
isTextSpanEnd,
|
||||||
splitInline,
|
splitTextSpan,
|
||||||
} from "./Inline.js";
|
} from "./TextSpan.js";
|
||||||
import { createLineBreak, isLineBreak } from "./LineBreak.js";
|
import { createLineBreak, isLineBreak } from "./LineBreak.js";
|
||||||
import { setStyles } from "./Style.js";
|
import { setStyles } from "./Style.js";
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export const STYLES = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* FIXME: This is a fix for Chrome that removes the
|
* FIXME: This is a fix for Chrome that removes the
|
||||||
* current inline when the last character is deleted
|
* current text span when the last character is deleted
|
||||||
* in `insertCompositionText`.
|
* in `insertCompositionText`.
|
||||||
*
|
*
|
||||||
* @param {*} node
|
* @param {*} node
|
||||||
@@ -60,7 +60,7 @@ export function fixParagraph(node) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const br = createLineBreak();
|
const br = createLineBreak();
|
||||||
node.replaceChildren(createInline(br));
|
node.replaceChildren(createTextSpan(br));
|
||||||
return br;
|
return br;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ export function fixParagraph(node) {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isLikeParagraph(element) {
|
export function isLikeParagraph(element) {
|
||||||
return !isLikeInline(element);
|
return !isLikeTextSpan(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,9 +85,9 @@ export function isLikeParagraph(element) {
|
|||||||
*/
|
*/
|
||||||
export function isEmptyParagraph(element) {
|
export function isEmptyParagraph(element) {
|
||||||
if (!isParagraph(element)) throw new TypeError("Invalid paragraph");
|
if (!isParagraph(element)) throw new TypeError("Invalid paragraph");
|
||||||
const inline = element.firstChild;
|
const textSpan = element.firstChild;
|
||||||
if (!isInline(inline)) throw new TypeError("Invalid inline");
|
if (!isTextSpan(textSpan)) throw new TypeError("Invalid text span");
|
||||||
return isLineBreak(inline.firstChild);
|
return isLineBreak(textSpan.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,20 +106,20 @@ export function isParagraph(node) {
|
|||||||
/**
|
/**
|
||||||
* Creates a new paragraph.
|
* Creates a new paragraph.
|
||||||
*
|
*
|
||||||
* @param {Array<HTMLDivElement>} inlines
|
* @param {Array<HTMLDivElement>} textSpans
|
||||||
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
||||||
* @param {Object.<string, *>} [attrs]
|
* @param {Object.<string, *>} [attrs]
|
||||||
* @returns {HTMLDivElement}
|
* @returns {HTMLDivElement}
|
||||||
*/
|
*/
|
||||||
export function createParagraph(inlines, styles, attrs) {
|
export function createParagraph(textSpans, styles, attrs) {
|
||||||
if (inlines && (!Array.isArray(inlines) || !inlines.every(isInline)))
|
if (textSpans && (!Array.isArray(textSpans) || !textSpans.every(isTextSpan)))
|
||||||
throw new TypeError("Invalid paragraph children");
|
throw new TypeError("Invalid paragraph children");
|
||||||
return createElement(TAG, {
|
return createElement(TAG, {
|
||||||
attributes: { id: createRandomId(), ...attrs },
|
attributes: { id: createRandomId(), ...attrs },
|
||||||
data: { itype: TYPE },
|
data: { itype: TYPE },
|
||||||
styles: styles,
|
styles: styles,
|
||||||
allowedStyles: STYLES,
|
allowedStyles: STYLES,
|
||||||
children: inlines,
|
children: textSpans,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ export function createParagraph(inlines, styles, attrs) {
|
|||||||
* @returns {HTMLDivElement}
|
* @returns {HTMLDivElement}
|
||||||
*/
|
*/
|
||||||
export function createEmptyParagraph(styles) {
|
export function createEmptyParagraph(styles) {
|
||||||
return createParagraph([createEmptyInline(styles)], styles);
|
return createParagraph([createEmptyTextSpan(styles)], styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,11 +177,11 @@ export function getParagraph(node) {
|
|||||||
export function isParagraphStart(node, offset) {
|
export function isParagraphStart(node, offset) {
|
||||||
const paragraph = getParagraph(node);
|
const paragraph = getParagraph(node);
|
||||||
if (!paragraph) throw new Error("Can't find the paragraph");
|
if (!paragraph) throw new Error("Can't find the paragraph");
|
||||||
const inline = getInline(node);
|
const textSpan = getTextSpan(node);
|
||||||
if (!inline) throw new Error("Can't find the inline");
|
if (!textSpan) throw new Error("Can't find the text span");
|
||||||
return (
|
return (
|
||||||
paragraph.firstElementChild === inline &&
|
paragraph.firstElementChild === textSpan &&
|
||||||
isOffsetAtStart(inline.firstChild, offset)
|
isOffsetAtStart(textSpan.firstChild, offset)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,11 +196,11 @@ export function isParagraphStart(node, offset) {
|
|||||||
export function isParagraphEnd(node, offset) {
|
export function isParagraphEnd(node, offset) {
|
||||||
const paragraph = getParagraph(node);
|
const paragraph = getParagraph(node);
|
||||||
if (!paragraph) throw new Error("Cannot find the paragraph");
|
if (!paragraph) throw new Error("Cannot find the paragraph");
|
||||||
const inline = getInline(node);
|
const textSpan = getTextSpan(node);
|
||||||
if (!inline) throw new Error("Cannot find the inline");
|
if (!textSpan) throw new Error("Cannot find the text span");
|
||||||
return (
|
return (
|
||||||
paragraph.lastElementChild === inline &&
|
paragraph.lastElementChild === textSpan &&
|
||||||
isOffsetAtEnd(inline.firstChild, offset)
|
isOffsetAtEnd(textSpan.firstChild, offset)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,17 +208,17 @@ export function isParagraphEnd(node, offset) {
|
|||||||
* Splits a paragraph.
|
* Splits a paragraph.
|
||||||
*
|
*
|
||||||
* @param {HTMLDivElement} paragraph
|
* @param {HTMLDivElement} paragraph
|
||||||
* @param {HTMLSpanElement} inline
|
* @param {HTMLSpanElement} textSpan
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
export function splitParagraph(paragraph, inline, offset) {
|
export function splitParagraph(paragraph, textSpan, offset) {
|
||||||
const style = paragraph.style;
|
const style = paragraph.style;
|
||||||
if (isInlineEnd(inline, offset)) {
|
if (isTextSpanEnd(textSpan, offset)) {
|
||||||
const newParagraph = createParagraph(getInlinesFrom(inline), style);
|
const newParagraph = createParagraph(getTextSpansFrom(textSpan), style);
|
||||||
return newParagraph;
|
return newParagraph;
|
||||||
}
|
}
|
||||||
const newInline = splitInline(inline, offset);
|
const newTextSpan = splitTextSpan(textSpan, offset);
|
||||||
const newParagraph = createParagraph([newInline], style);
|
const newParagraph = createParagraph([newTextSpan], style);
|
||||||
return newParagraph;
|
return newParagraph;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,11 +231,11 @@ export function splitParagraph(paragraph, inline, offset) {
|
|||||||
export function splitParagraphAtNode(paragraph, startIndex) {
|
export function splitParagraphAtNode(paragraph, startIndex) {
|
||||||
const style = paragraph.style;
|
const style = paragraph.style;
|
||||||
const newParagraph = createParagraph(null, style);
|
const newParagraph = createParagraph(null, style);
|
||||||
const newInlines = [];
|
const newTextSpans = [];
|
||||||
for (let index = startIndex; index < paragraph.children.length; index++) {
|
for (let index = startIndex; index < paragraph.children.length; index++) {
|
||||||
newInlines.push(paragraph.children.item(index));
|
newTextSpans.push(paragraph.children.item(index));
|
||||||
}
|
}
|
||||||
newParagraph.append(...newInlines);
|
newParagraph.append(...newTextSpans);
|
||||||
return newParagraph;
|
return newParagraph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
splitParagraphAtNode,
|
splitParagraphAtNode,
|
||||||
isEmptyParagraph,
|
isEmptyParagraph,
|
||||||
} from "./Paragraph.js";
|
} from "./Paragraph.js";
|
||||||
import { createInline, isInline } from "./Inline.js";
|
import { createTextSpan, isTextSpan } from "./TextSpan.js";
|
||||||
|
|
||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
describe("Paragraph", () => {
|
describe("Paragraph", () => {
|
||||||
@@ -28,7 +28,7 @@ describe("Paragraph", () => {
|
|||||||
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(emptyParagraph.nodeName).toBe(TAG);
|
expect(emptyParagraph.nodeName).toBe(TAG);
|
||||||
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
||||||
expect(isInline(emptyParagraph.firstChild)).toBe(true);
|
expect(isTextSpan(emptyParagraph.firstChild)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isParagraph should return true when the passed node is a paragraph", () => {
|
test("isParagraph should return true when the passed node is a paragraph", () => {
|
||||||
@@ -37,7 +37,7 @@ describe("Paragraph", () => {
|
|||||||
expect(isParagraph(document.createElement("h1"))).toBe(false);
|
expect(isParagraph(document.createElement("h1"))).toBe(false);
|
||||||
expect(isParagraph(createEmptyParagraph())).toBe(true);
|
expect(isParagraph(createEmptyParagraph())).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
isParagraph(createParagraph([createInline(new Text("Hello, World!"))])),
|
isParagraph(createParagraph([createTextSpan(new Text("Hello, World!"))])),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ describe("Paragraph", () => {
|
|||||||
|
|
||||||
test("getParagraph should return the closest paragraph of the passed node", () => {
|
test("getParagraph should return the closest paragraph of the passed node", () => {
|
||||||
const text = new Text("Hello, World!");
|
const text = new Text("Hello, World!");
|
||||||
const inline = createInline(text);
|
const textSpan = createTextSpan(text);
|
||||||
const paragraph = createParagraph([inline]);
|
const paragraph = createParagraph([textSpan]);
|
||||||
expect(getParagraph(text)).toBe(paragraph);
|
expect(getParagraph(text)).toBe(paragraph);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ describe("Paragraph", () => {
|
|||||||
|
|
||||||
test("isParagraphStart should return true on a paragraph", () => {
|
test("isParagraphStart should return true on a paragraph", () => {
|
||||||
const paragraph = createParagraph([
|
const paragraph = createParagraph([
|
||||||
createInline(new Text("Hello, World!")),
|
createTextSpan(new Text("Hello, World!")),
|
||||||
]);
|
]);
|
||||||
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true);
|
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -93,15 +93,15 @@ describe("Paragraph", () => {
|
|||||||
|
|
||||||
test("isParagraphEnd should return true on a paragraph", () => {
|
test("isParagraphEnd should return true on a paragraph", () => {
|
||||||
const paragraph = createParagraph([
|
const paragraph = createParagraph([
|
||||||
createInline(new Text("Hello, World!")),
|
createTextSpan(new Text("Hello, World!")),
|
||||||
]);
|
]);
|
||||||
expect(isParagraphEnd(paragraph.firstChild.firstChild, 13)).toBe(true);
|
expect(isParagraphEnd(paragraph.firstChild.firstChild, 13)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("splitParagraph should split a paragraph", () => {
|
test("splitParagraph should split a paragraph", () => {
|
||||||
const inline = createInline(new Text("Hello, World!"));
|
const textSpan = createTextSpan(new Text("Hello, World!"));
|
||||||
const paragraph = createParagraph([inline]);
|
const paragraph = createParagraph([textSpan]);
|
||||||
const newParagraph = splitParagraph(paragraph, inline, 6);
|
const newParagraph = splitParagraph(paragraph, textSpan, 6);
|
||||||
expect(newParagraph).toBeInstanceOf(HTMLDivElement);
|
expect(newParagraph).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(newParagraph.nodeName).toBe(TAG);
|
expect(newParagraph.nodeName).toBe(TAG);
|
||||||
expect(newParagraph.dataset.itype).toBe(TYPE);
|
expect(newParagraph.dataset.itype).toBe(TYPE);
|
||||||
@@ -109,10 +109,10 @@ describe("Paragraph", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("splitParagraphAtNode should split a paragraph at a specified node", () => {
|
test("splitParagraphAtNode should split a paragraph at a specified node", () => {
|
||||||
const helloInline = createInline(new Text("Hello, "));
|
const helloTextSpan = createTextSpan(new Text("Hello, "));
|
||||||
const worldInline = createInline(new Text("World"));
|
const worldTextSpan = createTextSpan(new Text("World"));
|
||||||
const exclInline = createInline(new Text("!"));
|
const exclTextSpan = createTextSpan(new Text("!"));
|
||||||
const paragraph = createParagraph([helloInline, worldInline, exclInline]);
|
const paragraph = createParagraph([helloTextSpan, worldTextSpan, exclTextSpan]);
|
||||||
const newParagraph = splitParagraphAtNode(paragraph, 1);
|
const newParagraph = splitParagraphAtNode(paragraph, 1);
|
||||||
expect(newParagraph).toBeInstanceOf(HTMLDivElement);
|
expect(newParagraph).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(newParagraph.nodeName).toBe(TAG);
|
expect(newParagraph.nodeName).toBe(TAG);
|
||||||
@@ -121,7 +121,7 @@ describe("Paragraph", () => {
|
|||||||
expect(newParagraph.textContent).toBe("World!");
|
expect(newParagraph.textContent).toBe("World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isLikeParagraph should return true if the element it's not an inline element", () => {
|
test("isLikeParagraph should return true if the element it's not an text span element", () => {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
const br = document.createElement("br");
|
const br = document.createElement("br");
|
||||||
@@ -149,23 +149,23 @@ describe("Paragraph", () => {
|
|||||||
paragraph.dataset.itype = "paragraph";
|
paragraph.dataset.itype = "paragraph";
|
||||||
paragraph.appendChild(document.createElement("svg"));
|
paragraph.appendChild(document.createElement("svg"));
|
||||||
isEmptyParagraph(paragraph);
|
isEmptyParagraph(paragraph);
|
||||||
}).toThrowError("Invalid inline");
|
}).toThrowError("Invalid text span");
|
||||||
|
|
||||||
const lineBreak = document.createElement("br");
|
const lineBreak = document.createElement("br");
|
||||||
const emptyInline = document.createElement("span");
|
const emptyTextSpan = document.createElement("span");
|
||||||
emptyInline.dataset.itype = "inline";
|
emptyTextSpan.dataset.itype = "span";
|
||||||
emptyInline.appendChild(lineBreak);
|
emptyTextSpan.appendChild(lineBreak);
|
||||||
const emptyParagraph = document.createElement("div");
|
const emptyParagraph = document.createElement("div");
|
||||||
emptyParagraph.dataset.itype = "paragraph";
|
emptyParagraph.dataset.itype = "paragraph";
|
||||||
emptyParagraph.appendChild(emptyInline);
|
emptyParagraph.appendChild(emptyTextSpan);
|
||||||
expect(isEmptyParagraph(emptyParagraph)).toBe(true);
|
expect(isEmptyParagraph(emptyParagraph)).toBe(true);
|
||||||
|
|
||||||
const nonEmptyInline = document.createElement("span");
|
const nonEmptyTextSpan = document.createElement("span");
|
||||||
nonEmptyInline.dataset.itype = "inline";
|
nonEmptyTextSpan.dataset.itype = "span";
|
||||||
nonEmptyInline.appendChild(new Text("Not empty!"));
|
nonEmptyTextSpan.appendChild(new Text("Not empty!"));
|
||||||
const nonEmptyParagraph = document.createElement("div");
|
const nonEmptyParagraph = document.createElement("div");
|
||||||
nonEmptyParagraph.dataset.itype = "paragraph";
|
nonEmptyParagraph.dataset.itype = "paragraph";
|
||||||
nonEmptyParagraph.appendChild(nonEmptyInline);
|
nonEmptyParagraph.appendChild(nonEmptyTextSpan);
|
||||||
expect(isEmptyParagraph(nonEmptyParagraph)).toBe(false);
|
expect(isEmptyParagraph(nonEmptyParagraph)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Copyright (c) KALEIDOS INC
|
* Copyright (c) KALEIDOS INC
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isInline } from "./Inline.js";
|
import { isTextSpan } from "./TextSpan.js";
|
||||||
import { isLineBreak } from "./LineBreak.js";
|
import { isLineBreak } from "./LineBreak.js";
|
||||||
import { isParagraph } from "./Paragraph.js";
|
import { isParagraph } from "./Paragraph.js";
|
||||||
import { isEditor } from "./Editor.js";
|
import { isEditor } from "./Editor.js";
|
||||||
@@ -58,7 +58,7 @@ export function getTextNodeLength(node) {
|
|||||||
*/
|
*/
|
||||||
export function getClosestTextNode(node) {
|
export function getClosestTextNode(node) {
|
||||||
if (isTextNode(node)) return node;
|
if (isTextNode(node)) return node;
|
||||||
if (isInline(node)) return node.firstChild;
|
if (isTextSpan(node)) return node.firstChild;
|
||||||
if (isParagraph(node)) return node.firstChild.firstChild;
|
if (isParagraph(node)) return node.firstChild.firstChild;
|
||||||
if (isRoot(node)) return node.firstChild.firstChild.firstChild;
|
if (isRoot(node)) return node.firstChild.firstChild.firstChild;
|
||||||
if (isEditor(node)) return node.firstChild.firstChild.firstChild.firstChild;
|
if (isEditor(node)) return node.firstChild.firstChild.firstChild.firstChild;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, test, expect } from "vitest";
|
import { describe, test, expect } from "vitest";
|
||||||
import TextNodeIterator from "./TextNodeIterator.js";
|
import TextNodeIterator from "./TextNodeIterator.js";
|
||||||
import { createInline } from "./Inline.js";
|
import { createTextSpan } from "./TextSpan.js";
|
||||||
import { createParagraph } from "./Paragraph.js";
|
import { createParagraph } from "./Paragraph.js";
|
||||||
import { createRoot } from "./Root.js";
|
import { createRoot } from "./Root.js";
|
||||||
import { createLineBreak } from "./LineBreak.js";
|
import { createLineBreak } from "./LineBreak.js";
|
||||||
@@ -21,16 +21,16 @@ describe("TextNodeIterator", () => {
|
|||||||
test("Create a new TextNodeIterator and iterate only over text nodes", () => {
|
test("Create a new TextNodeIterator and iterate only over text nodes", () => {
|
||||||
const rootNode = createRoot([
|
const rootNode = createRoot([
|
||||||
createParagraph([
|
createParagraph([
|
||||||
createInline(new Text("Hello, ")),
|
createTextSpan(new Text("Hello, ")),
|
||||||
createInline(new Text("World!")),
|
createTextSpan(new Text("World!")),
|
||||||
createInline(new Text("Whatever")),
|
createTextSpan(new Text("Whatever")),
|
||||||
]),
|
]),
|
||||||
createParagraph([createInline(createLineBreak())]),
|
createParagraph([createTextSpan(createLineBreak())]),
|
||||||
createParagraph([
|
createParagraph([
|
||||||
createInline(new Text("This is a ")),
|
createTextSpan(new Text("This is a ")),
|
||||||
createInline(new Text("test")),
|
createTextSpan(new Text("test")),
|
||||||
]),
|
]),
|
||||||
createParagraph([createInline(new Text("Hi!"))]),
|
createParagraph([createTextSpan(new Text("Hi!"))]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const textNodeIterator = new TextNodeIterator(rootNode);
|
const textNodeIterator = new TextNodeIterator(rootNode);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { setStyles, mergeStyles } from "./Style.js";
|
|||||||
import { createRandomId } from "./Element.js";
|
import { createRandomId } from "./Element.js";
|
||||||
|
|
||||||
export const TAG = "SPAN";
|
export const TAG = "SPAN";
|
||||||
export const TYPE = "inline";
|
export const TYPE = "span";
|
||||||
export const QUERY = `[data-itype="${TYPE}"]`;
|
export const QUERY = `[data-itype="${TYPE}"]`;
|
||||||
export const STYLES = [
|
export const STYLES = [
|
||||||
["--typography-ref-id"],
|
["--typography-ref-id"],
|
||||||
@@ -37,12 +37,12 @@ export const STYLES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if passed node is an inline.
|
* Returns true if passed node is a text span.
|
||||||
*
|
*
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isInline(node) {
|
export function isTextSpan(node) {
|
||||||
if (!node) return false;
|
if (!node) return false;
|
||||||
if (!isElement(node, TAG)) return false;
|
if (!isElement(node, TAG)) return false;
|
||||||
if (node.dataset.itype !== TYPE) return false;
|
if (node.dataset.itype !== TYPE) return false;
|
||||||
@@ -50,13 +50,13 @@ export function isInline(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the passed node "behaves" like an
|
* Returns true if the passed node "behaves" like a
|
||||||
* inline.
|
* text span.
|
||||||
*
|
*
|
||||||
* @param {Node} element
|
* @param {Node} element
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isLikeInline(element) {
|
export function isLikeTextSpan(element) {
|
||||||
return element
|
return element
|
||||||
? [
|
? [
|
||||||
"A",
|
"A",
|
||||||
@@ -97,26 +97,26 @@ export function isLikeInline(element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Inline
|
* Creates a new TextSpan
|
||||||
*
|
*
|
||||||
* @param {Text|HTMLBRElement} text
|
* @param {Text|HTMLBRElement} text
|
||||||
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
||||||
* @param {Object.<string, *>} [attrs]
|
* @param {Object.<string, *>} [attrs]
|
||||||
* @returns {HTMLSpanElement}
|
* @returns {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
export function createInline(textOrLineBreak, styles, attrs) {
|
export function createTextSpan(textOrLineBreak, styles, attrs) {
|
||||||
if (
|
if (
|
||||||
!(textOrLineBreak instanceof HTMLBRElement) &&
|
!(textOrLineBreak instanceof HTMLBRElement) &&
|
||||||
!(textOrLineBreak instanceof Text)
|
!(textOrLineBreak instanceof Text)
|
||||||
) {
|
) {
|
||||||
throw new TypeError("Invalid inline child");
|
throw new TypeError("Invalid text span child");
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
textOrLineBreak instanceof Text &&
|
textOrLineBreak instanceof Text &&
|
||||||
textOrLineBreak.nodeValue.length === 0
|
textOrLineBreak.nodeValue.length === 0
|
||||||
) {
|
) {
|
||||||
console.trace("nodeValue", textOrLineBreak.nodeValue);
|
console.trace("nodeValue", textOrLineBreak.nodeValue);
|
||||||
throw new TypeError("Invalid inline child, cannot be an empty text");
|
throw new TypeError("Invalid text span child, cannot be an empty text");
|
||||||
}
|
}
|
||||||
return createElement(TAG, {
|
return createElement(TAG, {
|
||||||
attributes: { id: createRandomId(), ...attrs },
|
attributes: { id: createRandomId(), ...attrs },
|
||||||
@@ -128,34 +128,42 @@ export function createInline(textOrLineBreak, styles, attrs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new inline from an older inline. This only
|
* Creates a new text span from an older text span. This only
|
||||||
* merges styles from the older inline to the new inline.
|
* merges styles from the older text span to the new text span.
|
||||||
*
|
*
|
||||||
* @param {HTMLSpanElement} inline
|
* @param {HTMLSpanElement} textSpan
|
||||||
* @param {Object.<string, *>} textOrLineBreak
|
* @param {Object.<string, *>} textOrLineBreak
|
||||||
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
||||||
* @param {Object.<string, *>} [attrs]
|
* @param {Object.<string, *>} [attrs]
|
||||||
* @returns {HTMLSpanElement}
|
* @returns {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
export function createInlineFrom(inline, textOrLineBreak, styles, attrs) {
|
export function createTextSpanFrom(textSpan, textOrLineBreak, styles, attrs) {
|
||||||
return createInline(
|
return createTextSpan(
|
||||||
textOrLineBreak,
|
textOrLineBreak,
|
||||||
mergeStyles(STYLES, inline.style, styles),
|
mergeStyles(STYLES, textSpan.style, styles),
|
||||||
attrs,
|
attrs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new empty inline.
|
* Creates a new empty text span.
|
||||||
*
|
*
|
||||||
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
||||||
* @returns {HTMLSpanElement}
|
* @returns {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
export function createEmptyInline(styles) {
|
export function createEmptyTextSpan(styles) {
|
||||||
return createInline(createLineBreak(), styles);
|
return createTextSpan(createLineBreak(), styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createVoidInline(styles) {
|
/**
|
||||||
|
* Creates a new text span with an empty text. The difference between
|
||||||
|
* this and the createEmptyTextSpan is that createEmptyTextSpan creates
|
||||||
|
* a text span with a <br> inside.
|
||||||
|
*
|
||||||
|
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
||||||
|
* @returns {HTMLSpanElement}
|
||||||
|
*/
|
||||||
|
export function createVoidTextSpan(styles) {
|
||||||
return createElement(TAG, {
|
return createElement(TAG, {
|
||||||
attributes: { id: createRandomId() },
|
attributes: { id: createRandomId() },
|
||||||
data: { itype: TYPE },
|
data: { itype: TYPE },
|
||||||
@@ -166,116 +174,116 @@ export function createVoidInline(styles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the inline styles.
|
* Sets the text span styles.
|
||||||
*
|
*
|
||||||
* @param {HTMLSpanElement} element
|
* @param {HTMLSpanElement} element
|
||||||
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
||||||
* @returns {HTMLSpanElement}
|
* @returns {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
export function setInlineStyles(element, styles) {
|
export function setTextSpanStyles(element, styles) {
|
||||||
return setStyles(element, STYLES, styles);
|
return setStyles(element, STYLES, styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the closest inline from a node.
|
* Gets the closest text span from a node.
|
||||||
*
|
*
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
* @returns {HTMLElement|null}
|
* @returns {HTMLElement|null}
|
||||||
*/
|
*/
|
||||||
export function getInline(node) {
|
export function getTextSpan(node) {
|
||||||
if (!node) return null; // FIXME: Should throw?
|
if (!node) return null; // FIXME: Should throw?
|
||||||
if (isInline(node)) return node;
|
if (isTextSpan(node)) return node;
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
const inline = node?.parentElement;
|
const textSpan = node?.parentElement;
|
||||||
if (!inline) return null;
|
if (!textSpan) return null;
|
||||||
if (!isInline(inline)) return null;
|
if (!isTextSpan(textSpan)) return null;
|
||||||
return inline;
|
return textSpan;
|
||||||
}
|
}
|
||||||
return node.closest(QUERY);
|
return node.closest(QUERY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if we are at the start offset
|
* Returns true if we are at the start offset
|
||||||
* of an inline.
|
* of a text span.
|
||||||
*
|
*
|
||||||
* NOTE: Only the first inline returns this as true
|
* NOTE: Only the first text span returns this as true
|
||||||
*
|
*
|
||||||
* @param {TextNode|HTMLBRElement} node
|
* @param {TextNode|HTMLBRElement} node
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isInlineStart(node, offset) {
|
export function isTextSpanStart(node, offset) {
|
||||||
const inline = getInline(node);
|
const textSpan = getTextSpan(node);
|
||||||
if (!inline) return false;
|
if (!textSpan) return false;
|
||||||
return isOffsetAtStart(inline, offset);
|
return isOffsetAtStart(textSpan, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if we are at the end offset
|
* Returns true if we are at the end offset
|
||||||
* of an inline.
|
* of a text span.
|
||||||
*
|
*
|
||||||
* @param {TextNode|HTMLBRElement} node
|
* @param {TextNode|HTMLBRElement} node
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isInlineEnd(node, offset) {
|
export function isTextSpanEnd(node, offset) {
|
||||||
const inline = getInline(node);
|
const textSpan = getTextSpan(node);
|
||||||
if (!inline) return false;
|
if (!textSpan) return false;
|
||||||
return isOffsetAtEnd(inline.firstChild, offset);
|
return isOffsetAtEnd(textSpan.firstChild, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits an inline.
|
* Splits a text span.
|
||||||
*
|
*
|
||||||
* @param {HTMLSpanElement} inline
|
* @param {HTMLSpanElement} textSpan
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
*/
|
*/
|
||||||
export function splitInline(inline, offset) {
|
export function splitTextSpan(textSpan, offset) {
|
||||||
const textNode = inline.firstChild;
|
const textNode = textSpan.firstChild;
|
||||||
const style = inline.style;
|
const style = textSpan.style;
|
||||||
const newTextNode = textNode.splitText(offset);
|
const newTextNode = textNode.splitText(offset);
|
||||||
return createInline(newTextNode, style);
|
return createTextSpan(newTextNode, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all the inlines of a paragraph starting at
|
* Returns all the text spans of a paragraph starting at
|
||||||
* the specified inline.
|
* the specified text span.
|
||||||
*
|
*
|
||||||
* @param {HTMLSpanElement} startInline
|
* @param {HTMLSpanElement} startTextSpan
|
||||||
* @returns {Array<HTMLSpanElement>}
|
* @returns {Array<HTMLSpanElement>}
|
||||||
*/
|
*/
|
||||||
export function getInlinesFrom(startInline) {
|
export function getTextSpansFrom(startTextSpan) {
|
||||||
const inlines = [];
|
const textSpans = [];
|
||||||
let currentInline = startInline;
|
let currentTextSpan = startTextSpan;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (currentInline) {
|
while (currentTextSpan) {
|
||||||
if (index > 0) inlines.push(currentInline);
|
if (index > 0) textSpans.push(currentTextSpan);
|
||||||
currentInline = currentInline.nextElementSibling;
|
currentTextSpan = currentTextSpan.nextElementSibling;
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
return inlines;
|
return textSpans;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the length of an inline.
|
* Returns the length of a text span.
|
||||||
*
|
*
|
||||||
* @param {HTMLElement} inline
|
* @param {HTMLElement} textSpan
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
export function getInlineLength(inline) {
|
export function getTextSpanLength(textSpan) {
|
||||||
if (!isInline(inline)) throw new Error("Invalid inline");
|
if (!isTextSpan(textSpan)) throw new Error("Invalid text span");
|
||||||
if (isLineBreak(inline.firstChild)) return 0;
|
if (isLineBreak(textSpan.firstChild)) return 0;
|
||||||
return inline.firstChild.nodeValue.length;
|
return textSpan.firstChild.nodeValue.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges two inlines.
|
* Merges two text spans.
|
||||||
*
|
*
|
||||||
* @param {HTMLSpanElement} a
|
* @param {HTMLSpanElement} a
|
||||||
* @param {HTMLSpanElement} b
|
* @param {HTMLSpanElement} b
|
||||||
* @returns {HTMLSpanElement}
|
* @returns {HTMLSpanElement}
|
||||||
*/
|
*/
|
||||||
export function mergeInlines(a, b) {
|
export function mergeTextSpans(a, b) {
|
||||||
a.append(...b.childNodes);
|
a.append(...b.childNodes);
|
||||||
b.remove();
|
b.remove();
|
||||||
// We need to normalize Text nodes.
|
// We need to normalize Text nodes.
|
||||||
@@ -8,18 +8,18 @@
|
|||||||
|
|
||||||
import { createLineBreak, isLineBreak } from "../content/dom/LineBreak.js";
|
import { createLineBreak, isLineBreak } from "../content/dom/LineBreak.js";
|
||||||
import {
|
import {
|
||||||
createInline,
|
createTextSpan,
|
||||||
createInlineFrom,
|
createTextSpanFrom,
|
||||||
getInline,
|
getTextSpan,
|
||||||
getInlineLength,
|
getTextSpanLength,
|
||||||
isInline,
|
isTextSpan,
|
||||||
isInlineStart,
|
isTextSpanStart,
|
||||||
isInlineEnd,
|
isTextSpanEnd,
|
||||||
setInlineStyles,
|
setTextSpanStyles,
|
||||||
splitInline,
|
splitTextSpan,
|
||||||
createEmptyInline,
|
createEmptyTextSpan,
|
||||||
createVoidInline,
|
createVoidTextSpan,
|
||||||
} from "../content/dom/Inline.js";
|
} from "../content/dom/TextSpan.js";
|
||||||
import {
|
import {
|
||||||
createEmptyParagraph,
|
createEmptyParagraph,
|
||||||
isEmptyParagraph,
|
isEmptyParagraph,
|
||||||
@@ -62,9 +62,9 @@ import SafeGuard from "./SafeGuard.js";
|
|||||||
/**
|
/**
|
||||||
* SelectionController uses the same concepts used by the Selection API but extending it to support
|
* SelectionController uses the same concepts used by the Selection API but extending it to support
|
||||||
* our own internal model based on paragraphs (in drafconst textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
* our own internal model based on paragraphs (in drafconst textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, "))]),
|
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||||
createEmptyParagraph(),
|
createEmptyParagraph(),
|
||||||
createParagraph([createInline(new Text("World!"))]),
|
createParagraph([createTextSpan(new Text("World!"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -88,12 +88,12 @@ import SafeGuard from "./SafeGuard.js";
|
|||||||
HTMLSpanElement
|
HTMLSpanElement
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline"
|
"span"
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
|
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
|
||||||
expect(textEditorMock.root.lastChild.textContent).toBe("World!");
|
expect(textEditorMock.root.lastChild.textContent).toBe("World!");
|
||||||
t.js they were called blocks) and inlines.
|
t.js they were called blocks) and text spans.
|
||||||
*/
|
*/
|
||||||
export class SelectionController extends EventTarget {
|
export class SelectionController extends EventTarget {
|
||||||
/**
|
/**
|
||||||
@@ -228,7 +228,7 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Styles of the current inline.
|
* Styles of the current text span.
|
||||||
*
|
*
|
||||||
* @type {CSSStyleDeclaration}
|
* @type {CSSStyleDeclaration}
|
||||||
*/
|
*/
|
||||||
@@ -266,18 +266,18 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates current styles based on the currently selected inline.
|
* Updates current styles based on the currently selected text span.
|
||||||
*
|
*
|
||||||
* @param {HTMLSpanElement} inline
|
* @param {HTMLSpanElement} textSpan
|
||||||
* @returns {SelectionController}
|
* @returns {SelectionController}
|
||||||
*/
|
*/
|
||||||
#updateCurrentStyle(inline) {
|
#updateCurrentStyle(textSpan) {
|
||||||
this.#applyDefaultStylesToCurrentStyle();
|
this.#applyDefaultStylesToCurrentStyle();
|
||||||
const root = inline.parentElement.parentElement;
|
const root = textSpan.parentElement.parentElement;
|
||||||
this.#applyStylesToCurrentStyle(root);
|
this.#applyStylesToCurrentStyle(root);
|
||||||
const paragraph = inline.parentElement;
|
const paragraph = textSpan.parentElement;
|
||||||
this.#applyStylesToCurrentStyle(paragraph);
|
this.#applyStylesToCurrentStyle(paragraph);
|
||||||
this.#applyStylesToCurrentStyle(inline);
|
this.#applyStylesToCurrentStyle(textSpan);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If focus node changed, we need to retrieve all the
|
// If focus node changed, we need to retrieve all the
|
||||||
// styles of the current inline and dispatch an event
|
// styles of the current text span and dispatch an event
|
||||||
// to notify that the styles have changed.
|
// to notify that the styles have changed.
|
||||||
if (focusNodeChanges) {
|
if (focusNodeChanges) {
|
||||||
this.#notifyStyleChange();
|
this.#notifyStyleChange();
|
||||||
@@ -355,19 +355,19 @@ export class SelectionController extends EventTarget {
|
|||||||
* Notifies that the styles have changed.
|
* Notifies that the styles have changed.
|
||||||
*/
|
*/
|
||||||
#notifyStyleChange() {
|
#notifyStyleChange() {
|
||||||
const inline = this.focusInline;
|
const textSpan = this.focusTextSpan;
|
||||||
if (inline) {
|
if (textSpan) {
|
||||||
this.#updateCurrentStyle(inline);
|
this.#updateCurrentStyle(textSpan);
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("stylechange", {
|
new CustomEvent("stylechange", {
|
||||||
detail: this.#currentStyle,
|
detail: this.#currentStyle,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const firstInline =
|
const firstTextSpan =
|
||||||
this.#textEditor.root?.firstElementChild?.firstElementChild;
|
this.#textEditor.root?.firstElementChild?.firstElementChild;
|
||||||
if (firstInline) {
|
if (firstTextSpan) {
|
||||||
this.#updateCurrentStyle(firstInline);
|
this.#updateCurrentStyle(firstTextSpan);
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("stylechange", {
|
new CustomEvent("stylechange", {
|
||||||
detail: this.#currentStyle,
|
detail: this.#currentStyle,
|
||||||
@@ -766,13 +766,13 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the inline in the focus node
|
* Returns the text span in the focus node
|
||||||
* of the current selection.
|
* of the current selection.
|
||||||
*
|
*
|
||||||
* @type {HTMLElement|null}
|
* @type {HTMLElement|null}
|
||||||
*/
|
*/
|
||||||
get focusInline() {
|
get focusTextSpan() {
|
||||||
return getInline(this.focusNode);
|
return getTextSpan(this.focusNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -786,13 +786,13 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current inline in the anchor
|
* Returns the current text span in the anchor
|
||||||
* node of the current selection.
|
* node of the current selection.
|
||||||
*
|
*
|
||||||
* @type {HTMLElement|null}
|
* @type {HTMLElement|null}
|
||||||
*/
|
*/
|
||||||
get anchorInline() {
|
get anchorTextSpan() {
|
||||||
return getInline(this.anchorNode);
|
return getTextSpan(this.anchorNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -829,14 +829,14 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start inline of the current page.
|
* Start text span of the current page.
|
||||||
*
|
*
|
||||||
* @type {HTMLElement|null}
|
* @type {HTMLElement|null}
|
||||||
*/
|
*/
|
||||||
get startInline() {
|
get startTextSpan() {
|
||||||
const startContainer = this.startContainer;
|
const startContainer = this.startContainer;
|
||||||
if (!startContainer) return null;
|
if (!startContainer) return null;
|
||||||
return getInline(startContainer);
|
return getTextSpan(startContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -876,15 +876,15 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inline element of the `endContainer` of
|
* TextSpan element of the `endContainer` of
|
||||||
* the current range.
|
* the current range.
|
||||||
*
|
*
|
||||||
* @type {HTMLElement|null}
|
* @type {HTMLElement|null}
|
||||||
*/
|
*/
|
||||||
get endInline() {
|
get endTextSpan() {
|
||||||
const endContainer = this.endContainer;
|
const endContainer = this.endContainer;
|
||||||
if (!endContainer) return null;
|
if (!endContainer) return null;
|
||||||
return getInline(endContainer);
|
return getTextSpan(endContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -919,21 +919,21 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is true if the current focus node is a inline.
|
* Is true if the current focus node is a text span.
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isInlineFocus() {
|
get isTextSpanFocus() {
|
||||||
return isInline(this.focusNode);
|
return isTextSpan(this.focusNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is true if the current anchor node is a inline.
|
* Is true if the current anchor node is a text span.
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isInlineAnchor() {
|
get isTextSpanAnchor() {
|
||||||
return isInline(this.anchorNode);
|
return isTextSpan(this.anchorNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -962,7 +962,7 @@ export class SelectionController extends EventTarget {
|
|||||||
get isLineBreakFocus() {
|
get isLineBreakFocus() {
|
||||||
return (
|
return (
|
||||||
isLineBreak(this.focusNode) ||
|
isLineBreak(this.focusNode) ||
|
||||||
(isInline(this.focusNode) && isLineBreak(this.focusNode.firstChild))
|
(isTextSpan(this.focusNode) && isLineBreak(this.focusNode.firstChild))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,35 +996,35 @@ export class SelectionController extends EventTarget {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that we have selected multiple
|
* Indicates that we have selected multiple
|
||||||
* inline elements.
|
* text span elements.
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isMultiInline() {
|
get isMultiTextSpan() {
|
||||||
return this.isMulti && this.focusInline !== this.anchorInline;
|
return this.isMulti && this.focusTextSpan !== this.anchorTextSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the caret (only the caret)
|
* Indicates that the caret (only the caret)
|
||||||
* is at the start of an inline.
|
* is at the start of an text span.
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isInlineStart() {
|
get isTextSpanStart() {
|
||||||
if (!this.isCollapsed) return false;
|
if (!this.isCollapsed) return false;
|
||||||
return isInlineStart(this.focusNode, this.focusOffset);
|
return isTextSpanStart(this.focusNode, this.focusOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that the caret (only the caret)
|
* Indicates that the caret (only the caret)
|
||||||
* is at the end of an inline. This value doesn't
|
* is at the end of an text span. This value doesn't
|
||||||
* matter when dealing with selections.
|
* matter when dealing with selections.
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
get isInlineEnd() {
|
get isTextSpanEnd() {
|
||||||
if (!this.isCollapsed) return false;
|
if (!this.isCollapsed) return false;
|
||||||
return isInlineEnd(this.focusNode, this.focusOffset);
|
return isTextSpanEnd(this.focusNode, this.focusOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1055,18 +1055,18 @@ export class SelectionController extends EventTarget {
|
|||||||
insertPaste(fragment) {
|
insertPaste(fragment) {
|
||||||
if (
|
if (
|
||||||
fragment.children.length === 1 &&
|
fragment.children.length === 1 &&
|
||||||
fragment.firstElementChild?.dataset?.inline === "force"
|
fragment.firstElementChild?.dataset?.textSpan === "force"
|
||||||
) {
|
) {
|
||||||
const collapseNode = fragment.firstElementChild.firstChild;
|
const collapseNode = fragment.firstElementChild.firstChild;
|
||||||
if (this.isInlineStart) {
|
if (this.isTextSpanStart) {
|
||||||
this.focusInline.before(...fragment.firstElementChild.children);
|
this.focusTextSpan.before(...fragment.firstElementChild.children);
|
||||||
} else if (this.isInlineEnd) {
|
} else if (this.isTextSpanEnd) {
|
||||||
this.focusInline.after(...fragment.firstElementChild.children);
|
this.focusTextSpan.after(...fragment.firstElementChild.children);
|
||||||
} else {
|
} else {
|
||||||
const newInline = splitInline(this.focusInline, this.focusOffset);
|
const newTextSpan = splitTextSpan(this.focusTextSpan, this.focusOffset);
|
||||||
this.focusInline.after(
|
this.focusTextSpan.after(
|
||||||
...fragment.firstElementChild.children,
|
...fragment.firstElementChild.children,
|
||||||
newInline,
|
newTextSpan,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.collapse(collapseNode, collapseNode.nodeValue.length);
|
return this.collapse(collapseNode, collapseNode.nodeValue.length);
|
||||||
@@ -1085,7 +1085,7 @@ export class SelectionController extends EventTarget {
|
|||||||
} else {
|
} else {
|
||||||
const newParagraph = splitParagraph(
|
const newParagraph = splitParagraph(
|
||||||
this.focusParagraph,
|
this.focusParagraph,
|
||||||
this.focusInline,
|
this.focusTextSpan,
|
||||||
this.focusOffset,
|
this.focusOffset,
|
||||||
);
|
);
|
||||||
this.focusParagraph.after(fragment, newParagraph);
|
this.focusParagraph.after(fragment, newParagraph);
|
||||||
@@ -1110,7 +1110,7 @@ export class SelectionController extends EventTarget {
|
|||||||
*/
|
*/
|
||||||
replaceLineBreak(text) {
|
replaceLineBreak(text) {
|
||||||
const newText = new Text(text);
|
const newText = new Text(text);
|
||||||
this.focusInline.replaceChildren(newText);
|
this.focusTextSpan.replaceChildren(newText);
|
||||||
this.collapse(newText, text.length);
|
this.collapse(newText, text.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1131,23 +1131,23 @@ export class SelectionController extends EventTarget {
|
|||||||
|
|
||||||
const paragraph = this.focusParagraph;
|
const paragraph = this.focusParagraph;
|
||||||
if (!paragraph) throw new Error("Cannot find paragraph");
|
if (!paragraph) throw new Error("Cannot find paragraph");
|
||||||
const inline = this.focusInline;
|
const textSpan = this.focusTextSpan;
|
||||||
if (!inline) throw new Error("Cannot find inline");
|
if (!textSpan) throw new Error("Cannot find text span");
|
||||||
|
|
||||||
const nextTextNode = this.#textNodeIterator.nextNode();
|
const nextTextNode = this.#textNodeIterator.nextNode();
|
||||||
if (this.focusNode.nodeValue === "") {
|
if (this.focusNode.nodeValue === "") {
|
||||||
this.focusNode.remove();
|
this.focusNode.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paragraph.childNodes.length === 1 && inline.childNodes.length === 0) {
|
if (paragraph.childNodes.length === 1 && textSpan.childNodes.length === 0) {
|
||||||
const lineBreak = createLineBreak();
|
const lineBreak = createLineBreak();
|
||||||
inline.appendChild(lineBreak);
|
textSpan.appendChild(lineBreak);
|
||||||
return this.collapse(lineBreak, 0);
|
return this.collapse(lineBreak, 0);
|
||||||
} else if (
|
} else if (
|
||||||
paragraph.childNodes.length > 1 &&
|
paragraph.childNodes.length > 1 &&
|
||||||
inline.childNodes.length === 0
|
textSpan.childNodes.length === 0
|
||||||
) {
|
) {
|
||||||
inline.remove();
|
textSpan.remove();
|
||||||
return this.collapse(nextTextNode, 0);
|
return this.collapse(nextTextNode, 0);
|
||||||
}
|
}
|
||||||
return this.collapse(this.focusNode, this.focusOffset);
|
return this.collapse(this.focusNode, this.focusOffset);
|
||||||
@@ -1177,23 +1177,23 @@ export class SelectionController extends EventTarget {
|
|||||||
|
|
||||||
const paragraph = this.focusParagraph;
|
const paragraph = this.focusParagraph;
|
||||||
if (!paragraph) throw new Error("Cannot find paragraph");
|
if (!paragraph) throw new Error("Cannot find paragraph");
|
||||||
const inline = this.focusInline;
|
const textSpan = this.focusTextSpan;
|
||||||
if (!inline) throw new Error("Cannot find inline");
|
if (!textSpan) throw new Error("Cannot find text span");
|
||||||
|
|
||||||
const previousTextNode = this.#textNodeIterator.previousNode();
|
const previousTextNode = this.#textNodeIterator.previousNode();
|
||||||
if (this.focusNode.nodeValue === "") {
|
if (this.focusNode.nodeValue === "") {
|
||||||
this.focusNode.remove();
|
this.focusNode.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paragraph.children.length === 1 && inline.childNodes.length === 0) {
|
if (paragraph.children.length === 1 && textSpan.childNodes.length === 0) {
|
||||||
const lineBreak = createLineBreak();
|
const lineBreak = createLineBreak();
|
||||||
inline.appendChild(lineBreak);
|
textSpan.appendChild(lineBreak);
|
||||||
return this.collapse(lineBreak, 0);
|
return this.collapse(lineBreak, 0);
|
||||||
} else if (
|
} else if (
|
||||||
paragraph.children.length > 1 &&
|
paragraph.children.length > 1 &&
|
||||||
inline.childNodes.length === 0
|
textSpan.childNodes.length === 0
|
||||||
) {
|
) {
|
||||||
inline.remove();
|
textSpan.remove();
|
||||||
return this.collapse(
|
return this.collapse(
|
||||||
previousTextNode,
|
previousTextNode,
|
||||||
getTextNodeLength(previousTextNode),
|
getTextNodeLength(previousTextNode),
|
||||||
@@ -1214,7 +1214,7 @@ export class SelectionController extends EventTarget {
|
|||||||
this.focusOffset,
|
this.focusOffset,
|
||||||
newText,
|
newText,
|
||||||
);
|
);
|
||||||
this.#mutations.update(this.focusInline);
|
this.#mutations.update(this.focusTextSpan);
|
||||||
return this.collapse(this.focusNode, this.focusOffset + newText.length);
|
return this.collapse(this.focusNode, this.focusOffset + newText.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1259,36 +1259,36 @@ export class SelectionController extends EventTarget {
|
|||||||
this.focusNode.replaceWith(new Text(newText));
|
this.focusNode.replaceWith(new Text(newText));
|
||||||
} else if (this.isRootFocus) {
|
} else if (this.isRootFocus) {
|
||||||
const newTextNode = new Text(newText);
|
const newTextNode = new Text(newText);
|
||||||
const newInline = createInline(newTextNode, this.#currentStyle);
|
const newTextSpan = createTextSpan(newTextNode, this.#currentStyle);
|
||||||
const newParagraph = createParagraph([newInline], this.#currentStyle);
|
const newParagraph = createParagraph([newTextSpan], this.#currentStyle);
|
||||||
this.focusNode.replaceChildren(newParagraph);
|
this.focusNode.replaceChildren(newParagraph);
|
||||||
return this.collapse(newTextNode, newText.length + 1);
|
return this.collapse(newTextNode, newText.length + 1);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown node type");
|
throw new Error("Unknown node type");
|
||||||
}
|
}
|
||||||
this.#mutations.update(this.focusInline);
|
this.#mutations.update(this.focusTextSpan);
|
||||||
return this.collapse(this.focusNode, startOffset + newText.length);
|
return this.collapse(this.focusNode, startOffset + newText.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the selected inlines with new text.
|
* Replaces the selected text spans with new text.
|
||||||
*
|
*
|
||||||
* @param {string} newText
|
* @param {string} newText
|
||||||
*/
|
*/
|
||||||
replaceInlines(newText) {
|
replaceTextSpans(newText) {
|
||||||
const currentParagraph = this.focusParagraph;
|
const currentParagraph = this.focusParagraph;
|
||||||
|
|
||||||
// This is the special (and fast) case where we're
|
// This is the special (and fast) case where we're
|
||||||
// removing everything inside a paragraph.
|
// removing everything inside a paragraph.
|
||||||
if (
|
if (
|
||||||
this.startInline === currentParagraph.firstChild &&
|
this.startTextSpan === currentParagraph.firstChild &&
|
||||||
this.startOffset === 0 &&
|
this.startOffset === 0 &&
|
||||||
this.endInline === currentParagraph.lastChild &&
|
this.endTextSpan === currentParagraph.lastChild &&
|
||||||
this.endOffset === currentParagraph.lastChild.textContent.length
|
this.endOffset === currentParagraph.lastChild.textContent.length
|
||||||
) {
|
) {
|
||||||
const newTextNode = new Text(newText);
|
const newTextNode = new Text(newText);
|
||||||
currentParagraph.replaceChildren(
|
currentParagraph.replaceChildren(
|
||||||
createInline(newTextNode, this.anchorInline.style),
|
createTextSpan(newTextNode, this.anchorTextSpan.style),
|
||||||
);
|
);
|
||||||
return this.collapse(newTextNode, newTextNode.nodeValue.length);
|
return this.collapse(newTextNode, newTextNode.nodeValue.length);
|
||||||
}
|
}
|
||||||
@@ -1304,10 +1304,10 @@ export class SelectionController extends EventTarget {
|
|||||||
);
|
);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// FIXME: I'm not sure if we should merge inlines when they share the same styles.
|
// FIXME: I'm not sure if we should merge text spans when they share the same styles.
|
||||||
// For example: if we have > 2 inlines and the start inline and the end inline
|
// For example: if we have > 2 text spans and the start text span and the end text span
|
||||||
// share the same styles, maybe we should merge them?
|
// share the same styles, maybe we should merge them?
|
||||||
// mergeInlines(startInline, endInline);
|
// mergeTextSpans(startTextSpan, endTextSpan);
|
||||||
return this.collapse(this.focusNode, this.focusOffset + newText.length);
|
return this.collapse(this.focusNode, this.focusOffset + newText.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1368,7 +1368,7 @@ export class SelectionController extends EventTarget {
|
|||||||
const currentParagraph = this.focusParagraph;
|
const currentParagraph = this.focusParagraph;
|
||||||
const newParagraph = splitParagraph(
|
const newParagraph = splitParagraph(
|
||||||
this.focusParagraph,
|
this.focusParagraph,
|
||||||
this.focusInline,
|
this.focusTextSpan,
|
||||||
this.#focusOffset,
|
this.#focusOffset,
|
||||||
);
|
);
|
||||||
this.focusParagraph.after(newParagraph);
|
this.focusParagraph.after(newParagraph);
|
||||||
@@ -1395,13 +1395,13 @@ export class SelectionController extends EventTarget {
|
|||||||
*/
|
*/
|
||||||
replaceWithParagraph() {
|
replaceWithParagraph() {
|
||||||
const currentParagraph = this.focusParagraph;
|
const currentParagraph = this.focusParagraph;
|
||||||
const currentInline = this.focusInline;
|
const currentTextSpan = this.focusTextSpan;
|
||||||
|
|
||||||
this.removeSelected();
|
this.removeSelected();
|
||||||
|
|
||||||
const newParagraph = splitParagraph(
|
const newParagraph = splitParagraph(
|
||||||
currentParagraph,
|
currentParagraph,
|
||||||
currentInline,
|
currentTextSpan,
|
||||||
this.focusOffset,
|
this.focusOffset,
|
||||||
);
|
);
|
||||||
currentParagraph.after(newParagraph);
|
currentParagraph.after(newParagraph);
|
||||||
@@ -1422,15 +1422,15 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
const paragraphToBeRemoved = this.focusParagraph;
|
const paragraphToBeRemoved = this.focusParagraph;
|
||||||
paragraphToBeRemoved.remove();
|
paragraphToBeRemoved.remove();
|
||||||
const previousInline =
|
const previousTextSpan =
|
||||||
previousParagraph.children.length > 1
|
previousParagraph.children.length > 1
|
||||||
? previousParagraph.lastElementChild
|
? previousParagraph.lastElementChild
|
||||||
: previousParagraph.firstChild;
|
: previousParagraph.firstChild;
|
||||||
const previousOffset = isLineBreak(previousInline.firstChild)
|
const previousOffset = isLineBreak(previousTextSpan.firstChild)
|
||||||
? 0
|
? 0
|
||||||
: previousInline.firstChild.nodeValue.length;
|
: previousTextSpan.firstChild.nodeValue.length;
|
||||||
this.#mutations.remove(paragraphToBeRemoved);
|
this.#mutations.remove(paragraphToBeRemoved);
|
||||||
return this.collapse(previousInline.firstChild, previousOffset);
|
return this.collapse(previousTextSpan.firstChild, previousOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1442,18 +1442,18 @@ export class SelectionController extends EventTarget {
|
|||||||
if (!previousParagraph) {
|
if (!previousParagraph) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let previousInline = previousParagraph.lastChild;
|
let previousTextSpan = previousParagraph.lastChild;
|
||||||
const previousOffset = getInlineLength(previousInline);
|
const previousOffset = getTextSpanLength(previousTextSpan);
|
||||||
if (isEmptyParagraph(previousParagraph)) {
|
if (isEmptyParagraph(previousParagraph)) {
|
||||||
previousParagraph.replaceChildren(...currentParagraph.children);
|
previousParagraph.replaceChildren(...currentParagraph.children);
|
||||||
previousInline = previousParagraph.firstChild;
|
previousTextSpan = previousParagraph.firstChild;
|
||||||
currentParagraph.remove();
|
currentParagraph.remove();
|
||||||
} else {
|
} else {
|
||||||
mergeParagraphs(previousParagraph, currentParagraph);
|
mergeParagraphs(previousParagraph, currentParagraph);
|
||||||
}
|
}
|
||||||
this.#mutations.remove(currentParagraph);
|
this.#mutations.remove(currentParagraph);
|
||||||
this.#mutations.update(previousParagraph);
|
this.#mutations.update(previousParagraph);
|
||||||
return this.collapse(previousInline.firstChild, previousOffset);
|
return this.collapse(previousTextSpan.firstChild, previousOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1482,24 +1482,24 @@ export class SelectionController extends EventTarget {
|
|||||||
}
|
}
|
||||||
const paragraphToBeRemoved = this.focusParagraph;
|
const paragraphToBeRemoved = this.focusParagraph;
|
||||||
paragraphToBeRemoved.remove();
|
paragraphToBeRemoved.remove();
|
||||||
const nextInline = nextParagraph.firstChild;
|
const nextTextSpan = nextParagraph.firstChild;
|
||||||
const nextOffset = this.focusOffset;
|
const nextOffset = this.focusOffset;
|
||||||
this.#mutations.remove(paragraphToBeRemoved);
|
this.#mutations.remove(paragraphToBeRemoved);
|
||||||
return this.collapse(nextInline.firstChild, nextOffset);
|
return this.collapse(nextTextSpan.firstChild, nextOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans up all the affected paragraphs.
|
* Cleans up all the affected paragraphs.
|
||||||
*
|
*
|
||||||
* @param {Set<HTMLDivElement>} affectedParagraphs
|
* @param {Set<HTMLDivElement>} affectedParagraphs
|
||||||
* @param {Set<HTMLSpanElement>} affectedInlines
|
* @param {Set<HTMLSpanElement>} affectedTextSpans
|
||||||
*/
|
*/
|
||||||
cleanUp(affectedParagraphs, affectedInlines) {
|
cleanUp(affectedParagraphs, affectedTextSpans) {
|
||||||
// Remove empty inlines
|
// Remove empty text spans
|
||||||
for (const inline of affectedInlines) {
|
for (const textSpan of affectedTextSpans) {
|
||||||
if (inline.textContent === "") {
|
if (textSpan.textContent === "") {
|
||||||
inline.remove();
|
textSpan.remove();
|
||||||
this.#mutations.remove(inline);
|
this.#mutations.remove(textSpan);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1520,7 +1520,7 @@ export class SelectionController extends EventTarget {
|
|||||||
removeSelected(options) {
|
removeSelected(options) {
|
||||||
if (this.isCollapsed) return;
|
if (this.isCollapsed) return;
|
||||||
|
|
||||||
const affectedInlines = new Set();
|
const affectedTextSpans = new Set();
|
||||||
const affectedParagraphs = new Set();
|
const affectedParagraphs = new Set();
|
||||||
|
|
||||||
const startNode = getClosestTextNode(this.#range.startContainer);
|
const startNode = getClosestTextNode(this.#range.startContainer);
|
||||||
@@ -1541,9 +1541,9 @@ export class SelectionController extends EventTarget {
|
|||||||
this.#textNodeIterator.currentNode = startNode;
|
this.#textNodeIterator.currentNode = startNode;
|
||||||
nextNode = this.#textNodeIterator.nextNode();
|
nextNode = this.#textNodeIterator.nextNode();
|
||||||
|
|
||||||
const inline = getInline(startNode);
|
const textSpan = getTextSpan(startNode);
|
||||||
const paragraph = getParagraph(startNode);
|
const paragraph = getParagraph(startNode);
|
||||||
affectedInlines.add(inline);
|
affectedTextSpans.add(textSpan);
|
||||||
affectedParagraphs.add(paragraph);
|
affectedParagraphs.add(paragraph);
|
||||||
|
|
||||||
const newNodeValue = removeSlice(
|
const newNodeValue = removeSlice(
|
||||||
@@ -1553,7 +1553,7 @@ export class SelectionController extends EventTarget {
|
|||||||
);
|
);
|
||||||
if (newNodeValue === "") {
|
if (newNodeValue === "") {
|
||||||
const lineBreak = createLineBreak();
|
const lineBreak = createLineBreak();
|
||||||
inline.replaceChildren(lineBreak);
|
textSpan.replaceChildren(lineBreak);
|
||||||
return this.collapse(lineBreak, 0);
|
return this.collapse(lineBreak, 0);
|
||||||
}
|
}
|
||||||
startNode.nodeValue = newNodeValue;
|
startNode.nodeValue = newNodeValue;
|
||||||
@@ -1567,9 +1567,9 @@ export class SelectionController extends EventTarget {
|
|||||||
// Select initial node.
|
// Select initial node.
|
||||||
this.#textNodeIterator.currentNode = startNode;
|
this.#textNodeIterator.currentNode = startNode;
|
||||||
|
|
||||||
const startInline = getInline(startNode);
|
const startTextSpan = getTextSpan(startNode);
|
||||||
const startParagraph = getParagraph(startNode);
|
const startParagraph = getParagraph(startNode);
|
||||||
const endInline = getInline(endNode);
|
const endTextSpan = getTextSpan(endNode);
|
||||||
const endParagraph = getParagraph(endNode);
|
const endParagraph = getParagraph(endNode);
|
||||||
|
|
||||||
SafeGuard.start();
|
SafeGuard.start();
|
||||||
@@ -1578,11 +1578,11 @@ export class SelectionController extends EventTarget {
|
|||||||
|
|
||||||
const { currentNode } = this.#textNodeIterator;
|
const { currentNode } = this.#textNodeIterator;
|
||||||
|
|
||||||
// We retrieve the inline and paragraph of the
|
// We retrieve the textSpan and paragraph of the
|
||||||
// current node.
|
// current node.
|
||||||
const inline = getInline(currentNode);
|
const textSpan = getTextSpan(currentNode);
|
||||||
const paragraph = getParagraph(currentNode);
|
const paragraph = getParagraph(currentNode);
|
||||||
affectedInlines.add(inline);
|
affectedTextSpans.add(textSpan);
|
||||||
affectedParagraphs.add(paragraph);
|
affectedParagraphs.add(paragraph);
|
||||||
|
|
||||||
let shouldRemoveNodeCompletely = false;
|
let shouldRemoveNodeCompletely = false;
|
||||||
@@ -1619,8 +1619,8 @@ export class SelectionController extends EventTarget {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inline.childNodes.length === 0) {
|
if (textSpan.childNodes.length === 0) {
|
||||||
inline.remove();
|
textSpan.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paragraph !== startParagraph && paragraph.children.length === 0) {
|
if (paragraph !== startParagraph && paragraph.children.length === 0) {
|
||||||
@@ -1636,44 +1636,44 @@ export class SelectionController extends EventTarget {
|
|||||||
if (startParagraph !== endParagraph) {
|
if (startParagraph !== endParagraph) {
|
||||||
const mergedParagraph = mergeParagraphs(startParagraph, endParagraph);
|
const mergedParagraph = mergeParagraphs(startParagraph, endParagraph);
|
||||||
if (mergedParagraph.children.length === 0) {
|
if (mergedParagraph.children.length === 0) {
|
||||||
const newEmptyInline = createEmptyInline(this.#currentStyle);
|
const newEmptyTextSpan = createEmptyTextSpan(this.#currentStyle);
|
||||||
mergedParagraph.appendChild(newEmptyInline);
|
mergedParagraph.appendChild(newEmptyTextSpan);
|
||||||
return this.collapse(newEmptyInline.firstChild, 0);
|
return this.collapse(newEmptyTextSpan.firstChild, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
startInline.childNodes.length === 0 &&
|
startTextSpan.childNodes.length === 0 &&
|
||||||
endInline.childNodes.length > 0
|
endTextSpan.childNodes.length > 0
|
||||||
) {
|
) {
|
||||||
startInline.remove();
|
startTextSpan.remove();
|
||||||
return this.collapse(endNode, 0);
|
return this.collapse(endNode, 0);
|
||||||
} else if (
|
} else if (
|
||||||
startInline.childNodes.length > 0 &&
|
startTextSpan.childNodes.length > 0 &&
|
||||||
endInline.childNodes.length === 0
|
endTextSpan.childNodes.length === 0
|
||||||
) {
|
) {
|
||||||
endInline.remove();
|
endTextSpan.remove();
|
||||||
return this.collapse(startNode, startOffset);
|
return this.collapse(startNode, startOffset);
|
||||||
} else if (
|
} else if (
|
||||||
startInline.childNodes.length === 0 &&
|
startTextSpan.childNodes.length === 0 &&
|
||||||
endInline.childNodes.length === 0
|
endTextSpan.childNodes.length === 0
|
||||||
) {
|
) {
|
||||||
const previousInline = startInline.previousElementSibling;
|
const previousTextSpan = startTextSpan.previousElementSibling;
|
||||||
const nextInline = endInline.nextElementSibling;
|
const nextTextSpan = endTextSpan.nextElementSibling;
|
||||||
startInline.remove();
|
startTextSpan.remove();
|
||||||
endInline.remove();
|
endTextSpan.remove();
|
||||||
if (previousInline) {
|
if (previousTextSpan) {
|
||||||
return this.collapse(
|
return this.collapse(
|
||||||
previousInline.firstChild,
|
previousTextSpan.firstChild,
|
||||||
previousInline.firstChild.nodeValue.length,
|
previousTextSpan.firstChild.nodeValue.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (nextInline) {
|
if (nextTextSpan) {
|
||||||
return this.collapse(nextInline.firstChild, 0);
|
return this.collapse(nextTextSpan.firstChild, 0);
|
||||||
}
|
}
|
||||||
const newEmptyInline = createEmptyInline(this.#currentStyle);
|
const newEmptyTextSpan = createEmptyTextSpan(this.#currentStyle);
|
||||||
startParagraph.appendChild(newEmptyInline);
|
startParagraph.appendChild(newEmptyTextSpan);
|
||||||
return this.collapse(newEmptyInline.firstChild, 0);
|
return this.collapse(newEmptyTextSpan.firstChild, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.collapse(startNode, startOffset);
|
return this.collapse(startNode, startOffset);
|
||||||
@@ -1701,30 +1701,30 @@ export class SelectionController extends EventTarget {
|
|||||||
// The styles are applied to the node completely.
|
// The styles are applied to the node completely.
|
||||||
if (startOffset === 0 && endOffset === endNode.nodeValue.length) {
|
if (startOffset === 0 && endOffset === endNode.nodeValue.length) {
|
||||||
const paragraph = this.startParagraph;
|
const paragraph = this.startParagraph;
|
||||||
const inline = this.startInline;
|
const textSpan = this.startTextSpan;
|
||||||
setParagraphStyles(paragraph, newStyles);
|
setParagraphStyles(paragraph, newStyles);
|
||||||
setInlineStyles(inline, newStyles);
|
setTextSpanStyles(textSpan, newStyles);
|
||||||
|
|
||||||
// The styles are applied to a part of the node.
|
// The styles are applied to a part of the node.
|
||||||
} else if (startOffset !== endOffset) {
|
} else if (startOffset !== endOffset) {
|
||||||
const paragraph = this.startParagraph;
|
const paragraph = this.startParagraph;
|
||||||
setParagraphStyles(paragraph, newStyles);
|
setParagraphStyles(paragraph, newStyles);
|
||||||
const inline = this.startInline;
|
const textSpan = this.startTextSpan;
|
||||||
const midText = startNode.splitText(startOffset);
|
const midText = startNode.splitText(startOffset);
|
||||||
const endText = midText.splitText(endOffset - startOffset);
|
const endText = midText.splitText(endOffset - startOffset);
|
||||||
const midInline = createInlineFrom(inline, midText, newStyles);
|
const midTextSpan = createTextSpanFrom(textSpan, midText, newStyles);
|
||||||
inline.after(midInline);
|
textSpan.after(midTextSpan);
|
||||||
if (endText.length > 0) {
|
if (endText.length > 0) {
|
||||||
const endInline = createInline(endText, inline.style);
|
const endTextSpan = createTextSpan(endText, textSpan.style);
|
||||||
midInline.after(endInline);
|
midTextSpan.after(endTextSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: This is necessary because sometimes
|
// NOTE: This is necessary because sometimes
|
||||||
// inlines are splitted from the beginning
|
// text spans are splitted from the beginning
|
||||||
// to a mid offset and then the starting node
|
// to a mid offset and then the starting node
|
||||||
// remains empty.
|
// remains empty.
|
||||||
if (inline.firstChild.nodeValue === "") {
|
if (textSpan.firstChild.nodeValue === "") {
|
||||||
inline.remove();
|
textSpan.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: This can change focus <-> anchor order.
|
// FIXME: This can change focus <-> anchor order.
|
||||||
@@ -1735,9 +1735,9 @@ export class SelectionController extends EventTarget {
|
|||||||
this.startOffset === this.endOffset &&
|
this.startOffset === this.endOffset &&
|
||||||
this.endOffset === endNode.nodeValue.length
|
this.endOffset === endNode.nodeValue.length
|
||||||
) {
|
) {
|
||||||
const newInline = createVoidInline(newStyles);
|
const newTextSpan = createVoidTextSpan(newStyles);
|
||||||
this.endInline.after(newInline);
|
this.endTextSpan.after(newTextSpan);
|
||||||
this.setSelection(newInline.firstChild, 0, newInline.firstChild, 0);
|
this.setSelection(newTextSpan.firstChild, 0, newTextSpan.firstChild, 0);
|
||||||
}
|
}
|
||||||
// The styles are applied to the paragraph
|
// The styles are applied to the paragraph
|
||||||
else {
|
else {
|
||||||
@@ -1758,17 +1758,17 @@ export class SelectionController extends EventTarget {
|
|||||||
|
|
||||||
const paragraph = getParagraph(this.#textNodeIterator.currentNode);
|
const paragraph = getParagraph(this.#textNodeIterator.currentNode);
|
||||||
setParagraphStyles(paragraph, newStyles);
|
setParagraphStyles(paragraph, newStyles);
|
||||||
const inline = getInline(this.#textNodeIterator.currentNode);
|
const textSpan = getTextSpan(this.#textNodeIterator.currentNode);
|
||||||
// If we're at the start node and offset is greater than 0
|
// If we're at the start node and offset is greater than 0
|
||||||
// then we should split the inline and apply styles to that
|
// then we should split the text span and apply styles to that
|
||||||
// new inline.
|
// new text span.
|
||||||
if (
|
if (
|
||||||
this.#textNodeIterator.currentNode === startNode &&
|
this.#textNodeIterator.currentNode === startNode &&
|
||||||
startOffset > 0
|
startOffset > 0
|
||||||
) {
|
) {
|
||||||
const newInline = splitInline(inline, startOffset);
|
const newTextSpan = splitTextSpan(textSpan, startOffset);
|
||||||
setInlineStyles(newInline, newStyles);
|
setTextSpanStyles(newTextSpan, newStyles);
|
||||||
inline.after(newInline);
|
textSpan.after(newTextSpan);
|
||||||
// If we're at the start node and offset is equal to 0
|
// If we're at the start node and offset is equal to 0
|
||||||
// or current node is different to start node and
|
// or current node is different to start node and
|
||||||
// different to end node or we're at the end node
|
// different to end node or we're at the end node
|
||||||
@@ -1781,16 +1781,16 @@ export class SelectionController extends EventTarget {
|
|||||||
(this.#textNodeIterator.currentNode === endNode &&
|
(this.#textNodeIterator.currentNode === endNode &&
|
||||||
endOffset === endNode.nodeValue.length)
|
endOffset === endNode.nodeValue.length)
|
||||||
) {
|
) {
|
||||||
setInlineStyles(inline, newStyles);
|
setTextSpanStyles(textSpan, newStyles);
|
||||||
|
|
||||||
// If we're at end node
|
// If we're at end node
|
||||||
} else if (
|
} else if (
|
||||||
this.#textNodeIterator.currentNode === endNode &&
|
this.#textNodeIterator.currentNode === endNode &&
|
||||||
endOffset < endNode.nodeValue.length
|
endOffset < endNode.nodeValue.length
|
||||||
) {
|
) {
|
||||||
const newInline = splitInline(inline, endOffset);
|
const newTextSpan = splitTextSpan(textSpan, endOffset);
|
||||||
setInlineStyles(inline, newStyles);
|
setTextSpanStyles(textSpan, newStyles);
|
||||||
inline.after(newInline);
|
textSpan.after(newTextSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We've reached the final node so we can return safely.
|
// We've reached the final node so we can return safely.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
createEmptyParagraph,
|
createEmptyParagraph,
|
||||||
createParagraph,
|
createParagraph,
|
||||||
} from "../content/dom/Paragraph.js";
|
} from "../content/dom/Paragraph.js";
|
||||||
import { createInline } from "../content/dom/Inline.js";
|
import { createTextSpan } from "../content/dom/TextSpan.js";
|
||||||
import { createLineBreak } from "../content/dom/LineBreak.js";
|
import { createLineBreak } from "../content/dom/LineBreak.js";
|
||||||
import { TextEditorMock } from "../../test/TextEditorMock.js";
|
import { TextEditorMock } from "../../test/TextEditorMock.js";
|
||||||
import { SelectionController } from "./SelectionController.js";
|
import { SelectionController } from "./SelectionController.js";
|
||||||
@@ -206,7 +206,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -235,7 +235,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -256,7 +256,7 @@ describe("SelectionController", () => {
|
|||||||
selection,
|
selection,
|
||||||
);
|
);
|
||||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
||||||
const paragraph = createParagraph([createInline(new Text("Hello"))]);
|
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -293,7 +293,7 @@ describe("SelectionController", () => {
|
|||||||
selection,
|
selection,
|
||||||
);
|
);
|
||||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length);
|
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length);
|
||||||
const paragraph = createParagraph([createInline(new Text("ipsum "))]);
|
const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
@@ -306,7 +306,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Lorem ipsum dolor");
|
expect(textEditorMock.root.textContent).toBe("Lorem ipsum dolor");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -337,7 +337,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.firstChild.firstChild,
|
root.firstChild.firstChild.firstChild,
|
||||||
"Hello".length,
|
"Hello".length,
|
||||||
);
|
);
|
||||||
const paragraph = createParagraph([createInline(new Text(", World!"))]);
|
const paragraph = createParagraph([createTextSpan(new Text(", World!"))]);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -364,7 +364,7 @@ describe("SelectionController", () => {
|
|||||||
).toBe(", World!");
|
).toBe(", World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert an inline from a pasted fragment (at start)", () => {
|
test("`insertPaste` should insert a text span from a pasted fragment (at start)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText(", World!");
|
const textEditorMock = TextEditorMock.createTextEditorMockWithText(", World!");
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -378,8 +378,8 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.firstChild.firstChild,
|
root.firstChild.firstChild.firstChild,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const paragraph = createParagraph([createInline(new Text("Hello"))]);
|
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
||||||
paragraph.dataset.inline = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -406,7 +406,7 @@ describe("SelectionController", () => {
|
|||||||
).toBe(", World!");
|
).toBe(", World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert an inline from a pasted fragment (at middle)", () => {
|
test("`insertPaste` should insert an text span from a pasted fragment (at middle)", () => {
|
||||||
const textEditorMock =
|
const textEditorMock =
|
||||||
TextEditorMock.createTextEditorMockWithText("Lorem dolor");
|
TextEditorMock.createTextEditorMockWithText("Lorem dolor");
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
@@ -416,8 +416,8 @@ describe("SelectionController", () => {
|
|||||||
selection,
|
selection,
|
||||||
);
|
);
|
||||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length);
|
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length);
|
||||||
const paragraph = createParagraph([createInline(new Text("ipsum "))]);
|
const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
||||||
paragraph.dataset.inline = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
@@ -430,7 +430,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Lorem ipsum dolor");
|
expect(textEditorMock.root.textContent).toBe("Lorem ipsum dolor");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -447,7 +447,7 @@ describe("SelectionController", () => {
|
|||||||
).toBe("dolor");
|
).toBe("dolor");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert an inline from a pasted fragment (at end)", () => {
|
test("`insertPaste` should insert an text span from a pasted fragment (at end)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
|
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -462,9 +462,9 @@ describe("SelectionController", () => {
|
|||||||
"Hello".length,
|
"Hello".length,
|
||||||
);
|
);
|
||||||
const paragraph = createParagraph([
|
const paragraph = createParagraph([
|
||||||
createInline(new Text(", World!"))
|
createTextSpan(new Text(", World!"))
|
||||||
]);
|
]);
|
||||||
paragraph.dataset.inline = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
@@ -477,7 +477,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -515,7 +515,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World");
|
expect(textEditorMock.root.textContent).toBe("Hello, World");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -549,15 +549,15 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("");
|
expect(textEditorMock.root.textContent).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, "))]),
|
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||||
createParagraph([createInline(new Text("World!"))]),
|
createParagraph([createTextSpan(new Text("World!"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -581,16 +581,16 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, "))]),
|
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||||
createEmptyParagraph(),
|
createEmptyParagraph(),
|
||||||
createParagraph([createInline(new Text("World!"))]),
|
createParagraph([createTextSpan(new Text("World!"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -614,7 +614,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
|
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
|
||||||
@@ -623,8 +623,8 @@ describe("SelectionController", () => {
|
|||||||
|
|
||||||
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, "))]),
|
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||||
createParagraph([createInline(new Text("World!"))]),
|
createParagraph([createTextSpan(new Text("World!"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -648,16 +648,16 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, "))]),
|
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||||
createEmptyParagraph(),
|
createEmptyParagraph(),
|
||||||
createParagraph([createInline(new Text("World!"))]),
|
createParagraph([createTextSpan(new Text("World!"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -681,7 +681,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
|
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
|
||||||
@@ -707,7 +707,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("ello, World!");
|
expect(textEditorMock.root.textContent).toBe("ello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -744,7 +744,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, Mundo!");
|
expect(textEditorMock.root.textContent).toBe("Hello, Mundo!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -755,10 +755,10 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceInlines` should replace the selected text in multiple inlines (2 completelly selected)", () => {
|
test("`replaceTextSpans` should replace the selected text in multiple text spans (2 completelly selected)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||||
createInline(new Text("Hello, ")),
|
createTextSpan(new Text("Hello, ")),
|
||||||
createInline(new Text("World!")),
|
createTextSpan(new Text("World!")),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -774,7 +774,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.lastChild.firstChild,
|
root.firstChild.lastChild.firstChild,
|
||||||
"World!".length,
|
"World!".length,
|
||||||
);
|
);
|
||||||
selectionController.replaceInlines("Mundo");
|
selectionController.replaceTextSpans("Mundo");
|
||||||
|
|
||||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||||
@@ -785,7 +785,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Mundo");
|
expect(textEditorMock.root.textContent).toBe("Mundo");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -796,10 +796,10 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceInlines` should replace the selected text in multiple inlines (2 partially selected)", () => {
|
test("`replaceTextSpans` should replace the selected text in multiple text spans (2 partially selected)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||||
createInline(new Text("Hello, ")),
|
createTextSpan(new Text("Hello, ")),
|
||||||
createInline(new Text("World!")),
|
createTextSpan(new Text("World!")),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -815,7 +815,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.lastChild.firstChild,
|
root.firstChild.lastChild.firstChild,
|
||||||
"World!".length - 3,
|
"World!".length - 3,
|
||||||
);
|
);
|
||||||
selectionController.replaceInlines("Mundo");
|
selectionController.replaceTextSpans("Mundo");
|
||||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||||
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
||||||
@@ -825,7 +825,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("HeMundold!");
|
expect(textEditorMock.root.textContent).toBe("HeMundold!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -842,10 +842,10 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceInlines` should replace the selected text in multiple inlines (1 partially selected, 1 completelly selected)", () => {
|
test("`replaceTextSpans` should replace the selected text in multiple text spans (1 partially selected, 1 completelly selected)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||||
createInline(new Text("Hello, ")),
|
createTextSpan(new Text("Hello, ")),
|
||||||
createInline(new Text("World!")),
|
createTextSpan(new Text("World!")),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -861,7 +861,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.lastChild.firstChild,
|
root.firstChild.lastChild.firstChild,
|
||||||
"World!".length,
|
"World!".length,
|
||||||
);
|
);
|
||||||
selectionController.replaceInlines("Mundo");
|
selectionController.replaceTextSpans("Mundo");
|
||||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||||
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
||||||
@@ -871,7 +871,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("HeMundo");
|
expect(textEditorMock.root.textContent).toBe("HeMundo");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -882,10 +882,10 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceInlines` should replace the selected text in multiple inlines (1 completelly selected, 1 partially selected)", () => {
|
test("`replaceTextSpans` should replace the selected text in multiple text spans (1 completelly selected, 1 partially selected)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||||
createInline(new Text("Hello, ")),
|
createTextSpan(new Text("Hello, ")),
|
||||||
createInline(new Text("World!")),
|
createTextSpan(new Text("World!")),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -901,7 +901,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.lastChild.firstChild,
|
root.firstChild.lastChild.firstChild,
|
||||||
"World!".length - 3,
|
"World!".length - 3,
|
||||||
);
|
);
|
||||||
selectionController.replaceInlines("Mundo");
|
selectionController.replaceTextSpans("Mundo");
|
||||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||||
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
||||||
@@ -911,7 +911,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Mundold!");
|
expect(textEditorMock.root.textContent).toBe("Mundold!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -949,7 +949,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, !");
|
expect(textEditorMock.root.textContent).toBe("Hello, !");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -960,10 +960,10 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`removeSelected` multiple inlines", () => {
|
test("`removeSelected` multiple text spans", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||||
createInline(new Text("Hello, ")),
|
createTextSpan(new Text("Hello, ")),
|
||||||
createInline(new Text("World!")),
|
createTextSpan(new Text("World!")),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -989,7 +989,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("");
|
expect(textEditorMock.root.textContent).toBe("");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -999,9 +999,9 @@ describe("SelectionController", () => {
|
|||||||
|
|
||||||
test("`removeSelected` multiple paragraphs", () => {
|
test("`removeSelected` multiple paragraphs", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, "))]),
|
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||||
createParagraph([createInline(createLineBreak())]),
|
createParagraph([createTextSpan(createLineBreak())]),
|
||||||
createParagraph([createInline(new Text("World!"))]),
|
createParagraph([createTextSpan(new Text("World!"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1028,7 +1028,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -1047,9 +1047,9 @@ describe("SelectionController", () => {
|
|||||||
|
|
||||||
test("`removeSelected` and `removeBackwardParagraph`", () => {
|
test("`removeSelected` and `removeBackwardParagraph`", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, World!"))]),
|
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||||
createParagraph([createInline(createLineBreak())]),
|
createParagraph([createTextSpan(createLineBreak())]),
|
||||||
createParagraph([createInline(new Text("This is a test"))]),
|
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1077,7 +1077,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -1090,9 +1090,9 @@ describe("SelectionController", () => {
|
|||||||
|
|
||||||
test("`removeSelected` and `removeForwardParagraph`", () => {
|
test("`removeSelected` and `removeForwardParagraph`", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, World!"))]),
|
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||||
createParagraph([createInline(createLineBreak())]),
|
createParagraph([createTextSpan(createLineBreak())]),
|
||||||
createParagraph([createInline(new Text("This is a test"))]),
|
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1120,7 +1120,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("This is a test");
|
expect(textEditorMock.root.textContent).toBe("This is a test");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -1133,9 +1133,9 @@ describe("SelectionController", () => {
|
|||||||
|
|
||||||
test("performing a `removeSelected` after a `removeSelected` should do nothing", () => {
|
test("performing a `removeSelected` after a `removeSelected` should do nothing", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, World!"))]),
|
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||||
createParagraph([createInline(createLineBreak())]),
|
createParagraph([createTextSpan(createLineBreak())]),
|
||||||
createParagraph([createInline(new Text("This is a test"))]),
|
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1166,7 +1166,7 @@ describe("SelectionController", () => {
|
|||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("This is a test");
|
expect(textEditorMock.root.textContent).toBe("This is a test");
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
@@ -1179,9 +1179,9 @@ describe("SelectionController", () => {
|
|||||||
|
|
||||||
test("`removeSelected` removes everything", () => {
|
test("`removeSelected` removes everything", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, World!"))]),
|
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||||
createParagraph([createInline(createLineBreak())]),
|
createParagraph([createTextSpan(createLineBreak())]),
|
||||||
createParagraph([createInline(new Text("This is a test"))]),
|
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1212,9 +1212,9 @@ describe("SelectionController", () => {
|
|||||||
|
|
||||||
test("`removeSelected` removes everything and insert text", () => {
|
test("`removeSelected` removes everything and insert text", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([createInline(new Text("Hello, World!"))]),
|
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||||
createParagraph([createInline(createLineBreak())]),
|
createParagraph([createTextSpan(createLineBreak())]),
|
||||||
createParagraph([createInline(new Text("This is a test"))]),
|
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1277,7 +1277,7 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.length).toBe(3);
|
expect(textEditorMock.root.firstChild.children.length).toBe(3);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||||
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
|
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
|
||||||
@@ -1291,12 +1291,12 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`applyStyles` to inlines", () => {
|
test("`applyStyles` to text spans", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||||
createInline(new Text("Hello, "), {
|
createTextSpan(new Text("Hello, "), {
|
||||||
"font-style": "italic",
|
"font-style": "italic",
|
||||||
}),
|
}),
|
||||||
createInline(new Text("World!"), {
|
createTextSpan(new Text("World!"), {
|
||||||
"font-style": "oblique",
|
"font-style": "oblique",
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@@ -1328,25 +1328,25 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.length).toBe(4);
|
expect(textEditorMock.root.firstChild.children.length).toBe(4);
|
||||||
expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
|
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
|
||||||
"He",
|
"He",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe(
|
expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe(
|
||||||
"llo, ",
|
"llo, ",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(2).dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.children.item(2).dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(2).textContent).toBe(
|
expect(textEditorMock.root.firstChild.children.item(2).textContent).toBe(
|
||||||
"Wor",
|
"Wor",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(3).dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.children.item(3).dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(3).textContent).toBe(
|
expect(textEditorMock.root.firstChild.children.item(3).textContent).toBe(
|
||||||
"ld!",
|
"ld!",
|
||||||
@@ -1356,12 +1356,12 @@ describe("SelectionController", () => {
|
|||||||
test("`applyStyles` to paragraphs", () => {
|
test("`applyStyles` to paragraphs", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([
|
createParagraph([
|
||||||
createInline(new Text("Hello, "), {
|
createTextSpan(new Text("Hello, "), {
|
||||||
"font-style": "italic",
|
"font-style": "italic",
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
createParagraph([
|
createParagraph([
|
||||||
createInline(new Text("World!"), {
|
createTextSpan(new Text("World!"), {
|
||||||
"font-style": "oblique",
|
"font-style": "oblique",
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@@ -1394,26 +1394,26 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.length).toBe(2);
|
expect(textEditorMock.root.firstChild.children.length).toBe(2);
|
||||||
expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.children.item(0).dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
|
expect(textEditorMock.root.firstChild.children.item(0).textContent).toBe(
|
||||||
"He",
|
"He",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe(
|
expect(textEditorMock.root.firstChild.children.item(1).dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe(
|
expect(textEditorMock.root.firstChild.children.item(1).textContent).toBe(
|
||||||
"llo, ",
|
"llo, ",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.lastChild.children.length).toBe(2);
|
expect(textEditorMock.root.lastChild.children.length).toBe(2);
|
||||||
expect(textEditorMock.root.lastChild.children.item(0).dataset.itype).toBe(
|
expect(textEditorMock.root.lastChild.children.item(0).dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.lastChild.children.item(0).textContent).toBe(
|
expect(textEditorMock.root.lastChild.children.item(0).textContent).toBe(
|
||||||
"Wor",
|
"Wor",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.lastChild.children.item(1).dataset.itype).toBe(
|
expect(textEditorMock.root.lastChild.children.item(1).dataset.itype).toBe(
|
||||||
"inline",
|
"span",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.lastChild.children.item(1).textContent).toBe(
|
expect(textEditorMock.root.lastChild.children.item(1).textContent).toBe(
|
||||||
"ld!",
|
"ld!",
|
||||||
|
|||||||
@@ -36,15 +36,15 @@ export class SelectionControllerDebug {
|
|||||||
update(selectionController) {
|
update(selectionController) {
|
||||||
this.#elements.direction.value = selectionController.direction;
|
this.#elements.direction.value = selectionController.direction;
|
||||||
this.#elements.multiElement.checked = selectionController.isMulti;
|
this.#elements.multiElement.checked = selectionController.isMulti;
|
||||||
this.#elements.multiInlineElement.checked =
|
this.#elements.multiTextSpanElement.checked =
|
||||||
selectionController.isMultiInline;
|
selectionController.isMultiTextSpan;
|
||||||
this.#elements.multiParagraphElement.checked =
|
this.#elements.multiParagraphElement.checked =
|
||||||
selectionController.isMultiParagraph;
|
selectionController.isMultiParagraph;
|
||||||
this.#elements.isParagraphStart.checked =
|
this.#elements.isParagraphStart.checked =
|
||||||
selectionController.isParagraphStart;
|
selectionController.isParagraphStart;
|
||||||
this.#elements.isParagraphEnd.checked = selectionController.isParagraphEnd;
|
this.#elements.isParagraphEnd.checked = selectionController.isParagraphEnd;
|
||||||
this.#elements.isInlineStart.checked = selectionController.isInlineStart;
|
this.#elements.isTextSpanStart.checked = selectionController.isTextSpanStart;
|
||||||
this.#elements.isInlineEnd.checked = selectionController.isInlineEnd;
|
this.#elements.isTextSpanEnd.checked = selectionController.isTextSpanEnd;
|
||||||
this.#elements.isTextAnchor.checked = selectionController.isTextAnchor;
|
this.#elements.isTextAnchor.checked = selectionController.isTextAnchor;
|
||||||
this.#elements.isTextFocus.checked = selectionController.isTextFocus;
|
this.#elements.isTextFocus.checked = selectionController.isTextFocus;
|
||||||
this.#elements.focusNode.value = this.getNodeDescription(
|
this.#elements.focusNode.value = this.getNodeDescription(
|
||||||
@@ -57,11 +57,11 @@ export class SelectionControllerDebug {
|
|||||||
selectionController.anchorOffset
|
selectionController.anchorOffset
|
||||||
);
|
);
|
||||||
this.#elements.anchorOffset.value = selectionController.anchorOffset;
|
this.#elements.anchorOffset.value = selectionController.anchorOffset;
|
||||||
this.#elements.focusInline.value = this.getNodeDescription(
|
this.#elements.focusTextSpan.value = this.getNodeDescription(
|
||||||
selectionController.focusInline
|
selectionController.focusTextSpan
|
||||||
);
|
);
|
||||||
this.#elements.anchorInline.value = this.getNodeDescription(
|
this.#elements.anchorTextSpan.value = this.getNodeDescription(
|
||||||
selectionController.anchorInline
|
selectionController.anchorTextSpan
|
||||||
);
|
);
|
||||||
this.#elements.focusParagraph.value = this.getNodeDescription(
|
this.#elements.focusParagraph.value = this.getNodeDescription(
|
||||||
selectionController.focusParagraph
|
selectionController.focusParagraph
|
||||||
|
|||||||
@@ -129,8 +129,8 @@
|
|||||||
<input id="focus-offset" readonly type="number">
|
<input id="focus-offset" readonly type="number">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="focus-inline">Focus Inline</label>
|
<label for="focus-textspan">Focus TextSpan</label>
|
||||||
<input id="focus-inline" readonly type="text" />
|
<input id="focus-textspan" readonly type="text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="focus-paragraph">Focus Paragraph</label>
|
<label for="focus-paragraph">Focus Paragraph</label>
|
||||||
@@ -145,8 +145,8 @@
|
|||||||
<input id="anchor-offset" readonly type="number">
|
<input id="anchor-offset" readonly type="number">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="anchor-inline">Anchor Inline</label>
|
<label for="anchor-textspan">Anchor TextSpan</label>
|
||||||
<input id="anchor-inline" readonly type="text" />
|
<input id="anchor-textspan" readonly type="text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="anchor-paragraph">Anchor Paragraph</label>
|
<label for="anchor-paragraph">Anchor Paragraph</label>
|
||||||
@@ -173,8 +173,8 @@
|
|||||||
<input id="multi" readonly type="checkbox" />
|
<input id="multi" readonly type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="multi-inline">Multi inline?</label>
|
<label for="multi-textspan">Multi text span?</label>
|
||||||
<input id="multi-inline" readonly type="checkbox" />
|
<input id="multi-textspan" readonly type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="multi-paragraph">Multi paragraph?</label>
|
<label for="multi-paragraph">Multi paragraph?</label>
|
||||||
@@ -197,12 +197,12 @@
|
|||||||
<input id="is-paragraph-end" readonly type="checkbox" />
|
<input id="is-paragraph-end" readonly type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="is-inline-start">Is inline start?</label>
|
<label for="is-textspan-start">Is text span start?</label>
|
||||||
<input id="is-inline-start" readonly type="checkbox" />
|
<input id="is-textspan-start" readonly type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="is-inline-end">Is inline end?</label>
|
<label for="is-textspan-end">Is text span end?</label>
|
||||||
<input id="is-inline-end" readonly type="checkbox">
|
<input id="is-textspan-end" readonly type="checkbox">
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { UUID } from "./playground/uuid.js";
|
|||||||
import { Rect, Point } from "./playground/geom.js";
|
import { Rect, Point } from "./playground/geom.js";
|
||||||
import { WASMModuleWrapper } from "./playground/wasm.js";
|
import { WASMModuleWrapper } from "./playground/wasm.js";
|
||||||
import { FontManager } from "./playground/font.js";
|
import { FontManager } from "./playground/font.js";
|
||||||
import { TextContent, TextParagraph, TextLeaf } from "./playground/text.js";
|
import { TextContent, TextParagraph, TextSpan } from "./playground/text.js";
|
||||||
import { Viewport } from "./playground/viewport.js";
|
import { Viewport } from "./playground/viewport.js";
|
||||||
import { Fill } from "./playground/fill.js";
|
import { Fill } from "./playground/fill.js";
|
||||||
import { Shape } from "./playground/shape.js";
|
import { Shape } from "./playground/shape.js";
|
||||||
@@ -80,21 +80,21 @@ class TextEditorPlayground {
|
|||||||
debug: new SelectionControllerDebug({
|
debug: new SelectionControllerDebug({
|
||||||
direction: document.getElementById("direction"),
|
direction: document.getElementById("direction"),
|
||||||
multiElement: document.getElementById("multi"),
|
multiElement: document.getElementById("multi"),
|
||||||
multiInlineElement: document.getElementById("multi-inline"),
|
multiTextSpanElement: document.getElementById("multi-textspan"),
|
||||||
multiParagraphElement: document.getElementById("multi-paragraph"),
|
multiParagraphElement: document.getElementById("multi-paragraph"),
|
||||||
isParagraphStart: document.getElementById("is-paragraph-start"),
|
isParagraphStart: document.getElementById("is-paragraph-start"),
|
||||||
isParagraphEnd: document.getElementById("is-paragraph-end"),
|
isParagraphEnd: document.getElementById("is-paragraph-end"),
|
||||||
isInlineStart: document.getElementById("is-inline-start"),
|
isTextSpanStart: document.getElementById("is-textspan-start"),
|
||||||
isInlineEnd: document.getElementById("is-inline-end"),
|
isTextSpanEnd: document.getElementById("is-textspan-end"),
|
||||||
isTextAnchor: document.getElementById("is-text-anchor"),
|
isTextAnchor: document.getElementById("is-text-anchor"),
|
||||||
isTextFocus: document.getElementById("is-text-focus"),
|
isTextFocus: document.getElementById("is-text-focus"),
|
||||||
focusNode: document.getElementById("focus-node"),
|
focusNode: document.getElementById("focus-node"),
|
||||||
focusOffset: document.getElementById("focus-offset"),
|
focusOffset: document.getElementById("focus-offset"),
|
||||||
focusInline: document.getElementById("focus-inline"),
|
focusTextSpan: document.getElementById("focus-textspan"),
|
||||||
focusParagraph: document.getElementById("focus-paragraph"),
|
focusParagraph: document.getElementById("focus-paragraph"),
|
||||||
anchorNode: document.getElementById("anchor-node"),
|
anchorNode: document.getElementById("anchor-node"),
|
||||||
anchorOffset: document.getElementById("anchor-offset"),
|
anchorOffset: document.getElementById("anchor-offset"),
|
||||||
anchorInline: document.getElementById("anchor-inline"),
|
anchorTextSpan: document.getElementById("anchor-textspan"),
|
||||||
anchorParagraph: document.getElementById("anchor-paragraph"),
|
anchorParagraph: document.getElementById("anchor-paragraph"),
|
||||||
startContainer: document.getElementById("start-container"),
|
startContainer: document.getElementById("start-container"),
|
||||||
startOffset: document.getElementById("start-offset"),
|
startOffset: document.getElementById("start-offset"),
|
||||||
@@ -496,7 +496,7 @@ class TextEditorPlayground {
|
|||||||
|
|
||||||
leaf.fills.forEach((fill, index) => {
|
leaf.fills.forEach((fill, index) => {
|
||||||
const fillOffset =
|
const fillOffset =
|
||||||
offset + TextLeaf.BYTE_LENGTH + index * Fill.BYTE_LENGTH;
|
offset + TextSpan.BYTE_LENGTH + index * Fill.BYTE_LENGTH;
|
||||||
if (fill.type === Fill.Type.SOLID) {
|
if (fill.type === Fill.Type.SOLID) {
|
||||||
view.setUint8(fillOffset + 0, fill.type, true);
|
view.setUint8(fillOffset + 0, fill.type, true);
|
||||||
view.setUint32(fillOffset + 4, fill.solid.color.argb32, true);
|
view.setUint32(fillOffset + 4, fill.solid.color.argb32, true);
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ export const TextTransform = {
|
|||||||
fromStyle,
|
fromStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class TextLeaf {
|
export class TextSpan {
|
||||||
static BYTE_LENGTH = 60;
|
static BYTE_LENGTH = 60;
|
||||||
|
|
||||||
static fromDOM(leafElement, fontManager) {
|
static fromDOM(spanElement, fontManager) {
|
||||||
const elementStyle = leafElement.style; //window.getComputedStyle(leafElement);
|
const elementStyle = spanElement.style; //window.getComputedStyle(leafElement);
|
||||||
const fontSize = parseFloat(
|
const fontSize = parseFloat(
|
||||||
elementStyle.getPropertyValue("font-size"),
|
elementStyle.getPropertyValue("font-size"),
|
||||||
);
|
);
|
||||||
@@ -77,7 +77,7 @@ export class TextLeaf {
|
|||||||
if (!font) {
|
if (!font) {
|
||||||
throw new Error(`Invalid font "${fontFamily}"`)
|
throw new Error(`Invalid font "${fontFamily}"`)
|
||||||
}
|
}
|
||||||
return new TextLeaf({
|
return new TextSpan({
|
||||||
fontId: font.id, // leafElement.style.getPropertyValue("--font-id"),
|
fontId: font.id, // leafElement.style.getPropertyValue("--font-id"),
|
||||||
fontFamilyHash: 0,
|
fontFamilyHash: 0,
|
||||||
fontVariantId: UUID.ZERO, // leafElement.style.getPropertyValue("--font-variant-id"),
|
fontVariantId: UUID.ZERO, // leafElement.style.getPropertyValue("--font-variant-id"),
|
||||||
@@ -88,7 +88,7 @@ export class TextLeaf {
|
|||||||
textDecoration,
|
textDecoration,
|
||||||
textTransform,
|
textTransform,
|
||||||
textDirection,
|
textDirection,
|
||||||
text: leafElement.textContent,
|
text: spanElement.textContent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ export class TextLeaf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get leafByteLength() {
|
get leafByteLength() {
|
||||||
return this.fills.length * Fill.BYTE_LENGTH + TextLeaf.BYTE_LENGTH;
|
return this.fills.length * Fill.BYTE_LENGTH + TextSpan.BYTE_LENGTH;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ export class TextParagraph {
|
|||||||
paragraphElement.style.getPropertyValue("letter-spacing"),
|
paragraphElement.style.getPropertyValue("letter-spacing"),
|
||||||
),
|
),
|
||||||
leaves: Array.from(paragraphElement.children, (leafElement) =>
|
leaves: Array.from(paragraphElement.children, (leafElement) =>
|
||||||
TextLeaf.fromDOM(leafElement, fontManager),
|
TextSpan.fromDOM(leafElement, fontManager),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ export class TextParagraph {
|
|||||||
this.#leaves = init?.leaves ?? [];
|
this.#leaves = init?.leaves ?? [];
|
||||||
if (
|
if (
|
||||||
!Array.isArray(this.#leaves) ||
|
!Array.isArray(this.#leaves) ||
|
||||||
!this.#leaves.every((leaf) => leaf instanceof TextLeaf)
|
!this.#leaves.every((leaf) => leaf instanceof TextSpan)
|
||||||
) {
|
) {
|
||||||
throw new TypeError("Invalid text leaves");
|
throw new TypeError("Invalid text leaves");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createRoot } from "../editor/content/dom/Root.js";
|
import { createRoot } from "../editor/content/dom/Root.js";
|
||||||
import { createParagraph } from "../editor/content/dom/Paragraph.js";
|
import { createParagraph } from "../editor/content/dom/Paragraph.js";
|
||||||
import { createEmptyInline, createInline } from "../editor/content/dom/Inline.js";
|
import { createEmptyTextSpan, createTextSpan } from "../editor/content/dom/TextSpan.js";
|
||||||
import { createLineBreak } from "../editor/content/dom/LineBreak.js";
|
import { createLineBreak } from "../editor/content/dom/LineBreak.js";
|
||||||
|
|
||||||
export class TextEditorMock extends EventTarget {
|
export class TextEditorMock extends EventTarget {
|
||||||
@@ -68,7 +68,7 @@ export class TextEditorMock extends EventTarget {
|
|||||||
*/
|
*/
|
||||||
static createTextEditorMockEmpty() {
|
static createTextEditorMockEmpty() {
|
||||||
const root = createRoot([
|
const root = createRoot([
|
||||||
createParagraph([createInline(createLineBreak())]),
|
createParagraph([createTextSpan(createLineBreak())]),
|
||||||
]);
|
]);
|
||||||
return this.createTextEditorMockWithRoot(root);
|
return this.createTextEditorMockWithRoot(root);
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ export class TextEditorMock extends EventTarget {
|
|||||||
/**
|
/**
|
||||||
* Creates a TextEditor mock with some text.
|
* Creates a TextEditor mock with some text.
|
||||||
*
|
*
|
||||||
* NOTE: If the text is empty an empty inline will be
|
* NOTE: If the text is empty an empty text span will be
|
||||||
* created.
|
* created.
|
||||||
*
|
*
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
@@ -86,21 +86,21 @@ export class TextEditorMock extends EventTarget {
|
|||||||
return this.createTextEditorMockWithParagraphs([
|
return this.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([
|
createParagraph([
|
||||||
text.length === 0
|
text.length === 0
|
||||||
? createEmptyInline()
|
? createEmptyTextSpan()
|
||||||
: createInline(new Text(text))
|
: createTextSpan(new Text(text))
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a TextEditor mock with some inlines and
|
* Creates a TextEditor mock with some textSpans and
|
||||||
* only one paragraph.
|
* only one paragraph.
|
||||||
*
|
*
|
||||||
* @param {Array<HTMLSpanElement>} inlines
|
* @param {Array<HTMLSpanElement>} textSpans
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static createTextEditorMockWithParagraph(inlines) {
|
static createTextEditorMockWithParagraph(textSpans) {
|
||||||
return this.createTextEditorMockWithParagraphs([createParagraph(inlines)]);
|
return this.createTextEditorMockWithParagraphs([createParagraph(textSpans)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#element = null;
|
#element = null;
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
for leaf in paragraph.children().iter() {
|
for span in paragraph.children().iter() {
|
||||||
let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds);
|
let text_paint: skia_safe::Handle<_> = merge_fills(span.fills(), *bounds);
|
||||||
let stroke_paints = get_text_stroke_paints(
|
let stroke_paints = get_text_stroke_paints(
|
||||||
stroke,
|
stroke,
|
||||||
bounds,
|
bounds,
|
||||||
@@ -43,7 +43,7 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
remove_stroke_alpha,
|
remove_stroke_alpha,
|
||||||
);
|
);
|
||||||
|
|
||||||
let text: String = leaf.apply_text_transform();
|
let text: String = span.apply_text_transform();
|
||||||
|
|
||||||
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
|
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
|
||||||
let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| {
|
let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| {
|
||||||
@@ -51,9 +51,9 @@ pub fn stroke_paragraph_builder_group_from_text(
|
|||||||
ParagraphBuilder::new(¶graph_style, fonts)
|
ParagraphBuilder::new(¶graph_style, fonts)
|
||||||
});
|
});
|
||||||
let stroke_paint = stroke_paint.clone();
|
let stroke_paint = stroke_paint.clone();
|
||||||
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent();
|
let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
|
||||||
let stroke_style =
|
let stroke_style =
|
||||||
leaf.to_stroke_style(&stroke_paint, fallback_fonts, remove_alpha);
|
span.to_stroke_style(&stroke_paint, fallback_fonts, remove_alpha);
|
||||||
builder.push_style(&stroke_style);
|
builder.push_style(&stroke_style);
|
||||||
builder.add_text(&text);
|
builder.add_text(&text);
|
||||||
}
|
}
|
||||||
@@ -342,7 +342,7 @@ fn render_text_decoration(
|
|||||||
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
|
let (max_underline_thickness, underline_y, max_strike_thickness, strike_y) =
|
||||||
calculate_decoration_metrics(&style_metrics, line_baseline);
|
calculate_decoration_metrics(&style_metrics, line_baseline);
|
||||||
|
|
||||||
// Draw decorations per segment (text leaf)
|
// Draw decorations per segment (text span)
|
||||||
for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() {
|
for (i, (style_start, style_metric)) in style_metrics.iter().enumerate() {
|
||||||
let text_style = &style_metric.text_style;
|
let text_style = &style_metric.text_style;
|
||||||
let style_end = style_metrics
|
let style_end = style_metrics
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ impl TextContentSize {
|
|||||||
pub struct TextPositionWithAffinity {
|
pub struct TextPositionWithAffinity {
|
||||||
pub position_with_affinity: PositionWithAffinity,
|
pub position_with_affinity: PositionWithAffinity,
|
||||||
pub paragraph: i32,
|
pub paragraph: i32,
|
||||||
pub leaf: i32,
|
pub span: i32,
|
||||||
pub offset: i32,
|
pub offset: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,13 +106,13 @@ impl TextPositionWithAffinity {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
position_with_affinity: PositionWithAffinity,
|
position_with_affinity: PositionWithAffinity,
|
||||||
paragraph: i32,
|
paragraph: i32,
|
||||||
leaf: i32,
|
span: i32,
|
||||||
offset: i32,
|
offset: i32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
position_with_affinity,
|
position_with_affinity,
|
||||||
paragraph,
|
paragraph,
|
||||||
leaf,
|
span,
|
||||||
offset,
|
offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ impl TextContent {
|
|||||||
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
||||||
|
|
||||||
let mut paragraph_index: i32 = -1;
|
let mut paragraph_index: i32 = -1;
|
||||||
let mut leaf_index: i32 = -1;
|
let mut span_index: i32 = -1;
|
||||||
for layout_paragraph in layout_paragraphs {
|
for layout_paragraph in layout_paragraphs {
|
||||||
paragraph_index += 1;
|
paragraph_index += 1;
|
||||||
let start_y = offset_y;
|
let start_y = offset_y;
|
||||||
@@ -303,17 +303,17 @@ impl TextContent {
|
|||||||
if let Some(paragraph) = self.paragraphs().get(paragraph_index as usize) {
|
if let Some(paragraph) = self.paragraphs().get(paragraph_index as usize) {
|
||||||
// Computed position keeps the current position in terms
|
// Computed position keeps the current position in terms
|
||||||
// of number of characters of text. This is used to know
|
// of number of characters of text. This is used to know
|
||||||
// in which leaf we are.
|
// in which span we are.
|
||||||
let mut computed_position = 0;
|
let mut computed_position = 0;
|
||||||
let mut leaf_offset = 0;
|
let mut span_offset = 0;
|
||||||
for leaf in paragraph.children() {
|
for span in paragraph.children() {
|
||||||
leaf_index += 1;
|
span_index += 1;
|
||||||
let length = leaf.text.len();
|
let length = span.text.len();
|
||||||
let start_position = computed_position;
|
let start_position = computed_position;
|
||||||
let end_position = computed_position + length;
|
let end_position = computed_position + length;
|
||||||
let current_position = position_with_affinity.position as usize;
|
let current_position = position_with_affinity.position as usize;
|
||||||
if start_position <= current_position && end_position >= current_position {
|
if start_position <= current_position && end_position >= current_position {
|
||||||
leaf_offset = position_with_affinity.position - start_position as i32;
|
span_offset = position_with_affinity.position - start_position as i32;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
computed_position += length;
|
computed_position += length;
|
||||||
@@ -321,8 +321,8 @@ impl TextContent {
|
|||||||
return Some(TextPositionWithAffinity::new(
|
return Some(TextPositionWithAffinity::new(
|
||||||
position_with_affinity,
|
position_with_affinity,
|
||||||
paragraph_index,
|
paragraph_index,
|
||||||
leaf_index,
|
span_index,
|
||||||
leaf_offset,
|
span_offset,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,10 +344,10 @@ impl TextContent {
|
|||||||
for paragraph in self.paragraphs() {
|
for paragraph in self.paragraphs() {
|
||||||
let paragraph_style = paragraph.paragraph_to_style();
|
let paragraph_style = paragraph.paragraph_to_style();
|
||||||
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
||||||
for leaf in paragraph.children() {
|
for span in paragraph.children() {
|
||||||
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent();
|
let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
|
||||||
let text_style = leaf.to_style(&self.bounds(), fallback_fonts, remove_alpha);
|
let text_style = span.to_style(&self.bounds(), fallback_fonts, remove_alpha);
|
||||||
let text = leaf.apply_text_transform();
|
let text = span.apply_text_transform();
|
||||||
builder.push_style(&text_style);
|
builder.push_style(&text_style);
|
||||||
builder.add_text(&text);
|
builder.add_text(&text);
|
||||||
}
|
}
|
||||||
@@ -519,7 +519,7 @@ pub struct Paragraph {
|
|||||||
letter_spacing: f32,
|
letter_spacing: f32,
|
||||||
typography_ref_file: Uuid,
|
typography_ref_file: Uuid,
|
||||||
typography_ref_id: Uuid,
|
typography_ref_id: Uuid,
|
||||||
children: Vec<TextLeaf>,
|
children: Vec<TextSpan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Paragraph {
|
impl Default for Paragraph {
|
||||||
@@ -549,7 +549,7 @@ impl Paragraph {
|
|||||||
letter_spacing: f32,
|
letter_spacing: f32,
|
||||||
typography_ref_file: Uuid,
|
typography_ref_file: Uuid,
|
||||||
typography_ref_id: Uuid,
|
typography_ref_id: Uuid,
|
||||||
children: Vec<TextLeaf>,
|
children: Vec<TextSpan>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text_align,
|
text_align,
|
||||||
@@ -565,17 +565,17 @@ impl Paragraph {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn set_children(&mut self, children: Vec<TextLeaf>) {
|
fn set_children(&mut self, children: Vec<TextSpan>) {
|
||||||
self.children = children;
|
self.children = children;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn children(&self) -> &[TextLeaf] {
|
pub fn children(&self) -> &[TextSpan] {
|
||||||
&self.children
|
&self.children
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn add_leaf(&mut self, leaf: TextLeaf) {
|
fn add_span(&mut self, span: TextSpan) {
|
||||||
self.children.push(leaf);
|
self.children.push(span);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: move serialization to wasm module
|
// FIXME: move serialization to wasm module
|
||||||
@@ -622,7 +622,7 @@ impl Paragraph {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct TextLeaf {
|
pub struct TextSpan {
|
||||||
text: String,
|
text: String,
|
||||||
font_family: FontFamily,
|
font_family: FontFamily,
|
||||||
font_size: f32,
|
font_size: f32,
|
||||||
@@ -635,7 +635,7 @@ pub struct TextLeaf {
|
|||||||
fills: Vec<shapes::Fill>,
|
fills: Vec<shapes::Fill>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextLeaf {
|
impl TextSpan {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
text: String,
|
text: String,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ impl TextPaths {
|
|||||||
let start = line_metrics.start_index;
|
let start = line_metrics.start_index;
|
||||||
let end = line_metrics.end_index;
|
let end = line_metrics.end_index;
|
||||||
|
|
||||||
// 3. Get styles present in line for each text leaf
|
// 3. Get styles present in line for each text span
|
||||||
let style_metrics = line_metrics.get_style_metrics(start..end);
|
let style_metrics = line_metrics.get_style_metrics(start..end);
|
||||||
|
|
||||||
let mut offset_x = 0.0;
|
let mut offset_x = 0.0;
|
||||||
@@ -56,23 +56,23 @@ impl TextPaths {
|
|||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.unwrap_or(text.len());
|
.unwrap_or(text.len());
|
||||||
|
|
||||||
let leaf_text = &text[start_byte..end_byte];
|
let span_text = &text[start_byte..end_byte];
|
||||||
|
|
||||||
let font = skia_paragraph.get_font_at(*start_index);
|
let font = skia_paragraph.get_font_at(*start_index);
|
||||||
|
|
||||||
let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
|
let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
|
||||||
let blob_offset_y = line_offset_y;
|
let blob_offset_y = line_offset_y;
|
||||||
|
|
||||||
// 4. Get the path for each text leaf
|
// 4. Get the path for each text span
|
||||||
if let Some((text_path, paint)) = self.generate_text_path(
|
if let Some((text_path, paint)) = self.generate_text_path(
|
||||||
leaf_text,
|
span_text,
|
||||||
&font,
|
&font,
|
||||||
blob_offset_x,
|
blob_offset_x,
|
||||||
blob_offset_y,
|
blob_offset_y,
|
||||||
style_metric,
|
style_metric,
|
||||||
antialias,
|
antialias,
|
||||||
) {
|
) {
|
||||||
let text_width = font.measure_text(leaf_text, None).0;
|
let text_width = font.measure_text(span_text, None).0;
|
||||||
offset_x += text_width;
|
offset_x += text_width;
|
||||||
paths.push((text_path, paint));
|
paths.push((text_path, paint));
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ impl TextPaths {
|
|||||||
|
|
||||||
fn generate_text_path(
|
fn generate_text_path(
|
||||||
&self,
|
&self,
|
||||||
leaf_text: &str,
|
span_text: &str,
|
||||||
font: &skia::Font,
|
font: &skia::Font,
|
||||||
blob_offset_x: f32,
|
blob_offset_x: f32,
|
||||||
blob_offset_y: f32,
|
blob_offset_y: f32,
|
||||||
@@ -99,10 +99,10 @@ impl TextPaths {
|
|||||||
// This is used to avoid rendering empty paths, but we can
|
// This is used to avoid rendering empty paths, but we can
|
||||||
// revisit this logic later
|
// revisit this logic later
|
||||||
if let Some((text_blob_path, text_blob_bounds)) =
|
if let Some((text_blob_path, text_blob_bounds)) =
|
||||||
Self::get_text_blob_path(leaf_text, font, blob_offset_x, blob_offset_y)
|
Self::get_text_blob_path(span_text, font, blob_offset_x, blob_offset_y)
|
||||||
{
|
{
|
||||||
let mut text_path = text_blob_path.clone();
|
let mut text_path = text_blob_path.clone();
|
||||||
let text_width = font.measure_text(leaf_text, None).0;
|
let text_width = font.measure_text(span_text, None).0;
|
||||||
|
|
||||||
let decoration = style_metric.text_style.decoration();
|
let decoration = style_metric.text_style.decoration();
|
||||||
let font_metrics = style_metric.font_metrics;
|
let font_metrics = style_metric.font_metrics;
|
||||||
@@ -165,13 +165,13 @@ impl TextPaths {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_text_blob_path(
|
fn get_text_blob_path(
|
||||||
leaf_text: &str,
|
span_text: &str,
|
||||||
font: &skia::Font,
|
font: &skia::Font,
|
||||||
blob_offset_x: f32,
|
blob_offset_x: f32,
|
||||||
blob_offset_y: f32,
|
blob_offset_y: f32,
|
||||||
) -> Option<(skia::Path, skia::Rect)> {
|
) -> Option<(skia::Path, skia::Rect)> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let utf16_text = leaf_text.encode_utf16().collect::<Vec<u16>>();
|
let utf16_text = span_text.encode_utf16().collect::<Vec<u16>>();
|
||||||
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
|
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
|
||||||
let emoji_font = state.render_state.fonts().get_emoji_font(font.size());
|
let emoji_font = state.render_state.fonts().get_emoji_font(font.size());
|
||||||
let use_font = emoji_font.as_ref().unwrap_or(font);
|
let use_font = emoji_font.as_ref().unwrap_or(font);
|
||||||
|
|||||||
@@ -3,21 +3,21 @@
|
|||||||
use crate::shapes::TextPositionWithAffinity;
|
use crate::shapes::TextPositionWithAffinity;
|
||||||
|
|
||||||
/// TODO: Now this is just a tuple with 2 i32 working
|
/// TODO: Now this is just a tuple with 2 i32 working
|
||||||
/// as indices (paragraph and leaf).
|
/// as indices (paragraph and span).
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
pub struct TextNodePosition {
|
pub struct TextNodePosition {
|
||||||
pub paragraph: i32,
|
pub paragraph: i32,
|
||||||
pub leaf: i32,
|
pub span: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextNodePosition {
|
impl TextNodePosition {
|
||||||
pub fn new(paragraph: i32, leaf: i32) -> Self {
|
pub fn new(paragraph: i32, span: i32) -> Self {
|
||||||
Self { paragraph, leaf }
|
Self { paragraph, span }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn is_invalid(&self) -> bool {
|
pub fn is_invalid(&self) -> bool {
|
||||||
self.paragraph < 0 || self.leaf < 0
|
self.paragraph < 0 || self.span < 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ impl TextEditorState {
|
|||||||
self.selection.set(
|
self.selection.set(
|
||||||
Some(TextNodePosition::new(
|
Some(TextNodePosition::new(
|
||||||
text_position_with_affinity.paragraph,
|
text_position_with_affinity.paragraph,
|
||||||
text_position_with_affinity.leaf,
|
text_position_with_affinity.span,
|
||||||
)),
|
)),
|
||||||
text_position_with_affinity.offset,
|
text_position_with_affinity.offset,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::shapes::{
|
|||||||
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
||||||
use crate::{with_current_shape_mut, with_state_mut, with_state_mut_current_shape, STATE};
|
use crate::{with_current_shape_mut, with_state_mut, with_state_mut_current_shape, STATE};
|
||||||
|
|
||||||
const RAW_LEAF_DATA_SIZE: usize = std::mem::size_of::<RawTextLeaf>();
|
const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
|
||||||
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
||||||
|
|
||||||
const MAX_TEXT_FILLS: usize = 8;
|
const MAX_TEXT_FILLS: usize = 8;
|
||||||
@@ -94,7 +94,7 @@ impl From<RawTextTransform> for Option<TextTransform> {
|
|||||||
#[repr(align(4))]
|
#[repr(align(4))]
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct RawParagraphData {
|
pub struct RawParagraphData {
|
||||||
leaf_count: u32,
|
span_count: u32,
|
||||||
text_align: RawTextAlign,
|
text_align: RawTextAlign,
|
||||||
text_direction: RawTextDirection,
|
text_direction: RawTextDirection,
|
||||||
text_decoration: RawTextDecoration,
|
text_decoration: RawTextDecoration,
|
||||||
@@ -124,7 +124,7 @@ impl TryFrom<&[u8]> for RawParagraphData {
|
|||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct RawTextLeaf {
|
pub struct RawTextSpan {
|
||||||
font_style: RawFontStyle,
|
font_style: RawFontStyle,
|
||||||
text_decoration: RawTextDecoration,
|
text_decoration: RawTextDecoration,
|
||||||
text_transform: RawTextTransform,
|
text_transform: RawTextTransform,
|
||||||
@@ -140,25 +140,25 @@ pub struct RawTextLeaf {
|
|||||||
fills: [RawFillData; MAX_TEXT_FILLS],
|
fills: [RawFillData; MAX_TEXT_FILLS],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<[u8; RAW_LEAF_DATA_SIZE]> for RawTextLeaf {
|
impl From<[u8; RAW_SPAN_DATA_SIZE]> for RawTextSpan {
|
||||||
fn from(bytes: [u8; RAW_LEAF_DATA_SIZE]) -> Self {
|
fn from(bytes: [u8; RAW_SPAN_DATA_SIZE]) -> Self {
|
||||||
unsafe { std::mem::transmute(bytes) }
|
unsafe { std::mem::transmute(bytes) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&[u8]> for RawTextLeaf {
|
impl TryFrom<&[u8]> for RawTextSpan {
|
||||||
type Error = String;
|
type Error = String;
|
||||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||||
let data: [u8; RAW_LEAF_DATA_SIZE] = bytes
|
let data: [u8; RAW_SPAN_DATA_SIZE] = bytes
|
||||||
.get(0..RAW_LEAF_DATA_SIZE)
|
.get(0..RAW_SPAN_DATA_SIZE)
|
||||||
.and_then(|slice| slice.try_into().ok())
|
.and_then(|slice| slice.try_into().ok())
|
||||||
.ok_or("Invalid text leaf data".to_string())?;
|
.ok_or("Invalid text span data".to_string())?;
|
||||||
Ok(RawTextLeaf::from(data))
|
Ok(RawTextSpan::from(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RawTextLeaf> for shapes::TextLeaf {
|
impl From<RawTextSpan> for shapes::TextSpan {
|
||||||
fn from(value: RawTextLeaf) -> Self {
|
fn from(value: RawTextSpan) -> Self {
|
||||||
let text = String::default();
|
let text = String::default();
|
||||||
|
|
||||||
let font_family = shapes::FontFamily::new(
|
let font_family = shapes::FontFamily::new(
|
||||||
@@ -193,7 +193,7 @@ impl From<RawTextLeaf> for shapes::TextLeaf {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RawParagraph {
|
pub struct RawParagraph {
|
||||||
attrs: RawParagraphData,
|
attrs: RawParagraphData,
|
||||||
leaves: Vec<RawTextLeaf>,
|
leaves: Vec<RawTextSpan>,
|
||||||
text_buffer: Vec<u8>,
|
text_buffer: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,12 +204,12 @@ impl TryFrom<&Vec<u8>> for RawParagraph {
|
|||||||
fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
|
fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
|
||||||
let attrs = RawParagraphData::try_from(&bytes[..RAW_PARAGRAPH_DATA_SIZE])?;
|
let attrs = RawParagraphData::try_from(&bytes[..RAW_PARAGRAPH_DATA_SIZE])?;
|
||||||
let mut offset = RAW_PARAGRAPH_DATA_SIZE;
|
let mut offset = RAW_PARAGRAPH_DATA_SIZE;
|
||||||
let mut raw_text_leaves: Vec<RawTextLeaf> = Vec::new();
|
let mut raw_text_leaves: Vec<RawTextSpan> = Vec::new();
|
||||||
|
|
||||||
for _ in 0..attrs.leaf_count {
|
for _ in 0..attrs.span_count {
|
||||||
let text_leaf = RawTextLeaf::try_from(&bytes[offset..(offset + RAW_LEAF_DATA_SIZE)])?;
|
let text_span = RawTextSpan::try_from(&bytes[offset..(offset + RAW_SPAN_DATA_SIZE)])?;
|
||||||
offset += RAW_LEAF_DATA_SIZE;
|
offset += RAW_SPAN_DATA_SIZE;
|
||||||
raw_text_leaves.push(text_leaf);
|
raw_text_leaves.push(text_span);
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_buffer = &bytes[offset..];
|
let text_buffer = &bytes[offset..];
|
||||||
@@ -230,16 +230,16 @@ impl From<RawParagraph> for shapes::Paragraph {
|
|||||||
let mut leaves = vec![];
|
let mut leaves = vec![];
|
||||||
|
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
for raw_leaf in value.leaves.into_iter() {
|
for raw_span in value.leaves.into_iter() {
|
||||||
let delta = raw_leaf.text_length as usize;
|
let delta = raw_span.text_length as usize;
|
||||||
let text_buffer = &value.text_buffer[offset..offset + delta];
|
let text_buffer = &value.text_buffer[offset..offset + delta];
|
||||||
|
|
||||||
let mut leaf = shapes::TextLeaf::from(raw_leaf);
|
let mut span = shapes::TextSpan::from(raw_span);
|
||||||
if !text_buffer.is_empty() {
|
if !text_buffer.is_empty() {
|
||||||
leaf.set_text(String::from_utf8_lossy(text_buffer).to_string());
|
span.set_text(String::from_utf8_lossy(text_buffer).to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
leaves.push(leaf);
|
leaves.push(span);
|
||||||
offset += delta;
|
offset += delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user