Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support OT for table #3590

Merged
merged 31 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c84c356
Support retain embed
luin May 28, 2021
6d7a147
Add table handler
luin May 30, 2021
cab87ef
Add random testing for table operations
luin May 30, 2021
d1d40e5
Upgrade delta
luin May 31, 2021
cc0ad88
Register table handler in a module
luin Jun 14, 2021
6e40f11
Fix composePosition
luin Jun 18, 2021
4ff9d39
Fix the test case for invert tables
luin Jun 20, 2021
af4f1eb
Update delta for embed transform
luin Jul 22, 2021
61a2fd5
Rename table to table-embed to avoid name conflicts
luin Aug 20, 2021
4da5c38
Upgrade delta for embed retaining
luin Aug 20, 2021
22d7cf6
Add table handler
luin May 30, 2021
a9c5343
Register table handler in a module
luin Jun 14, 2021
855433f
Add table handler
luin May 30, 2021
ee1db9b
Register table handler in a module
luin Jun 14, 2021
37e02ae
Ignore self-manage blots
luin Jul 22, 2021
8083458
Add scroll embed change event
luin Jul 22, 2021
8664379
Upgrade delta to fix invert
luin Jul 22, 2021
7104087
Allow different Delta constructors
luin Jul 26, 2021
c7e3c37
Rename event names
luin Aug 18, 2021
b65ccee
Support updating embed content
luin Aug 18, 2021
b7b3131
Expose composePosition
luin Oct 5, 2021
0c4122c
Fix getRange not work with nested Quill
luin Oct 21, 2021
d78cc96
Ignore keyboard events triggered from nested quill instances
luin Oct 21, 2021
89e4d68
Quill.find should accept bubble parameter
luin Nov 8, 2021
5268696
Upgrade Parchment to make sure Scroll#find() not return nested scroll…
luin Nov 8, 2021
3a88c93
Merge branch 'develop' into zh-table-ot
luin Nov 29, 2021
a1e81da
Merge branch 'develop' into zh-table-ot
luin Dec 17, 2021
e17f0a1
Merge branch 'develop' into zh-table-ot
luin Feb 14, 2022
4313e90
Merge branch 'develop' into zh-table-ot
luin Feb 18, 2022
fbb887b
Merge branch 'develop' into zh-table-ot
luin Mar 3, 2022
d00df81
Upgrade Parchment to 2.0
luin May 31, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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