Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2024-11-25 09:20:30 +01:00
commit 6f5c7c01bb
61 changed files with 583 additions and 506 deletions

View file

@ -524,6 +524,19 @@
(when (kbd/enter? event) (when (kbd/enter? event)
(on-add-shared event)))) (on-add-shared event))))
on-show-version-history
(mf/use-fn
(mf/deps file-id)
(fn [_]
(st/emit! (dw/toggle-layout-flag :document-history))))
on-show-version-history-key-down
(mf/use-fn
(mf/deps on-show-version-history)
(fn [event]
(when (kbd/enter? event)
(on-show-version-history event))))
on-export-shapes on-export-shapes
(mf/use-fn #(st/emit! (de/show-workspace-export-dialog {:origin "workspace:menu"}))) (mf/use-fn #(st/emit! (de/show-workspace-export-dialog {:origin "workspace:menu"})))
@ -575,14 +588,23 @@
:on-click on-remove-shared :on-click on-remove-shared
:on-key-down on-remove-shared-key-down :on-key-down on-remove-shared-key-down
:id "file-menu-remove-shared"} :id "file-menu-remove-shared"}
[:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]]) [:span {:class (stl/css :item-name)}
(tr "dashboard.unpublish-shared")]])
(when can-edit (when can-edit
[:> dropdown-menu-item* {:class (stl/css :submenu-item) [:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click on-add-shared :on-click on-add-shared
:on-key-down on-add-shared-key-down :on-key-down on-add-shared-key-down
:id "file-menu-add-shared"} :id "file-menu-add-shared"}
[:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]])) [:span {:class (stl/css :item-name)}
(tr "dashboard.add-shared")]]))
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click on-show-version-history
:on-key-down on-show-version-history-key-down
:id "file-menu-show-version-history"}
[:span {:class (stl/css :item-name)}
(tr "dashboard.show-version-history")]]
[:> dropdown-menu-item* {:class (stl/css :submenu-item) [:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click on-export-shapes :on-click on-export-shapes

View file

@ -1,29 +0,0 @@
import { describe, test, expect, vi } from 'vitest';
import { addEventListeners, removeEventListeners } from './Event';
/* @vitest-environment jsdom */
describe('Event', () => {
test('addEventListeners should add event listeners to an element using an object', () => {
const clickSpy = vi.fn();
const events = {
click: clickSpy
}
const element = document.createElement('div');
addEventListeners(element, events);
element.dispatchEvent(new Event('click'));
expect(clickSpy).toBeCalled();
});
test('removeEventListeners should remove event listeners to an element using an object', () => {
const clickSpy = vi.fn();
const events = {
click: clickSpy,
};
const element = document.createElement("div");
addEventListeners(element, events);
element.dispatchEvent(new Event("click"));
removeEventListeners(element, events);
element.dispatchEvent(new Event('click'))
expect(clickSpy).toBeCalledTimes(1);
})
});

View file

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"paths": { "paths": {
"~/*": ["./*"] "~/*": ["./src/*"]
} }
} }
} }

View file

@ -3,11 +3,9 @@
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"main": "editor/TextEditor.js", "main": "src/editor/TextEditor.js",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "./scripts/build.sh",
"preview": "vite preview",
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"test": "vitest --run", "test": "vitest --run",
"test:watch": "vitest", "test:watch": "vitest",
@ -23,6 +21,7 @@
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"jsdom": "^25.0.0", "jsdom": "^25.0.0",
"playwright": "^1.45.1", "playwright": "^1.45.1",
"prettier": "^3.3.3",
"vite": "^5.3.1", "vite": "^5.3.1",
"vitest": "^1.6.0" "vitest": "^1.6.0"
}, },

View file

@ -1,3 +0,0 @@
#!/usr/bin/env bash
esbuild --bundle --minify --sourcemap --target=es2021 --format=esm --platform=browser editor/TextEditor.js --outfile=dist/TextEditor.mjs

View file

@ -0,0 +1,29 @@
import { describe, test, expect, vi } from "vitest";
import { addEventListeners, removeEventListeners } from "./Event.js";
/* @vitest-environment jsdom */
describe("Event", () => {
test("addEventListeners should add event listeners to an element using an object", () => {
const clickSpy = vi.fn();
const events = {
click: clickSpy,
};
const element = document.createElement("div");
addEventListeners(element, events);
element.dispatchEvent(new Event("click"));
expect(clickSpy).toBeCalled();
});
test("removeEventListeners should remove event listeners to an element using an object", () => {
const clickSpy = vi.fn();
const events = {
click: clickSpy,
};
const element = document.createElement("div");
addEventListeners(element, events);
element.dispatchEvent(new Event("click"));
removeEventListeners(element, events);
element.dispatchEvent(new Event("click"));
expect(clickSpy).toBeCalledTimes(1);
});
});

View file

@ -8,15 +8,15 @@
import clipboard from "./clipboard/index.js"; import clipboard from "./clipboard/index.js";
import commands from "./commands/index.js"; import commands from "./commands/index.js";
import ChangeController from './controllers/ChangeController.js'; import ChangeController from "./controllers/ChangeController.js";
import SelectionController from './controllers/SelectionController.js'; import SelectionController from "./controllers/SelectionController.js";
import { createSelectionImposterFromClientRects } from './selection/Imposter.js'; import { createSelectionImposterFromClientRects } from "./selection/Imposter.js";
import { addEventListeners, removeEventListeners } from "./Event.js"; import { addEventListeners, removeEventListeners } from "./Event.js";
import { createRoot, createEmptyRoot } from './content/dom/Root.js'; import { createRoot, createEmptyRoot } from "./content/dom/Root.js";
import { createParagraph, fixParagraph, getParagraph } from './content/dom/Paragraph.js'; import { createParagraph } from "./content/dom/Paragraph.js";
import { createEmptyInline, createInline } from './content/dom/Inline.js'; import { createEmptyInline, createInline } from "./content/dom/Inline.js";
import { isLineBreak } from './content/dom/LineBreak.js'; import { isLineBreak } from "./content/dom/LineBreak.js";
import LayoutType from './layout/LayoutType.js'; import LayoutType from "./layout/LayoutType.js";
/** /**
* Text Editor. * Text Editor.

View file

@ -1,49 +1,49 @@
import { describe, test, expect } from 'vitest' import { describe, test, expect } from "vitest";
import { TextEditor } from './TextEditor' import { TextEditor } from "./TextEditor.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe('TextEditor', () => { describe("TextEditor", () => {
test('Creating TextEditor without element should throw', () => { test("Creating TextEditor without element should throw", () => {
expect(() => new TextEditor()).toThrowError('Invalid text editor element'); expect(() => new TextEditor()).toThrowError("Invalid text editor element");
}); });
test('Creating TextEditor with element should success', () => { test("Creating TextEditor with element should success", () => {
expect(new TextEditor(document.createElement('div'))).toBeInstanceOf(TextEditor); expect(new TextEditor(document.createElement("div"))).toBeInstanceOf(
TextEditor,
);
}); });
test('isEmpty should return true when editor is empty', () => { test("isEmpty should return true when editor is empty", () => {
const textEditor = new TextEditor(document.createElement("div")); const textEditor = new TextEditor(document.createElement("div"));
expect(textEditor).toBeInstanceOf(TextEditor); expect(textEditor).toBeInstanceOf(TextEditor);
expect(textEditor.isEmpty).toBe(true); expect(textEditor.isEmpty).toBe(true);
}); });
test('Num paragraphs should return 1 when empty', () => { test("Num paragraphs should return 1 when empty", () => {
const textEditor = new TextEditor(document.createElement("div")); const textEditor = new TextEditor(document.createElement("div"));
expect(textEditor).toBeInstanceOf(TextEditor); expect(textEditor).toBeInstanceOf(TextEditor);
expect(textEditor.numParagraphs).toBe(1); expect(textEditor.numParagraphs).toBe(1);
}); });
test('Num paragraphs should return the number of paragraphs', () => { test("Num paragraphs should return the number of paragraphs", () => {
const textEditor = new TextEditor(document.createElement("div")); const textEditor = new TextEditor(document.createElement("div"));
textEditor.root = textEditor.createRoot([ textEditor.root = textEditor.createRoot([
textEditor.createParagraph([ textEditor.createParagraph([
textEditor.createInlineFromString('Hello, World!') textEditor.createInlineFromString("Hello, World!"),
]),
textEditor.createParagraph([textEditor.createInlineFromString("")]),
textEditor.createParagraph([
textEditor.createInlineFromString("¡Hola, Mundo!"),
]), ]),
textEditor.createParagraph([ textEditor.createParagraph([
textEditor.createInlineFromString('') textEditor.createInlineFromString("Hallo, Welt!"),
]), ]),
textEditor.createParagraph([
textEditor.createInlineFromString('¡Hola, Mundo!')
]),
textEditor.createParagraph([
textEditor.createInlineFromString('Hallo, Welt!')
])
]); ]);
expect(textEditor).toBeInstanceOf(TextEditor); expect(textEditor).toBeInstanceOf(TextEditor);
expect(textEditor.numParagraphs).toBe(4); expect(textEditor.numParagraphs).toBe(4);
}); });
test('Disposing a TextEditor nullifies everything', () => { test("Disposing a TextEditor nullifies everything", () => {
const textEditor = new TextEditor(document.createElement("div")); const textEditor = new TextEditor(document.createElement("div"));
expect(textEditor).toBeInstanceOf(TextEditor); expect(textEditor).toBeInstanceOf(TextEditor);
textEditor.dispose(); textEditor.dispose();
@ -51,7 +51,7 @@ describe('TextEditor', () => {
expect(textEditor.element).toBe(null); expect(textEditor.element).toBe(null);
}); });
test('TextEditor focus should focus the contenteditable element', () => { test("TextEditor focus should focus the contenteditable element", () => {
const textEditorElement = document.createElement("div"); const textEditorElement = document.createElement("div");
document.body.appendChild(textEditorElement); document.body.appendChild(textEditorElement);
const textEditor = new TextEditor(textEditorElement); const textEditor = new TextEditor(textEditorElement);
@ -76,8 +76,8 @@ describe('TextEditor', () => {
const textEditor = new TextEditor(textEditorElement); const textEditor = new TextEditor(textEditorElement);
textEditor.root = textEditor.createRoot([ textEditor.root = textEditor.createRoot([
textEditor.createParagraph([ textEditor.createParagraph([
textEditor.createInlineFromString("Hello, World!") textEditor.createInlineFromString("Hello, World!"),
]) ]),
]); ]);
expect(textEditor).toBeInstanceOf(TextEditor); expect(textEditor).toBeInstanceOf(TextEditor);
textEditor.focus(); textEditor.focus();

View file

@ -6,7 +6,7 @@
* Copyright (c) KALEIDOS INC * Copyright (c) KALEIDOS INC
*/ */
import { mapContentFragmentFromHTML, mapContentFragmentFromString } from '../content/dom/Content.js'; import { mapContentFragmentFromHTML, mapContentFragmentFromString } from "../content/dom/Content.js";
/** /**
* When the user pastes some HTML, what we do is generate * When the user pastes some HTML, what we do is generate

View file

@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from "vitest";
import CommandMutations from './CommandMutations'; import CommandMutations from "./CommandMutations.js";
describe("CommandMutations", () => { describe("CommandMutations", () => {
test("should create a new CommandMutations", () => { test("should create a new CommandMutations", () => {
@ -67,5 +67,5 @@ describe("CommandMutations", () => {
expect(mutations.added).toBe(null); expect(mutations.added).toBe(null);
expect(mutations.updated).toBe(null); expect(mutations.updated).toBe(null);
expect(mutations.removed).toBe(null); expect(mutations.removed).toBe(null);
}) });
}); });

View file

@ -1,23 +1,26 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from "vitest";
import { mapContentFragmentFromHTML, mapContentFragmentFromString } from './Content.js'; import {
mapContentFragmentFromHTML,
mapContentFragmentFromString,
} from "./Content.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe('Content', () => { describe("Content", () => {
test("mapContentFragmentFromHTML should return a valid content for the editor", () => { test("mapContentFragmentFromHTML should return a valid content for the editor", () => {
const inertElement = document.createElement("div"); const inertElement = document.createElement("div");
const contentFragment = mapContentFragmentFromHTML( const contentFragment = mapContentFragmentFromHTML(
"<div>Hello, World!</div>", "<div>Hello, World!</div>",
inertElement.style inertElement.style,
); );
expect(contentFragment).toBeInstanceOf(DocumentFragment); expect(contentFragment).toBeInstanceOf(DocumentFragment);
expect(contentFragment.children).toHaveLength(1); expect(contentFragment.children).toHaveLength(1);
expect(contentFragment.firstElementChild).toBeInstanceOf(HTMLDivElement); expect(contentFragment.firstElementChild).toBeInstanceOf(HTMLDivElement);
expect(contentFragment.firstElementChild.firstElementChild).toBeInstanceOf( expect(contentFragment.firstElementChild.firstElementChild).toBeInstanceOf(
HTMLSpanElement HTMLSpanElement,
);
expect(contentFragment.firstElementChild.firstElementChild.firstChild).toBeInstanceOf(
Text
); );
expect(
contentFragment.firstElementChild.firstElementChild.firstChild,
).toBeInstanceOf(Text);
expect(contentFragment.textContent).toBe("Hello, World!"); expect(contentFragment.textContent).toBe("Hello, World!");
}); });
@ -25,18 +28,18 @@ describe('Content', () => {
const inertElement = document.createElement("div"); const inertElement = document.createElement("div");
const contentFragment = mapContentFragmentFromHTML( const contentFragment = mapContentFragmentFromHTML(
"<div>Hello,<br/><span> World!</span><br/></div>", "<div>Hello,<br/><span> World!</span><br/></div>",
inertElement.style inertElement.style,
); );
expect(contentFragment).toBeInstanceOf(DocumentFragment); expect(contentFragment).toBeInstanceOf(DocumentFragment);
expect(contentFragment.children).toHaveLength(1); expect(contentFragment.children).toHaveLength(1);
expect(contentFragment.firstElementChild).toBeInstanceOf(HTMLDivElement); expect(contentFragment.firstElementChild).toBeInstanceOf(HTMLDivElement);
expect(contentFragment.firstElementChild.children).toHaveLength(2); expect(contentFragment.firstElementChild.children).toHaveLength(2);
expect(contentFragment.firstElementChild.firstElementChild).toBeInstanceOf( expect(contentFragment.firstElementChild.firstElementChild).toBeInstanceOf(
HTMLSpanElement HTMLSpanElement,
);
expect(contentFragment.firstElementChild.firstElementChild.firstChild).toBeInstanceOf(
Text
); );
expect(
contentFragment.firstElementChild.firstElementChild.firstChild,
).toBeInstanceOf(Text);
expect(contentFragment.textContent).toBe("Hello, World!"); expect(contentFragment.textContent).toBe("Hello, World!");
}); });
@ -49,42 +52,46 @@ describe('Content', () => {
const inertElement = document.createElement("div"); const inertElement = document.createElement("div");
const contentFragment = mapContentFragmentFromHTML( const contentFragment = mapContentFragmentFromHTML(
"<div>Lorem ipsum</div><div>Dolor sit amet</div><div><br/></div><div>Sed iaculis blandit odio ornare sagittis.</div>", "<div>Lorem ipsum</div><div>Dolor sit amet</div><div><br/></div><div>Sed iaculis blandit odio ornare sagittis.</div>",
inertElement.style inertElement.style,
); );
expect(contentFragment).toBeInstanceOf(DocumentFragment); expect(contentFragment).toBeInstanceOf(DocumentFragment);
expect(contentFragment.children).toHaveLength(3); expect(contentFragment.children).toHaveLength(3);
for (let index = 0; index < contentFragment.children.length; index++) { for (let index = 0; index < contentFragment.children.length; index++) {
expect(contentFragment.children.item(index)).toBeInstanceOf(HTMLDivElement); expect(contentFragment.children.item(index)).toBeInstanceOf(
expect(contentFragment.children.item(index).firstElementChild).toBeInstanceOf( HTMLDivElement,
HTMLSpanElement
); );
expect( expect(
contentFragment.children.item(index).firstElementChild.firstChild contentFragment.children.item(index).firstElementChild,
).toBeInstanceOf(HTMLSpanElement);
expect(
contentFragment.children.item(index).firstElementChild.firstChild,
).toBeInstanceOf(Text); ).toBeInstanceOf(Text);
expect(contentFragment.children.item(index).textContent).toBe(paragraphs[index]); expect(contentFragment.children.item(index).textContent).toBe(
paragraphs[index],
);
} }
expect(contentFragment.textContent).toBe("Lorem ipsumDolor sit ametSed iaculis blandit odio ornare sagittis."); expect(contentFragment.textContent).toBe(
"Lorem ipsumDolor sit ametSed iaculis blandit odio ornare sagittis.",
);
}); });
test("mapContentFragmentFromString should return a valid content for the editor", () => { test("mapContentFragmentFromString should return a valid content for the editor", () => {
const contentFragment = mapContentFragmentFromString( const contentFragment = mapContentFragmentFromString("Hello, \nWorld!");
"Hello, \nWorld!"
);
expect(contentFragment).toBeInstanceOf(DocumentFragment); expect(contentFragment).toBeInstanceOf(DocumentFragment);
expect(contentFragment.children).toHaveLength(2); expect(contentFragment.children).toHaveLength(2);
expect(contentFragment.children.item(0)).toBeInstanceOf(HTMLDivElement); expect(contentFragment.children.item(0)).toBeInstanceOf(HTMLDivElement);
expect(contentFragment.children.item(1)).toBeInstanceOf(HTMLDivElement); expect(contentFragment.children.item(1)).toBeInstanceOf(HTMLDivElement);
expect(contentFragment.children.item(0).firstElementChild).toBeInstanceOf( expect(contentFragment.children.item(0).firstElementChild).toBeInstanceOf(
HTMLSpanElement HTMLSpanElement,
);
expect(contentFragment.children.item(0).firstElementChild.firstChild).toBeInstanceOf(
Text
);
expect(contentFragment.children.item(1).firstElementChild).toBeInstanceOf(
HTMLSpanElement
); );
expect( expect(
contentFragment.children.item(1).firstElementChild.firstChild contentFragment.children.item(0).firstElementChild.firstChild,
).toBeInstanceOf(Text);
expect(contentFragment.children.item(1).firstElementChild).toBeInstanceOf(
HTMLSpanElement,
);
expect(
contentFragment.children.item(1).firstElementChild.firstChild,
).toBeInstanceOf(Text); ).toBeInstanceOf(Text);
expect(contentFragment.textContent).toBe("Hello, World!"); expect(contentFragment.textContent).toBe("Hello, World!");
}); });

View file

@ -1,11 +1,17 @@
import { describe, test, expect } from "vitest"; import { describe, test, expect } from "vitest";
import { createElement, isElement, createRandomId, isOffsetAtStart, isOffsetAtEnd } from "./Element.js"; import {
createElement,
isElement,
createRandomId,
isOffsetAtStart,
isOffsetAtEnd,
} from "./Element.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe("Element", () => { describe("Element", () => {
test("createRandomId should create a new random id", () => { test("createRandomId should create a new random id", () => {
const randomId = createRandomId(); const randomId = createRandomId();
expect(typeof randomId).toBe('string'); expect(typeof randomId).toBe("string");
expect(randomId.length).toBeGreaterThan(0); expect(randomId.length).toBeGreaterThan(0);
expect(randomId.length).toBeLessThan(12); expect(randomId.length).toBeLessThan(12);
}); });
@ -20,8 +26,8 @@ describe("Element", () => {
const element = createElement("div", { const element = createElement("div", {
attributes: { attributes: {
"aria-multiline": true, "aria-multiline": true,
"role": "textbox" role: "textbox",
} },
}); });
expect(element.ariaMultiLine).toBe("true"); expect(element.ariaMultiLine).toBe("true");
expect(element.role).toBe("textbox"); expect(element.role).toBe("textbox");
@ -30,8 +36,8 @@ describe("Element", () => {
test("createElement should create a new element with data- properties", () => { test("createElement should create a new element with data- properties", () => {
const element = createElement("div", { const element = createElement("div", {
data: { data: {
itype: "root" itype: "root",
} },
}); });
expect(element.dataset.itype).toBe("root"); expect(element.dataset.itype).toBe("root");
}); });
@ -41,14 +47,14 @@ describe("Element", () => {
styles: { styles: {
"text-decoration": "underline", "text-decoration": "underline",
}, },
allowedStyles: [["text-decoration"]] allowedStyles: [["text-decoration"]],
}); });
expect(element.style.textDecoration).toBe("underline"); expect(element.style.textDecoration).toBe("underline");
}); });
test("createElement should create a new element with a child", () => { test("createElement should create a new element with a child", () => {
const element = createElement("div", { const element = createElement("div", {
children: new Text("Hello, World!") children: new Text("Hello, World!"),
}); });
expect(element.textContent).toBe("Hello, World!"); expect(element.textContent).toBe("Hello, World!");
}); });
@ -59,16 +65,18 @@ describe("Element", () => {
createElement("div", { createElement("div", {
children: [ children: [
createElement("div", { createElement("div", {
children: new Text("Hello, World!") children: new Text("Hello, World!"),
}) }),
] ],
}) }),
], ],
}); });
expect(element.textContent).toBe("Hello, World!"); expect(element.textContent).toBe("Hello, World!");
expect(element.firstChild.nodeType).toBe(Node.ELEMENT_NODE); expect(element.firstChild.nodeType).toBe(Node.ELEMENT_NODE);
expect(element.firstChild.firstChild.nodeType).toBe(Node.ELEMENT_NODE); expect(element.firstChild.firstChild.nodeType).toBe(Node.ELEMENT_NODE);
expect(element.firstChild.firstChild.firstChild.nodeType).toBe(Node.TEXT_NODE); expect(element.firstChild.firstChild.firstChild.nodeType).toBe(
Node.TEXT_NODE,
);
}); });
test("isElement returns true if the passed element is the expected element", () => { test("isElement returns true if the passed element is the expected element", () => {
@ -81,9 +89,9 @@ describe("Element", () => {
}); });
test("isOffsetAtStart should return true when offset is 0", () => { test("isOffsetAtStart should return true when offset is 0", () => {
const element = createElement('span', { const element = createElement("span", {
children: new Text("Hello") children: new Text("Hello"),
}) });
expect(isOffsetAtStart(element, 0)).toBe(true); expect(isOffsetAtStart(element, 0)).toBe(true);
}); });

View file

@ -1,12 +1,24 @@
import { describe, test, expect } from "vitest"; import { describe, test, expect } from "vitest";
import { createEmptyInline, createInline, getInline, getInlineLength, isInline, isInlineEnd, isInlineStart, isLikeInline, splitInline, TAG, TYPE } from "./Inline.js"; import {
createEmptyInline,
createInline,
getInline,
getInlineLength,
isInline,
isInlineEnd,
isInlineStart,
isLikeInline,
splitInline,
TAG,
TYPE,
} from "./Inline.js";
import { createLineBreak } from "./LineBreak.js"; import { createLineBreak } from "./LineBreak.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe("Inline", () => { describe("Inline", () => {
test("createInline should throw when passed an invalid child", () => { test("createInline should throw when passed an invalid child", () => {
expect(() => createInline("Hello, World!")).toThrowError( expect(() => createInline("Hello, World!")).toThrowError(
"Invalid inline child" "Invalid inline child",
); );
}); });
@ -44,7 +56,7 @@ describe("Inline", () => {
expect(isInline(a)).toBe(false); expect(isInline(a)).toBe(false);
const b = null; const b = null;
expect(isInline(b)).toBe(false); expect(isInline(b)).toBe(false);
const c = document.createElement('span'); const c = document.createElement("span");
expect(isInline(c)).toBe(false); expect(isInline(c)).toBe(false);
}); });
@ -82,11 +94,11 @@ describe("Inline", () => {
test("getInline ", () => { test("getInline ", () => {
expect(getInline(null)).toBe(null); expect(getInline(null)).toBe(null);
}) });
test("getInlineLength throws when the passed node is not an inline", () => { test("getInlineLength throws when the passed node is not an inline", () => {
const inline = document.createElement('div'); const inline = document.createElement("div");
expect(() => getInlineLength(inline)).toThrowError('Invalid inline'); expect(() => getInlineLength(inline)).toThrowError("Invalid inline");
}); });
test("getInlineLength returns the length of the inline content", () => { test("getInlineLength returns the length of the inline content", () => {

View file

@ -1,11 +1,11 @@
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from "vitest";
import { createLineBreak } from './LineBreak.js'; import { createLineBreak } from "./LineBreak.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe('LineBreak', () => { describe("LineBreak", () => {
test("createLineBreak should return a <br> element", () => { test("createLineBreak should return a <br> element", () => {
const br = createLineBreak(); const br = createLineBreak();
expect(br.nodeType).toBe(Node.ELEMENT_NODE); expect(br.nodeType).toBe(Node.ELEMENT_NODE);
expect(br.nodeName).toBe('BR'); expect(br.nodeName).toBe("BR");
}) });
}); });

View file

@ -7,6 +7,7 @@
*/ */
import { import {
createRandomId,
createElement, createElement,
isElement, isElement,
isOffsetAtStart, isOffsetAtStart,
@ -24,8 +25,6 @@ import {
} from "./Inline.js"; } from "./Inline.js";
import { createLineBreak, isLineBreak } from "./LineBreak.js"; import { createLineBreak, isLineBreak } from "./LineBreak.js";
import { setStyles } from "./Style.js"; import { setStyles } from "./Style.js";
import { createRandomId } from "./Element.js";
import { isEmptyTextNode, isTextNode } from './TextNode.js';
export const TAG = "DIV"; export const TAG = "DIV";
export const TYPE = "paragraph"; export const TYPE = "paragraph";

View file

@ -18,9 +18,9 @@ import { createInline, isInline } from "./Inline.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe("Paragraph", () => { describe("Paragraph", () => {
test("createParagraph should throw when passed invalid children", () => { test("createParagraph should throw when passed invalid children", () => {
expect(() => createParagraph([ expect(() => createParagraph(["Whatever"])).toThrowError(
"Whatever" "Invalid paragraph children",
])).toThrowError("Invalid paragraph children"); );
}); });
test("createEmptyParagraph should create a new empty paragraph", () => { test("createEmptyParagraph should create a new empty paragraph", () => {
@ -33,30 +33,30 @@ describe("Paragraph", () => {
test("isParagraph should return true when the passed node is a paragraph", () => { test("isParagraph should return true when the passed node is a paragraph", () => {
expect(isParagraph(null)).toBe(false); expect(isParagraph(null)).toBe(false);
expect(isParagraph(document.createElement('div'))).toBe(false); expect(isParagraph(document.createElement("div"))).toBe(false);
expect(isParagraph(document.createElement('h1'))).toBe(false); expect(isParagraph(document.createElement("h1"))).toBe(false);
expect(isParagraph(createEmptyParagraph())).toBe(true); expect(isParagraph(createEmptyParagraph())).toBe(true);
expect(isParagraph(createParagraph([ expect(
createInline(new Text('Hello, World!')) isParagraph(createParagraph([createInline(new Text("Hello, World!"))])),
]))).toBe(true); ).toBe(true);
}); });
test("isLikeParagraph should return true when node looks like a paragraph", () => { test("isLikeParagraph should return true when node looks like a paragraph", () => {
const p = document.createElement('p'); const p = document.createElement("p");
expect(isLikeParagraph(p)).toBe(true); expect(isLikeParagraph(p)).toBe(true);
const div = document.createElement('div'); const div = document.createElement("div");
expect(isLikeParagraph(div)).toBe(true); expect(isLikeParagraph(div)).toBe(true);
const h1 = document.createElement('h1'); const h1 = document.createElement("h1");
expect(isLikeParagraph(h1)).toBe(true); expect(isLikeParagraph(h1)).toBe(true);
const h2 = document.createElement('h2'); const h2 = document.createElement("h2");
expect(isLikeParagraph(h2)).toBe(true); expect(isLikeParagraph(h2)).toBe(true);
const h3 = document.createElement('h3'); const h3 = document.createElement("h3");
expect(isLikeParagraph(h3)).toBe(true); expect(isLikeParagraph(h3)).toBe(true);
const h4 = document.createElement('h4'); const h4 = document.createElement("h4");
expect(isLikeParagraph(h4)).toBe(true); expect(isLikeParagraph(h4)).toBe(true);
const h5 = document.createElement('h5'); const h5 = document.createElement("h5");
expect(isLikeParagraph(h5)).toBe(true); expect(isLikeParagraph(h5)).toBe(true);
const h6 = document.createElement('h6'); const h6 = document.createElement("h6");
expect(isLikeParagraph(h6)).toBe(true); expect(isLikeParagraph(h6)).toBe(true);
}); });
@ -69,7 +69,7 @@ describe("Paragraph", () => {
test("getParagraph should return null if there aren't closer paragraph nodes", () => { test("getParagraph should return null if there aren't closer paragraph nodes", () => {
const text = new Text("Hello, World!"); const text = new Text("Hello, World!");
const whatever = document.createElement('div'); const whatever = document.createElement("div");
whatever.appendChild(text); whatever.appendChild(text);
expect(getParagraph(text)).toBe(null); expect(getParagraph(text)).toBe(null);
}); });
@ -81,7 +81,7 @@ describe("Paragraph", () => {
test("isParagraphStart should return true on a paragraph", () => { test("isParagraphStart should return true on a paragraph", () => {
const paragraph = createParagraph([ const paragraph = createParagraph([
createInline(new Text("Hello, World!")) createInline(new Text("Hello, World!")),
]); ]);
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true); expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true);
}); });
@ -162,11 +162,10 @@ describe("Paragraph", () => {
const nonEmptyInline = document.createElement("span"); const nonEmptyInline = document.createElement("span");
nonEmptyInline.dataset.itype = "inline"; nonEmptyInline.dataset.itype = "inline";
nonEmptyInline.appendChild(new Text('Not empty!')); nonEmptyInline.appendChild(new Text("Not empty!"));
const nonEmptyParagraph = document.createElement("div"); const nonEmptyParagraph = document.createElement("div");
nonEmptyParagraph.dataset.itype = "paragraph"; nonEmptyParagraph.dataset.itype = "paragraph";
nonEmptyParagraph.appendChild(nonEmptyInline); nonEmptyParagraph.appendChild(nonEmptyInline);
expect(isEmptyParagraph(nonEmptyParagraph)).toBe(false); expect(isEmptyParagraph(nonEmptyParagraph)).toBe(false);
}); });
}); });

View file

@ -6,10 +6,9 @@
* Copyright (c) KALEIDOS INC * Copyright (c) KALEIDOS INC
*/ */
import { createElement, isElement } from "./Element.js"; import { createRandomId, createElement, isElement } from "./Element.js";
import { createEmptyParagraph, isParagraph } from "./Paragraph.js"; import { createEmptyParagraph, isParagraph } from "./Paragraph.js";
import { setStyles } from "./Style.js"; import { setStyles } from "./Style.js";
import { createRandomId } from "./Element.js";
export const TAG = "DIV"; export const TAG = "DIV";
export const TYPE = "root"; export const TYPE = "root";

View file

@ -1,11 +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", () => {
test("createRoot should throw when passed invalid children", () => { test("createRoot should throw when passed invalid children", () => {
expect(() => createRoot(["Whatever"])).toThrowError( expect(() => createRoot(["Whatever"])).toThrowError(
"Invalid root children" "Invalid root children",
); );
}); });
@ -16,18 +16,20 @@ describe("Root", () => {
expect(emptyRoot.dataset.itype).toBe(TYPE); expect(emptyRoot.dataset.itype).toBe(TYPE);
expect(emptyRoot.firstChild).toBeInstanceOf(HTMLDivElement); expect(emptyRoot.firstChild).toBeInstanceOf(HTMLDivElement);
expect(emptyRoot.firstChild.firstChild).toBeInstanceOf(HTMLSpanElement); expect(emptyRoot.firstChild.firstChild).toBeInstanceOf(HTMLSpanElement);
expect(emptyRoot.firstChild.firstChild.firstChild).toBeInstanceOf(HTMLBRElement); expect(emptyRoot.firstChild.firstChild.firstChild).toBeInstanceOf(
HTMLBRElement,
);
}); });
test("setRootStyles should apply only the styles of root to the root", () => { test("setRootStyles should apply only the styles of root to the root", () => {
const emptyRoot = createEmptyRoot(); const emptyRoot = createEmptyRoot();
setRootStyles(emptyRoot, { setRootStyles(emptyRoot, {
["--vertical-align"]: "top", ["--vertical-align"]: "top",
["font-size"]: "25px" ["font-size"]: "25px",
}); });
expect(emptyRoot.style.getPropertyValue("--vertical-align")).toBe("top"); expect(emptyRoot.style.getPropertyValue("--vertical-align")).toBe("top");
// We expect this style to be empty because we don't apply it // We expect this style to be empty because we don't apply it
// to the root. // to the root.
expect(emptyRoot.style.getPropertyValue("font-size")).toBe(""); expect(emptyRoot.style.getPropertyValue("font-size")).toBe("");
}) });
}); });

View file

@ -1,5 +1,11 @@
import { describe, test, expect, vi } from "vitest"; import { describe, test, expect, vi } from "vitest";
import { getStyles, isDisplayBlock, isDisplayInline, setStyle, setStyles } from "./Style.js"; import {
getStyles,
isDisplayBlock,
isDisplayInline,
setStyle,
setStyles,
} from "./Style.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe("Style", () => { describe("Style", () => {
@ -39,16 +45,16 @@ describe("Style", () => {
test("getStyles should retrieve a list of allowed styles", () => { test("getStyles should retrieve a list of allowed styles", () => {
const element = document.createElement("div"); const element = document.createElement("div");
element.style.display = 'block'; element.style.display = "block";
element.style.textDecoration = 'underline'; element.style.textDecoration = "underline";
element.style.fontSize = '32px'; element.style.fontSize = "32px";
const textDecorationStyles = getStyles(element, [["text-decoration"]]); const textDecorationStyles = getStyles(element, [["text-decoration"]]);
expect(textDecorationStyles).toStrictEqual({ expect(textDecorationStyles).toStrictEqual({
"text-decoration": "underline" "text-decoration": "underline",
}); });
const displayStyles = getStyles(element, [["display"]]); const displayStyles = getStyles(element, [["display"]]);
expect(displayStyles).toStrictEqual({ expect(displayStyles).toStrictEqual({
"display": "block", display: "block",
}); });
const fontSizeStyles = getStyles(element, [["font-size", "px"]]); const fontSizeStyles = getStyles(element, [["font-size", "px"]]);
expect(fontSizeStyles).toStrictEqual({ expect(fontSizeStyles).toStrictEqual({

View file

@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from "vitest";
import { isTextNode, getTextNodeLength } from './TextNode.js'; import { isTextNode, getTextNodeLength } from "./TextNode.js";
import { createLineBreak } from './LineBreak.js'; import { createLineBreak } from "./LineBreak.js";
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
describe("TextNode", () => { describe("TextNode", () => {
@ -11,16 +11,18 @@ describe("TextNode", () => {
expect(isTextNode("hola")).toBe(false); expect(isTextNode("hola")).toBe(false);
expect(isTextNode({})).toBe(false); expect(isTextNode({})).toBe(false);
expect(isTextNode([])).toBe(false); expect(isTextNode([])).toBe(false);
expect(() => isTextNode(undefined)).toThrowError('Invalid text node'); expect(() => isTextNode(undefined)).toThrowError("Invalid text node");
expect(() => isTextNode(null)).toThrowError('Invalid text node'); expect(() => isTextNode(null)).toThrowError("Invalid text node");
expect(() => isTextNode(0)).toThrowError('Invalid text node'); expect(() => isTextNode(0)).toThrowError("Invalid text node");
}); });
test("getTextNodeLength should return the length of the text node or 0 if it is a <br>", () => { test("getTextNodeLength should return the length of the text node or 0 if it is a <br>", () => {
expect(getTextNodeLength(new Text("Hello, World!"))).toBe(13); expect(getTextNodeLength(new Text("Hello, World!"))).toBe(13);
expect(getTextNodeLength(createLineBreak())).toBe(0); expect(getTextNodeLength(createLineBreak())).toBe(0);
expect(() => getTextNodeLength(undefined)).toThrowError('Invalid text node'); expect(() => getTextNodeLength(undefined)).toThrowError(
expect(() => getTextNodeLength(null)).toThrowError('Invalid text node'); "Invalid text node",
expect(() => getTextNodeLength(0)).toThrowError('Invalid text node'); );
expect(() => getTextNodeLength(null)).toThrowError("Invalid text node");
expect(() => getTextNodeLength(0)).toThrowError("Invalid text node");
}); });
}); });

View file

@ -10,11 +10,11 @@ describe("TextNodeIterator", () => {
test("Create a new TextNodeIterator with an invalid root should throw", () => { test("Create a new TextNodeIterator with an invalid root should throw", () => {
expect(() => new TextNodeIterator(null)).toThrowError("Invalid root node"); expect(() => new TextNodeIterator(null)).toThrowError("Invalid root node");
expect(() => new TextNodeIterator(Infinity)).toThrowError( expect(() => new TextNodeIterator(Infinity)).toThrowError(
"Invalid root node" "Invalid root node",
); );
expect(() => new TextNodeIterator(1)).toThrowError("Invalid root node"); expect(() => new TextNodeIterator(1)).toThrowError("Invalid root node");
expect(() => new TextNodeIterator("hola")).toThrowError( expect(() => new TextNodeIterator("hola")).toThrowError(
"Invalid root node" "Invalid root node",
); );
}); });
@ -26,7 +26,10 @@ describe("TextNodeIterator", () => {
createInline(new Text("Whatever")), createInline(new Text("Whatever")),
]), ]),
createParagraph([createInline(createLineBreak())]), createParagraph([createInline(createLineBreak())]),
createParagraph([createInline(new Text("This is a ")), createInline(new Text("test"))]), createParagraph([
createInline(new Text("This is a ")),
createInline(new Text("test")),
]),
createParagraph([createInline(new Text("Hi!"))]), createParagraph([createInline(new Text("Hi!"))]),
]); ]);

View file

@ -1,15 +1,15 @@
import { expect, describe, test, vi } from 'vitest' import { expect, describe, test, vi } from "vitest";
import ChangeController from './ChangeController' import ChangeController from "./ChangeController.js";
describe("ChangeController", () => { describe("ChangeController", () => {
test("Creating a ChangeController without a valid time should throw", () => { test("Creating a ChangeController without a valid time should throw", () => {
expect(() => new ChangeController(Infinity)).toThrowError('Invalid time') expect(() => new ChangeController(Infinity)).toThrowError("Invalid time");
}); });
test("A ChangeController should dispatch an event when `notifyImmediately` is called", () => { test("A ChangeController should dispatch an event when `notifyImmediately` is called", () => {
const changeListener = vi.fn(); const changeListener = vi.fn();
const changeController = new ChangeController(10); const changeController = new ChangeController(10);
changeController.addEventListener("change", changeListener) changeController.addEventListener("change", changeListener);
changeController.notifyImmediately(); changeController.notifyImmediately();
expect(changeController.hasPendingChanges).toBe(false); expect(changeController.hasPendingChanges).toBe(false);
expect(changeListener).toBeCalled(1); expect(changeListener).toBeCalled(1);

View file

@ -16,7 +16,6 @@ import {
isInlineStart, isInlineStart,
isInlineEnd, isInlineEnd,
setInlineStyles, setInlineStyles,
mergeInlines,
splitInline, splitInline,
createEmptyInline, createEmptyInline,
} from "../content/dom/Inline.js"; } from "../content/dom/Inline.js";
@ -29,7 +28,6 @@ import {
isParagraphEnd, isParagraphEnd,
setParagraphStyles, setParagraphStyles,
splitParagraph, splitParagraph,
splitParagraphAtNode,
mergeParagraphs, mergeParagraphs,
fixParagraph, fixParagraph,
} from "../content/dom/Paragraph.js"; } from "../content/dom/Paragraph.js";
@ -48,9 +46,6 @@ import { 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";
const SAFE_GUARD = true;
const SAFE_GUARD_TIME = true;
/** /**
* Supported options for the SelectionController. * Supported options for the SelectionController.
* *

View file

@ -6,7 +6,7 @@ test("Create selection DOM rects from client rects", () => {
const rect = new DOMRect(20, 20, 100, 50); const rect = new DOMRect(20, 20, 100, 50);
const clientRects = [ const clientRects = [
new DOMRect(20, 20, 100, 20), new DOMRect(20, 20, 100, 20),
new DOMRect(20, 50, 50, 20) new DOMRect(20, 50, 50, 20),
]; ];
const fragment = createSelectionImposterFromClientRects(rect, clientRects); const fragment = createSelectionImposterFromClientRects(rect, clientRects);
expect(fragment).toBeInstanceOf(DocumentFragment); expect(fragment).toBeInstanceOf(DocumentFragment);

View file

@ -1,7 +1,7 @@
import { createRoot } from "~/editor/content/dom/Root"; import { createRoot } from "../editor/content/dom/Root.js";
import { createParagraph } from "~/editor/content/dom/Paragraph"; import { createParagraph } from "../editor/content/dom/Paragraph.js";
import { createEmptyInline, createInline } from "~/editor/content/dom/Inline"; import { createEmptyInline, createInline } from "../editor/content/dom/Inline.js";
import { createLineBreak } from "~/editor/content/dom/LineBreak"; import { createLineBreak } from "../editor/content/dom/LineBreak.js";
export class TextEditorMock extends EventTarget { export class TextEditorMock extends EventTarget {
/** /**

View file

@ -1,26 +1,23 @@
import { resolve } from "node:path"; import { resolve } from "node:path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { coverageConfigDefaults } from 'vitest/config' import { coverageConfigDefaults } from "vitest/config"
export default defineConfig({ export default defineConfig({
root: "./src",
resolve: { resolve: {
alias: { alias: {
"~": resolve("."), "~": resolve("./src"),
}, },
}, },
build: { build: {
minify: false, minify: true,
sourcemap: true, sourcemap: true,
lib: { lib: {
entry: "editor/TextEditor.js", entry: "src/editor/TextEditor.js",
name: "TextEditor", name: "TextEditor",
fileName: "TextEditor", fileName: "TextEditor",
formats: ["es"], formats: ["es"],
}, },
terserOptions: {
compress: true,
mangle: true,
},
}, },
test: { test: {
coverage: { coverage: {

View file

@ -518,6 +518,7 @@ __metadata:
esbuild: "npm:^0.24.0" esbuild: "npm:^0.24.0"
jsdom: "npm:^25.0.0" jsdom: "npm:^25.0.0"
playwright: "npm:^1.45.1" playwright: "npm:^1.45.1"
prettier: "npm:^3.3.3"
vite: "npm:^5.3.1" vite: "npm:^5.3.1"
vitest: "npm:^1.6.0" vitest: "npm:^1.6.0"
languageName: unknown languageName: unknown
@ -2292,6 +2293,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prettier@npm:^3.3.3":
version: 3.3.3
resolution: "prettier@npm:3.3.3"
bin:
prettier: bin/prettier.cjs
checksum: 10c0/b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26
languageName: node
linkType: hard
"pretty-format@npm:^29.7.0": "pretty-format@npm:^29.7.0":
version: 29.7.0 version: 29.7.0
resolution: "pretty-format@npm:29.7.0" resolution: "pretty-format@npm:29.7.0"

View file

@ -396,6 +396,10 @@ msgstr "The token has no expiration date"
msgid "dashboard.add-shared" msgid "dashboard.add-shared"
msgstr "Add as Shared Library" msgstr "Add as Shared Library"
#: src/app/main/ui/workspace/main_menu.cljs:607
msgid "dashboard.show-version-history"
msgstr "Version history"
#: src/app/main/ui/settings/profile.cljs:72 #: src/app/main/ui/settings/profile.cljs:72
msgid "dashboard.change-email" msgid "dashboard.change-email"
msgstr "Change email" msgstr "Change email"

View file

@ -398,6 +398,10 @@ msgstr "El token no tiene fecha de expiración"
msgid "dashboard.add-shared" msgid "dashboard.add-shared"
msgstr "Añadir como Biblioteca Compartida" msgstr "Añadir como Biblioteca Compartida"
#: src/app/main/ui/workspace/main_menu.cljs:607
msgid "dashboard.show-version-history"
msgstr "Histórico de versiones"
#: src/app/main/ui/settings/profile.cljs:72 #: src/app/main/ui/settings/profile.cljs:72
msgid "dashboard.change-email" msgid "dashboard.change-email"
msgstr "Cambiar correo" msgstr "Cambiar correo"