const islandOnceCache = new Map(); class Island extends HTMLElement { static tagName = "is-land"; static fallback = { ":not(:defined)": (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)); } let children = Array.from(node.childNodes); for(let child of children) { cloned.append(child); // Keep the *same* child nodes, clicking on a details->summary child should keep the state of that child } node.replaceWith(cloned); return readyPromise.then(() => { // restore children (not cloned) for(let child of Array.from(cloned.childNodes)) { node.append(child); } cloned.replaceWith(node); }); } }; static autoinit = { "petite-vue": function(library) { library.createApp().mount(this); }, "vue": function(library) { library.createApp().mount(this); }, "svelte": function(mod) { new mod.default({ target: this }); }, "svelte-ssr": function(mod) { new mod.default({ target: this, hydrate: true }); }, "preact": function(mod) { mod.default(this); } } constructor() { super(); this.attrs = { autoInitType: "autoinit", import: "import", template: "data-island", ready: "ready", }; this.conditionMap = { visible: Conditions.visible, idle: Conditions.idle, interaction: Conditions.interaction, media: Conditions.media, "save-data": Conditions.saveData, }; // Internal promises this.ready = new Promise((resolve, reject) => { this.readyResolve = resolve; this.readyReject = reject; }); } static getParents(el, selector, stopAt = false) { let nodes = []; while(el) { if(el.matches && el.matches(selector)) { if(stopAt && el === stopAt) { break; } nodes.push(el); } el = el.parentNode; } return nodes; } static async ready(el) { let parents = Island.getParents(el, Island.tagName); let imports = await Promise.all(parents.map(el => el.wait())); // return innermost module import if(imports.length) { return imports[0]; } } async forceFallback() { let prefix = Island.tagName + "--"; let promises = []; 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 || node.localName === Island.tagName) { continue; } let readyPromise = Island.ready(node); promises.push(Island.fallback[selector](readyPromise, node, prefix)); } } return promises; } wait() { return this.ready; } getConditions() { let map = {}; for(let key of Object.keys(this.conditionMap)) { if(this.hasAttribute(`on:${key}`)) { map[key] = this.getAttribute(`on:${key}`); } } return map; } async connectedCallback() { // Keep fallback content without initializing the components // TODO improvement: only run this for not-eager components? await this.forceFallback(); await this.hydrate(); } getTemplates() { return this.querySelectorAll(`:scope template[${this.attrs.template}]`); } replaceTemplates(templates) { // replace