Skip to content

Commit

Permalink
Table cell and row should not be removed on backspace press or typing…
Browse files Browse the repository at this point in the history
… even if it is selected (T1062588) (#89)
  • Loading branch information
ksercs authored and Shpileva Yuliya committed Jul 19, 2023
1 parent d0c059d commit a072d21
Show file tree
Hide file tree
Showing 8 changed files with 796 additions and 10 deletions.
15 changes: 9 additions & 6 deletions 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,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();
}
Expand Down
18 changes: 17 additions & 1 deletion formats/table/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -183,6 +189,10 @@ class BaseCell extends Container {
}
super.optimize(...args);
}

deleteAt(index, length) {
deleteChildrenAt(this.children, index, length);
}
}
BaseCell.tagName = ['TD', 'TH'];

Expand Down Expand Up @@ -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) {
Expand All @@ -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() {
Expand Down Expand Up @@ -316,6 +328,10 @@ class TableRow extends BaseRow {

this.childFormatName = 'table';
}

deleteAt(index, length) {
deleteChildrenAt(this.children, index, length);
}
}
TableRow.blotName = 'tableRow';

Expand Down
16 changes: 13 additions & 3 deletions modules/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) || {};
Expand Down Expand Up @@ -451,8 +460,9 @@ Keyboard.DEFAULTS = {
},
tab: {
key: 'tab',
handler(range, context) {
if (context.format.table) return true;
handler(range, { format }) {
if (format.tableCellLine || format.tableHeaderCellLine
|| format.tableHeaderCell || format.table) return true;
this.quill.history.cutoff();
const delta = new Delta()
.retain(range.index)
Expand Down
208 changes: 208 additions & 0 deletions test/functional/epic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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(
`
<table>
<thead>
<tr>
<th><p>1</p></th>
<th><p>2c</p></th>
<th><p><br></p></th>
</tr>
</thead>
</table>
<p><br></p>
`.replace(/\s/g, ''),
);
});
});

describe('table:', 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.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>
<tbody>
<tr>
<td><p>1</p></td>
<td><p>2c</p></td>
<td><p><br></p></td>
</tr>
</tbody>
</table>
<p><br></p>
`.replace(/\s/g, ''),
);
});

it('backspace press on the position after table should remove an empty line and not add it to the cell', 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');

const html = await page.$eval('.ql-editor', (e) => e.innerHTML);
const sanitizeHtml = sanitizeTableHtml(html);
expect(sanitizeHtml).toEqual(
`
<table>
<tbody>
<tr>
<td><p>1</p></td>
<td><p>2</p></td>
<td><p>3</p></td>
</tr>
</tbody>
</table>
`.replace(/\s/g, ''),
);
});

it('backspace in multiline cell should work as usual', 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('4');
await page.keyboard.press('Enter');
await page.keyboard.press('Backspace');
await page.keyboard.press('Backspace');

const html = await page.$eval('.ql-editor', (e) => e.innerHTML);
const sanitizeHtml = sanitizeTableHtml(html);
expect(sanitizeHtml).toEqual(
`
<table>
<tbody>
<tr>
<td><p>1</p></td>
<td><p>2</p></td>
<td><p>3</p></td>
</tr>
</tbody>
</table>
<p><br></p>
`.replace(/\s/g, ''),
);
});

it('backspace press on the position after table should only move a caret to cell if next line is not 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('g');
await page.keyboard.press('ArrowLeft');
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(
`
<table>
<tbody>
<tr>
<td><p>1</p></td>
<td><p>2</p></td>
<td><p>3w</p></td>
</tr>
</tbody>
</table>
<p>g</p>
`.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(
`
<table>
<tbody>
<tr>
<td><p>1</p></td>
<td><p>2</p></td>
<td><p>3w</p></td>
</tr>
</tbody>
</table>
`.replace(/\s/g, ''),
);
});
});

// Copy/paste emulation des not working on Mac. See https://github.com/puppeteer/puppeteer/issues/1313
if (!isMac) {
describe('List copy/pasting into table', function () {
Expand Down
37 changes: 37 additions & 0 deletions test/functional/example/table.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!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>
<tbody>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
</tbody>
</table>
<p><br></p>
</div>
</div>
</body>

<script>
const editorElem = document.getElementById('editor');
const editor = new DevExpress.Quill(editorElem, {
modules: {
table: true
}
});
</script>
</html>
Loading

0 comments on commit a072d21

Please sign in to comment.