1// MIT License 2 3// Copyright (c) 2022â2023 Zach Leatherman @zachleat 4// https://github.com/11ty/is-land 5 6// Permission is hereby granted, free of charge, to any person obtaining a copy 7// of this software and associated documentation files (the "Software"), to deal 8// in the Software without restriction, including without limitation the rights 9// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10// copies of the Software, and to permit persons to whom the Software is 11// furnished to do so, subject to the following conditions: 12 13// The above copyright notice and this permission notice shall be included in all 14// copies or substantial portions of the Software. 15 16class Island extends HTMLElement { 17 static tagName = 'is-land' 18 static prefix = 'is-land--' 19 static attr = { template: 'data-island', ready: 'ready', defer: 'defer-hydration' } 20 static onceCache = new Map() 21 static onReady = new Map() 22 static fallback = { 23 ':not(is-land,:defined,[defer-hydration])': (readyPromise, node, prefix) => { 24 let cloned = document.createElement(prefix + node.localName) 25 for (let attr of node.getAttributeNames()) cloned.setAttribute(attr, node.getAttribute(attr)) 26 let shadowroot = node.shadowRoot 27 if (!shadowroot) { 28 let tmpl = node.querySelector(':scope > template:is([shadowrootmode], [shadowroot])') 29 if (tmpl) { 30 let mode = tmpl.getAttribute('shadowrootmode') || tmpl.getAttribute('shadowroot') || 'closed' 31 ;(shadowroot = node.attachShadow({ mode: mode })), shadowroot.appendChild(tmpl.content.cloneNode(!0)) 32 } 33 } 34 return ( 35 shadowroot && cloned.attachShadow({ mode: shadowroot.mode }).append(...shadowroot.childNodes), 36 cloned.append(...node.childNodes), 37 node.replaceWith(cloned), 38 readyPromise.then(() => { 39 cloned.shadowRoot && node.shadowRoot.append(...cloned.shadowRoot.childNodes), 40 node.append(...cloned.childNodes), 41 cloned.replaceWith(node) 42 }) 43 ) 44 } 45 } 46 constructor() { 47 super(), 48 (this.ready = new Promise((resolve) => { 49 this.readyResolve = resolve 50 })) 51 } 52 static getParents(el, stopAt = !1) { 53 let nodes = [] 54 for (; el; ) { 55 if (el.matches && el.matches(Island.tagName)) { 56 if (stopAt && el === stopAt) break 57 Conditions.hasConditions(el) && nodes.push(el) 58 } 59 el = el.parentNode 60 } 61 return nodes 62 } 63 static async ready(el, parents) { 64 if ((parents || (parents = Island.getParents(el)), 0 === parents.length)) return 65 let imports = await Promise.all(parents.map((p) => p.wait())) 66 return imports.length ? imports[0] : void 0 67 } 68 forceFallback() { 69 window.Island && Object.assign(Island.fallback, window.Island.fallback) 70 for (let selector in Island.fallback) { 71 let components = Array.from(this.querySelectorAll(selector)).reverse() 72 for (let node of components) { 73 if (!node.isConnected) continue 74 let parents = Island.getParents(node) 75 if (1 === parents.length) { 76 let p = Island.ready(node, parents) 77 Island.fallback[selector](p, node, Island.prefix) 78 } 79 } 80 } 81 } 82 wait() { 83 return this.ready 84 } 85 async connectedCallback() { 86 Conditions.hasConditions(this) && this.forceFallback(), await this.hydrate() 87 } 88 getTemplates() { 89 return this.querySelectorAll(`template[${Island.attr.template}]`) 90 } 91 replaceTemplates(templates) { 92 for (let node of templates) { 93 if (Island.getParents(node, this).length > 0) continue 94 let value = node.getAttribute(Island.attr.template) 95 if ('replace' === value) { 96 let children = Array.from(this.childNodes) 97 for (let child of children) this.removeChild(child) 98 this.appendChild(node.content) 99 break 100 } 101 { 102 let html = node.innerHTML 103 if ('once' === value && html) { 104 if (Island.onceCache.has(html)) return void node.remove() 105 Island.onceCache.set(html, !0) 106 } 107 node.replaceWith(node.content) 108 } 109 } 110 } 111 async hydrate() {
112 let conditions = [] 113 this.parentNode && conditions.push(Island.ready(this.parentNode)) 114 let attrs = Conditions.getConditions(this) 115 for (let condition in attrs) 116 Conditions.map[condition] && conditions.push(Conditions.map[condition](attrs[condition], this)) 117 await Promise.all(conditions), this.replaceTemplates(this.getTemplates()) 118 for (let fn of Island.onReady.values()) await fn.call(this, Island) 119 this.readyResolve(), 120 this.setAttribute(Island.attr.ready, ''), 121 this.querySelectorAll(`[${Island.attr.defer}]`).forEach((node) => node.removeAttribute(Island.attr.defer)) 122 } 123} 124class Conditions { 125 static map = { 126 visible: Conditions.visible, 127 idle: Conditions.idle, 128 interaction: Conditions.interaction, 129 media: Conditions.media, 130 'save-data': Conditions.saveData 131 } 132 static hasConditions(node) { 133 return Object.keys(Conditions.getConditions(node)).length > 0 134 } 135 static getConditions(node) { 136 let map = {} 137 for (let key of Object.keys(Conditions.map)) 138 node.hasAttribute(`on:${key}`) && (map[key] = node.getAttribute(`on:${key}`)) 139 return map 140 } 141 static visible(noop, el) { 142 if ('IntersectionObserver' in window) 143 return new Promise((resolve) => { 144 let observer = new IntersectionObserver((entries) => { 145 let [entry] = entries 146 entry.isIntersecting && (observer.unobserve(entry.target), resolve()) 147 }) 148 observer.observe(el) 149 }) 150 } 151 static idle() { 152 let onload = new Promise((resolve) => { 153 'complete' !== document.readyState ? window.addEventListener('load', () => resolve(), { once: !0 }) : resolve() 154 }) 155 return 'requestIdleCallback' in window 156 ? Promise.all([ 157 new Promise((resolve) => { 158 requestIdleCallback(() => { 159 resolve() 160 }) 161 }), 162 onload 163 ]) 164 : onload 165 } 166 static interaction(eventOverrides, el) { 167 let events = ['click', 'touchstart'] 168 return ( 169 eventOverrides && (events = (eventOverrides || '').split(',').map((entry) => entry.trim())), 170 new Promise((resolve) => { 171 function resolveFn(e) { 172 resolve() 173 for (let name of events) el.removeEventListener(name, resolveFn) 174 } 175 for (let name of events) el.addEventListener(name, resolveFn, { once: !0 }) 176 }) 177 ) 178 } 179 static media(query) { 180 let mm = { matches: !0 } 181 if ((query && 'matchMedia' in window && (mm = window.matchMedia(query)), !mm.matches)) 182 return new Promise((resolve) => { 183 mm.addListener((e) => { 184 e.matches && resolve() 185 }) 186 }) 187 } 188 static saveData(expects) {
189 if ('connection' in navigator && navigator.connection.saveData !== ('false' !== expects)) 190 return new Promise(() => {}) 191 } 192} 193'customElements' in window && (window.customElements.define(Island.tagName, Island), (window.Island = Island)) 194export { Island, Island as component } 195export const ready = Island.ready
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.