437 lines
17 KiB
JavaScript
437 lines
17 KiB
JavaScript
import {
|
|
getDeepestPosition,
|
|
isEmptyBlock,
|
|
isShrunkBlock,
|
|
isVisible,
|
|
isVisibleTextNode,
|
|
nextLeaf,
|
|
previousLeaf,
|
|
} from "@html_editor/utils/dom_info";
|
|
import { describe, expect, test } from "@odoo/hoot";
|
|
import { insertTestHtml } from "../_helpers/editor";
|
|
|
|
const base64Img =
|
|
"data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA\n AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO\n 9TXL0Y4OHwAAAABJRU5ErkJggg==";
|
|
|
|
describe("previousLeaf", () => {
|
|
test("should find the previous leaf of a deeply nested node", () => {
|
|
const [div] = insertTestHtml(
|
|
"<div><div><p><b>ab<i>cd<u>ef</u>gh</i></b><span>ij</span>kl</p></div></div>"
|
|
);
|
|
const editable = div.parentElement;
|
|
const p = div.firstChild.firstChild;
|
|
const gh = p.firstChild.childNodes[1].childNodes[2];
|
|
const ij = p.childNodes[1].firstChild;
|
|
const result = previousLeaf(ij, editable);
|
|
expect(result).toBe(gh);
|
|
});
|
|
|
|
test("should find no previous leaf and return undefined", () => {
|
|
const [div] = insertTestHtml(
|
|
"<div><div><p><b>ab<i>cd<u>ef</u>gh</i></b><span>ij</span>kl</p></div></div>"
|
|
);
|
|
const editable = div.parentElement;
|
|
const p = div.firstChild.firstChild;
|
|
const ab = p.firstChild.firstChild;
|
|
const result = previousLeaf(ab, editable);
|
|
expect(result).toBe(undefined);
|
|
});
|
|
|
|
test("should find the previous leaf of a deeply nested node, skipping invisible nodes", () => {
|
|
const [div] = insertTestHtml(
|
|
`<div>
|
|
<div>
|
|
<p>
|
|
<b>ab<i>cd<u>ef</u>gh</i></b>
|
|
</p>
|
|
<p>
|
|
<span>ij</span>kl
|
|
</p>
|
|
</div>
|
|
</div>`
|
|
);
|
|
const editable = div.parentElement;
|
|
const p1 = div.childNodes[1].childNodes[1];
|
|
const gh = p1.childNodes[1].childNodes[1].childNodes[2];
|
|
const p2 = div.childNodes[1].childNodes[3];
|
|
const ij = p2.childNodes[1].firstChild;
|
|
const result = previousLeaf(ij, editable, true);
|
|
expect(result).toBe(gh);
|
|
});
|
|
|
|
test("should find no previous leaf, skipping invisible nodes, and return undefined", () => {
|
|
const [div] = insertTestHtml(
|
|
`<div>
|
|
<div>
|
|
<p>
|
|
<b>ab<i>cd<u>ef</u>gh</i></b>
|
|
</p>
|
|
<p>
|
|
<span>ij</span>kl
|
|
</p>
|
|
</div>
|
|
</div>`
|
|
);
|
|
const editable = div.parentElement;
|
|
const p1 = div.childNodes[1].childNodes[1];
|
|
const ab = p1.childNodes[1].firstChild;
|
|
const result = previousLeaf(ab, editable, true);
|
|
expect(result).toBe(undefined);
|
|
});
|
|
|
|
test("should find the previous leaf of a deeply nested node to be whitespace", () => {
|
|
const [div] = insertTestHtml(
|
|
`<div>
|
|
<div>
|
|
<p>
|
|
<b>ab<i>cd<u>ef</u>gh</i></b>
|
|
</p>
|
|
<p>
|
|
<span>ij</span>kl
|
|
</p>
|
|
</div>
|
|
</div>`
|
|
);
|
|
const editable = div.parentElement;
|
|
const p2 = div.childNodes[1].childNodes[3];
|
|
const whitespace = p2.firstChild;
|
|
const ij = p2.childNodes[1].firstChild;
|
|
const result = previousLeaf(ij, editable);
|
|
expect(result).toBe(whitespace);
|
|
expect(whitespace.nodeType === Node.TEXT_NODE).toBe(true);
|
|
expect(whitespace.textContent).toBe(`
|
|
`);
|
|
expect(isVisibleTextNode(whitespace)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("nextLeaf", () => {
|
|
// TODO @phoenix: add nextLeaf test cases when we add it in the code base
|
|
test("should find the next leaf of a deeply nested node", () => {
|
|
const [div] = insertTestHtml(
|
|
"<div><div><p><b>ab<i>cd<u>ef</u>gh</i></b><span>ij</span>kl</p></div></div>"
|
|
);
|
|
const editable = div.parentElement;
|
|
const p = div.firstChild.firstChild;
|
|
const gh = p.firstChild.childNodes[1].childNodes[2];
|
|
const ij = p.childNodes[1].firstChild;
|
|
const result = nextLeaf(gh, editable);
|
|
expect(result).toBe(ij);
|
|
});
|
|
|
|
test("should find no next leaf and return undefined", () => {
|
|
const [div] = insertTestHtml(
|
|
"<div><div><p><b>ab<i>cd<u>ef</u>gh</i></b><span>ij</span>kl</p></div></div>"
|
|
);
|
|
const editable = div.parentElement;
|
|
const p = div.firstChild.firstChild;
|
|
const kl = p.childNodes[2];
|
|
const result = nextLeaf(kl, editable);
|
|
expect(result).toBe(undefined);
|
|
});
|
|
|
|
test("should find the next leaf of a deeply nested node, skipping invisible nodes", () => {
|
|
const [div] = insertTestHtml(
|
|
`<div>
|
|
<div>
|
|
<p>
|
|
<b>ab<i>cd<u>ef</u>gh</i></b>
|
|
</p>
|
|
<p>
|
|
<span>ij</span>kl
|
|
</p>
|
|
</div>
|
|
</div>`
|
|
);
|
|
const editable = div.parentElement;
|
|
const p1 = div.childNodes[1].childNodes[1];
|
|
const gh = p1.childNodes[1].childNodes[1].childNodes[2];
|
|
const p2 = div.childNodes[1].childNodes[3];
|
|
const ij = p2.childNodes[1].firstChild;
|
|
const result = nextLeaf(gh, editable, true);
|
|
expect(result).toBe(ij);
|
|
});
|
|
|
|
test("should find no next leaf, skipping invisible nodes, and return undefined", () => {
|
|
const [div] = insertTestHtml(
|
|
`<div>
|
|
<div>
|
|
<p>
|
|
<b>ab<i>cd<u>ef</u>gh</i></b>
|
|
</p>
|
|
<p>
|
|
<span>ij</span>kl
|
|
</p>
|
|
</div>
|
|
</div>`
|
|
);
|
|
const editable = div.parentElement;
|
|
const p2 = div.childNodes[1].childNodes[3];
|
|
const kl = p2.childNodes[2];
|
|
const result = nextLeaf(kl, editable, true);
|
|
expect(result).toBe(undefined);
|
|
});
|
|
|
|
test("should find the next leaf of a deeply nested node to be whitespace", () => {
|
|
const [div] = insertTestHtml(
|
|
`<div>
|
|
<div>
|
|
<p>
|
|
<b>ab<i>cd<u>ef</u>gh</i></b>
|
|
</p>
|
|
<p>
|
|
<span>ij</span>kl
|
|
</p>
|
|
</div>
|
|
</div>`
|
|
);
|
|
const editable = div.parentElement;
|
|
const p2 = div.childNodes[1].childNodes[3];
|
|
const kl = p2.childNodes[2];
|
|
const whitespace = div.childNodes[1].childNodes[4];
|
|
const result = nextLeaf(kl, editable);
|
|
expect(result).toBe(whitespace);
|
|
expect(whitespace.nodeType === Node.TEXT_NODE).toBe(true);
|
|
expect(whitespace.textContent).toBe(`
|
|
`);
|
|
expect(isVisibleTextNode(whitespace)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isVisible", () => {
|
|
describe("textNode", () => {
|
|
test("should identify an invisible textnode at the beginning of a paragraph before an inline node", () => {
|
|
const [p] = insertTestHtml("<p> <i>a</i></p>");
|
|
const result = isVisible(p.firstChild);
|
|
expect(result).not.toBe(true);
|
|
});
|
|
|
|
test("should identify invisible string space at the end of a paragraph after an inline node", () => {
|
|
const [p] = insertTestHtml("<p><i>a</i> </p>");
|
|
const result = isVisible(p.lastChild);
|
|
expect(result).not.toBe(true);
|
|
});
|
|
|
|
test("should identify a single visible space in an inline node in the middle of a paragraph", () => {
|
|
const [p] = insertTestHtml("<p>a<i> </i>b</p>");
|
|
const result = isVisible(p.querySelector("i").firstChild);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test("should identify a visible string with only one visible space in an inline node in the middle of a paragraph", () => {
|
|
const [p] = insertTestHtml("<p>a<i> </i>b</p>");
|
|
const result = isVisible(p.querySelector("i").firstChild);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test("should identify a visible space in the middle of a paragraph", () => {
|
|
const [p] = insertTestHtml("<p></p>");
|
|
// insert 'a b' as three separate text node inside p
|
|
const textNodes = "a b".split("").map((char) => {
|
|
const textNode = document.createTextNode(char);
|
|
p.appendChild(textNode);
|
|
return textNode;
|
|
});
|
|
const result = isVisible(textNodes[1]);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test("should identify a visible string space in the middle of a paragraph", () => {
|
|
const [p] = insertTestHtml("<p></p>");
|
|
// inserts 'a', ' ' and 'b' as 3 separate text nodes inside p
|
|
const textNodes = ["a", " ", "b"].map((char) => {
|
|
const textNode = document.createTextNode(char);
|
|
p.appendChild(textNode);
|
|
return textNode;
|
|
});
|
|
const result = isVisible(textNodes[1]);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test("should identify the first space in a series of spaces as in the middle of a paragraph as visible", () => {
|
|
const [p] = insertTestHtml("<p></p>");
|
|
// inserts 'a b' as 5 separate text nodes inside p
|
|
const textNodes = "a b".split("").map((char) => {
|
|
const textNode = document.createTextNode(char);
|
|
p.appendChild(textNode);
|
|
return textNode;
|
|
});
|
|
const result = isVisible(textNodes[1]);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test("should identify the second space in a series of spaces in the middle of a paragraph as invisible", () => {
|
|
const [p] = insertTestHtml("<p></p>");
|
|
// inserts 'a b' as 5 separate text nodes inside p
|
|
const textNodes = "a b".split("").map((char) => {
|
|
const textNode = document.createTextNode(char);
|
|
p.appendChild(textNode);
|
|
return textNode;
|
|
});
|
|
const result = isVisible(textNodes[2]);
|
|
expect(result).not.toBe(true);
|
|
});
|
|
|
|
test("should identify empty text node as invisible", () => {
|
|
const [p] = insertTestHtml("<p></p>");
|
|
// inserts 'a b' as 5 separate text nodes inside p
|
|
const textNode = document.createTextNode("");
|
|
p.appendChild(textNode);
|
|
const result = isVisible(textNode);
|
|
expect(result).not.toBe(true);
|
|
});
|
|
|
|
test("should identify a space between to visible char in inline nodes as visible", () => {
|
|
const [p] = insertTestHtml("<p><i>a</i> <i>b</i></p>");
|
|
const textNode = p.firstChild.nextSibling;
|
|
|
|
const result = isVisible(textNode);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getDeepestPosition", () => {
|
|
test("should get deepest position for text within paragraph", () => {
|
|
const [p] = insertTestHtml("<p>abc</p>");
|
|
const editable = p.parentElement;
|
|
const abc = p.firstChild;
|
|
let [node, offset] = getDeepestPosition(editable, 0);
|
|
expect([node, offset]).toEqual([abc, 0]);
|
|
[node, offset] = getDeepestPosition(editable, 1);
|
|
expect([node, offset]).toEqual([abc, 3]);
|
|
});
|
|
test("should get deepest position within nested formatting tags", () => {
|
|
const [p] = insertTestHtml("<p><span><b><i><u>abc</u></i></b></span></p>");
|
|
const editable = p.parentElement;
|
|
const abc = p.firstChild.firstChild.firstChild.firstChild.firstChild;
|
|
let [node, offset] = getDeepestPosition(editable, 0);
|
|
expect([node, offset]).toEqual([abc, 0]);
|
|
[node, offset] = getDeepestPosition(editable, 1);
|
|
expect([node, offset]).toEqual([abc, 3]);
|
|
});
|
|
test("should get deepest position in multiple paragraph", () => {
|
|
const [p1, p2] = insertTestHtml("<p>abc</p><p>def</p>");
|
|
const editable = p1.parentElement;
|
|
const abc = p1.firstChild;
|
|
const def = p2.firstChild;
|
|
let [node, offset] = getDeepestPosition(editable, 0);
|
|
expect([node, offset]).toEqual([abc, 0]);
|
|
[node, offset] = getDeepestPosition(editable, 1);
|
|
expect([node, offset]).toEqual([def, 0]);
|
|
[node, offset] = getDeepestPosition(editable, 2);
|
|
expect([node, offset]).toEqual([def, 3]);
|
|
});
|
|
test("should get deepest position for node with invisible element", () => {
|
|
const [p1] = insertTestHtml("<p></p><p>def</p>");
|
|
const editable = p1.parentElement;
|
|
const def = editable.lastChild.firstChild;
|
|
let [node, offset] = getDeepestPosition(editable, 0);
|
|
expect([node, offset]).toEqual([def, 0]);
|
|
[node, offset] = getDeepestPosition(editable, 2);
|
|
expect([node, offset]).toEqual([def, 3]);
|
|
});
|
|
test("should get deepest position for invisible block element", () => {
|
|
const [p1] = insertTestHtml("<p></p><p>def</p>");
|
|
const [node, offset] = getDeepestPosition(p1, 0);
|
|
expect([node, offset]).toEqual([p1, 0]);
|
|
});
|
|
test("should get deepest position for invisible block element(2)", () => {
|
|
const [p1] = insertTestHtml("<p>abc</p><p></p>");
|
|
const p2 = p1.nextSibling;
|
|
const [node, offset] = getDeepestPosition(p2, 0);
|
|
expect([node, offset]).toEqual([p2, 0]);
|
|
});
|
|
test("should get deepest position for elements containing invisible text nodes", () => {
|
|
const [p] = insertTestHtml(
|
|
`<p>
|
|
<i>a</i>
|
|
</p>`
|
|
);
|
|
const editable = p.parentElement;
|
|
const a = editable.firstChild.childNodes[1].firstChild;
|
|
let [node, offset] = getDeepestPosition(editable, 0);
|
|
expect([node, offset]).toEqual([a, 0]);
|
|
[node, offset] = getDeepestPosition(editable, 1);
|
|
expect([node, offset]).toEqual([a, 1]);
|
|
});
|
|
test("should not skip zwnbsp", () => {
|
|
const [a] = insertTestHtml('\ufeff<a href="#">abc</a>');
|
|
const editable = a.parentElement;
|
|
const zwnbsp = editable.firstChild;
|
|
const [node, offset] = getDeepestPosition(editable, 0);
|
|
expect([node, offset]).toEqual([zwnbsp, 0]);
|
|
});
|
|
});
|
|
|
|
describe("isEmptyBlock", () => {
|
|
test("should identify empty p element", () => {
|
|
const [p] = insertTestHtml("<p></p>");
|
|
const result = isEmptyBlock(p);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test("should identify p with single br tag as empty and multiple br tag as non-empty", () => {
|
|
const [p1, p2] = insertTestHtml("<p><br></p><p><br><br></p>");
|
|
const result1 = isEmptyBlock(p1);
|
|
const result2 = isEmptyBlock(p2);
|
|
expect(result1).toBe(true);
|
|
expect(result2).toBe(false);
|
|
});
|
|
|
|
test("should identify p element with text content as non-empty", () => {
|
|
const [p] = insertTestHtml("<p>abc</p>");
|
|
const result1 = isEmptyBlock(p);
|
|
const result2 = isEmptyBlock(p.firstChild);
|
|
expect(result1).toBe(false);
|
|
expect(result2).toBe(false);
|
|
});
|
|
|
|
test("should identify a empty span with display block", () => {
|
|
const [span] = insertTestHtml('<span style="display: block;"><br></span>');
|
|
const result = isEmptyBlock(span);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
test("should identify span with icon classes as non-empty", () => {
|
|
const [span] = insertTestHtml('<span class="fa fa-trash-o"></span>');
|
|
const result = isEmptyBlock(span);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
test("should identify img element as non-empty", () => {
|
|
const [img] = insertTestHtml(`<img src="${base64Img}" alt="image">`);
|
|
const result = isEmptyBlock(img);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
test("should identify empty a tag as non-empty", () => {
|
|
const [a] = insertTestHtml("<a></a>");
|
|
const result = isEmptyBlock(a);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
test("should identify a tag with text as non-empty", () => {
|
|
const [a] = insertTestHtml('<a href="#">Link text</a>');
|
|
const result = isEmptyBlock(a);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
test("should return false for a p containing media element", () => {
|
|
const [p] = insertTestHtml(
|
|
'<p><a href="#" title="document" data-mimetype="application/pdf" class="o_image" contenteditable="false"></a></p>'
|
|
);
|
|
const result = isEmptyBlock(p);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isShrunkBlock", () => {
|
|
test("should not consider a HR as a shrunk block", () => {
|
|
const [hr] = insertTestHtml("<hr>");
|
|
const result = isShrunkBlock(hr);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|