Skip to content

Commit

Permalink
Merge pull request #750 from streamich/peritext-inline-events
Browse files Browse the repository at this point in the history
Peritext rendering surface improvements
  • Loading branch information
streamich authored Nov 4, 2024
2 parents 813c224 + 986f70f commit cc9aaa3
Show file tree
Hide file tree
Showing 30 changed files with 490 additions and 233 deletions.
56 changes: 56 additions & 0 deletions src/json-crdt-peritext-ui/dom/CompositionController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type {PeritextEventTarget} from '../events/PeritextEventTarget';
import type {UiLifeCycles} from './types';
import type {Peritext} from '../../json-crdt-extensions/peritext';
import type {Printable} from 'tree-dump';

export interface CompositionControllerOpts {
source: HTMLElement;
txt: Peritext;
et: PeritextEventTarget;
}

export class CompositionController implements UiLifeCycles, Printable {
public composing = false;
public data: string = '';

public constructor(public readonly opts: CompositionControllerOpts) {}

/** -------------------------------------------------- {@link UiLifeCycles} */

public start(): void {
const el = this.opts.source;
el.addEventListener('compositionstart', this.onStart);
el.addEventListener('compositionupdate', this.onUpdate);
el.addEventListener('compositionend', this.onEnd);
}

public stop(): void {
const el = this.opts.source;
el.removeEventListener('compositionstart', this.onStart);
el.removeEventListener('compositionupdate', this.onUpdate);
el.removeEventListener('compositionend', this.onEnd);
}

private onStart = (event: CompositionEvent): void => {
this.composing = true;
this.data = event.data;
};

private onUpdate = (event: CompositionEvent): void => {
this.composing = true;
this.data = event.data;
};

private onEnd = (event: CompositionEvent): void => {
this.composing = false;
this.data = '';
const text = event.data;
if (text) this.opts.et.insert(text);
};

/** ----------------------------------------------------- {@link Printable} */

public toString(tab?: string): string {
return `composition { composing: ${this.composing}, data: ${JSON.stringify(this.data)} }`;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {getCursorPosition} from './util';
import {getCursorPosition, unit} from './util';
import {ElementAttr} from '../constants';
import {throttle} from '../../util/throttle';
import {ValueSyncStore} from '../../util/events/sync-store';
import type {Printable} from 'tree-dump';
import type {KeyController} from './KeyController';
import type {PeritextEventTarget} from '../events/PeritextEventTarget';
import type {Rect, UiLifeCycles} from './types';
import type {Peritext} from '../../json-crdt-extensions/peritext';
import type {Inline} from '../../json-crdt-extensions/peritext/block/Inline';

export interface SelectionControllerOpts {
export interface CursorControllerOpts {
/**
* Element to attach the controller to, this element will be used to listen to
* "beforeinput" events and will be put into "contenteditable" mode.
Expand All @@ -22,11 +24,10 @@ export interface SelectionControllerOpts {
* Controller for handling text selection and cursor movements. Listens to
* naive browser events and translates them into Peritext events.
*/
export class SelectionController implements UiLifeCycles {
protected isMouseDown: boolean = false;
export class CursorController implements UiLifeCycles, Printable {
public readonly caretId: string;

public constructor(public readonly opts: SelectionControllerOpts) {
public constructor(public readonly opts: CursorControllerOpts) {
this.caretId = 'jsonjoy.com-peritext-caret-' + opts.et.id;
}

Expand Down Expand Up @@ -96,33 +97,46 @@ export class SelectionController implements UiLifeCycles {

public start(): void {
const el = this.opts.source;
el.contentEditable = 'true';
el.addEventListener('mousedown', this.onMouseDown);
el.addEventListener('keydown', this.onKeyDown);
el.addEventListener('focus', this.onFocus);
el.addEventListener('blur', this.onBlur);
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
}

public stop(): void {
const el = this.opts.source;
if (el) el.contentEditable = 'false';
el.removeEventListener('mousedown', this.onMouseDown);
el.removeEventListener('keydown', this.onKeyDown);
el.removeEventListener('focus', this.onFocus);
el.removeEventListener('blur', this.onBlur);
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
this._cursor[1](); // Stop throttling loop.
}

private clientX = 0;
private clientY = 0;
public readonly focus = new ValueSyncStore<boolean>(false);

private readonly onFocus = (): void => {
this.focus.next(true);
};

private readonly onBlur = (): void => {
this.focus.next(false);
};

private x = 0;
private y = 0;
private mouseDown: boolean = false;

private readonly onMouseDown = (ev: MouseEvent): void => {
const {clientX, clientY} = ev;
this.clientX = clientX;
this.clientY = clientY;
this.x = clientX;
this.y = clientY;
switch (ev.detail) {
case 1: {
this.isMouseDown = false;
this.mouseDown = false;
const at = this.posAtPoint(clientX, clientY);
if (at === -1) return;
this.selAnchor = at;
Expand All @@ -135,59 +149,60 @@ export class SelectionController implements UiLifeCycles {
ev.preventDefault();
et.cursor({at, edge: 'new'});
} else {
this.isMouseDown = true;
this.mouseDown = true;
ev.preventDefault();
et.cursor({at});
}
break;
}
case 2:
this.isMouseDown = false;
this.mouseDown = false;
ev.preventDefault();
this.opts.et.cursor({unit: 'word'});
break;
case 3:
this.isMouseDown = false;
this.mouseDown = false;
ev.preventDefault();
this.opts.et.cursor({unit: 'block'});
break;
case 4:
this.isMouseDown = false;
this.mouseDown = false;
ev.preventDefault();
this.opts.et.cursor({unit: 'all'});
break;
}
};

private readonly onMouseMove = (ev: MouseEvent): void => {
if (!this.isMouseDown) return;
if (!this.mouseDown) return;
const at = this.selAnchor;
if (at < 0) return;
const {clientX, clientY} = ev;
const to = this.posAtPoint(clientX, clientY);
if (to < 0) return;
ev.preventDefault();
const mouseHasNotMoved = clientX === this.clientX && clientY === this.clientY;
const mouseHasNotMoved = clientX === this.x && clientY === this.y;
if (mouseHasNotMoved) return;
this.clientX = clientX;
this.clientY = clientY;
this.x = clientX;
this.y = clientY;
this._cursor[0]({at: to, edge: 'focus'});
};

private readonly onMouseUp = (ev: MouseEvent): void => {
this.isMouseDown = false;
this.mouseDown = false;
};

private onKeyDown = (event: KeyboardEvent): void => {
const key = event.key;
if (event.isComposing || key === 'Dead') return;
const et = this.opts.et;
switch (key) {
case 'ArrowUp':
case 'ArrowDown': {
event.preventDefault();
const direction = key === 'ArrowUp' ? -1 : 1;
const at = this.getNextLinePos(direction);
if (at !== undefined) {
event.preventDefault();
if (event.shiftKey) {
et.cursor({at, edge: 'focus'});
} else {
Expand All @@ -196,6 +211,36 @@ export class SelectionController implements UiLifeCycles {
}
break;
}
case 'ArrowLeft':
case 'ArrowRight': {
const direction = key === 'ArrowLeft' ? -1 : 1;
event.preventDefault();
if (event.shiftKey) et.move(direction, unit(event) || 'char', 'focus');
else if (event.metaKey) et.move(direction, 'line');
else if (event.altKey || event.ctrlKey) et.move(direction, 'word');
else et.move(direction);
break;
}
case 'Home':
case 'End': {
event.preventDefault();
const direction = key === 'End' ? 1 : -1;
const edge = event.shiftKey ? 'focus' : 'both';
et.move(direction, 'line', edge);
return;
}
case 'a':
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
et.cursor({unit: 'all'});
return;
}
}
};

/** ----------------------------------------------------- {@link Printable} */

public toString(tab?: string): string {
return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown} }`;
}
}
67 changes: 67 additions & 0 deletions src/json-crdt-peritext-ui/dom/DomController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {printTree, type Printable} from 'tree-dump';
import {InputController} from '../dom/InputController';
import {CursorController} from '../dom/CursorController';
import {RichTextController} from '../dom/RichTextController';
import {PeritextEventDefaults} from '../events/PeritextEventDefaults';
import {PeritextEventTarget} from '../events/PeritextEventTarget';
import {KeyController} from '../dom/KeyController';
import {CompositionController} from '../dom/CompositionController';
import type {UiLifeCycles} from '../dom/types';
import type {Peritext} from '../../json-crdt-extensions';

export interface DomControllerOpts {
source: HTMLElement;
txt: Peritext;
}

export class DomController implements UiLifeCycles, Printable {
public readonly et: PeritextEventTarget;
public readonly keys: KeyController;
public readonly comp: CompositionController;
public readonly input: InputController;
public readonly cursor: CursorController;
public readonly richText: RichTextController;

constructor(public readonly opts: DomControllerOpts) {
const {source, txt} = opts;
const et = (this.et = new PeritextEventTarget());
const defaults = new PeritextEventDefaults(txt, et);
et.defaults = defaults;
const keys = (this.keys = new KeyController());
const comp = (this.comp = new CompositionController({et, source, txt}));
this.input = new InputController({et, source, txt, comp});
this.cursor = new CursorController({et, source, txt, keys});
this.richText = new RichTextController({et, source, txt});
}

/** -------------------------------------------------- {@link UiLifeCycles} */

public start(): void {
this.keys.start();
this.comp.start();
this.input.start();
this.cursor.start();
this.richText.start();
}

public stop(): void {
this.keys.stop();
this.comp.stop();
this.input.stop();
this.cursor.stop();
this.richText.stop();
}

/** ----------------------------------------------------- {@link Printable} */

public toString(tab?: string): string {
return (
'DOM' +
printTree(tab, [
(tab) => this.cursor.toString(tab),
(tab) => this.keys.toString(tab),
(tab) => this.comp.toString(tab),
])
);
}
}
Loading

0 comments on commit cc9aaa3

Please sign in to comment.