mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 19:06:18 +00:00
Merge pull request #7950 from penpot/ladybenko-12851-fix-text-selection
🐛 Fix text selection when editor regains focus
This commit is contained in:
commit
c6aa9cc4b7
@ -32,8 +32,8 @@
|
|||||||
"e2e:server": "node ./scripts/e2e-server.js",
|
"e2e:server": "node ./scripts/e2e-server.js",
|
||||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||||
"fmt:clj:check": "cljfmt check --parallel=false 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": "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",
|
"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:clj": "clj-kondo --parallel --lint src/",
|
||||||
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
||||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
export class Clipboard {
|
export class Clipboard {
|
||||||
static Permission = {
|
static Permission = {
|
||||||
ONLY_READ: ['clipboard-read'],
|
ONLY_READ: ["clipboard-read"],
|
||||||
ONLY_WRITE: ['clipboard-write'],
|
ONLY_WRITE: ["clipboard-write"],
|
||||||
ALL: ['clipboard-read', 'clipboard-write']
|
ALL: ["clipboard-read", "clipboard-write"],
|
||||||
}
|
};
|
||||||
|
|
||||||
static enable(context, permissions) {
|
static enable(context, permissions) {
|
||||||
return context.grantPermissions(permissions)
|
return context.grantPermissions(permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
static writeText(page, text) {
|
static writeText(page, text) {
|
||||||
@ -18,8 +18,8 @@ export class Clipboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(page, context) {
|
constructor(page, context) {
|
||||||
this.page = page
|
this.page = page;
|
||||||
this.context = context
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
enable(permissions) {
|
enable(permissions) {
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
export class Transit {
|
export class Transit {
|
||||||
static parse(value) {
|
static parse(value) {
|
||||||
if (typeof value !== 'string')
|
if (typeof value !== "string") return value;
|
||||||
return value
|
|
||||||
|
|
||||||
if (value.startsWith('~'))
|
if (value.startsWith("~")) return value.slice(2);
|
||||||
return value.slice(2)
|
|
||||||
|
|
||||||
return value
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get(object, ...path) {
|
static get(object, ...path) {
|
||||||
let aux = object;
|
let aux = object;
|
||||||
for (const name of path) {
|
for (const name of path) {
|
||||||
if (typeof name !== 'string') {
|
if (typeof name !== "string") {
|
||||||
if (!(name in aux)) {
|
if (!(name in aux)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export class BasePage {
|
|||||||
*/
|
*/
|
||||||
static async mockRPCs(page, paths, options) {
|
static async mockRPCs(page, paths, options) {
|
||||||
for (const [path, jsonFilename] of Object.entries(paths)) {
|
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 { expect } from "@playwright/test";
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from "node:fs/promises";
|
||||||
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
||||||
import { Transit } from '../../helpers/Transit';
|
import { Transit } from "../../helpers/Transit";
|
||||||
|
|
||||||
export class WorkspacePage extends BaseWebSocketPage {
|
export class WorkspacePage extends BaseWebSocketPage {
|
||||||
static TextEditor = class TextEditor {
|
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",
|
pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d",
|
||||||
});
|
});
|
||||||
|
|
||||||
await workspacePage.page.waitForTimeout(1000)
|
await workspacePage.page.waitForTimeout(1000);
|
||||||
await workspacePage.waitForFirstRender();
|
await workspacePage.waitForFirstRender();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { Clipboard } from '../../helpers/Clipboard';
|
import { Clipboard } from "../../helpers/Clipboard";
|
||||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||||
|
|
||||||
const timeToWait = 100;
|
const timeToWait = 100;
|
||||||
@ -11,14 +11,14 @@ test.beforeEach(async ({ page, context }) => {
|
|||||||
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach(async ({ context}) => {
|
test.afterEach(async ({ context }) => {
|
||||||
context.clearPermissions();
|
context.clearPermissions();
|
||||||
})
|
});
|
||||||
|
|
||||||
test("Create a new text shape", async ({ page }) => {
|
test("Create a new text shape", async ({ page }) => {
|
||||||
const initialText = "Lorem ipsum";
|
const initialText = "Lorem ipsum";
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.goToWorkspace();
|
await workspace.goToWorkspace();
|
||||||
@ -36,10 +36,7 @@ test("Create a new text shape from pasting text", async ({ page, context }) => {
|
|||||||
textEditor: true,
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockRPC(
|
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||||
"update-file?id=*",
|
|
||||||
"text-editor/update-file.json",
|
|
||||||
);
|
|
||||||
await workspace.goToWorkspace();
|
await workspace.goToWorkspace();
|
||||||
|
|
||||||
await Clipboard.writeText(page, textToPaste);
|
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();
|
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 textToPaste = "Lorem ipsum";
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.goToWorkspace();
|
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);
|
expect(textContent).toBe(textToPaste);
|
||||||
|
|
||||||
await workspace.textEditor.stopEditing();
|
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, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
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,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
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,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
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();
|
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 textToPaste = " dolor sit amet";
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
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 ({
|
test("Update a new text shape prepending text by pasting text", async ({
|
||||||
page, context
|
page,
|
||||||
|
context,
|
||||||
}) => {
|
}) => {
|
||||||
const textToPaste = "Dolor sit amet ";
|
const textToPaste = "Dolor sit amet ";
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
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 textToPaste = "Dolor sit amet";
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
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 textToPaste = "dolor sit amet";
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
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 textToPaste = "dolor sit amet";
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
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,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const workspace = new WorkspacePage(page, {
|
const workspace = new WorkspacePage(page, {
|
||||||
textEditor: true
|
textEditor: true,
|
||||||
});
|
});
|
||||||
await workspace.setupEmptyFile();
|
await workspace.setupEmptyFile();
|
||||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||||
await workspace.mockRPC(
|
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||||
"update-file?id=*",
|
|
||||||
"text-editor/update-file.json",
|
|
||||||
);
|
|
||||||
await workspace.goToWorkspace();
|
await workspace.goToWorkspace();
|
||||||
await workspace.clickLeafLayer("Lorem ipsum");
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
await workspace.textEditor.startEditing();
|
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.selectFromStart(5);
|
||||||
await workspace.textEditor.changeLineHeight(1.4);
|
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");
|
expect(lineHeight).toBe("1.4");
|
||||||
|
|
||||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||||
|
|||||||
@ -303,7 +303,7 @@ test.describe("Tokens: Tokens Tab", () => {
|
|||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
await nameField.pressSequentially(".changed");
|
await nameField.pressSequentially(".changed");
|
||||||
|
|
||||||
await tokensUpdateCreateModal.getByRole("button", {name: "Save"}).click();
|
await tokensUpdateCreateModal.getByRole("button", { name: "Save" }).click();
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
export function addEventListeners(target, object, options) {
|
export function addEventListeners(target, object, options) {
|
||||||
Object.entries(object).forEach(([type, listener]) =>
|
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) {
|
export function removeEventListeners(target, object) {
|
||||||
Object.entries(object).forEach(([type, listener]) =>
|
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
|
* @param {boolean} allowHTMLPaste
|
||||||
* @returns {Root}
|
* @returns {Root}
|
||||||
*/
|
*/
|
||||||
export function createRootFromHTML(html, style = undefined, allowHTMLPaste = undefined) {
|
export function createRootFromHTML(
|
||||||
const fragment = mapContentFragmentFromHTML(html, style || undefined, allowHTMLPaste || undefined);
|
html,
|
||||||
|
style = undefined,
|
||||||
|
allowHTMLPaste = undefined,
|
||||||
|
) {
|
||||||
|
const fragment = mapContentFragmentFromHTML(
|
||||||
|
html,
|
||||||
|
style || undefined,
|
||||||
|
allowHTMLPaste || undefined,
|
||||||
|
);
|
||||||
const root = createRoot([], style);
|
const root = createRoot([], style);
|
||||||
root.replaceChildren(fragment);
|
root.replaceChildren(fragment);
|
||||||
resetInertElement();
|
resetInertElement();
|
||||||
|
|||||||
@ -18,7 +18,10 @@ import { TextEditor } from "../TextEditor.js";
|
|||||||
* @param {DataTransfer} clipboardData
|
* @param {DataTransfer} clipboardData
|
||||||
* @returns {DocumentFragment}
|
* @returns {DocumentFragment}
|
||||||
*/
|
*/
|
||||||
function getFormattedFragmentFromClipboardData(selectionController, clipboardData) {
|
function getFormattedFragmentFromClipboardData(
|
||||||
|
selectionController,
|
||||||
|
clipboardData,
|
||||||
|
) {
|
||||||
return mapContentFragmentFromHTML(
|
return mapContentFragmentFromHTML(
|
||||||
clipboardData.getData("text/html"),
|
clipboardData.getData("text/html"),
|
||||||
selectionController.currentStyle,
|
selectionController.currentStyle,
|
||||||
@ -79,9 +82,14 @@ export function paste(event, editor, selectionController) {
|
|||||||
|
|
||||||
let fragment = null;
|
let fragment = null;
|
||||||
if (editor?.options?.allowHTMLPaste) {
|
if (editor?.options?.allowHTMLPaste) {
|
||||||
fragment = getFormattedOrPlainFragmentFromClipboardData(event.clipboardData);
|
fragment = getFormattedOrPlainFragmentFromClipboardData(
|
||||||
|
event.clipboardData,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
fragment = getPlainFragmentFromClipboardData(selectionController, event.clipboardData);
|
fragment = getPlainFragmentFromClipboardData(
|
||||||
|
selectionController,
|
||||||
|
event.clipboardData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fragment) {
|
if (!fragment) {
|
||||||
@ -92,10 +100,9 @@ export function paste(event, editor, selectionController) {
|
|||||||
if (selectionController.isCollapsed) {
|
if (selectionController.isCollapsed) {
|
||||||
const hasOnlyOneParagraph = fragment.children.length === 1;
|
const hasOnlyOneParagraph = fragment.children.length === 1;
|
||||||
const hasOnlyOneTextSpan = fragment.firstElementChild.children.length === 1;
|
const hasOnlyOneTextSpan = fragment.firstElementChild.children.length === 1;
|
||||||
const forceTextSpan = fragment.firstElementChild.dataset.textSpan === "force";
|
const forceTextSpan =
|
||||||
if (hasOnlyOneParagraph
|
fragment.firstElementChild.dataset.textSpan === "force";
|
||||||
&& hasOnlyOneTextSpan
|
if (hasOnlyOneParagraph && hasOnlyOneTextSpan && forceTextSpan) {
|
||||||
&& forceTextSpan) {
|
|
||||||
selectionController.insertIntoFocus(fragment.textContent);
|
selectionController.insertIntoFocus(fragment.textContent);
|
||||||
} else {
|
} else {
|
||||||
selectionController.insertPaste(fragment);
|
selectionController.insertPaste(fragment);
|
||||||
@ -103,10 +110,9 @@ export function paste(event, editor, selectionController) {
|
|||||||
} else {
|
} else {
|
||||||
const hasOnlyOneParagraph = fragment.children.length === 1;
|
const hasOnlyOneParagraph = fragment.children.length === 1;
|
||||||
const hasOnlyOneTextSpan = fragment.firstElementChild.children.length === 1;
|
const hasOnlyOneTextSpan = fragment.firstElementChild.children.length === 1;
|
||||||
const forceTextSpan = fragment.firstElementChild.dataset.textSpan === "force";
|
const forceTextSpan =
|
||||||
if (hasOnlyOneParagraph
|
fragment.firstElementChild.dataset.textSpan === "force";
|
||||||
&& hasOnlyOneTextSpan
|
if (hasOnlyOneParagraph && hasOnlyOneTextSpan && forceTextSpan) {
|
||||||
&& forceTextSpan) {
|
|
||||||
selectionController.replaceText(fragment.textContent);
|
selectionController.replaceText(fragment.textContent);
|
||||||
} else {
|
} else {
|
||||||
selectionController.replaceWithPaste(fragment);
|
selectionController.replaceWithPaste(fragment);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export function deleteContentBackward(event, editor, selectionController) {
|
|||||||
// If not is collapsed AKA is a selection, then
|
// If not is collapsed AKA is a selection, then
|
||||||
// we removeSelected.
|
// we removeSelected.
|
||||||
if (!selectionController.isCollapsed) {
|
if (!selectionController.isCollapsed) {
|
||||||
return selectionController.removeSelected({ direction: 'backward' });
|
return selectionController.removeSelected({ direction: "backward" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're in a text node and the offset is
|
// 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) {
|
if (selectionController.isTextFocus && selectionController.focusOffset > 0) {
|
||||||
return selectionController.removeBackwardText();
|
return selectionController.removeBackwardText();
|
||||||
|
|
||||||
// If we're in a text node but we're at the end of the
|
// If we're in a text node but we're at the end of the
|
||||||
// paragraph, we should merge the current paragraph
|
// paragraph, we should merge the current paragraph
|
||||||
// with the following paragraph.
|
// with the following paragraph.
|
||||||
} else if (
|
} else if (
|
||||||
selectionController.isTextFocus &&
|
selectionController.isTextFocus &&
|
||||||
selectionController.focusAtStart
|
selectionController.focusAtStart
|
||||||
) {
|
) {
|
||||||
return selectionController.mergeBackwardParagraph();
|
return selectionController.mergeBackwardParagraph();
|
||||||
|
|
||||||
// If we're at an text span 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.isTextSpanFocus ||
|
selectionController.isTextSpanFocus ||
|
||||||
selectionController.isLineBreakFocus
|
selectionController.isLineBreakFocus
|
||||||
|
|||||||
@ -28,22 +28,21 @@ 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 text span)
|
// 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) {
|
|
||||||
return selectionController.mergeForwardParagraph();
|
return selectionController.mergeForwardParagraph();
|
||||||
|
|
||||||
// If we're in a text node but we're at the end of the
|
// If we're in a text node but we're at the end of the
|
||||||
// paragraph, we should merge the current paragraph
|
// paragraph, we should merge the current paragraph
|
||||||
// with the following paragraph.
|
// with the following paragraph.
|
||||||
} else if (
|
} else if (
|
||||||
selectionController.isTextFocus &&
|
selectionController.isTextFocus &&
|
||||||
selectionController.focusOffset >= 0
|
selectionController.focusOffset >= 0
|
||||||
) {
|
) {
|
||||||
return selectionController.removeForwardText();
|
return selectionController.removeForwardText();
|
||||||
|
|
||||||
// If we're at a text span 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.isTextSpanFocus ||
|
(selectionController.isTextSpanFocus ||
|
||||||
selectionController.isLineBreakFocus) &&
|
selectionController.isLineBreakFocus) &&
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
import { describe, test, expect } from 'vitest'
|
import { describe, test, expect } from "vitest";
|
||||||
import { insertInto, removeBackward, removeForward, replaceWith } from './Text';
|
import { insertInto, removeBackward, removeForward, replaceWith } from "./Text";
|
||||||
|
|
||||||
describe("Text", () => {
|
describe("Text", () => {
|
||||||
test("* should throw when passed wrong parameters", () => {
|
test("* should throw when passed wrong parameters", () => {
|
||||||
expect(() => insertInto(Infinity, Infinity, Infinity)).toThrowError('Invalid string');
|
expect(() => insertInto(Infinity, Infinity, Infinity)).toThrowError(
|
||||||
expect(() => insertInto('Hello', Infinity, Infinity)).toThrowError('Invalid offset');
|
"Invalid string",
|
||||||
expect(() => insertInto('Hello', 0, 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", () => {
|
test("`insertInto` should insert a string into an offset", () => {
|
||||||
@ -13,7 +19,9 @@ describe("Text", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceWith` should replace a string into a string", () => {
|
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)", () => {
|
test("`removeBackward` should remove string backward from start (offset 0)", () => {
|
||||||
@ -26,13 +34,13 @@ describe("Text", () => {
|
|||||||
|
|
||||||
test("`removeBackward` should remove string backward from end", () => {
|
test("`removeBackward` should remove string backward from end", () => {
|
||||||
expect(removeBackward("Hello, World!", "Hello, World!".length)).toBe(
|
expect(removeBackward("Hello, World!", "Hello, World!".length)).toBe(
|
||||||
"Hello, World"
|
"Hello, World",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`removeForward` should remove string forward from end", () => {
|
test("`removeForward` should remove string forward from end", () => {
|
||||||
expect(removeForward("Hello, World!", "Hello, World!".length)).toBe(
|
expect(removeForward("Hello, World!", "Hello, World!".length)).toBe(
|
||||||
"Hello, World!"
|
"Hello, World!",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ function getContext() {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
context = canvas.getContext("2d");
|
context = canvas.getContext("2d");
|
||||||
}
|
}
|
||||||
return context
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -230,15 +230,10 @@ export function mapContentFragmentFromString(string, styleDefaults) {
|
|||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line === "") {
|
if (line === "") {
|
||||||
fragment.appendChild(
|
fragment.appendChild(createEmptyParagraph(styleDefaults));
|
||||||
createEmptyParagraph(styleDefaults)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const textSpan = createTextSpan(new Text(line), styleDefaults);
|
const textSpan = createTextSpan(new Text(line), styleDefaults);
|
||||||
const paragraph = createParagraph(
|
const paragraph = createParagraph([textSpan], styleDefaults);
|
||||||
[textSpan],
|
|
||||||
styleDefaults,
|
|
||||||
);
|
|
||||||
if (lines.length === 1) {
|
if (lines.length === 1) {
|
||||||
paragraph.dataset.textSpan = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,7 +112,11 @@ describe("Paragraph", () => {
|
|||||||
const helloTextSpan = createTextSpan(new Text("Hello, "));
|
const helloTextSpan = createTextSpan(new Text("Hello, "));
|
||||||
const worldTextSpan = createTextSpan(new Text("World"));
|
const worldTextSpan = createTextSpan(new Text("World"));
|
||||||
const exclTextSpan = createTextSpan(new Text("!"));
|
const exclTextSpan = createTextSpan(new Text("!"));
|
||||||
const paragraph = createParagraph([helloTextSpan, worldTextSpan, exclTextSpan]);
|
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);
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import { describe, test, expect } from "vitest";
|
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 */
|
/* @vitest-environment jsdom */
|
||||||
describe("Root", () => {
|
describe("Root", () => {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
* Copyright (c) KALEIDOS INC
|
* Copyright (c) KALEIDOS INC
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import StyleDeclaration from '../../controllers/StyleDeclaration.js';
|
import StyleDeclaration from "../../controllers/StyleDeclaration.js";
|
||||||
import { getFills } from "./Color.js";
|
import { getFills } from "./Color.js";
|
||||||
|
|
||||||
const DEFAULT_FONT_SIZE = "16px";
|
const DEFAULT_FONT_SIZE = "16px";
|
||||||
@ -339,8 +339,7 @@ export function setStylesFromObject(element, allowedStyles, styleObject) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let styleValue = styleObject[styleName];
|
let styleValue = styleObject[styleName];
|
||||||
if (!styleValue)
|
if (!styleValue) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
if (styleName === "font-family") {
|
if (styleName === "font-family") {
|
||||||
styleValue = sanitizeFontFamily(styleValue);
|
styleValue = sanitizeFontFamily(styleValue);
|
||||||
@ -388,8 +387,10 @@ export function setStylesFromDeclaration(
|
|||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
export function setStyles(element, allowedStyles, styleObjectOrDeclaration) {
|
export function setStyles(element, allowedStyles, styleObjectOrDeclaration) {
|
||||||
if (styleObjectOrDeclaration instanceof CSSStyleDeclaration
|
if (
|
||||||
|| styleObjectOrDeclaration instanceof StyleDeclaration) {
|
styleObjectOrDeclaration instanceof CSSStyleDeclaration ||
|
||||||
|
styleObjectOrDeclaration instanceof StyleDeclaration
|
||||||
|
) {
|
||||||
return setStylesFromDeclaration(
|
return setStylesFromDeclaration(
|
||||||
element,
|
element,
|
||||||
allowedStyles,
|
allowedStyles,
|
||||||
|
|||||||
@ -22,8 +22,7 @@ import { isRoot } from "./Root.js";
|
|||||||
*/
|
*/
|
||||||
export function isTextNode(node) {
|
export function isTextNode(node) {
|
||||||
if (!node) throw new TypeError("Invalid text node");
|
if (!node) throw new TypeError("Invalid text node");
|
||||||
return node.nodeType === Node.TEXT_NODE
|
return node.nodeType === Node.TEXT_NODE || isLineBreak(node);
|
||||||
|| isLineBreak(node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,8 +32,7 @@ export function isTextNode(node) {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function isEmptyTextNode(node) {
|
export function isEmptyTextNode(node) {
|
||||||
return node.nodeType === Node.TEXT_NODE
|
return node.nodeType === Node.TEXT_NODE && node.nodeValue === "";
|
||||||
&& node.nodeValue === "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
* Copyright (c) KALEIDOS INC
|
* Copyright (c) KALEIDOS INC
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SafeGuard from '../../controllers/SafeGuard.js';
|
import SafeGuard from "../../controllers/SafeGuard.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterator direction.
|
* Iterator direction.
|
||||||
@ -58,7 +58,7 @@ export class TextNodeIterator {
|
|||||||
startNode,
|
startNode,
|
||||||
rootNode,
|
rootNode,
|
||||||
skipNodes = new Set(),
|
skipNodes = new Set(),
|
||||||
direction = TextNodeIteratorDirection.FORWARD
|
direction = TextNodeIteratorDirection.FORWARD,
|
||||||
) {
|
) {
|
||||||
if (startNode === rootNode) {
|
if (startNode === rootNode) {
|
||||||
return TextNodeIterator.findDown(
|
return TextNodeIterator.findDown(
|
||||||
@ -67,7 +67,7 @@ export class TextNodeIterator {
|
|||||||
: startNode.lastChild,
|
: startNode.lastChild,
|
||||||
rootNode,
|
rootNode,
|
||||||
skipNodes,
|
skipNodes,
|
||||||
direction
|
direction,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ export class TextNodeIterator {
|
|||||||
: currentNode.lastChild,
|
: currentNode.lastChild,
|
||||||
rootNode,
|
rootNode,
|
||||||
skipNodes,
|
skipNodes,
|
||||||
direction
|
direction,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
currentNode =
|
currentNode =
|
||||||
@ -119,7 +119,7 @@ export class TextNodeIterator {
|
|||||||
startNode,
|
startNode,
|
||||||
rootNode,
|
rootNode,
|
||||||
backTrack = new Set(),
|
backTrack = new Set(),
|
||||||
direction = TextNodeIteratorDirection.FORWARD
|
direction = TextNodeIteratorDirection.FORWARD,
|
||||||
) {
|
) {
|
||||||
backTrack.add(startNode);
|
backTrack.add(startNode);
|
||||||
if (TextNodeIterator.isTextNode(startNode)) {
|
if (TextNodeIterator.isTextNode(startNode)) {
|
||||||
@ -127,14 +127,14 @@ export class TextNodeIterator {
|
|||||||
startNode.parentNode,
|
startNode.parentNode,
|
||||||
rootNode,
|
rootNode,
|
||||||
backTrack,
|
backTrack,
|
||||||
direction
|
direction,
|
||||||
);
|
);
|
||||||
} else if (TextNodeIterator.isContainerNode(startNode)) {
|
} else if (TextNodeIterator.isContainerNode(startNode)) {
|
||||||
const found = TextNodeIterator.findDown(
|
const found = TextNodeIterator.findDown(
|
||||||
startNode,
|
startNode,
|
||||||
rootNode,
|
rootNode,
|
||||||
backTrack,
|
backTrack,
|
||||||
direction
|
direction,
|
||||||
);
|
);
|
||||||
if (found) {
|
if (found) {
|
||||||
return found;
|
return found;
|
||||||
@ -144,7 +144,7 @@ export class TextNodeIterator {
|
|||||||
startNode.parentNode,
|
startNode.parentNode,
|
||||||
rootNode,
|
rootNode,
|
||||||
backTrack,
|
backTrack,
|
||||||
direction
|
direction,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,7 +214,7 @@ export class TextNodeIterator {
|
|||||||
this.#currentNode,
|
this.#currentNode,
|
||||||
this.#rootNode,
|
this.#rootNode,
|
||||||
new Set(),
|
new Set(),
|
||||||
TextNodeIteratorDirection.FORWARD
|
TextNodeIteratorDirection.FORWARD,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!nextNode) {
|
if (!nextNode) {
|
||||||
@ -237,7 +237,7 @@ export class TextNodeIterator {
|
|||||||
this.#currentNode,
|
this.#currentNode,
|
||||||
this.#rootNode,
|
this.#rootNode,
|
||||||
new Set(),
|
new Set(),
|
||||||
TextNodeIteratorDirection.BACKWARD
|
TextNodeIteratorDirection.BACKWARD,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!previousNode) {
|
if (!previousNode) {
|
||||||
@ -270,10 +270,8 @@ export class TextNodeIterator {
|
|||||||
* @param {TextNode} endNode
|
* @param {TextNode} endNode
|
||||||
* @yields {TextNode}
|
* @yields {TextNode}
|
||||||
*/
|
*/
|
||||||
* iterateFrom(startNode, endNode) {
|
*iterateFrom(startNode, endNode) {
|
||||||
const comparedPosition = startNode.compareDocumentPosition(
|
const comparedPosition = startNode.compareDocumentPosition(endNode);
|
||||||
endNode
|
|
||||||
);
|
|
||||||
this.#currentNode = startNode;
|
this.#currentNode = startNode;
|
||||||
SafeGuard.start();
|
SafeGuard.start();
|
||||||
while (this.#currentNode !== endNode) {
|
while (this.#currentNode !== endNode) {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export class ChangeController extends EventTarget {
|
|||||||
* @param {number} [time=500]
|
* @param {number} [time=500]
|
||||||
*/
|
*/
|
||||||
constructor(time = 500) {
|
constructor(time = 500) {
|
||||||
super()
|
super();
|
||||||
if (typeof time === "number" && (!Number.isInteger(time) || time <= 0)) {
|
if (typeof time === "number" && (!Number.isInteger(time) || time <= 0)) {
|
||||||
throw new TypeError("Invalid time");
|
throw new TypeError("Invalid time");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,19 +24,19 @@ export function start() {
|
|||||||
*/
|
*/
|
||||||
export function update() {
|
export function update() {
|
||||||
if (Date.now - startTime >= SAFE_GUARD_TIME) {
|
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) {
|
export function throwAfter(error, timeout = SAFE_GUARD_TIME) {
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
throw error
|
throw error;
|
||||||
}, timeout)
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function throwCancel() {
|
export function throwCancel() {
|
||||||
clearTimeout(timeoutId)
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -54,7 +54,7 @@ import { isRoot, setRootStyles } from "../content/dom/Root.js";
|
|||||||
import { SelectionDirection } from "./SelectionDirection.js";
|
import { SelectionDirection } from "./SelectionDirection.js";
|
||||||
import SafeGuard from "./SafeGuard.js";
|
import SafeGuard from "./SafeGuard.js";
|
||||||
import { sanitizeFontFamily } from "../content/dom/Style.js";
|
import { sanitizeFontFamily } from "../content/dom/Style.js";
|
||||||
import StyleDeclaration from './StyleDeclaration.js';
|
import StyleDeclaration from "./StyleDeclaration.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported options for the SelectionController.
|
* 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
|
// FIXME: I don't like this approximation. Having to iterate nodes twice
|
||||||
// is bad for performance. I think we need another way of "computing"
|
// is bad for performance. I think we need another way of "computing"
|
||||||
// the cascade.
|
// 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;
|
const paragraph = textNode.parentElement.parentElement;
|
||||||
this.#applyStylesFromElementToCurrentStyle(paragraph);
|
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;
|
const textSpan = textNode.parentElement;
|
||||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||||
}
|
}
|
||||||
@ -498,19 +504,12 @@ export class SelectionController extends EventTarget {
|
|||||||
if (!this.#savedSelection) return false;
|
if (!this.#savedSelection) return false;
|
||||||
|
|
||||||
if (this.#savedSelection.anchorNode && this.#savedSelection.focusNode) {
|
if (this.#savedSelection.anchorNode && this.#savedSelection.focusNode) {
|
||||||
if (this.#savedSelection.anchorNode === this.#savedSelection.focusNode) {
|
this.#selection.setBaseAndExtent(
|
||||||
this.#selection.setPosition(
|
this.#savedSelection.anchorNode,
|
||||||
this.#savedSelection.focusNode,
|
this.#savedSelection.anchorOffset,
|
||||||
this.#savedSelection.focusOffset,
|
this.#savedSelection.focusNode,
|
||||||
);
|
this.#savedSelection.focusOffset,
|
||||||
} else {
|
);
|
||||||
this.#selection.setBaseAndExtent(
|
|
||||||
this.#savedSelection.anchorNode,
|
|
||||||
this.#savedSelection.anchorOffset,
|
|
||||||
this.#savedSelection.focusNode,
|
|
||||||
this.#savedSelection.focusOffset,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.#savedSelection = null;
|
this.#savedSelection = null;
|
||||||
return true;
|
return true;
|
||||||
@ -1132,10 +1131,7 @@ export class SelectionController extends EventTarget {
|
|||||||
const hasOnlyOneParagraph = fragment.children.length === 1;
|
const hasOnlyOneParagraph = fragment.children.length === 1;
|
||||||
const forceTextSpan =
|
const forceTextSpan =
|
||||||
fragment.firstElementChild?.dataset?.textSpan === "force";
|
fragment.firstElementChild?.dataset?.textSpan === "force";
|
||||||
if (
|
if (hasOnlyOneParagraph && forceTextSpan) {
|
||||||
hasOnlyOneParagraph &&
|
|
||||||
forceTextSpan
|
|
||||||
) {
|
|
||||||
// first text span
|
// first text span
|
||||||
const collapseNode = fragment.firstElementChild.firstElementChild;
|
const collapseNode = fragment.firstElementChild.firstElementChild;
|
||||||
if (this.isTextSpanStart) {
|
if (this.isTextSpanStart) {
|
||||||
@ -1403,7 +1399,7 @@ export class SelectionController extends EventTarget {
|
|||||||
// the focus node is a <span>.
|
// the focus node is a <span>.
|
||||||
if (isTextSpan(this.focusNode)) {
|
if (isTextSpan(this.focusNode)) {
|
||||||
this.focusNode.firstElementChild.replaceWith(textNode);
|
this.focusNode.firstElementChild.replaceWith(textNode);
|
||||||
// the focus node is a <br>.
|
// the focus node is a <br>.
|
||||||
} else {
|
} else {
|
||||||
this.focusNode.replaceWith(textNode);
|
this.focusNode.replaceWith(textNode);
|
||||||
}
|
}
|
||||||
@ -1981,8 +1977,7 @@ export class SelectionController extends EventTarget {
|
|||||||
this.setSelection(newTextSpan.firstChild, 0, newTextSpan.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 {
|
||||||
{
|
|
||||||
const paragraph = this.startParagraph;
|
const paragraph = this.startParagraph;
|
||||||
setParagraphStyles(paragraph, newStyles);
|
setParagraphStyles(paragraph, newStyles);
|
||||||
// Apply styles to child text spans.
|
// Apply styles to child text spans.
|
||||||
|
|||||||
@ -278,9 +278,9 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
"Hello",
|
"Hello",
|
||||||
);
|
);
|
||||||
expect(
|
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
textEditorMock.root.lastChild.firstChild.firstChild.nodeValue,
|
", World!",
|
||||||
).toBe(", World!");
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert a paragraph from a pasted fragment (at middle)", () => {
|
test("`insertPaste` should insert a paragraph from a pasted fragment (at middle)", () => {
|
||||||
@ -292,7 +292,12 @@ describe("SelectionController", () => {
|
|||||||
textEditorMock,
|
textEditorMock,
|
||||||
selection,
|
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 paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
@ -315,9 +320,9 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
"Lorem ",
|
"Lorem ",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.children.item(1).firstChild.firstChild.nodeValue).toBe(
|
expect(
|
||||||
"ipsum ",
|
textEditorMock.root.children.item(1).firstChild.firstChild.nodeValue,
|
||||||
);
|
).toBe("ipsum ");
|
||||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
"dolor",
|
"dolor",
|
||||||
);
|
);
|
||||||
@ -359,25 +364,21 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
"Hello",
|
"Hello",
|
||||||
);
|
);
|
||||||
expect(
|
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
textEditorMock.root.lastChild.firstChild.firstChild.nodeValue,
|
", World!",
|
||||||
).toBe(", World!");
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert a text span 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();
|
||||||
const selectionController = new SelectionController(
|
const selectionController = new SelectionController(
|
||||||
textEditorMock,
|
textEditorMock,
|
||||||
selection,
|
selection,
|
||||||
);
|
);
|
||||||
focus(
|
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
||||||
selection,
|
|
||||||
textEditorMock,
|
|
||||||
root.firstChild.firstChild.firstChild,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
||||||
paragraph.dataset.textSpan = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
@ -415,7 +416,12 @@ describe("SelectionController", () => {
|
|||||||
textEditorMock,
|
textEditorMock,
|
||||||
selection,
|
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 paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
||||||
paragraph.dataset.textSpan = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
@ -439,9 +445,9 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
"Lorem ",
|
"Lorem ",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue).toBe(
|
expect(
|
||||||
"ipsum ",
|
textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue,
|
||||||
);
|
).toBe("ipsum ");
|
||||||
expect(
|
expect(
|
||||||
textEditorMock.root.firstChild.children.item(2).firstChild.nodeValue,
|
textEditorMock.root.firstChild.children.item(2).firstChild.nodeValue,
|
||||||
).toBe("dolor");
|
).toBe("dolor");
|
||||||
@ -461,9 +467,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.firstChild.firstChild,
|
root.firstChild.firstChild.firstChild,
|
||||||
"Hello".length,
|
"Hello".length,
|
||||||
);
|
);
|
||||||
const paragraph = createParagraph([
|
const paragraph = createParagraph([createTextSpan(new Text(", World!"))]);
|
||||||
createTextSpan(new Text(", World!"))
|
|
||||||
]);
|
|
||||||
paragraph.dataset.textSpan = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
@ -486,9 +490,9 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
"Hello",
|
"Hello",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue).toBe(
|
expect(
|
||||||
", World!",
|
textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue,
|
||||||
);
|
).toBe(", World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`removeBackwardText` should remove text in backward direction (backspace)", () => {
|
test("`removeBackwardText` should remove text in backward direction (backspace)", () => {
|
||||||
|
|||||||
@ -77,7 +77,10 @@ export class StyleDeclaration {
|
|||||||
const currentValue = this.getPropertyValue(name);
|
const currentValue = this.getPropertyValue(name);
|
||||||
if (this.#isQuotedValue(currentValue, value)) {
|
if (this.#isQuotedValue(currentValue, value)) {
|
||||||
return this.setProperty(name, 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);
|
return this.setProperty(name, value);
|
||||||
} else if (currentValue === "" && ["initial", "none"].includes(value)) {
|
} else if (currentValue === "" && ["initial", "none"].includes(value)) {
|
||||||
return this.setProperty(name, 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 =
|
this.#elements.isParagraphStart.checked =
|
||||||
selectionController.isParagraphStart;
|
selectionController.isParagraphStart;
|
||||||
this.#elements.isParagraphEnd.checked = selectionController.isParagraphEnd;
|
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.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(
|
||||||
selectionController.focusNode,
|
selectionController.focusNode,
|
||||||
selectionController.focusOffset
|
selectionController.focusOffset,
|
||||||
);
|
);
|
||||||
this.#elements.focusOffset.value = selectionController.focusOffset;
|
this.#elements.focusOffset.value = selectionController.focusOffset;
|
||||||
this.#elements.anchorNode.value = this.getNodeDescription(
|
this.#elements.anchorNode.value = this.getNodeDescription(
|
||||||
selectionController.anchorNode,
|
selectionController.anchorNode,
|
||||||
selectionController.anchorOffset
|
selectionController.anchorOffset,
|
||||||
);
|
);
|
||||||
this.#elements.anchorOffset.value = selectionController.anchorOffset;
|
this.#elements.anchorOffset.value = selectionController.anchorOffset;
|
||||||
this.#elements.focusTextSpan.value = this.getNodeDescription(
|
this.#elements.focusTextSpan.value = this.getNodeDescription(
|
||||||
selectionController.focusTextSpan
|
selectionController.focusTextSpan,
|
||||||
);
|
);
|
||||||
this.#elements.anchorTextSpan.value = this.getNodeDescription(
|
this.#elements.anchorTextSpan.value = this.getNodeDescription(
|
||||||
selectionController.anchorTextSpan
|
selectionController.anchorTextSpan,
|
||||||
);
|
);
|
||||||
this.#elements.focusParagraph.value = this.getNodeDescription(
|
this.#elements.focusParagraph.value = this.getNodeDescription(
|
||||||
selectionController.focusParagraph
|
selectionController.focusParagraph,
|
||||||
);
|
);
|
||||||
this.#elements.anchorParagraph.value = this.getNodeDescription(
|
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) {
|
polar(angle, length = 1.0) {
|
||||||
return this.set(
|
return this.set(Math.cos(angle) * length, Math.sin(angle) * length);
|
||||||
Math.cos(angle) * length,
|
|
||||||
Math.sin(angle) * length
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add({ x, y }) {
|
add({ x, y }) {
|
||||||
@ -119,10 +116,7 @@ export class Point {
|
|||||||
|
|
||||||
export class Rect {
|
export class Rect {
|
||||||
static create(x, y, width, height) {
|
static create(x, y, width, height) {
|
||||||
return new Rect(
|
return new Rect(new Point(width, height), new Point(x, y));
|
||||||
new Point(width, height),
|
|
||||||
new Point(x, y),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#size;
|
#size;
|
||||||
@ -228,10 +222,7 @@ export class Rect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
return new Rect(
|
return new Rect(this.#size.clone(), this.#position.clone());
|
||||||
this.#size.clone(),
|
|
||||||
this.#position.clone(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toFixed(fractionDigits = 0) {
|
toFixed(fractionDigits = 0) {
|
||||||
|
|||||||
@ -82,13 +82,13 @@ export class Shape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get rotation() {
|
get rotation() {
|
||||||
return this.#rotation
|
return this.#rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
set rotation(newRotation) {
|
set rotation(newRotation) {
|
||||||
if (!Number.isFinite(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]) =>
|
const entry = Object.entries(this).find(([name, value]) =>
|
||||||
name === fromStyleValue(style) ? value : 0,
|
name === fromStyleValue(style) ? value : 0,
|
||||||
);
|
);
|
||||||
if (!entry)
|
if (!entry) return;
|
||||||
return;
|
|
||||||
|
|
||||||
const [name] = entry;
|
const [name] = entry;
|
||||||
return name;
|
return name;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Point } from './geom';
|
import { Point } from "./geom";
|
||||||
|
|
||||||
export class Viewport {
|
export class Viewport {
|
||||||
#zoom;
|
#zoom;
|
||||||
@ -38,7 +38,7 @@ export class Viewport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pan(dx, dy) {
|
pan(dx, dy) {
|
||||||
this.#position.x += dx / this.#zoom
|
this.#position.x += dx / this.#zoom;
|
||||||
this.#position.y += dy / this.#zoom
|
this.#position.y += dy / this.#zoom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
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 { 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";
|
import { createLineBreak } from "../editor/content/dom/LineBreak.js";
|
||||||
|
|
||||||
export class TextEditorMock extends EventTarget {
|
export class TextEditorMock extends EventTarget {
|
||||||
@ -38,14 +41,14 @@ export class TextEditorMock extends EventTarget {
|
|||||||
static createTextEditorMockWithRoot(root) {
|
static createTextEditorMockWithRoot(root) {
|
||||||
const container = TextEditorMock.getTemplate();
|
const container = TextEditorMock.getTemplate();
|
||||||
const selectionImposterElement = container.querySelector(
|
const selectionImposterElement = container.querySelector(
|
||||||
".text-editor-selection-imposter"
|
".text-editor-selection-imposter",
|
||||||
);
|
);
|
||||||
const textEditorMock = new TextEditorMock(
|
const textEditorMock = new TextEditorMock(
|
||||||
container.querySelector(".text-editor-content"),
|
container.querySelector(".text-editor-content"),
|
||||||
{
|
{
|
||||||
root,
|
root,
|
||||||
selectionImposterElement,
|
selectionImposterElement,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return textEditorMock;
|
return textEditorMock;
|
||||||
}
|
}
|
||||||
@ -86,8 +89,8 @@ export class TextEditorMock extends EventTarget {
|
|||||||
return this.createTextEditorMockWithParagraphs([
|
return this.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([
|
createParagraph([
|
||||||
text.length === 0
|
text.length === 0
|
||||||
? createEmptyTextSpan()
|
? createEmptyTextSpan()
|
||||||
: createTextSpan(new Text(text))
|
: createTextSpan(new Text(text)),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -100,7 +103,9 @@ export class TextEditorMock extends EventTarget {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static createTextEditorMockWithParagraph(textSpans) {
|
static createTextEditorMockWithParagraph(textSpans) {
|
||||||
return this.createTextEditorMockWithParagraphs([createParagraph(textSpans)]);
|
return this.createTextEditorMockWithParagraphs([
|
||||||
|
createParagraph(textSpans),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#element = null;
|
#element = null;
|
||||||
|
|||||||
@ -1,30 +1,28 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from 'node:fs/promises';
|
import fs from "node:fs/promises";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { coverageConfigDefaults } from "vitest/config";
|
import { coverageConfigDefaults } from "vitest/config";
|
||||||
|
|
||||||
async function waitFor(timeInMillis) {
|
async function waitFor(timeInMillis) {
|
||||||
return new Promise(resolve =>
|
return new Promise((resolve) => setTimeout((_) => resolve(), timeInMillis));
|
||||||
setTimeout(_ => resolve(), timeInMillis)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasmWatcherPlugin = (options = {}) => {
|
const wasmWatcherPlugin = (options = {}) => {
|
||||||
return {
|
return {
|
||||||
name: "vite-wasm-watcher-plugin",
|
name: "vite-wasm-watcher-plugin",
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
server.watcher.add("../resources/public/js/render_wasm.wasm")
|
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.js");
|
||||||
server.watcher.on("change", async (file) => {
|
server.watcher.on("change", async (file) => {
|
||||||
if (file.includes("../resources/")) {
|
if (file.includes("../resources/")) {
|
||||||
// If we copy the files immediately, we end
|
// If we copy the files immediately, we end
|
||||||
// up with an empty .js file (I don't know why).
|
// up with an empty .js file (I don't know why).
|
||||||
await waitFor(100)
|
await waitFor(100);
|
||||||
// copy files.
|
// copy files.
|
||||||
await fs.copyFile(
|
await fs.copyFile(
|
||||||
path.resolve(file),
|
path.resolve(file),
|
||||||
path.resolve('./src/wasm/', path.basename(file))
|
path.resolve("./src/wasm/", path.basename(file)),
|
||||||
)
|
);
|
||||||
console.log(`${file} changed`);
|
console.log(`${file} changed`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -49,9 +47,7 @@ const wasmWatcherPlugin = (options = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [wasmWatcherPlugin()],
|
||||||
wasmWatcherPlugin()
|
|
||||||
],
|
|
||||||
root: "./src",
|
root: "./src",
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user