From c84c356f088758ae47cb034a799669e2da49a68a Mon Sep 17 00:00:00 2001 From: luin Date: Fri, 28 May 2021 23:21:20 +0800 Subject: [PATCH 01/26] Support retain embed --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a57f16cfcc..9f10c448aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9457,8 +9457,8 @@ "dev": true }, "quill-delta": { - "version": "4.2.2", - "resolved": "github:quilljs/delta#e5517726f6665e293e851457b1cc0c7a17576e50", + "version": "github:quilljs/delta#d89b4e3f57d150fa04c07d8566407806ab24c271", + "from": "github:quilljs/delta#d89b4e3", "requires": { "fast-diff": "1.2.0", "lodash.clonedeep": "^4.5.0", diff --git a/package.json b/package.json index 53492be397..58a3156e08 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "lodash.isequal": "^4.5.0", "lodash.merge": "^4.5.0", "parchment": "2.0.0-dev.2", - "quill-delta": "4.2.2" + "quill-delta": "quilljs/delta#d89b4e3" }, "devDependencies": { "@babel/core": "^7.9.0", From 6d7a1479c45437a82adb7f7e579e5d85c6d74cf4 Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 30 May 2021 14:14:28 +0800 Subject: [PATCH 02/26] Add table handler --- blots/block.js | 2 +- core/editor.js | 3 +- core/quill.js | 6 +- modules/clipboard.js | 7 +- modules/keyboard.js | 5 +- modules/syntax.js | 8 +- modules/table.js | 2 +- modules/toolbar.js | 4 +- modules/uploader.js | 2 +- package.json | 1 + test/unit/core/editor.js | 2 +- test/unit/core/quill.js | 6 +- test/unit/formats/align.js | 2 +- test/unit/formats/code.js | 2 +- test/unit/formats/color.js | 2 +- test/unit/formats/header.js | 2 +- test/unit/formats/indent.js | 2 +- test/unit/formats/link.js | 2 +- test/unit/formats/list.js | 2 +- test/unit/formats/table.js | 2 +- test/unit/modules/clipboard.js | 4 +- test/unit/modules/history.js | 2 +- test/unit/modules/syntax.js | 2 +- test/unit/modules/table.js | 2 +- test/unit/utils/delta.js | 516 +++++++++++++++++++++++++++++++++ utils/delta.js | 215 ++++++++++++++ 26 files changed, 769 insertions(+), 36 deletions(-) create mode 100644 test/unit/utils/delta.js create mode 100644 utils/delta.js diff --git a/blots/block.js b/blots/block.js index ecdd4b0b05..e0e5efd60a 100644 --- a/blots/block.js +++ b/blots/block.js @@ -1,4 +1,3 @@ -import Delta from 'quill-delta'; import { AttributorStore, BlockBlot, @@ -6,6 +5,7 @@ import { LeafBlot, Scope, } from 'parchment'; +import Delta from '../utils/delta'; import Break from './break'; import Inline from './inline'; import TextBlot from './text'; diff --git a/core/editor.js b/core/editor.js index 94f6af97a3..566300fe08 100644 --- a/core/editor.js +++ b/core/editor.js @@ -1,13 +1,14 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import merge from 'lodash.merge'; -import Delta, { AttributeMap, Op } from 'quill-delta'; +import { AttributeMap, Op } from 'quill-delta'; import { LeafBlot, Scope } from 'parchment'; import { Range } from './selection'; import CursorBlot from '../blots/cursor'; import Block, { BlockEmbed, bubbleFormats } from '../blots/block'; import Break from '../blots/break'; import TextBlot, { escapeText } from '../blots/text'; +import Delta from '../utils/delta'; const ASCII = /^[ -~]*$/; diff --git a/core/quill.js b/core/quill.js index 44bf15dd97..d39b58bcbe 100644 --- a/core/quill.js +++ b/core/quill.js @@ -1,13 +1,13 @@ -import Delta from 'quill-delta'; import cloneDeep from 'lodash.clonedeep'; import merge from 'lodash.merge'; import * as Parchment from 'parchment'; +import Delta from '../utils/delta'; import Editor from './editor'; import Emitter from './emitter'; -import Module from './module'; -import Selection, { Range } from './selection'; import instances from './instances'; import logger from './logger'; +import Module from './module'; +import Selection, { Range } from './selection'; import Theme from './theme'; const debug = logger('quill'); diff --git a/modules/clipboard.js b/modules/clipboard.js index 8eadd406a6..f95b61c6d7 100644 --- a/modules/clipboard.js +++ b/modules/clipboard.js @@ -1,17 +1,15 @@ -import Delta from 'quill-delta'; import { Attributor, + BlockBlot, ClassAttributor, EmbedBlot, Scope, StyleAttributor, - BlockBlot, } from 'parchment'; import { BlockEmbed } from '../blots/block'; -import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; - +import Quill from '../core/quill'; import { AlignAttribute, AlignStyle } from '../formats/align'; import { BackgroundStyle } from '../formats/background'; import CodeBlock from '../formats/code'; @@ -19,6 +17,7 @@ import { ColorStyle } from '../formats/color'; import { DirectionAttribute, DirectionStyle } from '../formats/direction'; import { FontStyle } from '../formats/font'; import { SizeStyle } from '../formats/size'; +import Delta from '../utils/delta'; import { deleteRange } from './keyboard'; const debug = logger('quill:clipboard'); diff --git a/modules/keyboard.js b/modules/keyboard.js index 76bacc8de2..7e000a7d34 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -1,10 +1,11 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; -import Delta, { AttributeMap } from 'quill-delta'; import { EmbedBlot, Scope, TextBlot } from 'parchment'; -import Quill from '../core/quill'; +import { AttributeMap } from 'quill-delta'; import logger from '../core/logger'; import Module from '../core/module'; +import Quill from '../core/quill'; +import Delta from '../utils/delta'; const debug = logger('quill:keyboard'); diff --git a/modules/syntax.js b/modules/syntax.js index 70e135423a..af25590287 100644 --- a/modules/syntax.js +++ b/modules/syntax.js @@ -1,13 +1,13 @@ -import Delta from 'quill-delta'; import { ClassAttributor, Scope } from 'parchment'; -import Inline from '../blots/inline'; -import Quill from '../core/quill'; -import Module from '../core/module'; import { blockDelta } from '../blots/block'; import BreakBlot from '../blots/break'; import CursorBlot from '../blots/cursor'; +import Inline from '../blots/inline'; import TextBlot, { escapeText } from '../blots/text'; +import Module from '../core/module'; +import Quill from '../core/quill'; import CodeBlock, { CodeBlockContainer } from '../formats/code'; +import Delta from '../utils/delta'; import { traverse } from './clipboard'; const TokenAttributor = new ClassAttributor('code-token', 'hljs', { diff --git a/modules/table.js b/modules/table.js index 4fde5dc745..5e21dab439 100644 --- a/modules/table.js +++ b/modules/table.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../utils/delta'; import Quill from '../core/quill'; import Module from '../core/module'; import { diff --git a/modules/toolbar.js b/modules/toolbar.js index a16923ec0a..a54603af1b 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -1,8 +1,8 @@ -import Delta from 'quill-delta'; import { EmbedBlot, Scope } from 'parchment'; -import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; +import Quill from '../core/quill'; +import Delta from '../utils/delta'; const debug = logger('quill:toolbar'); diff --git a/modules/uploader.js b/modules/uploader.js index b3eabb19be..d1f05dc0bf 100644 --- a/modules/uploader.js +++ b/modules/uploader.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../utils/delta'; import Emitter from '../core/emitter'; import Module from '../core/module'; diff --git a/package.json b/package.json index 58a3156e08..417ab6572b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "modules", "themes", "ui", + "utils", "dist/quill.bubble.css", "dist/quill.snow.css", "dist/quill.core.css", diff --git a/test/unit/core/editor.js b/test/unit/core/editor.js index f4ce692f92..83c11d2d33 100644 --- a/test/unit/core/editor.js +++ b/test/unit/core/editor.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Selection, { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Editor', function() { describe('insert', function() { diff --git a/test/unit/core/quill.js b/test/unit/core/quill.js index 424a979f82..d83ad638e4 100644 --- a/test/unit/core/quill.js +++ b/test/unit/core/quill.js @@ -1,10 +1,10 @@ -import Delta from 'quill-delta'; +import Emitter from '../../../core/emitter'; import Quill, { expandConfig, overload } from '../../../core/quill'; +import { Range } from '../../../core/selection'; import Theme from '../../../core/theme'; -import Emitter from '../../../core/emitter'; import Toolbar from '../../../modules/toolbar'; import Snow from '../../../themes/snow'; -import { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Quill', function() { it('imports', function() { diff --git a/test/unit/formats/align.js b/test/unit/formats/align.js index 0571ea402d..f73c5638fe 100644 --- a/test/unit/formats/align.js +++ b/test/unit/formats/align.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Align', function() { it('add', function() { diff --git a/test/unit/formats/code.js b/test/unit/formats/code.js index 2497bb7f83..e3c02780c8 100644 --- a/test/unit/formats/code.js +++ b/test/unit/formats/code.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Code', function() { it('format newline', function() { diff --git a/test/unit/formats/color.js b/test/unit/formats/color.js index 9d50cd41fe..179131e1cf 100644 --- a/test/unit/formats/color.js +++ b/test/unit/formats/color.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Color', function() { it('add', function() { diff --git a/test/unit/formats/header.js b/test/unit/formats/header.js index 43dbc8917e..c1ff711dbb 100644 --- a/test/unit/formats/header.js +++ b/test/unit/formats/header.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../../../utils/delta'; import Editor from '../../../core/editor'; describe('Header', function() { diff --git a/test/unit/formats/indent.js b/test/unit/formats/indent.js index c38a0d2c6d..3ae67f2751 100644 --- a/test/unit/formats/indent.js +++ b/test/unit/formats/indent.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Indent', function() { it('+1', function() { diff --git a/test/unit/formats/link.js b/test/unit/formats/link.js index 4197d1b9a0..ca81250e30 100644 --- a/test/unit/formats/link.js +++ b/test/unit/formats/link.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Link from '../../../formats/link'; +import Delta from '../../../utils/delta'; describe('Link', function() { it('add', function() { diff --git a/test/unit/formats/list.js b/test/unit/formats/list.js index d0b9f24127..37e9dee814 100644 --- a/test/unit/formats/list.js +++ b/test/unit/formats/list.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('List', function() { it('add', function() { diff --git a/test/unit/formats/table.js b/test/unit/formats/table.js index e3a299dbee..7f82e3453a 100644 --- a/test/unit/formats/table.js +++ b/test/unit/formats/table.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; const tableDelta = new Delta() .insert('A1') diff --git a/test/unit/modules/clipboard.js b/test/unit/modules/clipboard.js index 8a76748690..3a204cfb39 100644 --- a/test/unit/modules/clipboard.js +++ b/test/unit/modules/clipboard.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; -import { Range } from '../../../core/selection'; import Quill from '../../../core'; +import { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Clipboard', function() { describe('events', function() { diff --git a/test/unit/modules/history.js b/test/unit/modules/history.js index a176f93234..36e10cbe7a 100644 --- a/test/unit/modules/history.js +++ b/test/unit/modules/history.js @@ -1,7 +1,7 @@ -import Delta from 'quill-delta'; import Quill from '../../../core'; import { globalRegistry } from '../../../core/quill'; import { getLastChangeIndex } from '../../../modules/history'; +import Delta from '../../../utils/delta'; describe('History', function() { describe('getLastChangeIndex', function() { diff --git a/test/unit/modules/syntax.js b/test/unit/modules/syntax.js index 828e955989..131d624df7 100644 --- a/test/unit/modules/syntax.js +++ b/test/unit/modules/syntax.js @@ -1,9 +1,9 @@ import hljs from 'highlight.js'; -import Delta from 'quill-delta'; import Quill from '../../../core/quill'; import BoldBlot from '../../../formats/bold'; import CodeBlock, { CodeBlockContainer } from '../../../formats/code'; import Syntax, { CodeBlock as SyntaxCodeBlock } from '../../../modules/syntax'; +import Delta from '../../../utils/delta'; const HIGHLIGHT_INTERVAL = 10; diff --git a/test/unit/modules/table.js b/test/unit/modules/table.js index bb9d8015c1..921a528633 100644 --- a/test/unit/modules/table.js +++ b/test/unit/modules/table.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../../../utils/delta'; import Quill from '../../../core/quill'; describe('Table Module', function() { diff --git a/test/unit/utils/delta.js b/test/unit/utils/delta.js new file mode 100644 index 0000000000..3d018e8381 --- /dev/null +++ b/test/unit/utils/delta.js @@ -0,0 +1,516 @@ +import Delta from '../../../utils/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' }, + }, + }, + }, + }, + }, + ]), + ); + }); + }); +}); diff --git a/utils/delta.js b/utils/delta.js new file mode 100644 index 0000000000..90b4dff3bf --- /dev/null +++ b/utils/delta.js @@ -0,0 +1,215 @@ +import Delta from 'quill-delta'; + +const parseCellIdentity = identity => { + const parts = identity.split(':'); + return [Number(parts[0]) - 1, Number(parts[1]) - 1]; +}; + +const stringifyCellIdentity = (row, column) => `${row + 1}:${column + 1}`; + +const composePosition = (delta, index) => { + let newIndex = index; + const thisIter = Delta.Op.iterator(delta.ops); + let offset = 0; + while (thisIter.hasNext() && offset <= index) { + const length = thisIter.peekLength(); + const nextType = thisIter.peekType(); + thisIter.next(); + switch (nextType) { + case 'delete': + if (length > index - 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; +}; + +Delta.registerHandler('table', { + compose(a, b, keepNull) { + // Step 2~3: Compose rows and columns separately + const rows = new Delta(a.rows || []).compose(new Delta(b.rows || [])); + const columns = new Delta(a.columns || []).compose( + new Delta(b.columns || []), + ); + + // Step 4: Reindex cell identities according to B's rows and columns + const cells = reindexCellIdentities(a.cells || {}, { + rows: new Delta(b.rows || []), + columns: new Delta(b.columns || []), + }); + + // Step 5: Compose cell content and attributes + 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 }); + }, +}); + +export default Delta; From cab87efa496fdb9146f8d94025d064e679647c23 Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 30 May 2021 17:01:48 +0800 Subject: [PATCH 03/26] Add random testing for table operations --- package-lock.json | 389 +++++++++++++++++++++++++++++++++++++++++++ package.json | 7 +- test/random/delta.js | 158 ++++++++++++++++++ 3 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 test/random/delta.js diff --git a/package-lock.json b/package-lock.json index 9f10c448aa..5143c59669 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", @@ -1717,6 +1752,12 @@ } } }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -2423,6 +2464,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", @@ -2475,6 +2526,20 @@ "integrity": "sha512-Ep0tEPeI5wCvmJNrXjE3etgfI+lkl1fTDU6Y3ZH1mhrjkPlVI9W4pcKbMo+BQLpEWKVYYp2EmYaRsqpPC3k7lQ==", "dev": true }, + "chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -2492,6 +2557,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -2603,6 +2674,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 +2919,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", @@ -3156,6 +3244,15 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -5662,6 +5759,23 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "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 +5957,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 +6716,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 +6731,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 +6839,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 +6865,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 +8330,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 +8385,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 +8681,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", @@ -8942,6 +9242,12 @@ } } }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "pbkdf2": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", @@ -8988,6 +9294,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", @@ -10259,6 +10574,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", @@ -11946,6 +12270,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -12023,6 +12353,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 +12608,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 +13337,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 417ab6572b..ee2c848f2c 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,11 @@ }, "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", + "chai": "^4.3.4", "css-loader": "^3.5.1", "eslint": "^6.8.0", "eslint-config-airbnb": "^18.1.0", @@ -156,10 +158,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 test/random/delta.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/delta.js b/test/random/delta.js new file mode 100644 index 0000000000..ea39719adb --- /dev/null +++ b/test/random/delta.js @@ -0,0 +1,158 @@ +import { expect } from 'chai'; +import Delta from '../../utils/delta'; + +// 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.rows || []).length(), + columns: new Delta(base.ops[0].insert.table.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 } })]); +}; + +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 } }]); +}; + +const runTestCase = () => { + const base = getRandomBase(); + const change = getRandomChange(base); + expect(base).eql(base.compose(change).compose(change.invert(base))); + + const anotherChange = getRandomChange(base); + expect(change.compose(change.transform(anotherChange, true))).eql( + anotherChange.compose(anotherChange.transform(change)), + ); +}; + +for (let i = 0; i < 20; i += 1) { + for (let j = 0; j < 1000; j += 1) { + runTestCase(); + } + process.stdout.write('.'); +} + +process.stdout.write('\n'); From d1d40e5387908fc755f480b95811b12753a1d478 Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 31 May 2021 22:27:47 +0800 Subject: [PATCH 04/26] Upgrade delta --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5143c59669..e814f99750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9772,8 +9772,8 @@ "dev": true }, "quill-delta": { - "version": "github:quilljs/delta#d89b4e3f57d150fa04c07d8566407806ab24c271", - "from": "github:quilljs/delta#d89b4e3", + "version": "github:quilljs/delta#d5efd1569a61fe667af313ef7408cd94ef924a12", + "from": "github:quilljs/delta#d5efd1569a61fe667af313ef7408cd94ef924a12", "requires": { "fast-diff": "1.2.0", "lodash.clonedeep": "^4.5.0", diff --git a/package.json b/package.json index ee2c848f2c..910a7e6a92 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "lodash.isequal": "^4.5.0", "lodash.merge": "^4.5.0", "parchment": "2.0.0-dev.2", - "quill-delta": "quilljs/delta#d89b4e3" + "quill-delta": "github:quilljs/delta#d5efd1569a61fe667af313ef7408cd94ef924a12" }, "devDependencies": { "@babel/core": "^7.9.0", From cc0ad88474978a5d9dea0cdab49246b81f5119ae Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 14 Jun 2021 15:59:06 +0800 Subject: [PATCH 05/26] Register table handler in a module --- blots/block.js | 2 +- core/editor.js | 3 +- core/quill.js | 6 +-- modules/clipboard.js | 7 +-- modules/keyboard.js | 5 +-- modules/syntax.js | 8 ++-- modules/table.js | 2 +- utils/delta.js => modules/tableEmbed.js | 21 ++++++--- modules/toolbar.js | 4 +- modules/uploader.js | 2 +- package-lock.json | 57 +------------------------ package.json | 6 +-- test/{random/delta.js => random.js} | 27 +++++++----- test/unit/core/editor.js | 2 +- test/unit/core/quill.js | 6 +-- test/unit/formats/align.js | 2 +- test/unit/formats/code.js | 2 +- test/unit/formats/color.js | 2 +- test/unit/formats/header.js | 2 +- test/unit/formats/indent.js | 2 +- test/unit/formats/link.js | 2 +- test/unit/formats/list.js | 2 +- test/unit/formats/table.js | 2 +- test/unit/modules/clipboard.js | 4 +- test/unit/modules/history.js | 2 +- test/unit/modules/syntax.js | 2 +- test/unit/modules/table.js | 2 +- test/unit/utils/delta.js | 2 +- 28 files changed, 71 insertions(+), 115 deletions(-) rename utils/delta.js => modules/tableEmbed.js (94%) rename test/{random/delta.js => random.js} (91%) diff --git a/blots/block.js b/blots/block.js index e0e5efd60a..ecdd4b0b05 100644 --- a/blots/block.js +++ b/blots/block.js @@ -1,3 +1,4 @@ +import Delta from 'quill-delta'; import { AttributorStore, BlockBlot, @@ -5,7 +6,6 @@ import { LeafBlot, Scope, } from 'parchment'; -import Delta from '../utils/delta'; import Break from './break'; import Inline from './inline'; import TextBlot from './text'; diff --git a/core/editor.js b/core/editor.js index 566300fe08..94f6af97a3 100644 --- a/core/editor.js +++ b/core/editor.js @@ -1,14 +1,13 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import merge from 'lodash.merge'; -import { AttributeMap, Op } from 'quill-delta'; +import Delta, { AttributeMap, Op } from 'quill-delta'; import { LeafBlot, Scope } from 'parchment'; import { Range } from './selection'; import CursorBlot from '../blots/cursor'; import Block, { BlockEmbed, bubbleFormats } from '../blots/block'; import Break from '../blots/break'; import TextBlot, { escapeText } from '../blots/text'; -import Delta from '../utils/delta'; const ASCII = /^[ -~]*$/; diff --git a/core/quill.js b/core/quill.js index d39b58bcbe..44bf15dd97 100644 --- a/core/quill.js +++ b/core/quill.js @@ -1,13 +1,13 @@ +import Delta from 'quill-delta'; import cloneDeep from 'lodash.clonedeep'; import merge from 'lodash.merge'; import * as Parchment from 'parchment'; -import Delta from '../utils/delta'; import Editor from './editor'; import Emitter from './emitter'; -import instances from './instances'; -import logger from './logger'; import Module from './module'; import Selection, { Range } from './selection'; +import instances from './instances'; +import logger from './logger'; import Theme from './theme'; const debug = logger('quill'); diff --git a/modules/clipboard.js b/modules/clipboard.js index f95b61c6d7..8eadd406a6 100644 --- a/modules/clipboard.js +++ b/modules/clipboard.js @@ -1,15 +1,17 @@ +import Delta from 'quill-delta'; import { Attributor, - BlockBlot, ClassAttributor, EmbedBlot, Scope, StyleAttributor, + BlockBlot, } from 'parchment'; import { BlockEmbed } from '../blots/block'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; + import { AlignAttribute, AlignStyle } from '../formats/align'; import { BackgroundStyle } from '../formats/background'; import CodeBlock from '../formats/code'; @@ -17,7 +19,6 @@ import { ColorStyle } from '../formats/color'; import { DirectionAttribute, DirectionStyle } from '../formats/direction'; import { FontStyle } from '../formats/font'; import { SizeStyle } from '../formats/size'; -import Delta from '../utils/delta'; import { deleteRange } from './keyboard'; const debug = logger('quill:clipboard'); diff --git a/modules/keyboard.js b/modules/keyboard.js index 7e000a7d34..76bacc8de2 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -1,11 +1,10 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; +import Delta, { AttributeMap } from 'quill-delta'; import { EmbedBlot, Scope, TextBlot } from 'parchment'; -import { AttributeMap } from 'quill-delta'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; -import Delta from '../utils/delta'; const debug = logger('quill:keyboard'); diff --git a/modules/syntax.js b/modules/syntax.js index af25590287..70e135423a 100644 --- a/modules/syntax.js +++ b/modules/syntax.js @@ -1,13 +1,13 @@ +import Delta from 'quill-delta'; import { ClassAttributor, Scope } from 'parchment'; +import Inline from '../blots/inline'; +import Quill from '../core/quill'; +import Module from '../core/module'; import { blockDelta } from '../blots/block'; import BreakBlot from '../blots/break'; import CursorBlot from '../blots/cursor'; -import Inline from '../blots/inline'; import TextBlot, { escapeText } from '../blots/text'; -import Module from '../core/module'; -import Quill from '../core/quill'; import CodeBlock, { CodeBlockContainer } from '../formats/code'; -import Delta from '../utils/delta'; import { traverse } from './clipboard'; const TokenAttributor = new ClassAttributor('code-token', 'hljs', { diff --git a/modules/table.js b/modules/table.js index 5e21dab439..4fde5dc745 100644 --- a/modules/table.js +++ b/modules/table.js @@ -1,4 +1,4 @@ -import Delta from '../utils/delta'; +import Delta from 'quill-delta'; import Quill from '../core/quill'; import Module from '../core/module'; import { diff --git a/utils/delta.js b/modules/tableEmbed.js similarity index 94% rename from utils/delta.js rename to modules/tableEmbed.js index 90b4dff3bf..057e1df5e4 100644 --- a/utils/delta.js +++ b/modules/tableEmbed.js @@ -1,4 +1,5 @@ import Delta from 'quill-delta'; +import Module from '../core/module'; const parseCellIdentity = identity => { const parts = identity.split(':'); @@ -78,21 +79,18 @@ const reindexCellIdentities = (cells, { rows, columns }) => { return reindexedCells; }; -Delta.registerHandler('table', { +export const tableHandler = { compose(a, b, keepNull) { - // Step 2~3: Compose rows and columns separately const rows = new Delta(a.rows || []).compose(new Delta(b.rows || [])); const columns = new Delta(a.columns || []).compose( new Delta(b.columns || []), ); - // Step 4: Reindex cell identities according to B's rows and columns const cells = reindexCellIdentities(a.cells || {}, { rows: new Delta(b.rows || []), columns: new Delta(b.columns || []), }); - // Step 5: Compose cell content and attributes Object.keys(b.cells || {}).forEach(identity => { const aCell = cells[identity] || {}; const bCell = b.cells[identity]; @@ -177,7 +175,10 @@ Delta.registerHandler('table', { const columns = new Delta(change.columns || []).invert( new Delta(base.columns || []), ); - const cells = reindexCellIdentities(change.cells || {}, { rows, columns }); + const cells = reindexCellIdentities(change.cells || {}, { + rows, + columns, + }); Object.keys(cells).forEach(identity => { const changeCell = cells[identity] || {}; const baseCell = (base.cells || {})[identity] || {}; @@ -210,6 +211,12 @@ Delta.registerHandler('table', { return compactTableData({ rows, columns, cells }); }, -}); +}; + +class TableEmbed extends Module { + static register() { + Delta.registerEmbed('table', tableHandler); + } +} -export default Delta; +export default TableEmbed; diff --git a/modules/toolbar.js b/modules/toolbar.js index a54603af1b..a16923ec0a 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -1,8 +1,8 @@ +import Delta from 'quill-delta'; import { EmbedBlot, Scope } from 'parchment'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; -import Delta from '../utils/delta'; const debug = logger('quill:toolbar'); diff --git a/modules/uploader.js b/modules/uploader.js index d1f05dc0bf..b3eabb19be 100644 --- a/modules/uploader.js +++ b/modules/uploader.js @@ -1,4 +1,4 @@ -import Delta from '../utils/delta'; +import Delta from 'quill-delta'; import Emitter from '../core/emitter'; import Module from '../core/module'; diff --git a/package-lock.json b/package-lock.json index e814f99750..bfd144194e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1752,12 +1752,6 @@ } } }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -2526,20 +2520,6 @@ "integrity": "sha512-Ep0tEPeI5wCvmJNrXjE3etgfI+lkl1fTDU6Y3ZH1mhrjkPlVI9W4pcKbMo+BQLpEWKVYYp2EmYaRsqpPC3k7lQ==", "dev": true }, - "chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -2557,12 +2537,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -3244,15 +3218,6 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -5759,12 +5724,6 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -9242,12 +9201,6 @@ } } }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, "pbkdf2": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", @@ -9772,8 +9725,8 @@ "dev": true }, "quill-delta": { - "version": "github:quilljs/delta#d5efd1569a61fe667af313ef7408cd94ef924a12", - "from": "github:quilljs/delta#d5efd1569a61fe667af313ef7408cd94ef924a12", + "version": "github:quilljs/delta#6ddb8ddf121ad018ca6a6e7593e4a06c471c4dda", + "from": "github:quilljs/delta#6ddb8dd", "requires": { "fast-diff": "1.2.0", "lodash.clonedeep": "^4.5.0", @@ -12270,12 +12223,6 @@ "prelude-ls": "~1.1.2" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", diff --git a/package.json b/package.json index 910a7e6a92..d61a8ed627 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "modules", "themes", "ui", - "utils", "dist/quill.bubble.css", "dist/quill.snow.css", "dist/quill.core.css", @@ -38,7 +37,7 @@ "lodash.isequal": "^4.5.0", "lodash.merge": "^4.5.0", "parchment": "2.0.0-dev.2", - "quill-delta": "github:quilljs/delta#d5efd1569a61fe667af313ef7408cd94ef924a12" + "quill-delta": "github:quilljs/delta#37b73be28d02ff5395268c460c3201f6fe37892f" }, "devDependencies": { "@babel/core": "^7.9.0", @@ -46,7 +45,6 @@ "@babel/preset-env": "^7.9.5", "babel-loader": "^8.1.0", "babel-plugin-istanbul": "^6.0.0", - "chai": "^4.3.4", "css-loader": "^3.5.1", "eslint": "^6.8.0", "eslint-config-airbnb": "^18.1.0", @@ -162,7 +160,7 @@ "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 test/random/delta.js --presets=@babel/preset-env", + "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/delta.js b/test/random.js similarity index 91% rename from test/random/delta.js rename to test/random.js index ea39719adb..0a1ee68763 100644 --- a/test/random/delta.js +++ b/test/random.js @@ -1,5 +1,5 @@ -import { expect } from 'chai'; -import Delta from '../../utils/delta'; +import Delta from 'quill-delta'; +import TableEmbed from '../modules/tableEmbed'; // Random testing in order to find unknown issues. @@ -140,19 +140,24 @@ const getRandomBase = () => { const runTestCase = () => { const base = getRandomBase(); const change = getRandomChange(base); - expect(base).eql(base.compose(change).compose(change.invert(base))); + expect(base).toEqual(base.compose(change).compose(change.invert(base))); const anotherChange = getRandomChange(base); - expect(change.compose(change.transform(anotherChange, true))).eql( + expect(change.compose(change.transform(anotherChange, true))).toEqual( anotherChange.compose(anotherChange.transform(change)), ); }; -for (let i = 0; i < 20; i += 1) { - for (let j = 0; j < 1000; j += 1) { - runTestCase(); - } - process.stdout.write('.'); -} +describe('random tests', () => { + beforeAll(() => { + TableEmbed.register(); + }); -process.stdout.write('\n'); + it('delta', () => { + for (let i = 0; i < 20; i += 1) { + for (let j = 0; j < 1000; j += 1) { + runTestCase(); + } + } + }); +}); diff --git a/test/unit/core/editor.js b/test/unit/core/editor.js index 83c11d2d33..f4ce692f92 100644 --- a/test/unit/core/editor.js +++ b/test/unit/core/editor.js @@ -1,6 +1,6 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Selection, { Range } from '../../../core/selection'; -import Delta from '../../../utils/delta'; describe('Editor', function() { describe('insert', function() { diff --git a/test/unit/core/quill.js b/test/unit/core/quill.js index d83ad638e4..424a979f82 100644 --- a/test/unit/core/quill.js +++ b/test/unit/core/quill.js @@ -1,10 +1,10 @@ -import Emitter from '../../../core/emitter'; +import Delta from 'quill-delta'; import Quill, { expandConfig, overload } from '../../../core/quill'; -import { Range } from '../../../core/selection'; import Theme from '../../../core/theme'; +import Emitter from '../../../core/emitter'; import Toolbar from '../../../modules/toolbar'; import Snow from '../../../themes/snow'; -import Delta from '../../../utils/delta'; +import { Range } from '../../../core/selection'; describe('Quill', function() { it('imports', function() { diff --git a/test/unit/formats/align.js b/test/unit/formats/align.js index f73c5638fe..0571ea402d 100644 --- a/test/unit/formats/align.js +++ b/test/unit/formats/align.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Align', function() { it('add', function() { diff --git a/test/unit/formats/code.js b/test/unit/formats/code.js index e3c02780c8..2497bb7f83 100644 --- a/test/unit/formats/code.js +++ b/test/unit/formats/code.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Code', function() { it('format newline', function() { diff --git a/test/unit/formats/color.js b/test/unit/formats/color.js index 179131e1cf..9d50cd41fe 100644 --- a/test/unit/formats/color.js +++ b/test/unit/formats/color.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Color', function() { it('add', function() { diff --git a/test/unit/formats/header.js b/test/unit/formats/header.js index c1ff711dbb..43dbc8917e 100644 --- a/test/unit/formats/header.js +++ b/test/unit/formats/header.js @@ -1,4 +1,4 @@ -import Delta from '../../../utils/delta'; +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; describe('Header', function() { diff --git a/test/unit/formats/indent.js b/test/unit/formats/indent.js index 3ae67f2751..c38a0d2c6d 100644 --- a/test/unit/formats/indent.js +++ b/test/unit/formats/indent.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Indent', function() { it('+1', function() { diff --git a/test/unit/formats/link.js b/test/unit/formats/link.js index ca81250e30..4197d1b9a0 100644 --- a/test/unit/formats/link.js +++ b/test/unit/formats/link.js @@ -1,6 +1,6 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Link from '../../../formats/link'; -import Delta from '../../../utils/delta'; describe('Link', function() { it('add', function() { diff --git a/test/unit/formats/list.js b/test/unit/formats/list.js index 37e9dee814..d0b9f24127 100644 --- a/test/unit/formats/list.js +++ b/test/unit/formats/list.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('List', function() { it('add', function() { diff --git a/test/unit/formats/table.js b/test/unit/formats/table.js index 7f82e3453a..e3a299dbee 100644 --- a/test/unit/formats/table.js +++ b/test/unit/formats/table.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; const tableDelta = new Delta() .insert('A1') diff --git a/test/unit/modules/clipboard.js b/test/unit/modules/clipboard.js index 3a204cfb39..8a76748690 100644 --- a/test/unit/modules/clipboard.js +++ b/test/unit/modules/clipboard.js @@ -1,6 +1,6 @@ -import Quill from '../../../core'; +import Delta from 'quill-delta'; import { Range } from '../../../core/selection'; -import Delta from '../../../utils/delta'; +import Quill from '../../../core'; describe('Clipboard', function() { describe('events', function() { diff --git a/test/unit/modules/history.js b/test/unit/modules/history.js index 36e10cbe7a..a176f93234 100644 --- a/test/unit/modules/history.js +++ b/test/unit/modules/history.js @@ -1,7 +1,7 @@ +import Delta from 'quill-delta'; import Quill from '../../../core'; import { globalRegistry } from '../../../core/quill'; import { getLastChangeIndex } from '../../../modules/history'; -import Delta from '../../../utils/delta'; describe('History', function() { describe('getLastChangeIndex', function() { diff --git a/test/unit/modules/syntax.js b/test/unit/modules/syntax.js index 131d624df7..828e955989 100644 --- a/test/unit/modules/syntax.js +++ b/test/unit/modules/syntax.js @@ -1,9 +1,9 @@ import hljs from 'highlight.js'; +import Delta from 'quill-delta'; import Quill from '../../../core/quill'; import BoldBlot from '../../../formats/bold'; import CodeBlock, { CodeBlockContainer } from '../../../formats/code'; import Syntax, { CodeBlock as SyntaxCodeBlock } from '../../../modules/syntax'; -import Delta from '../../../utils/delta'; const HIGHLIGHT_INTERVAL = 10; diff --git a/test/unit/modules/table.js b/test/unit/modules/table.js index 921a528633..bb9d8015c1 100644 --- a/test/unit/modules/table.js +++ b/test/unit/modules/table.js @@ -1,4 +1,4 @@ -import Delta from '../../../utils/delta'; +import Delta from 'quill-delta'; import Quill from '../../../core/quill'; describe('Table Module', function() { diff --git a/test/unit/utils/delta.js b/test/unit/utils/delta.js index 3d018e8381..d690c4919d 100644 --- a/test/unit/utils/delta.js +++ b/test/unit/utils/delta.js @@ -1,4 +1,4 @@ -import Delta from '../../../utils/delta'; +import Delta from 'quill-delta'; describe('Delta', () => { describe('compose', () => { From 6e40f1196d4254adc74faf98185c6f4625cfa969 Mon Sep 17 00:00:00 2001 From: luin Date: Fri, 18 Jun 2021 14:42:53 +0800 Subject: [PATCH 06/26] Fix composePosition --- modules/tableEmbed.js | 4 +- test/unit.js | 1 + .../{utils/delta.js => modules/tableEmbed.js} | 70 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) rename test/unit/{utils/delta.js => modules/tableEmbed.js} (87%) diff --git a/modules/tableEmbed.js b/modules/tableEmbed.js index 057e1df5e4..8f2cbf1cc8 100644 --- a/modules/tableEmbed.js +++ b/modules/tableEmbed.js @@ -12,13 +12,13 @@ const composePosition = (delta, index) => { let newIndex = index; const thisIter = Delta.Op.iterator(delta.ops); let offset = 0; - while (thisIter.hasNext() && offset <= index) { + while (thisIter.hasNext() && offset <= newIndex) { const length = thisIter.peekLength(); const nextType = thisIter.peekType(); thisIter.next(); switch (nextType) { case 'delete': - if (length > index - offset) { + if (length > newIndex - offset) { return null; } newIndex -= length; 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/utils/delta.js b/test/unit/modules/tableEmbed.js similarity index 87% rename from test/unit/utils/delta.js rename to test/unit/modules/tableEmbed.js index d690c4919d..9cb59091d8 100644 --- a/test/unit/utils/delta.js +++ b/test/unit/modules/tableEmbed.js @@ -1,6 +1,11 @@ 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([ @@ -57,6 +62,71 @@ describe('Delta', () => { ); }); + it('adds two rows', () => { + 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' } }, + { insert: { id: '66666666' } }, + ], + }, + }, + }, + ]); + + expect(base.compose(change)).toEqual( + new Delta([ + { + insert: { + table: { + 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([ { From 4ff9d3975cb16660f0c8502565a9ef1182752093 Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 20 Jun 2021 10:32:42 +0800 Subject: [PATCH 07/26] Fix the test case for invert tables --- test/unit/modules/tableEmbed.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/unit/modules/tableEmbed.js b/test/unit/modules/tableEmbed.js index 9cb59091d8..b2daef9737 100644 --- a/test/unit/modules/tableEmbed.js +++ b/test/unit/modules/tableEmbed.js @@ -454,7 +454,7 @@ describe('Delta', () => { { retain: { table: { - rows: [{ remove: { id: '22222222' } }], + rows: [{ delete: 1 }], columns: [{ retain: 1 }, { delete: 1 }], }, }, @@ -466,6 +466,7 @@ describe('Delta', () => { { retain: { table: { + rows: [{ insert: { id: '11111111' } }], columns: [ { retain: 1 }, { insert: { id: '44444444' }, attributes: { width: 100 } }, From af4f1eb2b14384ddc9af75a584919e36d7b6aeb2 Mon Sep 17 00:00:00 2001 From: luin Date: Thu, 22 Jul 2021 14:51:59 +0800 Subject: [PATCH 08/26] Update delta for embed transform --- modules/tableEmbed.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/tableEmbed.js b/modules/tableEmbed.js index 8f2cbf1cc8..984fdc72af 100644 --- a/modules/tableEmbed.js +++ b/modules/tableEmbed.js @@ -215,7 +215,7 @@ export const tableHandler = { class TableEmbed extends Module { static register() { - Delta.registerEmbed('table', tableHandler); + Delta.registerEmbed('table-embed', tableHandler); } } diff --git a/package-lock.json b/package-lock.json index bfd144194e..761fee929d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9725,8 +9725,8 @@ "dev": true }, "quill-delta": { - "version": "github:quilljs/delta#6ddb8ddf121ad018ca6a6e7593e4a06c471c4dda", - "from": "github:quilljs/delta#6ddb8dd", + "version": "github:quilljs/delta#4a4c97af897abe0337cc8c9e419cd7f26a39de94", + "from": "github:quilljs/delta#4a4c97af897abe0337cc8c9e419cd7f26a39de94", "requires": { "fast-diff": "1.2.0", "lodash.clonedeep": "^4.5.0", diff --git a/package.json b/package.json index d61a8ed627..0248d90f96 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "lodash.isequal": "^4.5.0", "lodash.merge": "^4.5.0", "parchment": "2.0.0-dev.2", - "quill-delta": "github:quilljs/delta#37b73be28d02ff5395268c460c3201f6fe37892f" + "quill-delta": "github:quilljs/delta#4a4c97af897abe0337cc8c9e419cd7f26a39de94" }, "devDependencies": { "@babel/core": "^7.9.0", From 61a2fd5cd32a1e72df2b3ae3822bbd17bedc1778 Mon Sep 17 00:00:00 2001 From: luin Date: Fri, 20 Aug 2021 13:44:55 +0800 Subject: [PATCH 09/26] Rename table to table-embed to avoid name conflicts --- test/random.js | 10 ++-- test/unit/modules/tableEmbed.js | 88 +++++++++++++++++++-------------- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/test/random.js b/test/random.js index 0a1ee68763..81ed72ec30 100644 --- a/test/random.js +++ b/test/random.js @@ -54,8 +54,10 @@ const getRandomCellContent = () => { const getRandomChange = base => { const table = {}; const dimension = { - rows: new Delta(base.ops[0].insert.table.rows || []).length(), - columns: new Delta(base.ops[0].insert.table.columns || []).length(), + 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]; @@ -99,7 +101,7 @@ const getRandomChange = base => { }), }; }); - return new Delta([attachAttributes({ retain: { table } })]); + return new Delta([attachAttributes({ retain: { 'table-embed': table } })]); }; const getRandomRowColumnInsert = count => { @@ -134,7 +136,7 @@ const getRandomBase = () => { }); if (Object.keys(cells).length) table.cells = cells; } - return new Delta([{ insert: { table } }]); + return new Delta([{ insert: { 'table-embed': table } }]); }; const runTestCase = () => { diff --git a/test/unit/modules/tableEmbed.js b/test/unit/modules/tableEmbed.js index b2daef9737..670692bce5 100644 --- a/test/unit/modules/tableEmbed.js +++ b/test/unit/modules/tableEmbed.js @@ -11,7 +11,7 @@ describe('Delta', () => { const base = new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' }, attributes: { height: 20 } }, ], @@ -32,14 +32,16 @@ describe('Delta', () => { ]); const change = new Delta([ - { retain: { table: { rows: [{ insert: { id: '55555555' } }] } } }, + { + retain: { 'table-embed': { rows: [{ insert: { id: '55555555' } }] } }, + }, ]); expect(base.compose(change)).toEqual( new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '55555555' } }, { insert: { id: '11111111' }, attributes: { height: 20 } }, @@ -66,7 +68,7 @@ describe('Delta', () => { const base = new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' }, attributes: { height: 20 } }, ], @@ -89,7 +91,7 @@ describe('Delta', () => { const change = new Delta([ { retain: { - table: { + 'table-embed': { rows: [ { insert: { id: '55555555' } }, { insert: { id: '66666666' } }, @@ -103,7 +105,7 @@ describe('Delta', () => { new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '55555555' } }, { insert: { id: '66666666' } }, @@ -131,7 +133,7 @@ describe('Delta', () => { const base = new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' } }, { insert: { id: '22222222' }, attributes: { height: 20 } }, @@ -153,7 +155,7 @@ describe('Delta', () => { const change = new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ insert: { id: '66666666' } }], cells: { '3:2': { attributes: { align: 'right' } }, @@ -168,7 +170,7 @@ describe('Delta', () => { new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '66666666' } }, { insert: { id: '11111111' } }, @@ -197,7 +199,7 @@ describe('Delta', () => { const base = new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' }, attributes: { height: 20 } }, ], @@ -220,7 +222,7 @@ describe('Delta', () => { const change = new Delta([ { retain: { - table: { + 'table-embed': { columns: [{ retain: 1 }, { delete: 1 }], }, }, @@ -231,7 +233,7 @@ describe('Delta', () => { new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' }, attributes: { height: 20 } }, ], @@ -250,7 +252,9 @@ describe('Delta', () => { const base = new Delta([ { insert: { - table: { cells: { '1:2': { attributes: { align: 'center' } } } }, + 'table-embed': { + cells: { '1:2': { attributes: { align: 'center' } } }, + }, }, }, ]); @@ -258,27 +262,31 @@ describe('Delta', () => { const change = new Delta([ { retain: { - table: { cells: { '1:2': { attributes: { align: null } } } }, + 'table-embed': { + cells: { '1:2': { attributes: { align: null } } }, + }, }, }, ]); expect(base.compose(change)).toEqual( - new Delta([{ insert: { table: {} } }]), + new Delta([{ insert: { 'table-embed': {} } }]), ); }); it('removes all rows', () => { const base = new Delta([ - { insert: { table: { rows: [{ insert: { id: '11111111' } }] } } }, + { + insert: { 'table-embed': { rows: [{ insert: { id: '11111111' } }] } }, + }, ]); const change = new Delta([ - { retain: { table: { rows: [{ delete: 1 }] } } }, + { retain: { 'table-embed': { rows: [{ delete: 1 }] } } }, ]); expect(base.compose(change)).toEqual( - new Delta([{ insert: { table: {} } }]), + new Delta([{ insert: { 'table-embed': {} } }]), ); }); }); @@ -288,7 +296,7 @@ describe('Delta', () => { const change1 = new Delta([ { retain: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' } }, { insert: { id: '22222222' } }, @@ -307,7 +315,7 @@ describe('Delta', () => { const change2 = new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ delete: 1 }, { retain: 1, attributes: { height: 50 } }], columns: [ { delete: 1 }, @@ -322,7 +330,7 @@ describe('Delta', () => { new Delta([ { retain: { - table: { + 'table-embed': { rows: [ { retain: 3 }, { delete: 1 }, @@ -344,7 +352,7 @@ describe('Delta', () => { const change1 = new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ insert: { id: '22222222' } }], cells: { '8:1': { @@ -362,7 +370,7 @@ describe('Delta', () => { const change2 = new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ delete: 1 }], cells: { '6:1': { @@ -381,7 +389,7 @@ describe('Delta', () => { new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ retain: 1 }, { delete: 1 }], cells: { '7:1': { @@ -402,7 +410,9 @@ describe('Delta', () => { const change1 = new Delta([ { retain: { - table: { cells: { '8:1': { attributes: { align: 'right' } } } }, + 'table-embed': { + cells: { '8:1': { attributes: { align: 'right' } } }, + }, }, }, ]); @@ -410,7 +420,9 @@ describe('Delta', () => { const change2 = new Delta([ { retain: { - table: { cells: { '8:1': { attributes: { align: 'left' } } } }, + 'table-embed': { + cells: { '8:1': { attributes: { align: 'left' } } }, + }, }, }, ]); @@ -419,14 +431,16 @@ describe('Delta', () => { new Delta([ { retain: { - table: { cells: { '8:1': { attributes: { align: 'left' } } } }, + 'table-embed': { + cells: { '8:1': { attributes: { align: 'left' } } }, + }, }, }, ]), ); expect(change1.transform(change2, true)).toEqual( - new Delta([{ retain: { table: {} } }]), + new Delta([{ retain: { 'table-embed': {} } }]), ); }); }); @@ -436,7 +450,7 @@ describe('Delta', () => { const base = new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' } }, { insert: { id: '22222222' } }, @@ -453,7 +467,7 @@ describe('Delta', () => { const change = new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ delete: 1 }], columns: [{ retain: 1 }, { delete: 1 }], }, @@ -465,7 +479,7 @@ describe('Delta', () => { new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ insert: { id: '11111111' } }], columns: [ { retain: 1 }, @@ -482,7 +496,7 @@ describe('Delta', () => { const base = new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' } }, { insert: { id: '22222222' } }, @@ -504,7 +518,7 @@ describe('Delta', () => { const change = new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ insert: { id: '55555555' } }], cells: { '2:2': { @@ -519,7 +533,7 @@ describe('Delta', () => { new Delta([ { retain: { - table: { + 'table-embed': { rows: [{ delete: 1 }], cells: { '1:2': { @@ -537,7 +551,7 @@ describe('Delta', () => { const base = new Delta([ { insert: { - table: { + 'table-embed': { rows: [ { insert: { id: '11111111' } }, { insert: { id: '22222222' } }, @@ -559,7 +573,7 @@ describe('Delta', () => { const change = new Delta([ { retain: { - table: { + 'table-embed': { columns: [{ retain: 1 }, { delete: 1 }], }, }, @@ -569,7 +583,7 @@ describe('Delta', () => { new Delta([ { retain: { - table: { + 'table-embed': { columns: [{ retain: 1 }, { insert: { id: '44444444' } }], cells: { '1:2': { From 4da5c387e854baa5eeab9bb4859f1ee63391b7c2 Mon Sep 17 00:00:00 2001 From: luin Date: Fri, 20 Aug 2021 14:19:36 +0800 Subject: [PATCH 10/26] Upgrade delta for embed retaining --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 761fee929d..a03dbc08c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9725,8 +9725,8 @@ "dev": true }, "quill-delta": { - "version": "github:quilljs/delta#4a4c97af897abe0337cc8c9e419cd7f26a39de94", - "from": "github:quilljs/delta#4a4c97af897abe0337cc8c9e419cd7f26a39de94", + "version": "github:quilljs/delta#87cd1e6de795eb29abe79a29429ca3b126dc9031", + "from": "github:quilljs/delta#87cd1e6de795eb29abe79a29429ca3b126dc9031", "requires": { "fast-diff": "1.2.0", "lodash.clonedeep": "^4.5.0", diff --git a/package.json b/package.json index 0248d90f96..fd592e91cd 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "lodash.isequal": "^4.5.0", "lodash.merge": "^4.5.0", "parchment": "2.0.0-dev.2", - "quill-delta": "github:quilljs/delta#4a4c97af897abe0337cc8c9e419cd7f26a39de94" + "quill-delta": "github:quilljs/delta#87cd1e6de795eb29abe79a29429ca3b126dc9031" }, "devDependencies": { "@babel/core": "^7.9.0", From 22d7cf689674a3f095f31658fee9d8d15f41593c Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 30 May 2021 14:14:28 +0800 Subject: [PATCH 11/26] Add table handler --- blots/block.js | 2 +- core/editor.js | 3 +- core/quill.js | 6 +- modules/clipboard.js | 7 +- modules/keyboard.js | 5 +- modules/syntax.js | 8 +- modules/table.js | 2 +- modules/toolbar.js | 4 +- modules/uploader.js | 2 +- package.json | 1 + test/unit/core/editor.js | 2 +- test/unit/core/quill.js | 6 +- test/unit/formats/align.js | 2 +- test/unit/formats/code.js | 2 +- test/unit/formats/color.js | 2 +- test/unit/formats/header.js | 2 +- test/unit/formats/indent.js | 2 +- test/unit/formats/link.js | 2 +- test/unit/formats/list.js | 2 +- test/unit/formats/table.js | 2 +- test/unit/modules/clipboard.js | 4 +- test/unit/modules/history.js | 2 +- test/unit/modules/syntax.js | 2 +- test/unit/modules/table.js | 2 +- test/unit/utils/delta.js | 516 +++++++++++++++++++++++++++++++++ utils/delta.js | 215 ++++++++++++++ 26 files changed, 769 insertions(+), 36 deletions(-) create mode 100644 test/unit/utils/delta.js create mode 100644 utils/delta.js diff --git a/blots/block.js b/blots/block.js index ecdd4b0b05..e0e5efd60a 100644 --- a/blots/block.js +++ b/blots/block.js @@ -1,4 +1,3 @@ -import Delta from 'quill-delta'; import { AttributorStore, BlockBlot, @@ -6,6 +5,7 @@ import { LeafBlot, Scope, } from 'parchment'; +import Delta from '../utils/delta'; import Break from './break'; import Inline from './inline'; import TextBlot from './text'; diff --git a/core/editor.js b/core/editor.js index 94f6af97a3..566300fe08 100644 --- a/core/editor.js +++ b/core/editor.js @@ -1,13 +1,14 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import merge from 'lodash.merge'; -import Delta, { AttributeMap, Op } from 'quill-delta'; +import { AttributeMap, Op } from 'quill-delta'; import { LeafBlot, Scope } from 'parchment'; import { Range } from './selection'; import CursorBlot from '../blots/cursor'; import Block, { BlockEmbed, bubbleFormats } from '../blots/block'; import Break from '../blots/break'; import TextBlot, { escapeText } from '../blots/text'; +import Delta from '../utils/delta'; const ASCII = /^[ -~]*$/; diff --git a/core/quill.js b/core/quill.js index 44bf15dd97..d39b58bcbe 100644 --- a/core/quill.js +++ b/core/quill.js @@ -1,13 +1,13 @@ -import Delta from 'quill-delta'; import cloneDeep from 'lodash.clonedeep'; import merge from 'lodash.merge'; import * as Parchment from 'parchment'; +import Delta from '../utils/delta'; import Editor from './editor'; import Emitter from './emitter'; -import Module from './module'; -import Selection, { Range } from './selection'; import instances from './instances'; import logger from './logger'; +import Module from './module'; +import Selection, { Range } from './selection'; import Theme from './theme'; const debug = logger('quill'); diff --git a/modules/clipboard.js b/modules/clipboard.js index 8eadd406a6..f95b61c6d7 100644 --- a/modules/clipboard.js +++ b/modules/clipboard.js @@ -1,17 +1,15 @@ -import Delta from 'quill-delta'; import { Attributor, + BlockBlot, ClassAttributor, EmbedBlot, Scope, StyleAttributor, - BlockBlot, } from 'parchment'; import { BlockEmbed } from '../blots/block'; -import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; - +import Quill from '../core/quill'; import { AlignAttribute, AlignStyle } from '../formats/align'; import { BackgroundStyle } from '../formats/background'; import CodeBlock from '../formats/code'; @@ -19,6 +17,7 @@ import { ColorStyle } from '../formats/color'; import { DirectionAttribute, DirectionStyle } from '../formats/direction'; import { FontStyle } from '../formats/font'; import { SizeStyle } from '../formats/size'; +import Delta from '../utils/delta'; import { deleteRange } from './keyboard'; const debug = logger('quill:clipboard'); diff --git a/modules/keyboard.js b/modules/keyboard.js index 76bacc8de2..7e000a7d34 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -1,10 +1,11 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; -import Delta, { AttributeMap } from 'quill-delta'; import { EmbedBlot, Scope, TextBlot } from 'parchment'; -import Quill from '../core/quill'; +import { AttributeMap } from 'quill-delta'; import logger from '../core/logger'; import Module from '../core/module'; +import Quill from '../core/quill'; +import Delta from '../utils/delta'; const debug = logger('quill:keyboard'); diff --git a/modules/syntax.js b/modules/syntax.js index 70e135423a..af25590287 100644 --- a/modules/syntax.js +++ b/modules/syntax.js @@ -1,13 +1,13 @@ -import Delta from 'quill-delta'; import { ClassAttributor, Scope } from 'parchment'; -import Inline from '../blots/inline'; -import Quill from '../core/quill'; -import Module from '../core/module'; import { blockDelta } from '../blots/block'; import BreakBlot from '../blots/break'; import CursorBlot from '../blots/cursor'; +import Inline from '../blots/inline'; import TextBlot, { escapeText } from '../blots/text'; +import Module from '../core/module'; +import Quill from '../core/quill'; import CodeBlock, { CodeBlockContainer } from '../formats/code'; +import Delta from '../utils/delta'; import { traverse } from './clipboard'; const TokenAttributor = new ClassAttributor('code-token', 'hljs', { diff --git a/modules/table.js b/modules/table.js index 4fde5dc745..5e21dab439 100644 --- a/modules/table.js +++ b/modules/table.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../utils/delta'; import Quill from '../core/quill'; import Module from '../core/module'; import { diff --git a/modules/toolbar.js b/modules/toolbar.js index a16923ec0a..a54603af1b 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -1,8 +1,8 @@ -import Delta from 'quill-delta'; import { EmbedBlot, Scope } from 'parchment'; -import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; +import Quill from '../core/quill'; +import Delta from '../utils/delta'; const debug = logger('quill:toolbar'); diff --git a/modules/uploader.js b/modules/uploader.js index b3eabb19be..d1f05dc0bf 100644 --- a/modules/uploader.js +++ b/modules/uploader.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../utils/delta'; import Emitter from '../core/emitter'; import Module from '../core/module'; diff --git a/package.json b/package.json index fd592e91cd..63d6a33d55 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "modules", "themes", "ui", + "utils", "dist/quill.bubble.css", "dist/quill.snow.css", "dist/quill.core.css", diff --git a/test/unit/core/editor.js b/test/unit/core/editor.js index f4ce692f92..83c11d2d33 100644 --- a/test/unit/core/editor.js +++ b/test/unit/core/editor.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Selection, { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Editor', function() { describe('insert', function() { diff --git a/test/unit/core/quill.js b/test/unit/core/quill.js index 424a979f82..d83ad638e4 100644 --- a/test/unit/core/quill.js +++ b/test/unit/core/quill.js @@ -1,10 +1,10 @@ -import Delta from 'quill-delta'; +import Emitter from '../../../core/emitter'; import Quill, { expandConfig, overload } from '../../../core/quill'; +import { Range } from '../../../core/selection'; import Theme from '../../../core/theme'; -import Emitter from '../../../core/emitter'; import Toolbar from '../../../modules/toolbar'; import Snow from '../../../themes/snow'; -import { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Quill', function() { it('imports', function() { diff --git a/test/unit/formats/align.js b/test/unit/formats/align.js index 0571ea402d..f73c5638fe 100644 --- a/test/unit/formats/align.js +++ b/test/unit/formats/align.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Align', function() { it('add', function() { diff --git a/test/unit/formats/code.js b/test/unit/formats/code.js index 2497bb7f83..e3c02780c8 100644 --- a/test/unit/formats/code.js +++ b/test/unit/formats/code.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Code', function() { it('format newline', function() { diff --git a/test/unit/formats/color.js b/test/unit/formats/color.js index 9d50cd41fe..179131e1cf 100644 --- a/test/unit/formats/color.js +++ b/test/unit/formats/color.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Color', function() { it('add', function() { diff --git a/test/unit/formats/header.js b/test/unit/formats/header.js index 43dbc8917e..c1ff711dbb 100644 --- a/test/unit/formats/header.js +++ b/test/unit/formats/header.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../../../utils/delta'; import Editor from '../../../core/editor'; describe('Header', function() { diff --git a/test/unit/formats/indent.js b/test/unit/formats/indent.js index c38a0d2c6d..3ae67f2751 100644 --- a/test/unit/formats/indent.js +++ b/test/unit/formats/indent.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Indent', function() { it('+1', function() { diff --git a/test/unit/formats/link.js b/test/unit/formats/link.js index 4197d1b9a0..ca81250e30 100644 --- a/test/unit/formats/link.js +++ b/test/unit/formats/link.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Link from '../../../formats/link'; +import Delta from '../../../utils/delta'; describe('Link', function() { it('add', function() { diff --git a/test/unit/formats/list.js b/test/unit/formats/list.js index d0b9f24127..37e9dee814 100644 --- a/test/unit/formats/list.js +++ b/test/unit/formats/list.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('List', function() { it('add', function() { diff --git a/test/unit/formats/table.js b/test/unit/formats/table.js index e3a299dbee..7f82e3453a 100644 --- a/test/unit/formats/table.js +++ b/test/unit/formats/table.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; const tableDelta = new Delta() .insert('A1') diff --git a/test/unit/modules/clipboard.js b/test/unit/modules/clipboard.js index 8a76748690..3a204cfb39 100644 --- a/test/unit/modules/clipboard.js +++ b/test/unit/modules/clipboard.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; -import { Range } from '../../../core/selection'; import Quill from '../../../core'; +import { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Clipboard', function() { describe('events', function() { diff --git a/test/unit/modules/history.js b/test/unit/modules/history.js index a176f93234..36e10cbe7a 100644 --- a/test/unit/modules/history.js +++ b/test/unit/modules/history.js @@ -1,7 +1,7 @@ -import Delta from 'quill-delta'; import Quill from '../../../core'; import { globalRegistry } from '../../../core/quill'; import { getLastChangeIndex } from '../../../modules/history'; +import Delta from '../../../utils/delta'; describe('History', function() { describe('getLastChangeIndex', function() { diff --git a/test/unit/modules/syntax.js b/test/unit/modules/syntax.js index 828e955989..131d624df7 100644 --- a/test/unit/modules/syntax.js +++ b/test/unit/modules/syntax.js @@ -1,9 +1,9 @@ import hljs from 'highlight.js'; -import Delta from 'quill-delta'; import Quill from '../../../core/quill'; import BoldBlot from '../../../formats/bold'; import CodeBlock, { CodeBlockContainer } from '../../../formats/code'; import Syntax, { CodeBlock as SyntaxCodeBlock } from '../../../modules/syntax'; +import Delta from '../../../utils/delta'; const HIGHLIGHT_INTERVAL = 10; diff --git a/test/unit/modules/table.js b/test/unit/modules/table.js index bb9d8015c1..921a528633 100644 --- a/test/unit/modules/table.js +++ b/test/unit/modules/table.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../../../utils/delta'; import Quill from '../../../core/quill'; describe('Table Module', function() { diff --git a/test/unit/utils/delta.js b/test/unit/utils/delta.js new file mode 100644 index 0000000000..3d018e8381 --- /dev/null +++ b/test/unit/utils/delta.js @@ -0,0 +1,516 @@ +import Delta from '../../../utils/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' }, + }, + }, + }, + }, + }, + ]), + ); + }); + }); +}); diff --git a/utils/delta.js b/utils/delta.js new file mode 100644 index 0000000000..90b4dff3bf --- /dev/null +++ b/utils/delta.js @@ -0,0 +1,215 @@ +import Delta from 'quill-delta'; + +const parseCellIdentity = identity => { + const parts = identity.split(':'); + return [Number(parts[0]) - 1, Number(parts[1]) - 1]; +}; + +const stringifyCellIdentity = (row, column) => `${row + 1}:${column + 1}`; + +const composePosition = (delta, index) => { + let newIndex = index; + const thisIter = Delta.Op.iterator(delta.ops); + let offset = 0; + while (thisIter.hasNext() && offset <= index) { + const length = thisIter.peekLength(); + const nextType = thisIter.peekType(); + thisIter.next(); + switch (nextType) { + case 'delete': + if (length > index - 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; +}; + +Delta.registerHandler('table', { + compose(a, b, keepNull) { + // Step 2~3: Compose rows and columns separately + const rows = new Delta(a.rows || []).compose(new Delta(b.rows || [])); + const columns = new Delta(a.columns || []).compose( + new Delta(b.columns || []), + ); + + // Step 4: Reindex cell identities according to B's rows and columns + const cells = reindexCellIdentities(a.cells || {}, { + rows: new Delta(b.rows || []), + columns: new Delta(b.columns || []), + }); + + // Step 5: Compose cell content and attributes + 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 }); + }, +}); + +export default Delta; From a9c53434a3bd4d52149aa2bec4d55aad9f76481b Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 14 Jun 2021 15:59:06 +0800 Subject: [PATCH 12/26] Register table handler in a module --- blots/block.js | 2 +- core/editor.js | 5 +- core/quill.js | 6 +- modules/clipboard.js | 7 +- modules/keyboard.js | 5 +- modules/syntax.js | 8 +- modules/table.js | 2 +- modules/toolbar.js | 4 +- modules/uploader.js | 2 +- package.json | 1 - test/unit/core/editor.js | 2 +- test/unit/core/quill.js | 6 +- test/unit/formats/align.js | 2 +- test/unit/formats/code.js | 2 +- test/unit/formats/color.js | 2 +- test/unit/formats/header.js | 2 +- test/unit/formats/indent.js | 2 +- test/unit/formats/link.js | 2 +- test/unit/formats/list.js | 2 +- test/unit/formats/table.js | 2 +- test/unit/modules/clipboard.js | 4 +- test/unit/modules/history.js | 2 +- test/unit/modules/syntax.js | 2 +- test/unit/modules/table.js | 2 +- test/unit/utils/delta.js | 2 +- utils/delta.js | 215 --------------------------------- 26 files changed, 40 insertions(+), 253 deletions(-) delete mode 100644 utils/delta.js diff --git a/blots/block.js b/blots/block.js index e0e5efd60a..ecdd4b0b05 100644 --- a/blots/block.js +++ b/blots/block.js @@ -1,3 +1,4 @@ +import Delta from 'quill-delta'; import { AttributorStore, BlockBlot, @@ -5,7 +6,6 @@ import { LeafBlot, Scope, } from 'parchment'; -import Delta from '../utils/delta'; import Break from './break'; import Inline from './inline'; import TextBlot from './text'; diff --git a/core/editor.js b/core/editor.js index 566300fe08..5a84bbeba0 100644 --- a/core/editor.js +++ b/core/editor.js @@ -1,14 +1,17 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import merge from 'lodash.merge'; +<<<<<<< HEAD import { AttributeMap, Op } from 'quill-delta'; +======= +import Delta, { AttributeMap } from 'quill-delta'; +>>>>>>> c9fa49e4 (Register table handler in a module) import { LeafBlot, Scope } from 'parchment'; import { Range } from './selection'; import CursorBlot from '../blots/cursor'; import Block, { BlockEmbed, bubbleFormats } from '../blots/block'; import Break from '../blots/break'; import TextBlot, { escapeText } from '../blots/text'; -import Delta from '../utils/delta'; const ASCII = /^[ -~]*$/; diff --git a/core/quill.js b/core/quill.js index d39b58bcbe..44bf15dd97 100644 --- a/core/quill.js +++ b/core/quill.js @@ -1,13 +1,13 @@ +import Delta from 'quill-delta'; import cloneDeep from 'lodash.clonedeep'; import merge from 'lodash.merge'; import * as Parchment from 'parchment'; -import Delta from '../utils/delta'; import Editor from './editor'; import Emitter from './emitter'; -import instances from './instances'; -import logger from './logger'; import Module from './module'; import Selection, { Range } from './selection'; +import instances from './instances'; +import logger from './logger'; import Theme from './theme'; const debug = logger('quill'); diff --git a/modules/clipboard.js b/modules/clipboard.js index f95b61c6d7..8eadd406a6 100644 --- a/modules/clipboard.js +++ b/modules/clipboard.js @@ -1,15 +1,17 @@ +import Delta from 'quill-delta'; import { Attributor, - BlockBlot, ClassAttributor, EmbedBlot, Scope, StyleAttributor, + BlockBlot, } from 'parchment'; import { BlockEmbed } from '../blots/block'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; + import { AlignAttribute, AlignStyle } from '../formats/align'; import { BackgroundStyle } from '../formats/background'; import CodeBlock from '../formats/code'; @@ -17,7 +19,6 @@ import { ColorStyle } from '../formats/color'; import { DirectionAttribute, DirectionStyle } from '../formats/direction'; import { FontStyle } from '../formats/font'; import { SizeStyle } from '../formats/size'; -import Delta from '../utils/delta'; import { deleteRange } from './keyboard'; const debug = logger('quill:clipboard'); diff --git a/modules/keyboard.js b/modules/keyboard.js index 7e000a7d34..76bacc8de2 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -1,11 +1,10 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; +import Delta, { AttributeMap } from 'quill-delta'; import { EmbedBlot, Scope, TextBlot } from 'parchment'; -import { AttributeMap } from 'quill-delta'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; -import Delta from '../utils/delta'; const debug = logger('quill:keyboard'); diff --git a/modules/syntax.js b/modules/syntax.js index af25590287..70e135423a 100644 --- a/modules/syntax.js +++ b/modules/syntax.js @@ -1,13 +1,13 @@ +import Delta from 'quill-delta'; import { ClassAttributor, Scope } from 'parchment'; +import Inline from '../blots/inline'; +import Quill from '../core/quill'; +import Module from '../core/module'; import { blockDelta } from '../blots/block'; import BreakBlot from '../blots/break'; import CursorBlot from '../blots/cursor'; -import Inline from '../blots/inline'; import TextBlot, { escapeText } from '../blots/text'; -import Module from '../core/module'; -import Quill from '../core/quill'; import CodeBlock, { CodeBlockContainer } from '../formats/code'; -import Delta from '../utils/delta'; import { traverse } from './clipboard'; const TokenAttributor = new ClassAttributor('code-token', 'hljs', { diff --git a/modules/table.js b/modules/table.js index 5e21dab439..4fde5dc745 100644 --- a/modules/table.js +++ b/modules/table.js @@ -1,4 +1,4 @@ -import Delta from '../utils/delta'; +import Delta from 'quill-delta'; import Quill from '../core/quill'; import Module from '../core/module'; import { diff --git a/modules/toolbar.js b/modules/toolbar.js index a54603af1b..a16923ec0a 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -1,8 +1,8 @@ +import Delta from 'quill-delta'; import { EmbedBlot, Scope } from 'parchment'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; -import Delta from '../utils/delta'; const debug = logger('quill:toolbar'); diff --git a/modules/uploader.js b/modules/uploader.js index d1f05dc0bf..b3eabb19be 100644 --- a/modules/uploader.js +++ b/modules/uploader.js @@ -1,4 +1,4 @@ -import Delta from '../utils/delta'; +import Delta from 'quill-delta'; import Emitter from '../core/emitter'; import Module from '../core/module'; diff --git a/package.json b/package.json index 63d6a33d55..fd592e91cd 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "modules", "themes", "ui", - "utils", "dist/quill.bubble.css", "dist/quill.snow.css", "dist/quill.core.css", diff --git a/test/unit/core/editor.js b/test/unit/core/editor.js index 83c11d2d33..f4ce692f92 100644 --- a/test/unit/core/editor.js +++ b/test/unit/core/editor.js @@ -1,6 +1,6 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Selection, { Range } from '../../../core/selection'; -import Delta from '../../../utils/delta'; describe('Editor', function() { describe('insert', function() { diff --git a/test/unit/core/quill.js b/test/unit/core/quill.js index d83ad638e4..424a979f82 100644 --- a/test/unit/core/quill.js +++ b/test/unit/core/quill.js @@ -1,10 +1,10 @@ -import Emitter from '../../../core/emitter'; +import Delta from 'quill-delta'; import Quill, { expandConfig, overload } from '../../../core/quill'; -import { Range } from '../../../core/selection'; import Theme from '../../../core/theme'; +import Emitter from '../../../core/emitter'; import Toolbar from '../../../modules/toolbar'; import Snow from '../../../themes/snow'; -import Delta from '../../../utils/delta'; +import { Range } from '../../../core/selection'; describe('Quill', function() { it('imports', function() { diff --git a/test/unit/formats/align.js b/test/unit/formats/align.js index f73c5638fe..0571ea402d 100644 --- a/test/unit/formats/align.js +++ b/test/unit/formats/align.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Align', function() { it('add', function() { diff --git a/test/unit/formats/code.js b/test/unit/formats/code.js index e3c02780c8..2497bb7f83 100644 --- a/test/unit/formats/code.js +++ b/test/unit/formats/code.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Code', function() { it('format newline', function() { diff --git a/test/unit/formats/color.js b/test/unit/formats/color.js index 179131e1cf..9d50cd41fe 100644 --- a/test/unit/formats/color.js +++ b/test/unit/formats/color.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Color', function() { it('add', function() { diff --git a/test/unit/formats/header.js b/test/unit/formats/header.js index c1ff711dbb..43dbc8917e 100644 --- a/test/unit/formats/header.js +++ b/test/unit/formats/header.js @@ -1,4 +1,4 @@ -import Delta from '../../../utils/delta'; +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; describe('Header', function() { diff --git a/test/unit/formats/indent.js b/test/unit/formats/indent.js index 3ae67f2751..c38a0d2c6d 100644 --- a/test/unit/formats/indent.js +++ b/test/unit/formats/indent.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Indent', function() { it('+1', function() { diff --git a/test/unit/formats/link.js b/test/unit/formats/link.js index ca81250e30..4197d1b9a0 100644 --- a/test/unit/formats/link.js +++ b/test/unit/formats/link.js @@ -1,6 +1,6 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Link from '../../../formats/link'; -import Delta from '../../../utils/delta'; describe('Link', function() { it('add', function() { diff --git a/test/unit/formats/list.js b/test/unit/formats/list.js index 37e9dee814..d0b9f24127 100644 --- a/test/unit/formats/list.js +++ b/test/unit/formats/list.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('List', function() { it('add', function() { diff --git a/test/unit/formats/table.js b/test/unit/formats/table.js index 7f82e3453a..e3a299dbee 100644 --- a/test/unit/formats/table.js +++ b/test/unit/formats/table.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; const tableDelta = new Delta() .insert('A1') diff --git a/test/unit/modules/clipboard.js b/test/unit/modules/clipboard.js index 3a204cfb39..8a76748690 100644 --- a/test/unit/modules/clipboard.js +++ b/test/unit/modules/clipboard.js @@ -1,6 +1,6 @@ -import Quill from '../../../core'; +import Delta from 'quill-delta'; import { Range } from '../../../core/selection'; -import Delta from '../../../utils/delta'; +import Quill from '../../../core'; describe('Clipboard', function() { describe('events', function() { diff --git a/test/unit/modules/history.js b/test/unit/modules/history.js index 36e10cbe7a..a176f93234 100644 --- a/test/unit/modules/history.js +++ b/test/unit/modules/history.js @@ -1,7 +1,7 @@ +import Delta from 'quill-delta'; import Quill from '../../../core'; import { globalRegistry } from '../../../core/quill'; import { getLastChangeIndex } from '../../../modules/history'; -import Delta from '../../../utils/delta'; describe('History', function() { describe('getLastChangeIndex', function() { diff --git a/test/unit/modules/syntax.js b/test/unit/modules/syntax.js index 131d624df7..828e955989 100644 --- a/test/unit/modules/syntax.js +++ b/test/unit/modules/syntax.js @@ -1,9 +1,9 @@ import hljs from 'highlight.js'; +import Delta from 'quill-delta'; import Quill from '../../../core/quill'; import BoldBlot from '../../../formats/bold'; import CodeBlock, { CodeBlockContainer } from '../../../formats/code'; import Syntax, { CodeBlock as SyntaxCodeBlock } from '../../../modules/syntax'; -import Delta from '../../../utils/delta'; const HIGHLIGHT_INTERVAL = 10; diff --git a/test/unit/modules/table.js b/test/unit/modules/table.js index 921a528633..bb9d8015c1 100644 --- a/test/unit/modules/table.js +++ b/test/unit/modules/table.js @@ -1,4 +1,4 @@ -import Delta from '../../../utils/delta'; +import Delta from 'quill-delta'; import Quill from '../../../core/quill'; describe('Table Module', function() { diff --git a/test/unit/utils/delta.js b/test/unit/utils/delta.js index 3d018e8381..d690c4919d 100644 --- a/test/unit/utils/delta.js +++ b/test/unit/utils/delta.js @@ -1,4 +1,4 @@ -import Delta from '../../../utils/delta'; +import Delta from 'quill-delta'; describe('Delta', () => { describe('compose', () => { diff --git a/utils/delta.js b/utils/delta.js deleted file mode 100644 index 90b4dff3bf..0000000000 --- a/utils/delta.js +++ /dev/null @@ -1,215 +0,0 @@ -import Delta from 'quill-delta'; - -const parseCellIdentity = identity => { - const parts = identity.split(':'); - return [Number(parts[0]) - 1, Number(parts[1]) - 1]; -}; - -const stringifyCellIdentity = (row, column) => `${row + 1}:${column + 1}`; - -const composePosition = (delta, index) => { - let newIndex = index; - const thisIter = Delta.Op.iterator(delta.ops); - let offset = 0; - while (thisIter.hasNext() && offset <= index) { - const length = thisIter.peekLength(); - const nextType = thisIter.peekType(); - thisIter.next(); - switch (nextType) { - case 'delete': - if (length > index - 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; -}; - -Delta.registerHandler('table', { - compose(a, b, keepNull) { - // Step 2~3: Compose rows and columns separately - const rows = new Delta(a.rows || []).compose(new Delta(b.rows || [])); - const columns = new Delta(a.columns || []).compose( - new Delta(b.columns || []), - ); - - // Step 4: Reindex cell identities according to B's rows and columns - const cells = reindexCellIdentities(a.cells || {}, { - rows: new Delta(b.rows || []), - columns: new Delta(b.columns || []), - }); - - // Step 5: Compose cell content and attributes - 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 }); - }, -}); - -export default Delta; From 855433f7a8faa160eac7270a217c6ed3989139c0 Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 30 May 2021 14:14:28 +0800 Subject: [PATCH 13/26] Add table handler --- blots/block.js | 2 +- core/editor.js | 6 +- core/quill.js | 6 +- modules/clipboard.js | 7 +- modules/keyboard.js | 5 +- modules/syntax.js | 8 +- modules/table.js | 2 +- modules/toolbar.js | 4 +- modules/uploader.js | 2 +- package.json | 1 + test/unit/core/editor.js | 2 +- test/unit/core/quill.js | 6 +- test/unit/formats/align.js | 2 +- test/unit/formats/code.js | 2 +- test/unit/formats/color.js | 2 +- test/unit/formats/header.js | 2 +- test/unit/formats/indent.js | 2 +- test/unit/formats/link.js | 2 +- test/unit/formats/list.js | 2 +- test/unit/formats/table.js | 2 +- test/unit/modules/clipboard.js | 4 +- test/unit/modules/history.js | 2 +- test/unit/modules/syntax.js | 2 +- test/unit/modules/table.js | 2 +- utils/delta.js | 215 +++++++++++++++++++++++++++++++++ 25 files changed, 252 insertions(+), 40 deletions(-) create mode 100644 utils/delta.js diff --git a/blots/block.js b/blots/block.js index ecdd4b0b05..e0e5efd60a 100644 --- a/blots/block.js +++ b/blots/block.js @@ -1,4 +1,3 @@ -import Delta from 'quill-delta'; import { AttributorStore, BlockBlot, @@ -6,6 +5,7 @@ import { LeafBlot, Scope, } from 'parchment'; +import Delta from '../utils/delta'; import Break from './break'; import Inline from './inline'; import TextBlot from './text'; diff --git a/core/editor.js b/core/editor.js index 5a84bbeba0..94f6af97a3 100644 --- a/core/editor.js +++ b/core/editor.js @@ -1,11 +1,7 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; import merge from 'lodash.merge'; -<<<<<<< HEAD -import { AttributeMap, Op } from 'quill-delta'; -======= -import Delta, { AttributeMap } from 'quill-delta'; ->>>>>>> c9fa49e4 (Register table handler in a module) +import Delta, { AttributeMap, Op } from 'quill-delta'; import { LeafBlot, Scope } from 'parchment'; import { Range } from './selection'; import CursorBlot from '../blots/cursor'; diff --git a/core/quill.js b/core/quill.js index 44bf15dd97..d39b58bcbe 100644 --- a/core/quill.js +++ b/core/quill.js @@ -1,13 +1,13 @@ -import Delta from 'quill-delta'; import cloneDeep from 'lodash.clonedeep'; import merge from 'lodash.merge'; import * as Parchment from 'parchment'; +import Delta from '../utils/delta'; import Editor from './editor'; import Emitter from './emitter'; -import Module from './module'; -import Selection, { Range } from './selection'; import instances from './instances'; import logger from './logger'; +import Module from './module'; +import Selection, { Range } from './selection'; import Theme from './theme'; const debug = logger('quill'); diff --git a/modules/clipboard.js b/modules/clipboard.js index 8eadd406a6..f95b61c6d7 100644 --- a/modules/clipboard.js +++ b/modules/clipboard.js @@ -1,17 +1,15 @@ -import Delta from 'quill-delta'; import { Attributor, + BlockBlot, ClassAttributor, EmbedBlot, Scope, StyleAttributor, - BlockBlot, } from 'parchment'; import { BlockEmbed } from '../blots/block'; -import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; - +import Quill from '../core/quill'; import { AlignAttribute, AlignStyle } from '../formats/align'; import { BackgroundStyle } from '../formats/background'; import CodeBlock from '../formats/code'; @@ -19,6 +17,7 @@ import { ColorStyle } from '../formats/color'; import { DirectionAttribute, DirectionStyle } from '../formats/direction'; import { FontStyle } from '../formats/font'; import { SizeStyle } from '../formats/size'; +import Delta from '../utils/delta'; import { deleteRange } from './keyboard'; const debug = logger('quill:clipboard'); diff --git a/modules/keyboard.js b/modules/keyboard.js index 76bacc8de2..7e000a7d34 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -1,10 +1,11 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; -import Delta, { AttributeMap } from 'quill-delta'; import { EmbedBlot, Scope, TextBlot } from 'parchment'; -import Quill from '../core/quill'; +import { AttributeMap } from 'quill-delta'; import logger from '../core/logger'; import Module from '../core/module'; +import Quill from '../core/quill'; +import Delta from '../utils/delta'; const debug = logger('quill:keyboard'); diff --git a/modules/syntax.js b/modules/syntax.js index 70e135423a..af25590287 100644 --- a/modules/syntax.js +++ b/modules/syntax.js @@ -1,13 +1,13 @@ -import Delta from 'quill-delta'; import { ClassAttributor, Scope } from 'parchment'; -import Inline from '../blots/inline'; -import Quill from '../core/quill'; -import Module from '../core/module'; import { blockDelta } from '../blots/block'; import BreakBlot from '../blots/break'; import CursorBlot from '../blots/cursor'; +import Inline from '../blots/inline'; import TextBlot, { escapeText } from '../blots/text'; +import Module from '../core/module'; +import Quill from '../core/quill'; import CodeBlock, { CodeBlockContainer } from '../formats/code'; +import Delta from '../utils/delta'; import { traverse } from './clipboard'; const TokenAttributor = new ClassAttributor('code-token', 'hljs', { diff --git a/modules/table.js b/modules/table.js index 4fde5dc745..5e21dab439 100644 --- a/modules/table.js +++ b/modules/table.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../utils/delta'; import Quill from '../core/quill'; import Module from '../core/module'; import { diff --git a/modules/toolbar.js b/modules/toolbar.js index a16923ec0a..a54603af1b 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -1,8 +1,8 @@ -import Delta from 'quill-delta'; import { EmbedBlot, Scope } from 'parchment'; -import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; +import Quill from '../core/quill'; +import Delta from '../utils/delta'; const debug = logger('quill:toolbar'); diff --git a/modules/uploader.js b/modules/uploader.js index b3eabb19be..d1f05dc0bf 100644 --- a/modules/uploader.js +++ b/modules/uploader.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../utils/delta'; import Emitter from '../core/emitter'; import Module from '../core/module'; diff --git a/package.json b/package.json index fd592e91cd..63d6a33d55 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "modules", "themes", "ui", + "utils", "dist/quill.bubble.css", "dist/quill.snow.css", "dist/quill.core.css", diff --git a/test/unit/core/editor.js b/test/unit/core/editor.js index f4ce692f92..83c11d2d33 100644 --- a/test/unit/core/editor.js +++ b/test/unit/core/editor.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Selection, { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Editor', function() { describe('insert', function() { diff --git a/test/unit/core/quill.js b/test/unit/core/quill.js index 424a979f82..d83ad638e4 100644 --- a/test/unit/core/quill.js +++ b/test/unit/core/quill.js @@ -1,10 +1,10 @@ -import Delta from 'quill-delta'; +import Emitter from '../../../core/emitter'; import Quill, { expandConfig, overload } from '../../../core/quill'; +import { Range } from '../../../core/selection'; import Theme from '../../../core/theme'; -import Emitter from '../../../core/emitter'; import Toolbar from '../../../modules/toolbar'; import Snow from '../../../themes/snow'; -import { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Quill', function() { it('imports', function() { diff --git a/test/unit/formats/align.js b/test/unit/formats/align.js index 0571ea402d..f73c5638fe 100644 --- a/test/unit/formats/align.js +++ b/test/unit/formats/align.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Align', function() { it('add', function() { diff --git a/test/unit/formats/code.js b/test/unit/formats/code.js index 2497bb7f83..e3c02780c8 100644 --- a/test/unit/formats/code.js +++ b/test/unit/formats/code.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Code', function() { it('format newline', function() { diff --git a/test/unit/formats/color.js b/test/unit/formats/color.js index 9d50cd41fe..179131e1cf 100644 --- a/test/unit/formats/color.js +++ b/test/unit/formats/color.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Color', function() { it('add', function() { diff --git a/test/unit/formats/header.js b/test/unit/formats/header.js index 43dbc8917e..c1ff711dbb 100644 --- a/test/unit/formats/header.js +++ b/test/unit/formats/header.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../../../utils/delta'; import Editor from '../../../core/editor'; describe('Header', function() { diff --git a/test/unit/formats/indent.js b/test/unit/formats/indent.js index c38a0d2c6d..3ae67f2751 100644 --- a/test/unit/formats/indent.js +++ b/test/unit/formats/indent.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('Indent', function() { it('+1', function() { diff --git a/test/unit/formats/link.js b/test/unit/formats/link.js index 4197d1b9a0..ca81250e30 100644 --- a/test/unit/formats/link.js +++ b/test/unit/formats/link.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Link from '../../../formats/link'; +import Delta from '../../../utils/delta'; describe('Link', function() { it('add', function() { diff --git a/test/unit/formats/list.js b/test/unit/formats/list.js index d0b9f24127..37e9dee814 100644 --- a/test/unit/formats/list.js +++ b/test/unit/formats/list.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; describe('List', function() { it('add', function() { diff --git a/test/unit/formats/table.js b/test/unit/formats/table.js index e3a299dbee..7f82e3453a 100644 --- a/test/unit/formats/table.js +++ b/test/unit/formats/table.js @@ -1,5 +1,5 @@ -import Delta from 'quill-delta'; import Editor from '../../../core/editor'; +import Delta from '../../../utils/delta'; const tableDelta = new Delta() .insert('A1') diff --git a/test/unit/modules/clipboard.js b/test/unit/modules/clipboard.js index 8a76748690..3a204cfb39 100644 --- a/test/unit/modules/clipboard.js +++ b/test/unit/modules/clipboard.js @@ -1,6 +1,6 @@ -import Delta from 'quill-delta'; -import { Range } from '../../../core/selection'; import Quill from '../../../core'; +import { Range } from '../../../core/selection'; +import Delta from '../../../utils/delta'; describe('Clipboard', function() { describe('events', function() { diff --git a/test/unit/modules/history.js b/test/unit/modules/history.js index a176f93234..36e10cbe7a 100644 --- a/test/unit/modules/history.js +++ b/test/unit/modules/history.js @@ -1,7 +1,7 @@ -import Delta from 'quill-delta'; import Quill from '../../../core'; import { globalRegistry } from '../../../core/quill'; import { getLastChangeIndex } from '../../../modules/history'; +import Delta from '../../../utils/delta'; describe('History', function() { describe('getLastChangeIndex', function() { diff --git a/test/unit/modules/syntax.js b/test/unit/modules/syntax.js index 828e955989..131d624df7 100644 --- a/test/unit/modules/syntax.js +++ b/test/unit/modules/syntax.js @@ -1,9 +1,9 @@ import hljs from 'highlight.js'; -import Delta from 'quill-delta'; import Quill from '../../../core/quill'; import BoldBlot from '../../../formats/bold'; import CodeBlock, { CodeBlockContainer } from '../../../formats/code'; import Syntax, { CodeBlock as SyntaxCodeBlock } from '../../../modules/syntax'; +import Delta from '../../../utils/delta'; const HIGHLIGHT_INTERVAL = 10; diff --git a/test/unit/modules/table.js b/test/unit/modules/table.js index bb9d8015c1..921a528633 100644 --- a/test/unit/modules/table.js +++ b/test/unit/modules/table.js @@ -1,4 +1,4 @@ -import Delta from 'quill-delta'; +import Delta from '../../../utils/delta'; import Quill from '../../../core/quill'; describe('Table Module', function() { diff --git a/utils/delta.js b/utils/delta.js new file mode 100644 index 0000000000..90b4dff3bf --- /dev/null +++ b/utils/delta.js @@ -0,0 +1,215 @@ +import Delta from 'quill-delta'; + +const parseCellIdentity = identity => { + const parts = identity.split(':'); + return [Number(parts[0]) - 1, Number(parts[1]) - 1]; +}; + +const stringifyCellIdentity = (row, column) => `${row + 1}:${column + 1}`; + +const composePosition = (delta, index) => { + let newIndex = index; + const thisIter = Delta.Op.iterator(delta.ops); + let offset = 0; + while (thisIter.hasNext() && offset <= index) { + const length = thisIter.peekLength(); + const nextType = thisIter.peekType(); + thisIter.next(); + switch (nextType) { + case 'delete': + if (length > index - 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; +}; + +Delta.registerHandler('table', { + compose(a, b, keepNull) { + // Step 2~3: Compose rows and columns separately + const rows = new Delta(a.rows || []).compose(new Delta(b.rows || [])); + const columns = new Delta(a.columns || []).compose( + new Delta(b.columns || []), + ); + + // Step 4: Reindex cell identities according to B's rows and columns + const cells = reindexCellIdentities(a.cells || {}, { + rows: new Delta(b.rows || []), + columns: new Delta(b.columns || []), + }); + + // Step 5: Compose cell content and attributes + 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 }); + }, +}); + +export default Delta; From ee1db9b2b5843b986e760172ba1cd90c72334a8a Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 14 Jun 2021 15:59:06 +0800 Subject: [PATCH 14/26] Register table handler in a module --- blots/block.js | 2 +- core/quill.js | 6 +- modules/clipboard.js | 7 +- modules/keyboard.js | 5 +- modules/syntax.js | 8 +- modules/table.js | 2 +- modules/toolbar.js | 4 +- modules/uploader.js | 2 +- package.json | 1 - test/unit/core/editor.js | 2 +- test/unit/core/quill.js | 6 +- test/unit/formats/align.js | 2 +- test/unit/formats/code.js | 2 +- test/unit/formats/color.js | 2 +- test/unit/formats/header.js | 2 +- test/unit/formats/indent.js | 2 +- test/unit/formats/link.js | 2 +- test/unit/formats/list.js | 2 +- test/unit/formats/table.js | 2 +- test/unit/modules/clipboard.js | 4 +- test/unit/modules/history.js | 2 +- test/unit/modules/syntax.js | 2 +- test/unit/modules/table.js | 2 +- utils/delta.js | 215 --------------------------------- 24 files changed, 35 insertions(+), 251 deletions(-) delete mode 100644 utils/delta.js diff --git a/blots/block.js b/blots/block.js index e0e5efd60a..ecdd4b0b05 100644 --- a/blots/block.js +++ b/blots/block.js @@ -1,3 +1,4 @@ +import Delta from 'quill-delta'; import { AttributorStore, BlockBlot, @@ -5,7 +6,6 @@ import { LeafBlot, Scope, } from 'parchment'; -import Delta from '../utils/delta'; import Break from './break'; import Inline from './inline'; import TextBlot from './text'; diff --git a/core/quill.js b/core/quill.js index d39b58bcbe..44bf15dd97 100644 --- a/core/quill.js +++ b/core/quill.js @@ -1,13 +1,13 @@ +import Delta from 'quill-delta'; import cloneDeep from 'lodash.clonedeep'; import merge from 'lodash.merge'; import * as Parchment from 'parchment'; -import Delta from '../utils/delta'; import Editor from './editor'; import Emitter from './emitter'; -import instances from './instances'; -import logger from './logger'; import Module from './module'; import Selection, { Range } from './selection'; +import instances from './instances'; +import logger from './logger'; import Theme from './theme'; const debug = logger('quill'); diff --git a/modules/clipboard.js b/modules/clipboard.js index f95b61c6d7..8eadd406a6 100644 --- a/modules/clipboard.js +++ b/modules/clipboard.js @@ -1,15 +1,17 @@ +import Delta from 'quill-delta'; import { Attributor, - BlockBlot, ClassAttributor, EmbedBlot, Scope, StyleAttributor, + BlockBlot, } from 'parchment'; import { BlockEmbed } from '../blots/block'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; + import { AlignAttribute, AlignStyle } from '../formats/align'; import { BackgroundStyle } from '../formats/background'; import CodeBlock from '../formats/code'; @@ -17,7 +19,6 @@ import { ColorStyle } from '../formats/color'; import { DirectionAttribute, DirectionStyle } from '../formats/direction'; import { FontStyle } from '../formats/font'; import { SizeStyle } from '../formats/size'; -import Delta from '../utils/delta'; import { deleteRange } from './keyboard'; const debug = logger('quill:clipboard'); diff --git a/modules/keyboard.js b/modules/keyboard.js index 7e000a7d34..76bacc8de2 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -1,11 +1,10 @@ import cloneDeep from 'lodash.clonedeep'; import isEqual from 'lodash.isequal'; +import Delta, { AttributeMap } from 'quill-delta'; import { EmbedBlot, Scope, TextBlot } from 'parchment'; -import { AttributeMap } from 'quill-delta'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; -import Delta from '../utils/delta'; const debug = logger('quill:keyboard'); diff --git a/modules/syntax.js b/modules/syntax.js index af25590287..70e135423a 100644 --- a/modules/syntax.js +++ b/modules/syntax.js @@ -1,13 +1,13 @@ +import Delta from 'quill-delta'; import { ClassAttributor, Scope } from 'parchment'; +import Inline from '../blots/inline'; +import Quill from '../core/quill'; +import Module from '../core/module'; import { blockDelta } from '../blots/block'; import BreakBlot from '../blots/break'; import CursorBlot from '../blots/cursor'; -import Inline from '../blots/inline'; import TextBlot, { escapeText } from '../blots/text'; -import Module from '../core/module'; -import Quill from '../core/quill'; import CodeBlock, { CodeBlockContainer } from '../formats/code'; -import Delta from '../utils/delta'; import { traverse } from './clipboard'; const TokenAttributor = new ClassAttributor('code-token', 'hljs', { diff --git a/modules/table.js b/modules/table.js index 5e21dab439..4fde5dc745 100644 --- a/modules/table.js +++ b/modules/table.js @@ -1,4 +1,4 @@ -import Delta from '../utils/delta'; +import Delta from 'quill-delta'; import Quill from '../core/quill'; import Module from '../core/module'; import { diff --git a/modules/toolbar.js b/modules/toolbar.js index a54603af1b..a16923ec0a 100644 --- a/modules/toolbar.js +++ b/modules/toolbar.js @@ -1,8 +1,8 @@ +import Delta from 'quill-delta'; import { EmbedBlot, Scope } from 'parchment'; +import Quill from '../core/quill'; import logger from '../core/logger'; import Module from '../core/module'; -import Quill from '../core/quill'; -import Delta from '../utils/delta'; const debug = logger('quill:toolbar'); diff --git a/modules/uploader.js b/modules/uploader.js index d1f05dc0bf..b3eabb19be 100644 --- a/modules/uploader.js +++ b/modules/uploader.js @@ -1,4 +1,4 @@ -import Delta from '../utils/delta'; +import Delta from 'quill-delta'; import Emitter from '../core/emitter'; import Module from '../core/module'; diff --git a/package.json b/package.json index 63d6a33d55..fd592e91cd 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "modules", "themes", "ui", - "utils", "dist/quill.bubble.css", "dist/quill.snow.css", "dist/quill.core.css", diff --git a/test/unit/core/editor.js b/test/unit/core/editor.js index 83c11d2d33..f4ce692f92 100644 --- a/test/unit/core/editor.js +++ b/test/unit/core/editor.js @@ -1,6 +1,6 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Selection, { Range } from '../../../core/selection'; -import Delta from '../../../utils/delta'; describe('Editor', function() { describe('insert', function() { diff --git a/test/unit/core/quill.js b/test/unit/core/quill.js index d83ad638e4..424a979f82 100644 --- a/test/unit/core/quill.js +++ b/test/unit/core/quill.js @@ -1,10 +1,10 @@ -import Emitter from '../../../core/emitter'; +import Delta from 'quill-delta'; import Quill, { expandConfig, overload } from '../../../core/quill'; -import { Range } from '../../../core/selection'; import Theme from '../../../core/theme'; +import Emitter from '../../../core/emitter'; import Toolbar from '../../../modules/toolbar'; import Snow from '../../../themes/snow'; -import Delta from '../../../utils/delta'; +import { Range } from '../../../core/selection'; describe('Quill', function() { it('imports', function() { diff --git a/test/unit/formats/align.js b/test/unit/formats/align.js index f73c5638fe..0571ea402d 100644 --- a/test/unit/formats/align.js +++ b/test/unit/formats/align.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Align', function() { it('add', function() { diff --git a/test/unit/formats/code.js b/test/unit/formats/code.js index e3c02780c8..2497bb7f83 100644 --- a/test/unit/formats/code.js +++ b/test/unit/formats/code.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Code', function() { it('format newline', function() { diff --git a/test/unit/formats/color.js b/test/unit/formats/color.js index 179131e1cf..9d50cd41fe 100644 --- a/test/unit/formats/color.js +++ b/test/unit/formats/color.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Color', function() { it('add', function() { diff --git a/test/unit/formats/header.js b/test/unit/formats/header.js index c1ff711dbb..43dbc8917e 100644 --- a/test/unit/formats/header.js +++ b/test/unit/formats/header.js @@ -1,4 +1,4 @@ -import Delta from '../../../utils/delta'; +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; describe('Header', function() { diff --git a/test/unit/formats/indent.js b/test/unit/formats/indent.js index 3ae67f2751..c38a0d2c6d 100644 --- a/test/unit/formats/indent.js +++ b/test/unit/formats/indent.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('Indent', function() { it('+1', function() { diff --git a/test/unit/formats/link.js b/test/unit/formats/link.js index ca81250e30..4197d1b9a0 100644 --- a/test/unit/formats/link.js +++ b/test/unit/formats/link.js @@ -1,6 +1,6 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; import Link from '../../../formats/link'; -import Delta from '../../../utils/delta'; describe('Link', function() { it('add', function() { diff --git a/test/unit/formats/list.js b/test/unit/formats/list.js index 37e9dee814..d0b9f24127 100644 --- a/test/unit/formats/list.js +++ b/test/unit/formats/list.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; describe('List', function() { it('add', function() { diff --git a/test/unit/formats/table.js b/test/unit/formats/table.js index 7f82e3453a..e3a299dbee 100644 --- a/test/unit/formats/table.js +++ b/test/unit/formats/table.js @@ -1,5 +1,5 @@ +import Delta from 'quill-delta'; import Editor from '../../../core/editor'; -import Delta from '../../../utils/delta'; const tableDelta = new Delta() .insert('A1') diff --git a/test/unit/modules/clipboard.js b/test/unit/modules/clipboard.js index 3a204cfb39..8a76748690 100644 --- a/test/unit/modules/clipboard.js +++ b/test/unit/modules/clipboard.js @@ -1,6 +1,6 @@ -import Quill from '../../../core'; +import Delta from 'quill-delta'; import { Range } from '../../../core/selection'; -import Delta from '../../../utils/delta'; +import Quill from '../../../core'; describe('Clipboard', function() { describe('events', function() { diff --git a/test/unit/modules/history.js b/test/unit/modules/history.js index 36e10cbe7a..a176f93234 100644 --- a/test/unit/modules/history.js +++ b/test/unit/modules/history.js @@ -1,7 +1,7 @@ +import Delta from 'quill-delta'; import Quill from '../../../core'; import { globalRegistry } from '../../../core/quill'; import { getLastChangeIndex } from '../../../modules/history'; -import Delta from '../../../utils/delta'; describe('History', function() { describe('getLastChangeIndex', function() { diff --git a/test/unit/modules/syntax.js b/test/unit/modules/syntax.js index 131d624df7..828e955989 100644 --- a/test/unit/modules/syntax.js +++ b/test/unit/modules/syntax.js @@ -1,9 +1,9 @@ import hljs from 'highlight.js'; +import Delta from 'quill-delta'; import Quill from '../../../core/quill'; import BoldBlot from '../../../formats/bold'; import CodeBlock, { CodeBlockContainer } from '../../../formats/code'; import Syntax, { CodeBlock as SyntaxCodeBlock } from '../../../modules/syntax'; -import Delta from '../../../utils/delta'; const HIGHLIGHT_INTERVAL = 10; diff --git a/test/unit/modules/table.js b/test/unit/modules/table.js index 921a528633..bb9d8015c1 100644 --- a/test/unit/modules/table.js +++ b/test/unit/modules/table.js @@ -1,4 +1,4 @@ -import Delta from '../../../utils/delta'; +import Delta from 'quill-delta'; import Quill from '../../../core/quill'; describe('Table Module', function() { diff --git a/utils/delta.js b/utils/delta.js deleted file mode 100644 index 90b4dff3bf..0000000000 --- a/utils/delta.js +++ /dev/null @@ -1,215 +0,0 @@ -import Delta from 'quill-delta'; - -const parseCellIdentity = identity => { - const parts = identity.split(':'); - return [Number(parts[0]) - 1, Number(parts[1]) - 1]; -}; - -const stringifyCellIdentity = (row, column) => `${row + 1}:${column + 1}`; - -const composePosition = (delta, index) => { - let newIndex = index; - const thisIter = Delta.Op.iterator(delta.ops); - let offset = 0; - while (thisIter.hasNext() && offset <= index) { - const length = thisIter.peekLength(); - const nextType = thisIter.peekType(); - thisIter.next(); - switch (nextType) { - case 'delete': - if (length > index - 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; -}; - -Delta.registerHandler('table', { - compose(a, b, keepNull) { - // Step 2~3: Compose rows and columns separately - const rows = new Delta(a.rows || []).compose(new Delta(b.rows || [])); - const columns = new Delta(a.columns || []).compose( - new Delta(b.columns || []), - ); - - // Step 4: Reindex cell identities according to B's rows and columns - const cells = reindexCellIdentities(a.cells || {}, { - rows: new Delta(b.rows || []), - columns: new Delta(b.columns || []), - }); - - // Step 5: Compose cell content and attributes - 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 }); - }, -}); - -export default Delta; From 37e02ae9e90a161d9b2e4e9725fcd8f0306420ca Mon Sep 17 00:00:00 2001 From: luin Date: Thu, 22 Jul 2021 19:17:58 +0800 Subject: [PATCH 15/26] Ignore self-manage blots --- blots/scroll.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blots/scroll.js b/blots/scroll.js index 4d6ff5cb43..cbd7fe4d74 100644 --- a/blots/scroll.js +++ b/blots/scroll.js @@ -166,7 +166,7 @@ class Scroll extends ScrollBlot { } mutations = mutations.filter(({ target }) => { const blot = this.find(target, true); - return blot && blot.scroll === this; + return blot && blot.scroll === this && !blot.updateContent; }); if (mutations.length > 0) { this.emitter.emit(Emitter.events.SCROLL_BEFORE_UPDATE, source, mutations); From 808345825f94efa965737adcc9f586c63b35c384 Mon Sep 17 00:00:00 2001 From: luin Date: Thu, 22 Jul 2021 23:03:40 +0800 Subject: [PATCH 16/26] Add scroll embed change event --- core/emitter.js | 1 + core/quill.js | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/core/emitter.js b/core/emitter.js index 0f9ba4f06a..13f2cc21f3 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_CHANGE: 'scroll-embed-change', SELECTION_CHANGE: 'selection-change', TEXT_CHANGE: 'text-change', }; diff --git a/core/quill.js b/core/quill.js index 44bf15dd97..e1fda7835b 100644 --- a/core/quill.js +++ b/core/quill.js @@ -109,6 +109,16 @@ class Quill { source, ); }); + this.emitter.on(Emitter.events.SCROLL_EMBED_CHANGE, (blot, delta) => { + modify.call( + this, + () => + new Delta() + .retain(blot.offset(this)) + .retain({ [blot.statics.blotName]: delta }), + Quill.sources.USER, + ); + }); const contents = this.clipboard.convert({ html: `${html}


`, text: '\n', From 8664379e012eb3b530c74dfd58d2d7d7f60fb21c Mon Sep 17 00:00:00 2001 From: luin Date: Fri, 23 Jul 2021 00:17:29 +0800 Subject: [PATCH 17/26] Upgrade delta to fix invert --- core/quill.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/quill.js b/core/quill.js index e1fda7835b..4cec874409 100644 --- a/core/quill.js +++ b/core/quill.js @@ -110,12 +110,18 @@ class Quill { ); }); this.emitter.on(Emitter.events.SCROLL_EMBED_CHANGE, (blot, delta) => { + const oldRange = this.selection.lastRange; + const [newRange] = this.selection.getRange(); + const selectionInfo = + oldRange && newRange ? { oldRange, newRange } : undefined; modify.call( this, - () => - new Delta() + () => { + const change = new Delta() .retain(blot.offset(this)) - .retain({ [blot.statics.blotName]: delta }), + .retain({ [blot.statics.blotName]: delta }); + return this.editor.update(change, [], selectionInfo); + }, Quill.sources.USER, ); }); From 7104087869d533218f27ccb488401240004c4b06 Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 26 Jul 2021 16:10:18 +0800 Subject: [PATCH 18/26] Allow different Delta constructors --- core/quill.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/quill.js b/core/quill.js index 4cec874409..f42b3c2e34 100644 --- a/core/quill.js +++ b/core/quill.js @@ -622,7 +622,7 @@ function shiftRange(range, index, length, source) { if (range == null) return null; let start; let end; - if (index instanceof Delta) { + if (index.transformPosition) { [start, end] = [range.index, range.index + range.length].map(pos => index.transformPosition(pos, source !== Emitter.sources.USER), ); From c7e3c37f3d34a256b9448959bbaf0535b882e513 Mon Sep 17 00:00:00 2001 From: luin Date: Wed, 18 Aug 2021 10:34:15 +0800 Subject: [PATCH 19/26] Rename event names --- core/emitter.js | 2 +- core/quill.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/emitter.js b/core/emitter.js index 13f2cc21f3..fa4495c763 100644 --- a/core/emitter.js +++ b/core/emitter.js @@ -51,7 +51,7 @@ Emitter.events = { SCROLL_BLOT_UNMOUNT: 'scroll-blot-unmount', SCROLL_OPTIMIZE: 'scroll-optimize', SCROLL_UPDATE: 'scroll-update', - SCROLL_EMBED_CHANGE: 'scroll-embed-change', + 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 f42b3c2e34..31b8028f4d 100644 --- a/core/quill.js +++ b/core/quill.js @@ -109,7 +109,7 @@ class Quill { source, ); }); - this.emitter.on(Emitter.events.SCROLL_EMBED_CHANGE, (blot, delta) => { + this.emitter.on(Emitter.events.SCROLL_EMBED_UPDATE, (blot, delta) => { const oldRange = this.selection.lastRange; const [newRange] = this.selection.getRange(); const selectionInfo = From b65ccee8652438d7f4d742e1c160a8a686f7fcbf Mon Sep 17 00:00:00 2001 From: luin Date: Wed, 18 Aug 2021 10:46:11 +0800 Subject: [PATCH 20/26] Support updating embed content --- blots/scroll.js | 13 +++++++++++++ core/editor.js | 6 ++++++ core/quill.js | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/blots/scroll.js b/blots/scroll.js index cbd7fe4d74..66a0da8a8d 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); @@ -176,6 +180,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/quill.js b/core/quill.js index 31b8028f4d..5f721ebaa1 100644 --- a/core/quill.js +++ b/core/quill.js @@ -622,7 +622,7 @@ function shiftRange(range, index, length, source) { if (range == null) return null; let start; let end; - if (index.transformPosition) { + if (index && typeof index.transformPosition === 'function') { [start, end] = [range.index, range.index + range.length].map(pos => index.transformPosition(pos, source !== Emitter.sources.USER), ); From b7b3131c1e791d69f1a4d93285b7d1016998f0df Mon Sep 17 00:00:00 2001 From: luin Date: Tue, 5 Oct 2021 17:12:36 +0800 Subject: [PATCH 21/26] Expose composePosition --- modules/tableEmbed.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tableEmbed.js b/modules/tableEmbed.js index 984fdc72af..7f3c3f47b5 100644 --- a/modules/tableEmbed.js +++ b/modules/tableEmbed.js @@ -8,7 +8,7 @@ const parseCellIdentity = identity => { const stringifyCellIdentity = (row, column) => `${row + 1}:${column + 1}`; -const composePosition = (delta, index) => { +export const composePosition = (delta, index) => { let newIndex = index; const thisIter = Delta.Op.iterator(delta.ops); let offset = 0; From 0c4122c4415360c6d9205a62274f4a2df59125e2 Mon Sep 17 00:00:00 2001 From: luin Date: Thu, 21 Oct 2021 15:59:28 +0800 Subject: [PATCH 22/26] Fix getRange not work with nested Quill --- core/selection.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/selection.js b/core/selection.js index 8c3624dbe9..d6f8245fe1 100644 --- a/core/selection.js +++ b/core/selection.js @@ -13,6 +13,19 @@ class Range { } } +function getBlotInScroll(node, scroll) { + let current = node; + while (current) { + const blot = scroll.find(current, true); + const currentScroll = blot && blot.scroll; + if (currentScroll === scroll) { + return blot; + } + current = currentScroll && currentScroll.domNode.parentElement; + } + return null; +} + class Selection { constructor(scroll, emitter) { this.emitter = emitter; @@ -206,7 +219,7 @@ class Selection { } const indexes = positions.map(position => { const [node, offset] = position; - const blot = this.scroll.find(node, true); + const blot = getBlotInScroll(node, this.scroll); const index = blot.offset(this.scroll); if (offset === 0) { return index; From d78cc968d29096c7f661237227ab22a65875b22f Mon Sep 17 00:00:00 2001 From: luin Date: Thu, 21 Oct 2021 19:01:34 +0800 Subject: [PATCH 23/26] Ignore keyboard events triggered from nested quill instances --- modules/keyboard.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/keyboard.js b/modules/keyboard.js index 76bacc8de2..c60574a675 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -111,6 +111,8 @@ class Keyboard extends Module { listen() { this.quill.root.addEventListener('keydown', evt => { if (evt.defaultPrevented || evt.isComposing) return; + const blot = this.quill.scroll.find(evt.target, true); + if (blot && blot.scroll !== this.quill.scroll) return; const bindings = (this.bindings[evt.key] || []).concat( this.bindings[evt.which] || [], ); From 89e4d6838e559fa5fa180fafc539bc0f26dbeb85 Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 8 Nov 2021 21:19:30 +0800 Subject: [PATCH 24/26] Quill.find should accept bubble parameter https://quilljs.com/docs/api/#find-experimental --- core/quill.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/quill.js b/core/quill.js index 5f721ebaa1..7799cde265 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) { From 5268696e35836dc631b975264c0fb7047ec35ce3 Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 8 Nov 2021 21:27:47 +0800 Subject: [PATCH 25/26] Upgrade Parchment to make sure Scroll#find() not return nested scroll blots --- blots/scroll.js | 2 +- core/selection.js | 15 +-------------- modules/keyboard.js | 4 ++-- package-lock.json | 6 +++--- package.json | 2 +- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/blots/scroll.js b/blots/scroll.js index 66a0da8a8d..5a68225da9 100644 --- a/blots/scroll.js +++ b/blots/scroll.js @@ -170,7 +170,7 @@ class Scroll extends ScrollBlot { } mutations = mutations.filter(({ target }) => { const blot = this.find(target, true); - return blot && blot.scroll === this && !blot.updateContent; + return blot && !blot.updateContent; }); if (mutations.length > 0) { this.emitter.emit(Emitter.events.SCROLL_BEFORE_UPDATE, source, mutations); diff --git a/core/selection.js b/core/selection.js index d6f8245fe1..8c3624dbe9 100644 --- a/core/selection.js +++ b/core/selection.js @@ -13,19 +13,6 @@ class Range { } } -function getBlotInScroll(node, scroll) { - let current = node; - while (current) { - const blot = scroll.find(current, true); - const currentScroll = blot && blot.scroll; - if (currentScroll === scroll) { - return blot; - } - current = currentScroll && currentScroll.domNode.parentElement; - } - return null; -} - class Selection { constructor(scroll, emitter) { this.emitter = emitter; @@ -219,7 +206,7 @@ class Selection { } const indexes = positions.map(position => { const [node, offset] = position; - const blot = getBlotInScroll(node, this.scroll); + const blot = this.scroll.find(node, true); const index = blot.offset(this.scroll); if (offset === 0) { return index; diff --git a/modules/keyboard.js b/modules/keyboard.js index c60574a675..2ba8a84c05 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -111,13 +111,13 @@ class Keyboard extends Module { listen() { this.quill.root.addEventListener('keydown', evt => { if (evt.defaultPrevented || evt.isComposing) return; - const blot = this.quill.scroll.find(evt.target, true); - if (blot && blot.scroll !== this.quill.scroll) return; const bindings = (this.bindings[evt.key] || []).concat( this.bindings[evt.which] || [], ); 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/package-lock.json b/package-lock.json index a03dbc08c2..5602d66489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9039,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-dev.3", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-2.0.0-dev.3.tgz", + "integrity": "sha512-+Ueh17KHT/CEZYm8WxNSGQ7veOfaKAAxtn1pNF7dR4ZM3CGavTXpNFQWGZaOb+UdWCV9JFIBACuPmEiDcxARbA==" }, "parent-module": { "version": "1.0.1", diff --git a/package.json b/package.json index fd592e91cd..fb7c03a68e 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.5.0", "lodash.merge": "^4.5.0", - "parchment": "2.0.0-dev.2", + "parchment": "2.0.0-dev.3", "quill-delta": "github:quilljs/delta#87cd1e6de795eb29abe79a29429ca3b126dc9031" }, "devDependencies": { From d00df81e52eb53d14516f8c5d533b534ff619d48 Mon Sep 17 00:00:00 2001 From: Zihua Li Date: Tue, 31 May 2022 22:09:48 +0800 Subject: [PATCH 26/26] Upgrade Parchment to 2.0 --- _develop/webpack.config.js | 1 - package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) 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/package-lock.json b/package-lock.json index 5602d66489..f124c6f76b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9039,9 +9039,9 @@ } }, "parchment": { - "version": "2.0.0-dev.3", - "resolved": "https://registry.npmjs.org/parchment/-/parchment-2.0.0-dev.3.tgz", - "integrity": "sha512-+Ueh17KHT/CEZYm8WxNSGQ7veOfaKAAxtn1pNF7dR4ZM3CGavTXpNFQWGZaOb+UdWCV9JFIBACuPmEiDcxARbA==" + "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", diff --git a/package.json b/package.json index fb7c03a68e..442460a88a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.5.0", "lodash.merge": "^4.5.0", - "parchment": "2.0.0-dev.3", + "parchment": "^2.0.0", "quill-delta": "github:quilljs/delta#87cd1e6de795eb29abe79a29429ca3b126dc9031" }, "devDependencies": {