Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor, admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS. Kredit & lisensi (ISC): - Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak atas base kode dan repo yang bermanfaat. - Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder. - Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
105 lines
4.9 KiB
JavaScript
105 lines
4.9 KiB
JavaScript
/* admin-theme-sync.js — Menjaga konsistensi tema admin (dark/light).
|
|
PENTING: observer TIDAK boleh menulis ulang `data-theme` (bisa memicu loop). */
|
|
(function () {
|
|
var KEY = 'app-theme';
|
|
function read() { try { return localStorage.getItem(KEY); } catch (e) { return null; } }
|
|
|
|
// Sinkronkan atribut turunan berdasarkan data-theme saat ini (tanpa menulis data-theme).
|
|
function syncAttrs() {
|
|
var isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
var bs = isLight ? 'light' : 'dark';
|
|
if (document.documentElement.getAttribute('data-bs-theme') !== bs) document.documentElement.setAttribute('data-bs-theme', bs);
|
|
if (document.documentElement.classList.contains('light-theme') !== isLight) document.documentElement.classList.toggle('light-theme', isLight);
|
|
if (document.body && document.body.classList.contains('light-theme') !== isLight) document.body.classList.toggle('light-theme', isLight);
|
|
try { if (document.documentElement.style.colorScheme !== bs) document.documentElement.style.colorScheme = bs; } catch (e) {}
|
|
}
|
|
|
|
function applyTheme(t) {
|
|
t = t || read() || 'dark';
|
|
if (document.documentElement.getAttribute('data-theme') !== t) document.documentElement.setAttribute('data-theme', t);
|
|
syncAttrs();
|
|
return t;
|
|
}
|
|
|
|
window.__applyAdminTheme = applyTheme;
|
|
applyTheme();
|
|
|
|
// Observer hanya menyinkronkan atribut turunan (AMAN, tanpa loop).
|
|
try { new MutationObserver(syncAttrs).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] }); } catch (e) {}
|
|
|
|
window.addEventListener('DOMContentLoaded', function () { applyTheme(); });
|
|
window.addEventListener('pageshow', function () { applyTheme(); });
|
|
|
|
if (typeof window.toggleAppTheme !== 'function') {
|
|
window.toggleAppTheme = function () {
|
|
var cur = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
|
|
try { localStorage.setItem(KEY, cur); } catch (e) {}
|
|
applyTheme(cur);
|
|
if (typeof window.updateThemeToggleIcons === 'function') window.updateThemeToggleIcons(cur);
|
|
};
|
|
}
|
|
|
|
document.addEventListener('click', function (e) {
|
|
var btn = e.target && e.target.closest ? e.target.closest('.theme-toggle-btn') : null;
|
|
if (!btn) return;
|
|
if (btn.dataset.themeDelegate === '1') return;
|
|
btn.dataset.themeDelegate = '1';
|
|
if (!btn.hasAttribute('onclick')) { e.preventDefault(); window.toggleAppTheme(); }
|
|
}, true);
|
|
})();
|
|
|
|
// ── Dropdown Aksi (kebab menu) ──
|
|
// Menu di-"portal" ke <body> saat terbuka, lalu diposisikan fixed relatif tombol.
|
|
// Ini menghilangkan SEMUA pengaruh ancestor (overflow tabel/kartu, stacking, dsb)
|
|
// sehingga menu selalu solid & tampil utuh di mana pun (termasuk baris Lunas).
|
|
var _actOpen = null, _actMenu = null, _actHome = null;
|
|
function actCloseAll() {
|
|
if (_actMenu) {
|
|
try { _actMenu.classList.remove('fixed', 'up'); _actMenu.style.left = ''; _actMenu.style.top = ''; } catch (e) {}
|
|
try {
|
|
if (_actHome && _actHome.parent) _actHome.parent.insertBefore(_actMenu, _actHome.next);
|
|
else document.body.appendChild(_actMenu);
|
|
} catch (e) {}
|
|
}
|
|
document.querySelectorAll('.act.open').forEach(function (x) { x.classList.remove('open'); });
|
|
_actOpen = null; _actMenu = null; _actHome = null;
|
|
}
|
|
function actPlace(btn, menu) {
|
|
menu.style.left = '0px'; menu.style.top = '0px';
|
|
var w = menu.offsetWidth, h = menu.offsetHeight;
|
|
var br = btn.getBoundingClientRect();
|
|
var vw = window.innerWidth, vh = window.innerHeight, pad = 8, gap = 6;
|
|
var left = br.right - w; // rata kanan ke tombol
|
|
if (left + w > vw - pad) left = vw - pad - w;
|
|
if (left < pad) left = pad;
|
|
var top = br.bottom + gap; // default: di bawah
|
|
if (top + h > vh - pad) top = br.top - gap - h; // tak cukup -> ke atas
|
|
if (top < pad) top = pad;
|
|
menu.style.left = Math.round(left) + 'px';
|
|
menu.style.top = Math.round(top) + 'px';
|
|
}
|
|
window.actToggle = function (e, btn) {
|
|
try { if (e) { e.preventDefault(); e.stopPropagation(); } } catch (_) {}
|
|
var a = btn && btn.closest ? btn.closest('.act') : null; if (!a) return;
|
|
var wasOpen = a.classList.contains('open');
|
|
actCloseAll();
|
|
if (!wasOpen) {
|
|
a.classList.add('open');
|
|
var menu = a.querySelector('.act-menu');
|
|
if (!menu) return;
|
|
_actOpen = a; _actMenu = menu;
|
|
_actHome = { parent: menu.parentNode, next: menu.nextSibling };
|
|
document.body.appendChild(menu); // pindah ke body
|
|
menu.classList.add('fixed');
|
|
actPlace(btn, menu);
|
|
}
|
|
};
|
|
// klik di luar menu -> tutup; klik di dalam menu -> biarkan aksi jalan dulu, tutup setelahnya
|
|
document.addEventListener('click', function (ev) {
|
|
if (_actMenu && ev.target && _actMenu.contains(ev.target)) { setTimeout(actCloseAll, 0); return; }
|
|
actCloseAll();
|
|
});
|
|
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape') actCloseAll(); });
|
|
window.addEventListener('scroll', actCloseAll, true);
|
|
window.addEventListener('resize', actCloseAll);
|