From c1ff03c4e5cdfac505844acad152508c926a2184 Mon Sep 17 00:00:00 2001 From: ari melody Date: Fri, 22 Mar 2024 07:30:22 +0000 Subject: [PATCH] broken but cool htmx! also improved templating Signed-off-by: ari melody --- main.go | 40 ++++--- public/script/header.js | 8 +- public/script/lib/htmx-head-support.js | 141 +++++++++++++++++++++++++ public/script/main.js | 14 +++ public/script/music-gateway.js | 4 +- public/script/music.js | 16 +-- public/style/main.css | 27 +++++ public/style/music-gateway.css | 3 +- public/style/music.css | 9 +- public/style/prideflag.css | 25 +++++ views/base.html | 26 ++--- views/footer.html | 2 +- views/header.html | 6 +- views/htmx-base.html | 33 ++---- views/index.html | 1 - views/music-gateway.html | 4 +- views/music.html | 14 +-- views/prideflag.html | 21 ++++ 18 files changed, 297 insertions(+), 97 deletions(-) create mode 100644 public/script/lib/htmx-head-support.js create mode 100644 public/style/prideflag.css create mode 100644 views/prideflag.html diff --git a/main.go b/main.go index 12377bc..21822a4 100644 --- a/main.go +++ b/main.go @@ -1,20 +1,21 @@ package main import ( - "fmt" - "html/template" - "log" - "net/http" - "os" - "strconv" - "strings" - "time" + "fmt" + "html/template" + "log" + "net/http" + "os" + "regexp" + "strconv" + "strings" + "time" - "arimelody.me/arimelody.me/api/v1/music" + "arimelody.me/arimelody.me/api/v1/music" - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" ) const PORT int = 8080 @@ -34,6 +35,7 @@ var base_template = template.Must(template.ParseFiles( "views/base.html", "views/header.html", "views/footer.html", + "views/prideflag.html", )) var htmx_template = template.Must(template.New("root").Parse(`{{block "head" .}}{{end}}{{block "content" .}}{{end}}`)) @@ -59,11 +61,21 @@ func handle_request(writer http.ResponseWriter, req *http.Request) { uri := req.URL.Path start_time := time.Now() - hx_boosted := len(req.Header["Hx-Boosted"]) > 0 && req.Header["Hx-Boosted"][0] == "true" + hx_request := len(req.Header["Hx-Request"]) > 0 && req.Header["Hx-Request"][0] == "true" + + // don't bother fulfilling requests to a page that's already loaded on the client! + if hx_request && len(req.Header["Referer"]) > 0 && len(req.Header["Hx-Current-Url"]) > 0 { + regex := regexp.MustCompile(`https?:\/\/[^\/]+`) + current_location := regex.ReplaceAllString(req.Header["Hx-Current-Url"][0], "") + if current_location == req.URL.Path { + writer.WriteHeader(204); + return + } + } code := func(writer http.ResponseWriter, req *http.Request) int { var root *template.Template - if hx_boosted { + if hx_request { root = template.Must(htmx_template.Clone()) } else { root = template.Must(base_template.Clone()) diff --git a/public/script/header.js b/public/script/header.js index 4db08c6..4d939c9 100644 --- a/public/script/header.js +++ b/public/script/header.js @@ -1,4 +1,3 @@ -const header_home = document.getElementById("header-home"); const header_links = document.getElementById("header-links"); const hamburger = document.getElementById("header-links-toggle"); @@ -7,14 +6,9 @@ function toggle_header_links() { } document.addEventListener("click", event => { - if (!header_links.contains(event.target) && !hamburger.contains(event.target)) { + if (!header_links.contains(event.target) && !hamburger.contains(event.target) && !header_links.href) { header_links.classList.remove("open"); } }); hamburger.addEventListener("click", event => { toggle_header_links(); }); - -header_home.addEventListener("click", event => { - event.preventDefault(); - location.href = "/"; -}); diff --git a/public/script/lib/htmx-head-support.js b/public/script/lib/htmx-head-support.js new file mode 100644 index 0000000..67cfc69 --- /dev/null +++ b/public/script/lib/htmx-head-support.js @@ -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(' -1) { + const htmlDoc = document.createElement("html"); + // remove svgs to avoid conflicts + var contentWithSvgsRemoved = newContent.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, ''); + // extract head tag + var headTag = contentWithSvgsRemoved.match(/(]*>|>)([\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; + }) + } + }); + +})() \ No newline at end of file diff --git a/public/script/main.js b/public/script/main.js index 0553eb8..2d22447 100644 --- a/public/script/main.js +++ b/public/script/main.js @@ -63,3 +63,17 @@ document.addEventListener("htmx:afterSwap", async event => { } window.scrollY = 0; }); + +const top_button = document.getElementById("backtotop"); +window.onscroll = () => { + if (!top_button) return; + const btt_threshold = 100; + if ( + document.body.scrollTop > btt_threshold || + document.documentElement.scrollTop > btt_threshold + ) { + top_button.classList.add("active"); + } else { + top_button.classList.remove("active"); + } +} diff --git a/public/script/music-gateway.js b/public/script/music-gateway.js index 1b19a4f..8b5155f 100644 --- a/public/script/music-gateway.js +++ b/public/script/music-gateway.js @@ -12,7 +12,9 @@ share_btn.onclick = (e) => { share_btn.classList.add('active'); } -document.getElementById("go-back").addEventListener("click", () => { +const go_back_btn = document.getElementById("go-back") +go_back_btn.innerText = "<"; +go_back_btn.addEventListener("click", () => { window.history.back(); }); diff --git a/public/script/music.js b/public/script/music.js index 250203f..7e05eb7 100644 --- a/public/script/music.js +++ b/public/script/music.js @@ -1,15 +1,5 @@ -document.querySelectorAll("h2.question").forEach(element => { - element.onclick = (e) => { - const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}#${e.target.id}`; - window.location = url; - }; -}); +import "./main.js"; -document.querySelectorAll("div.music").forEach(element => { - console.log(element); - element.addEventListener("click", (e) => { - const url = `${window.location.protocol}//${window.location.host}/music/${element.id}`; - window.location = url; - }); +document.querySelectorAll("h1.music-title").forEach(element => { + element.href = ""; }); - diff --git a/public/style/main.css b/public/style/main.css index 9b913e6..52093bd 100644 --- a/public/style/main.css +++ b/public/style/main.css @@ -1,6 +1,7 @@ @import url("/style/colours.css"); @import url("/style/header.css"); @import url("/style/footer.css"); +@import url("/style/prideflag.css"); @font-face { font-family: "Monaspace Argon"; @@ -42,6 +43,32 @@ span.newchar { animation: newchar 0.25s; } +a#backtotop { + position: fixed; + left: 50%; + transform: translateX(-50%); + padding: .5em .8em; + display: block; + border-radius: 2px; + border: 1px solid transparent; + text-decoration: none; + opacity: .5; + transition-property: opacity, transform, border-color, background-color, color; + transition-duration: .2s; +} + +a#backtotop.active { + top: 4rem; +} + +a#backtotop:hover { + color: #eee; + border-color: #eee; + background-color: var(--links); + box-shadow: 0 0 1em var(--links); + opacity: 1; +} + @keyframes newchar { from { background: #fff8; diff --git a/public/style/music-gateway.css b/public/style/music-gateway.css index 1b11c62..bfed501 100644 --- a/public/style/music-gateway.css +++ b/public/style/music-gateway.css @@ -241,7 +241,7 @@ div#info p { #title { margin: 0; line-height: 1em; - font-size: 3em; + font-size: 2.5em; } #year { @@ -571,6 +571,7 @@ footer a:hover { } div#info > div { + min-width: auto; min-height: auto; padding: 0; margin: 0; diff --git a/public/style/music.css b/public/style/music.css index 8a71143..91d30ba 100644 --- a/public/style/music.css +++ b/public/style/music.css @@ -1,5 +1,12 @@ @import url("/style/index.css"); +main { + width: min(calc(100% - 4rem), 720px); + min-height: calc(100vh - 10.3rem); + margin: 0 auto 2rem auto; + padding-top: 4rem; +} + div.music { margin-bottom: 1rem; padding: 1.5rem; @@ -114,7 +121,7 @@ h2.question { cursor: pointer; } -.collapse { +div.answer { margin: -1rem 0 1rem 0; padding: .5em 1.5em; border-radius: 4px; diff --git a/public/style/prideflag.css b/public/style/prideflag.css new file mode 100644 index 0000000..16f2e25 --- /dev/null +++ b/public/style/prideflag.css @@ -0,0 +1,25 @@ +#prideflag svg { + position: fixed; + top: 0; + right: 0; + width: 120px; + transform-origin: 100% 0%; + transition: transform .5s cubic-bezier(.32,1.63,.41,1.01); + z-index: 8008135; + pointer-events: none; +} +#prideflag svg:hover { + transform: scale(110%); +} +#prideflag svg:active { + transform: scale(110%); +} +#prideflag svg * { + pointer-events: all; +} + +@media screen and (max-width: 950px) { + #prideflag { + display: none; + } +} diff --git a/views/base.html b/views/base.html index fd786a9..e34761f 100644 --- a/views/base.html +++ b/views/base.html @@ -7,28 +7,14 @@ - {{block "head" .}} - - - - - - - - - - - - - - - - {{end}} + {{block "head" .}}{{end}} - + + + - + {{template "header"}} {{block "content" .}} @@ -43,8 +29,8 @@ {{end}} {{template "footer"}} -
+ {{template "prideflag"}} diff --git a/views/footer.html b/views/footer.html index b6817ed..46450d7 100644 --- a/views/footer.html +++ b/views/footer.html @@ -1,6 +1,6 @@ {{define "footer"}} -