Skip to content

Commit

Permalink
Merge pull request #740 from streamich/peritext-event-improvements
Browse files Browse the repository at this point in the history
Peritext `insert` and `delete` events
  • Loading branch information
streamich authored Nov 3, 2024
2 parents 2b77daf + 38cfd15 commit 882aead
Show file tree
Hide file tree
Showing 7 changed files with 380 additions and 29 deletions.
4 changes: 4 additions & 0 deletions src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class Editor<T = string> {
this.local = new EditorSlices(txt, txt.localSlices);
}

public text(): string {
return this.txt.strApi().view();
}

// ------------------------------------------------------------------ cursors

public addCursor(range: Range<T>, anchor: CursorAnchor = CursorAnchor.Start): Cursor<T> {
Expand Down
6 changes: 1 addition & 5 deletions src/json-crdt-peritext-ui/dom/InputController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,7 @@ export class InputController implements UiLifeCycles {
et.insert(event.data);
} else {
const item = event.dataTransfer ? event.dataTransfer.items[0] : null;
if (item) {
item.getAsString((text) => {
et.insert(text);
});
}
if (item) item.getAsString((text) => et.insert(text));
}
break;
}
Expand Down
25 changes: 14 additions & 11 deletions src/json-crdt-peritext-ui/events/PeritextEventDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import type {EditorSlices} from '../../json-crdt-extensions/peritext/editor/Edit
import type {PeritextEventHandlerMap, PeritextEventTarget} from './PeritextEventTarget';
import type * as events from './types';

/**
* Implementation of default handlers for Peritext events, such as "insert",
* "delete", "cursor", etc. These implementations are used by the
* {@link PeritextEventTarget} to provide default behavior for each event type.
* If `event.preventDefault()` is called on a Peritext event, the default handler
* will not be executed.
*/
export class PeritextEventDefaults implements PeritextEventHandlerMap {
public constructor(
protected readonly txt: Peritext,
Expand All @@ -14,17 +21,18 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
};

public readonly insert = (event: CustomEvent<events.InsertDetail>) => {
if (event.defaultPrevented) return;
const text = event.detail.text;
this.txt.editor.insert(text);
this.et.change(event);
};

public readonly delete = (event: CustomEvent<events.DeleteDetail>) => {
if (event.defaultPrevented) return;
const {direction = -1, unit = 'char'} = event.detail;
this.txt.editor.delete(direction, unit);
this.et.change(event);
const {len = -1, unit = 'char', at} = event.detail;
const editor = this.txt.editor;
if (at !== undefined) {
const point = editor.point(at);
editor.cursor.set(point);
}
editor.delete(len, unit);
};

public readonly cursor = (event: CustomEvent<events.CursorDetail>) => {
Expand Down Expand Up @@ -57,15 +65,13 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
}
}
}
this.et.change(event);
return;
}

// If `edge` is specified.
const isSpecificEdgeSelected = edge === 'focus' || edge === 'anchor';
if (isSpecificEdgeSelected) {
editor.move(len, unit ?? 'char', edge === 'focus' ? 0 : 1, false);
this.et.change(event);
return;
}

Expand All @@ -77,14 +83,12 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
if (len > 0) cursor.collapseToEnd();
else cursor.collapseToStart();
}
this.et.change(event);
return;
}

// If `unit` is specified.
if (unit) {
editor.select(unit);
this.et.change(event);
return;
}
};
Expand All @@ -103,7 +107,6 @@ export class PeritextEventDefaults implements PeritextEventHandlerMap {
default:
slices.insOverwrite(type, data);
}
this.et.change(event);
};

public readonly marker = (event: CustomEvent<events.MarkerDetail>) => {
Expand Down
20 changes: 13 additions & 7 deletions src/json-crdt-peritext-ui/events/PeritextEventTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,35 @@ export type PeritextEventHandlerMap = {
[K in keyof PeritextEventMap]: (event: CustomEvent<PeritextEventMap[K]>) => void;
};

let id = 0;
let __id = 0;

export class PeritextEventTarget extends TypedEventTarget<PeritextEventMap> {
public readonly id: number = id++;
public readonly id: number = __id++;

public defaults: Partial<PeritextEventHandlerMap> = {};

public dispatch<K extends keyof PeritextEventMap>(type: K, detail: PeritextEventMap[K]): void {
public dispatch<K extends keyof Omit<PeritextEventMap, 'change'>>(
type: K,
detail: Omit<PeritextEventMap, 'change'>[K],
): void {
const event = new CustomEvent<PeritextEventMap[K]>(type, {detail});
this.dispatchEvent(event);
this.defaults[type]?.(event);
if (!event.defaultPrevented) this.defaults[type]?.(event);
this.change(event);
}

public change(ev?: CustomEvent<any>): void {
this.dispatch('change', {ev});
const event = new CustomEvent<PeritextEventMap['change']>('change', {detail: {ev}});
this.dispatchEvent(event);
if (!event.defaultPrevented) this.defaults.change?.(event);
}

public insert(text: string): void {
this.dispatch('insert', {text});
}

public delete(direction?: -1 | 0 | 1, unit?: DeleteDetail['unit']): void {
this.dispatch('delete', {direction, unit});
public delete(len: DeleteDetail['len'], unit?: DeleteDetail['unit'], at?: DeleteDetail['at']): void {
this.dispatch('delete', {len, unit, at});
}

public cursor(detail: CursorDetail): void {
Expand Down
228 changes: 228 additions & 0 deletions src/json-crdt-peritext-ui/events/__tests__/delete.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import {type Kit, runAlphabetKitTestSuite} from '../../../json-crdt-extensions/peritext/__tests__/setup';
import {PeritextEventDefaults} from '../PeritextEventDefaults';
import {PeritextEventTarget} from '../PeritextEventTarget';

const testSuite = (getKit: () => Kit) => {
const setup = () => {
const kit = getKit();
const et = new PeritextEventTarget();
const defaults = new PeritextEventDefaults(kit.peritext, et);
et.defaults = defaults;
return {...kit, et};
};

test('can delete by a single character backwards', async () => {
const kit = setup();
kit.editor.cursor.setAt(10);
expect(kit.peritext.str.view()).toBe('abcdefghijklmnopqrstuvwxyz');
kit.et.delete(-1);
expect(kit.peritext.str.view()).toBe('abcdefghiklmnopqrstuvwxyz');
kit.et.delete(-1);
expect(kit.peritext.str.view()).toBe('abcdefghklmnopqrstuvwxyz');
kit.editor.cursor.setAt(5);
kit.et.delete(-1);
expect(kit.peritext.str.view()).toBe('abcdfghklmnopqrstuvwxyz');
});

describe('characters', () => {
describe('can delete all characters backwards', () => {
test('one character at a time', async () => {
const kit = setup();
const view = kit.peritext.str.view() as string;
const length = view.length;
kit.editor.cursor.setAt(view.length);
expect(kit.peritext.str.view()).toBe(view);
for (let i = length - 1; i >= 0; i--) {
kit.et.delete(-1);
expect(kit.peritext.str.view()).toBe(view.slice(0, i));
}
expect(kit.peritext.str.view()).toBe('');
kit.peritext.refresh();
expect(kit.peritext.str.view()).toBe('');
});

test('two characters at a time', async () => {
const kit = setup();
const view = kit.peritext.str.view() as string;
const step = 2;
kit.editor.cursor.setAt(view.length);
let i = 1;
expect(kit.peritext.str.view()).toBe(view);
while (kit.editor.text()) {
kit.et.delete(-step);
expect(kit.peritext.str.view()).toBe(view.slice(0, view.length - i * step));
i++;
}
expect(kit.peritext.str.view()).toBe('');
});

test('five characters at a time', async () => {
const kit = setup();
const view = kit.peritext.str.view() as string;
const step = 5;
kit.editor.cursor.setAt(view.length);
let i = 1;
expect(kit.peritext.str.view()).toBe(view);
while (kit.editor.text()) {
kit.et.delete(-step);
if (!kit.editor.text()) break;
expect(kit.peritext.str.view()).toBe(view.slice(0, view.length - i * step));
i++;
}
expect(kit.editor.text()).toBe('');
});
});

describe('can delete all characters forwards', () => {
test('one character at a time', async () => {
const kit = setup();
const view = kit.peritext.str.view() as string;
const length = view.length;
kit.editor.cursor.setAt(0);
expect(kit.peritext.str.view()).toBe(view);
for (let i = 1; i <= length; i++) {
kit.et.delete(1);
expect(kit.editor.text()).toBe(view.slice(i));
}
expect(kit.peritext.str.view()).toBe('');
kit.peritext.refresh();
expect(kit.peritext.str.view()).toBe('');
});

test('two characters at a time', async () => {
const kit = setup();
const view = kit.peritext.str.view() as string;
const step = 2;
kit.editor.cursor.setAt(0);
let i = 1;
expect(kit.peritext.str.view()).toBe(view);
while (kit.editor.text()) {
kit.et.delete(step);
expect(kit.peritext.str.view()).toBe(view.slice(i * step));
i++;
}
expect(kit.peritext.str.view()).toBe('');
});

test('five characters at a time', async () => {
const kit = setup();
const view = kit.peritext.str.view() as string;
const step = 5;
kit.editor.cursor.setAt(0);
let i = 1;
expect(kit.peritext.str.view()).toBe(view);
while (kit.editor.text()) {
kit.et.delete(step);
if (!kit.editor.text()) break;
expect(kit.peritext.str.view()).toBe(view.slice(i * step));
i++;
}
expect(kit.editor.text()).toBe('');
});
});
});

describe('words', () => {
test('can delete a word backwards', async () => {
const kit = setup();
kit.editor.cursor.setAt(10);
kit.et.insert(' ');
kit.editor.cursor.setAt(5);
kit.et.insert(' ');
kit.editor.cursor.setAt(8);
expect(kit.editor.text()).toBe('abcde fghij klmnopqrstuvwxyz');
kit.et.delete(-1, 'word');
expect(kit.editor.text()).toBe('abcde hij klmnopqrstuvwxyz');
});

test('can delete a word forwards', async () => {
const kit = setup();
kit.editor.cursor.setAt(10);
kit.et.insert(' ');
kit.editor.cursor.setAt(5);
kit.et.insert(' ');
kit.editor.cursor.setAt(8);
expect(kit.editor.text()).toBe('abcde fghij klmnopqrstuvwxyz');
kit.et.delete(1, 'word');
expect(kit.editor.text()).toBe('abcde fg klmnopqrstuvwxyz');
});

test('can delete a word in both directions', async () => {
const kit = setup();
kit.editor.cursor.setAt(10);
kit.et.insert(' ');
kit.editor.cursor.setAt(5);
kit.et.insert(' ');
kit.editor.cursor.setAt(8);
expect(kit.editor.text()).toBe('abcde fghij klmnopqrstuvwxyz');
const DIRECTION = 0;
kit.et.delete(DIRECTION, 'word');
expect(kit.editor.text()).toBe('abcde klmnopqrstuvwxyz');
});

test('can delete a word at specific position', async () => {
const kit = setup();
kit.editor.cursor.setAt(10);
kit.et.insert(' ');
kit.editor.cursor.setAt(5);
kit.et.insert(' ');
kit.editor.delCursors();
expect(kit.editor.text()).toBe('abcde fghij klmnopqrstuvwxyz');
kit.et.delete(0, 'word', 8);
expect(kit.editor.text()).toBe('abcde klmnopqrstuvwxyz');
expect(kit.editor.cursor.start.viewPos()).toBe(6);
expect(kit.editor.cursor.isCollapsed()).toBe(true);
kit.et.delete(0, 'word', 3);
expect(kit.editor.text()).toBe(' klmnopqrstuvwxyz');
expect(kit.editor.cursor.start.viewPos()).toBe(0);
expect(kit.editor.cursor.isCollapsed()).toBe(true);
kit.et.delete(0, 'word', 10);
expect(kit.editor.text()).toBe(' ');
expect(kit.editor.cursor.start.viewPos()).toBe(2);
expect(kit.editor.cursor.isCollapsed()).toBe(true);
});
});

describe('lines', () => {
test('can delete a line backwards', async () => {
const kit = setup();
kit.editor.cursor.setAt(10);
kit.et.insert('\n');
kit.editor.cursor.setAt(5);
kit.et.insert('\n');
kit.editor.cursor.setAt(8);
expect(kit.editor.text()).toBe('abcde\nfghij\nklmnopqrstuvwxyz');
kit.et.delete(-1, 'line');
expect(kit.editor.text()).toBe('abcde\nhij\nklmnopqrstuvwxyz');
});

test('can delete a line forwards', async () => {
const kit = setup();
kit.editor.cursor.setAt(10);
kit.et.insert('\n');
kit.editor.cursor.setAt(5);
kit.et.insert('\n');
kit.editor.cursor.setAt(8);
expect(kit.editor.text()).toBe('abcde\nfghij\nklmnopqrstuvwxyz');
kit.et.delete(1, 'line');
expect(kit.editor.text()).toBe('abcde\nfg\nklmnopqrstuvwxyz');
});

test('can delete a word in both directions', async () => {
const kit = setup();
kit.editor.cursor.setAt(10);
kit.et.insert('\n');
kit.editor.cursor.setAt(5);
kit.et.insert('\n');
kit.editor.cursor.setAt(8);
expect(kit.editor.text()).toBe('abcde\nfghij\nklmnopqrstuvwxyz');
const DIRECTION = 0;
kit.et.delete(DIRECTION, 'line');
expect(kit.editor.text()).toBe('abcde\n\nklmnopqrstuvwxyz');
});
});
};

describe('"delete" event', () => {
runAlphabetKitTestSuite(testSuite);
});
Loading

0 comments on commit 882aead

Please sign in to comment.