← back to Dw Theme Compact Toolbar

assets/is-land.min.js

196 lines

// 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