mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
Merge pull request #7950 from penpot/ladybenko-12851-fix-text-selection
🐛 Fix text selection when editor regains focus
This commit is contained in:
@@ -32,8 +32,8 @@
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w",
|
||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
|
||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
|
||||
"lint:clj": "clj-kondo --parallel --lint src/",
|
||||
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export class Clipboard {
|
||||
static Permission = {
|
||||
ONLY_READ: ['clipboard-read'],
|
||||
ONLY_WRITE: ['clipboard-write'],
|
||||
ALL: ['clipboard-read', 'clipboard-write']
|
||||
}
|
||||
ONLY_READ: ["clipboard-read"],
|
||||
ONLY_WRITE: ["clipboard-write"],
|
||||
ALL: ["clipboard-read", "clipboard-write"],
|
||||
};
|
||||
|
||||
static enable(context, permissions) {
|
||||
return context.grantPermissions(permissions)
|
||||
return context.grantPermissions(permissions);
|
||||
}
|
||||
|
||||
static writeText(page, text) {
|
||||
@@ -18,8 +18,8 @@ export class Clipboard {
|
||||
}
|
||||
|
||||
constructor(page, context) {
|
||||
this.page = page
|
||||
this.context = context
|
||||
this.page = page;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
enable(permissions) {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
export class Transit {
|
||||
static parse(value) {
|
||||
if (typeof value !== 'string')
|
||||
return value
|
||||
if (typeof value !== "string") return value;
|
||||
|
||||
if (value.startsWith('~'))
|
||||
return value.slice(2)
|
||||
if (value.startsWith("~")) return value.slice(2);
|
||||
|
||||
return value
|
||||
return value;
|
||||
}
|
||||
|
||||
static get(object, ...path) {
|
||||
let aux = object;
|
||||
for (const name of path) {
|
||||
if (typeof name !== 'string') {
|
||||
if (typeof name !== "string") {
|
||||
if (!(name in aux)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export class BasePage {
|
||||
*/
|
||||
static async mockRPCs(page, paths, options) {
|
||||
for (const [path, jsonFilename] of Object.entries(paths)) {
|
||||
await this.mockRPC(page, path, jsonFilename, options)
|
||||
await this.mockRPC(page, path, jsonFilename, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
||||
import { Transit } from '../../helpers/Transit';
|
||||
import { Transit } from "../../helpers/Transit";
|
||||
|
||||
export class WorkspacePage extends BaseWebSocketPage {
|
||||
static TextEditor = class TextEditor {
|
||||
|
||||
@@ -51,7 +51,7 @@ test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
||||
pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d",
|
||||
});
|
||||
|
||||
await workspacePage.page.waitForTimeout(1000)
|
||||
await workspacePage.page.waitForTimeout(1000);
|
||||
await workspacePage.waitForFirstRender();
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { Clipboard } from '../../helpers/Clipboard';
|
||||
import { Clipboard } from "../../helpers/Clipboard";
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
|
||||
const timeToWait = 100;
|
||||
@@ -11,14 +11,14 @@ test.beforeEach(async ({ page, context }) => {
|
||||
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ context}) => {
|
||||
test.afterEach(async ({ context }) => {
|
||||
context.clearPermissions();
|
||||
})
|
||||
});
|
||||
|
||||
test("Create a new text shape", async ({ page }) => {
|
||||
const initialText = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
@@ -36,10 +36,7 @@ test("Create a new text shape from pasting text", async ({ page, context }) => {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockRPC(
|
||||
"update-file?id=*",
|
||||
"text-editor/update-file.json",
|
||||
);
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
@@ -55,10 +52,13 @@ test("Create a new text shape from pasting text", async ({ page, context }) => {
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Create a new text shape from pasting text using context menu", async ({ page, context }) => {
|
||||
test("Create a new text shape from pasting text using context menu", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
@@ -72,11 +72,13 @@ test("Create a new text shape from pasting text using context menu", async ({ pa
|
||||
expect(textContent).toBe(textToPaste);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
})
|
||||
});
|
||||
|
||||
test("Update an already created text shape by appending text", async ({ page }) => {
|
||||
test("Update an already created text shape by appending text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
@@ -94,7 +96,7 @@ test("Update an already created text shape by prepending text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
@@ -112,7 +114,7 @@ test("Update an already created text shape by inserting text in between", async
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
@@ -126,10 +128,13 @@ test("Update an already created text shape by inserting text in between", async
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape appending text by pasting text", async ({ page, context }) => {
|
||||
test("Update a new text shape appending text by pasting text", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = " dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
@@ -147,11 +152,12 @@ test("Update a new text shape appending text by pasting text", async ({ page, co
|
||||
});
|
||||
|
||||
test("Update a new text shape prepending text by pasting text", async ({
|
||||
page, context
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = "Dolor sit amet ";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
@@ -173,7 +179,7 @@ test("Update a new text shape replacing (starting) text with pasted text", async
|
||||
}) => {
|
||||
const textToPaste = "Dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
@@ -197,7 +203,7 @@ test("Update a new text shape replacing (ending) text with pasted text", async (
|
||||
}) => {
|
||||
const textToPaste = "dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
@@ -221,7 +227,7 @@ test("Update a new text shape replacing (in between) text with pasted text", asy
|
||||
}) => {
|
||||
const textToPaste = "dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
@@ -244,14 +250,11 @@ test("Update text font size selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC(
|
||||
"update-file?id=*",
|
||||
"text-editor/update-file.json",
|
||||
);
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
@@ -280,7 +283,10 @@ test.skip("Update text line height selecting a part of it (starting)", async ({
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeLineHeight(1.4);
|
||||
|
||||
const lineHeight = await workspace.textEditor.waitForParagraphStyle(1, 'line-height');
|
||||
const lineHeight = await workspace.textEditor.waitForParagraphStyle(
|
||||
1,
|
||||
"line-height",
|
||||
);
|
||||
expect(lineHeight).toBe("1.4");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
|
||||
@@ -303,7 +303,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||
await nameField.pressSequentially(".changed");
|
||||
|
||||
await tokensUpdateCreateModal.getByRole("button", {name: "Save"}).click();
|
||||
await tokensUpdateCreateModal.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -498,19 +504,12 @@ export class SelectionController extends EventTarget {
|
||||
if (!this.#savedSelection) return false;
|
||||
|
||||
if (this.#savedSelection.anchorNode && this.#savedSelection.focusNode) {
|
||||
if (this.#savedSelection.anchorNode === this.#savedSelection.focusNode) {
|
||||
this.#selection.setPosition(
|
||||
this.#savedSelection.focusNode,
|
||||
this.#savedSelection.focusOffset,
|
||||
);
|
||||
} else {
|
||||
this.#selection.setBaseAndExtent(
|
||||
this.#savedSelection.anchorNode,
|
||||
this.#savedSelection.anchorOffset,
|
||||
this.#savedSelection.focusNode,
|
||||
this.#savedSelection.focusOffset,
|
||||
);
|
||||
}
|
||||
this.#selection.setBaseAndExtent(
|
||||
this.#savedSelection.anchorNode,
|
||||
this.#savedSelection.anchorOffset,
|
||||
this.#savedSelection.focusNode,
|
||||
this.#savedSelection.focusOffset,
|
||||
);
|
||||
}
|
||||
this.#savedSelection = null;
|
||||
return true;
|
||||
@@ -1132,10 +1131,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 +1399,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 +1977,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