main.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. // log htmx on dev
  2. // htmx.logAll();
  3. // add text/html accept header to receive html instead of json for the requests
  4. document.body.addEventListener('htmx:configRequest', function(evt) {
  5. evt.detail.headers["Accept"] = "text/html,*/*";
  6. });
  7. // redirect to homepage
  8. document.body.addEventListener("redirectToHomepage", function() {
  9. setTimeout(() => {
  10. window.location.replace("/");
  11. }, 1500);
  12. });
  13. // reset form if event is sent from the backend
  14. function resetForm(id) {
  15. return function() {
  16. const form = document.getElementById(id);
  17. if (!form) return;
  18. form.reset();
  19. }
  20. }
  21. document.body.addEventListener('resetChangePasswordForm', resetForm("change-password"));
  22. document.body.addEventListener('resetChangeEmailForm', resetForm("change-email"));
  23. // an htmx extension to use the specifed params in the path instead of the query or body
  24. htmx.defineExtension("path-params", {
  25. onEvent: function(name, evt) {
  26. if (name === "htmx:configRequest") {
  27. evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function(_, param) {
  28. var val = evt.detail.parameters[param]
  29. delete evt.detail.parameters[param]
  30. return val === undefined ? '{' + param + '}' : encodeURIComponent(val)
  31. })
  32. }
  33. }
  34. })
  35. // find closest element
  36. function closest(selector, elm) {
  37. let element = elm || this;
  38. while (element && element.nodeType === 1) {
  39. if (element.matches(selector)) {
  40. return element;
  41. }
  42. element = element.parentNode;
  43. }
  44. return null;
  45. };
  46. // get url query param
  47. function getQueryParams() {
  48. const search = window.location.search.replace("?", "");
  49. const query = {};
  50. search.split("&").map(q => {
  51. const keyvalue = q.split("=");
  52. query[keyvalue[0]] = keyvalue[1];
  53. });
  54. return query;
  55. }
  56. // trim text
  57. function trimText(selector, length) {
  58. const element = document.querySelector(selector);
  59. if (!element) return;
  60. let text = element.textContent;
  61. if (typeof text !== "string") return;
  62. text = text.trim();
  63. if (text.length > length) {
  64. element.textContent = text.split("").slice(0, length).join("") + "...";
  65. }
  66. }
  67. function formatDateHour(selector) {
  68. const element = document.querySelector(selector);
  69. if (!element) return;
  70. const dateString = element.dataset.date;
  71. if (!dateString) return;
  72. const date = new Date(dateString);
  73. element.textContent = date.getHours() + ":" + date.getMinutes();
  74. }
  75. // show QR code
  76. function handleQRCode(element) {
  77. const dialog = document.querySelector("#link-dialog");
  78. const dialogContent = dialog.querySelector(".content-wrapper");
  79. if (!dialogContent) return;
  80. openDialog("link-dialog", "qrcode");
  81. dialogContent.textContent = "";
  82. const qrcode = new QRCode(dialogContent, {
  83. text: element.dataset.url,
  84. width: 200,
  85. height: 200,
  86. colorDark : "#000000",
  87. colorLight : "#ffffff",
  88. correctLevel : QRCode.CorrectLevel.H
  89. });
  90. }
  91. // copy the link to clipboard
  92. function handleCopyLink(element) {
  93. navigator.clipboard.writeText(element.dataset.url);
  94. }
  95. // copy the link and toggle copy button style
  96. function handleShortURLCopyLink(element) {
  97. handleCopyLink(element);
  98. const clipboard = element.parentNode.querySelector(".clipboard") || closest(".clipboard", element);
  99. if (!clipboard || clipboard.classList.contains("copied")) return;
  100. clipboard.classList.add("copied");
  101. setTimeout(function() {
  102. clipboard.classList.remove("copied");
  103. }, 1000);
  104. }
  105. // open and close dialog
  106. function openDialog(id, name) {
  107. const dialog = document.getElementById(id);
  108. if (!dialog) return;
  109. dialog.classList.add("open");
  110. if (name) {
  111. dialog.classList.add(name);
  112. }
  113. }
  114. function closeDialog() {
  115. const dialog = document.querySelector(".dialog");
  116. if (!dialog) return;
  117. while (dialog.classList.length > 0) {
  118. dialog.classList.remove(dialog.classList[0]);
  119. }
  120. dialog.classList.add("dialog");
  121. }
  122. window.addEventListener("click", function(event) {
  123. const dialog = document.querySelector(".dialog");
  124. if (dialog && event.target === dialog) {
  125. closeDialog();
  126. }
  127. });
  128. // handle navigation in the table of links
  129. function setLinksLimit(event) {
  130. const buttons = Array.from(document.querySelectorAll('table .nav .limit button'));
  131. const limitInput = document.querySelector('#limit');
  132. if (!limitInput || !buttons || !buttons.length) return;
  133. limitInput.value = event.target.textContent;
  134. buttons.forEach(b => {
  135. b.disabled = b.textContent === event.target.textContent;
  136. });
  137. }
  138. function setLinksSkip(event, action) {
  139. const buttons = Array.from(document.querySelectorAll('table .nav .pagination button'));
  140. const limitElm = document.querySelector('#limit');
  141. const totalElm = document.querySelector('#total');
  142. const skipElm = document.querySelector('#skip');
  143. if (!buttons || !limitElm || !totalElm || !skipElm) return;
  144. const skip = parseInt(skipElm.value);
  145. const limit = parseInt(limitElm.value);
  146. const total = parseInt(totalElm.value);
  147. skipElm.value = action === "next" ? skip + limit : Math.max(skip - limit, 0);
  148. document.querySelectorAll('.pagination .next').forEach(elm => {
  149. elm.disabled = total <= parseInt(skipElm.value) + limit;
  150. });
  151. document.querySelectorAll('.pagination .prev').forEach(elm => {
  152. elm.disabled = parseInt(skipElm.value) <= 0;
  153. });
  154. }
  155. function updateLinksNav() {
  156. const totalElm = document.querySelector('#total');
  157. const skipElm = document.querySelector('#skip');
  158. const limitElm = document.querySelector('#limit');
  159. if (!totalElm || !skipElm || !limitElm) return;
  160. const total = parseInt(totalElm.value);
  161. const skip = parseInt(skipElm.value);
  162. const limit = parseInt(limitElm.value);
  163. document.querySelectorAll('.pagination .next').forEach(elm => {
  164. elm.disabled = total <= skip + limit;
  165. });
  166. document.querySelectorAll('.pagination .prev').forEach(elm => {
  167. elm.disabled = skip <= 0;
  168. });
  169. }
  170. function resetLinkNav() {
  171. const totalElm = document.querySelector('#total');
  172. const skipElm = document.querySelector('#skip');
  173. const limitElm = document.querySelector('#limit');
  174. if (!totalElm || !skipElm || !limitElm) return;
  175. skipElm.value = 0;
  176. limitElm.value = 10;
  177. const skip = parseInt(skipElm.value);
  178. const limit = parseInt(limitElm.value);
  179. document.querySelectorAll('.pagination .next').forEach(elm => {
  180. elm.disabled = total <= skip + limit;
  181. });
  182. document.querySelectorAll('.pagination .prev').forEach(elm => {
  183. elm.disabled = skip <= 0;
  184. });
  185. document.querySelectorAll('table .nav .limit button').forEach(b => {
  186. b.disabled = b.textContent === limit.toString();
  187. });
  188. }
  189. // create views chart label
  190. function createViewsChartLabel(ctx) {
  191. const period = ctx.dataset.period;
  192. let labels = [];
  193. if (period === "day") {
  194. const nowHour = new Date().getHours();
  195. for (let i = 23; i >= 0; --i) {
  196. let h = nowHour - i;
  197. if (h < 0) h = 24 + h;
  198. labels.push(`${Math.floor(h)}:00`);
  199. }
  200. }
  201. if (period === "week") {
  202. const nowDay = new Date().getDate();
  203. for (let i = 6; i >= 0; --i) {
  204. const date = new Date(new Date().setDate(nowDay - i));
  205. labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
  206. }
  207. }
  208. if (period === "month") {
  209. const nowDay = new Date().getDate();
  210. for (let i = 29; i >= 0; --i) {
  211. const date = new Date(new Date().setDate(nowDay - i));
  212. labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
  213. }
  214. }
  215. if (period === "year") {
  216. const nowMonth = new Date().getMonth();
  217. for (let i = 11; i >= 0; --i) {
  218. const date = new Date(new Date().setMonth(nowMonth - i));
  219. labels.push(`${date.toLocaleString("default",{month:"short"})} ${date.toLocaleString("default",{year:"numeric"})}`);
  220. }
  221. }
  222. return labels;
  223. }
  224. // create views chart
  225. function createViewsChart() {
  226. const canvases = document.querySelectorAll("canvas.visits");
  227. if (!canvases || !canvases.length) return;
  228. canvases.forEach(ctx => {
  229. const data = JSON.parse(ctx.dataset.data);
  230. const period = ctx.dataset.period;
  231. const labels = createViewsChartLabel(ctx);
  232. const maxTicksLimitX = period === "year" ? 6 : period === "month" ? 15 : 12;
  233. const gradient = ctx.getContext("2d").createLinearGradient(0, 0, 0, 300);
  234. gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
  235. gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
  236. new Chart(ctx, {
  237. type: "line",
  238. data: {
  239. labels: labels,
  240. datasets: [{
  241. label: "Views",
  242. data,
  243. tension: 0.3,
  244. elements: {
  245. point: {
  246. pointRadius: 0,
  247. pointHoverRadius: 4
  248. }
  249. },
  250. fill: {
  251. target: "start",
  252. },
  253. backgroundColor: gradient,
  254. borderColor: "rgb(179, 157, 219)",
  255. borderWidth: 1,
  256. }]
  257. },
  258. options: {
  259. plugins: {
  260. legend: {
  261. display: false,
  262. },
  263. tooltip: {
  264. backgroundColor: "rgba(255, 255, 255, 0.95)",
  265. titleColor: "#333",
  266. titleFont: { weight: "normal", size: 15 },
  267. bodyFont: { weight: "normal", size: 16 },
  268. bodyColor: "rgb(179, 157, 219)",
  269. padding: 12,
  270. cornerRadius: 2,
  271. borderColor: "rgba(0, 0, 0, 0.1)",
  272. borderWidth: 1,
  273. displayColors: false,
  274. }
  275. },
  276. responsive: true,
  277. interaction: {
  278. intersect: false,
  279. usePointStyle: true,
  280. mode: "index",
  281. },
  282. scales: {
  283. y: {
  284. grace: "10%",
  285. beginAtZero: true,
  286. ticks: {
  287. maxTicksLimit: 5
  288. }
  289. },
  290. x: {
  291. ticks: {
  292. maxTicksLimit: maxTicksLimitX,
  293. }
  294. }
  295. }
  296. }
  297. });
  298. // reset the display: block style that chart.js applies automatically
  299. ctx.style.display = "";
  300. });
  301. }
  302. // beautify browser lables
  303. function beautifyBrowserName(name) {
  304. if (name === "firefox") return "Firefox";
  305. if (name === "chrome") return "Chrome";
  306. if (name === "edge") return "Edge";
  307. if (name === "opera") return "Opera";
  308. if (name === "safari") return "Safari";
  309. if (name === "other") return "Other";
  310. if (name === "ie") return "IE";
  311. return name;
  312. }
  313. // create browsers chart
  314. function createBrowsersChart() {
  315. const canvases = document.querySelectorAll("canvas.browsers");
  316. if (!canvases || !canvases.length) return;
  317. canvases.forEach(ctx => {
  318. const data = JSON.parse(ctx.dataset.data);
  319. const period = ctx.dataset.period;
  320. const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
  321. const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
  322. gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
  323. gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
  324. gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");
  325. gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
  326. new Chart(ctx, {
  327. type: "bar",
  328. data: {
  329. labels: data.map(d => beautifyBrowserName(d.name)),
  330. datasets: [{
  331. label: "Views",
  332. data: data.map(d => d.value),
  333. backgroundColor: gradient,
  334. borderColor: "rgba(179, 157, 219, 1)",
  335. borderWidth: 1,
  336. hoverBackgroundColor: gradientHover,
  337. hoverBorderWidth: 2
  338. }]
  339. },
  340. options: {
  341. indexAxis: "y",
  342. plugins: {
  343. legend: {
  344. display: false,
  345. },
  346. tooltip: {
  347. backgroundColor: "rgba(255, 255, 255, 0.95)",
  348. titleColor: "#333",
  349. titleFont: { weight: "normal", size: 15 },
  350. bodyFont: { weight: "normal", size: 16 },
  351. bodyColor: "rgb(179, 157, 219)",
  352. padding: 12,
  353. cornerRadius: 2,
  354. borderColor: "rgba(0, 0, 0, 0.1)",
  355. borderWidth: 1,
  356. displayColors: false,
  357. }
  358. },
  359. responsive: true,
  360. interaction: {
  361. intersect: false,
  362. mode: "index",
  363. axis: "y"
  364. },
  365. scales: {
  366. x: {
  367. grace: "5%",
  368. beginAtZero: true,
  369. ticks: {
  370. maxTicksLimit: 6,
  371. }
  372. }
  373. }
  374. }
  375. });
  376. // reset the display: block style that chart.js applies automatically
  377. ctx.style.display = "";
  378. });
  379. }
  380. // create referrers chart
  381. function createReferrersChart() {
  382. const canvases = document.querySelectorAll("canvas.referrers");
  383. if (!canvases || !canvases.length) return;
  384. canvases.forEach(ctx => {
  385. const data = JSON.parse(ctx.dataset.data);
  386. const period = ctx.dataset.period;
  387. let max = Array.from(data).sort((a, b) => a.value > b.value ? -1 : 1)[0];
  388. let tooltipEnabled = true;
  389. let hoverBackgroundColor = "rgba(179, 157, 219, 1)";
  390. let hoverBorderWidth = 2;
  391. let borderColor = "rgba(179, 157, 219, 1)";
  392. if (data.length === 0) {
  393. data.push({ name: "No views.", value: 1 });
  394. max = { value: 1000 };
  395. tooltipEnabled = false;
  396. hoverBackgroundColor = "rgba(179, 157, 219, 0.1)";
  397. hoverBorderWidth = 1;
  398. borderColor = "rgba(179, 157, 219, 0.2)";
  399. }
  400. new Chart(ctx, {
  401. type: "doughnut",
  402. data: {
  403. labels: data.map(d => d.name.replace(/\[dot\]/g, ".")),
  404. datasets: [{
  405. label: "Views",
  406. data: data.map(d => d.value),
  407. backgroundColor: data.map(d => `rgba(179, 157, 219, ${Math.max((d.value / max.value) - 0.2, 0.1).toFixed(2)})`),
  408. borderWidth: 1,
  409. borderColor,
  410. hoverBackgroundColor,
  411. hoverBorderWidth,
  412. }]
  413. },
  414. options: {
  415. plugins: {
  416. legend: {
  417. position: "left",
  418. labels: {
  419. boxWidth: 25,
  420. font: { size: 11 }
  421. }
  422. },
  423. tooltip: {
  424. enabled: tooltipEnabled,
  425. backgroundColor: "rgba(255, 255, 255, 0.95)",
  426. titleColor: "#333",
  427. titleFont: { weight: "normal", size: 15 },
  428. bodyFont: { weight: "normal", size: 16 },
  429. bodyColor: "rgb(179, 157, 219)",
  430. padding: 12,
  431. cornerRadius: 2,
  432. borderColor: "rgba(0, 0, 0, 0.1)",
  433. borderWidth: 1,
  434. displayColors: false,
  435. }
  436. },
  437. responsive: false,
  438. }
  439. });
  440. // reset the display: block style that chart.js applies automatically
  441. ctx.style.display = "";
  442. });
  443. }
  444. // beautify browser lables
  445. function beautifyOsName(name) {
  446. if (name === "android") return "Android";
  447. if (name === "ios") return "iOS";
  448. if (name === "linux") return "Linux";
  449. if (name === "macos") return "macOS";
  450. if (name === "windows") return "Windows";
  451. if (name === "other") return "Other";
  452. return name;
  453. }
  454. // create operation systems chart
  455. function createOsChart() {
  456. const canvases = document.querySelectorAll("canvas.os");
  457. if (!canvases || !canvases.length) return;
  458. canvases.forEach(ctx => {
  459. const data = JSON.parse(ctx.dataset.data);
  460. const period = ctx.dataset.period;
  461. const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
  462. const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
  463. gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");
  464. gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
  465. gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");
  466. gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
  467. new Chart(ctx, {
  468. type: "bar",
  469. data: {
  470. labels: data.map(d => beautifyOsName(d.name)),
  471. datasets: [{
  472. label: "Views",
  473. data: data.map(d => d.value),
  474. backgroundColor: gradient,
  475. borderColor: "rgba(179, 157, 219, 1)",
  476. borderWidth: 1,
  477. hoverBackgroundColor: gradientHover,
  478. hoverBorderWidth: 2
  479. }]
  480. },
  481. options: {
  482. indexAxis: "y",
  483. plugins: {
  484. legend: {
  485. display: false,
  486. },
  487. tooltip: {
  488. backgroundColor: "rgba(255, 255, 255, 0.95)",
  489. titleColor: "#333",
  490. titleFont: { weight: "normal", size: 15 },
  491. bodyFont: { weight: "normal", size: 16 },
  492. bodyColor: "rgb(179, 157, 219)",
  493. padding: 12,
  494. cornerRadius: 2,
  495. borderColor: "rgba(0, 0, 0, 0.1)",
  496. borderWidth: 1,
  497. displayColors: false,
  498. }
  499. },
  500. responsive: true,
  501. interaction: {
  502. intersect: false,
  503. mode: "index",
  504. axis: "y"
  505. },
  506. scales: {
  507. x: {
  508. grace:"5%",
  509. beginAtZero: true,
  510. ticks: {
  511. maxTicksLimit: 6,
  512. }
  513. }
  514. }
  515. }
  516. });
  517. // reset the display: block style that chart.js applies automatically
  518. ctx.style.display = "";
  519. });
  520. }
  521. // add data to the map
  522. function feedMapData(period) {
  523. const map = document.querySelector("svg.map");
  524. const paths = map.querySelectorAll("path");
  525. if (!map || !paths || !paths.length) return;
  526. let data = JSON.parse(map.dataset[period || "day"]);
  527. if (!data) return;
  528. let max = data.sort((a, b) => a.value > b.value ? -1 : 1)[0];
  529. if (!max) max = { value: 1 }
  530. data = data.reduce((a, c) => ({ ...a, [c.name]: c.value }), {});
  531. for (let i = 0; i < paths.length; ++i) {
  532. const id = paths[i].dataset.id;
  533. const views = data[id] || 0;
  534. paths[i].dataset.views = views;
  535. const colorLevel = Math.ceil((views / max.value) * 6);
  536. const classList = paths[i].classList;
  537. for (let j = 1; j < 7; j++) {
  538. paths[i].classList.remove(`color-${j}`);
  539. }
  540. paths[i].classList.add(`color-${colorLevel}`)
  541. paths[i].dataset.views = views;
  542. }
  543. }
  544. // handle map tooltip hover
  545. function mapTooltipHoverOver() {
  546. const tooltip = document.querySelector("#map-tooltip");
  547. if (!tooltip) return;
  548. if (!event.target.dataset.id) return mapTooltipHoverOut();
  549. if (!tooltip.classList.contains("active")) {
  550. tooltip.classList.add("visible");
  551. }
  552. tooltip.dataset.tooltip = `${event.target.ariaLabel}: ${event.target.dataset.views || 0}`;
  553. const rect = event.target.getBoundingClientRect();
  554. tooltip.style.top = rect.top + (rect.height / 2) + "px";
  555. tooltip.style.left = rect.left + (rect.width / 2) + "px";
  556. event.target.classList.add("active");
  557. }
  558. function mapTooltipHoverOut() {
  559. const tooltip = document.querySelector("#map-tooltip");
  560. const map = document.querySelector("svg.map");
  561. const paths = map.querySelectorAll("path");
  562. if (!tooltip || !map) return;
  563. tooltip.classList.remove("visible");
  564. for (let i = 0; i < paths.length; ++i) {
  565. paths[i].classList.remove("active");
  566. }
  567. }
  568. // create stats charts
  569. function createCharts() {
  570. createViewsChart();
  571. createBrowsersChart();
  572. createReferrersChart();
  573. createOsChart();
  574. feedMapData();
  575. }
  576. // change stats period for showing charts and data
  577. function changeStatsPeriod(event) {
  578. const period = event.target.dataset.period;
  579. if (!period) return;
  580. const canvases = document.querySelector("#stats").querySelectorAll("[data-period]");
  581. const buttons = document.querySelector("#stats").querySelectorAll(".nav");
  582. if (!buttons || !canvases) return;
  583. buttons.forEach(b => b.disabled = false);
  584. event.target.disabled = true;
  585. canvases.forEach(canvas => {
  586. if (canvas.dataset.period === period) {
  587. canvas.classList.remove("hidden");
  588. } else {
  589. canvas.classList.add("hidden");
  590. }
  591. });
  592. feedMapData(period);
  593. }
  594. // htmx prefetch extension
  595. // https://github.com/bigskysoftware/htmx-extensions/blob/main/src/preload/README.md
  596. htmx.defineExtension('preload', {
  597. onEvent: function(name, event) {
  598. if (name !== 'htmx:afterProcessNode') {
  599. return
  600. }
  601. var attr = function(node, property) {
  602. if (node == undefined) { return undefined }
  603. return node.getAttribute(property) || node.getAttribute('data-' + property) || attr(node.parentElement, property)
  604. }
  605. var load = function(node) {
  606. var done = function(html) {
  607. if (!node.preloadAlways) {
  608. node.preloadState = 'DONE'
  609. }
  610. if (attr(node, 'preload-images') == 'true') {
  611. document.createElement('div').innerHTML = html
  612. }
  613. }
  614. return function() {
  615. if (node.preloadState !== 'READY') {
  616. return
  617. }
  618. var hxGet = node.getAttribute('hx-get') || node.getAttribute('data-hx-get')
  619. if (hxGet) {
  620. htmx.ajax('GET', hxGet, {
  621. source: node,
  622. handler: function(elt, info) {
  623. done(info.xhr.responseText)
  624. }
  625. })
  626. return
  627. }
  628. if (node.getAttribute('href')) {
  629. var r = new XMLHttpRequest()
  630. r.open('GET', node.getAttribute('href'))
  631. r.onload = function() { done(r.responseText) }
  632. r.send()
  633. }
  634. }
  635. }
  636. var init = function(node) {
  637. if (node.getAttribute('href') + node.getAttribute('hx-get') + node.getAttribute('data-hx-get') == '') {
  638. return
  639. }
  640. if (node.preloadState !== undefined) {
  641. return
  642. }
  643. var on = attr(node, 'preload') || 'mousedown'
  644. const always = on.indexOf('always') !== -1
  645. if (always) {
  646. on = on.replace('always', '').trim()
  647. }
  648. node.addEventListener(on, function(evt) {
  649. if (node.preloadState === 'PAUSE') {
  650. node.preloadState = 'READY'
  651. if (on === 'mouseover') {
  652. window.setTimeout(load(node), 100)
  653. } else {
  654. load(node)()
  655. }
  656. }
  657. })
  658. switch (on) {
  659. case 'mouseover':
  660. node.addEventListener('touchstart', load(node))
  661. node.addEventListener('mouseout', function(evt) {
  662. if ((evt.target === node) && (node.preloadState === 'READY')) {
  663. node.preloadState = 'PAUSE'
  664. }
  665. })
  666. break
  667. case 'mousedown':
  668. node.addEventListener('touchstart', load(node))
  669. break
  670. }
  671. node.preloadState = 'PAUSE'
  672. node.preloadAlways = always
  673. htmx.trigger(node, 'preload:init')
  674. }
  675. const parent = event.target || event.detail.elt;
  676. parent.querySelectorAll("[preload]").forEach(function(node) {
  677. init(node)
  678. node.querySelectorAll('a,[hx-get],[data-hx-get]').forEach(init)
  679. })
  680. }
  681. })