/* ZnoteX Admin Control Panel - day/night theme, menu search, side sections. */ (function () { 'use strict'; var root = document.documentElement; var backdrop = document.getElementById('acpBackdrop'); var burger = document.getElementById('acpBurger'); var filter = document.getElementById('acpFilter'); var nav = document.getElementById('acpNav'); var themeBtn = document.getElementById('acpTheme'); var MOBILE = '(max-width: 840px)'; function store(key, value) { try { localStorage.setItem('acp.' + key, value); } catch (e) {} } function isMobile() { return window.matchMedia(MOBILE).matches; } function openDrawer(open) { root.classList.toggle('acp-nav-open', open); if (backdrop) { backdrop.hidden = !open; } } if (burger) { burger.addEventListener('click', function () { if (isMobile()) { openDrawer(!root.classList.contains('acp-nav-open')); return; } var closed = root.classList.toggle('acp-sidebar-closed'); store('sidebar_closed', closed ? '1' : '0'); }); } if (backdrop) { backdrop.addEventListener('click', function () { openDrawer(false); }); } document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { openDrawer(false); } }); window.addEventListener('resize', function () { if (!isMobile()) { openDrawer(false); } }); if (nav) { Array.prototype.slice.call(nav.querySelectorAll('.acp-nav-group')).forEach(function (group) { var button = group.querySelector('.acp-nav-group-label'); var active = group.querySelector('.acp-nav-link.is-active'); group.classList.toggle('is-open', !!active); if (!active && group === nav.querySelector('.acp-nav-group')) { group.classList.add('is-open'); } if (button) { button.setAttribute('aria-expanded', group.classList.contains('is-open') ? 'true' : 'false'); button.addEventListener('click', function () { var open = group.classList.toggle('is-open'); button.setAttribute('aria-expanded', open ? 'true' : 'false'); }); } }); // Same collapse behaviour, one level down: a plugin's own sub-list of // pages inside a group. Already open when one of its pages is the // current one (server-rendered with is-open), closed otherwise. Array.prototype.slice.call(nav.querySelectorAll('.acp-nav-plugin')).forEach(function (cluster) { var button = cluster.querySelector('.acp-nav-plugin-label'); if (!button) { return; } button.addEventListener('click', function () { var open = cluster.classList.toggle('is-open'); button.setAttribute('aria-expanded', open ? 'true' : 'false'); }); }); } if (filter && nav) { var links = Array.prototype.slice.call(nav.querySelectorAll('.acp-nav-link')); var clusters = Array.prototype.slice.call(nav.querySelectorAll('.acp-nav-plugin')); var groups = Array.prototype.slice.call(nav.querySelectorAll('.acp-nav-group')); var noMatch = nav.querySelector('.acp-nav-nomatch'); function applyFilter() { var q = filter.value.trim().toLowerCase(); var hits = 0; links.forEach(function (link) { var match = q === '' || (link.getAttribute('data-title') || '').indexOf(q) !== -1; link.parentNode.hidden = !match; if (match) { hits++; } }); // A plugin's own
  • never matches .acp-nav-link (it is the // cluster wrapper, not a page), so it needs its own visibility // pass based on whether any page inside it just matched above. clusters.forEach(function (cluster) { var visible = cluster.querySelectorAll('.acp-nav-link:not([hidden])').length > 0; cluster.hidden = !visible; if (q !== '' && visible) { cluster.classList.add('is-open'); var pbutton = cluster.querySelector('.acp-nav-plugin-label'); if (pbutton) { pbutton.setAttribute('aria-expanded', 'true'); } } }); groups.forEach(function (group) { var visible = group.querySelectorAll('li:not([hidden])').length > 0; group.hidden = !visible; if (q !== '' && visible) { group.classList.add('is-open'); var button = group.querySelector('.acp-nav-group-label'); if (button) { button.setAttribute('aria-expanded', 'true'); } } }); if (noMatch) { noMatch.hidden = hits > 0; } } filter.addEventListener('input', applyFilter); filter.addEventListener('keydown', function (e) { if (e.key === 'Escape') { filter.value = ''; applyFilter(); } if (e.key === 'Enter') { var first = nav.querySelector('li:not([hidden]) .acp-nav-link'); if (first) { window.location.href = first.href; } } }); } function syncTheme() { if (!themeBtn) { return; } var dark = root.getAttribute('data-acp-theme') === 'dark'; var icon = themeBtn.querySelector('.fa'); var label = themeBtn.querySelector('span'); if (icon) { icon.className = 'fa ' + (dark ? 'fa-sun-o' : 'fa-moon-o'); } if (label) { label.textContent = dark ? 'Day' : 'Night'; } themeBtn.title = dark ? 'Switch to day mode' : 'Switch to night mode'; } if (themeBtn) { themeBtn.addEventListener('click', function () { var dark = root.getAttribute('data-acp-theme') === 'dark'; root.setAttribute('data-acp-theme', dark ? 'light' : 'dark'); store('theme', dark ? 'light' : 'dark'); syncTheme(); }); syncTheme(); } var shopPreview = document.getElementById('shopOfferPreview'); var shopType = document.getElementById('type'); var shopItem = document.getElementById('itemid'); var shopCount = document.getElementById('count'); if (shopPreview && shopType && shopItem && shopCount) { function image(src, className) { var img = document.createElement('img'); img.src = src; img.className = className; img.alt = ''; return img; } function outfitUrl(outfitId, mountId) { var server = shopPreview.getAttribute('data-outfit-server') || ''; var count = parseInt(shopCount.value, 10) || 0; if (!server || !outfitId) { return ''; } var url = server + '?id=' + encodeURIComponent(outfitId) + '&addons=' + encodeURIComponent(count) + '&head=78&body=68&legs=58&feet=76&direction=2'; if (mountId) { url += '&mount=' + encodeURIComponent(mountId); } return url; } function updateShopPreview() { var type = parseInt(shopType.value, 10) || 0; var raw = shopItem.value.trim(); var template = shopPreview.getAttribute('data-item-template') || ''; shopPreview.innerHTML = ''; if (type === 5) { var pair = raw.match(/^\s*(\d+)\s*,\s*(\d+)\s*$/); if (pair) { shopPreview.appendChild(image(outfitUrl(pair[1], ''), 'acp-shop-preview-img')); shopPreview.appendChild(image(outfitUrl(pair[2], ''), 'acp-shop-preview-img')); return; } } else if (type === 6) { if (/^\d+$/.test(raw)) { shopPreview.appendChild(image(outfitUrl(128, raw), 'acp-shop-preview-img')); return; } } else if (/^\d+$/.test(raw) && template) { shopPreview.appendChild(image(template.replace('{id}', raw), 'acp-shop-preview-item')); return; } var muted = document.createElement('span'); muted.className = 'is-muted'; muted.appendChild(document.createTextNode('Preview')); shopPreview.appendChild(muted); } shopType.addEventListener('change', updateShopPreview); shopItem.addEventListener('input', updateShopPreview); shopCount.addEventListener('input', updateShopPreview); updateShopPreview(); } document.addEventListener('submit', function (e) { var form = e.target; var message = form.getAttribute('data-confirm'); if (message && !window.confirm(message)) { e.preventDefault(); } }); })(); /* * Click-to-sort for any . Sorts the * rows currently in the DOM - on a paginated list that is only the current * page, which is fine: the point is letting an admin re-order what is on * screen (highest points first, most recent first), not a full-dataset sort. */ (function () { function cellText(row, index) { var cell = row.children[index]; if (!cell) return ''; return (cell.getAttribute('data-sort-value') || cell.textContent || '').trim(); } function looksNumeric(value) { return value !== '' && !isNaN(parseFloat(value.replace(/[^0-9.\-]/g, ''))) && /^[\s0-9.,\-]+$/.test(value); } Array.prototype.slice.call(document.querySelectorAll('table.acp-table[data-sortable]')).forEach(function (table) { var thead = table.querySelector('thead'); var tbody = table.querySelector('tbody'); if (!thead || !tbody) return; var headers = Array.prototype.slice.call(thead.querySelectorAll('th')); headers.forEach(function (th, index) { if (th.textContent.trim() === '') return; // icon/action-only columns stay unsortable th.classList.add('acp-th-sortable'); th.setAttribute('role', 'button'); th.setAttribute('tabindex', '0'); var sort = function () { var dir = th.getAttribute('data-sort-dir') === 'asc' ? 'desc' : 'asc'; headers.forEach(function (h) { h.removeAttribute('data-sort-dir'); h.classList.remove('is-sorted-asc', 'is-sorted-desc'); }); th.setAttribute('data-sort-dir', dir); th.classList.add(dir === 'asc' ? 'is-sorted-asc' : 'is-sorted-desc'); var rows = Array.prototype.slice.call(tbody.querySelectorAll(':scope > tr')); var numeric = rows.length > 0 && looksNumeric(cellText(rows[0], index)); rows.sort(function (a, b) { var av = cellText(a, index); var bv = cellText(b, index); var cmp; if (numeric) { cmp = (parseFloat(av.replace(/[^0-9.\-]/g, '')) || 0) - (parseFloat(bv.replace(/[^0-9.\-]/g, '')) || 0); } else { cmp = av.localeCompare(bv, undefined, { sensitivity: 'base', numeric: true }); } return dir === 'asc' ? cmp : -cmp; }); rows.forEach(function (row) { tbody.appendChild(row); }); }; th.addEventListener('click', sort); th.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); sort(); } }); }); }); })(); /* * Client-side row filter for small lists (plugins, shop offers - anything * that is already fully rendered on the page, not a paginated server query). * Usage: filters * #tableId tbody tr[data-acp-search] by substring match, and optionally * updates a live count in [data-acp-search-count="tableId"]. */ (function () { Array.prototype.slice.call(document.querySelectorAll('[data-acp-search-input]')).forEach(function (input) { var tableId = input.getAttribute('data-acp-search-input'); var table = document.getElementById(tableId); if (!table) return; var rows = Array.prototype.slice.call(table.querySelectorAll('tbody tr[data-acp-search]')); var count = document.querySelector('[data-acp-search-count="' + tableId + '"]'); input.addEventListener('keyup', function () { var needle = input.value.trim().toLowerCase(); var shown = 0; rows.forEach(function (row) { var match = needle === '' || (row.getAttribute('data-acp-search') || '').indexOf(needle) !== -1; row.hidden = !match; if (match) shown++; }); if (count) count.textContent = needle === '' ? '' : (shown + ' / ' + rows.length); }); }); })(); /* * Dynamic add/remove-row tables: any element with data-acp-table-add="key" * clones the row template registered as data-acp-table-template="key" into * the matching [data-acp-table="key"] container; data-acp-table-remove * removes its own row. Shared by settings.php's 'table' schema fields and * the serverdata single-record editors (items/creatures attribute rows). */ (function () { document.addEventListener('click', function (e) { var addBtn = e.target.closest('[data-acp-table-add]'); if (addBtn) { var key = addBtn.getAttribute('data-acp-table-add'); var container = document.querySelector('[data-acp-table="' + key + '"]'); var template = document.querySelector('template[data-acp-table-template="' + key + '"]'); if (!container || !template) return; var body = container.querySelector('[data-acp-table-body]') || container; // fragments only parse correctly inside a wrapper; anything // else (the permissions table's plain
    account blocks) parses fine // in a generic
    . var wrapper = document.createElement(body.tagName === 'TBODY' ? 'tbody' : 'div'); var nextIndex = body.children.length; var html = template.innerHTML.split('__ROWIDX__').join(String(nextIndex)); wrapper.innerHTML = html; body.appendChild(wrapper.firstElementChild); return; } var removeBtn = e.target.closest('[data-acp-table-remove]'); if (removeBtn) { var row = removeBtn.closest('tr, .acp-perm-account'); if (row) row.remove(); } }); })(); /* Ctrl+K / Cmd+K jumps focus to the top-bar search, from anywhere in the panel. */ (function () { document.addEventListener('keydown', function (e) { if (!(e.ctrlKey || e.metaKey) || e.key.toLowerCase() !== 'k') return; var input = document.getElementById('acpTopSearch'); if (!input) return; e.preventDefault(); input.focus(); input.select(); }); })(); /* Arriving from search: highlight the field the anchor points at. */ (function () { var id = (window.location.hash || '').replace('#', ''); if (!id) return; var el = document.getElementById(id); if (!el) return; var field = el.closest ? el.closest('.acp-field') : null; var target = field || el; target.classList.add('acp-field--found'); setTimeout(function () { target.classList.remove('acp-field--found'); }, 2600); setTimeout(function () { target.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (el.focus && el.type !== 'checkbox') { try { el.focus({ preventScroll: true }); } catch (e) {} } }, 60); })();