// https://dom.spec.whatwg.org/#interface-element import { ATTRIBUTE_NODE, BLOCK_ELEMENTS, CDATA_SECTION_NODE, COMMENT_NODE, ELEMENT_NODE, NODE_END, TEXT_NODE, SVG_NAMESPACE } from '../shared/constants.js'; import { setAttribute, removeAttribute, numericAttribute, stringAttribute } from '../shared/attributes.js'; import { CLASS_LIST, DATASET, STYLE, END, NEXT, PREV, START, MIME } from '../shared/symbols.js'; import { ignoreCase, knownAdjacent, localCase, String } from '../shared/utils.js'; import {elementAsJSON} from '../shared/jsdon.js'; import {matches, prepareMatch} from '../shared/matches.js'; import {shadowRoots} from '../shared/shadow-roots.js'; import {isConnected, parentElement, previousSibling, nextSibling} from '../shared/node.js'; import {previousElementSibling, nextElementSibling} from '../mixin/non-document-type-child-node.js'; import {before, after, replaceWith, remove} from '../mixin/child-node.js'; import {getInnerHtml, setInnerHtml} from '../mixin/inner-html.js'; import {ParentNode} from '../mixin/parent-node.js'; import {DOMStringMap} from '../dom/string-map.js'; import {DOMTokenList} from '../dom/token-list.js'; import {CSSStyleDeclaration} from './css-style-declaration.js'; import {Event} from './event.js'; import {NamedNodeMap} from './named-node-map.js'; import {ShadowRoot} from './shadow-root.js'; import {NodeList} from './node-list.js'; import {Attr} from './attr.js'; import {Text} from './text.js'; import {escape} from '../shared/text-escaper.js'; // const attributesHandler = { get(target, key) { return key in target ? target[key] : target.find(({name}) => name === key); } }; const create = (ownerDocument, element, localName) => { if ('ownerSVGElement' in element) { const svg = ownerDocument.createElementNS(SVG_NAMESPACE, localName); svg.ownerSVGElement = element.ownerSVGElement; return svg; } return ownerDocument.createElement(localName); }; const isVoid = ({localName, ownerDocument}) => { return ownerDocument[MIME].voidElements.test(localName); }; // /** * @implements globalThis.Element */ export class Element extends ParentNode { constructor(ownerDocument, localName) { super(ownerDocument, localName, ELEMENT_NODE); this[CLASS_LIST] = null; this[DATASET] = null; this[STYLE] = null; } // get isConnected() { return isConnected(this); } get parentElement() { return parentElement(this); } get previousSibling() { return previousSibling(this); } get nextSibling() { return nextSibling(this); } get namespaceURI() { return 'http://www.w3.org/1999/xhtml'; } get previousElementSibling() { return previousElementSibling(this); } get nextElementSibling() { return nextElementSibling(this); } before(...nodes) { before(this, nodes); } after(...nodes) { after(this, nodes); } replaceWith(...nodes) { replaceWith(this, nodes); } remove() { remove(this[PREV], this, this[END][NEXT]); } // // get id() { return stringAttribute.get(this, 'id'); } set id(value) { stringAttribute.set(this, 'id', value); } get className() { return this.classList.value; } set className(value) { const {classList} = this; classList.clear(); classList.add(...(String(value).split(/\s+/))); } get nodeName() { return localCase(this); } get tagName() { return localCase(this); } get classList() { return this[CLASS_LIST] || ( this[CLASS_LIST] = new DOMTokenList(this) ); } get dataset() { return this[DATASET] || ( this[DATASET] = new DOMStringMap(this) ); } getBoundingClientRect() { return { x: 0, y: 0, bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0 }; } get nonce() { return stringAttribute.get(this, 'nonce'); } set nonce(value) { stringAttribute.set(this, 'nonce', value); } get style() { return this[STYLE] || ( this[STYLE] = new CSSStyleDeclaration(this) ); } get tabIndex() { return numericAttribute.get(this, 'tabindex') || -1; } set tabIndex(value) { numericAttribute.set(this, 'tabindex', value); } get slot() { return stringAttribute.get(this, 'slot'); } set slot(value) { stringAttribute.set(this, 'slot', value); } // // get innerText() { const text = []; let {[NEXT]: next, [END]: end} = this; while (next !== end) { if (next.nodeType === TEXT_NODE) { text.push(next.textContent.replace(/\s+/g, ' ')); } else if( text.length && next[NEXT] != end && BLOCK_ELEMENTS.has(next.tagName) ) { text.push('\n'); } next = next[NEXT]; } return text.join(''); } /** * @returns {String} */ get textContent() { const text = []; let {[NEXT]: next, [END]: end} = this; while (next !== end) { const nodeType = next.nodeType; if (nodeType === TEXT_NODE || nodeType === CDATA_SECTION_NODE) text.push(next.textContent); next = next[NEXT]; } return text.join(''); } set textContent(text) { this.replaceChildren(); if (text != null && text !== '') this.appendChild(new Text(this.ownerDocument, text)); } get innerHTML() { return getInnerHtml(this); } set innerHTML(html) { setInnerHtml(this, html); } get outerHTML() { return this.toString(); } set outerHTML(html) { const template = this.ownerDocument.createElement(''); template.innerHTML = html; this.replaceWith(...template.childNodes); } // // get attributes() { const attributes = new NamedNodeMap(this); let next = this[NEXT]; while (next.nodeType === ATTRIBUTE_NODE) { attributes.push(next); next = next[NEXT]; } return new Proxy(attributes, attributesHandler); } focus() { this.dispatchEvent(new Event('focus')); } getAttribute(name) { if (name === 'class') return this.className; const attribute = this.getAttributeNode(name); return attribute && (ignoreCase(this) ? attribute.value : escape(attribute.value)); } getAttributeNode(name) { let next = this[NEXT]; while (next.nodeType === ATTRIBUTE_NODE) { if (next.name === name) return next; next = next[NEXT]; } return null; } getAttributeNames() { const attributes = new NodeList; let next = this[NEXT]; while (next.nodeType === ATTRIBUTE_NODE) { attributes.push(next.name); next = next[NEXT]; } return attributes; } hasAttribute(name) { return !!this.getAttributeNode(name); } hasAttributes() { return this[NEXT].nodeType === ATTRIBUTE_NODE; } removeAttribute(name) { if (name === 'class' && this[CLASS_LIST]) this[CLASS_LIST].clear(); let next = this[NEXT]; while (next.nodeType === ATTRIBUTE_NODE) { if (next.name === name) { removeAttribute(this, next); return; } next = next[NEXT]; } } removeAttributeNode(attribute) { let next = this[NEXT]; while (next.nodeType === ATTRIBUTE_NODE) { if (next === attribute) { removeAttribute(this, next); return; } next = next[NEXT]; } } setAttribute(name, value) { if (name === 'class') this.className = value; else { const attribute = this.getAttributeNode(name); if (attribute) attribute.value = value; else setAttribute(this, new Attr(this.ownerDocument, name, value)); } } setAttributeNode(attribute) { const {name} = attribute; const previously = this.getAttributeNode(name); if (previously !== attribute) { if (previously) this.removeAttributeNode(previously); const {ownerElement} = attribute; if (ownerElement) ownerElement.removeAttributeNode(attribute); setAttribute(this, attribute); } return previously; } toggleAttribute(name, force) { if (this.hasAttribute(name)) { if (!force) { this.removeAttribute(name); return false; } return true; } else if (force || arguments.length === 1) { this.setAttribute(name, ''); return true; } return false; } // // get shadowRoot() { if (shadowRoots.has(this)) { const {mode, shadowRoot} = shadowRoots.get(this); if (mode === 'open') return shadowRoot; } return null; } attachShadow(init) { if (shadowRoots.has(this)) throw new Error('operation not supported'); // TODO: shadowRoot should be likely a specialized class that extends DocumentFragment // but until DSD is out, I am not sure I should spend time on this. const shadowRoot = new ShadowRoot(this); shadowRoots.set(this, { mode: init.mode, shadowRoot }); return shadowRoot; } // // matches(selectors) { return matches(this, selectors); } closest(selectors) { let parentElement = this; const matches = prepareMatch(parentElement, selectors); while (parentElement && !matches(parentElement)) parentElement = parentElement.parentElement; return parentElement; } // // insertAdjacentElement(position, element) { const {parentElement} = this; switch (position) { case 'beforebegin': if (parentElement) { parentElement.insertBefore(element, this); break; } return null; case 'afterbegin': this.insertBefore(element, this.firstChild); break; case 'beforeend': this.insertBefore(element, null); break; case 'afterend': if (parentElement) { parentElement.insertBefore(element, this.nextSibling); break; } return null; } return element; } insertAdjacentHTML(position, html) { const template = this.ownerDocument.createElement('template'); template.innerHTML = html; this.insertAdjacentElement(position, template.content); } insertAdjacentText(position, text) { const node = this.ownerDocument.createTextNode(text); this.insertAdjacentElement(position, node); } // cloneNode(deep = false) { const {ownerDocument, localName} = this; const addNext = next => { next.parentNode = parentNode; knownAdjacent($next, next); $next = next; }; const clone = create(ownerDocument, this, localName); let parentNode = clone, $next = clone; let {[NEXT]: next, [END]: prev} = this; while (next !== prev && (deep || next.nodeType === ATTRIBUTE_NODE)) { switch (next.nodeType) { case NODE_END: knownAdjacent($next, parentNode[END]); $next = parentNode[END]; parentNode = parentNode.parentNode; break; case ELEMENT_NODE: { const node = create(ownerDocument, next, next.localName); addNext(node); parentNode = node; break; } case ATTRIBUTE_NODE: { const attr = next.cloneNode(deep); attr.ownerElement = parentNode; addNext(attr); break; } case TEXT_NODE: case COMMENT_NODE: case CDATA_SECTION_NODE: addNext(next.cloneNode(deep)); break; } next = next[NEXT]; } knownAdjacent($next, clone[END]); return clone; } // toString() { const out = []; const {[END]: end} = this; let next = {[NEXT]: this}; let isOpened = false; do { next = next[NEXT]; switch (next.nodeType) { case ATTRIBUTE_NODE: { const attr = ' ' + next; switch (attr) { case ' id': case ' class': case ' style': break; default: out.push(attr); } break; } case NODE_END: { const start = next[START]; if (isOpened) { if ('ownerSVGElement' in start) out.push(' />'); else if (isVoid(start)) out.push(ignoreCase(start) ? '>' : ' />'); else out.push(`>`); isOpened = false; } else out.push(``); break; } case ELEMENT_NODE: if (isOpened) out.push('>'); if (next.toString !== this.toString) { out.push(next.toString()); next = next[END]; isOpened = false; } else { out.push(`<${next.localName}`); isOpened = true; } break; case TEXT_NODE: case COMMENT_NODE: case CDATA_SECTION_NODE: out.push((isOpened ? '>' : '') + next); isOpened = false; break; } } while (next !== end); return out.join(''); } toJSON() { const json = []; elementAsJSON(this, json); return json; } // /* c8 ignore start */ getAttributeNS(_, name) { return this.getAttribute(name); } getElementsByTagNameNS(_, name) { return this.getElementsByTagName(name); } hasAttributeNS(_, name) { return this.hasAttribute(name); } removeAttributeNS(_, name) { this.removeAttribute(name); } setAttributeNS(_, name, value) { this.setAttribute(name, value); } setAttributeNodeNS(attr) { return this.setAttributeNode(attr); } /* c8 ignore stop */ }