Skip to content

Commit 53e8e09

Browse files
authored
Table cell and row should not be removed on backspace press or typing even if it is selected (T1062588) (#89)
1 parent d0c059d commit 53e8e09

File tree

7 files changed

+549
-8
lines changed

7 files changed

+549
-8
lines changed

blots/scroll.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Emitter from '../core/emitter';
33
import Block, { BlockEmbed } from './block';
44
import Break from './break';
55
import Container from './container';
6+
import { CellLine } from '../formats/table';
67

78
function isLine(blot) {
89
return blot instanceof Block || blot instanceof BlockEmbed;
@@ -44,13 +45,15 @@ class Scroll extends ScrollBlot {
4445
const [last] = this.line(index + length);
4546
super.deleteAt(index, length);
4647
if (last != null && first !== last && offset > 0) {
47-
if (first instanceof BlockEmbed || last instanceof BlockEmbed) {
48-
this.optimize();
49-
return;
48+
const isCrossCellDelete = (first instanceof CellLine || last instanceof CellLine)
49+
&& first.parent !== last.parent;
50+
const includesEmbedBlock = first instanceof BlockEmbed || last instanceof BlockEmbed;
51+
52+
if (!includesEmbedBlock && !isCrossCellDelete) {
53+
const ref = last.children.head instanceof Break ? null : last.children.head;
54+
first.moveChildren(last, ref);
55+
first.remove();
5056
}
51-
const ref = last.children.head instanceof Break ? null : last.children.head;
52-
first.moveChildren(last, ref);
53-
first.remove();
5457
}
5558
this.optimize();
5659
}

formats/table/index.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ const CELL_IDENTITY_KEYS = ['row', 'cell'];
1111
const TABLE_TAGS = ['TD', 'TH', 'TR', 'TBODY', 'THEAD', 'TABLE'];
1212
const DATA_PREFIX = 'data-table-';
1313

14+
function deleteChildrenAt(children, index, length) {
15+
children.forEachAt(index, length, (child, offset, childLength) => {
16+
child.deleteAt(offset, childLength);
17+
});
18+
}
19+
1420
class CellLine extends Block {
1521
static create(value) {
1622
const node = super.create(value);
1723
CELL_IDENTITY_KEYS.forEach((key) => {
1824
const identityMarker = key === 'row' ? tableId : cellId;
19-
node.setAttribute(`${DATA_PREFIX}${key}`, value[key] ?? identityMarker());
25+
node.setAttribute(`${DATA_PREFIX}${key}`, value?.[key] ?? identityMarker());
2026
});
2127

2228
return node;
@@ -183,6 +189,10 @@ class BaseCell extends Container {
183189
}
184190
super.optimize(...args);
185191
}
192+
193+
deleteAt(index, length) {
194+
deleteChildrenAt(this.children, index, length);
195+
}
186196
}
187197
BaseCell.tagName = ['TD', 'TH'];
188198

@@ -210,6 +220,7 @@ class TableCell extends BaseCell {
210220
TableCell.blotName = 'tableCell';
211221
TableCell.className = 'ql-table-data-cell';
212222
TableCell.dataAttribute = `${DATA_PREFIX}row`;
223+
TableCell.defaultChild = CellLine;
213224

214225
class TableHeaderCell extends BaseCell {
215226
static create(value) {
@@ -236,6 +247,7 @@ TableHeaderCell.tagName = ['TH', 'TD'];
236247
TableHeaderCell.className = 'ql-table-header-cell';
237248
TableHeaderCell.blotName = 'tableHeaderCell';
238249
TableHeaderCell.dataAttribute = `${DATA_PREFIX}header-row`;
250+
TableHeaderCell.defaultChild = HeaderCellLine;
239251

240252
class BaseRow extends Container {
241253
checkMerge() {
@@ -316,6 +328,10 @@ class TableRow extends BaseRow {
316328

317329
this.childFormatName = 'table';
318330
}
331+
332+
deleteAt(index, length) {
333+
deleteChildrenAt(this.children, index, length);
334+
}
319335
}
320336
TableRow.blotName = 'tableRow';
321337

modules/keyboard.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,16 @@ class Keyboard extends Module {
309309
const [prev] = this.quill.getLine(range.index - 1);
310310
if (prev) {
311311
const isPrevLineEmpty = prev.statics.blotName === 'block' && prev.length() <= 1;
312-
if (!isPrevLineEmpty) {
312+
const isPrevLineTable = prev.statics.blotName.startsWith('table');
313+
const isLineEmpty = line.statics.blotName === 'block' && line.length() <= 1;
314+
315+
if (isPrevLineTable) {
316+
if (isLineEmpty) {
317+
line.remove();
318+
}
319+
this.quill.setSelection(range.index - 1);
320+
}
321+
if (!isPrevLineEmpty && !isPrevLineTable) {
313322
const curFormats = line.formats();
314323
const prevFormats = this.quill.getFormat(range.index - 1, 1);
315324
formats = AttributeMap.diff(curFormats, prevFormats) || {};

test/functional/epic.js

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ const P1 = 'Call me Ishmael. Some years ago—never mind how long precisely-havi
1313
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.';
1414
const HOST = 'http://127.0.0.1:8080';
1515

16+
function sanitizeTableHtml(html) {
17+
return html.replace(/(<\w+)((\s+class\s*=\s*"[^"]*")|(\s+data-[\w-]+\s*=\s*"[^"]*"))*(\s*>)/gi, '$1$5');
18+
}
19+
1620
describe('quill', function () {
1721
it('compose an epic', async function () {
1822
const browser = await puppeteer.launch({
@@ -249,6 +253,210 @@ describe('quill', function () {
249253
});
250254
});
251255

256+
describe('table header: ', function () {
257+
it('cell should not be removed on typing if it is selected', async function () {
258+
const browser = await puppeteer.launch({
259+
headless: false,
260+
});
261+
const page = await browser.newPage();
262+
263+
await page.goto(`${HOST}/table_header.html`);
264+
await page.waitForSelector('.ql-editor', { timeout: 10000 });
265+
266+
await page.click('[data-table-cell="3"]');
267+
268+
await page.keyboard.down('Shift');
269+
await page.keyboard.press('ArrowLeft');
270+
await page.keyboard.press('ArrowLeft');
271+
await page.keyboard.up('Shift');
272+
273+
await page.keyboard.press('c');
274+
275+
const html = await page.$eval('.ql-editor', (e) => e.innerHTML);
276+
const sanitizeHtml = sanitizeTableHtml(html);
277+
expect(sanitizeHtml).toEqual(
278+
`
279+
<table>
280+
<thead>
281+
<tr>
282+
<th><p>1</p></th>
283+
<th><p>2c</p></th>
284+
<th><p><br></p></th>
285+
</tr>
286+
</thead>
287+
</table>
288+
<p><br></p>
289+
`.replace(/\s/g, ''),
290+
);
291+
});
292+
});
293+
294+
describe('table:', function () {
295+
it('cell should not be removed on typing if it is selected', async function () {
296+
const browser = await puppeteer.launch({
297+
headless: false,
298+
});
299+
const page = await browser.newPage();
300+
301+
await page.goto(`${HOST}/table.html`);
302+
await page.waitForSelector('.ql-editor', { timeout: 10000 });
303+
304+
await page.click('[data-table-cell="3"]');
305+
306+
await page.keyboard.down('Shift');
307+
await page.keyboard.press('ArrowLeft');
308+
await page.keyboard.press('ArrowLeft');
309+
await page.keyboard.up('Shift');
310+
311+
await page.keyboard.press('c');
312+
313+
const html = await page.$eval('.ql-editor', (e) => e.innerHTML);
314+
const sanitizeHtml = sanitizeTableHtml(html);
315+
expect(sanitizeHtml).toEqual(
316+
`
317+
<table>
318+
<tbody>
319+
<tr>
320+
<td><p>1</p></td>
321+
<td><p>2c</p></td>
322+
<td><p><br></p></td>
323+
</tr>
324+
</tbody>
325+
</table>
326+
<p><br></p>
327+
`.replace(/\s/g, ''),
328+
);
329+
});
330+
331+
it('backspace press on the position after table should remove an empty line and not add it to the cell', async function () {
332+
const browser = await puppeteer.launch({
333+
headless: false,
334+
});
335+
const page = await browser.newPage();
336+
337+
await page.goto(`${HOST}/table.html`);
338+
await page.waitForSelector('.ql-editor', { timeout: 10000 });
339+
340+
await page.click('[data-table-cell="3"]');
341+
await page.keyboard.press('ArrowRight');
342+
await page.keyboard.press('Backspace');
343+
344+
const html = await page.$eval('.ql-editor', (e) => e.innerHTML);
345+
const sanitizeHtml = sanitizeTableHtml(html);
346+
expect(sanitizeHtml).toEqual(
347+
`
348+
<table>
349+
<tbody>
350+
<tr>
351+
<td><p>1</p></td>
352+
<td><p>2</p></td>
353+
<td><p>3</p></td>
354+
</tr>
355+
</tbody>
356+
</table>
357+
`.replace(/\s/g, ''),
358+
);
359+
});
360+
361+
it('backspace in multiline cell should work as usual', async function () {
362+
const browser = await puppeteer.launch({
363+
headless: false,
364+
});
365+
const page = await browser.newPage();
366+
367+
await page.goto(`${HOST}/table.html`);
368+
await page.waitForSelector('.ql-editor', { timeout: 10000 });
369+
370+
await page.click('[data-table-cell="3"]');
371+
await page.keyboard.press('4');
372+
await page.keyboard.press('Enter');
373+
await page.keyboard.press('Backspace');
374+
await page.keyboard.press('Backspace');
375+
376+
const html = await page.$eval('.ql-editor', (e) => e.innerHTML);
377+
const sanitizeHtml = sanitizeTableHtml(html);
378+
expect(sanitizeHtml).toEqual(
379+
`
380+
<table>
381+
<tbody>
382+
<tr>
383+
<td><p>1</p></td>
384+
<td><p>2</p></td>
385+
<td><p>3</p></td>
386+
</tr>
387+
</tbody>
388+
</table>
389+
<p><br></p>
390+
`.replace(/\s/g, ''),
391+
);
392+
});
393+
394+
it('backspace press on the position after table should only move a caret to cell if next line is not empty', async function () {
395+
const browser = await puppeteer.launch({
396+
headless: false,
397+
});
398+
const page = await browser.newPage();
399+
400+
await page.goto(`${HOST}/table.html`);
401+
await page.waitForSelector('.ql-editor', { timeout: 10000 });
402+
403+
await page.click('[data-table-cell="3"]');
404+
await page.keyboard.press('ArrowRight');
405+
await page.keyboard.press('g');
406+
await page.keyboard.press('ArrowLeft');
407+
await page.keyboard.press('Backspace');
408+
await page.keyboard.press('w');
409+
410+
const html = await page.$eval('.ql-editor', (e) => e.innerHTML);
411+
const sanitizeHtml = sanitizeTableHtml(html);
412+
expect(sanitizeHtml).toEqual(
413+
`
414+
<table>
415+
<tbody>
416+
<tr>
417+
<td><p>1</p></td>
418+
<td><p>2</p></td>
419+
<td><p>3w</p></td>
420+
</tr>
421+
</tbody>
422+
</table>
423+
<p>g</p>
424+
`.replace(/\s/g, ''),
425+
);
426+
});
427+
428+
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 () {
429+
const browser = await puppeteer.launch({
430+
headless: false,
431+
});
432+
const page = await browser.newPage();
433+
434+
await page.goto(`${HOST}/table.html`);
435+
await page.waitForSelector('.ql-editor', { timeout: 10000 });
436+
437+
await page.click('[data-table-cell="3"]');
438+
await page.keyboard.press('ArrowRight');
439+
await page.keyboard.press('Backspace');
440+
await page.keyboard.press('w');
441+
442+
const html = await page.$eval('.ql-editor', (e) => e.innerHTML);
443+
const sanitizeHtml = sanitizeTableHtml(html);
444+
expect(sanitizeHtml).toEqual(
445+
`
446+
<table>
447+
<tbody>
448+
<tr>
449+
<td><p>1</p></td>
450+
<td><p>2</p></td>
451+
<td><p>3w</p></td>
452+
</tr>
453+
</tbody>
454+
</table>
455+
`.replace(/\s/g, ''),
456+
);
457+
});
458+
});
459+
252460
// Copy/paste emulation des not working on Mac. See https://github.com/puppeteer/puppeteer/issues/1313
253461
if (!isMac) {
254462
describe('List copy/pasting into table', function () {

test/functional/example/table.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>DevExtreme-Quill Base Editing</title>
8+
<link rel="stylesheet" type="text/css" href="src/dx-quill.core.css"/>
9+
<script type="text/javascript" src="src/dx-quill.js"></script>
10+
</head>
11+
12+
<body>
13+
<div>
14+
<div id="editor">
15+
<table>
16+
<tbody>
17+
<tr>
18+
<td>1</td>
19+
<td>2</td>
20+
<td>3</td>
21+
</tr>
22+
</tbody>
23+
</table>
24+
<p><br></p>
25+
</div>
26+
</div>
27+
</body>
28+
29+
<script>
30+
const editorElem = document.getElementById('editor');
31+
const editor = new DevExpress.Quill(editorElem, {
32+
modules: {
33+
table: true
34+
}
35+
});
36+
</script>
37+
</html>

0 commit comments

Comments
 (0)