diff --git a/package.json b/package.json index 54b568baf..8ae0a6a51 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "brotli": "4.5 kB" }, "./dist/amp-production/worker/worker.mjs": { - "brotli": "13 kB" + "brotli": "14 kB" }, "./dist/amp-production/worker/worker.nodom.mjs": { "brotli": "2 kB" diff --git a/src/test/node/nextSibling.test.ts b/src/test/node/nextSibling.test.ts index 89c14209d..dd8e7f209 100644 --- a/src/test/node/nextSibling.test.ts +++ b/src/test/node/nextSibling.test.ts @@ -1,11 +1,16 @@ import anyTest, { TestInterface } from 'ava'; +import { Comment } from '../../worker-thread/dom/Comment'; import { Element } from '../../worker-thread/dom/Element'; +import { Text } from '../../worker-thread/dom/Text'; import { createTestingDocument } from '../DocumentCreation'; const test = anyTest as TestInterface<{ node: Element; child: Element; childTwo: Element; + textNode: Text; + textNodeTwo: Text; + commentNode: Comment; }>; test.beforeEach((t) => { @@ -15,6 +20,9 @@ test.beforeEach((t) => { node: document.createElement('div'), child: document.createElement('div'), childTwo: document.createElement('div'), + textNode: document.createTextNode('Hello'), + textNodeTwo: document.createTextNode('World'), + commentNode: document.createComment('comment'), }; }); @@ -24,12 +32,14 @@ test('when a parent contains two children, the next sibling of the first is the node.appendChild(child); node.appendChild(childTwo); t.deepEqual(child.nextSibling, childTwo); + t.deepEqual(child.nextElementSibling, childTwo); }); test('when a node does not have a parent, its next sibling is null', (t) => { const { node } = t.context; t.is(node.nextSibling, null); + t.is(node.nextElementSibling, null); }); test('when a node is the last child of a parent, the next sibling is null', (t) => { @@ -38,4 +48,21 @@ test('when a node is the last child of a parent, the next sibling is null', (t) node.appendChild(child); node.appendChild(childTwo); t.is(childTwo.nextSibling, null); + t.is(childTwo.nextElementSibling, null); +}); + +test('nextElementSibling skips over non-element nodes', (t) => { + const { node, child, childTwo, textNode, textNodeTwo, commentNode } = t.context; + + node.appendChild(child); + node.appendChild(commentNode); + node.appendChild(textNode); + node.appendChild(childTwo); + node.appendChild(textNodeTwo); + + t.is(child.nextElementSibling, childTwo); + t.is(commentNode.nextElementSibling, childTwo); + t.is(textNode.nextElementSibling, childTwo); + t.is(childTwo.nextElementSibling, null); + t.is(textNodeTwo.nextElementSibling, null); }); diff --git a/src/test/node/previousSibling.test.ts b/src/test/node/previousSibling.test.ts index 0a20d43ce..a69162ed5 100644 --- a/src/test/node/previousSibling.test.ts +++ b/src/test/node/previousSibling.test.ts @@ -1,11 +1,16 @@ import anyTest, { TestInterface } from 'ava'; +import { Comment } from '../../worker-thread/dom/Comment'; import { Element } from '../../worker-thread/dom/Element'; +import { Text } from '../../worker-thread/dom/Text'; import { createTestingDocument } from '../DocumentCreation'; const test = anyTest as TestInterface<{ node: Element; child: Element; childTwo: Element; + textNode: Text; + textNodeTwo: Text; + commentNode: Comment; }>; test.beforeEach((t) => { @@ -15,6 +20,9 @@ test.beforeEach((t) => { node: document.createElement('div'), child: document.createElement('div'), childTwo: document.createElement('div'), + textNode: document.createTextNode('Hello world'), + textNodeTwo: document.createTextNode('World'), + commentNode: document.createComment('comment'), }; }); @@ -24,12 +32,14 @@ test('when a parent contains two children, the previous sibling of the second is node.appendChild(child); node.appendChild(childTwo); t.deepEqual(childTwo.previousSibling, child); + t.deepEqual(childTwo.previousElementSibling, child); }); test('when a node does not have a parent, its previous sibling is null', (t) => { const { node } = t.context; t.is(node.previousSibling, null); + t.is(node.previousElementSibling, null); }); test('when a node is the first child of a parent, the previous sibling is null', (t) => { @@ -38,3 +48,23 @@ test('when a node is the first child of a parent, the previous sibling is null', node.appendChild(child); t.is(child.previousSibling, null); }); + +test('previousElementSibling skips over text nodes', (t) => { + const { node, child, childTwo, textNode, textNodeTwo, commentNode } = t.context; + + node.appendChild(child); + node.appendChild(commentNode); + node.appendChild(textNode); + node.appendChild(textNodeTwo); + node.appendChild(childTwo); + + t.is(commentNode.previousElementSibling, child); + t.is(textNode.previousElementSibling, child); + t.is(textNodeTwo.previousElementSibling, child); + t.is(childTwo.previousElementSibling, child); + + node.innerHTML = ''; + node.appendChild(textNode); + node.appendChild(textNodeTwo); + t.is(textNodeTwo.previousElementSibling, null); +}); diff --git a/src/worker-thread/dom/CharacterData.ts b/src/worker-thread/dom/CharacterData.ts index 003cc4f7a..dd55f4400 100644 --- a/src/worker-thread/dom/CharacterData.ts +++ b/src/worker-thread/dom/CharacterData.ts @@ -6,6 +6,7 @@ import { Document } from './Document'; import { NodeType } from '../../transfer/TransferrableNodes'; import { TransferrableKeys } from '../../transfer/TransferrableKeys'; import { TransferrableMutationType } from '../../transfer/TransferrableMutation'; +import { getNextElementSibling, getPreviousElementSibling } from './elementSibling'; // @see https://developer.mozilla.org/en-US/docs/Web/API/CharacterData export abstract class CharacterData extends Node { @@ -73,4 +74,22 @@ export abstract class CharacterData extends Node { set nodeValue(value: string) { this.data = value; } + + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/previousElementSibling + * Returns the Element immediately prior to the specified one in its parent's children list, + * or null if the specified element is the first one in the list. + */ + get previousElementSibling(): Node | null { + return getPreviousElementSibling(this); + } + + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/nextElementSibling + * Returns the Element immediately following the specified one in its parent's children list, + * or null if the specified element is the last one in the list. + */ + get nextElementSibling(): Node | null { + return getNextElementSibling(this); + } } diff --git a/src/worker-thread/dom/Element.ts b/src/worker-thread/dom/Element.ts index 378fe081f..de9b901a6 100644 --- a/src/worker-thread/dom/Element.ts +++ b/src/worker-thread/dom/Element.ts @@ -19,6 +19,7 @@ import { MessageToWorker, MessageType, BoundingClientRectToWorker } from '../../ import { parse } from '../../third_party/html-parser/html-parser'; import { propagate } from './Node'; import { Event } from '../Event'; +import { getNextElementSibling, getPreviousElementSibling } from './elementSibling'; export const NS_NAME_TO_CLASS: { [key: string]: typeof Element } = {}; export const registerSubclass = (localName: string, subclass: typeof Element, namespace: string = HTML_NAMESPACE): any => @@ -115,9 +116,7 @@ export class Element extends ParentNode { // Element.clientTop – https://developer.mozilla.org/en-US/docs/Web/API/Element/clientTop // Element.clientWidth – https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth // set Element.innerHTML – https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML - // NonDocumentTypeChildNode.nextElementSibling – https://developer.mozilla.org/en-US/docs/Web/API/NonDocumentTypeChildNode/nextElementSibling // Element.prefix – https://developer.mozilla.org/en-US/docs/Web/API/Element/prefix - // NonDocummentTypeChildNode.previousElementSibling – https://developer.mozilla.org/en-US/docs/Web/API/NonDocumentTypeChildNode/previousElementSibling // Element.scrollHeight – https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight // Element.scrollLeft – https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft // Element.scrollLeftMax – https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeftMax @@ -161,6 +160,24 @@ export class Element extends ParentNode { // Mixins not implemented // Slotable.assignedSlot – https://developer.mozilla.org/en-US/docs/Web/API/Slotable/assignedSlot + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/previousElementSibling + * Returns the Element immediately prior to the specified one in its parent's children list, + * or null if the specified element is the first one in the list. + */ + get previousElementSibling(): Node | null { + return getPreviousElementSibling(this); + } + + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/nextElementSibling + * Returns the Element immediately following the specified one in its parent's children list, + * or null if the specified element is the last one in the list. + */ + get nextElementSibling(): Node | null { + return getNextElementSibling(this); + } + /** * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML * @return string representation of serialized HTML describing the Element and its descendants. diff --git a/src/worker-thread/dom/elementSibling.ts b/src/worker-thread/dom/elementSibling.ts new file mode 100644 index 000000000..efd096d66 --- /dev/null +++ b/src/worker-thread/dom/elementSibling.ts @@ -0,0 +1,33 @@ +import { NodeType } from '../../transfer/TransferrableNodes'; +import { Element } from './Element'; +import { Node } from './Node'; + +export function getPreviousElementSibling(node: Node): Element | null { + let parentNodes = node.parentNode && node.parentNode.childNodes; + if (!parentNodes) { + return null; + } + + for (let i = parentNodes.indexOf(node) - 1; i >= 0; i--) { + let node = parentNodes[i]; + if (node.nodeType === NodeType.ELEMENT_NODE) { + return node as Element; + } + } + return null; +} + +export function getNextElementSibling(node: Node): Element | null { + let parentNodes = node.parentNode && node.parentNode.childNodes; + if (!parentNodes) { + return null; + } + + for (let i = parentNodes.indexOf(node) + 1; i < parentNodes.length; i++) { + let node = parentNodes[i]; + if (node.nodeType === NodeType.ELEMENT_NODE) { + return node as Element; + } + } + return null; +} diff --git a/web_compat_table.md b/web_compat_table.md index aed29248c..81d4a7fc8 100644 --- a/web_compat_table.md +++ b/web_compat_table.md @@ -747,8 +747,8 @@ This section highlights the DOM APIs that are implemented in WorkerDOM currently | NodeList.keys() | ✖️ | | | NodeList.length | ✖️ | | | NodeList.values() | ✖️ | | -| NonDocumentTypeChildNode.nextElementSibling | ✖️ | | -| NonDocumentTypeChildNode.previousElementSibling | ✖️ | | +| NonDocumentTypeChildNode.nextElementSibling | ✔️ | | +| NonDocumentTypeChildNode.previousElementSibling | ️✔️ | | | ParentNode.append() | ✖️ | | | ParentNode.childElementCount | ✔️ | | | ParentNode.children | ✔️ | |