UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

185 lines (184 loc) 6.61 kB
import { getCursorPosition, unit } from '../util'; import { ElementAttr } from '../constants'; import { throttle } from '../../../util/throttle'; import { ValueSyncStore } from '../../../util/events/sync-store'; /** * Controller for handling text selection and cursor movements. Listens to * naive browser events and translates them into Peritext events. */ export class CursorController { opts; caretId; _cursor; constructor(opts) { this.opts = opts; this.caretId = 'jsonjoy.com-peritext-caret-' + opts.et.id; this._cursor = throttle(opts.et.cursor.bind(opts.et), 25); } /** The position where user started to scrub the text. */ selAnchor = -1; /** * String position at coordinate, or -1, if unknown. */ posAtPoint(x, y) { const res = getCursorPosition(x, y); if (res) { let node = res[0]; const offset = res[1]; for (let i = 0; i < 5 && node; i++) { const inline = node[ElementAttr.InlineOffset]; const pos = inline?.pos?.(); if (typeof pos === 'number') return pos + offset; node = node.parentNode; } } return -1; } /** -------------------------------------------------- {@link UiLifeCycles} */ start() { const el = this.opts.source; 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. }; } focus = new ValueSyncStore(false); onFocus = (event) => { this.focus.next(true); }; onBlur = () => { this.focus.next(false); }; x = 0; y = 0; mouseDown = new ValueSyncStore(false); onMouseDown = (ev) => { if (!this.focus.value && this.opts.txt.editor.hasCursor()) return; const { clientX, clientY } = ev; this.x = clientX; this.y = clientY; 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.opts.keys.pressed; const et = this.opts.et; if (pressed.has('Shift')) { ev.preventDefault(); et.cursor({ at, unit: 'word' }); } else if (pressed.has('Alt')) { ev.preventDefault(); et.cursor({ at, edge: 'new' }); } else { this.mouseDown.next(true); ev.preventDefault(); et.cursor({ at }); } break; } case 2: this.mouseDown.next(false); ev.preventDefault(); this.opts.et.cursor({ unit: 'word' }); break; case 3: this.mouseDown.next(false); ev.preventDefault(); this.opts.et.cursor({ unit: 'block' }); break; case 4: this.mouseDown.next(false); ev.preventDefault(); this.opts.et.cursor({ unit: 'all' }); break; } }; 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]({ at: to, edge: 'focus' }); }; onMouseUp = (ev) => { this.mouseDown.next(false); }; onKeyDown = (event) => { 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; et.move(direction, 'vert', event.shiftKey ? 'focus' : 'both'); 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, 'point'); 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} */ toString(tab) { return `cursor { focus: ${this.focus.value}, x: ${this.x}, y: ${this.y}, mouseDown: ${this.mouseDown.value} }`; } }