class Island extends HTMLElement { static tagName = "is-land"; static prefix = "is-land--"; static attr = { template: "data-island", ready: "ready", defer: "defer-hydration", }; static onceCache = new Map(); static onReady = new Map(); static fallback = { ":not(is-land,:defined,[defer-hydration])": (readyPromise, node, prefix) => { // remove from document to prevent web component init let cloned = document.createElement(prefix + node.localName); for(let attr of node.getAttributeNames()) { cloned.setAttribute(attr, node.getAttribute(attr)); } // Declarative Shadow DOM (with polyfill) let shadowroot = node.shadowRoot; if(!shadowroot) { let tmpl = node.querySelector(":scope > template:is([shadowrootmode], [shadowroot])"); if(tmpl) { let mode = tmpl.getAttribute("shadowrootmode") || tmpl.getAttribute("shadowroot") || "closed"; shadowroot = node.attachShadow({ mode }); // default is closed shadowroot.appendChild(tmpl.content.cloneNode(true)); } } // Cheers to https://gist.github.com/developit/45c85e9be01e8c3f1a0ec073d600d01e if(shadowroot) { cloned.attachShadow({ mode: shadowroot.mode }).append(...shadowroot.childNodes); } // Keep *same* child nodes to preserve state of children (e.g. details->summary) cloned.append(...node.childNodes); node.replaceWith(cloned); return readyPromise.then(() => { // Restore original children and shadow DOM if(cloned.shadowRoot) { node.shadowRoot.append(...cloned.shadowRoot.childNodes); } node.append(...cloned.childNodes); cloned.replaceWith(node); }); } } constructor() { super(); // Internal promises this.ready = new Promise(resolve => { this.readyResolve = resolve; }); } // any parents of `el` that are (with conditions) static getParents(el, stopAt = false) { let nodes = []; while(el) { if(el.matches && el.matches(Island.tagName)) { if(stopAt && el === stopAt) { break; } if(Conditions.hasConditions(el)) { nodes.push(el); } } el = el.parentNode; } return nodes; } static async ready(el, parents) { if(!parents) { parents = Island.getParents(el); } if(parents.length === 0) { return; } let imports = await Promise.all(parents.map(p => p.wait())); // return innermost module import if(imports.length) { return imports[0]; } } forceFallback() { if(window.Island) { Object.assign(Island.fallback, window.Island.fallback); } for(let selector in Island.fallback) { // Reverse here as a cheap way to get the deepest nodes first let components = Array.from(this.querySelectorAll(selector)).reverse(); // with thanks to https://gist.github.com/cowboy/938767 for(let node of components) { if(!node.isConnected) { continue; } let parents = Island.getParents(node); // must be in a leaf island (not nested deep) if(parents.length === 1) { let p = Island.ready(node, parents); Island.fallback[selector](p, node, Island.prefix); } } } } wait() { return this.ready; } async connectedCallback() { // Only use fallback content with loading conditions if(Conditions.hasConditions(this)) { // Keep fallback content without initializing the components this.forceFallback(); } await this.hydrate(); } getTemplates() { return this.querySelectorAll(`template[${Island.attr.template}]`); } replaceTemplates(templates) { // replace