window.addEventListener("load", (e) => { fileInputUpdatePath(); dateTimeShortcutsOverlay(); renderCharts(); filterForm(); warnWithoutSaving(); tabNavigation(); }); /************************************************************* * Move not visible tab items to dropdown *************************************************************/ function tabNavigation() { const itemsDropdown = document.getElementById("tabs-dropdown"); const itemsList = document.getElementById("tabs-items"); const widths = []; if (!itemsDropdown || !itemsList) { return; } handleTabNavigationResize(); window.addEventListener("resize", function () { handleTabNavigationResize(); }); function handleTabNavigationResize() { const contentWidth = document.getElementById("content").offsetWidth; const tabsWidth = document.getElementById("tabs-wrapper").scrollWidth; const availableWidth = itemsList.parentElement.offsetWidth - itemsList.offsetWidth - 48; if (tabsWidth > contentWidth) { const lastTabItem = itemsList ? itemsList.lastElementChild : null; if (lastTabItem) { widths.push(lastTabItem.offsetWidth); itemsList.removeChild(lastTabItem); itemsDropdown.appendChild(lastTabItem); // If there is still not enough space, move the last item to the dropdown again if ( document.getElementById("content").offsetWidth < document.getElementById("tabs-wrapper").scrollWidth ) { handleTabNavigationResize(); } } } else if ( widths.length > 0 && widths[widths.length - 1] < availableWidth ) { const lastTabItem = itemsDropdown ? itemsDropdown.lastElementChild : null; if (lastTabItem) { itemsDropdown.removeChild(lastTabItem); itemsList.appendChild(lastTabItem); widths.pop(); } } // Show/hide dropdown based on the number of items in dropdown if (itemsDropdown.childElementCount === 0) { itemsDropdown.parentElement.classList.add("hidden"); } else { itemsDropdown.parentElement.classList.remove("hidden"); } } } /************************************************************* * Alpine.sort.js callback after sorting *************************************************************/ const sortRecords = (e) => { const orderingField = e.from.dataset.orderingField; const weightInputs = Array.from( e.from.querySelectorAll( `.has_original input[name$=-${orderingField}], td.field-${orderingField} input[name$=-${orderingField}]` ) ); weightInputs.forEach((input, index) => { input.value = index; }); }; /************************************************************* * Search form *************************************************************/ function searchForm() { return { applyShortcut(event) { if ( event.key === "/" && document.activeElement.tagName.toLowerCase() !== "input" && document.activeElement.tagName.toLowerCase() !== "textarea" && !document.activeElement.isContentEditable ) { event.preventDefault(); this.$refs.searchInput.focus(); } }, }; } /************************************************************* * Search dropdown *************************************************************/ function searchDropdown() { return { openSearchResults: false, currentIndex: 0, applyShortcut(event) { if ( event.key === "t" && document.activeElement.tagName.toLowerCase() !== "input" && document.activeElement.tagName.toLowerCase() !== "textarea" && !document.activeElement.isContentEditable ) { event.preventDefault(); this.$refs.searchInput.focus(); } }, nextItem() { if (this.currentIndex < this.maxItem()) { this.currentIndex++; } }, prevItem() { if (this.currentIndex > 1) { this.currentIndex--; } }, maxItem() { return document.getElementById("search-results").querySelectorAll("li") .length; }, selectItem() { const href = this.items[this.currentIndex - 1].querySelector("a").href; window.location = href; }, }; } /************************************************************* * Search command *************************************************************/ function searchCommand() { return { el: document.getElementById("command-results"), items: undefined, hasResults: false, openCommandResults: false, currentIndex: 0, totalItems: 0, commandHistory: JSON.parse(localStorage.getItem("commandHistory") || "[]"), handleOpen() { this.openCommandResults = true; this.toggleBodyOverflow(); setTimeout(() => { this.$refs.searchInputCommand.focus(); }, 20); this.items = document.querySelectorAll("#command-history li"); this.totalItems = this.items.length; }, handleShortcut(event) { if ( event.key === "k" && (event.metaKey || event.ctrlKey) && document.activeElement.tagName.toLowerCase() !== "input" && document.activeElement.tagName.toLowerCase() !== "textarea" && !document.activeElement.isContentEditable ) { event.preventDefault(); this.handleOpen(); } }, handleEscape() { if (this.$refs.searchInputCommand.value === "") { this.toggleBodyOverflow(); this.openCommandResults = false; this.el.innerHTML = ""; this.items = undefined; this.totalItems = 0; this.currentIndex = 0; } else { this.$refs.searchInputCommand.value = ""; } }, handleContentLoaded(event) { if ( event.target.id !== "command-results" && event.target.id !== "command-results-list" ) { return; } const commandResultsList = document.getElementById( "command-results-list" ); if (commandResultsList) { this.items = commandResultsList.querySelectorAll("li"); this.totalItems = this.items.length; } else { this.items = undefined; this.totalItems = 0; } if (event.target.id === "command-results") { this.currentIndex = 0; if (this.items) { this.totalItems = this.items.length; } else { this.totalItems = 0; } } this.hasResults = this.totalItems > 0; if (!this.hasResults) { this.items = document.querySelectorAll("#command-history li"); } }, handleOutsideClick() { this.$refs.searchInputCommand.value = ""; this.openCommandResults = false; this.toggleBodyOverflow(); }, toggleBodyOverflow() { document .getElementsByTagName("body")[0] .classList.toggle("overflow-hidden"); }, scrollToActiveItem() { const item = this.items[this.currentIndex - 1]; if (item) { item.scrollIntoView({ behavior: "smooth", block: "nearest", }); } }, nextItem() { if (this.currentIndex < this.totalItems) { this.currentIndex++; this.scrollToActiveItem(); } }, prevItem() { if (this.currentIndex > 1) { this.currentIndex--; this.scrollToActiveItem(); } }, selectItem(addHistory) { const link = this.items[this.currentIndex - 1].querySelector("a"); const data = { title: link.dataset.title, description: link.dataset.description, link: link.href, favorite: false, }; if (addHistory) { this.addToHistory(data); } window.location = link.href; }, addToHistory(data) { let commandHistory = JSON.parse( localStorage.getItem("commandHistory") || "[]" ); for (const [index, item] of commandHistory.entries()) { if (item.link === data.link) { commandHistory.splice(index, 1); } } commandHistory.unshift(data); commandHistory = commandHistory.slice(0, 10); this.commandHistory = commandHistory; localStorage.setItem("commandHistory", JSON.stringify(commandHistory)); }, removeFromHistory(event, index) { event.preventDefault(); const commandHistory = JSON.parse( localStorage.getItem("commandHistory") || "[]" ); commandHistory.splice(index, 1); this.commandHistory = commandHistory; localStorage.setItem("commandHistory", JSON.stringify(commandHistory)); }, toggleFavorite(event, index) { event.preventDefault(); const commandHistory = JSON.parse( localStorage.getItem("commandHistory") || "[]" ); commandHistory[index].favorite = !commandHistory[index].favorite; this.commandHistory = commandHistory.sort( (a, b) => Number(b.favorite) - Number(a.favorite) ); localStorage.setItem("commandHistory", JSON.stringify(commandHistory)); }, }; } /************************************************************* * Warn without saving *************************************************************/ const warnWithoutSaving = () => { let formChanged = false; const form = document.querySelector("form.warn-unsaved-form"); const checkFormChanged = () => { const elements = document.querySelectorAll( "form.warn-unsaved-form input, form.warn-unsaved-form select, form.warn-unsaved-form textarea" ); for (const field of elements) { field.addEventListener("input", () => { formChanged = true; }); } }; if (!form) { return; } new MutationObserver((mutationsList, observer) => { checkFormChanged(); }).observe(form, { attributes: true, childList: true, subtree: true }); checkFormChanged(); preventLeaving = (e) => { if (formChanged) { e.preventDefault(); } }; form.addEventListener("submit", (e) => { window.removeEventListener("beforeunload", preventLeaving); }); window.addEventListener("beforeunload", preventLeaving); }; /************************************************************* * Filter form *************************************************************/ const filterForm = () => { const filterForm = document.getElementById("filter-form"); if (!filterForm) { return; } filterForm.addEventListener("formdata", (event) => { Array.from(event.formData.entries()).forEach(([key, value]) => { if (value === "") event.formData.delete(key); }); }); }; /************************************************************* * Class watcher *************************************************************/ const watchClassChanges = (selector, callback) => { const body = document.querySelector(selector); const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if ( mutation.type === "attributes" && mutation.attributeName === "class" ) { callback(); } } }); observer.observe(body, { attributes: true, attributeFilter: ["class"] }); }; /************************************************************* * Calendar & clock *************************************************************/ const dateTimeShortcutsOverlay = () => { const observer = new MutationObserver((mutations) => { for (const mutationRecord of mutations) { const display = mutationRecord.target.style.display; const overlay = document.getElementById("modal-overlay"); if (display === "block") { overlay.style.display = "block"; } else { overlay.style.display = "none"; } } }); const targets = document.querySelectorAll(".calendarbox, .clockbox"); for (const target of targets) { observer.observe(target, { attributes: true, attributeFilter: ["style"], }); } }; /************************************************************* * File upload path *************************************************************/ const fileInputUpdatePath = () => { const checkInputChanged = () => { for (const input of document.querySelectorAll("input[type=file]")) { if (input.hasChangeListener) { continue; } input.addEventListener("change", (e) => { const parts = e.target.value.split("\\"); const placeholder = input.parentNode.parentNode.parentNode.querySelector( "input[type=text]" ); placeholder.setAttribute("value", parts[parts.length - 1]); }); input.hasChangeListener = true; } }; new MutationObserver(() => { checkInputChanged(); }).observe(document.body, { childList: true, subtree: true, }); checkInputChanged(); }; /************************************************************* * Chart *************************************************************/ const DEFAULT_CHART_OPTIONS = { animation: false, barPercentage: 1, base: 0, grouped: false, maxBarThickness: 4, responsive: true, maintainAspectRatio: false, datasets: { bar: { borderRadius: 12, border: { width: 0, }, borderSkipped: "middle", }, line: { borderWidth: 2, pointBorderWidth: 0, pointStyle: false, }, pie: { borderWidth: 0, }, doughnut: { borderWidth: 0, }, }, plugins: { legend: { align: "end", display: false, position: "top", labels: { boxHeight: 5, boxWidth: 5, color: "#9ca3af", pointStyle: "circle", usePointStyle: true, }, }, tooltip: { enabled: true, }, }, scales: { x: { display: function (context) { if (["pie", "doughnut", "radar"].includes(context.chart.config.type)) { return false; } return true; }, border: { dash: [5, 5], dashOffset: 2, width: 0, }, ticks: { color: "#9ca3af", display: true, maxTicksLimit: function (context) { return context.chart.data.datasets.find( (dataset) => dataset.maxTicksXLimit )?.maxTicksXLimit; }, }, grid: { display: true, tickWidth: 0, }, }, y: { display: function (context) { if (["pie", "doughnut", "radar"].includes(context.chart.config.type)) { return false; } return true; }, border: { dash: [5, 5], dashOffset: 5, width: 0, }, ticks: { color: "#9ca3af", display: function (context) { return context.chart.data.datasets.some((dataset) => { return ( dataset.hasOwnProperty("displayYAxis") && dataset.displayYAxis ); }); }, callback: function (value) { const suffix = this.chart.data.datasets.find( (dataset) => dataset.suffixYAxis )?.suffixYAxis; if (suffix) { return `${value} ${suffix}`; } return value; }, }, grid: { lineWidth: (context) => { if (context.tick.value === 0) { return 1; } return 0; }, tickWidth: 0, }, }, }, }; const renderCharts = () => { const charts = []; const changeDarkModeSettings = () => { const hasDarkClass = document .querySelector("html") .classList.contains("dark"); const baseColorDark = getComputedStyle(document.documentElement) .getPropertyValue("--color-base-700") .trim(); const baseColorLight = getComputedStyle(document.documentElement) .getPropertyValue("--color-base-300") .trim(); const borderColor = hasDarkClass ? baseColorDark : baseColorLight; for (const chart of charts) { if (chart.options.scales.x) { chart.options.scales.x.grid.color = borderColor; } if (chart.options.scales.y) { chart.options.scales.y.grid.color = borderColor; } if (chart.options.scales.r) { chart.options.scales.r.grid.color = borderColor; } chart.update(); } }; for (const chart of document.querySelectorAll(".chart")) { const ctx = chart.getContext("2d"); const data = chart.dataset.value; const type = chart.dataset.type; const options = chart.dataset.options; if (!data) { continue; } const parsedData = JSON.parse(chart.dataset.value); for (const key in parsedData.datasets) { const dataset = parsedData.datasets[key]; const processColor = (colorProp) => { if (Array.isArray(dataset?.[colorProp])) { for (const [index, prop] of dataset?.[colorProp].entries()) { if (prop.startsWith("var(")) { const cssVar = prop.match(/var\((.*?)\)/)[1]; const color = getComputedStyle(document.documentElement) .getPropertyValue(cssVar) .trim(); dataset[colorProp][index] = color; } } } else if (dataset?.[colorProp]?.startsWith("var(")) { const cssVar = dataset[colorProp].match(/var\((.*?)\)/)[1]; const color = getComputedStyle(document.documentElement) .getPropertyValue(cssVar) .trim(); dataset[colorProp] = color; } }; processColor("borderColor"); processColor("backgroundColor"); } CHART_OPTIONS = { ...DEFAULT_CHART_OPTIONS }; if (type === "radar") { CHART_OPTIONS.scales = { r: { ticks: { backdropColor: "transparent", }, pointLabels: { color: "#9ca3af", font: { size: 12, }, }, }, }; } Chart.defaults.font.family = "Inter"; Chart.defaults.font.size = 12; charts.push( new Chart(ctx, { type: type || "bar", data: parsedData, options: options ? JSON.parse(options) : { ...CHART_OPTIONS }, }) ); } changeDarkModeSettings(); watchClassChanges("html", () => { changeDarkModeSettings(); }); };