UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

437 lines (436 loc) 19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PeritextEventDefaults = void 0; const constants_1 = require("../../../json-crdt-extensions/peritext/slice/constants"); const constants_2 = require("../../../json-crdt-extensions/peritext/rga/constants"); const annals_1 = require("./annals"); const toText = (buf) => new TextDecoder().decode(buf); /** * 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. */ class PeritextEventDefaults { constructor(txt, et, opts = {}) { this.txt = txt; this.et = et; this.opts = opts; this.editorUi = { eol: (point, steps) => { const ui = this.ui; if (!ui) return; const res = ui.getLineEnd(point, steps > 0); return res ? res[0] : void 0; }, vert: (point, steps) => { const ui = this.ui; if (!ui) return; const pos = ui.pointX(point); if (!pos) return; const currLine = ui.getLineInfo(point); if (!currLine) return; const x = pos[0]; const iterations = Math.abs(steps); let nextPoint = point; for (let i = 0; i < iterations; i++) { const nextLine = steps > 0 ? ui.getNextLineInfo(currLine) : ui.getNextLineInfo(currLine, -1); if (!nextLine) break; nextPoint = ui.findPointAtX(x, nextLine); if (!nextPoint) break; if (point.anchor === constants_2.Anchor.Before) nextPoint.refBefore(); else nextPoint.refAfter(); } return nextPoint; }, }; this.change = (event) => { }; this.insert = (event) => { const text = event.detail.text; const editor = this.txt.editor; editor.insert(text); this.undo?.capture(); }; this.delete = (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); this.undo?.capture(); }; this.cursor = (event) => { const { at, edge, len, unit } = event.detail; const txt = this.txt; const editor = txt.editor; // If `at` is specified, it represents the absolute position. We move the // cursor to that position, and leave only one active cursor. All other // are automatically removed when `editor.cursor` getter is accessed. if ((typeof at === 'number' && at >= 0) || typeof at === 'object') { const point = editor.point(at); switch (edge) { case 'focus': case 'anchor': { const cursor = editor.cursor; cursor.setEndpoint(point, edge === 'focus' ? 0 : 1); if (cursor.isCollapsed()) { const start = cursor.start; start.refAfter(); cursor.set(start); } break; } case 'new': { editor.addCursor(txt.range(point)); break; } // both default: { // Select a range from the "at" position to the specified length. if (!!len && typeof len === 'number') { const point2 = editor.skip(point, len, unit ?? 'char', this.editorUi); const range = txt.rangeFromPoints(point, point2); // Sorted range. editor.cursor.set(range.start, range.end, len < 0 ? constants_1.CursorAnchor.End : constants_1.CursorAnchor.Start); } // Set caret (a collapsed cursor) at the specified position. else { point.refAfter(); editor.cursor.set(point); if (unit) editor.select(unit, this.editorUi); } } } return; } // If `edge` is specified. const isSpecificEdgeSelected = edge === 'focus' || edge === 'anchor'; if (isSpecificEdgeSelected) { editor.move(len ?? 0, unit ?? 'char', edge === 'focus' ? 0 : 1, false, this.editorUi); return; } // If `len` is specified. if (len) { const cursor = editor.cursor; if (cursor.isCollapsed()) editor.move(len, unit ?? 'char', void 0, void 0, this.editorUi); else { if (len > 0) cursor.collapseToEnd(); else cursor.collapseToStart(); } return; } // If `unit` is specified. if (unit) { editor.select(unit, this.editorUi); return; } }; this.format = (event) => { const { type, store = 'saved', behavior = 'one', data } = event.detail; const editor = this.txt.editor; const slices = store === 'saved' ? editor.saved : store === 'extra' ? editor.extra : editor.local; switch (behavior) { case 'many': { if (type === undefined) throw new Error('TYPE_REQUIRED'); slices.insStack(type, data); break; } case 'one': { if (type === undefined) throw new Error('TYPE_REQUIRED'); editor.toggleExclFmt(type, data, slices); break; } case 'erase': { if (type === undefined) editor.eraseFormatting(slices); else slices.insErase(type, data); break; } case 'clear': { editor.clearFormatting(slices); break; } } this.undo?.capture(); }; this.marker = (event) => { const { action, type, data } = event.detail; const editor = this.txt.editor; switch (action) { case 'ins': { editor.split(type, data); break; } case 'tog': { if (type === undefined) throw new Error('TYPE_REQUIRED'); editor.tglMarker(type, data); break; } case 'upd': { if (type === undefined) throw new Error('TYPE_REQUIRED'); editor.updMarker(type, data); break; } case 'del': { editor.delMarker(); break; } } this.undo?.capture(); }; this.buffer = async (event) => { const opts = this.opts; const clipboard = opts.clipboard; if (!clipboard) return; const detail = event.detail; const { action, format } = detail; let range; const txt = this.txt; const editor = txt.editor; if (detail.range) { const p1 = editor.point(detail.range[0]); const p2 = editor.point(detail.range[1]); range = txt.rangeFromPoints(p1, p2); } else { range = editor.getCursor()?.range(); if (!range) range = txt.rangeAll(); } if (!range) return; switch (action) { case 'cut': case 'copy': { const copyStyle = () => { if (!range) return; if (range.length() < 1) { range.end.step(1); if (range.length() < 1) range.start.step(-1); } const data = opts.transfer?.toFormat?.(range); clipboard.write(data)?.catch((err) => console.error(err)); }; switch (format) { case 'text': { const text = range.text(); clipboard.writeText(text)?.catch((err) => console.error(err)); if (action === 'cut') editor.collapseCursors(); break; } case 'style': { copyStyle(); break; } case 'html': case 'hast': case 'json': case 'jsonml': case 'mdast': case 'md': case 'fragment': { const transfer = opts.transfer; if (!transfer) return; let text = ''; switch (format) { case 'html': { text = transfer.toHtml(range); break; } case 'hast': { text = JSON.stringify(transfer.toHast(range), null, 2); break; } case 'jsonml': { text = JSON.stringify(transfer.toJson(range), null, 2); break; } case 'json': { text = JSON.stringify(transfer.toView(range), null, 2); break; } case 'mdast': { text = JSON.stringify(transfer.toMdast(range), null, 2); break; } case 'md': { text = transfer.toMarkdown(range); break; } case 'fragment': { text = transfer.toFragment(range) + ''; break; } } clipboard.writeText(text)?.catch((err) => console.error(err)); if (action === 'cut') editor.collapseCursors(); break; } default: { // `auto' const transfer = opts.transfer; if (!transfer) return; if (range.length() < 1) { copyStyle(); } else { const data = transfer.toClipboard(range); clipboard.write(data)?.catch((err) => console.error(err)); if (action === 'cut') editor.collapseCursors(); } } } break; } case 'paste': { switch (format) { case 'text': { const data = await clipboard.read(['text/plain', 'text/html']); let buffer; if ((buffer = data['text/plain'])) { const text = toText(buffer); this.et.insert(text); } else if ((buffer = data['text/html'])) { const html = toText(buffer); const text = opts.transfer?.textFromHtml?.(html) ?? html; this.et.insert(text); } break; } case 'style': { const transfer = opts.transfer; if (transfer) { const { html } = detail.data || (await clipboard.readData()); if (html) { transfer.fromStyle(range, html); this.et.change(); } } break; } case 'html': case 'hast': case 'json': case 'jsonml': case 'mdast': case 'md': { const data = detail.data; const transfer = opts.transfer; if (!transfer) return; let text = data?.text || ''; if (!text) { const clipboardData = await clipboard.read(['text/plain']); const buffer = clipboardData['text/plain']; if (buffer) text = toText(buffer); } if (!range.isCollapsed()) editor.delRange(range); range.collapseToStart(); const start = range.start; const pos = start.viewPos(); let inserted = 0; switch (format) { case 'html': { inserted = transfer.fromHtml(pos, text); break; } case 'hast': { const json = JSON.parse(text); inserted = transfer.fromHast(pos, json); break; } case 'jsonml': { const json = JSON.parse(text); inserted = transfer.fromJson(pos, json); break; } case 'json': { const json = JSON.parse(text); inserted = transfer.fromView(pos, json); break; } case 'mdast': { const json = JSON.parse(text); inserted = transfer.fromMdast(pos, json); break; } case 'md': { inserted = transfer.fromMarkdown(pos, text); break; } } if (inserted) this.et.move(inserted, 'char'); this.et.change(); break; } default: { // 'auto' let data = detail.data; const transfer = opts.transfer; if (!transfer) { let text = data?.text || ''; if (!text) { const clipboardData = await clipboard.read(['text/plain']); const buffer = clipboardData['text/plain']; if (buffer) text = toText(buffer); } if (text) this.et.insert(text); return; } if (!data) data = await clipboard.readData(); const inserted = transfer.fromClipboard(range, data); if (inserted) this.et.move(inserted, 'char'); this.et.change(); } } break; } } this.undo?.capture(); }; this.annals = (event) => { const { batch } = event.detail; this.txt.model.applyBatch(batch); const txt = this.txt; const cursor = (0, annals_1.placeCursor)(txt, batch); if (cursor) txt.editor.cursor.setRange(cursor); }; } } exports.PeritextEventDefaults = PeritextEventDefaults;