1// log htmx on dev 2// htmx.logAll(); 3 4// add text/html accept header to receive html instead of json for the requests 5document.body.addEventListener("htmx:configRequest", function(evt) { 6 evt.detail.headers["Accept"] = "text/html,*/*"; 7}); 8 9// redirect to homepage 10document.body.addEventListener("redirectToHomepage", function() { 11 setTimeout(() => { 12 window.location.replace("/"); 13 }, 1500); 14}); 15 16// reset form if event is sent from the backend 17function resetForm(id) { 18 return function() { 19 const form = document.getElementById(id); 20 if (!form) return; 21 form.reset(); 22 } 23} 24document.body.addEventListener("resetChangePasswordForm", resetForm("change-password")); 25document.body.addEventListener("resetChangeEmailForm", resetForm("change-email")); 26 27// an htmx extension to use the specifed params in the path instead of the query or body 28htmx.defineExtension("path-params", { 29 onEvent: function(name, evt) { 30 if (name === "htmx:configRequest") { 31 evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function(_, param) { 32 var val = evt.detail.parameters[param] 33 delete evt.detail.parameters[param] 34 return val === undefined ? "{" + param + "}" : encodeURIComponent(val) 35 }) 36 } 37 } 38}) 39 40// find closest element 41function closest(selector, elm) { 42 let element = elm || this; 43 44 while (element && element.nodeType === 1) { 45 if (element.matches(selector)) { 46 return element; 47 } 48 49 element = element.parentNode; 50 } 51 52 return null; 53}; 54 55// get url query param 56function getQueryParams() { 57 const search = window.location.search.replace("?", ""); 58 const query = {}; 59 search.split("&").map(q => { 60 const keyvalue = q.split("="); 61 query[keyvalue[0]] = keyvalue[1]; 62 }); 63 return query; 64} 65 66// trim text 67function trimText(selector, length) { 68 const element = document.querySelector(selector); 69 if (!element) return; 70 let text = element.textContent; 71 if (typeof text !== "string") return; 72 text = text.trim(); 73 if (text.length > length) { 74 element.textContent = text.split("").slice(0, length).join("") + "..."; 75 } 76} 77 78function formatDateHour(selector) { 79 const element = document.querySelector(selector); 80 if (!element) return; 81 const dateString = element.dataset.date; 82 if (!dateString) return; 83 const date = new Date(dateString); 84 element.textContent = date.getHours() + ":" + date.getMinutes(); 85} 86 87// show QR code 88function handleQRCode(element, id) { 89 const dialog = document.getElementById(id); 90 const dialogContent = dialog.querySelector(".content-wrapper"); 91 if (!dialogContent) return; 92 openDialog(id, "qrcode"); 93 dialogContent.textContent = ""; 94 const qrcode = new QRCode(dialogContent, { 95 text: element.dataset.url, 96 width: 200, 97 height: 200, 98 colorDark : "#000000", 99 colorLight : "#ffffff", 100 correctLevel : QRCode.CorrectLevel.H 101 }); 102} 103 104// copy the link to clipboard 105function handleCopyLink(element) { 106 navigator.clipboard.writeText(element.dataset.url); 107} 108 109// copy the link and toggle copy button style 110function handleShortURLCopyLink(element) { 111 handleCopyLink(element); 112 const clipboard = element.parentNode.querySelector(".clipboard") || closest(".clipboard", element); 113 if (!clipboard || clipboard.classList.contains("copied")) return; 114 clipboard.classList.add("copied"); 115 setTimeout(function() { 116 clipboard.classList.remove("copied"); 117 }, 1000); 118} 119 120// open and close dialog 121function openDialog(id, name) { 122 const dialog = document.getElementById(id); 123 if (!dialog) return; 124 dialog.classList.add("open"); 125 if (name) { 126 dialog.classList.add(name); 127 } 128} 129 130function closeDialog() { 131 const dialog = document.querySelector(".dialog"); 132 if (!dialog) return; 133 while (dialog.classList.length > 0) { 134 dialog.classList.remove(dialog.classList[0]); 135 } 136 dialog.classList.add("dialog"); 137} 138 139window.addEventListener("click", function(event) { 140 const dialog = document.querySelector(".dialog"); 141 if (dialog && event.target === dialog) { 142 closeDialog(); 143 } 144}); 145 146// handle navigation in the table of links 147function setLinksLimit(event) { 148 const buttons = Array.from(document.querySelectorAll("table .nav .limit button")); 149 const limitInput = document.querySelector("#limit"); 150 if (!limitInput || !buttons || !buttons.length) return; 151 limitInput.value = event.target.textContent; 152 buttons.forEach(b => { 153 b.disabled = b.textContent === event.target.textContent; 154 }); 155} 156 157function setLinksSkip(event, action) { 158 const buttons = Array.from(document.querySelectorAll("table .nav .pagination button")); 159 const limitElm = document.querySelector("#limit"); 160 const totalElm = document.querySelector("#total"); 161 const skipElm = document.querySelector("#skip"); 162 if (!buttons || !limitElm || !totalElm || !skipElm) return; 163 const skip = parseInt(skipElm.value); 164 const limit = parseInt(limitElm.value); 165 const total = parseInt(totalElm.value); 166 skipElm.value = action === "next" ? skip + limit : Math.max(skip - limit, 0); 167 document.querySelectorAll(".pagination .next").forEach(elm => { 168 elm.disabled = total <= parseInt(skipElm.value) + limit; 169 }); 170 document.querySelectorAll(".pagination .prev").forEach(elm => { 171 elm.disabled = parseInt(skipElm.value) <= 0; 172 }); 173} 174 175function updateLinksNav() { 176 const totalElm = document.querySelector("#total"); 177 const skipElm = document.querySelector("#skip"); 178 const limitElm = document.querySelector("#limit"); 179 if (!totalElm || !skipElm || !limitElm) return; 180 const total = parseInt(totalElm.value); 181 const skip = parseInt(skipElm.value); 182 const limit = parseInt(limitElm.value); 183 document.querySelectorAll(".pagination .next").forEach(elm => { 184 elm.disabled = total <= skip + limit; 185 }); 186 document.querySelectorAll(".pagination .prev").forEach(elm => { 187 elm.disabled = skip <= 0; 188 }); 189} 190 191function resetTableNav() { 192 const totalElm = document.querySelector("#total"); 193 const skipElm = document.querySelector("#skip"); 194 const limitElm = document.querySelector("#limit"); 195 if (!totalElm || !skipElm || !limitElm) return; 196 skipElm.value = 0; 197 limitElm.value = 10; 198 const total = parseInt(totalElm.value); 199 const skip = parseInt(skipElm.value); 200 const limit = parseInt(limitElm.value); 201 document.querySelectorAll(".pagination .next").forEach(elm => { 202 elm.disabled = total <= skip + limit; 203 }); 204 document.querySelectorAll(".pagination .prev").forEach(elm => { 205 elm.disabled = skip <= 0; 206 }); 207 document.querySelectorAll("table .nav .limit button").forEach(b => { 208 b.disabled = b.textContent === limit.toString(); 209 }); 210} 211 212// tab click 213function setTab(event, targetId) { 214 const tabs = Array.from(closest("nav", event.target).children);
215 tabs.forEach(function (tab) { 216 tab.classList.remove("active"); 217 }); 218 if (targetId) { 219 document.getElementById(targetId).classList.add("active"); 220 } else { 221 event.target.classList.add("active"); 222 } 223} 224 225// show clear search button 226function onSearchChange(event) { 227 const clearButton = event.target.parentElement.querySelector("button.clear"); 228 if (!clearButton) return; 229 clearButton.style.display = event.target.value.length > 0 ? "block" : "none"; 230} 231 232function clearSeachInput(event) { 233 event.preventDefault(); 234 const button = closest("button", event.target); 235 const input = button.parentElement.querySelector("input"); 236 if (!input) return; 237 input.value = ""; 238 button.style.display = "none"; 239 htmx.trigger("body", "reloadMainTable"); 240} 241 242// detect if search inputs have value on load to show clear button 243function onSearchInputLoad() { 244 const linkSearchInput = document.getElementById("search"); 245 if (!linkSearchInput) return; 246 const linkClearButton = linkSearchInput.parentElement.querySelector("button.clear") 247 linkClearButton.style.display = linkSearchInput.value.length > 0 ? "block" : "none"; 248 249 const userSearchInput = document.getElementById("search_user"); 250 if (!userSearchInput) return; 251 const userClearButton = userSearchInput.parentElement.querySelector("button.clear") 252 userClearButton.style.display = userSearchInput.value.length > 0 ? "block" : "none"; 253 254 const domainSearchInput = document.getElementById("search_domain"); 255 if (!domainSearchInput) return; 256 const domainClearButton = domainSearchInput.parentElement.querySelector("button.clear") 257 domainClearButton.style.display = domainSearchInput.value.length > 0 ? "block" : "none"; 258} 259 260onSearchInputLoad(); 261 262// create user checkbox control 263function canSendVerificationEmail() { 264 const canSendVerificationEmail = !document.getElementById("create-user-verified").checked && !document.getElementById("create-user-banned").checked; 265 const checkbox = document.getElementById("send-email-label"); 266 if (canSendVerificationEmail) 267 checkbox.classList.remove("hidden"); 268 if (!canSendVerificationEmail && !checkbox.classList.contains("hidden")) 269 checkbox.classList.add("hidden"); 270} 271 272// htmx prefetch extension 273// https://github.com/bigskysoftware/htmx-extensions/blob/main/src/preload/README.md 274htmx.defineExtension("preload", { 275 onEvent: function(name, event) { 276 if (name !== "htmx:afterProcessNode") { 277 return 278 } 279 var attr = function(node, property) { 280 if (node == undefined) { return undefined } 281 return node.getAttribute(property) || node.getAttribute("data-" + property) || attr(node.parentElement, property) 282 } 283 var load = function(node) { 284 var done = function(html) { 285 if (!node.preloadAlways) { 286 node.preloadState = "DONE" 287 } 288 289 if (attr(node, "preload-images") == "true") { 290 document.createElement("div").innerHTML = html 291 } 292 } 293 294 return function() { 295 if (node.preloadState !== "READY") { 296 return 297 } 298 var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get") 299 if (hxGet) { 300 htmx.ajax("GET", hxGet, { 301 source: node, 302 handler: function(elt, info) { 303 done(info.xhr.responseText) 304 } 305 }) 306 return 307 } 308 if (node.getAttribute("href")) { 309 var r = new XMLHttpRequest() 310 r.open("GET", node.getAttribute("href")) 311 r.onload = function() { done(r.responseText) } 312 r.send() 313 } 314 } 315 } 316 var init = function(node) { 317 if (node.getAttribute("href") + node.getAttribute("hx-get") + node.getAttribute("data-hx-get") == "") { 318 return 319 } 320 if (node.preloadState !== undefined) { 321 return 322 } 323 var on = attr(node, "preload") || "mousedown" 324 const always = on.indexOf("always") !== -1 325 if (always) { 326 on = on.replace("always", "").trim() 327 } 328 node.addEventListener(on, function(evt) { 329 if (node.preloadState === "PAUSE") { 330 node.preloadState = "READY" 331 if (on === "mouseover") { 332 window.setTimeout(load(node), 100) 333 } else { 334 load(node)() 335 } 336 } 337 }) 338 switch (on) { 339 case "mouseover": 340 node.addEventListener("touchstart", load(node)) 341 node.addEventListener("mouseout", function(evt) { 342 if ((evt.target === node) && (node.preloadState === "READY")) { 343 node.preloadState = "PAUSE" 344 } 345 }) 346 break 347 348 case "mousedown": 349 node.addEventListener("touchstart", load(node)) 350 break 351 } 352 node.preloadState = "PAUSE" 353 node.preloadAlways = always 354 htmx.trigger(node, "preload:init") 355 } 356 const parent = event.target || event.detail.elt; 357 parent.querySelectorAll("[preload]").forEach(function(node) { 358 init(node) 359 node.querySelectorAll("a,[hx-get],[data-hx-get]").forEach(init) 360 }) 361 } 362})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.