json-joy
Version:
Collection of libraries for building collaborative editing apps.
148 lines (147 loc) • 5.47 kB
JavaScript
import { printTree } from 'tree-dump';
import { AvlMap } from 'sonic-forest/lib/avl/AvlMap';
import { InputController } from './InputController';
import { CursorController } from './CursorController';
import { RichTextController } from './RichTextController';
import { KeyController } from './KeyController';
import { CompositionController } from './CompositionController';
import { AnnalsController } from './annals/AnnalsController';
import { ElementAttr } from '../constants';
import { Anchor } from '../../../json-crdt-extensions/peritext/rga/constants';
import { UiHandle } from '../../events/defaults/ui/UiHandle';
import { compare } from '../../../json-crdt-patch';
export class DomController {
opts;
txt;
et;
keys;
comp;
input;
cursor;
richText;
annals;
/**
* Index of block HTML <div> elements keyed by the ID (timestamp) of the split
* boundary that starts that block element.
*/
blocks = new AvlMap(compare);
/**
* Index of inline HTML <span> elements keyed by the slice start {@link Point}.
*/
inlines = new AvlMap((a, b) => a.cmpSpatial(b));
constructor(opts) {
this.opts = opts;
const { source, events, log } = opts;
const { txt } = events;
this.txt = txt;
const et = (this.et = opts.events.et);
const keys = (this.keys = new KeyController({ source }));
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 });
this.annals = new AnnalsController({ et, txt, log });
const uiHandle = new UiHandle(txt, this);
events.ui = uiHandle;
events.undo = this.annals;
}
/** -------------------------------------------------- {@link UiLifeCycles} */
start() {
const stopKeys = this.keys.start();
const stopComp = this.comp.start();
const stopInput = this.input.start();
const stopCursor = this.cursor.start();
const stopRichText = this.richText.start();
const stopAnnals = this.annals.start();
return () => {
stopKeys();
stopComp();
stopInput();
stopCursor();
stopRichText();
stopAnnals();
};
}
/** ------------------------------------------------- {@link PeritextUiApi} */
focus() {
this.opts.source.focus();
}
getSpans(blockInnerId) {
let el;
if (blockInnerId) {
const txt = this.txt;
const marker = txt.overlay.getOrNextLowerMarker(blockInnerId);
const markerId = marker?.id ?? txt.str.id;
el = this.blocks.get(markerId);
}
el ??= this.opts.source;
return el.querySelectorAll('.jsonjoy-peritext-inline');
}
findSpanContaining(char) {
const start = char.start;
const overlayPoint = this.txt.overlay.getOrNextLower(start);
if (overlayPoint) {
const span = this.inlines.get(overlayPoint);
if (span) {
const inline = span[ElementAttr.InlineOffset];
if (inline) {
const contains = inline.contains(char);
if (contains)
return span;
}
}
}
const spans = this.getSpans(start);
const length = spans.length;
for (let i = 0; i < length; i++) {
const span = spans[i];
const inline = span[ElementAttr.InlineOffset];
if (inline) {
const contains = inline.contains(char);
if (contains)
return span;
}
}
return;
}
getCharRect(char) {
const txt = this.opts.events.txt;
const id = typeof char === 'number' ? txt.str.find(char) : char;
if (!id)
return;
const start = txt.point(id, Anchor.Before);
const end = txt.point(id, Anchor.After);
const charRange = txt.range(start, end);
const span = this.findSpanContaining(charRange);
if (!span)
return;
const inline = span[ElementAttr.InlineOffset];
if (!inline)
return;
const textNode = span.firstChild;
if (!textNode)
return;
const range = document.createRange();
range.selectNode(textNode);
const offset = Math.max(0, Math.min(textNode.length - 1, charRange.start.viewPos() - inline.start.viewPos()));
range.setStart(textNode, offset);
range.setEnd(textNode, offset + 1);
const rects = range.getClientRects();
return rects[0];
}
caretRect() {
return document.getElementById(this.cursor.caretId)?.getBoundingClientRect?.();
}
/** ----------------------------------------------------- {@link Printable} */
toString(tab) {
return ('DOM' +
printTree(tab, [
(tab) => 'blocks: ' + this.blocks.size(),
(tab) => 'inlines: ' + this.inlines.size(),
(tab) => this.cursor.toString(tab),
(tab) => this.keys.toString(tab),
(tab) => this.comp.toString(tab),
(tab) => this.annals.toString(tab),
]));
}
}