json-joy
Version:
Collection of libraries for building collaborative editing apps.
219 lines (218 loc) • 8.51 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CursorController = void 0;
const util_1 = require("../util");
const constants_1 = require("../constants");
const throttle_1 = require("../../../util/throttle");
const sync_store_1 = require("../../../util/events/sync-store");
/**
* Controller for handling text selection and cursor movements. Listens to
* naive browser events and translates them into Peritext events.
*/
class CursorController {
constructor(dom) {
this.dom = dom;
/** The position where user started to scrub the text. */
this.selAnchor = -1;
this.focus = new sync_store_1.ValueSyncStore(false);
this.onFocus = (ev) => {
if (!this.dom.isEditable(ev.target))
return;
this.focus.next(true);
};
this.onBlur = (ev) => {
if (!this.dom.isEditable(ev.target))
return;
this.focus.next(false);
};
this.x = 0;
this.y = 0;
this.mouseDown = new sync_store_1.ValueSyncStore(false);
this.onMouseDown = (ev) => {
if (!this.dom.isEditable(ev.target))
return;
if (!this.focus.value && this.dom.txt.editor.hasCursor())
return;
const { clientX, clientY } = ev;
this.x = clientX;
this.y = clientY;
const et = this.dom.et;
switch (ev.detail) {
case 1: {
this.mouseDown.next(false);
const at = this.posAtPoint(clientX, clientY);
if (at === -1)
return;
this.selAnchor = at;
const pressed = this.dom.keys.pressed;
if (pressed.has('Shift')) {
ev.preventDefault();
et.move([
['anchor', 'word', -1],
['focus', 'word', 1],
], [at]);
}
else if (pressed.has('Alt')) {
ev.preventDefault();
et.cursor({ at: [at], add: true });
}
else {
this.mouseDown.next(true);
ev.preventDefault();
et.cursor({ at: [at] });
}
break;
}
case 2:
this.mouseDown.next(false);
ev.preventDefault();
et.move([
['anchor', 'word', -1],
['focus', 'word', 1],
]);
break;
case 3:
this.mouseDown.next(false);
ev.preventDefault();
et.move([
['start', 'word', -1],
['end', 'word', 1],
]);
break;
case 4:
this.mouseDown.next(false);
ev.preventDefault();
et.move([
['start', 'line', -1],
['end', 'line', 1],
]);
break;
case 5:
this.mouseDown.next(false);
ev.preventDefault();
et.move([
['start', 'block', -1],
['end', 'block', 1],
]);
break;
case 6:
this.mouseDown.next(false);
ev.preventDefault();
et.move([
['start', 'all', -1],
['end', 'all', 1],
]);
break;
}
};
this.onMouseMove = (ev) => {
if (!this.mouseDown.value)
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.x && clientY === this.y;
if (mouseHasNotMoved)
return;
this.x = clientX;
this.y = clientY;
this._cursor[0]({ move: [['focus', to]] });
};
this.onMouseUp = (ev) => {
this.mouseDown.next(false);
};
this.onKeyDown = (event) => {
if (!this.dom.isEditable(event.target))
return;
const key = event.key;
if (event.isComposing || key === 'Dead')
return;
const et = this.dom.et;
switch (key) {
case 'ArrowUp':
case 'ArrowDown': {
event.preventDefault();
et.move('focus', 'vert', key === 'ArrowUp' ? -1 : 1, !event.shiftKey);
break;
}
case 'ArrowLeft':
case 'ArrowRight': {
const direction = key === 'ArrowLeft' ? -1 : 1;
event.preventDefault();
if (event.metaKey)
et.move('focus', 'line', direction, !event.shiftKey);
else if (event.altKey && event.ctrlKey)
et.move('focus', 'point', direction, !event.shiftKey);
else if (event.altKey || event.ctrlKey)
et.move('focus', 'word', direction, !event.shiftKey);
else
et.move('focus', (0, util_1.unit)(event) || 'char', direction, !event.shiftKey);
break;
}
case 'Home':
case 'End': {
event.preventDefault();
const direction = key === 'End' ? 1 : -1;
et.move('focus', 'line', direction, !event.shiftKey);
return;
}
case 'a':
if (event.metaKey || event.ctrlKey) {
event.preventDefault();
et.cursor({ at: [0, 0], move: [['end', 'all', 1]] });
return;
}
}
};
const et = dom.et;
this.caretId = 'jsonjoy.com-peritext-caret-' + et.id;
this._cursor = (0, throttle_1.throttle)(et.cursor.bind(et), 25);
}
/**
* String position at coordinate, or -1, if unknown.
*/
posAtPoint(x, y) {
const res = (0, util_1.getCursorPosition)(x, y);
if (res) {
let node = res[0];
const offset = res[1];
for (let i = 0; i < 5 && node; i++) {
const inline = node[constants_1.ElementAttr.InlineOffset];
const pos = inline?.pos?.();
if (typeof pos === 'number')
return pos + offset;
node = node.parentNode;
}
}
return -1;
}
/** -------------------------------------------------- {@link UiLifeCycles} */
start() {
const el = this.dom.el;
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);
return () => {
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.
};
}
/** ----------------------------------------------------- {@link Printable} */
toString(tab) {
return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown.value} }`;
}
}
exports.CursorController = CursorController;