mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
🔧 Add formatting rules to the TextEditor
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
export function addEventListeners(target, object, options) {
|
||||
Object.entries(object).forEach(([type, listener]) =>
|
||||
target.addEventListener(type, listener, options)
|
||||
target.addEventListener(type, listener, options),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ export function addEventListeners(target, object, options) {
|
||||
*/
|
||||
export function removeEventListeners(target, object) {
|
||||
Object.entries(object).forEach(([type, listener]) =>
|
||||
target.removeEventListener(type, listener)
|
||||
target.removeEventListener(type, listener),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -664,8 +664,16 @@ export class TextEditor extends EventTarget {
|
||||
* @param {boolean} allowHTMLPaste
|
||||
* @returns {Root}
|
||||
*/
|
||||
export function createRootFromHTML(html, style = undefined, allowHTMLPaste = undefined) {
|
||||
const fragment = mapContentFragmentFromHTML(html, style || undefined, allowHTMLPaste || undefined);
|
||||
export function createRootFromHTML(
|
||||
html,
|
||||
style = undefined,
|
||||
allowHTMLPaste = undefined,
|
||||
) {
|
||||
const fragment = mapContentFragmentFromHTML(
|
||||
html,
|
||||
style || undefined,
|
||||
allowHTMLPaste || undefined,
|
||||
);
|
||||
const root = createRoot([], style);
|
||||
root.replaceChildren(fragment);
|
||||
resetInertElement();
|
||||
|
||||
@@ -18,7 +18,10 @@ import { TextEditor } from "../TextEditor.js";
|
||||
* @param {DataTransfer} clipboardData
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
function getFormattedFragmentFromClipboardData(selectionController, clipboardData) {
|
||||
function getFormattedFragmentFromClipboardData(
|
||||
selectionController,
|
||||
clipboardData,
|
||||
) {
|
||||
return mapContentFragmentFromHTML(
|
||||
clipboardData.getData("text/html"),
|
||||
selectionController.currentStyle,
|
||||
@@ -79,9 +82,14 @@ export function paste(event, editor, selectionController) {
|
||||
|
||||
let fragment = null;
|
||||
if (editor?.options?.allowHTMLPaste) {
|
||||
fragment = getFormattedOrPlainFragmentFromClipboardData(event.clipboardData);
|
||||
fragment = getFormattedOrPlainFragmentFromClipboardData(
|
||||
event.clipboardData,
|
||||
);
|
||||
} else {
|
||||
fragment = getPlainFragmentFromClipboardData(selectionController, event.clipboardData);
|
||||
fragment = getPlainFragmentFromClipboardData(
|
||||
selectionController,
|
||||
event.clipboardData,
|
||||
);
|
||||
}
|
||||
|
||||
if (!fragment) {
|
||||
@@ -92,10 +100,9 @@ export function paste(event, editor, selectionController) {
|
||||
if (selectionController.isCollapsed) {
|
||||
const hasOnlyOneParagraph = fragment.children.length === 1;
|
||||
const hasOnlyOneTextSpan = fragment.firstElementChild.children.length === 1;
|
||||
const forceTextSpan = fragment.firstElementChild.dataset.textSpan === "force";
|
||||
if (hasOnlyOneParagraph
|
||||
&& hasOnlyOneTextSpan
|
||||
&& forceTextSpan) {
|
||||
const forceTextSpan =
|
||||
fragment.firstElementChild.dataset.textSpan === "force";
|
||||
if (hasOnlyOneParagraph && hasOnlyOneTextSpan && forceTextSpan) {
|
||||
selectionController.insertIntoFocus(fragment.textContent);
|
||||
} else {
|
||||
selectionController.insertPaste(fragment);
|
||||
@@ -103,10 +110,9 @@ export function paste(event, editor, selectionController) {
|
||||
} else {
|
||||
const hasOnlyOneParagraph = fragment.children.length === 1;
|
||||
const hasOnlyOneTextSpan = fragment.firstElementChild.children.length === 1;
|
||||
const forceTextSpan = fragment.firstElementChild.dataset.textSpan === "force";
|
||||
if (hasOnlyOneParagraph
|
||||
&& hasOnlyOneTextSpan
|
||||
&& forceTextSpan) {
|
||||
const forceTextSpan =
|
||||
fragment.firstElementChild.dataset.textSpan === "force";
|
||||
if (hasOnlyOneParagraph && hasOnlyOneTextSpan && forceTextSpan) {
|
||||
selectionController.replaceText(fragment.textContent);
|
||||
} else {
|
||||
selectionController.replaceWithPaste(fragment);
|
||||
|
||||
@@ -23,7 +23,7 @@ export function deleteContentBackward(event, editor, selectionController) {
|
||||
// If not is collapsed AKA is a selection, then
|
||||
// we removeSelected.
|
||||
if (!selectionController.isCollapsed) {
|
||||
return selectionController.removeSelected({ direction: 'backward' });
|
||||
return selectionController.removeSelected({ direction: "backward" });
|
||||
}
|
||||
|
||||
// If we're in a text node and the offset is
|
||||
@@ -32,18 +32,18 @@ export function deleteContentBackward(event, editor, selectionController) {
|
||||
if (selectionController.isTextFocus && selectionController.focusOffset > 0) {
|
||||
return selectionController.removeBackwardText();
|
||||
|
||||
// If we're in a text node but we're at the end of the
|
||||
// paragraph, we should merge the current paragraph
|
||||
// with the following paragraph.
|
||||
// If we're in a text node but we're at the end of the
|
||||
// paragraph, we should merge the current paragraph
|
||||
// with the following paragraph.
|
||||
} else if (
|
||||
selectionController.isTextFocus &&
|
||||
selectionController.focusAtStart
|
||||
) {
|
||||
return selectionController.mergeBackwardParagraph();
|
||||
|
||||
// If we're at an text span or a line break paragraph
|
||||
// and there's more than one paragraph, then we should
|
||||
// remove the next paragraph.
|
||||
// If we're at an text span or a line break paragraph
|
||||
// and there's more than one paragraph, then we should
|
||||
// remove the next paragraph.
|
||||
} else if (
|
||||
selectionController.isTextSpanFocus ||
|
||||
selectionController.isLineBreakFocus
|
||||
|
||||
@@ -28,22 +28,21 @@ export function deleteContentForward(event, editor, selectionController) {
|
||||
// If we're in a text node and the offset is
|
||||
// greater than 0 (not at the start of the text span)
|
||||
// we simple remove a character from the text.
|
||||
if (selectionController.isTextFocus
|
||||
&& selectionController.focusAtEnd) {
|
||||
if (selectionController.isTextFocus && selectionController.focusAtEnd) {
|
||||
return selectionController.mergeForwardParagraph();
|
||||
|
||||
// If we're in a text node but we're at the end of the
|
||||
// paragraph, we should merge the current paragraph
|
||||
// with the following paragraph.
|
||||
// If we're in a text node but we're at the end of the
|
||||
// paragraph, we should merge the current paragraph
|
||||
// with the following paragraph.
|
||||
} else if (
|
||||
selectionController.isTextFocus &&
|
||||
selectionController.focusOffset >= 0
|
||||
) {
|
||||
return selectionController.removeForwardText();
|
||||
|
||||
// If we're at a text span or a line break paragraph
|
||||
// and there's more than one paragraph, then we should
|
||||
// remove the next paragraph.
|
||||
// If we're at a text span or a line break paragraph
|
||||
// and there's more than one paragraph, then we should
|
||||
// remove the next paragraph.
|
||||
} else if (
|
||||
(selectionController.isTextSpanFocus ||
|
||||
selectionController.isLineBreakFocus) &&
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { describe, test, expect } from 'vitest'
|
||||
import { insertInto, removeBackward, removeForward, replaceWith } from './Text';
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { insertInto, removeBackward, removeForward, replaceWith } from "./Text";
|
||||
|
||||
describe("Text", () => {
|
||||
test("* should throw when passed wrong parameters", () => {
|
||||
expect(() => insertInto(Infinity, Infinity, Infinity)).toThrowError('Invalid string');
|
||||
expect(() => insertInto('Hello', Infinity, Infinity)).toThrowError('Invalid offset');
|
||||
expect(() => insertInto('Hello', 0, Infinity)).toThrowError('Invalid string');
|
||||
expect(() => insertInto(Infinity, Infinity, Infinity)).toThrowError(
|
||||
"Invalid string",
|
||||
);
|
||||
expect(() => insertInto("Hello", Infinity, Infinity)).toThrowError(
|
||||
"Invalid offset",
|
||||
);
|
||||
expect(() => insertInto("Hello", 0, Infinity)).toThrowError(
|
||||
"Invalid string",
|
||||
);
|
||||
});
|
||||
|
||||
test("`insertInto` should insert a string into an offset", () => {
|
||||
@@ -13,7 +19,9 @@ describe("Text", () => {
|
||||
});
|
||||
|
||||
test("`replaceWith` should replace a string into a string", () => {
|
||||
expect(replaceWith("Hello, Something!", 7, 16, "World")).toBe("Hello, World!");
|
||||
expect(replaceWith("Hello, Something!", 7, 16, "World")).toBe(
|
||||
"Hello, World!",
|
||||
);
|
||||
});
|
||||
|
||||
test("`removeBackward` should remove string backward from start (offset 0)", () => {
|
||||
@@ -26,13 +34,13 @@ describe("Text", () => {
|
||||
|
||||
test("`removeBackward` should remove string backward from end", () => {
|
||||
expect(removeBackward("Hello, World!", "Hello, World!".length)).toBe(
|
||||
"Hello, World"
|
||||
"Hello, World",
|
||||
);
|
||||
});
|
||||
|
||||
test("`removeForward` should remove string forward from end", () => {
|
||||
expect(removeForward("Hello, World!", "Hello, World!".length)).toBe(
|
||||
"Hello, World!"
|
||||
"Hello, World!",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ function getContext() {
|
||||
if (!context) {
|
||||
context = canvas.getContext("2d");
|
||||
}
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -230,15 +230,10 @@ export function mapContentFragmentFromString(string, styleDefaults) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const line of lines) {
|
||||
if (line === "") {
|
||||
fragment.appendChild(
|
||||
createEmptyParagraph(styleDefaults)
|
||||
);
|
||||
fragment.appendChild(createEmptyParagraph(styleDefaults));
|
||||
} else {
|
||||
const textSpan = createTextSpan(new Text(line), styleDefaults);
|
||||
const paragraph = createParagraph(
|
||||
[textSpan],
|
||||
styleDefaults,
|
||||
);
|
||||
const paragraph = createParagraph([textSpan], styleDefaults);
|
||||
if (lines.length === 1) {
|
||||
paragraph.dataset.textSpan = "force";
|
||||
}
|
||||
|
||||
@@ -112,7 +112,11 @@ describe("Paragraph", () => {
|
||||
const helloTextSpan = createTextSpan(new Text("Hello, "));
|
||||
const worldTextSpan = createTextSpan(new Text("World"));
|
||||
const exclTextSpan = createTextSpan(new Text("!"));
|
||||
const paragraph = createParagraph([helloTextSpan, worldTextSpan, exclTextSpan]);
|
||||
const paragraph = createParagraph([
|
||||
helloTextSpan,
|
||||
worldTextSpan,
|
||||
exclTextSpan,
|
||||
]);
|
||||
const newParagraph = splitParagraphAtNode(paragraph, 1);
|
||||
expect(newParagraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(newParagraph.nodeName).toBe(TAG);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { createEmptyRoot, createRoot, setRootStyles, TAG, TYPE } from "./Root.js";
|
||||
import {
|
||||
createEmptyRoot,
|
||||
createRoot,
|
||||
setRootStyles,
|
||||
TAG,
|
||||
TYPE,
|
||||
} from "./Root.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("Root", () => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Copyright (c) KALEIDOS INC
|
||||
*/
|
||||
|
||||
import StyleDeclaration from '../../controllers/StyleDeclaration.js';
|
||||
import StyleDeclaration from "../../controllers/StyleDeclaration.js";
|
||||
import { getFills } from "./Color.js";
|
||||
|
||||
const DEFAULT_FONT_SIZE = "16px";
|
||||
@@ -339,8 +339,7 @@ export function setStylesFromObject(element, allowedStyles, styleObject) {
|
||||
continue;
|
||||
}
|
||||
let styleValue = styleObject[styleName];
|
||||
if (!styleValue)
|
||||
continue;
|
||||
if (!styleValue) continue;
|
||||
|
||||
if (styleName === "font-family") {
|
||||
styleValue = sanitizeFontFamily(styleValue);
|
||||
@@ -388,8 +387,10 @@ export function setStylesFromDeclaration(
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function setStyles(element, allowedStyles, styleObjectOrDeclaration) {
|
||||
if (styleObjectOrDeclaration instanceof CSSStyleDeclaration
|
||||
|| styleObjectOrDeclaration instanceof StyleDeclaration) {
|
||||
if (
|
||||
styleObjectOrDeclaration instanceof CSSStyleDeclaration ||
|
||||
styleObjectOrDeclaration instanceof StyleDeclaration
|
||||
) {
|
||||
return setStylesFromDeclaration(
|
||||
element,
|
||||
allowedStyles,
|
||||
|
||||
@@ -22,8 +22,7 @@ import { isRoot } from "./Root.js";
|
||||
*/
|
||||
export function isTextNode(node) {
|
||||
if (!node) throw new TypeError("Invalid text node");
|
||||
return node.nodeType === Node.TEXT_NODE
|
||||
|| isLineBreak(node);
|
||||
return node.nodeType === Node.TEXT_NODE || isLineBreak(node);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,8 +32,7 @@ export function isTextNode(node) {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEmptyTextNode(node) {
|
||||
return node.nodeType === Node.TEXT_NODE
|
||||
&& node.nodeValue === "";
|
||||
return node.nodeType === Node.TEXT_NODE && node.nodeValue === "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Copyright (c) KALEIDOS INC
|
||||
*/
|
||||
|
||||
import SafeGuard from '../../controllers/SafeGuard.js';
|
||||
import SafeGuard from "../../controllers/SafeGuard.js";
|
||||
|
||||
/**
|
||||
* Iterator direction.
|
||||
@@ -58,7 +58,7 @@ export class TextNodeIterator {
|
||||
startNode,
|
||||
rootNode,
|
||||
skipNodes = new Set(),
|
||||
direction = TextNodeIteratorDirection.FORWARD
|
||||
direction = TextNodeIteratorDirection.FORWARD,
|
||||
) {
|
||||
if (startNode === rootNode) {
|
||||
return TextNodeIterator.findDown(
|
||||
@@ -67,7 +67,7 @@ export class TextNodeIterator {
|
||||
: startNode.lastChild,
|
||||
rootNode,
|
||||
skipNodes,
|
||||
direction
|
||||
direction,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export class TextNodeIterator {
|
||||
: currentNode.lastChild,
|
||||
rootNode,
|
||||
skipNodes,
|
||||
direction
|
||||
direction,
|
||||
);
|
||||
}
|
||||
currentNode =
|
||||
@@ -119,7 +119,7 @@ export class TextNodeIterator {
|
||||
startNode,
|
||||
rootNode,
|
||||
backTrack = new Set(),
|
||||
direction = TextNodeIteratorDirection.FORWARD
|
||||
direction = TextNodeIteratorDirection.FORWARD,
|
||||
) {
|
||||
backTrack.add(startNode);
|
||||
if (TextNodeIterator.isTextNode(startNode)) {
|
||||
@@ -127,14 +127,14 @@ export class TextNodeIterator {
|
||||
startNode.parentNode,
|
||||
rootNode,
|
||||
backTrack,
|
||||
direction
|
||||
direction,
|
||||
);
|
||||
} else if (TextNodeIterator.isContainerNode(startNode)) {
|
||||
const found = TextNodeIterator.findDown(
|
||||
startNode,
|
||||
rootNode,
|
||||
backTrack,
|
||||
direction
|
||||
direction,
|
||||
);
|
||||
if (found) {
|
||||
return found;
|
||||
@@ -144,7 +144,7 @@ export class TextNodeIterator {
|
||||
startNode.parentNode,
|
||||
rootNode,
|
||||
backTrack,
|
||||
direction
|
||||
direction,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export class TextNodeIterator {
|
||||
this.#currentNode,
|
||||
this.#rootNode,
|
||||
new Set(),
|
||||
TextNodeIteratorDirection.FORWARD
|
||||
TextNodeIteratorDirection.FORWARD,
|
||||
);
|
||||
|
||||
if (!nextNode) {
|
||||
@@ -237,7 +237,7 @@ export class TextNodeIterator {
|
||||
this.#currentNode,
|
||||
this.#rootNode,
|
||||
new Set(),
|
||||
TextNodeIteratorDirection.BACKWARD
|
||||
TextNodeIteratorDirection.BACKWARD,
|
||||
);
|
||||
|
||||
if (!previousNode) {
|
||||
@@ -270,10 +270,8 @@ export class TextNodeIterator {
|
||||
* @param {TextNode} endNode
|
||||
* @yields {TextNode}
|
||||
*/
|
||||
* iterateFrom(startNode, endNode) {
|
||||
const comparedPosition = startNode.compareDocumentPosition(
|
||||
endNode
|
||||
);
|
||||
*iterateFrom(startNode, endNode) {
|
||||
const comparedPosition = startNode.compareDocumentPosition(endNode);
|
||||
this.#currentNode = startNode;
|
||||
SafeGuard.start();
|
||||
while (this.#currentNode !== endNode) {
|
||||
|
||||
@@ -38,7 +38,7 @@ export class ChangeController extends EventTarget {
|
||||
* @param {number} [time=500]
|
||||
*/
|
||||
constructor(time = 500) {
|
||||
super()
|
||||
super();
|
||||
if (typeof time === "number" && (!Number.isInteger(time) || time <= 0)) {
|
||||
throw new TypeError("Invalid time");
|
||||
}
|
||||
|
||||
@@ -24,19 +24,19 @@ export function start() {
|
||||
*/
|
||||
export function update() {
|
||||
if (Date.now - startTime >= SAFE_GUARD_TIME) {
|
||||
throw new Error('Safe guard timeout');
|
||||
throw new Error("Safe guard timeout");
|
||||
}
|
||||
}
|
||||
|
||||
let timeoutId = 0
|
||||
let timeoutId = 0;
|
||||
export function throwAfter(error, timeout = SAFE_GUARD_TIME) {
|
||||
timeoutId = setTimeout(() => {
|
||||
throw error
|
||||
}, timeout)
|
||||
throw error;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
export function throwCancel() {
|
||||
clearTimeout(timeoutId)
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -54,7 +54,7 @@ import { isRoot, setRootStyles } from "../content/dom/Root.js";
|
||||
import { SelectionDirection } from "./SelectionDirection.js";
|
||||
import SafeGuard from "./SafeGuard.js";
|
||||
import { sanitizeFontFamily } from "../content/dom/Style.js";
|
||||
import StyleDeclaration from './StyleDeclaration.js';
|
||||
import StyleDeclaration from "./StyleDeclaration.js";
|
||||
|
||||
/**
|
||||
* Supported options for the SelectionController.
|
||||
@@ -280,11 +280,17 @@ export class SelectionController extends EventTarget {
|
||||
// FIXME: I don't like this approximation. Having to iterate nodes twice
|
||||
// is bad for performance. I think we need another way of "computing"
|
||||
// the cascade.
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(startNode, endNode)) {
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const paragraph = textNode.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(paragraph);
|
||||
}
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(startNode, endNode)) {
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const textSpan = textNode.parentElement;
|
||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||
}
|
||||
@@ -1132,10 +1138,7 @@ export class SelectionController extends EventTarget {
|
||||
const hasOnlyOneParagraph = fragment.children.length === 1;
|
||||
const forceTextSpan =
|
||||
fragment.firstElementChild?.dataset?.textSpan === "force";
|
||||
if (
|
||||
hasOnlyOneParagraph &&
|
||||
forceTextSpan
|
||||
) {
|
||||
if (hasOnlyOneParagraph && forceTextSpan) {
|
||||
// first text span
|
||||
const collapseNode = fragment.firstElementChild.firstElementChild;
|
||||
if (this.isTextSpanStart) {
|
||||
@@ -1403,7 +1406,7 @@ export class SelectionController extends EventTarget {
|
||||
// the focus node is a <span>.
|
||||
if (isTextSpan(this.focusNode)) {
|
||||
this.focusNode.firstElementChild.replaceWith(textNode);
|
||||
// the focus node is a <br>.
|
||||
// the focus node is a <br>.
|
||||
} else {
|
||||
this.focusNode.replaceWith(textNode);
|
||||
}
|
||||
@@ -1981,8 +1984,7 @@ export class SelectionController extends EventTarget {
|
||||
this.setSelection(newTextSpan.firstChild, 0, newTextSpan.firstChild, 0);
|
||||
}
|
||||
// The styles are applied to the paragraph
|
||||
else
|
||||
{
|
||||
else {
|
||||
const paragraph = this.startParagraph;
|
||||
setParagraphStyles(paragraph, newStyles);
|
||||
// Apply styles to child text spans.
|
||||
|
||||
@@ -278,9 +278,9 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Hello",
|
||||
);
|
||||
expect(
|
||||
textEditorMock.root.lastChild.firstChild.firstChild.nodeValue,
|
||||
).toBe(", World!");
|
||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||
", World!",
|
||||
);
|
||||
});
|
||||
|
||||
test("`insertPaste` should insert a paragraph from a pasted fragment (at middle)", () => {
|
||||
@@ -292,7 +292,12 @@ describe("SelectionController", () => {
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstChild.firstChild.firstChild,
|
||||
"Lorem ".length,
|
||||
);
|
||||
const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(paragraph);
|
||||
@@ -315,9 +320,9 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Lorem ",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1).firstChild.firstChild.nodeValue).toBe(
|
||||
"ipsum ",
|
||||
);
|
||||
expect(
|
||||
textEditorMock.root.children.item(1).firstChild.firstChild.nodeValue,
|
||||
).toBe("ipsum ");
|
||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"dolor",
|
||||
);
|
||||
@@ -359,25 +364,21 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Hello",
|
||||
);
|
||||
expect(
|
||||
textEditorMock.root.lastChild.firstChild.firstChild.nodeValue,
|
||||
).toBe(", World!");
|
||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||
", World!",
|
||||
);
|
||||
});
|
||||
|
||||
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 selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstChild.firstChild.firstChild,
|
||||
0,
|
||||
);
|
||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
||||
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
||||
paragraph.dataset.textSpan = "force";
|
||||
const fragment = document.createDocumentFragment();
|
||||
@@ -415,7 +416,12 @@ describe("SelectionController", () => {
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, "Lorem ".length);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstChild.firstChild.firstChild,
|
||||
"Lorem ".length,
|
||||
);
|
||||
const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
||||
paragraph.dataset.textSpan = "force";
|
||||
const fragment = document.createDocumentFragment();
|
||||
@@ -439,9 +445,9 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Lorem ",
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue).toBe(
|
||||
"ipsum ",
|
||||
);
|
||||
expect(
|
||||
textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue,
|
||||
).toBe("ipsum ");
|
||||
expect(
|
||||
textEditorMock.root.firstChild.children.item(2).firstChild.nodeValue,
|
||||
).toBe("dolor");
|
||||
@@ -461,9 +467,7 @@ describe("SelectionController", () => {
|
||||
root.firstChild.firstChild.firstChild,
|
||||
"Hello".length,
|
||||
);
|
||||
const paragraph = createParagraph([
|
||||
createTextSpan(new Text(", World!"))
|
||||
]);
|
||||
const paragraph = createParagraph([createTextSpan(new Text(", World!"))]);
|
||||
paragraph.dataset.textSpan = "force";
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(paragraph);
|
||||
@@ -486,9 +490,9 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Hello",
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue).toBe(
|
||||
", World!",
|
||||
);
|
||||
expect(
|
||||
textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue,
|
||||
).toBe(", World!");
|
||||
});
|
||||
|
||||
test("`removeBackwardText` should remove text in backward direction (backspace)", () => {
|
||||
|
||||
@@ -77,7 +77,10 @@ export class StyleDeclaration {
|
||||
const currentValue = this.getPropertyValue(name);
|
||||
if (this.#isQuotedValue(currentValue, value)) {
|
||||
return this.setProperty(name, value);
|
||||
} else if (currentValue === "" && value === StyleDeclaration.Property.NULL) {
|
||||
} else if (
|
||||
currentValue === "" &&
|
||||
value === StyleDeclaration.Property.NULL
|
||||
) {
|
||||
return this.setProperty(name, value);
|
||||
} else if (currentValue === "" && ["initial", "none"].includes(value)) {
|
||||
return this.setProperty(name, value);
|
||||
@@ -107,4 +110,4 @@ export class StyleDeclaration {
|
||||
}
|
||||
}
|
||||
|
||||
export default StyleDeclaration
|
||||
export default StyleDeclaration;
|
||||
|
||||
@@ -43,33 +43,38 @@ export class SelectionControllerDebug {
|
||||
this.#elements.isParagraphStart.checked =
|
||||
selectionController.isParagraphStart;
|
||||
this.#elements.isParagraphEnd.checked = selectionController.isParagraphEnd;
|
||||
this.#elements.isTextSpanStart.checked = selectionController.isTextSpanStart;
|
||||
this.#elements.isTextSpanStart.checked =
|
||||
selectionController.isTextSpanStart;
|
||||
this.#elements.isTextSpanEnd.checked = selectionController.isTextSpanEnd;
|
||||
this.#elements.isTextAnchor.checked = selectionController.isTextAnchor;
|
||||
this.#elements.isTextFocus.checked = selectionController.isTextFocus;
|
||||
this.#elements.focusNode.value = this.getNodeDescription(
|
||||
selectionController.focusNode,
|
||||
selectionController.focusOffset
|
||||
selectionController.focusOffset,
|
||||
);
|
||||
this.#elements.focusOffset.value = selectionController.focusOffset;
|
||||
this.#elements.anchorNode.value = this.getNodeDescription(
|
||||
selectionController.anchorNode,
|
||||
selectionController.anchorOffset
|
||||
selectionController.anchorOffset,
|
||||
);
|
||||
this.#elements.anchorOffset.value = selectionController.anchorOffset;
|
||||
this.#elements.focusTextSpan.value = this.getNodeDescription(
|
||||
selectionController.focusTextSpan
|
||||
selectionController.focusTextSpan,
|
||||
);
|
||||
this.#elements.anchorTextSpan.value = this.getNodeDescription(
|
||||
selectionController.anchorTextSpan
|
||||
selectionController.anchorTextSpan,
|
||||
);
|
||||
this.#elements.focusParagraph.value = this.getNodeDescription(
|
||||
selectionController.focusParagraph
|
||||
selectionController.focusParagraph,
|
||||
);
|
||||
this.#elements.anchorParagraph.value = this.getNodeDescription(
|
||||
selectionController.anchorParagraph
|
||||
selectionController.anchorParagraph,
|
||||
);
|
||||
this.#elements.startContainer.value = this.getNodeDescription(
|
||||
selectionController.startContainer,
|
||||
);
|
||||
this.#elements.endContainer.value = this.getNodeDescription(
|
||||
selectionController.endContainer,
|
||||
);
|
||||
this.#elements.startContainer.value = this.getNodeDescription(selectionController.startContainer);
|
||||
this.#elements.endContainer.value = this.getNodeDescription(selectionController.endContainer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,7 @@ export class Point {
|
||||
}
|
||||
|
||||
polar(angle, length = 1.0) {
|
||||
return this.set(
|
||||
Math.cos(angle) * length,
|
||||
Math.sin(angle) * length
|
||||
);
|
||||
return this.set(Math.cos(angle) * length, Math.sin(angle) * length);
|
||||
}
|
||||
|
||||
add({ x, y }) {
|
||||
@@ -119,10 +116,7 @@ export class Point {
|
||||
|
||||
export class Rect {
|
||||
static create(x, y, width, height) {
|
||||
return new Rect(
|
||||
new Point(width, height),
|
||||
new Point(x, y),
|
||||
);
|
||||
return new Rect(new Point(width, height), new Point(x, y));
|
||||
}
|
||||
|
||||
#size;
|
||||
@@ -228,10 +222,7 @@ export class Rect {
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Rect(
|
||||
this.#size.clone(),
|
||||
this.#position.clone(),
|
||||
);
|
||||
return new Rect(this.#size.clone(), this.#position.clone());
|
||||
}
|
||||
|
||||
toFixed(fractionDigits = 0) {
|
||||
|
||||
@@ -82,13 +82,13 @@ export class Shape {
|
||||
}
|
||||
|
||||
get rotation() {
|
||||
return this.#rotation
|
||||
return this.#rotation;
|
||||
}
|
||||
|
||||
set rotation(newRotation) {
|
||||
if (!Number.isFinite(newRotation)) {
|
||||
throw new TypeError('Invalid rotation')
|
||||
throw new TypeError("Invalid rotation");
|
||||
}
|
||||
this.#rotation = newRotation
|
||||
this.#rotation = newRotation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ export function fromStyle(style) {
|
||||
const entry = Object.entries(this).find(([name, value]) =>
|
||||
name === fromStyleValue(style) ? value : 0,
|
||||
);
|
||||
if (!entry)
|
||||
return;
|
||||
if (!entry) return;
|
||||
|
||||
const [name] = entry;
|
||||
return name;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Point } from './geom';
|
||||
import { Point } from "./geom";
|
||||
|
||||
export class Viewport {
|
||||
#zoom;
|
||||
@@ -38,7 +38,7 @@ export class Viewport {
|
||||
}
|
||||
|
||||
pan(dx, dy) {
|
||||
this.#position.x += dx / this.#zoom
|
||||
this.#position.y += dy / this.#zoom
|
||||
this.#position.x += dx / this.#zoom;
|
||||
this.#position.y += dy / this.#zoom;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createRoot } from "../editor/content/dom/Root.js";
|
||||
import { createParagraph } from "../editor/content/dom/Paragraph.js";
|
||||
import { createEmptyTextSpan, createTextSpan } from "../editor/content/dom/TextSpan.js";
|
||||
import {
|
||||
createEmptyTextSpan,
|
||||
createTextSpan,
|
||||
} from "../editor/content/dom/TextSpan.js";
|
||||
import { createLineBreak } from "../editor/content/dom/LineBreak.js";
|
||||
|
||||
export class TextEditorMock extends EventTarget {
|
||||
@@ -38,14 +41,14 @@ export class TextEditorMock extends EventTarget {
|
||||
static createTextEditorMockWithRoot(root) {
|
||||
const container = TextEditorMock.getTemplate();
|
||||
const selectionImposterElement = container.querySelector(
|
||||
".text-editor-selection-imposter"
|
||||
".text-editor-selection-imposter",
|
||||
);
|
||||
const textEditorMock = new TextEditorMock(
|
||||
container.querySelector(".text-editor-content"),
|
||||
{
|
||||
root,
|
||||
selectionImposterElement,
|
||||
}
|
||||
},
|
||||
);
|
||||
return textEditorMock;
|
||||
}
|
||||
@@ -86,8 +89,8 @@ export class TextEditorMock extends EventTarget {
|
||||
return this.createTextEditorMockWithParagraphs([
|
||||
createParagraph([
|
||||
text.length === 0
|
||||
? createEmptyTextSpan()
|
||||
: createTextSpan(new Text(text))
|
||||
? createEmptyTextSpan()
|
||||
: createTextSpan(new Text(text)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
@@ -100,7 +103,9 @@ export class TextEditorMock extends EventTarget {
|
||||
* @returns
|
||||
*/
|
||||
static createTextEditorMockWithParagraph(textSpans) {
|
||||
return this.createTextEditorMockWithParagraphs([createParagraph(textSpans)]);
|
||||
return this.createTextEditorMockWithParagraphs([
|
||||
createParagraph(textSpans),
|
||||
]);
|
||||
}
|
||||
|
||||
#element = null;
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
import path from "node:path";
|
||||
import fs from 'node:fs/promises';
|
||||
import fs from "node:fs/promises";
|
||||
import { defineConfig } from "vite";
|
||||
import { coverageConfigDefaults } from "vitest/config";
|
||||
|
||||
async function waitFor(timeInMillis) {
|
||||
return new Promise(resolve =>
|
||||
setTimeout(_ => resolve(), timeInMillis)
|
||||
);
|
||||
return new Promise((resolve) => setTimeout((_) => resolve(), timeInMillis));
|
||||
}
|
||||
|
||||
const wasmWatcherPlugin = (options = {}) => {
|
||||
return {
|
||||
name: "vite-wasm-watcher-plugin",
|
||||
configureServer(server) {
|
||||
server.watcher.add("../resources/public/js/render_wasm.wasm")
|
||||
server.watcher.add("../resources/public/js/render_wasm.js")
|
||||
server.watcher.add("../resources/public/js/render_wasm.wasm");
|
||||
server.watcher.add("../resources/public/js/render_wasm.js");
|
||||
server.watcher.on("change", async (file) => {
|
||||
if (file.includes("../resources/")) {
|
||||
// If we copy the files immediately, we end
|
||||
// up with an empty .js file (I don't know why).
|
||||
await waitFor(100)
|
||||
await waitFor(100);
|
||||
// copy files.
|
||||
await fs.copyFile(
|
||||
path.resolve(file),
|
||||
path.resolve('./src/wasm/', path.basename(file))
|
||||
)
|
||||
path.resolve("./src/wasm/", path.basename(file)),
|
||||
);
|
||||
console.log(`${file} changed`);
|
||||
}
|
||||
});
|
||||
@@ -49,9 +47,7 @@ const wasmWatcherPlugin = (options = {}) => {
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
wasmWatcherPlugin()
|
||||
],
|
||||
plugins: [wasmWatcherPlugin()],
|
||||
root: "./src",
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user