🎉 Sync 2026-10-01 14:45:51

This commit is contained in:
github-actions[bot] committed 2026-10-01 14:45:51 +08:00
1 parent f1518a803c
commit 620e59fee5
124 files changed
+21594 -1

No files matched your search

@@ -0,0 +1,296 @@
'use strict';
'require baseclass';
'require ui';
/* Liquid theme menu renderer, based on luci-theme-openwrt-2020's
* menu-openwrt2020.js (Apache 2.0). Renders the left sidebar into
* #mainmenu, the mode menu into #modemenu and sub page tabs into
* #tabmenu. */
return baseclass.extend({
__init__() {
ui.menu.load().then((tree) => this.render(tree));
},
render(tree) {
let node = tree;
let url = '';
this.renderModeMenu(node);
if (L.env.dispatchpath.length >= 3) {
for (var i = 0; i < 3 && node; i++) {
node = node.children[L.env.dispatchpath[i]];
url = url + (url ? '/' : '') + L.env.dispatchpath[i];
}
if (node)
this.renderTabMenu(node, url);
}
document.querySelector('#menubar > .navigation')
.addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle'));
this.initMenuIndicator();
},
/* hover 追踪:一个玻璃滑块平滑跟随鼠标在一级菜单项间移动 */
initMenuIndicator() {
const menu = document.querySelector('#mainmenu');
if (!menu)
return;
const indicator = E('div', { 'class': 'liquid-menu-indicator' });
menu.appendChild(indicator);
const items = menu.querySelectorAll('ul.mainmenu.l1 > li');
/* 追踪鼠标位置,供 handleMenuExpand 展开/折叠后重新定位滑块 */
var self = this;
self._menu = menu;
self._indicator = indicator;
self._lastX = 0;
self._lastY = 0;
menu.addEventListener('mousemove', function (ev) {
self._lastX = ev.clientX;
self._lastY = ev.clientY;
});
menu.addEventListener('mouseleave', function () {
menu.classList.remove('liquid-menu-hovering');
});
menu.addEventListener('scroll', function () {
menu.classList.remove('liquid-menu-hovering');
menu.querySelectorAll('ul.mainmenu.l2.liquid-sub-hovering').forEach(function (ul) {
ul.classList.remove('liquid-sub-hovering');
});
});
items.forEach(function (li) {
li.addEventListener('mouseenter', function () {
if (li.classList.contains('selected')) {
/* 当前菜单已有自身高亮,滑块滑到它时不叠加,避免遮挡 */
menu.classList.remove('liquid-menu-hovering');
return;
}
const a = li.querySelector(':scope > a');
const menuRect = menu.getBoundingClientRect();
const aRect = a.getBoundingClientRect();
indicator.style.transform =
'translateY(%dpx)'.format(aRect.top - menuRect.top + menu.scrollTop);
indicator.style.height = '%dpx'.format(aRect.height);
menu.classList.add('liquid-menu-hovering');
});
});
/* 二级菜单追踪滑块:每个展开的 ul.l2 一个 */
menu.querySelectorAll('ul.mainmenu.l2').forEach(function (ul) {
const sub = E('div', { 'class': 'liquid-sub-indicator' });
ul.appendChild(sub);
ul.querySelectorAll(':scope > li').forEach(function (li) {
li.addEventListener('mouseenter', function () {
if (li.classList.contains('selected')) {
/* 当前选中项已有胶囊高亮,滑块不叠加 */
ul.classList.remove('liquid-sub-hovering');
return;
}
const a = li.querySelector(':scope > a');
const ulRect = ul.getBoundingClientRect();
const aRect = a.getBoundingClientRect();
sub.style.transform = 'translateY(%dpx)'.format(aRect.top - ulRect.top);
sub.style.height = '%dpx'.format(aRect.height);
ul.classList.add('liquid-sub-hovering');
});
});
ul.addEventListener('mouseleave', function () {
ul.classList.remove('liquid-sub-hovering');
});
});
},
/* 展开/折叠后,用当前鼠标位置重新定位一级滑块。
点击切换分类时菜单项垂直位移,但鼠标未离开——mouseenter
不触发,滑块停留在旧位置。此处用最近的鼠标坐标查出
当前悬停的 li 并重算 translateY。 */
_repositionMenuIndicator() {
var menu = this._menu;
var indicator = this._indicator;
if (!menu || !indicator || !menu.classList.contains('liquid-menu-hovering'))
return;
var hovered = document.elementFromPoint(this._lastX, this._lastY);
if (!hovered)
return;
var li = hovered.closest('ul.mainmenu.l1 > li');
if (!li || li.classList.contains('selected')) {
menu.classList.remove('liquid-menu-hovering');
return;
}
var a = li.querySelector(':scope > a');
if (!a)
return;
var menuRect = menu.getBoundingClientRect();
var aRect = a.getBoundingClientRect();
indicator.style.transform =
'translateY(%dpx)'.format(aRect.top - menuRect.top + menu.scrollTop);
indicator.style.height = '%dpx'.format(aRect.height);
},
handleMenuExpand(ev) {
const a = ev.target;
const li = a.parentNode;
const ul1 = li.parentNode;
const ul2 = a.nextElementSibling;
/* 点击已展开的一级菜单 → 折叠(收起其子菜单) */
if (li.classList.contains('active') && ul2 && ul2.children.length > 0) {
li.classList.remove('active');
a.blur();
this._repositionMenuIndicator();
ev.preventDefault();
ev.stopPropagation();
return;
}
/* 未展开:收起其他已展开的一级菜单,再展开当前 */
document.querySelectorAll('ul.mainmenu.l1 > li.active').forEach(other => {
if (other !== li)
other.classList.remove('active');
});
if (!ul2)
return;
if (ul2.parentNode.offsetLeft + ul2.offsetWidth <= ul1.offsetLeft + ul1.offsetWidth)
ul2.classList.add('align-left');
ul1.classList.add('active');
li.classList.add('active');
a.blur();
/* 展开/折叠后菜单项垂直位移,滑块位置需跟着更新 */
this._repositionMenuIndicator();
ev.preventDefault();
ev.stopPropagation();
},
renderMainMenu(tree, url, level) {
const l = (level || 0) + 1;
const ul = E('ul', { 'class': 'mainmenu l%d'.format(l) });
const children = ui.menu.getChildren(tree);
if (children.length == 0 || l > 2)
return E([]);
children.forEach(child => {
/* 当前所在一级菜单:高亮(selected)并展开其二级菜单(active);
其余一级菜单保持收起,点击才展开 */
const isActive = (L.env.dispatchpath[l] == child.name);
const activeClass = 'mainmenu-item-%s%s'.format(
child.name, isActive ? ' selected active' : '');
ul.appendChild(E('li', { 'class': activeClass }, [
E('a', {
'href': L.url(url, child.name),
'data-title': child.title,
'click': (l == 1) ? ui.createHandlerFn(this, 'handleMenuExpand') : ''
}, [
_(child.title)
]),
this.renderMainMenu(child, url + '/' + child.name, l)
]));
});
if (l == 1)
document.querySelector('#mainmenu').appendChild(E('div', [ ul ]));
return ul;
},
renderModeMenu(tree) {
const menu = document.querySelector('#modemenu');
const children = ui.menu.getChildren(tree);
children.forEach((child, index) => {
const isActive = L.env.requestpath.length
? child.name === L.env.requestpath[0]
: index === 0;
if (index > 0)
menu.appendChild(E([], ['\u00a0|\u00a0']));
menu.appendChild(E('div', { 'class': isActive ? 'active' : '' }, [
E('a', { href: L.url(child.name) }, [
_(child.title)
])
]));
if (isActive)
this.renderMainMenu(child, child.name);
});
if (menu.children.length > 1)
menu.style.display = '';
},
renderTabMenu(tree, url, level) {
const container = document.querySelector('#tabmenu');
const l = (level || 0) + 1;
const ul = E('ul', { 'class': 'cbi-tabmenu' });
const children = ui.menu.getChildren(tree);
let activeNode = null;
if (children.length == 0)
return E([]);
children.forEach(child => {
const isActive = (L.env.dispatchpath[l + 2] == child.name);
const activeClass = isActive ? ' cbi-tab' : '';
const className = 'tabmenu-item-%s %s'.format(child.name, activeClass);
ul.appendChild(E('li', { 'class': className }, [
E('a', { 'href': L.url(url, child.name) }, [
_(child.title)
])
]));
if (isActive)
activeNode = child;
});
container.appendChild(ul);
container.style.display = '';
if (activeNode)
container.appendChild(this.renderTabMenu(activeNode, url + '/' + activeNode.name, l));
return ul;
},
handleSidebarToggle(ev) {
const btn = ev.currentTarget;
const bar = document.querySelector('#mainmenu');
if (btn.classList.contains('active')) {
btn.classList.remove('active');
bar.classList.remove('active');
}
else {
btn.classList.add('active');
bar.classList.add('active');
}
}
});
@@ -0,0 +1,45 @@
'use strict';
'require ui';
'require view';
/* Liquid theme login — fully static DOM. The form is rendered directly in
<div id="liquid-login"><div class="modal login"> by the ucode template;
CSS centers it and adds the glass card. No ui.showModal, no DOM moving —
password managers (Bitwarden etc.) always see the complete form. */
return view.extend({
render: function() {
var card = document.getElementById('liquid-login');
var form = card.querySelector('form'),
btn = card.querySelector('form button');
/* optional hostname prefix on title */
var hostname = (document.body && document.body.getAttribute('data-hostname')) || '';
var h4 = card.querySelector('h4');
if (h4 && hostname)
h4.textContent = hostname + ' · ' + _('Authorization Required');
if (form) {
form.addEventListener('keypress', function(ev) {
if (ev.key == 'Enter' && btn)
btn.click();
});
}
if (btn) {
btn.addEventListener('click', function(ev) {
ev.preventDefault();
card.querySelectorAll('.modal.login > *').forEach(function(node) { node.style.display = 'none'; });
card.querySelector('.modal.login').appendChild(E('div', { 'class': 'spinning' }, _('Logging in…')));
if (form) form.submit();
});
}
var pw = card.querySelector('input[type="password"]');
if (pw) pw.focus();
return '';
},
addFooter: function() {}
});