json-joy
Version:
Collection of libraries for building collaborative editing apps.
121 lines (120 loc) • 4.4 kB
JavaScript
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;
}
}
}
}
}