diff --git a/internal/web/profile.go b/internal/web/profile.go index e8483921d..dd3ed8e64 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -249,7 +249,7 @@ func (m *Module) profileMicroblog(c *gin.Context, p *profile) { }, { Bottom: true, - Src: jsBlurhash, + Src: jsFrontendPrerender, }, }, Extra: map[string]any{ @@ -323,7 +323,7 @@ func (m *Module) profileGallery(c *gin.Context, p *profile) { }, { Bottom: true, - Src: jsBlurhash, + Src: jsFrontendPrerender, }, }, Extra: map[string]any{ diff --git a/internal/web/thread.go b/internal/web/thread.go index 0b296a75b..d46e52ac7 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -154,7 +154,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { }, { Bottom: true, - Src: jsBlurhash, + Src: jsFrontendPrerender, }, }, Extra: map[string]any{ diff --git a/internal/web/web.go b/internal/web/web.go index 15814942a..5bccca06d 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -67,9 +67,9 @@ const ( cssSettings = distPathPrefix + "/settings-style.css" cssTag = distPathPrefix + "/tag.css" - jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS. - jsBlurhash = distPathPrefix + "/blurhash.js" // Blurhash rendering JS. - jsSettings = distPathPrefix + "/settings.js" // Settings panel React application. + jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS. + jsFrontendPrerender = distPathPrefix + "/frontend_prerender.js" // Frontend JS that should run before page renders. + jsSettings = distPathPrefix + "/settings.js" // Settings panel React application. ) type Module struct { diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js index da158ed77..47879b2e2 100644 --- a/web/source/frontend/index.js +++ b/web/source/frontend/index.js @@ -17,6 +17,15 @@ along with this program. If not, see . */ +/* + WHAT SHOULD GO IN THIS FILE? + + This script is loaded in the document head, and deferred + async, + so it's *usually* run after the user is already looking at the page. + Put stuff in here that doesn't shift the layout, and it doesn't really + matter whether it loads immediately. So, progressive enhancement stuff. +*/ + const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js"); const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js"); const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default; @@ -165,89 +174,6 @@ lightbox.on('uiRegister', function() { lightbox.init(); -function dynamicSpoiler(className, updateFunc) { - Array.from(document.getElementsByClassName(className)).forEach((spoiler) => { - const update = updateFunc(spoiler); - if (update) { - update(); - spoiler.addEventListener("toggle", update); - } - }); -} - -dynamicSpoiler("text-spoiler", (details) => { - const summary = details.children[0]; - const button = details.querySelector(".button"); - - // Use button *instead of summary* - // to toggle post visibility. - summary.tabIndex = "-1"; - button.tabIndex = "0"; - button.setAttribute("aria-role", "button"); - button.onclick = (e) => { - e.preventDefault(); - return details.hasAttribute("open") - ? details.removeAttribute("open") - : details.setAttribute("open", ""); - }; - - // Let enter also trigger the button - // (for those using keyboard to navigate). - button.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - button.click(); - } - }); - - // Change button text depending on - // whether spoiler is open or closed rn. - return () => { - button.textContent = details.open - ? "Show less" - : "Show more"; - }; -}); - -dynamicSpoiler("media-spoiler", (details) => { - const summary = details.children[0]; - const button = details.querySelector(".eye.button"); - const video = details.querySelector(".plyr-video"); - const loopingAuto = !reduceMotion.matches && video != null && video.classList.contains("gifv"); - - // Use button *instead of summary* - // to toggle media visibility. - summary.tabIndex = "-1"; - button.tabIndex = "0"; - button.setAttribute("aria-role", "button"); - button.onclick = (e) => { - e.preventDefault(); - return details.hasAttribute("open") - ? details.removeAttribute("open") - : details.setAttribute("open", ""); - }; - - // Let enter also trigger the button - // (for those using keyboard to navigate). - button.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - button.click(); - } - }); - - return () => { - if (details.open) { - button.setAttribute("aria-label", "Hide media"); - } else { - button.setAttribute("aria-label", "Show media"); - if (video && !loopingAuto) { - video.pause(); - } - } - }; -}); - Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => { const loopingAuto = !reduceMotion.matches && video.classList.contains("gifv"); let player = new Plyr(video, { @@ -315,30 +241,6 @@ function inLightbox(element) { lightbox.pswp.currSlide.data.attachmentId; } -// Define + reuse one DateTimeFormat (cheaper). -const dateTimeFormat = Intl.DateTimeFormat( - undefined, - { - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false - }, -); - -// Reformat time text to browser locale. -Array.from(document.getElementsByTagName('time')).forEach(timeTag => { - const datetime = timeTag.getAttribute('datetime'); - const currentText = timeTag.textContent.trim(); - // Only format if current text contains precise time. - if (currentText.match(/\d{2}:\d{2}/)) { - const date = new Date(datetime); - timeTag.textContent = dateTimeFormat.format(date); - } -}); - // When clicking anywhere that's not an open // stats-info-more-content details dropdown, // close that open dropdown. diff --git a/web/source/blurhash/index.js b/web/source/frontend_prerender/index.js similarity index 56% rename from web/source/blurhash/index.js rename to web/source/frontend_prerender/index.js index c964f69c4..294c1ddb1 100644 --- a/web/source/blurhash/index.js +++ b/web/source/frontend_prerender/index.js @@ -17,8 +17,18 @@ along with this program. If not, see . */ +/* + WHAT SHOULD GO IN THIS FILE? + + This script is loaded just before the end of the HTML body, so + put stuff in here that should be run *before* the user sees the page. + So, stuff that shifts the layout or causes elements to jump around. +*/ + import { decode } from "blurhash"; +const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + // Generate a blurhash canvas for each image for // each blurhash container and put it in the summary. Array.from(document.getElementsByClassName('blurhash-container')).forEach(blurhashContainer => { @@ -144,3 +154,110 @@ Array.from(document.getElementsByTagName('img')).forEach(img => { } }); }); + +// Change the spoiler / content warning boxes from generic +// "toggle visibility" to show/hide depending on state, +// and add keyboard functionality to spoiler buttons. +function dynamicSpoiler(className, updateFunc) { + Array.from(document.getElementsByClassName(className)).forEach((spoiler) => { + const update = updateFunc(spoiler); + if (update) { + update(); + spoiler.addEventListener("toggle", update); + } + }); +} +dynamicSpoiler("text-spoiler", (details) => { + const summary = details.children[0]; + const button = details.querySelector(".button"); + + // Use button *instead of summary* + // to toggle post visibility. + summary.tabIndex = "-1"; + button.tabIndex = "0"; + button.setAttribute("aria-role", "button"); + button.onclick = (e) => { + e.preventDefault(); + return details.hasAttribute("open") + ? details.removeAttribute("open") + : details.setAttribute("open", ""); + }; + + // Let enter also trigger the button + // (for those using keyboard to navigate). + button.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + button.click(); + } + }); + + // Change button text depending on + // whether spoiler is open or closed rn. + return () => { + button.textContent = details.open + ? "Show less" + : "Show more"; + }; +}); +dynamicSpoiler("media-spoiler", (details) => { + const summary = details.children[0]; + const button = details.querySelector(".eye.button"); + const video = details.querySelector(".plyr-video"); + const loopingAuto = !reduceMotion.matches && video != null && video.classList.contains("gifv"); + + // Use button *instead of summary* + // to toggle media visibility. + summary.tabIndex = "-1"; + button.tabIndex = "0"; + button.setAttribute("aria-role", "button"); + button.onclick = (e) => { + e.preventDefault(); + return details.hasAttribute("open") + ? details.removeAttribute("open") + : details.setAttribute("open", ""); + }; + + // Let enter also trigger the button + // (for those using keyboard to navigate). + button.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + button.click(); + } + }); + + return () => { + if (details.open) { + button.setAttribute("aria-label", "Hide media"); + } else { + button.setAttribute("aria-label", "Show media"); + if (video && !loopingAuto) { + video.pause(); + } + } + }; +}); + +// Reformat time text to browser locale. +// Define + reuse one DateTimeFormat (cheaper). +const dateTimeFormat = Intl.DateTimeFormat( + undefined, + { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false + }, +); +Array.from(document.getElementsByTagName('time')).forEach(timeTag => { + const datetime = timeTag.getAttribute('datetime'); + const currentText = timeTag.textContent.trim(); + // Only format if current text contains precise time. + if (currentText.match(/\d{2}:\d{2}/)) { + const date = new Date(datetime); + timeTag.textContent = dateTimeFormat.format(date); + } +}); diff --git a/web/source/index.js b/web/source/index.js index c47e9c5bb..d66afe757 100644 --- a/web/source/index.js +++ b/web/source/index.js @@ -64,9 +64,9 @@ skulk({ }] ], }, - blurhash: { - entryFile: "blurhash", - outputFile: "blurhash.js", + frontend_prerender: { + entryFile: "frontend_prerender", + outputFile: "frontend_prerender.js", preset: ["js"], prodCfg: prodCfg, transform: [