1const islandOnceCache = new Map(); 2 3class Island extends HTMLElement { 4 static tagName = "is-land"; 5 6 static fallback = { 7 ":not(:defined)": (readyPromise, node, prefix) => { 8 // remove from document to prevent web component init 9 let cloned = document.createElement(prefix + node.localName); 10 for(let attr of node.getAttributeNames()) { 11 cloned.setAttribute(attr, node.getAttribute(attr)); 12 } 13 14 let children = Array.from(node.childNodes); 15 for(let child of children) { 16 cloned.append(child); // Keep the *same* child nodes, clicking on a details->summary child should keep the state of that child 17 } 18 node.replaceWith(cloned); 19 20 return readyPromise.then(() => { 21 // restore children (not cloned) 22 for(let child of Array.from(cloned.childNodes)) { 23 node.append(child); 24 } 25 cloned.replaceWith(node); 26 }); 27 } 28 }; 29 30 static autoinit = { 31 "petite-vue": function(library) { 32 library.createApp().mount(this); 33 }, 34 "vue": function(library) { 35 library.createApp().mount(this); 36 }, 37 "svelte": function(mod) { 38 new mod.default({ target: this }); 39 }, 40 "svelte-ssr": function(mod) { 41 new mod.default({ target: this, hydrate: true }); 42 }, 43 "preact": function(mod) { 44 mod.default(this); 45 } 46 } 47 48 constructor() { 49 super(); 50 51 this.attrs = { 52 autoInitType: "autoinit", 53 import: "import", 54 template: "data-island", 55 ready: "ready", 56 }; 57 58 this.conditionMap = { 59 visible: Conditions.visible, 60 idle: Conditions.idle, 61 interaction: Conditions.interaction, 62 media: Conditions.media, 63 "save-data": Conditions.saveData, 64 }; 65 66 // Internal promises 67 this.ready = new Promise((resolve, reject) => { 68 this.readyResolve = resolve; 69 this.readyReject = reject; 70 }); 71 } 72 73 static getParents(el, selector, stopAt = false) { 74 let nodes = []; 75 while(el) { 76 if(el.matches && el.matches(selector)) { 77 if(stopAt && el === stopAt) { 78 break; 79 } 80 81 nodes.push(el); 82 } 83 el = el.parentNode; 84 } 85 return nodes; 86 } 87 88 static async ready(el) { 89 let parents = Island.getParents(el, Island.tagName); 90 let imports = await Promise.all(parents.map(el => el.wait())); 91 92 // return innermost module import 93 if(imports.length) { 94 return imports[0]; 95 } 96 } 97 98 async forceFallback() { 99 let prefix = Island.tagName + "--"; 100 let promises = []; 101 102 if(window.Island) { 103 Object.assign(Island.fallback, window.Island.fallback); 104 } 105 106 for(let selector in Island.fallback) { 107 // Reverse here as a cheap way to get the deepest nodes first 108 let components = Array.from(this.querySelectorAll(selector)).reverse(); 109 110 // with thanks to https://gist.github.com/cowboy/938767 111 for(let node of components) { 112 if(!node.isConnected || node.localName === Island.tagName) { 113 continue; 114 } 115 116 let readyPromise = Island.ready(node); 117 promises.push(Island.fallback[selector](readyPromise, node, prefix)); 118 } 119 } 120 121 return promises; 122 } 123 124 wait() { 125 return this.ready; 126 } 127 128 getConditions() { 129 let map = {}; 130 for(let key of Object.keys(this.conditionMap)) { 131 if(this.hasAttribute(`on:${key}`)) { 132 map[key] = this.getAttribute(`on:${key}`); 133 } 134 } 135 136 return map; 137 } 138 139 async connectedCallback() { 140 // Keep fallback content without initializing the components 141 // TODO improvement: only run this for not-eager components? 142 await this.forceFallback(); 143 144 await this.hydrate(); 145 } 146 147 getTemplates() { 148 return this.querySelectorAll(`:scope template[${this.attrs.template}]`); 149 } 150 151 replaceTemplates(templates) { 152 // replace <template> with the live content 153 for(let node of templates) { 154 if(Island.getParents(node, Island.tagName, this).length > 0) { 155 continue; 156 } 157 let value = node.getAttribute(this.attrs.template); 158 // get rid of the rest of the content on the island 159 if(value === "replace") { 160 let children = Array.from(this.childNodes); 161 for(let child of children) { 162 this.removeChild(child); 163 } 164 this.appendChild(node.content); 165 break; 166 } else { 167 if(value === "once" && node.innerHTML) {
168 if(islandOnceCache.has(node.innerHTML)) { 169 node.remove(); 170 return; 171 } 172 173 islandOnceCache.set(node.innerHTML, true); 174 } 175 176 node.replaceWith(node.content); 177 } 178 } 179 } 180 181 async hydrate() { 182 let conditions = []; 183 if(this.parentNode) { 184 // wait for all parents before hydrating 185 conditions.push(Island.ready(this.parentNode)); 186 } 187 188 let attrs = this.getConditions(); 189 for(let condition in attrs) { 190 if(this.conditionMap[condition]) { 191 conditions.push(this.conditionMap[condition](attrs[condition], this)); 192 } 193 } 194 195 // Loading conditions must finish before dependencies are loaded 196 await Promise.all(conditions); 197 198 this.replaceTemplates(this.getTemplates()); 199 200 let mod; 201 // [dependency="my-component-code.js"] 202 let importScript = this.getAttribute(this.attrs.import); 203 if(importScript) { 204 // we could resolve import maps here manually but youâd still have to use the full URL in your scriptâs import anyway 205 mod = await import(importScript); 206 } 207 208 if(mod) { 209 // Use `import=""` for when import maps are available e.g. `import="petite-vue"` 210 let fn = Island.autoinit[this.getAttribute(this.attrs.autoInitType) || importScript]; 211 212 if(fn) { 213 await fn.call(this, mod); 214 } 215 } 216 217 this.readyResolve({ 218 import: mod 219 }); 220 221 this.setAttribute(this.attrs.ready, ""); 222 } 223} 224 225class Conditions { 226 static visible(noop, el) { 227 if(!('IntersectionObserver' in window)) { 228 // runs immediately 229 return; 230 } 231 232 return new Promise(resolve => { 233 let observer = new IntersectionObserver(entries => { 234 let [entry] = entries; 235 if(entry.isIntersecting) { 236 observer.unobserve(entry.target); 237 resolve(); 238 } 239 }); 240 241 observer.observe(el); 242 }); 243 } 244 245 // TODO make sure this runs after all of the conditions have finished, otherwise it will 246 // finish way before the other lazy loaded promises and will be the same as a noop when 247 // on:interaction or on:visible finishes much later 248 static idle() { 249 let onload = new Promise(resolve => { 250 if(document.readyState !== "complete") { 251 window.addEventListener("load", () => resolve(), { once: true }); 252 } else { 253 resolve(); 254 } 255 }); 256 257 if(!("requestIdleCallback" in window)) { 258 // run immediately 259 return onload; 260 } 261 262 // both idle and onload 263 return Promise.all([ 264 new Promise(resolve => { 265 requestIdleCallback(() => { 266 resolve(); 267 }); 268 }), 269 onload, 270 ]); 271 } 272 273 static interaction(eventOverrides, el) { 274 let events = ["click", "touchstart"]; 275 // event overrides e.g. on:interaction="mouseenter" 276 if(eventOverrides) { 277 events = (eventOverrides || "").split(",").map(entry => entry.trim()); 278 } 279 280 return new Promise(resolve => { 281 function resolveFn(e) { 282 resolve(); 283 284 // cleanup the other event handlers 285 for(let name of events) { 286 el.removeEventListener(name, resolveFn); 287 } 288 } 289 290 for(let name of events) { 291 el.addEventListener(name, resolveFn, { once: true }); 292 } 293 }); 294 } 295 296 static media(query) { 297 let mm = { 298 matches: true 299 }; 300
301 if(query && ("matchMedia" in window)) { 302 mm = window.matchMedia(query); 303 } 304 305 if(mm.matches) { 306 return; 307 } 308 309 return new Promise(resolve => { 310 mm.addListener(e => { 311 if(e.matches) { 312 resolve(); 313 } 314 }); 315 }); 316 } 317 318 static saveData(expects) { 319 // return early if API does not exist 320 if(!("connection" in navigator) || navigator.connection.saveData === (expects !== "false")) { 321 return Promise.resolve(); 322 } 323 324 // dangly promise 325 return new Promise(() => {}); 326 } 327} 328 329// Should this auto define? Folks can redefine later using { component } export 330if("customElements" in window) { 331 window.customElements.define(Island.tagName, Island); 332 window.Island = Island; 333} 334 335export const component = Island; 336 337export 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.