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

// 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;
}
}