-
-
Save martok/d675bead18a98a087d84f506d327b504 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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