diff --git a/blots/scroll.js b/blots/scroll.js index 824a42299e..c32291fe4b 100644 --- a/blots/scroll.js +++ b/blots/scroll.js @@ -3,6 +3,7 @@ import Emitter from '../core/emitter'; import Block, { BlockEmbed } from './block'; import Break from './break'; import Container from './container'; +import { CellLine } from '../formats/table'; function isLine(blot) { return blot instanceof Block || blot instanceof BlockEmbed; @@ -44,13 +45,15 @@ class Scroll extends ScrollBlot { const [last] = this.line(index + length); super.deleteAt(index, length); if (last != null && first !== last && offset > 0) { - if (first instanceof BlockEmbed || last instanceof BlockEmbed) { - this.optimize(); - return; + const isCrossCellDelete = (first instanceof CellLine || last instanceof CellLine) + && first.parent !== last.parent; + const includesEmbedBlock = first instanceof BlockEmbed || last instanceof BlockEmbed; + + if (!includesEmbedBlock && !isCrossCellDelete) { + const ref = last.children.head instanceof Break ? null : last.children.head; + first.moveChildren(last, ref); + first.remove(); } - const ref = last.children.head instanceof Break ? null : last.children.head; - first.moveChildren(last, ref); - first.remove(); } this.optimize(); } diff --git a/formats/table/index.js b/formats/table/index.js index 900c65cefd..1c72018885 100644 --- a/formats/table/index.js +++ b/formats/table/index.js @@ -11,12 +11,18 @@ const CELL_IDENTITY_KEYS = ['row', 'cell']; const TABLE_TAGS = ['TD', 'TH', 'TR', 'TBODY', 'THEAD', 'TABLE']; const DATA_PREFIX = 'data-table-'; +function deleteChildrenAt(children, index, length) { + children.forEachAt(index, length, (child, offset, childLength) => { + child.deleteAt(offset, childLength); + }); +} + class CellLine extends Block { static create(value) { const node = super.create(value); CELL_IDENTITY_KEYS.forEach((key) => { const identityMarker = key === 'row' ? tableId : cellId; - node.setAttribute(`${DATA_PREFIX}${key}`, value[key] ?? identityMarker()); + node.setAttribute(`${DATA_PREFIX}${key}`, value?.[key] ?? identityMarker()); }); return node; @@ -183,6 +189,10 @@ class BaseCell extends Container { } super.optimize(...args); } + + deleteAt(index, length) { + deleteChildrenAt(this.children, index, length); + } } BaseCell.tagName = ['TD', 'TH']; @@ -210,6 +220,7 @@ class TableCell extends BaseCell { TableCell.blotName = 'tableCell'; TableCell.className = 'ql-table-data-cell'; TableCell.dataAttribute = `${DATA_PREFIX}row`; +TableCell.defaultChild = CellLine; class TableHeaderCell extends BaseCell { static create(value) { @@ -236,6 +247,7 @@ TableHeaderCell.tagName = ['TH', 'TD']; TableHeaderCell.className = 'ql-table-header-cell'; TableHeaderCell.blotName = 'tableHeaderCell'; TableHeaderCell.dataAttribute = `${DATA_PREFIX}header-row`; +TableHeaderCell.defaultChild = HeaderCellLine; class BaseRow extends Container { checkMerge() { @@ -316,6 +328,10 @@ class TableRow extends BaseRow { this.childFormatName = 'table'; } + + deleteAt(index, length) { + deleteChildrenAt(this.children, index, length); + } } TableRow.blotName = 'tableRow'; diff --git a/modules/keyboard.js b/modules/keyboard.js index a8e14d86d1..0bbba9404c 100644 --- a/modules/keyboard.js +++ b/modules/keyboard.js @@ -309,7 +309,16 @@ class Keyboard extends Module { const [prev] = this.quill.getLine(range.index - 1); if (prev) { const isPrevLineEmpty = prev.statics.blotName === 'block' && prev.length() <= 1; - if (!isPrevLineEmpty) { + const isPrevLineTable = prev.statics.blotName.startsWith('table'); + const isLineEmpty = line.statics.blotName === 'block' && line.length() <= 1; + + if (isPrevLineTable) { + if (isLineEmpty) { + line.remove(); + } + this.quill.setSelection(range.index - 1); + } + if (!isPrevLineEmpty && !isPrevLineTable) { const curFormats = line.formats(); const prevFormats = this.quill.getFormat(range.index - 1, 1); formats = AttributeMap.diff(curFormats, prevFormats) || {}; diff --git a/test/functional/epic.js b/test/functional/epic.js index c06e5e5b0c..e94dae1074 100644 --- a/test/functional/epic.js +++ b/test/functional/epic.js @@ -13,6 +13,10 @@ const P1 = 'Call me Ishmael. Some years ago—never mind how long precisely-havi const P2 = 'There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs—commerce surrounds it with her surf. Right and left, the streets take you waterward. Its extreme downtown is the battery, where that noble mole is washed by waves, and cooled by breezes, which a few hours previous were out of sight of land. Look at the crowds of water-gazers there.'; const HOST = 'http://127.0.0.1:8080'; +function sanitizeTableHtml(html) { + return html.replace(/(<\w+)((\s+class\s*=\s*"[^"]*")|(\s+data-[\w-]+\s*=\s*"[^"]*"))*(\s*>)/gi, '$1$5'); +} + describe('quill', function () { it('compose an epic', async function () { const browser = await puppeteer.launch({ @@ -249,6 +253,210 @@ describe('quill', function () { }); }); +describe('table header: ', function () { + it('cell should not be removed on typing if it is selected', async function () { + const browser = await puppeteer.launch({ + headless: false, + }); + const page = await browser.newPage(); + + await page.goto(`${HOST}/table_header.html`); + await page.waitForSelector('.ql-editor', { timeout: 10000 }); + + await page.click('[data-table-cell="3"]'); + + await page.keyboard.down('Shift'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.press('ArrowLeft'); + await page.keyboard.up('Shift'); + + await page.keyboard.press('c'); + + const html = await page.$eval('.ql-editor', (e) => e.innerHTML); + const sanitizeHtml = sanitizeTableHtml(html); + expect(sanitizeHtml).toEqual( + ` +
1 |
+ 2c |
+
---|
1 |
+ 2c |
+
1 |
+ 2 |
+ 3 |
+
1 |
+ 2 |
+ 3 |
+
1 |
+ 2 |
+ 3w |
+
g
+ `.replace(/\s/g, ''), + ); + }); + + it('backspace press on the position after table should remove empty line and move caret to a cell if next line is empty', async function () { + const browser = await puppeteer.launch({ + headless: false, + }); + const page = await browser.newPage(); + + await page.goto(`${HOST}/table.html`); + await page.waitForSelector('.ql-editor', { timeout: 10000 }); + + await page.click('[data-table-cell="3"]'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('w'); + + const html = await page.$eval('.ql-editor', (e) => e.innerHTML); + const sanitizeHtml = sanitizeTableHtml(html); + expect(sanitizeHtml).toEqual( + ` +1 |
+ 2 |
+ 3w |
+
1 | +2 | +3 | +
1 | +2 | +3 | +
---|
b1 |
+ b2 |
+
a1 a2 |
+
a1 |
+
b |
+
h1 |
+ h2 |
+ h3 |
+
---|
h1 |
+
---|
a1 |
+ a2 |
+ |
h1 |
+
h1 |
+
h1 |
+
---|
h1 |
+
---|
a1 |
+ a2 |
+ a3 |
+
b1 |
+ b2_INPUT_ |
+