Improved page navigation and import of on-demand assets
This commit is contained in:
parent
a0a680c8d2
commit
4663341497
14 changed files with 248 additions and 55 deletions
public/vendor
src
routes
scripts/pages
styles/pages
view
1
public/vendor/alpine-ext/async-alpine@1.2.2.js
vendored
Normal file
1
public/vendor/alpine-ext/async-alpine@1.2.2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/vendor/alpine-ext/morph@3.13.8.js
vendored
Normal file
1
public/vendor/alpine-ext/morph@3.13.8.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(()=>{function k(u,l,o){Y();let g,h,y,B,O,E,v,T,_,A;function V(e={}){let n=a=>a.getAttribute("key"),d=()=>{};O=e.updating||d,E=e.updated||d,v=e.removing||d,T=e.removed||d,_=e.adding||d,A=e.added||d,y=e.key||n,B=e.lookahead||!1}function D(e,n){if(W(e,n))return q(e,n);let d=!1;if(!b(O,e,n,()=>d=!0)){if(e.nodeType===1&&window.Alpine&&window.Alpine.cloneNode(e,n),X(n)){$(e,n),E(e,n);return}d||G(e,n),E(e,n),L(e,n)}}function W(e,n){return e.nodeType!=n.nodeType||e.nodeName!=n.nodeName||x(e)!=x(n)}function q(e,n){if(b(v,e))return;let d=n.cloneNode(!0);b(_,d)||(e.replaceWith(d),T(e),A(d))}function $(e,n){let d=n.nodeValue;e.nodeValue!==d&&(e.nodeValue=d)}function G(e,n){if(e._x_transitioning||e._x_isShown&&!n._x_isShown||!e._x_isShown&&n._x_isShown)return;let d=Array.from(e.attributes),a=Array.from(n.attributes);for(let i=d.length-1;i>=0;i--){let t=d[i].name;n.hasAttribute(t)||e.removeAttribute(t)}for(let i=a.length-1;i>=0;i--){let t=a[i].name,m=a[i].value;e.getAttribute(t)!==m&&e.setAttribute(t,m)}}function L(e,n){e._x_teleport&&(e=e._x_teleport),n._x_teleport&&(n=n._x_teleport);let d=H(e.children),a={},i=I(n),t=I(e);for(;i;){Z(i,t);let s=x(i),p=x(t);if(!t)if(s&&a[s]){let r=a[s];e.appendChild(r),t=r}else{if(!b(_,i)){let r=i.cloneNode(!0);e.appendChild(r),A(r)}i=c(n,i);continue}let C=r=>r&&r.nodeType===8&&r.textContent==="[if BLOCK]><![endif]",S=r=>r&&r.nodeType===8&&r.textContent==="[if ENDBLOCK]><![endif]";if(C(i)&&C(t)){let r=0,N=t;for(;t;){let f=c(e,t);if(C(f))r++;else if(S(f)&&r>0)r--;else if(S(f)&&r===0){t=f;break}t=f}let R=t;r=0;let j=i;for(;i;){let f=c(n,i);if(C(f))r++;else if(S(f)&&r>0)r--;else if(S(f)&&r===0){i=f;break}i=f}let z=i,J=new w(N,R),Q=new w(j,z);L(J,Q);continue}if(t.nodeType===1&&B&&!t.isEqualNode(i)){let r=c(n,i),N=!1;for(;!N&&r;)r.nodeType===1&&t.isEqualNode(r)&&(N=!0,t=K(e,i,t),p=x(t)),r=c(n,r)}if(s!==p){if(!s&&p){a[p]=t,t=K(e,i,t),a[p].remove(),t=c(e,t),i=c(n,i);continue}if(s&&!p&&d[s]&&(t.replaceWith(d[s]),t=d[s]),s&&p){let r=d[s];if(r)a[p]=t,t.replaceWith(r),t=r;else{a[p]=t,t=K(e,i,t),a[p].remove(),t=c(e,t),i=c(n,i);continue}}}let P=t&&c(e,t);D(t,i),i=i&&c(n,i),t=P}let m=[];for(;t;)b(v,t)||m.push(t),t=c(e,t);for(;m.length;){let s=m.shift();s.remove(),T(s)}}function x(e){return e&&e.nodeType===1&&y(e)}function H(e){let n={};for(let d of e){let a=x(d);a&&(n[a]=d)}return n}function K(e,n,d){if(!b(_,n)){let a=n.cloneNode(!0);return e.insertBefore(a,d),A(a),a}return n}return V(o),g=u,h=typeof l=="string"?U(l):l,window.Alpine&&window.Alpine.closestDataStack&&!u._x_dataStack&&(h._x_dataStack=window.Alpine.closestDataStack(u),h._x_dataStack&&window.Alpine.cloneNode(u,h)),D(u,h),g=void 0,h=void 0,u}k.step=()=>{};k.log=()=>{};function b(u,...l){let o=!1;return u(...l,()=>o=!0),o}var F=!1;function U(u){let l=document.createElement("template");return l.innerHTML=u,l.content.firstElementChild}function X(u){return u.nodeType===3||u.nodeType===8}var w=class{constructor(l,o){this.startComment=l,this.endComment=o}get children(){let l=[],o=this.startComment.nextSibling;for(;o&&o!==this.endComment;)l.push(o),o=o.nextSibling;return l}appendChild(l){this.endComment.before(l)}get firstChild(){let l=this.startComment.nextSibling;if(l!==this.endComment)return l}nextNode(l){let o=l.nextSibling;if(o!==this.endComment)return o}insertBefore(l,o){return o.before(l),l}};function I(u){return u.firstChild}function c(u,l){let o;return u instanceof w?o=u.nextNode(l):o=l.nextSibling,o}function Y(){if(F)return;F=!0;let u=Element.prototype.setAttribute,l=document.createElement("div");Element.prototype.setAttribute=function(g,h){if(!g.includes("@"))return u.call(this,g,h);l.innerHTML=`<span ${g}="${h}"></span>`;let y=l.firstElementChild.getAttributeNode(g);l.firstElementChild.removeAttributeNode(y),this.setAttributeNode(y)}}function Z(u,l){let o=l&&l._x_bindings&&l._x_bindings.id;o&&(u.setAttribute("id",o),u.id=o)}function M(u){u.morph=k}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(M)});})();
|
16
public/vendor/htmx-ext/alpine-morph@1.9.12.js
vendored
Normal file
16
public/vendor/htmx-ext/alpine-morph@1.9.12.js
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
htmx.defineExtension('alpine-morph', {
|
||||
isInlineSwap: function (swapStyle) {
|
||||
return swapStyle === 'morph';
|
||||
},
|
||||
handleSwap: function (swapStyle, target, fragment) {
|
||||
if (swapStyle === 'morph') {
|
||||
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||||
Alpine.morph(target, fragment.firstElementChild);
|
||||
return [target];
|
||||
} else {
|
||||
Alpine.morph(target, fragment.outerHTML);
|
||||
return [target];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
141
public/vendor/htmx-ext/head-support@1.9.12.js
vendored
Normal file
141
public/vendor/htmx-ext/head-support@1.9.12.js
vendored
Normal file
|
@ -0,0 +1,141 @@
|
|||
//==========================================================
|
||||
// head-support.js
|
||||
//
|
||||
// An extension to htmx 1.0 to add head tag merging.
|
||||
//==========================================================
|
||||
(function(){
|
||||
|
||||
var api = null;
|
||||
|
||||
function log() {
|
||||
//console.log(arguments);
|
||||
}
|
||||
|
||||
function mergeHead(newContent, defaultMergeStrategy) {
|
||||
|
||||
if (newContent && newContent.indexOf('<head') > -1) {
|
||||
const htmlDoc = document.createElement("html");
|
||||
// remove svgs to avoid conflicts
|
||||
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
||||
// extract head tag
|
||||
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
|
||||
|
||||
// if the head tag exists...
|
||||
if (headTag) {
|
||||
|
||||
var added = []
|
||||
var removed = []
|
||||
var preserved = []
|
||||
var nodesToAppend = []
|
||||
|
||||
htmlDoc.innerHTML = headTag;
|
||||
var newHeadTag = htmlDoc.querySelector("head");
|
||||
var currentHead = document.head;
|
||||
|
||||
if (newHeadTag == null) {
|
||||
return;
|
||||
} else {
|
||||
// put all new head elements into a Map, by their outerHTML
|
||||
var srcToNewHeadNodes = new Map();
|
||||
for (const newHeadChild of newHeadTag.children) {
|
||||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// determine merge strategy
|
||||
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
|
||||
|
||||
// get the current head
|
||||
for (const currentHeadElt of currentHead.children) {
|
||||
|
||||
// If the current head element is in the map
|
||||
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
||||
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
|
||||
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
|
||||
if (inNewContent || isPreserved) {
|
||||
if (isReAppended) {
|
||||
// remove the current version and let the new version replace it and re-execute
|
||||
removed.push(currentHeadElt);
|
||||
} else {
|
||||
// this element already exists and should not be re-appended, so remove it from
|
||||
// the new content map, preserving it in the DOM
|
||||
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
||||
preserved.push(currentHeadElt);
|
||||
}
|
||||
} else {
|
||||
if (mergeStrategy === "append") {
|
||||
// we are appending and this existing element is not new content
|
||||
// so if and only if it is marked for re-append do we do anything
|
||||
if (isReAppended) {
|
||||
removed.push(currentHeadElt);
|
||||
nodesToAppend.push(currentHeadElt);
|
||||
}
|
||||
} else {
|
||||
// if this is a merge, we remove this content since it is not in the new head
|
||||
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
|
||||
removed.push(currentHeadElt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push the tremaining new head elements in the Map into the
|
||||
// nodes to append to the head tag
|
||||
nodesToAppend.push(...srcToNewHeadNodes.values());
|
||||
log("to append: ", nodesToAppend);
|
||||
|
||||
for (const newNode of nodesToAppend) {
|
||||
log("adding: ", newNode);
|
||||
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
|
||||
log(newElt);
|
||||
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
|
||||
currentHead.appendChild(newElt);
|
||||
added.push(newElt);
|
||||
}
|
||||
}
|
||||
|
||||
// remove all removed elements, after we have appended the new elements to avoid
|
||||
// additional network requests for things like style sheets
|
||||
for (const removedElement of removed) {
|
||||
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
|
||||
currentHead.removeChild(removedElement);
|
||||
}
|
||||
}
|
||||
|
||||
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
htmx.defineExtension("head-support", {
|
||||
init: function(apiRef) {
|
||||
// store a reference to the internal API.
|
||||
api = apiRef;
|
||||
|
||||
htmx.on('htmx:afterSwap', function(evt){
|
||||
var serverResponse = evt.detail.xhr.response;
|
||||
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
|
||||
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
|
||||
}
|
||||
})
|
||||
|
||||
htmx.on('htmx:historyRestore', function(evt){
|
||||
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
|
||||
if (evt.detail.cacheMiss) {
|
||||
mergeHead(evt.detail.serverResponse, "merge");
|
||||
} else {
|
||||
mergeHead(evt.detail.item.head, "merge");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
htmx.on('htmx:historyItemCreated', function(evt){
|
||||
var historyItem = evt.detail.item;
|
||||
historyItem.head = document.head.outerHTML;
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
})()
|
|
@ -11,10 +11,14 @@
|
|||
|
||||
(pi:define-element page ()
|
||||
(pi:h
|
||||
(section :data-css "pages/about"
|
||||
(section (view:asset-props :style "pages/about"
|
||||
:script "pages/about"
|
||||
:x-data "aboutPageState")
|
||||
(h1 "About")
|
||||
(a :href "/"
|
||||
"top"))))
|
||||
(a :href "/" :hx-boost "true"
|
||||
"top")
|
||||
(button :@click "decrement()"
|
||||
(span :x-text "count")))))
|
||||
|
||||
(defun handle-get (params)
|
||||
(declare (ignore params))
|
||||
|
|
|
@ -7,13 +7,18 @@
|
|||
|
||||
(pi:define-element page ()
|
||||
(pi:h
|
||||
(section :data-css "pages/index"
|
||||
(section (view:asset-props :style "pages/index"
|
||||
:script "pages/index"
|
||||
:x-data "indexPageState")
|
||||
(h1
|
||||
"Hello, World!")
|
||||
(a :href "/about"
|
||||
(a :href "/about" :hx-boost "true"
|
||||
"About")
|
||||
(button :x-data t :@click "$store.darkMode.toggle()"
|
||||
"Toggle theme"))))
|
||||
"Toggle theme")
|
||||
(button
|
||||
:@click "increment()"
|
||||
(span :x-text "count")))))
|
||||
|
||||
(defun handle-get (params)
|
||||
(declare (ignore params))
|
||||
|
|
9
src/scripts/pages/about.js
Normal file
9
src/scripts/pages/about.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export function aboutPageState() {
|
||||
return {
|
||||
count: 0,
|
||||
|
||||
decrement() {
|
||||
this.count--
|
||||
}
|
||||
}
|
||||
}
|
9
src/scripts/pages/index.js
Normal file
9
src/scripts/pages/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export function indexPageState() {
|
||||
return {
|
||||
count: 0,
|
||||
|
||||
increment() {
|
||||
this.count++
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@scope ([data-css='pages/about']) {
|
||||
@scope ([data-style='pages/about']) {
|
||||
:scope {
|
||||
height: 100svh;
|
||||
display: grid;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@scope ([data-css='pages/index']) {
|
||||
@scope ([data-style='pages/index']) {
|
||||
:scope {
|
||||
height: 100svh;
|
||||
display: grid;
|
||||
|
|
31
src/view/asset.lisp
Normal file
31
src/view/asset.lisp
Normal file
|
@ -0,0 +1,31 @@
|
|||
(defpackage #:hp/view/asset
|
||||
(:use #:cl)
|
||||
(:local-nicknames (#:re #:cl-ppcre))
|
||||
(:export #:get-css-links
|
||||
#:asset-props))
|
||||
(in-package #:hp/view/asset)
|
||||
|
||||
(defun detect-data-props (html-str data-prop-name)
|
||||
(remove-duplicates (re:all-matches-as-strings (format nil
|
||||
"(?<=~a=\")[^\"]*(?=\")"
|
||||
data-prop-name)
|
||||
html-str)
|
||||
:test #'string=))
|
||||
|
||||
(defun data-props->asset-links (parent-path extension data-props)
|
||||
(mapcar (lambda (data-prop)
|
||||
(concatenate 'string parent-path data-prop extension))
|
||||
data-props))
|
||||
|
||||
(defun get-css-links (html-str)
|
||||
(data-props->asset-links "/styles/"
|
||||
".css"
|
||||
(detect-data-props html-str "data-style")))
|
||||
|
||||
(defun asset-props (&key style script x-data)
|
||||
(append (and style `(:data-style ,style))
|
||||
(and script x-data
|
||||
`(:ax-load t
|
||||
:ax-load-src ,(format nil "/scripts/~a.js" script)
|
||||
:x-ignore t
|
||||
:x-data ,x-data))))
|
|
@ -1,55 +1,19 @@
|
|||
(defpackage #:hp/view/components/document
|
||||
(:use #:cl)
|
||||
(:local-nicknames (#:re #:cl-ppcre))
|
||||
(:local-nicknames (#:pi #:piccolo))
|
||||
(:export #:document))
|
||||
(:local-nicknames (#:asset #:hp/view/asset))
|
||||
(:export #:document
|
||||
#:partial-document))
|
||||
(in-package #:hp/view/components/document)
|
||||
|
||||
(defun detect-data-props (html-str data-prop-name)
|
||||
(remove-duplicates (re:all-matches-as-strings (format nil
|
||||
"(?<=~a=\")[^\"]*(?=\")"
|
||||
data-prop-name)
|
||||
html-str)
|
||||
:test #'string=))
|
||||
|
||||
(defun data-props->asset-links (parent-path extension data-props)
|
||||
(mapcar (lambda (data-prop)
|
||||
(concatenate 'string parent-path data-prop extension))
|
||||
data-props))
|
||||
|
||||
(defun get-css-links (html-str)
|
||||
(data-props->asset-links "/styles/"
|
||||
".css"
|
||||
(detect-data-props html-str "data-css")))
|
||||
|
||||
(defun get-js-links (html-str)
|
||||
(data-props->asset-links "/scripts/"
|
||||
".js"
|
||||
(detect-data-props html-str "data-js")))
|
||||
|
||||
(pi:define-element scripts (srcs)
|
||||
(pi:h
|
||||
(<>
|
||||
(mapcar (lambda (src)
|
||||
(script :src src :defer t))
|
||||
srcs))))
|
||||
|
||||
(pi:define-element stylesheets (hrefs)
|
||||
(pi:h
|
||||
(<>
|
||||
(mapcar (lambda (href)
|
||||
(link :rel "stylesheet" :type "text/css" :href href))
|
||||
hrefs))))
|
||||
|
||||
(pi:define-element on-demand-assets (component)
|
||||
(let* ((pi:*escape-html* nil)
|
||||
(html-str (pi:elem-str component))
|
||||
(css-links (get-css-links html-str))
|
||||
(js-links (get-js-links html-str)))
|
||||
(pi:define-element on-demand-stylesheets ()
|
||||
(let* ((html-str (pi:elem-str pi:children))
|
||||
(css-links (asset:get-css-links html-str)))
|
||||
(pi:h
|
||||
(<>
|
||||
(stylesheets :hrefs css-links)
|
||||
(scripts :srcs js-links)))))
|
||||
(mapcar (lambda (href)
|
||||
(link :rel "stylesheet" :type "text/css" :href href))
|
||||
css-links)))))
|
||||
|
||||
(pi:define-element document (title description)
|
||||
(pi:h
|
||||
|
@ -58,18 +22,29 @@
|
|||
(meta :charset "UTF-8")
|
||||
(link :rel "stylesheet" :type "text/css" :href "/vendor/ress.css")
|
||||
(link :rel "stylesheet" :type "text/css" :href "/styles/global.css")
|
||||
(on-demand-stylesheets pi:children)
|
||||
(link :rel "preconnect" :href "https://fonts.googleapis.com")
|
||||
(link :rel "preconnect" :href "https://fonts.gstatic.com" :crossorigin t)
|
||||
(link
|
||||
:rel "stylesheet"
|
||||
:href "https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap")
|
||||
(script :src "/vendor/htmx@1.9.12.js")
|
||||
(script :src "/vendor/htmx-ext/alpine-morph@1.9.12.js")
|
||||
(script :src "/vendor/htmx-ext/head-support@1.9.12.js")
|
||||
(script :src "/vendor/alpine-ext/async-alpine@1.2.2.js" :defer t)
|
||||
(script :src "/vendor/alpine-ext/persist@3.13.8.js" :defer t)
|
||||
(script :src "/vendor/alpine-ext/morph@3.13.8.js" :defer t)
|
||||
(script :src "/scripts/global.js" :defer t)
|
||||
(on-demand-assets :component pi:children)
|
||||
(script :src "/vendor/alpine@3.13.8.js" :defer t)
|
||||
(title (format nil "~@[~a - ~]skyizwhite.dev" title))
|
||||
(meta
|
||||
:name "description"
|
||||
:content (or description "pakuの個人サイト")))
|
||||
pi:children)))
|
||||
|
||||
(pi:define-element partial-document ()
|
||||
(pi:h
|
||||
(<>
|
||||
(head :hx-head "append"
|
||||
(on-demand-stylesheets pi:children))
|
||||
pi:children)))
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(pi:define-element layout ()
|
||||
(pi:h
|
||||
(body
|
||||
:hx-ext "head-support,alpine-morph"
|
||||
:x-data t
|
||||
:|:data-dark| "$store.darkMode.on"
|
||||
; header
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
(defun partial-render (component &key status)
|
||||
(jg:with-html-response
|
||||
(if status (jg:set-response-status status))
|
||||
(pi:elem-str component)))
|
||||
(pi:elem-str (cmp:partial-document component))))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue