mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 05:43:56 +08:00
🎉 Sync 2026-10-01 14:45:51
This commit is contained in:
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() {}
|
||||
});
|
||||
Reference in new issue
Block a user