Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Table cell and row should not be removed on backspace press or typing even if it is selected (T1062588) #89

Merged
merged 13 commits into from
Apr 19, 2023
9 changes: 8 additions & 1 deletion blots/scroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,7 +45,13 @@ 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) {
const isCrossCellDelete = first instanceof CellLine
&& last instanceof CellLine
&& first.parent !== last.parent;
if (
first instanceof BlockEmbed || last instanceof BlockEmbed
|| isCrossCellDelete
) {
this.optimize();
return;
}
Expand Down
16 changes: 15 additions & 1 deletion formats/table/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class CellLine extends Block {
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;
Expand Down Expand Up @@ -183,6 +183,12 @@ class BaseCell extends Container {
}
super.optimize(...args);
}

deleteAt(index, length) {
this.children.forEachAt(index, length, (child, offset, childLength) => {
child.deleteAt(offset, childLength);
});
}
}
BaseCell.tagName = ['TD', 'TH'];

Expand Down Expand Up @@ -210,6 +216,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) {
Expand All @@ -236,6 +243,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() {
Expand Down Expand Up @@ -316,6 +324,12 @@ class TableRow extends BaseRow {

this.childFormatName = 'table';
}

deleteAt(index, length) {
ksercs marked this conversation as resolved.
Show resolved Hide resolved
this.children.forEachAt(index, length, (child, offset, childLength) => {
child.deleteAt(offset, childLength);
});
}
}
TableRow.blotName = 'tableRow';

Expand Down
41 changes: 41 additions & 0 deletions test/functional/epic.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const EMBED = `<span>${GUARD_CHAR}<span contenteditable="false"><span contentedi
const P1 = 'Call me Ishmael. Some years ago—never mind how long precisely-having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.';
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.';

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({
Expand Down Expand Up @@ -247,6 +251,43 @@ 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('http://127.0.0.1:8080/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(
`
<table>
<thead>
<tr>
<th><p>1</p></th>
<th><p>2c</p></th>
<th><p><br></p></th>
</tr>
</thead>
</table>
`.replace(/\s/g, ''),
);
});
});

function getSelectionInTextNode() {
const {
anchorNode, anchorOffset, focusNode, focusOffset,
Expand Down
36 changes: 36 additions & 0 deletions test/functional/example/table_header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevExtreme-Quill Base Editing</title>
<link rel="stylesheet" type="text/css" href="src/dx-quill.core.css"/>
<script type="text/javascript" src="src/dx-quill.js"></script>
</head>

<body>
<div>
<div id="editor">
<table>
<thead>
<tr>
<th>1</th>
<th>2</th>
<th>3</th>
</tr>
</thead>
</table>
</div>
</div>
</body>

<script>
const editorElem = document.getElementById('editor');
const editor = new DevExpress.Quill(editorElem, {
modules: {
table: true
}
});
</script>
</html>
167 changes: 167 additions & 0 deletions test/unit/modules/table_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,173 @@ describe('Table Module', function () {
true,
);
});

describe('deleteAt', function () {
it('should not remove a cell (T1062588)', function () {
this.quill.setSelection(0);
this.table.deleteRow();

this.quill.scroll.deleteAt(5, 3);
expect(this.quill.root).toEqualHTML(
`
<table>
<tbody>
<tr>
<td><p>b1</p></td>
<td><p>b2</p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
`,
true,
);
});

it('should remove a cell line', function () {
const markup = `
<table>
<tbody>
<tr>
<td>a1<br>a2</td>
</tr>
</tbody>
</table>
`;
this.quill = this.initialize(Quill, markup, this.container, {
modules: {
table: true,
},
});

this.quill.scroll.deleteAt(2, 3);
expect(this.quill.root).toEqualHTML(
`
<table>
<tbody>
<tr>
<td><p>a1</p></td>
</tr>
</tbody>
</table>
`,
true,
);
});

it('should not remove a cell if several cells are selected', function () {
this.quill.setSelection(0);
this.table.deleteRow();

this.quill.scroll.deleteAt(1, 7);
expect(this.quill.root).toEqualHTML(
`
<table>
<tbody>
<tr>
<td><p>b</p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
`,
true,
);
});

it('should not remove a header cell if several cells are selected', function () {
const markup = `
<table>
<thead>
<tr>
<th><p>h1</p></td>
<th><p>h2</p></td>
<th><p>h3</p></td>
</tr>
</thead>
</table>
`;
this.quill = this.initialize(Quill, markup, this.container, {
modules: {
table: true,
},
});

this.quill.scroll.deleteAt(3, 7);
expect(this.quill.root).toEqualHTML(
`
<table>
<thead>
<tr>
<th><p>h1</p></td>
<th><p><br></p></td>
<th><p><br></p></td>
</tr>
</thead>
</table>
`,
true,
);
});

it('should not remove a row', function () {
this.quill.scroll.deleteAt(5, 12);
expect(this.quill.root).toEqualHTML(
`
<table>
<tbody>
<tr>
<td><p>a1</p></td>
<td><p>a2</p></td>
<td><p><br></p></td>
</tr>
<tr>
<td><p><br></p></td>
<td><p><br></p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
`,
true,
);
});

it('should remove a table', function () {
this.quill.scroll.deleteAt(0, 18);
expect(this.quill.root).toEqualHTML(
`
<p><br></p>
`,
true,
);
});
});

it('type should not remove a cell if it is selected', function () {
this.quill.updateContents(new Delta().retain(14).delete(5).insert('_INPUT_'), 'user');

expect(this.quill.root).toEqualHTML(
`
<table>
<tbody>
<tr>
<td><p>a1</p></td>
<td><p>a2</p></td>
<td><p>a3</p></td>
</tr>
<tr>
<td><p>b1</p></td>
<td><p>b2_INPUT_</p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
`,
true,
);
});
});

describe('customize table', function () {
Expand Down