Import text-editor code into the repository

This commit is contained in:
Andrey Antukh 2024-11-19 17:05:30 +01:00
parent 68397edd4d
commit 04a0d867b0
65 changed files with 11112 additions and 7 deletions

View file

@ -0,0 +1,92 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Copyright (c) KALEIDOS INC
*/
/**
* Change controller is responsible of notifying when a change happens.
*/
export class ChangeController extends EventTarget {
/**
* Keeps the timeout id.
*
* @type {number}
*/
#timeout = null;
/**
* Keeps the time at which we're going to
* call the debounced change calls.
*
* @type {number}
*/
#time = 1000;
/**
* Keeps if we have some pending changes or not.
*
* @type {boolean}
*/
#hasPendingChanges = false;
/**
* Constructor
*
* @param {number} [time=500]
*/
constructor(time = 500) {
super()
if (typeof time === "number" && (!Number.isInteger(time) || time <= 0)) {
throw new TypeError("Invalid time");
}
this.#time = time ?? 500;
}
/**
* Indicates that there are some pending changes.
*
* @type {boolean}
*/
get hasPendingChanges() {
return this.#hasPendingChanges;
}
#onTimeout = () => {
this.dispatchEvent(new Event("change"));
};
/**
* Tells the ChangeController that a change has been made
* but that you need to delay the notification (and debounce)
* for sometime.
*/
notifyDebounced() {
this.#hasPendingChanges = true;
clearTimeout(this.#timeout);
this.#timeout = setTimeout(this.#onTimeout, this.#time);
}
/**
* Tells the ChangeController that a change should be notified
* immediately.
*/
notifyImmediately() {
clearTimeout(this.#timeout);
this.#onTimeout();
}
/**
* Disposes the referenced resources.
*/
dispose() {
if (this.hasPendingChanges) {
this.notifyImmediately();
}
clearTimeout(this.#timeout);
}
}
export default ChangeController;

View file

@ -0,0 +1,36 @@
import { expect, describe, test, vi } from 'vitest'
import ChangeController from './ChangeController'
describe("ChangeController", () => {
test("Creating a ChangeController without a valid time should throw", () => {
expect(() => new ChangeController(Infinity)).toThrowError('Invalid time')
});
test("A ChangeController should dispatch an event when `notifyImmediately` is called", () => {
const changeListener = vi.fn();
const changeController = new ChangeController(10);
changeController.addEventListener("change", changeListener)
changeController.notifyImmediately();
expect(changeController.hasPendingChanges).toBe(false);
expect(changeListener).toBeCalled(1);
});
test("A ChangeController should dispatch an event when `notifyDebounced` is called", async () => {
return new Promise((resolve) => {
const changeController = new ChangeController(10);
changeController.addEventListener("change", () => resolve());
changeController.notifyDebounced();
expect(changeController.hasPendingChanges).toBe(true);
});
});
test("A ChangeController should dispatch an event when `notifyDebounced` is called and disposed is called right after", async () => {
return new Promise((resolve) => {
const changeController = new ChangeController(10);
changeController.addEventListener("change", () => resolve());
changeController.notifyDebounced();
expect(changeController.hasPendingChanges).toBe(true);
changeController.dispose();
});
});
});

View file

@ -0,0 +1,34 @@
/**
* Max. amount of time we should allow.
*
* @type {number}
*/
const SAFE_GUARD_TIME = 1000;
/**
* Time at which the safeguard started.
*
* @type {number}
*/
let startTime = Date.now();
/**
* Marks the start of the safeguard.
*/
export function start() {
startTime = Date.now();
}
/**
* Checks if the safeguard should throw.
*/
export function update() {
if (Date.now - startTime >= SAFE_GUARD_TIME) {
throw new Error('Safe guard timeout');
}
}
export default {
start,
update,
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
/**
* Indicates the direction of the selection.
*
* @readonly
* @enum {number}
*/
export const SelectionDirection = {
/** The anchorNode is behind the focusNode */
FORWARD: 1,
/** The focusNode and the anchorNode are collapsed */
NONE: 0,
/** The focusNode is behind the anchorNode */
BACKWARD: -1,
};
export default SelectionDirection;