// MIT License // Copyright (c) 2022–2023 Zach Leatherman @zachleat // https://github.com/11ty/is-land // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. 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) => { let cloned = document.createElement(prefix + node.localName) for (let attr of node.getAttributeNames()) cloned.setAttribute(attr, node.getAttribute(attr)) 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: mode })), shadowroot.appendChild(tmpl.content.cloneNode(!0)) } } return ( shadowroot && cloned.attachShadow({ mode: shadowroot.mode }).append(...shadowroot.childNodes), cloned.append(...node.childNodes), node.replaceWith(cloned), readyPromise.then(() => { cloned.shadowRoot && node.shadowRoot.append(...cloned.shadowRoot.childNodes), node.append(...cloned.childNodes), cloned.replaceWith(node) }) ) } } constructor() { super(), (this.ready = new Promise((resolve) => { this.readyResolve = resolve })) } static getParents(el, stopAt = !1) { let nodes = [] for (; el; ) { if (el.matches && el.matches(Island.tagName)) { if (stopAt && el === stopAt) break Conditions.hasConditions(el) && nodes.push(el) } el = el.parentNode } return nodes } static async ready(el, parents) { if ((parents || (parents = Island.getParents(el)), 0 === parents.length)) return let imports = await Promise.all(parents.map((p) => p.wait())) return imports.length ? imports[0] : void 0 } forceFallback() { window.Island && Object.assign(Island.fallback, window.Island.fallback) for (let selector in Island.fallback) { let components = Array.from(this.querySelectorAll(selector)).reverse() for (let node of components) { if (!node.isConnected) continue let parents = Island.getParents(node) if (1 === parents.length) { let p = Island.ready(node, parents) Island.fallback[selector](p, node, Island.prefix) } } } } wait() { return this.ready } async connectedCallback() { Conditions.hasConditions(this) && this.forceFallback(), await this.hydrate() } getTemplates() { return this.querySelectorAll(`template[${Island.attr.template}]`) } replaceTemplates(templates) { for (let node of templates) { if (Island.getParents(node, this).length > 0) continue let value = node.getAttribute(Island.attr.template) if ('replace' === value) { let children = Array.from(this.childNodes) for (let child of children) this.removeChild(child) this.appendChild(node.content) break } { let html = node.innerHTML if ('once' === value && html) { if (Island.onceCache.has(html)) return void node.remove() Island.onceCache.set(html, !0) } node.replaceWith(node.content) } } } async hydrate() { let conditions = [] this.parentNode && conditions.push(Island.ready(this.parentNode)) let attrs = Conditions.getConditions(this) for (let condition in attrs) Conditions.map[condition] && conditions.push(Conditions.map[condition](attrs[condition], this)) await Promise.all(conditions), this.replaceTemplates(this.getTemplates()) for (let fn of Island.onReady.values()) await fn.call(this, Island) this.readyResolve(), this.setAttribute(Island.attr.ready, ''), this.querySelectorAll(`[${Island.attr.defer}]`).forEach((node) => node.removeAttribute(Island.attr.defer)) } } class Conditions { static map = { visible: Conditions.visible, idle: Conditions.idle, interaction: Conditions.interaction, media: Conditions.media, 'save-data': Conditions.saveData } static hasConditions(node) { return Object.keys(Conditions.getConditions(node)).length > 0 } static getConditions(node) { let map = {} for (let key of Object.keys(Conditions.map)) node.hasAttribute(`on:${key}`) && (map[key] = node.getAttribute(`on:${key}`)) return map } static visible(noop, el) { if ('IntersectionObserver' in window) return new Promise((resolve) => { let observer = new IntersectionObserver((entries) => { let [entry] = entries entry.isIntersecting && (observer.unobserve(entry.target), resolve()) }) observer.observe(el) }) } static idle() { let onload = new Promise((resolve) => { 'complete' !== document.readyState ? window.addEventListener('load', () => resolve(), { once: !0 }) : resolve() }) return 'requestIdleCallback' in window ? Promise.all([ new Promise((resolve) => { requestIdleCallback(() => { resolve() }) }), onload ]) : onload } static interaction(eventOverrides, el) { let events = ['click', 'touchstart'] return ( eventOverrides && (events = (eventOverrides || '').split(',').map((entry) => entry.trim())), new Promise((resolve) => { function resolveFn(e) { resolve() for (let name of events) el.removeEventListener(name, resolveFn) } for (let name of events) el.addEventListener(name, resolveFn, { once: !0 }) }) ) } static media(query) { let mm = { matches: !0 } if ((query && 'matchMedia' in window && (mm = window.matchMedia(query)), !mm.matches)) return new Promise((resolve) => { mm.addListener((e) => { e.matches && resolve() }) }) } static saveData(expects) { if ('connection' in navigator && navigator.connection.saveData !== ('false' !== expects)) return new Promise(() => {}) } } 'customElements' in window && (window.customElements.define(Island.tagName, Island), (window.Island = Island)) export { Island, Island as component } export const ready = Island.ready