Dash
Items
自定义 JavaScript
保存
取消
/* Heimdall Modern UI — JS enhancements * - Tile stagger entry animation (IntersectionObserver) * - Inject Inter font from Google Fonts * - Collapsible config-buttons toolbar (collapsed on mobile, expanded on desktop) * Safe to load AFTER jQuery + app.js. Uses vanilla JS only. */ (function () { 'use strict'; var MOBILE_BP = 768; // ---------- 1. Inject Inter web font ---------- function injectFont() { if (document.getElementById('hm-font-link')) return; var pre1 = document.createElement('link'); pre1.rel = 'preconnect'; pre1.href = 'https://fonts.googleapis.com'; var pre2 = document.createElement('link'); pre2.rel = 'preconnect'; pre2.href = 'https://fonts.gstatic.com'; pre2.crossOrigin = 'anonymous'; var link = document.createElement('link'); link.id = 'hm-font-link'; link.rel = 'stylesheet'; link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'; document.head.appendChild(pre1); document.head.appendChild(pre2); document.head.appendChild(link); } // ---------- 2. Config-buttons collapse/expand ---------- function isMobile() { return window.matchMedia('(max-width: ' + MOBILE_BP + 'px)').matches; } function applyCollapsed(bar, collapsed) { if (collapsed) bar.classList.add('hm-collapsed'); else bar.classList.remove('hm-collapsed'); var toggle = document.getElementById('hm-cfg-toggle'); if (toggle) { var tip = toggle.querySelector('.tooltip'); if (tip) tip.textContent = collapsed ? 'Expand' : 'Collapse'; toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); } } function addCollapseToggle(bar) { if (document.getElementById('hm-cfg-toggle')) return; var a = document.createElement('a'); a.id = 'hm-cfg-toggle'; a.className = 'config'; a.href = '#'; a.setAttribute('role', 'button'); a.setAttribute('aria-controls', 'config-buttons'); a.innerHTML = '<i class="fas fa-chevron-up"></i>' + '<div class="tooltip left">Collapse</div>'; a.addEventListener('click', function (e) { e.preventDefault(); var collapsed = !bar.classList.contains('hm-collapsed'); applyCollapsed(bar, collapsed); }); bar.appendChild(a); } function initConfigButtons() { var bar = document.getElementById('config-buttons'); if (!bar) return; addCollapseToggle(bar); // Sync once on init applyCollapsed(bar, isMobile()); // Sync on viewport changes (matchMedia is the precise source of truth) var mq = window.matchMedia('(max-width: ' + MOBILE_BP + 'px)'); var onMQChange = function (e) { applyCollapsed(bar, e.matches); }; if (mq.addEventListener) mq.addEventListener('change', onMQChange); else if (mq.addListener) mq.addListener(onMQChange); // Debounced resize fallback (handles edge cases like devtools toggling // and zoom changes that some browsers don't dispatch as MQ change) var resizeTimer; var lastMobile = isMobile(); window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { var nowMobile = isMobile(); if (nowMobile !== lastMobile) { lastMobile = nowMobile; applyCollapsed(bar, nowMobile); } }, 150); }); } // ---------- 3. Stagger entry animation for tiles ---------- function staggerTiles() { var tiles = document.querySelectorAll('.item-container'); if (!tiles.length) return; var supportsIO = typeof IntersectionObserver !== 'undefined'; var index = 0; function reveal(el) { var delay = Math.min(index * 40, 600); index++; setTimeout(function () { el.classList.add('hm-in'); }, delay); } if (supportsIO) { var io = new IntersectionObserver( function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { reveal(entry.target); io.unobserve(entry.target); } }); }, { rootMargin: '0px 0px -10% 0px', threshold: 0.05 } ); tiles.forEach(function (t) { io.observe(t); }); } else { tiles.forEach(reveal); } } // ---------- 4. Keep newly-created tiles animated too ---------- function watchDynamicTiles() { if (typeof MutationObserver === 'undefined') return; var sortable = document.getElementById('sortable'); if (!sortable) return; var mo = new MutationObserver(function (mutations) { mutations.forEach(function (m) { m.addedNodes.forEach(function (node) { if ( node.nodeType === 1 && node.classList && node.classList.contains('item-container') && !node.classList.contains('hm-in') ) { requestAnimationFrame(function () { node.classList.add('hm-in'); }); } }); }); }); mo.observe(sortable, { childList: true, subtree: true }); } // ---------- Boot ---------- function boot() { injectFont(); initConfigButtons(); staggerTiles(); watchDynamicTiles(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();
仪表盘
用户
应用列表
标签列表
设置