/* eslint-env node */
/**
* Node.js polyfill for rendering Panel components without a browser.
* Makes the following objects globally available:
* Comment, document, Document, Element, HTMLElement, Node, requestAnimationFrame, Text.
* Most of the available DOM API functionality is provided by
* [html-element]{@link https://github.com/1N50MN14/html-element}, with some patches for
* the Web Components API.
*
* @module isorender/dom-shims
*
* @example <caption>Rendering app HTML to stdout</caption>
* import 'panel/isorender/dom-shims';
* import { Component } from 'panel';
* customElements.define('my-widget', class extends Component {
* // app definition
* });
* const myWidget = document.createElement('my-widget');
* document.body.appendChild(myWidget);
* requestAnimationFrame(() => console.log(myWidget.outerHTML));
*/
import 'html-element/global-shim';
import requestAnimationFrame from 'raf';
// make raf globally available unless a requestAnimationFrame implementation
// is already there
global.requestAnimationFrame = global.requestAnimationFrame || requestAnimationFrame;
// patch DOM insertion functions to call connectedCallback on Custom Elements
[`appendChild`, `insertBefore`, `replaceChild`].forEach((funcName) => {
const origFunc = Element.prototype[funcName];
Element.prototype[funcName] = function() {
const child = origFunc.apply(this, arguments);
requestAnimationFrame(() => {
if (!child.initialized && child.connectedCallback) {
child.connectedCallback();
}
});
};
});
// html-element only provides Element (with a lot of the HTMLElement API baked in).
// Use HTMLElement as our Web Components-ready extension.
class HTMLElement extends Element {
setAttribute(name, value) {
const oldValue = this.getAttribute(name);
super.setAttribute(...arguments);
if (this.attributeChangedCallback && this.__attrIsObserved(name)) {
this.attributeChangedCallback(name, oldValue, value);
}
}
hasAttribute(name) {
return !!this.attributes.find((attr) => attr.name === name);
}
__attrIsObserved(name) {
if (!this.__observedAttrs) {
this.__observedAttrs = this.constructor.observedAttributes || [];
}
return this.__observedAttrs.includes(name);
}
attachShadow() {
return document.createElement(`shadow-root`);
}
}
global.HTMLElement = HTMLElement;
// Document patches for Custom Elements
const customElementsRegistry = (global._customElementsRegistry = global._customElementsRegistry || {});
const originalCreateElement = Document.prototype.createElement;
Document.prototype.createElement = function(tagName) {
tagName = tagName.toLowerCase();
const customElClass = customElementsRegistry[tagName];
let el;
if (customElClass) {
el = new customElClass();
el.nodeName = el.tagName = tagName;
} else {
el = originalCreateElement(...arguments);
}
return el;
};
global.customElements = global.customElements || {
get(tagName) {
return customElementsRegistry[tagName];
},
define(tagName, proto) {
tagName = tagName.toLowerCase();
if (customElementsRegistry[tagName]) {
throw new Error(`Registration failed for type '${tagName}'. A type with that name is already registered.`);
} else {
customElementsRegistry[tagName] = proto;
}
},
};