Skip to content

Commit

Permalink
Merge pull request slab#3590 from quilljs/zh-table-ot
Browse files Browse the repository at this point in the history
Support OT for table
  • Loading branch information
jhchen committed Jun 1, 2022
2 parents 0f20a58 + d00df81 commit 271c47d
Show file tree
Hide file tree
Showing 13 changed files with 1,894 additions and 14 deletions.
1 change: 0 additions & 1 deletion _develop/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ const tsRules = {
loader: 'ts-loader',
options: {
compilerOptions: {
declaration: false,
module: 'es6',
sourceMap: true,
target: 'es6',
Expand Down
15 changes: 14 additions & 1 deletion blots/scroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class Scroll extends ScrollBlot {
this.emitter.emit(Emitter.events.SCROLL_BLOT_UNMOUNT, blot);
}

emitEmbedUpdate(blot, change) {
this.emitter.emit(Emitter.events.SCROLL_EMBED_UPDATE, blot, change);
}

deleteAt(index, length) {
const [first, offset] = this.line(index);
const [last] = this.line(index + length);
Expand Down Expand Up @@ -170,7 +174,7 @@ class Scroll extends ScrollBlot {
}
mutations = mutations.filter(({ target }) => {
const blot = this.find(target, true);
return blot && blot.scroll === this;
return blot && !blot.updateContent;
});
if (mutations.length > 0) {
this.emitter.emit(Emitter.events.SCROLL_BEFORE_UPDATE, source, mutations);
Expand All @@ -180,6 +184,15 @@ class Scroll extends ScrollBlot {
this.emitter.emit(Emitter.events.SCROLL_UPDATE, source, mutations);
}
}

updateEmbedAt(index, key, change) {
// Currently it only supports top-level embeds (BlockEmbed).
// We can update `ParentBlot` in parchment to support inline embeds.
const [blot] = this.descendant(b => b instanceof BlockEmbed, index);
if (blot && blot.statics.blotName === key) {
blot.updateContent(change);
}
}
}
Scroll.blotName = 'scroll';
Scroll.className = 'ql-editor';
Expand Down
6 changes: 6 additions & 0 deletions core/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ class Editor {
scrollLength += length;
} else {
deleteDelta.push(op);

if (op.retain !== null && typeof op.retain === 'object') {
const key = Object.keys(op.retain)[0];
if (key == null) return index;
this.scroll.updateEmbedAt(index, key, op.retain[key]);
}
}
Object.keys(attributes).forEach(name => {
this.scroll.formatAt(index, length, name, attributes[name]);
Expand Down
1 change: 1 addition & 0 deletions core/emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Emitter.events = {
SCROLL_BLOT_UNMOUNT: 'scroll-blot-unmount',
SCROLL_OPTIMIZE: 'scroll-optimize',
SCROLL_UPDATE: 'scroll-update',
SCROLL_EMBED_UPDATE: 'scroll-embed-update',
SELECTION_CHANGE: 'selection-change',
TEXT_CHANGE: 'text-change',
};
Expand Down
22 changes: 19 additions & 3 deletions core/quill.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ class Quill {
logger.level(limit);
}

static find(node) {
return instances.get(node) || globalRegistry.find(node);
static find(node, bubble = false) {
return instances.get(node) || globalRegistry.find(node, bubble);
}

static import(name) {
Expand Down Expand Up @@ -109,6 +109,22 @@ class Quill {
source,
);
});
this.emitter.on(Emitter.events.SCROLL_EMBED_UPDATE, (blot, delta) => {
const oldRange = this.selection.lastRange;
const [newRange] = this.selection.getRange();
const selectionInfo =
oldRange && newRange ? { oldRange, newRange } : undefined;
modify.call(
this,
() => {
const change = new Delta()
.retain(blot.offset(this))
.retain({ [blot.statics.blotName]: delta });
return this.editor.update(change, [], selectionInfo);
},
Quill.sources.USER,
);
});
if (html) {
const contents = this.clipboard.convert({
html: `${html}<p><br></p>`,
Expand Down Expand Up @@ -609,7 +625,7 @@ function shiftRange(range, index, length, source) {
if (range == null) return null;
let start;
let end;
if (index instanceof Delta) {
if (index && typeof index.transformPosition === 'function') {
[start, end] = [range.index, range.index + range.length].map(pos =>
index.transformPosition(pos, source !== Emitter.sources.USER),
);
Expand Down
2 changes: 2 additions & 0 deletions modules/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ class Keyboard extends Module {
);
const matches = bindings.filter(binding => Keyboard.match(evt, binding));
if (matches.length === 0) return;
const blot = Quill.find(evt.target, true);
if (blot && blot.scroll !== this.quill.scroll) return;
const range = this.quill.getSelection();
if (range == null || !this.quill.hasFocus()) return;
const [line, offset] = this.quill.getLine(range.index);
Expand Down
222 changes: 222 additions & 0 deletions modules/tableEmbed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import Delta from 'quill-delta';
import Module from '../core/module';

const parseCellIdentity = identity => {
const parts = identity.split(':');
return [Number(parts[0]) - 1, Number(parts[1]) - 1];
};

const stringifyCellIdentity = (row, column) => `${row + 1}:${column + 1}`;

export const composePosition = (delta, index) => {
let newIndex = index;
const thisIter = Delta.Op.iterator(delta.ops);
let offset = 0;
while (thisIter.hasNext() && offset <= newIndex) {
const length = thisIter.peekLength();
const nextType = thisIter.peekType();
thisIter.next();
switch (nextType) {
case 'delete':
if (length > newIndex - offset) {
return null;
}
newIndex -= length;
break;
case 'insert':
newIndex += length;
offset += length;
break;
default:
offset += length;
break;
}
}
return newIndex;
};

const compactCellData = ({ content, attributes }) => {
const data = {};
if (content.length() > 0) {
data.content = content.ops;
}
if (attributes && Object.keys(attributes).length > 0) {
data.attributes = attributes;
}
return Object.keys(data).length > 0 ? data : null;
};

const compactTableData = ({ rows, columns, cells }) => {
const data = {};
if (rows.length() > 0) {
data.rows = rows.ops;
}

if (columns.length() > 0) {
data.columns = columns.ops;
}

if (Object.keys(cells).length) {
data.cells = cells;
}

return data;
};

const reindexCellIdentities = (cells, { rows, columns }) => {
const reindexedCells = {};
Object.keys(cells).forEach(identity => {
let [row, column] = parseCellIdentity(identity);

row = composePosition(rows, row);
column = composePosition(columns, column);

if (row !== null && column !== null) {
const newPosition = stringifyCellIdentity(row, column);
reindexedCells[newPosition] = cells[identity];
}
}, false);
return reindexedCells;
};

export const tableHandler = {
compose(a, b, keepNull) {
const rows = new Delta(a.rows || []).compose(new Delta(b.rows || []));
const columns = new Delta(a.columns || []).compose(
new Delta(b.columns || []),
);

const cells = reindexCellIdentities(a.cells || {}, {
rows: new Delta(b.rows || []),
columns: new Delta(b.columns || []),
});

Object.keys(b.cells || {}).forEach(identity => {
const aCell = cells[identity] || {};
const bCell = b.cells[identity];

const content = new Delta(aCell.content || []).compose(
new Delta(bCell.content || []),
);

const attributes = Delta.AttributeMap.compose(
aCell.attributes,
bCell.attributes,
keepNull,
);

const cell = compactCellData({ content, attributes });
if (cell) {
cells[identity] = cell;
} else {
delete cells[identity];
}
});

return compactTableData({ rows, columns, cells });
},
transform(a, b, priority) {
const aDeltas = {
rows: new Delta(a.rows || []),
columns: new Delta(a.columns || []),
};

const bDeltas = {
rows: new Delta(b.rows || []),
columns: new Delta(b.columns || []),
};

const rows = aDeltas.rows.transform(bDeltas.rows, priority);
const columns = aDeltas.columns.transform(bDeltas.columns, priority);

const cells = reindexCellIdentities(b.cells || {}, {
rows: bDeltas.rows.transform(aDeltas.rows, !priority),
columns: bDeltas.columns.transform(aDeltas.columns, !priority),
});

Object.keys(a.cells || {}).forEach(identity => {
let [row, column] = parseCellIdentity(identity);
row = composePosition(rows, row);
column = composePosition(columns, column);

if (row !== null && column !== null) {
const newIdentity = stringifyCellIdentity(row, column);

const aCell = a.cells[identity];
const bCell = cells[newIdentity];
if (bCell) {
const content = new Delta(aCell.content || []).transform(
new Delta(bCell.content || []),
priority,
);

const attributes = Delta.AttributeMap.transform(
aCell.attributes,
bCell.attributes,
priority,
);

const cell = compactCellData({ content, attributes });
if (cell) {
cells[newIdentity] = cell;
} else {
delete cells[newIdentity];
}
}
}
});

return compactTableData({ rows, columns, cells });
},
invert(change, base) {
const rows = new Delta(change.rows || []).invert(
new Delta(base.rows || []),
);
const columns = new Delta(change.columns || []).invert(
new Delta(base.columns || []),
);
const cells = reindexCellIdentities(change.cells || {}, {
rows,
columns,
});
Object.keys(cells).forEach(identity => {
const changeCell = cells[identity] || {};
const baseCell = (base.cells || {})[identity] || {};
const content = new Delta(changeCell.content || []).invert(
new Delta(baseCell.content || []),
);
const attributes = Delta.AttributeMap.invert(
changeCell.attributes,
baseCell.attributes,
);
const cell = compactCellData({ content, attributes });
if (cell) {
cells[identity] = cell;
} else {
delete cells[identity];
}
});

// Cells may be removed when their row or column is removed
// by row/column deltas. We should add them back.
Object.keys(base.cells || {}).forEach(identity => {
const [row, column] = parseCellIdentity(identity);
if (
composePosition(new Delta(change.rows || []), row) === null ||
composePosition(new Delta(change.columns || []), column) === null
) {
cells[identity] = base.cells[identity];
}
});

return compactTableData({ rows, columns, cells });
},
};

class TableEmbed extends Module {
static register() {
Delta.registerEmbed('table-embed', tableHandler);
}
}

export default TableEmbed;
Loading

0 comments on commit 271c47d

Please sign in to comment.