UNPKG

json-joy

Version:

Collection of libraries for building collaborative editing apps.

121 lines (120 loc) 4.4 kB
import { QuillConst } from './constants'; import { NodeApi } from '../../json-crdt/model/api/nodes'; import { konst } from '../../json-crdt-patch/builder/Konst'; import { SliceStacking } from '../peritext/slice/constants'; import { PersistedSlice } from '../peritext/slice/PersistedSlice'; import { diffAttributes, getAttributes, removeErasures } from './util'; const updateAttributes = (txt, attributes, pos, len) => { if (!attributes) return; const range = txt.rangeAt(pos, len); const keys = Object.keys(attributes); const length = keys.length; const savedSlices = txt.savedSlices; for (let i = 0; i < length; i++) { const key = keys[i]; const value = attributes[key]; if (value === null) { savedSlices.ins(range, SliceStacking.Erase, key); } else { savedSlices.ins(range, SliceStacking.One, key, konst(value)); } } }; const rewriteAttributes = (txt, attributes, pos, len) => { if (typeof attributes !== 'object') return; const range = txt.rangeAt(pos, len); range.expand(); const slices = txt.overlay.findOverlapping(range); const length = slices.size; const relevantOverlappingButNotContained = new Set(); if (length) { const savedSlices = txt.savedSlices; // biome-ignore lint: slices is not iterable slices.forEach((slice) => { if (slice instanceof PersistedSlice) { const isContained = range.contains(slice); if (!isContained) { relevantOverlappingButNotContained.add(slice.type); return; } const type = slice.type; if (type in attributes) { savedSlices.del(slice.id); } } }); } const keys = Object.keys(attributes); const attributeLength = keys.length; const attributesCopy = { ...attributes }; for (let i = 0; i < attributeLength; i++) { const key = keys[i]; const value = attributes[key]; if (value === null && !relevantOverlappingButNotContained.has(key)) { delete attributesCopy[key]; } } updateAttributes(txt, attributesCopy, pos, len); }; const maybeUpdateAttributes = (txt, attributes, pos, len) => { const range = txt.rangeAt(pos, 1); const overlayPoint = txt.overlay.getOrNextLower(range.start); if (!overlayPoint && !attributes) return; if (!overlayPoint) { updateAttributes(txt, removeErasures(attributes), pos, len); return; } const pointAttributes = getAttributes(overlayPoint); const attributeDiff = diffAttributes(pointAttributes, attributes); if (attributeDiff) updateAttributes(txt, attributeDiff, pos, len); }; export class QuillDeltaApi extends NodeApi { text() { return this.api.wrap(this.node.text()); } slices() { return this.api.wrap(this.node.slices()); } apply(ops) { const txt = this.node.txt; const overlay = txt.overlay; const length = ops.length; let pos = 0; for (let i = 0; i < length; i++) { overlay.refresh(true); const op = ops[i]; if (typeof op.retain === 'number') { const { retain, attributes } = op; if (attributes) rewriteAttributes(txt, attributes, pos, retain); pos += retain; } else if (typeof op.delete === 'number') { txt.delAt(pos, op.delete); } else if (op.insert) { const { insert } = op; let { attributes } = op; if (typeof insert === 'string') { txt.insAt(pos, insert); const insertLength = insert.length; maybeUpdateAttributes(txt, attributes, pos, insertLength); pos += insertLength; } else { txt.insAt(pos, QuillConst.EmbedChar); if (!attributes) attributes = {}; attributes[QuillConst.EmbedSliceType] = insert; maybeUpdateAttributes(txt, attributes, pos, 1); pos += 1; } } } } }