mirror of
https://github.com/bringout/oca-ocb-web.git
synced 2026-04-19 03:52:05 +02:00
replace stale web_editor with html_editor and html_builder for 19.0
web_editor was removed in Odoo 19.0 and replaced by html_editor
and html_builder. The old web_editor was incorrectly included in
the 19.0 vanilla import.
🤖 assisted by claude
This commit is contained in:
parent
4b94f0abc5
commit
f866779561
1513 changed files with 396049 additions and 358525 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,108 @@
|
|||
import { test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { testEditor } from "../_helpers/editor";
|
||||
import { unformat } from "../_helpers/format";
|
||||
import { mockUserAgent } from "@odoo/hoot-mock";
|
||||
|
||||
const ctrlShiftBackspace = () => press(["Ctrl", "Shift", "Backspace"]);
|
||||
|
||||
test("should do nothing on ctrl+shift+backspace", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>[]<br></p>",
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: "<p>[]<br></p>",
|
||||
});
|
||||
});
|
||||
test("should delete to start of paragraph with ctrl+shift+backspace", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>abc def[]</p>",
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: "<p>[]<br></p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete to start of paragraph with ctrl+shift+backspace (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>abc def[] ghi</p>",
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: "<p>[] ghi</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test.tags("focus required");
|
||||
test("should delete to start of paragraph with ctrl+shift+backspace (3)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>first paragraph</p><p>abc def[]</p>",
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: "<p>first paragraph</p><p>[]<br></p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete text between line-break and cursor", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>text<br>abc def []ghi</p>",
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: "<p>text<br>[]ghi</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete text between line-break and cursor (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>text<br>abc def ghi[]</p>",
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: "<p>text<br>[]<br></p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete text between cursor and previous line-break", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>text<br>more text<br>abc def []ghi</p>",
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: "<p>text<br>more text<br>[]ghi</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove an unremovable element on CTRL+SHIFT+BACKSPACE", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p>abc</p>
|
||||
<p class="oe_unremovable">[]<br></p>`),
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: unformat(`
|
||||
<p>abc</p>
|
||||
<p class="oe_unremovable">[]<br></p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not merge an unbreakable element on CTRL+SHIFT+BACKSPACE", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div class="oe_unbreakable">abc</div>
|
||||
<p>[]def</p>`),
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: unformat(`
|
||||
<div class="oe_unbreakable">abc</div>
|
||||
<p>[]def</p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not merge an unbreakable element on CTRL+SHIFT+BACKSPACE (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p>abc</p>
|
||||
<div class="oe_unbreakable">[]def</div>`),
|
||||
stepFunction: ctrlShiftBackspace,
|
||||
contentAfter: unformat(`
|
||||
<p>abc</p>
|
||||
<div class="oe_unbreakable">[]def</div>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("Should delete last line on MacOS", async () => {
|
||||
mockUserAgent("mac");
|
||||
await testEditor({
|
||||
contentBefore: `<p>hello world, How Are you ?[]</p>`,
|
||||
stepFunction: () => press(["Meta", "Backspace"]),
|
||||
contentAfter: `<p>[]<br></p>`,
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import { test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { testEditor } from "../_helpers/editor";
|
||||
import { unformat } from "../_helpers/format";
|
||||
import { mockUserAgent } from "@odoo/hoot-mock";
|
||||
|
||||
// CTRL+BACKSPACE
|
||||
test("should not remove the last p with ctrl+backspace", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`<p>[]<br></p>`),
|
||||
stepFunction: () => press(["Ctrl", "Backspace"]),
|
||||
contentAfter: unformat(`<p>[]<br></p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove the last p enclosed in a contenteditable=false with ctrl+backspace", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p>text</p>
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>`),
|
||||
stepFunction: () => press(["Ctrl", "Backspace"]),
|
||||
contentAfter: unformat(`
|
||||
<p>text</p>
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should add a <p><br></p> element when deleting the last child of the editable with ctrl+backspace", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<blockquote>
|
||||
[]<br>
|
||||
</blockquote>`),
|
||||
stepFunction: () => press(["Ctrl", "Backspace"]),
|
||||
contentAfter: unformat(`<p>[]<br></p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should add a <p><br></p> element when deleting the last child of an element with ctrl+backspace", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<blockquote>
|
||||
[]<br>
|
||||
</blockquote>
|
||||
</div></div>`),
|
||||
stepFunction: () => press(["Ctrl", "Backspace"]),
|
||||
contentAfter: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<p>[]<br></p>
|
||||
</div></div>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove an unremovable element on CTRL+BACKSPACE", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<blockquote class="oe_unremovable">
|
||||
[]<br>
|
||||
</blockquote>
|
||||
</div></div>`),
|
||||
stepFunction: () => press(["Ctrl", "Backspace"]),
|
||||
contentAfter: unformat(`
|
||||
<div contenteditable="false"><div contenteditable="true">
|
||||
<blockquote class="oe_unremovable">
|
||||
[]<br>
|
||||
</blockquote>
|
||||
</div></div>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove an unremovable element on CTRL+BACKSPACE (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p>abc</p>
|
||||
<p class="oe_unremovable">[]<br></p>`),
|
||||
stepFunction: () => press(["Ctrl", "Backspace"]),
|
||||
contentAfter: unformat(`
|
||||
<p>abc</p>
|
||||
<p class="oe_unremovable">[]<br></p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not merge an unbreakable element on CTRL+BACKSPACE", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div class="oe_unbreakable">abc</div>
|
||||
<p>[]def</p>`),
|
||||
stepFunction: () => press(["Ctrl", "Backspace"]),
|
||||
contentAfter: unformat(`
|
||||
<div class="oe_unbreakable">abc</div>
|
||||
<p>[]def</p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not merge an unbreakable element on CTRL+BACKSPACE (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p>abc</p>
|
||||
<div class="oe_unbreakable">[]def</div>`),
|
||||
stepFunction: () => press(["Ctrl", "Backspace"]),
|
||||
contentAfter: unformat(`
|
||||
<p>abc</p>
|
||||
<div class="oe_unbreakable">[]def</div>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("Should delete last word on MacOS", async () => {
|
||||
mockUserAgent("mac");
|
||||
await testEditor({
|
||||
contentBefore: `<p>hello world[]</p>`,
|
||||
stepFunction: () => press(["Alt", "Backspace"]),
|
||||
contentAfter: `<p>hello []</p>`,
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,541 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setupEditor, testEditor } from "../_helpers/editor";
|
||||
import { unformat } from "../_helpers/format";
|
||||
import { CORE_PLUGINS } from "@html_editor/plugin_sets";
|
||||
import { getContent, setSelection } from "../_helpers/selection";
|
||||
|
||||
async function testCoreEditor(testConfig) {
|
||||
return testEditor({ ...testConfig, config: { Plugins: CORE_PLUGINS } });
|
||||
}
|
||||
|
||||
// Tests the deleteRange shared method.
|
||||
async function deleteRange(editor) {
|
||||
// Avoid SelectionPlugin methods to avoid normalization. The goal is to
|
||||
// simulate the range passed as argument to the deleteRange method.
|
||||
const selection = editor.document.getSelection();
|
||||
let range = selection.getRangeAt(0);
|
||||
|
||||
range = editor.shared.delete.deleteRange(range);
|
||||
|
||||
const { startContainer, startOffset, endContainer, endOffset } = range;
|
||||
selection.setBaseAndExtent(startContainer, startOffset, endContainer, endOffset);
|
||||
}
|
||||
|
||||
// Tests the DELETE_SELECTION command.
|
||||
async function deleteSelection(editor) {
|
||||
editor.shared.delete.deleteSelection();
|
||||
}
|
||||
|
||||
describe("deleteRange method", () => {
|
||||
describe("Basic", () => {
|
||||
test("should delete a range inside a text node in a paragraph", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p>a[bc]d</p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit: "<p>a[]d</p>",
|
||||
});
|
||||
});
|
||||
test("should delete a range across different nodes in a paragraph", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p>a[b<i>cd</i>ef<strong>gh</strong>i]j</p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit: "<p>a[]j</p>",
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Inside inline", () => {
|
||||
test("should delete a range inside an inline element", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p><strong>a[bc]d</strong></p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit: "<p><strong>a[]d</strong></p>",
|
||||
});
|
||||
});
|
||||
test("should delete a range inside an inline element and fill empty inline", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p><strong>[abcd]</strong></p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit:
|
||||
'<p><strong data-oe-zws-empty-inline="">[]\u200b</strong><br></p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Across inlines", () => {
|
||||
test("delete across two inlines (no merge)", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p><i>a[bc</i>de<i>fg]h</i></p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit: "<p><i>a[]</i><i>h</i></p>",
|
||||
});
|
||||
});
|
||||
test("delete across two inlines, start one left empty (should fill empty inline) ", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p><i>[abc</i>de<i>fg]h</i></p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit: '<p><i data-oe-zws-empty-inline="">[]\u200b</i><i>h</i></p>',
|
||||
});
|
||||
});
|
||||
test("delete across two inlines, end one left empty (should fill empty inline) ", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p><i>a[bc</i>de<i>fgh]</i></p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit: '<p><i>a[]</i><i data-oe-zws-empty-inline="">\u200b</i></p>',
|
||||
});
|
||||
});
|
||||
test("delete across two inlines, both left empty (should fill both)", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p><i>[abc</i>de<i>fgh]</i>jkl</p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit:
|
||||
'<p><i data-oe-zws-empty-inline="">[]\u200b</i><i data-oe-zws-empty-inline="">\u200b</i>jkl</p>',
|
||||
});
|
||||
});
|
||||
test("delete across two inlines, both left empty, block left shrunk (should fill inlines and block", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p><i>[abc</i>de<i>fgh]</i></p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit:
|
||||
'<p><i data-oe-zws-empty-inline="">[]\u200b</i><i data-oe-zws-empty-inline="">\u200b</i><br></p>',
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Inside block", () => {
|
||||
test("should delete a range inside a text node in a paragraph and fill shrunk block", async () => {
|
||||
await testCoreEditor({
|
||||
contentBefore: "<p>[abcd]</p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfterEdit: "<p>[]<br></p>",
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Across blocks", () => {
|
||||
test("should merge paragraphs", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>ab[c</p><p>d]ef</p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<p>ab[]ef</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge right block's content into left block", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<h1>ab[c</h1><p>d]ef</p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<h1>ab[]ef</h1>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge right block's content into fully selected left block", async () => {
|
||||
// As opposed to the DELETE_SELECTION command, in which fully selected block on the left is removed.
|
||||
// See "should remove fully selected left block and keep second block"
|
||||
await testEditor({
|
||||
contentBefore: "<h1>[abc</h1><p>d]ef</p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<h1>[]ef</h1>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge right block's content into left block and fill shrunk block", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<h1>[abc</h1><p>def]</p>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<h1>[]<br></h1>",
|
||||
});
|
||||
});
|
||||
test("should not merge paragraph with paragraph before it", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<div><p>abc</p>[<p>]def</p></div>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div><p>abc</p>[]<p>def</p></div>",
|
||||
});
|
||||
});
|
||||
test("should merge paragraph with paragraph before it", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<div><p>abc[</p><p>]def</p></div>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div><p>abc[]def</p></div>",
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Block + inline", () => {
|
||||
test("should merge paragraph with inline content after it", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<div><p>ab[c</p>d]ef</div>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div><p>ab[]ef</p></div>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge paragraph with inline content after it (2)", async () => {
|
||||
// This is the kind of range passed to deleteRange on `...</p>[]def...` + deleteBackward
|
||||
await testEditor({
|
||||
contentBefore: "<div><p>abc[</p>]def</div>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div><p>abc[]def</p></div>",
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Inline + block", () => {
|
||||
test("should merge paragraph with inline content before it (remove paragraph)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<div>ab[c<p>d]ef</p></div>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div>ab[]ef</div>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge paragraph with inline content before it", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<div>ab[c<p>d]ef</p><p>ghi</p></div>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div>ab[]ef<p>ghi</p></div>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge paragraph with inline content before it (remove paragraph) (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<div>abc[<p>]def</p></div>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div>abc[]def</div>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge paragraph with inline content before it and insert a line-break after it", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<div>ab[c<p>d]ef</p>ghi</div>",
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div>ab[]ef<br>ghi</div>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge nested paragraph with inline content before it and insert a line-break after it", async () => {
|
||||
await testEditor({
|
||||
contentBefore: `<div>ab[c<custom-block style="display: block;"><p>d]ef</p></custom-block>ghi</div>`,
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: "<div>ab[]ef<br>ghi</div>",
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Fake line breaks", () => {
|
||||
test("should not crash if cursor is inside a fake BR", async () => {
|
||||
// The goal of this tests is to make sure deleteRange does not rely
|
||||
// on selection normaliztion. It should not assume that the cursor
|
||||
// is never inside a BR.
|
||||
const contentBefore = unformat(
|
||||
`<table><tbody>
|
||||
<tr><td><br></td><td><br></td></tr>
|
||||
<tr><td><br></td><td><br></td></tr>
|
||||
</tbody></table>`
|
||||
);
|
||||
const { editor, el } = await setupEditor(contentBefore);
|
||||
// Place the cursor inside the BR.
|
||||
setSelection({
|
||||
anchorNode: el,
|
||||
anchorOffset: 0,
|
||||
focusNode: el.querySelector("tr:nth-child(2) td br"),
|
||||
focusOffset: 0,
|
||||
});
|
||||
/* [<table><tbody>
|
||||
<tr><td><br></td><td><br></td></tr>
|
||||
<tr><td><]br></td><td><br></td></tr>
|
||||
</tbody></table>
|
||||
*/
|
||||
deleteRange(editor);
|
||||
const contentAfter = unformat(
|
||||
`[<table><tbody>
|
||||
<tr><td><br></td><td><br></td></tr>
|
||||
<tr><td>]<br></td><td><br></td></tr>
|
||||
</tbody></table>`
|
||||
);
|
||||
expect(getContent(el)).toBe(contentAfter);
|
||||
});
|
||||
});
|
||||
describe("Fill shrunk blocks", () => {
|
||||
test("should not fill a HR with BR", async () => {
|
||||
const { editor, el } = await setupEditor("<hr><p>abc[</p><p>]def</p>");
|
||||
deleteRange(editor);
|
||||
const hr = el.firstElementChild;
|
||||
expect(hr.childNodes.length).toBe(0);
|
||||
});
|
||||
});
|
||||
describe("Delete Columns", () => {
|
||||
test("should delete columns when all selected", async () => {
|
||||
await testEditor({
|
||||
contentBefore: `[<div class="container o_text_columns"><div class="row"><div class="col-4"><p>a</p></div><div class="col-4"><p>b</p></div><div class="col-4"><p>c</p></div></div></div>]`,
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: `[]<p><br></p>`,
|
||||
});
|
||||
});
|
||||
test("should delete columns when all selected along with text from an outer node", async () => {
|
||||
await testEditor({
|
||||
contentBefore: `<p>a[b</p><div class="container o_text_columns"><div class="row"><div class="col-4"><p><br></p></div><div class="col-4"><p><br></p></div><div class="col-4"><p>c</p></div></div></div>]`,
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: `<p>a[]</p>`,
|
||||
});
|
||||
});
|
||||
test("should delete all columns when all selected within a text", async () => {
|
||||
await testEditor({
|
||||
contentBefore: `<p>a[b</p><div class="container o_text_columns"><div class="row"><div class="col-4"><p><br></p></div><div class="col-4"><p><br></p></div><div class="col-4"><p><br></p></div></div></div><p>a]b</p>`,
|
||||
stepFunction: deleteRange,
|
||||
contentAfter: `<p>a[]b</p>`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteSelection", () => {
|
||||
describe("Merge blocks", () => {
|
||||
test("should remove fully selected left block and keep second block", async () => {
|
||||
// As opposed to the deleteRange method.
|
||||
// This is done by expanding the range to fully include the left
|
||||
// block before calling deleteRange. See `includeEndOrStartBlock` method.
|
||||
// <h1>[abc</h1><p>d]ef</p> -> [<h1>abc</h1><p>d]ef</p> -> deleteRange
|
||||
await testEditor({
|
||||
contentBefore: "<h1>[abc</h1><p>d]ef</p>",
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: "<p>[]ef</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should keep left block if both have been emptied", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<h1>[abc</h1><p>def]</p>",
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: "<h1>[]<br></h1>",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Unmergeables", () => {
|
||||
test("should not merge paragraph with unmeargeble block", async () => {
|
||||
await testEditor({
|
||||
contentBefore: `<p>ab[c</p><div class="oe_unbreakable">d]ef</div>`,
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: `<p>ab[]</p><div class="oe_unbreakable">ef</div>`,
|
||||
});
|
||||
});
|
||||
|
||||
test("should remove unmergeable block that has been emptied", async () => {
|
||||
// `includeEndOrStartBlock` fully includes the right block.
|
||||
// <p>ab[c</p><div>def]</div> -> <p>ab[c</p><div>def</div>] -> deleteRange
|
||||
await testEditor({
|
||||
contentBefore: `<p>ab[c</p><div class="oe_unbreakable">def]</div>`,
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: "<p>ab[]</p>",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Unremovables", () => {
|
||||
test("should not remove unremovable node, but clear its content", async () => {
|
||||
await testEditor({
|
||||
contentBefore: `<p>a[bc</p><div class="oe_unremovable">def</div><p>gh]i</p>`,
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: `<p>a[]</p><div class="oe_unremovable"><br></div><p>i</p>`,
|
||||
});
|
||||
});
|
||||
test("should move the unremovable up the tree", async () => {
|
||||
await testEditor({
|
||||
contentBefore: `<p>a[bc</p><div><div class="oe_unremovable">def</div></div><p>gh]i</p>`,
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: `<p>a[]</p><div class="oe_unremovable"><br></div><p>i</p>`,
|
||||
});
|
||||
});
|
||||
test("should preserve parent-child relations between unremovables", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(
|
||||
`<p>a[bc</p>
|
||||
<div>
|
||||
<div class="oe_unremovable">
|
||||
<div class="oe_unremovable">jkl</div>
|
||||
<p>mno</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>gh]i</p>`
|
||||
),
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: unformat(
|
||||
`<p>a[]</p>
|
||||
<div class="oe_unremovable">
|
||||
<div class="oe_unremovable"><br></div>
|
||||
</div>
|
||||
<p>i</p>`
|
||||
),
|
||||
});
|
||||
});
|
||||
test("should preserve parent-child relations between unremovables (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(
|
||||
`<p>a[bc</p>
|
||||
<div class="oe_unremovable">xyz</div>
|
||||
<div>
|
||||
<div class="oe_unremovable">
|
||||
<div>
|
||||
<div class="oe_unremovable">jkl</div>
|
||||
</div>
|
||||
<p>mno</p>
|
||||
<div class="oe_unremovable">mno</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>gh]i</p>`
|
||||
),
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: unformat(
|
||||
`<p>a[]</p>
|
||||
<div class="oe_unremovable"><br></div>
|
||||
<div class="oe_unremovable">
|
||||
<div class="oe_unremovable"><br></div>
|
||||
<div class="oe_unremovable"><br></div>
|
||||
</div>
|
||||
<p>i</p>`
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Conditional unremovables", () => {
|
||||
describe("Bootstrap columns", () => {
|
||||
test("should not remove bootstrap columns, but clear its content", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(
|
||||
`<div class="container o_text_columns o-contenteditable-false">
|
||||
<div class="row">
|
||||
<div class="col-6 o-contenteditable-true">a[bc</div>
|
||||
<div class="col-6 o-contenteditable-true">def</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>gh]i</p>`
|
||||
),
|
||||
stepFunction: deleteSelection,
|
||||
contentAfterEdit: unformat(
|
||||
`<div class="container o_text_columns o-contenteditable-false" contenteditable="false">
|
||||
<div class="row">
|
||||
<div class="col-6 o-contenteditable-true" contenteditable="true">a[]</div>
|
||||
<div class="col-6 o-contenteditable-true" contenteditable="true"><p o-we-hint-text="Empty column" class="o-we-hint"><br></p></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>i</p>`
|
||||
),
|
||||
contentAfter: unformat(
|
||||
`<div class="container o_text_columns o-contenteditable-false">
|
||||
<div class="row">
|
||||
<div class="col-6 o-contenteditable-true">a[]</div>
|
||||
<div class="col-6 o-contenteditable-true"><p><br></p></div>
|
||||
</div>
|
||||
</div>
|
||||
<p>i</p>`
|
||||
),
|
||||
});
|
||||
});
|
||||
test("should remove bootstrap columns", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(
|
||||
`<p>x[yz</p>
|
||||
<div class="container o_text_columns o-contenteditable-false">
|
||||
<div class="row">
|
||||
<div class="col-6 o-contenteditable-true">abc</div>
|
||||
<div class="col-6 o-contenteditable-true">def</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>gh]i</p>`
|
||||
),
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: "<p>x[]i</p>",
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Table cells", () => {
|
||||
test("should not remove table cell, but clear its content", async () => {
|
||||
// Actually this is handled by the table plugin, and does not
|
||||
// involve the unremovable mechanism.
|
||||
await testEditor({
|
||||
contentBefore: unformat(
|
||||
`<table><tbody>
|
||||
<tr>
|
||||
<td>[a</td> <td>b]</td> <td>c</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>d</td> <td>e</td> <td>f</td>
|
||||
</tr>
|
||||
</tbody></table>`
|
||||
),
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: unformat(
|
||||
`<table><tbody>
|
||||
<tr>
|
||||
<td><p>[]<br></p></td> <td><p><br></p></td> <td>c</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>d</td> <td>e</td> <td>f</td>
|
||||
</tr>
|
||||
</tbody></table>`
|
||||
),
|
||||
});
|
||||
});
|
||||
test("should remove table", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(
|
||||
`<p>a[bc</p>
|
||||
<table><tbody>
|
||||
<tr>
|
||||
<td><p>abc</p></td><td><p>def</p></td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<p>gh]i</p>`
|
||||
),
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: "<p>a[]i</p>",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Allowed content mismatch on blocks merge", () => {
|
||||
test("should not add H1 (flow content) to P (allows phrasing content only)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(
|
||||
`<p>a[bc</p>
|
||||
<ul>
|
||||
<li>
|
||||
<h1>def</h1>]
|
||||
<h1>ghi</h1>
|
||||
</li>
|
||||
</ul>`
|
||||
),
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: unformat(
|
||||
`<p>a[]</p>
|
||||
<ul>
|
||||
<li>
|
||||
<h1>ghi</h1>
|
||||
</li>
|
||||
</ul>`
|
||||
),
|
||||
});
|
||||
});
|
||||
test("should add P (flow content) to LI (allows flow content) ", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(
|
||||
`<ul>
|
||||
<li>
|
||||
<h1>abc</h1>
|
||||
[<h1>def</h1>
|
||||
</li>
|
||||
<li>
|
||||
<p>ghi</p>]
|
||||
<p>jkl</p>
|
||||
</li>
|
||||
</ul>`
|
||||
),
|
||||
stepFunction: deleteSelection,
|
||||
contentAfter: unformat(
|
||||
`<ul>
|
||||
<li>
|
||||
<h1>abc</h1>
|
||||
<p>[]jkl</p>
|
||||
</li>
|
||||
</ul>`
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setupEditor } from "../_helpers/editor";
|
||||
import { getContent, setSelection } from "../_helpers/selection";
|
||||
import { unformat } from "../_helpers/format";
|
||||
import { FilePlugin } from "@html_editor/main/media/file_plugin";
|
||||
import { CORE_PLUGINS } from "@html_editor/plugin_sets";
|
||||
|
||||
function findAdjacentPosition(editor, direction) {
|
||||
const deletePlugin = editor.plugins.find((p) => p.constructor.id === "delete");
|
||||
const selection = editor.document.getSelection();
|
||||
const { anchorNode, anchorOffset } = selection;
|
||||
|
||||
return deletePlugin.findAdjacentPosition(anchorNode, anchorOffset, direction);
|
||||
}
|
||||
|
||||
function assertAdjacentPositions(editor, previous, next) {
|
||||
let [node, offset] = findAdjacentPosition(editor, "forward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(editor.editable)).toBe(next);
|
||||
|
||||
[node, offset] = findAdjacentPosition(editor, "backward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(editor.editable)).toBe(previous);
|
||||
}
|
||||
|
||||
describe("findAdjacentPosition method", () => {
|
||||
describe("Basic", () => {
|
||||
test("should find adjacent character", async () => {
|
||||
const previous = "<p>a[]bcd</p>";
|
||||
const next = "<p>ab[]cd</p>";
|
||||
const { editor } = await setupEditor(previous);
|
||||
assertAdjacentPositions(editor, previous, next);
|
||||
});
|
||||
test("should find adjacent character (2)", async () => {
|
||||
const previous = "<p>[]abcd</p>";
|
||||
const next = "<p>a[]bcd</p>";
|
||||
const { editor } = await setupEditor(previous);
|
||||
assertAdjacentPositions(editor, previous, next);
|
||||
});
|
||||
test("should find adjacent character in different text node", async () => {
|
||||
const previous = "<p>a[]bcd</p>";
|
||||
const next = "<p>ab[]cd</p>";
|
||||
const { editor, el } = await setupEditor(previous);
|
||||
// Split text node between 'a' and 'b'
|
||||
const textNode = el.firstChild.firstChild;
|
||||
textNode.splitText(1);
|
||||
setSelection({ anchorNode: textNode, anchorOffset: 1 });
|
||||
assertAdjacentPositions(editor, previous, next);
|
||||
});
|
||||
test("should find first position after paragraph break", async () => {
|
||||
const previous = "<p>ab[]</p><p>cd</p>";
|
||||
const next = "<p>ab</p><p>[]cd</p>";
|
||||
const { editor } = await setupEditor(previous);
|
||||
assertAdjacentPositions(editor, previous, next);
|
||||
});
|
||||
test("should not find anything before the first position", async () => {
|
||||
const { editor } = await setupEditor("<p>[]abc</p>");
|
||||
const [node, offset] = findAdjacentPosition(editor, "backward");
|
||||
expect(node).toBe(null);
|
||||
expect(offset).toBe(null);
|
||||
});
|
||||
test("should not find anything after the last position", async () => {
|
||||
const { editor } = await setupEditor("<p>abc[]</p>");
|
||||
const [node, offset] = findAdjacentPosition(editor, "forward");
|
||||
expect(node).toBe(null);
|
||||
expect(offset).toBe(null);
|
||||
});
|
||||
test("should skip invisible character", async () => {
|
||||
const { editor, el } = await setupEditor("<p>d[]\u200bef</p>");
|
||||
const [node, offset] = findAdjacentPosition(editor, "forward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(el)).toBe("<p>d\u200be[]f</p>");
|
||||
// @todo: non-reversible operation (e.g. backward results in
|
||||
// <p>d\u200b[]ef</p>). Should it be?
|
||||
});
|
||||
test("should skip invisible character (2)", async () => {
|
||||
const { editor, el } = await setupEditor("<p>d\u200b[]ef</p>");
|
||||
const [node, offset] = findAdjacentPosition(editor, "backward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(el)).toBe("<p>[]d\u200bef</p>");
|
||||
});
|
||||
});
|
||||
describe("Contenteditable=false elements", () => {
|
||||
describe("Inlines", () => {
|
||||
test("Should find position after the span", async () => {
|
||||
const previous = '<p>a[]<span contenteditable="false">b</span>c</p>';
|
||||
const next = '<p>a<span contenteditable="false">b</span>[]c</p>';
|
||||
const { editor } = await setupEditor(previous);
|
||||
assertAdjacentPositions(editor, previous, next);
|
||||
});
|
||||
test("Should find position after paragraph break", async () => {
|
||||
const previous = '<div><p>a[]</p><span contenteditable="false">b</span></div>';
|
||||
const next = '<div><p>a</p>[]<span contenteditable="false">b</span></div>';
|
||||
const { editor } = await setupEditor(previous);
|
||||
assertAdjacentPositions(editor, previous, next);
|
||||
});
|
||||
test("Should find position before filebox", async () => {
|
||||
const content = `<div>\ufeff<span contenteditable="false" class="o_file_box"></span>\ufeff[]</div>`;
|
||||
const { editor, el } = await setupEditor(content, {
|
||||
config: { Plugins: [...CORE_PLUGINS, FilePlugin] },
|
||||
});
|
||||
const [node, offset] = findAdjacentPosition(editor, "backward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(el)).toBe(
|
||||
`<div class="o-paragraph">\ufeff[]<span contenteditable="false" class="o_file_box"></span>\ufeff<br></div>`
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("Blocks", () => {
|
||||
test("Should find position after the div", async () => {
|
||||
const { editor, el } = await setupEditor(
|
||||
'<p>a[]</p><div contenteditable="false">b</div><p>c</p>'
|
||||
);
|
||||
const [node, offset] = findAdjacentPosition(editor, "forward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(el)).toBe(
|
||||
// This position is not reachable with the keyboard, but
|
||||
// it's the desirable one to compose a range for deletion,
|
||||
// allowing to remove the div with deleteForward without
|
||||
// afecting the paragraph after it.
|
||||
'<p>a</p><div contenteditable="false">b</div>[]<p>c</p>'
|
||||
);
|
||||
});
|
||||
test("Should find position before the div", async () => {
|
||||
const { editor, el } = await setupEditor(
|
||||
'<p>a</p><div contenteditable="false">b</div><p>[]c</p>'
|
||||
);
|
||||
const [node, offset] = findAdjacentPosition(editor, "backward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(el)).toBe(
|
||||
// This position is not reachable with the keyboard, but
|
||||
// it's the desirable one to compose a range for deletion,
|
||||
// allowing to remove the div with deleteBackward without
|
||||
// afecting the paragraph before it.
|
||||
'<p>a</p>[]<div contenteditable="false">b</div><p>c</p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("Different editable zones", () => {
|
||||
test("should find adjacent character", async () => {
|
||||
const previous = unformat(`
|
||||
<div contenteditable="false">
|
||||
<p>abc</p>
|
||||
<p contenteditable="true">[]def</p>
|
||||
</div>
|
||||
<p>fgh</p>
|
||||
`);
|
||||
const next = unformat(`
|
||||
<div contenteditable="false">
|
||||
<p>abc</p>
|
||||
<p contenteditable="true">d[]ef</p>
|
||||
</div>
|
||||
<p>fgh</p>
|
||||
`);
|
||||
const { editor } = await setupEditor(previous);
|
||||
assertAdjacentPositions(editor, previous, next);
|
||||
});
|
||||
test("should not find anything outside the closest editable root", async () => {
|
||||
const { editor } = await setupEditor(
|
||||
unformat(`
|
||||
<div contenteditable="false">
|
||||
<p>abc</p>
|
||||
<p contenteditable="true">[]def</p>
|
||||
</div>
|
||||
<p>fgh</p>
|
||||
`)
|
||||
);
|
||||
const [node, offset] = findAdjacentPosition(editor, "backward");
|
||||
expect(node).toBe(null);
|
||||
expect(offset).toBe(null);
|
||||
});
|
||||
test("should not find anything outside the closest editable root (2)", async () => {
|
||||
const { editor } = await setupEditor(
|
||||
unformat(`
|
||||
<div contenteditable="false">
|
||||
<p>abc</p>
|
||||
<p contenteditable="true">def[]</p>
|
||||
</div>
|
||||
<p>fgh</p>
|
||||
`)
|
||||
);
|
||||
const [node, offset] = findAdjacentPosition(editor, "forward");
|
||||
expect(node).toBe(null);
|
||||
expect(offset).toBe(null);
|
||||
});
|
||||
test("Should find position before the div", async () => {
|
||||
const { editor, el } = await setupEditor(
|
||||
unformat(`
|
||||
<div contenteditable="false">
|
||||
<p>abc</p>
|
||||
<p contenteditable="true">def</p>
|
||||
</div>
|
||||
<p>[]fgh</p>
|
||||
`)
|
||||
);
|
||||
const [node, offset] = findAdjacentPosition(editor, "backward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(el)).toBe(
|
||||
// This position is not reachable with the keyboard, but
|
||||
// it's the desirable one to compose a range for deletion,
|
||||
// allowing to remove the div with deleteBackward
|
||||
unformat(`
|
||||
[]<div contenteditable="false">
|
||||
<p>abc</p>
|
||||
<p contenteditable="true">def</p>
|
||||
</div>
|
||||
<p>fgh</p>
|
||||
`)
|
||||
);
|
||||
});
|
||||
test("Should find position after the div", async () => {
|
||||
const { editor, el } = await setupEditor(
|
||||
unformat(`
|
||||
<p>fgh[]</p>
|
||||
<div contenteditable="false">
|
||||
<p>abc</p>
|
||||
<p contenteditable="true">def</p>
|
||||
</div>
|
||||
`)
|
||||
);
|
||||
const [node, offset] = findAdjacentPosition(editor, "forward");
|
||||
setSelection({ anchorNode: node, anchorOffset: offset });
|
||||
expect(getContent(el)).toBe(
|
||||
// This position is not reachable with the keyboard, but
|
||||
// it's the desirable one to compose a range for deletion,
|
||||
// allowing to remove the div with deleteForward
|
||||
unformat(`
|
||||
<p>fgh</p>
|
||||
<div contenteditable="false">
|
||||
<p>abc</p>
|
||||
<p contenteditable="true">def</p>
|
||||
</div>[]
|
||||
`)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
import { describe, test } from "@odoo/hoot";
|
||||
import { testEditor } from "../_helpers/editor";
|
||||
import { unformat } from "../_helpers/format";
|
||||
import { deleteBackward } from "../_helpers/user_actions";
|
||||
|
||||
describe("Adjust base container on delete", () => {
|
||||
test("should remove empty o-paragraph block", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
<div class="o-paragraph">[]<br></div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]<br></div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove empty o-paragraph block", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
<div class="o-paragraph">[]<br></div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div>
|
||||
<div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]<br></div>
|
||||
</div>
|
||||
`),
|
||||
config: { cleanEmptyStructuralContainers: false },
|
||||
});
|
||||
});
|
||||
|
||||
test("should remove empty o-paragraph block after text", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
abc<div class="o-paragraph">[]<br></div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div class="o-paragraph">abc[]</div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should remove div with selected text", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
<div>[abc]</div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]<br></div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should remove empty nested paragraph", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
<div>
|
||||
<p>[]<br></p>
|
||||
</div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div><div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]<br></div></div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove standalone paragraph", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p>[]<br></p>
|
||||
`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<p o-we-hint-text='Type "/" for commands' class="o-we-hint">[]<br></p>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove unremovable div with selected text", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div class="oe_unremovable">
|
||||
[abc]
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div class="oe_unremovable">[]<br></div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should keep paragraph inside unremovable div", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div class="oe_unremovable">
|
||||
<p>[abc]</p>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div class="oe_unremovable">
|
||||
<p o-we-hint-text='Type "/" for commands' class="o-we-hint">[]<br></p>
|
||||
</div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should keep block inside non-editable parent", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false">
|
||||
<div contenteditable="true">[abc]</div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
// The P is added by the deletion, not by `cleanEmptyStructuralContainers`.
|
||||
contentAfterEdit: unformat(`
|
||||
<div contenteditable="false">
|
||||
<div contenteditable="true">
|
||||
<p o-we-hint-text='Type "/" for commands' class="o-we-hint">[]<br></p>
|
||||
</div>
|
||||
</div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should keep o-paragraph inside non-editable parent", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false">
|
||||
<div contenteditable="true">
|
||||
<div class="o-paragraph">[]<br></div>
|
||||
</div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div contenteditable="false">
|
||||
<div contenteditable="true">
|
||||
<div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]<br></div>
|
||||
</div>
|
||||
</div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should remove nested o-paragraph with inner paragraph", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
<div class="o-paragraph">
|
||||
<p class="inner-paragraph">[]<br></p>
|
||||
</div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div>
|
||||
<div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]</div>
|
||||
</div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should remove first of multiple consecutive empty blocks", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
<div>[]<br></div>
|
||||
<div><br></div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div>
|
||||
<div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]<br></div>
|
||||
</div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove block with cursor at beginning of content", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
<div>[]text</div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div>
|
||||
<div class="o-paragraph">[]text</div>
|
||||
</div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge blocks when range spans multiple o-paragraphs", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div>
|
||||
<div class="o-paragraph">[abc</div>
|
||||
<div class="o-paragraph">def]</div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]<br></div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should remove o-paragraph inside nested non-editable structure", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div contenteditable="false">
|
||||
<div contenteditable="true">
|
||||
<div class="o-paragraph">[]<br></div>
|
||||
</div>
|
||||
</div>`),
|
||||
stepFunction: async (editor) => {
|
||||
deleteBackward(editor);
|
||||
},
|
||||
contentAfterEdit: unformat(`
|
||||
<div contenteditable="false">
|
||||
<div contenteditable="true">
|
||||
<div class="o-paragraph o-we-hint" o-we-hint-text='Type "/" for commands'>[]<br></div>
|
||||
</div>
|
||||
</div>
|
||||
`),
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,107 @@
|
|||
import { test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { testEditor } from "../_helpers/editor";
|
||||
import { unformat } from "../_helpers/format";
|
||||
import { mockUserAgent } from "@odoo/hoot-mock";
|
||||
|
||||
const ctrlShiftDelete = () => press(["Ctrl", "Shift", "Delete"]);
|
||||
|
||||
test("should do nothing on ctrl+shift+delete", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>[]<br></p>",
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: "<p>[]<br></p>",
|
||||
});
|
||||
});
|
||||
test("should delete to end of paragraph with ctrl+shift+delete", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>[]abc def</p>",
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: "<p>[]<br></p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete to end of paragraph with ctrl+shift+delete (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>abc []def ghi</p>",
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: "<p>abc []</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete to end of paragraph with ctrl+shift+delete (3)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>[]abc def</p><p>second paragraph</p>",
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: "<p>[]<br></p><p>second paragraph</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete text between line-break and cursor", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>abc[] def ghi<br>text</p>",
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: "<p>abc[]<br>text</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete text between line-break and cursor (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>[]abc def ghi<br>text</p>",
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: "<p>[]<br>text</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should delete text between cursor and next line-break", async () => {
|
||||
await testEditor({
|
||||
contentBefore: "<p>abc[] def ghi<br>text<br>more text</p>",
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: "<p>abc[]<br>text<br>more text</p>",
|
||||
});
|
||||
});
|
||||
|
||||
test("should not remove an unremovable element on CTRL+SHIFT+DELETE", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p class="oe_unremovable">[]<br></p>
|
||||
<p>abc</p>`),
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: unformat(`
|
||||
<p class="oe_unremovable">[]<br></p>
|
||||
<p>abc</p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not merge an unbreakable element on CTRL+SHIFT+DELETE", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div class="oe_unbreakable">abc[]</div>
|
||||
<p>def</p>`),
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: unformat(`
|
||||
<div class="oe_unbreakable">abc[]</div>
|
||||
<p>def</p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not merge an unbreakable element on CTRL+SHIFT+DELETE (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p>abc[]</p>
|
||||
<div class="oe_unbreakable">def</div>`),
|
||||
stepFunction: ctrlShiftDelete,
|
||||
contentAfter: unformat(`
|
||||
<p>abc[]</p>
|
||||
<div class="oe_unbreakable">def</div>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("Should delete last line on MacOS", async () => {
|
||||
mockUserAgent("mac");
|
||||
await testEditor({
|
||||
contentBefore: `<p>[]hello world, How Are you ?</p>`,
|
||||
stepFunction: () => press(["Meta", "Delete"]),
|
||||
contentAfter: `<p>[]<br></p>`,
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { test } from "@odoo/hoot";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { testEditor } from "../_helpers/editor";
|
||||
import { unformat } from "../_helpers/format";
|
||||
import { mockUserAgent } from "@odoo/hoot-mock";
|
||||
|
||||
test("should not remove an unremovable element on CTRL+DELETE", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p class="oe_unremovable">[]<br></p>
|
||||
<p>abc</p>`),
|
||||
stepFunction: () => press(["Ctrl", "Delete"]),
|
||||
contentAfter: unformat(`
|
||||
<p class="oe_unremovable">[]<br></p>
|
||||
<p>abc</p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not merge an unbreakable element on CTRL+DELETE", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<div class="oe_unbreakable">abc[]</div>
|
||||
<p>def</p>`),
|
||||
stepFunction: () => press(["Ctrl", "Delete"]),
|
||||
contentAfter: unformat(`
|
||||
<div class="oe_unbreakable">abc[]</div>
|
||||
<p>def</p>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("should not merge an unbreakable element on CTRL+DELETE (2)", async () => {
|
||||
await testEditor({
|
||||
contentBefore: unformat(`
|
||||
<p>abc[]</p>
|
||||
<div class="oe_unbreakable">def</div>`),
|
||||
stepFunction: () => press(["Ctrl", "Delete"]),
|
||||
contentAfter: unformat(`
|
||||
<p>abc[]</p>
|
||||
<div class="oe_unbreakable">def</div>`),
|
||||
});
|
||||
});
|
||||
|
||||
test("Should delete last word on MacOS", async () => {
|
||||
mockUserAgent("mac");
|
||||
await testEditor({
|
||||
contentBefore: `<p>hello[] world</p>`,
|
||||
stepFunction: () => press(["Alt", "Delete"]),
|
||||
contentAfter: `<p>hello[]</p>`,
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setupEditor } from "../_helpers/editor";
|
||||
import { press } from "@odoo/hoot-dom";
|
||||
import { getContent } from "../_helpers/selection";
|
||||
import { execCommand } from "../_helpers/userCommands";
|
||||
|
||||
describe("delete backward", () => {
|
||||
test("should register a history step (collapsed selection)", async () => {
|
||||
const { el, editor } = await setupEditor("<p>ab[]cd</p>");
|
||||
|
||||
await press("Backspace");
|
||||
expect(getContent(el)).toBe("<p>a[]cd</p>");
|
||||
|
||||
execCommand(editor, "historyUndo");
|
||||
expect(getContent(el)).toBe("<p>ab[]cd</p>");
|
||||
|
||||
execCommand(editor, "historyRedo");
|
||||
expect(getContent(el)).toBe("<p>a[]cd</p>");
|
||||
});
|
||||
|
||||
test("should register a history step (non-collapsed selection)", async () => {
|
||||
const { el, editor } = await setupEditor("<p>ab[cd]ef</p>");
|
||||
|
||||
await press("Backspace");
|
||||
expect(getContent(el)).toBe("<p>ab[]ef</p>");
|
||||
|
||||
execCommand(editor, "historyUndo");
|
||||
expect(getContent(el)).toBe("<p>ab[cd]ef</p>");
|
||||
|
||||
execCommand(editor, "historyRedo");
|
||||
expect(getContent(el)).toBe("<p>ab[]ef</p>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete forward", () => {
|
||||
test("should register a history step (collapsed selection)", async () => {
|
||||
const { el, editor } = await setupEditor("<p>ab[]cd</p>");
|
||||
|
||||
await press("Delete");
|
||||
expect(getContent(el)).toBe("<p>ab[]d</p>");
|
||||
|
||||
execCommand(editor, "historyUndo");
|
||||
expect(getContent(el)).toBe("<p>ab[]cd</p>");
|
||||
|
||||
execCommand(editor, "historyRedo");
|
||||
expect(getContent(el)).toBe("<p>ab[]d</p>");
|
||||
});
|
||||
|
||||
test("should register a history step (non-collapsed selection)", async () => {
|
||||
const { el, editor } = await setupEditor("<p>ab[cd]ef</p>");
|
||||
|
||||
await press("Delete");
|
||||
expect(getContent(el)).toBe("<p>ab[]ef</p>");
|
||||
|
||||
execCommand(editor, "historyUndo");
|
||||
expect(getContent(el)).toBe("<p>ab[cd]ef</p>");
|
||||
|
||||
execCommand(editor, "historyRedo");
|
||||
expect(getContent(el)).toBe("<p>ab[]ef</p>");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue