Skip to content

Instantly share code, notes, and snippets.

@martok
Last active November 17, 2022 20:34
Show Gist options
  • Save martok/d675bead18a98a087d84f506d327b504 to your computer and use it in GitHub Desktop.
Save martok/d675bead18a98a087d84f506d327b504 to your computer and use it in GitHub Desktop.
(function () {
"use strict";
const Native_CE_define = customElements.define,
Native_EventTarget_ael = EventTarget.prototype.addEventListener,
Native_EventTarget_rel = EventTarget.prototype.removeEventListener,
Native_EventTarget_disp = EventTarget.prototype.dispatchEvent;
function patchOwnProperties(target, template) {
for (const name of Object.getOwnPropertyNames(template)) {
Object.defineProperty(target, name, Object.getOwnPropertyDescriptor(template, name));
}
}
// Ensure config dom.getRootNode.enabled is "false", or it would not work correctly
if (Node.prototype.getRootNode === undefined) {
Node.prototype.getRootNode = function getRootNode(opt) {
let composed = typeof opt === "object" && Boolean(opt.composed);
return composed ? getShadowIncludingRoot(this) : getRoot(this);
}
function getShadowIncludingRoot(node) {
let root = getRoot(node);
while (isShadowRoot(root))
root = getRoot(root.host);
return root;
}
function getRoot(node) {
while (node.parentNode)
node = node.parentNode;
return node;
}
function isShadowRoot(node) {
return node.nodeName === "#document-fragment" &&
node.constructor.name === "ShadowRoot";
}
}
const clickForwarder = new Map();
const EventTargetMixin = {
addEventListener: function (type, listener, options) {
const target = this, parent = target.parentNode,
ael = EventTarget.prototype.addEventListener;
let fwd = null;
if (parent && type==="click" && parent.localName==="button") {
if (typeof (fwd=clickForwarder.get(parent))==="undefined") {
fwd = (event) => {target.dispatchEvent(new Event(event.type))};
clickForwarder.set(parent, fwd);
}
ael.call(parent, type, fwd, options);
}
ael.call(target, type, listener, options);
},
removeEventListener: function (type, listener, options) {
const target = this, parent = target.parentNode,
rel = EventTarget.prototype.removeEventListener;
let fwd = null;
if (parent && typeof (fwd=clickForwarder.get(parent))!=="undefined") {
rel.call(parent, type, fwd, options);
}
rel.call(target, type, listener, options);
},
}
if (window.ShadowRoot === undefined) {
const tempSheet = document.createElement("style");
let shadow_UID = 1000;
document.head.appendChild(tempSheet);
const shadow_Observer = new MutationObserver((mutations, observer) => {
// assemble list of all affected ShadowRoots ...
const roots = new Set();
for (const m of mutations) {
roots.add(m.target.getRootNode());
}
// ... and tell them to update
for (const r of roots) {
r._content_modified();
}
});
window.ShadowRoot = class ShadowRoot extends DocumentFragment {
constructor() {
super();
shadow_Observer.observe(this, { attributes: true, childList: true, characterData: true, subtree: true });
this._renderRequested = false;
}
get isConnected() {
return this.host && this.host.isConnected;
}
get innerHTML() {
// TODO: probably want to cache this
const parts = [];
for (const child of this.childNodes) {
parts.push(child.outerHTML);
}
return parts.join("");
}
set innerHTML(html) {
// DocumentFragment(Node) does not have innerHTML(Element)!
// 1. clear current children
while (this.firstChild) {
this.firstChild.remove();
}
// 2. use template to parse it
const template = document.createElement("template");
template.innerHTML = html;
// 3. move to self (no need to clone Node)
this.appendChild(template.content);
}
_content_modified() {
this._renderAsync();
}
_renderAsync() {
this._renderRequested = true;
window.queueMicrotask(() => this._render());
}
_render() {
if (!this._renderRequested)
return;
const selfSelector = `${this.host.localName}[shadow-uid="${this.host.getAttribute('shadow-uid')}"]`;
const selfHost = this.host;
const renderedDoc = recursiveRender(document.createDocumentFragment(), this);
selfHost.innerHTML = "";
selfHost.appendChild(renderedDoc);
this._renderRequested = false;
function recursiveRender(target, parent) {
let node = parent.firstChild;
while (node) {
if (node.nodeType == Node.ELEMENT_NODE && node.localName == "slot") {
if (!slotFill(target, node.getAttribute("name"))) {
recursiveRender(target, node);
}
} else if (node.nodeType == Node.ELEMENT_NODE && node.localName == "style") {
globalizeStyle(target, node);
} else {
duplicateNode(target, node);
}
node = node.nextSibling;
}
return target;
}
function duplicateNode(target, node) {
const newNode = node.cloneNode();
target.appendChild(newNode);
if (node.firstChild)
recursiveRender(newNode, node);
}
function slotFill(target, slotName) {
if (slotName) {
// named slot, find the element that wants to fill it (bonus: this also finds the element if a previous _render() call slotted it!)
const elm = selfHost.querySelector(`*[slot="${slotName}"]`);
if (elm) {
target.appendChild(elm);
return true;
}
} else {
// An unnamed <slot> will be filled with all of the custom element's top-level child nodes that do not have the slot attribute. This includes text nodes.
let any = false;
for (const node of selfHost.childNodes) {
if (node.nodeType !== Node.ELEMENT_NODE || !node.hasAttribute("slot")) {
target.appendChild(node);
any = true;
}
}
return any;
}
return false;
}
function globalizeStyle(target, style) {
// 1. translate :host(-content) pseudoselector, because it would be a parser error
tempSheet.textContent = style.textContent.replace(
// flag "s" is broken in SeaMonkey
/:host(-context)?(?:\(([\s\S]+?)\))?/g,
function ($, context, selectors) {
return !context ? !selectors ? selfSelector :
`${selfSelector}:-moz-any(${selectors})` :
`:-moz-any(${selectors}) ${selfSelector}`;
});
// 2. prefix all rules that were not :host-relative already with the host child selector and transfer back to local style
const newRules = [];
for (const rule of tempSheet.sheet.cssRules) {
const css = (rule instanceof CSSStyleRule) && !rule.selectorText.includes(selfSelector) ?
selfSelector + " " + rule.cssText :
rule.cssText;
newRules.push(css);
}
tempSheet.textContent = "";
// 3. assign modified sheet before it gets inserted and parsed for real
const newNode = style.cloneNode();
newNode.textContent = newRules.join("\n");
target.appendChild(newNode);
}
}
};
const observer = new MutationObserver(function(mutations) {
for (const {target, attributeName, oldValue} of mutations) {
target.attributeChangedCallback(attributeName, oldValue, target.getAttribute(attributeName));
}
}),
asNames = new Set(["article", "aside", "blockquote", "body", "div",
"footer", "h1", "h2", "h3", "h4", "h5", "h6",
"header", "main", "nav", "p", "section", "span"]);
Element.prototype.attachShadow = function attachShadow(init) {
if (this.shadowRoot !== undefined)
throw new DOMException(
`The <${this.tagName}> element has be tried to attach to is already a shadow host.`,
"InvalidStateError");
if (!asNames.has(this.localName))
throw new DOMException(
`The <${this.tagName}> element does not supported to attach shadow`,
"NotSupportedError");
// customElements observe Shadow failed, so re-start observe here
const {observedAttributes} = this.__CE_definition || {};
if (observedAttributes) {
observer.observe(this, {
attributes: true,
attributeOldValue: true,
attributeFilter: observedAttributes
});
}
let sr = new ShadowRoot();
Object.defineProperty(sr, "host", {value: this});
Object.defineProperty(sr, "mode", {value: init.mode});
Object.defineProperty(sr, "delegatesFocus", {value: !!init.delegatesFocus});
Object.defineProperty(this, "shadowRoot", {value: init.mode === "closed" ? null : sr});
this.setAttribute("shadow-uid", shadow_UID++);
return sr;
};
customElements.define = function (name, cls) {
asNames.add(name);
patchOwnProperties(cls.prototype, EventTargetMixin);
try {
return Native_CE_define.call(this, name, cls);
} catch(c) {
// rewrite JS exception classes from polyfill to correct DOMException
if (c instanceof SyntaxError) {
throw new DOMException(c.message, "SyntaxError");
} else
if (c instanceof Error && c.name==="Error") {
throw new DOMException(c.message, "NotSupportedError");
} else
throw c;
}
};
}
}())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment