diff --git a/_develop/webpack.config.js b/_develop/webpack.config.js
index dbe7eb1201..a96edee143 100644
--- a/_develop/webpack.config.js
+++ b/_develop/webpack.config.js
@@ -85,7 +85,6 @@ const tsRules = {
loader: 'ts-loader',
options: {
compilerOptions: {
- declaration: false,
module: 'es6',
sourceMap: true,
target: 'es6',
diff --git a/blots/scroll.js b/blots/scroll.js
index aa810342ed..32b6e89dee 100644
--- a/blots/scroll.js
+++ b/blots/scroll.js
@@ -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);
@@ -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);
@@ -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';
diff --git a/core/editor.js b/core/editor.js
index 94f6af97a3..661e1c25f7 100644
--- a/core/editor.js
+++ b/core/editor.js
@@ -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]);
diff --git a/core/emitter.js b/core/emitter.js
index 0f9ba4f06a..fa4495c763 100644
--- a/core/emitter.js
+++ b/core/emitter.js
@@ -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',
};
diff --git a/core/quill.js b/core/quill.js
index f24718b3cf..413875b12f 100644
--- a/core/quill.js
+++ b/core/quill.js
@@ -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) {
@@ -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}
`,
@@ -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),
);
diff --git a/modules/keyboard.js b/modules/keyboard.js
index 76bacc8de2..2ba8a84c05 100644
--- a/modules/keyboard.js
+++ b/modules/keyboard.js
@@ -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);
diff --git a/modules/tableEmbed.js b/modules/tableEmbed.js
new file mode 100644
index 0000000000..7f3c3f47b5
--- /dev/null
+++ b/modules/tableEmbed.js
@@ -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;
diff --git a/package-lock.json b/package-lock.json
index a57f16cfcc..f124c6f76b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -348,6 +348,28 @@
"js-tokens": "^4.0.0"
}
},
+ "@babel/node": {
+ "version": "7.14.2",
+ "resolved": "https://registry.npmjs.org/@babel/node/-/node-7.14.2.tgz",
+ "integrity": "sha512-QB/C+Kl6gIYpTjZ/hcZj+chkiAVGcgSHuR849cdNvNJBz4VztO2775/o2ge8imB94EAsLcgkrdWH/3+UIVv1TA==",
+ "dev": true,
+ "requires": {
+ "@babel/register": "^7.13.16",
+ "commander": "^4.0.1",
+ "core-js": "^3.2.1",
+ "node-environment-flags": "^1.0.5",
+ "regenerator-runtime": "^0.13.4",
+ "v8flags": "^3.1.1"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true
+ }
+ }
+ },
"@babel/parser": {
"version": "7.9.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz",
@@ -963,6 +985,19 @@
"esutils": "^2.0.2"
}
},
+ "@babel/register": {
+ "version": "7.13.16",
+ "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.13.16.tgz",
+ "integrity": "sha512-dh2t11ysujTwByQjXNgJ48QZ2zcXKQVdV8s0TbeMI0flmtGWCdTwK9tJiACHXPLmncm5+ktNn/diojA45JE4jg==",
+ "dev": true,
+ "requires": {
+ "clone-deep": "^4.0.1",
+ "find-cache-dir": "^2.0.0",
+ "make-dir": "^2.1.0",
+ "pirates": "^4.0.0",
+ "source-map-support": "^0.5.16"
+ }
+ },
"@babel/runtime": {
"version": "7.9.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz",
@@ -2423,6 +2458,16 @@
"unset-value": "^1.0.0"
}
},
+ "call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
"callsite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
@@ -2603,6 +2648,17 @@
"wrap-ansi": "^5.1.0"
}
},
+ "clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ }
+ },
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -2837,6 +2893,12 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
"dev": true
},
+ "core-js": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.13.1.tgz",
+ "integrity": "sha512-JqveUc4igkqwStL2RTRn/EPFGBOfEZHxJl/8ej1mXJR75V3go2mFF4bmUYkEIT1rveHKnkUlcJX/c+f1TyIovQ==",
+ "dev": true
+ },
"core-js-compat": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.4.tgz",
@@ -5662,6 +5724,17 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
+ "get-intrinsic": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
+ "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ }
+ },
"get-stdin": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
@@ -5843,6 +5916,12 @@
"function-bind": "^1.1.1"
}
},
+ "has-bigints": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
+ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+ "dev": true
+ },
"has-binary2": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
@@ -6596,6 +6675,12 @@
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
+ "is-bigint": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz",
+ "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==",
+ "dev": true
+ },
"is-binary-path": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
@@ -6605,6 +6690,15 @@
"binary-extensions": "^1.0.0"
}
},
+ "is-boolean-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz",
+ "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
@@ -6704,6 +6798,12 @@
"is-extglob": "^2.1.1"
}
},
+ "is-negative-zero": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
+ "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==",
+ "dev": true
+ },
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -6724,6 +6824,12 @@
}
}
},
+ "is-number-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz",
+ "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==",
+ "dev": true
+ },
"is-path-cwd": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
@@ -8183,6 +8289,24 @@
}
}
},
+ "node-environment-flags": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
+ "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
+ "dev": true,
+ "requires": {
+ "object.getownpropertydescriptors": "^2.0.3",
+ "semver": "^5.7.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ }
+ }
+ },
"node-forge": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
@@ -8220,6 +8344,12 @@
"vm-browserify": "0.0.4"
}
},
+ "node-modules-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz",
+ "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=",
+ "dev": true
+ },
"node-releases": {
"version": "1.1.53",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz",
@@ -8510,6 +8640,135 @@
}
}
},
+ "object.getownpropertydescriptors": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz",
+ "integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.18.0-next.2"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.3",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz",
+ "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "is-callable": "^1.2.3",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.3",
+ "is-string": "^1.0.6",
+ "object-inspect": "^1.10.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+ "dev": true
+ },
+ "is-callable": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz",
+ "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==",
+ "dev": true
+ },
+ "is-regex": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz",
+ "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-string": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz",
+ "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==",
+ "dev": true
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "object-inspect": {
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
+ "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
+ "dev": true
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
+ },
+ "object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
+ "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
+ "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ }
+ }
+ },
"object.pick": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
@@ -8780,9 +9039,9 @@
}
},
"parchment": {
- "version": "2.0.0-dev.2",
- "resolved": "https://registry.npmjs.org/parchment/-/parchment-2.0.0-dev.2.tgz",
- "integrity": "sha512-4fgRny4pPISoML08Zp7poi52Dff3E2G1ORTi2D/acJ/RiROdDAMDB6VcQNfBcmehrX5Wixp6dxh6JjLyE5yUNQ=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/parchment/-/parchment-2.0.0.tgz",
+ "integrity": "sha512-lLq8Q43ZcH3lfhY5F22F8lSkWay7nQMWeIpAIwywe1NFd5tWkhH7cyboM71MXz+00ZTGR86UM+0K9cGIx/jR7g=="
},
"parent-module": {
"version": "1.0.1",
@@ -8988,6 +9247,15 @@
"pinkie": "^2.0.0"
}
},
+ "pirates": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz",
+ "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==",
+ "dev": true,
+ "requires": {
+ "node-modules-regexp": "^1.0.0"
+ }
+ },
"pkg-dir": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
@@ -9457,8 +9725,8 @@
"dev": true
},
"quill-delta": {
- "version": "4.2.2",
- "resolved": "github:quilljs/delta#e5517726f6665e293e851457b1cc0c7a17576e50",
+ "version": "github:quilljs/delta#87cd1e6de795eb29abe79a29429ca3b126dc9031",
+ "from": "github:quilljs/delta#87cd1e6de795eb29abe79a29429ca3b126dc9031",
"requires": {
"fast-diff": "1.2.0",
"lodash.clonedeep": "^4.5.0",
@@ -10259,6 +10527,15 @@
"safe-buffer": "^5.0.1"
}
},
+ "shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.2"
+ }
+ },
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -12023,6 +12300,26 @@
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
"dev": true
},
+ "unbox-primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
+ "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "dependencies": {
+ "has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+ "dev": true
+ }
+ }
+ },
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -12258,6 +12555,15 @@
"integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==",
"dev": true
},
+ "v8flags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
+ "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==",
+ "dev": true,
+ "requires": {
+ "homedir-polyfill": "^1.0.1"
+ }
+ },
"validate-npm-package-license": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz",
@@ -12978,6 +13284,36 @@
"isexe": "^2.0.0"
}
},
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "dependencies": {
+ "has-symbols": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
+ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+ "dev": true
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ }
+ }
+ },
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
diff --git a/package.json b/package.json
index 53492be397..442460a88a 100644
--- a/package.json
+++ b/package.json
@@ -36,11 +36,12 @@
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.5.0",
- "parchment": "2.0.0-dev.2",
- "quill-delta": "4.2.2"
+ "parchment": "^2.0.0",
+ "quill-delta": "github:quilljs/delta#87cd1e6de795eb29abe79a29429ca3b126dc9031"
},
"devDependencies": {
"@babel/core": "^7.9.0",
+ "@babel/node": "^7.14.2",
"@babel/preset-env": "^7.9.5",
"babel-loader": "^8.1.0",
"babel-plugin-istanbul": "^6.0.0",
@@ -155,10 +156,11 @@
"develop": "npm run start",
"lint": "eslint blots core formats modules themes ui test",
"start": "npm run build:webpack; bundle exec foreman start -f _develop/procfile",
- "test": "npm run test:unit",
- "test:all": "npm run test:unit; npm run test:functional",
+ "test": "npm run test:unit; npm run test:random",
+ "test:all": "npm run test:unit; npm run test:functional; npm run test:random",
"test:functional": "./_develop/scripts/puppeteer.sh",
"test:unit": "npm run build; karma start _develop/karma.config.js",
+ "test:random": "babel-node ./node_modules/.bin/jasmine test/random.js --presets=@babel/preset-env",
"test:coverage": "webpack --env.coverage --config _develop/webpack.config.js; karma start _develop/karma.config.js --reporters coverage",
"travis": "npm run lint && karma start _develop/karma.config.js --reporters dots,saucelabs"
},
diff --git a/test/random.js b/test/random.js
new file mode 100644
index 0000000000..81ed72ec30
--- /dev/null
+++ b/test/random.js
@@ -0,0 +1,165 @@
+import Delta from 'quill-delta';
+import TableEmbed from '../modules/tableEmbed';
+
+// Random testing in order to find unknown issues.
+
+const random = choices => {
+ if (typeof choices === 'number') {
+ return Math.floor(Math.random() * choices);
+ }
+ return choices[random(choices.length)];
+};
+
+const getRandomRowColumnId = () => {
+ const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
+ return new Array(8)
+ .fill(0)
+ .map(() => characters.charAt(Math.floor(Math.random() * characters.length)))
+ .join('');
+};
+
+const attachAttributes = obj => {
+ const getRandomAttributes = () => {
+ const attributeCount = random([1, 4, 8]);
+ const allowedAttributes = ['align', 'background', 'color', 'font'];
+ const allowedValues = ['center', 'red', 'left', 'uppercase'];
+ const attributes = {};
+ new Array(attributeCount).fill(0).forEach(() => {
+ attributes[random(allowedAttributes)] = random(allowedValues);
+ });
+ return attributes;
+ };
+ if (random([true, false])) {
+ obj.attributes = getRandomAttributes();
+ }
+ return obj;
+};
+
+const getRandomCellContent = () => {
+ const opCount = random([1, 2, 3]);
+ const delta = new Delta();
+ new Array(opCount).fill(0).forEach(() => {
+ delta.push(
+ attachAttributes({
+ insert: new Array(random(10) + 1)
+ .fill(0)
+ .map(() => random(['a', 'b', 'c', 'c', 'e', 'f', 'g']))
+ .join(''),
+ }),
+ );
+ });
+ return delta.ops;
+};
+
+const getRandomChange = base => {
+ const table = {};
+ const dimension = {
+ rows: new Delta(base.ops[0].insert['table-embed'].rows || []).length(),
+ columns: new Delta(
+ base.ops[0].insert['table-embed'].columns || [],
+ ).length(),
+ };
+ ['rows', 'columns'].forEach(field => {
+ const baseLength = dimension[field];
+ const action = random(['insert', 'delete', 'retain']);
+ const delta = new Delta();
+ switch (action) {
+ case 'insert':
+ delta.retain(random(baseLength + 1));
+ delta.push(
+ attachAttributes({ insert: { id: getRandomRowColumnId() } }),
+ );
+ break;
+ case 'delete':
+ if (baseLength >= 1) {
+ delta.retain(random(baseLength));
+ delta.delete(1);
+ }
+ break;
+ case 'retain':
+ if (baseLength >= 1) {
+ delta.retain(random(baseLength));
+ delta.push(attachAttributes({ retain: 1 }));
+ }
+ break;
+ default:
+ break;
+ }
+ if (delta.length() > 0) {
+ table[field] = delta.ops;
+ }
+ });
+
+ const updateCellCount = random([0, 1, 2, 3]);
+ new Array(updateCellCount).fill(0).forEach(() => {
+ const row = random(dimension.rows);
+ const column = random(dimension.columns);
+ const cellIdentityToModify = `${row + 1}:${column + 1}`;
+ table.cells = {
+ [cellIdentityToModify]: attachAttributes({
+ content: getRandomCellContent(),
+ }),
+ };
+ });
+ return new Delta([attachAttributes({ retain: { 'table-embed': table } })]);
+};
+
+const getRandomRowColumnInsert = count => {
+ return new Delta(
+ new Array(count)
+ .fill(0)
+ .map(() => attachAttributes({ insert: { id: getRandomRowColumnId() } })),
+ ).ops;
+};
+
+const getRandomBase = () => {
+ const rowCount = random([0, 1, 2, 3]);
+ const columnCount = random([0, 1, 2]);
+ const cellCount = random([0, 1, 2, 3, 4, 5]);
+
+ const table = {};
+ if (rowCount) table.rows = getRandomRowColumnInsert(rowCount);
+ if (columnCount) table.columns = getRandomRowColumnInsert(columnCount);
+ if (cellCount) {
+ const cells = {};
+ new Array(cellCount).fill(0).forEach(() => {
+ const row = random(rowCount);
+ const column = random(columnCount);
+ const identity = `${row + 1}:${column + 1}`;
+ const cell = attachAttributes({});
+ if (random([true, false])) {
+ cell.content = getRandomCellContent();
+ }
+ if (Object.keys(cell).length) {
+ cells[identity] = cell;
+ }
+ });
+ if (Object.keys(cells).length) table.cells = cells;
+ }
+ return new Delta([{ insert: { 'table-embed': table } }]);
+};
+
+const runTestCase = () => {
+ const base = getRandomBase();
+ const change = getRandomChange(base);
+ expect(base).toEqual(base.compose(change).compose(change.invert(base)));
+
+ const anotherChange = getRandomChange(base);
+ expect(change.compose(change.transform(anotherChange, true))).toEqual(
+ anotherChange.compose(anotherChange.transform(change)),
+ );
+};
+
+describe('random tests', () => {
+ beforeAll(() => {
+ TableEmbed.register();
+ });
+
+ it('delta', () => {
+ for (let i = 0; i < 20; i += 1) {
+ for (let j = 0; j < 1000; j += 1) {
+ runTestCase();
+ }
+ }
+ });
+});
diff --git a/test/unit.js b/test/unit.js
index 64d2529545..a6bc9c9a6d 100644
--- a/test/unit.js
+++ b/test/unit.js
@@ -30,6 +30,7 @@ import './unit/modules/history';
import './unit/modules/keyboard';
import './unit/modules/syntax';
import './unit/modules/table';
+import './unit/modules/tableEmbed';
import './unit/modules/toolbar';
import './unit/ui/picker';
diff --git a/test/unit/modules/tableEmbed.js b/test/unit/modules/tableEmbed.js
new file mode 100644
index 0000000000..670692bce5
--- /dev/null
+++ b/test/unit/modules/tableEmbed.js
@@ -0,0 +1,601 @@
+import Delta from 'quill-delta';
+import TableEmbed from '../../../modules/tableEmbed';
+
+describe('Delta', () => {
+ beforeAll(() => {
+ TableEmbed.register();
+ });
+
+ describe('compose', () => {
+ it('adds a row', () => {
+ const base = new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { width: 30 } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: { 'table-embed': { rows: [{ insert: { id: '55555555' } }] } },
+ },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '55555555' } },
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { width: 30 } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '2:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('adds two rows', () => {
+ const base = new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { width: 30 } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '55555555' } },
+ { insert: { id: '66666666' } },
+ ],
+ },
+ },
+ },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '55555555' } },
+ { insert: { id: '66666666' } },
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { width: 30 } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '3:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('adds a row and changes cell content', () => {
+ const base = new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' }, attributes: { width: 30 } },
+ { insert: { id: '55555555' } },
+ ],
+ cells: {
+ '2:2': { content: [{ insert: 'Hello' }] },
+ '2:3': { content: [{ insert: 'World' }] },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ insert: { id: '66666666' } }],
+ cells: {
+ '3:2': { attributes: { align: 'right' } },
+ '3:3': { content: [{ insert: 'Hello ' }] },
+ },
+ },
+ },
+ },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '66666666' } },
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' }, attributes: { width: 30 } },
+ { insert: { id: '55555555' } },
+ ],
+ cells: {
+ '3:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'right' },
+ },
+ '3:3': { content: [{ insert: 'Hello World' }] },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('deletes a column', () => {
+ const base = new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { width: 30 } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ columns: [{ retain: 1 }, { delete: 1 }],
+ },
+ },
+ },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '44444444' } },
+ ],
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('removes a cell attributes', () => {
+ const base = new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ cells: { '1:2': { attributes: { align: 'center' } } },
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ cells: { '1:2': { attributes: { align: null } } },
+ },
+ },
+ },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([{ insert: { 'table-embed': {} } }]),
+ );
+ });
+
+ it('removes all rows', () => {
+ const base = new Delta([
+ {
+ insert: { 'table-embed': { rows: [{ insert: { id: '11111111' } }] } },
+ },
+ ]);
+
+ const change = new Delta([
+ { retain: { 'table-embed': { rows: [{ delete: 1 }] } } },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([{ insert: { 'table-embed': {} } }]),
+ );
+ });
+ });
+
+ describe('transform', () => {
+ it('transform rows and columns', () => {
+ const change1 = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { height: 100 } },
+ ],
+ columns: [
+ { insert: { id: '44444444' }, attributes: { width: 100 } },
+ { insert: { id: '55555555' } },
+ { insert: { id: '66666666' } },
+ ],
+ },
+ },
+ },
+ ]);
+
+ const change2 = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ delete: 1 }, { retain: 1, attributes: { height: 50 } }],
+ columns: [
+ { delete: 1 },
+ { retain: 2, attributes: { width: 40 } },
+ ],
+ },
+ },
+ },
+ ]);
+
+ expect(change1.transform(change2)).toEqual(
+ new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [
+ { retain: 3 },
+ { delete: 1 },
+ { retain: 1, attributes: { height: 50 } },
+ ],
+ columns: [
+ { retain: 3 },
+ { delete: 1 },
+ { retain: 2, attributes: { width: 40 } },
+ ],
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('transform cells', () => {
+ const change1 = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ insert: { id: '22222222' } }],
+ cells: {
+ '8:1': {
+ content: [{ insert: 'Hello 8:1!' }],
+ },
+ '21:2': {
+ content: [{ insert: 'Hello 21:2!' }],
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change2 = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ delete: 1 }],
+ cells: {
+ '6:1': {
+ content: [{ insert: 'Hello 6:1!' }],
+ },
+ '52:8': {
+ content: [{ insert: 'Hello 52:8!' }],
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ expect(change1.transform(change2)).toEqual(
+ new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ retain: 1 }, { delete: 1 }],
+ cells: {
+ '7:1': {
+ content: [{ insert: 'Hello 6:1!' }],
+ },
+ '53:8': {
+ content: [{ insert: 'Hello 52:8!' }],
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('transform cell attributes', () => {
+ const change1 = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ cells: { '8:1': { attributes: { align: 'right' } } },
+ },
+ },
+ },
+ ]);
+
+ const change2 = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ cells: { '8:1': { attributes: { align: 'left' } } },
+ },
+ },
+ },
+ ]);
+
+ expect(change1.transform(change2)).toEqual(
+ new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ cells: { '8:1': { attributes: { align: 'left' } } },
+ },
+ },
+ },
+ ]),
+ );
+
+ expect(change1.transform(change2, true)).toEqual(
+ new Delta([{ retain: { 'table-embed': {} } }]),
+ );
+ });
+ });
+
+ describe('invert', () => {
+ it('reverts rows and columns', () => {
+ const base = new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' }, attributes: { width: 100 } },
+ ],
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ delete: 1 }],
+ columns: [{ retain: 1 }, { delete: 1 }],
+ },
+ },
+ },
+ ]);
+
+ expect(change.invert(base)).toEqual(
+ new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ insert: { id: '11111111' } }],
+ columns: [
+ { retain: 1 },
+ { insert: { id: '44444444' }, attributes: { width: 100 } },
+ ],
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('inverts cell content', () => {
+ const base = new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'Hello 1:2' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+ const change = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ insert: { id: '55555555' } }],
+ cells: {
+ '2:2': {
+ content: [{ retain: 6 }, { insert: '2' }, { delete: 1 }],
+ },
+ },
+ },
+ },
+ },
+ ]);
+ expect(change.invert(base)).toEqual(
+ new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ rows: [{ delete: 1 }],
+ cells: {
+ '1:2': {
+ content: [{ retain: 6 }, { insert: '1' }, { delete: 1 }],
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('inverts cells removed by row/column delta', () => {
+ const base = new Delta([
+ {
+ insert: {
+ 'table-embed': {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'content' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+ const change = new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ columns: [{ retain: 1 }, { delete: 1 }],
+ },
+ },
+ },
+ ]);
+ expect(change.invert(base)).toEqual(
+ new Delta([
+ {
+ retain: {
+ 'table-embed': {
+ columns: [{ retain: 1 }, { insert: { id: '44444444' } }],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'content' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+ });
+});
diff --git a/test/unit/utils/delta.js b/test/unit/utils/delta.js
new file mode 100644
index 0000000000..d690c4919d
--- /dev/null
+++ b/test/unit/utils/delta.js
@@ -0,0 +1,516 @@
+import Delta from 'quill-delta';
+
+describe('Delta', () => {
+ describe('compose', () => {
+ it('adds a row', () => {
+ const base = new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { width: 30 } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ { retain: { table: { rows: [{ insert: { id: '55555555' } }] } } },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '55555555' } },
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { width: 30 } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '2:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('adds a row and changes cell content', () => {
+ const base = new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' }, attributes: { width: 30 } },
+ { insert: { id: '55555555' } },
+ ],
+ cells: {
+ '2:2': { content: [{ insert: 'Hello' }] },
+ '2:3': { content: [{ insert: 'World' }] },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ table: {
+ rows: [{ insert: { id: '66666666' } }],
+ cells: {
+ '3:2': { attributes: { align: 'right' } },
+ '3:3': { content: [{ insert: 'Hello ' }] },
+ },
+ },
+ },
+ },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '66666666' } },
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' }, attributes: { width: 30 } },
+ { insert: { id: '55555555' } },
+ ],
+ cells: {
+ '3:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'right' },
+ },
+ '3:3': { content: [{ insert: 'Hello World' }] },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('deletes a column', () => {
+ const base = new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { width: 30 } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'Hello' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ table: {
+ columns: [{ retain: 1 }, { delete: 1 }],
+ },
+ },
+ },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '11111111' }, attributes: { height: 20 } },
+ ],
+ columns: [
+ { insert: { id: '22222222' } },
+ { insert: { id: '44444444' } },
+ ],
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('removes a cell attributes', () => {
+ const base = new Delta([
+ {
+ insert: {
+ table: { cells: { '1:2': { attributes: { align: 'center' } } } },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ table: { cells: { '1:2': { attributes: { align: null } } } },
+ },
+ },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([{ insert: { table: {} } }]),
+ );
+ });
+
+ it('removes all rows', () => {
+ const base = new Delta([
+ { insert: { table: { rows: [{ insert: { id: '11111111' } }] } } },
+ ]);
+
+ const change = new Delta([
+ { retain: { table: { rows: [{ delete: 1 }] } } },
+ ]);
+
+ expect(base.compose(change)).toEqual(
+ new Delta([{ insert: { table: {} } }]),
+ );
+ });
+ });
+
+ describe('transform', () => {
+ it('transform rows and columns', () => {
+ const change1 = new Delta([
+ {
+ retain: {
+ table: {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' } },
+ { insert: { id: '33333333' }, attributes: { height: 100 } },
+ ],
+ columns: [
+ { insert: { id: '44444444' }, attributes: { width: 100 } },
+ { insert: { id: '55555555' } },
+ { insert: { id: '66666666' } },
+ ],
+ },
+ },
+ },
+ ]);
+
+ const change2 = new Delta([
+ {
+ retain: {
+ table: {
+ rows: [{ delete: 1 }, { retain: 1, attributes: { height: 50 } }],
+ columns: [
+ { delete: 1 },
+ { retain: 2, attributes: { width: 40 } },
+ ],
+ },
+ },
+ },
+ ]);
+
+ expect(change1.transform(change2)).toEqual(
+ new Delta([
+ {
+ retain: {
+ table: {
+ rows: [
+ { retain: 3 },
+ { delete: 1 },
+ { retain: 1, attributes: { height: 50 } },
+ ],
+ columns: [
+ { retain: 3 },
+ { delete: 1 },
+ { retain: 2, attributes: { width: 40 } },
+ ],
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('transform cells', () => {
+ const change1 = new Delta([
+ {
+ retain: {
+ table: {
+ rows: [{ insert: { id: '22222222' } }],
+ cells: {
+ '8:1': {
+ content: [{ insert: 'Hello 8:1!' }],
+ },
+ '21:2': {
+ content: [{ insert: 'Hello 21:2!' }],
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ const change2 = new Delta([
+ {
+ retain: {
+ table: {
+ rows: [{ delete: 1 }],
+ cells: {
+ '6:1': {
+ content: [{ insert: 'Hello 6:1!' }],
+ },
+ '52:8': {
+ content: [{ insert: 'Hello 52:8!' }],
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ expect(change1.transform(change2)).toEqual(
+ new Delta([
+ {
+ retain: {
+ table: {
+ rows: [{ retain: 1 }, { delete: 1 }],
+ cells: {
+ '7:1': {
+ content: [{ insert: 'Hello 6:1!' }],
+ },
+ '53:8': {
+ content: [{ insert: 'Hello 52:8!' }],
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('transform cell attributes', () => {
+ const change1 = new Delta([
+ {
+ retain: {
+ table: { cells: { '8:1': { attributes: { align: 'right' } } } },
+ },
+ },
+ ]);
+
+ const change2 = new Delta([
+ {
+ retain: {
+ table: { cells: { '8:1': { attributes: { align: 'left' } } } },
+ },
+ },
+ ]);
+
+ expect(change1.transform(change2)).toEqual(
+ new Delta([
+ {
+ retain: {
+ table: { cells: { '8:1': { attributes: { align: 'left' } } } },
+ },
+ },
+ ]),
+ );
+
+ expect(change1.transform(change2, true)).toEqual(
+ new Delta([{ retain: { table: {} } }]),
+ );
+ });
+ });
+
+ describe('invert', () => {
+ it('reverts rows and columns', () => {
+ const base = new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' }, attributes: { width: 100 } },
+ ],
+ },
+ },
+ },
+ ]);
+
+ const change = new Delta([
+ {
+ retain: {
+ table: {
+ rows: [{ remove: { id: '22222222' } }],
+ columns: [{ retain: 1 }, { delete: 1 }],
+ },
+ },
+ },
+ ]);
+
+ expect(change.invert(base)).toEqual(
+ new Delta([
+ {
+ retain: {
+ table: {
+ columns: [
+ { retain: 1 },
+ { insert: { id: '44444444' }, attributes: { width: 100 } },
+ ],
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('inverts cell content', () => {
+ const base = new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'Hello 1:2' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+ const change = new Delta([
+ {
+ retain: {
+ table: {
+ rows: [{ insert: { id: '55555555' } }],
+ cells: {
+ '2:2': {
+ content: [{ retain: 6 }, { insert: '2' }, { delete: 1 }],
+ },
+ },
+ },
+ },
+ },
+ ]);
+ expect(change.invert(base)).toEqual(
+ new Delta([
+ {
+ retain: {
+ table: {
+ rows: [{ delete: 1 }],
+ cells: {
+ '1:2': {
+ content: [{ retain: 6 }, { insert: '1' }, { delete: 1 }],
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+
+ it('inverts cells removed by row/column delta', () => {
+ const base = new Delta([
+ {
+ insert: {
+ table: {
+ rows: [
+ { insert: { id: '11111111' } },
+ { insert: { id: '22222222' } },
+ ],
+ columns: [
+ { insert: { id: '33333333' } },
+ { insert: { id: '44444444' } },
+ ],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'content' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]);
+ const change = new Delta([
+ {
+ retain: {
+ table: {
+ columns: [{ retain: 1 }, { delete: 1 }],
+ },
+ },
+ },
+ ]);
+ expect(change.invert(base)).toEqual(
+ new Delta([
+ {
+ retain: {
+ table: {
+ columns: [{ retain: 1 }, { insert: { id: '44444444' } }],
+ cells: {
+ '1:2': {
+ content: [{ insert: 'content' }],
+ attributes: { align: 'center' },
+ },
+ },
+ },
+ },
+ },
+ ]),
+ );
+ });
+ });
+});