diff --git a/import_map.json b/import_map.json index b3263ef..93dd884 100644 --- a/import_map.json +++ b/import_map.json @@ -1,5 +1,5 @@ { "imports": { - "lume/": "https://deno.land/x/lume@v1.11.3/" + "lume/": "https://deno.land/x/lume@v1.11.4/" } } diff --git a/src/404.tmpl.js b/src/404.tmpl.js index c9f41c5..69ddfe6 100644 --- a/src/404.tmpl.js +++ b/src/404.tmpl.js @@ -13,7 +13,7 @@ export default function () { url: url }, slots: { - hero: `

+ hero: `

${search()}
Browse other posts
diff --git a/src/_includes/partials/head.ts b/src/_includes/partials/head.ts index 6a69ab2..b297fb2 100644 --- a/src/_includes/partials/head.ts +++ b/src/_includes/partials/head.ts @@ -39,6 +39,7 @@ export default function (metaInfo: MetaInfo) { ${metaInfo.title} — ${app.title} + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..a56bc64 --- /dev/null +++ b/src/main.js @@ -0,0 +1,2 @@ +import './scripts/components/is-land.js' +import './scripts/components/random-404.js' diff --git a/src/scripts/components/is-land.js b/src/scripts/components/is-land.js new file mode 100644 index 0000000..953f7d5 --- /dev/null +++ b/src/scripts/components/is-land.js @@ -0,0 +1,342 @@ +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', + scriptType: 'module/island', + 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) { + let nodes = []; + while (el) { + if (el.matches && el.matches(selector)) { + 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(); + } + + getInitScripts() { + return this.querySelectorAll( + `:scope script[type="${this.attrs.scriptType}"]` + ); + } + + getTemplates() { + return this.querySelectorAll(`:scope template[${this.attrs.template}]`); + } + + replaceTemplates(templates) { + // replace