You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
7.7 KiB
JavaScript
294 lines
7.7 KiB
JavaScript
// https://dom.spec.whatwg.org/#interface-parentnode
|
|
// Document, DocumentFragment, Element
|
|
|
|
import {
|
|
ATTRIBUTE_NODE,
|
|
DOCUMENT_FRAGMENT_NODE,
|
|
ELEMENT_NODE,
|
|
TEXT_NODE,
|
|
NODE_END,
|
|
CDATA_SECTION_NODE,
|
|
COMMENT_NODE
|
|
} from '../shared/constants.js';
|
|
|
|
import {PRIVATE, END, NEXT, PREV, START, VALUE} from '../shared/symbols.js';
|
|
|
|
import {prepareMatch} from '../shared/matches.js';
|
|
import {previousSibling, nextSibling} from '../shared/node.js';
|
|
import {getEnd, knownAdjacent, knownBoundaries, knownSegment, knownSiblings, localCase} from '../shared/utils.js';
|
|
|
|
import {Node} from '../interface/node.js';
|
|
import {Text} from '../interface/text.js';
|
|
import {NodeList} from '../interface/node-list.js';
|
|
|
|
import {moCallback} from '../interface/mutation-observer.js';
|
|
import {connectedCallback} from '../interface/custom-element-registry.js';
|
|
|
|
import {nextElementSibling} from './non-document-type-child-node.js';
|
|
|
|
const isNode = node => node instanceof Node;
|
|
|
|
const insert = (parentNode, child, nodes) => {
|
|
const {ownerDocument} = parentNode;
|
|
for (const node of nodes)
|
|
parentNode.insertBefore(
|
|
isNode(node) ? node : new Text(ownerDocument, node),
|
|
child
|
|
);
|
|
};
|
|
|
|
/** @typedef { import('../interface/element.js').Element & {
|
|
[typeof NEXT]: NodeStruct,
|
|
[typeof PREV]: NodeStruct,
|
|
[typeof START]: NodeStruct,
|
|
nodeType: typeof ATTRIBUTE_NODE | typeof DOCUMENT_FRAGMENT_NODE | typeof ELEMENT_NODE | typeof TEXT_NODE | typeof NODE_END | typeof COMMENT_NODE | typeof CDATA_SECTION_NODE,
|
|
ownerDocument: Document,
|
|
parentNode: ParentNode,
|
|
}} NodeStruct */
|
|
|
|
export class ParentNode extends Node {
|
|
constructor(ownerDocument, localName, nodeType) {
|
|
super(ownerDocument, localName, nodeType);
|
|
this[PRIVATE] = null;
|
|
/** @type {NodeStruct} */
|
|
this[NEXT] = this[END] = {
|
|
[NEXT]: null,
|
|
[PREV]: this,
|
|
[START]: this,
|
|
nodeType: NODE_END,
|
|
ownerDocument: this.ownerDocument,
|
|
parentNode: null
|
|
};
|
|
}
|
|
|
|
get childNodes() {
|
|
const childNodes = new NodeList;
|
|
let {firstChild} = this;
|
|
while (firstChild) {
|
|
childNodes.push(firstChild);
|
|
firstChild = nextSibling(firstChild);
|
|
}
|
|
return childNodes;
|
|
}
|
|
|
|
get children() {
|
|
const children = new NodeList;
|
|
let {firstElementChild} = this;
|
|
while (firstElementChild) {
|
|
children.push(firstElementChild);
|
|
firstElementChild = nextElementSibling(firstElementChild);
|
|
}
|
|
return children;
|
|
}
|
|
|
|
/**
|
|
* @returns {NodeStruct | null}
|
|
*/
|
|
get firstChild() {
|
|
let {[NEXT]: next, [END]: end} = this;
|
|
while (next.nodeType === ATTRIBUTE_NODE)
|
|
next = next[NEXT];
|
|
return next === end ? null : next;
|
|
}
|
|
|
|
/**
|
|
* @returns {NodeStruct | null}
|
|
*/
|
|
get firstElementChild() {
|
|
let {firstChild} = this;
|
|
while (firstChild) {
|
|
if (firstChild.nodeType === ELEMENT_NODE)
|
|
return firstChild;
|
|
firstChild = nextSibling(firstChild);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
get lastChild() {
|
|
const prev = this[END][PREV];
|
|
switch (prev.nodeType) {
|
|
case NODE_END:
|
|
return prev[START];
|
|
case ATTRIBUTE_NODE:
|
|
return null;
|
|
}
|
|
return prev === this ? null : prev;
|
|
}
|
|
|
|
get lastElementChild() {
|
|
let {lastChild} = this;
|
|
while (lastChild) {
|
|
if (lastChild.nodeType === ELEMENT_NODE)
|
|
return lastChild;
|
|
lastChild = previousSibling(lastChild);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
get childElementCount() {
|
|
return this.children.length;
|
|
}
|
|
|
|
prepend(...nodes) {
|
|
insert(this, this.firstChild, nodes);
|
|
}
|
|
|
|
append(...nodes) {
|
|
insert(this, this[END], nodes);
|
|
}
|
|
|
|
replaceChildren(...nodes) {
|
|
let {[NEXT]: next, [END]: end} = this;
|
|
while (next !== end && next.nodeType === ATTRIBUTE_NODE)
|
|
next = next[NEXT];
|
|
while (next !== end) {
|
|
const after = getEnd(next)[NEXT];
|
|
next.remove();
|
|
next = after;
|
|
}
|
|
if (nodes.length)
|
|
insert(this, end, nodes);
|
|
}
|
|
|
|
getElementsByClassName(className) {
|
|
const elements = new NodeList;
|
|
let {[NEXT]: next, [END]: end} = this;
|
|
while (next !== end) {
|
|
if (
|
|
next.nodeType === ELEMENT_NODE &&
|
|
next.hasAttribute('class') &&
|
|
next.classList.has(className)
|
|
)
|
|
elements.push(next);
|
|
next = next[NEXT];
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
getElementsByTagName(tagName) {
|
|
const elements = new NodeList;
|
|
let {[NEXT]: next, [END]: end} = this;
|
|
while (next !== end) {
|
|
if (next.nodeType === ELEMENT_NODE && (
|
|
next.localName === tagName ||
|
|
localCase(next) === tagName
|
|
))
|
|
elements.push(next);
|
|
next = next[NEXT];
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
querySelector(selectors) {
|
|
const matches = prepareMatch(this, selectors);
|
|
let {[NEXT]: next, [END]: end} = this;
|
|
while (next !== end) {
|
|
if (next.nodeType === ELEMENT_NODE && matches(next))
|
|
return next;
|
|
next = next.nodeType === ELEMENT_NODE && next.localName === 'template' ? next[END] : next[NEXT];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
querySelectorAll(selectors) {
|
|
const matches = prepareMatch(this, selectors);
|
|
const elements = new NodeList;
|
|
let {[NEXT]: next, [END]: end} = this;
|
|
while (next !== end) {
|
|
if (next.nodeType === ELEMENT_NODE && matches(next))
|
|
elements.push(next);
|
|
next = next.nodeType === ELEMENT_NODE && next.localName === 'template' ? next[END] : next[NEXT];
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
appendChild(node) {
|
|
return this.insertBefore(node, this[END]);
|
|
}
|
|
|
|
contains(node) {
|
|
let parentNode = node;
|
|
while (parentNode && parentNode !== this)
|
|
parentNode = parentNode.parentNode;
|
|
return parentNode === this;
|
|
}
|
|
|
|
insertBefore(node, before = null) {
|
|
if (node === before)
|
|
return node;
|
|
if (node === this)
|
|
throw new Error('unable to append a node to itself');
|
|
const next = before || this[END];
|
|
switch (node.nodeType) {
|
|
case ELEMENT_NODE:
|
|
node.remove();
|
|
node.parentNode = this;
|
|
knownBoundaries(next[PREV], node, next);
|
|
moCallback(node, null);
|
|
connectedCallback(node);
|
|
break;
|
|
case DOCUMENT_FRAGMENT_NODE: {
|
|
let {[PRIVATE]: parentNode, firstChild, lastChild} = node;
|
|
if (firstChild) {
|
|
knownSegment(next[PREV], firstChild, lastChild, next);
|
|
knownAdjacent(node, node[END]);
|
|
if (parentNode)
|
|
parentNode.replaceChildren();
|
|
do {
|
|
firstChild.parentNode = this;
|
|
moCallback(firstChild, null);
|
|
if (firstChild.nodeType === ELEMENT_NODE)
|
|
connectedCallback(firstChild);
|
|
} while (
|
|
firstChild !== lastChild &&
|
|
(firstChild = nextSibling(firstChild))
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case TEXT_NODE:
|
|
case COMMENT_NODE:
|
|
case CDATA_SECTION_NODE:
|
|
node.remove();
|
|
/* eslint no-fallthrough:0 */
|
|
// this covers DOCUMENT_TYPE_NODE too
|
|
default:
|
|
node.parentNode = this;
|
|
knownSiblings(next[PREV], node, next);
|
|
moCallback(node, null);
|
|
break;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
normalize() {
|
|
let {[NEXT]: next, [END]: end} = this;
|
|
while (next !== end) {
|
|
const {[NEXT]: $next, [PREV]: $prev, nodeType} = next;
|
|
if (nodeType === TEXT_NODE) {
|
|
if (!next[VALUE])
|
|
next.remove();
|
|
else if ($prev && $prev.nodeType === TEXT_NODE) {
|
|
$prev.textContent += next.textContent;
|
|
next.remove();
|
|
}
|
|
}
|
|
next = $next;
|
|
}
|
|
}
|
|
|
|
removeChild(node) {
|
|
if (node.parentNode !== this)
|
|
throw new Error('node is not a child');
|
|
node.remove();
|
|
return node;
|
|
}
|
|
|
|
replaceChild(node, replaced) {
|
|
const next = getEnd(replaced)[NEXT];
|
|
replaced.remove();
|
|
this.insertBefore(node, next);
|
|
return replaced;
|
|
}
|
|
}
|