added js code for quicklaunch

This commit is contained in:
2026-01-14 19:39:08 +01:00
parent 75f1d1f18a
commit e492b16f21
+231
View File
@@ -38,6 +38,237 @@ data:
port: 8080
assets-path: /app/config/assets
document:
head: |
<style>
/* Glance Quick Launch overlay */
#gql-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.35);
display: none;
align-items: flex-start;
justify-content: center;
padding-top: 10vh;
z-index: 99999;
}
#gql-box {
width: min(720px, 92vw);
border-radius: 14px;
background: rgba(20, 22, 30, .92);
box-shadow: 0 10px 30px rgba(0,0,0,.35);
overflow: hidden;
backdrop-filter: blur(8px);
}
#gql-input {
width: 100%;
box-sizing: border-box;
padding: 14px 16px;
border: 0;
outline: none;
font-size: 16px;
color: #fff;
background: rgba(0,0,0,.25);
}
#gql-hint {
padding: 8px 16px;
font-size: 12px;
opacity: .75;
color: #fff;
}
#gql-list {
max-height: 360px;
overflow: auto;
padding: 6px;
}
.gql-item {
padding: 10px 10px;
border-radius: 10px;
cursor: pointer;
color: #fff;
display: flex;
gap: 10px;
align-items: baseline;
}
.gql-item:hover, .gql-item.active { background: rgba(255,255,255,.10); }
.gql-title { font-weight: 600; }
.gql-url { font-size: 12px; opacity: .7; word-break: break-all; }
</style>
<script>
(() => {
const MAX_RESULTS = 12;
const overlay = document.createElement('div');
overlay.id = 'gql-overlay';
overlay.innerHTML = `
<div id="gql-box" role="dialog" aria-modal="true">
<input id="gql-input" type="text" autocomplete="off" spellcheck="false" placeholder="Type to search bookmarks…"/>
<div id="gql-hint">↑/↓ to navigate • Enter to open • Esc to close</div>
<div id="gql-list"></div>
</div>
`;
document.addEventListener('DOMContentLoaded', () => document.body.appendChild(overlay));
const $ = (sel) => overlay.querySelector(sel);
const input = () => $('#gql-input');
const list = () => $('#gql-list');
let indexed = [];
let activeIndex = 0;
let lastQuery = '';
function normalize(s) {
return (s || '').toLowerCase().replace(/\s+/g, ' ').trim();
}
function indexLinks() {
// Prefer bookmarks widgets; fallback to any link inside widgets if Glance changes classes.
const anchors =
Array.from(document.querySelectorAll('.widget.widget-type-bookmarks a[href]')).length
? document.querySelectorAll('.widget.widget-type-bookmarks a[href]')
: document.querySelectorAll('.widget a[href]');
indexed = Array.from(anchors)
.map(a => ({
title: (a.textContent || '').trim(),
url: a.href
}))
.filter(x => x.title || x.url);
// Deduplicate by URL
const seen = new Set();
indexed = indexed.filter(x => (seen.has(x.url) ? false : seen.add(x.url)));
}
function score(item, q) {
const t = normalize(item.title);
const u = normalize(item.url);
if (!q) return 0;
// simple scoring: prefix > includes, title > url
if (t.startsWith(q)) return 100;
if (t.includes(q)) return 70;
if (u.includes(q)) return 40;
return -1;
}
function render(q) {
const results = indexed
.map(it => ({ ...it, s: score(it, q) }))
.filter(it => it.s >= 0)
.sort((a,b) => b.s - a.s)
.slice(0, MAX_RESULTS);
activeIndex = 0;
list().innerHTML = results.map((r, i) => `
<div class="gql-item ${i===0 ? 'active' : ''}" data-i="${i}">
<div class="gql-title">${escapeHtml(r.title || r.url)}</div>
<div class="gql-url">${escapeHtml(r.url)}</div>
</div>
`).join('');
list().onclick = (e) => {
const item = e.target.closest('.gql-item');
if (!item) return;
const i = Number(item.dataset.i);
openResult(results, i, false);
};
return results;
}
function setActive(i) {
const items = Array.from(list().querySelectorAll('.gql-item'));
items.forEach(el => el.classList.remove('active'));
if (items[i]) {
items[i].classList.add('active');
items[i].scrollIntoView({ block: 'nearest' });
}
}
function openOverlay(withInitialText = '') {
indexLinks();
overlay.style.display = 'flex';
input().value = withInitialText;
lastQuery = withInitialText;
const results = render(normalize(withInitialText));
input().focus();
function onInput() {
lastQuery = input().value;
render(normalize(lastQuery));
}
input().oninput = onInput;
input().onkeydown = (e) => {
const items = list().querySelectorAll('.gql-item');
const count = items.length;
if (e.key === 'Escape') { e.preventDefault(); closeOverlay(); return; }
if (e.key === 'ArrowDown' && count) { e.preventDefault(); activeIndex = Math.min(activeIndex+1, count-1); setActive(activeIndex); return; }
if (e.key === 'ArrowUp' && count) { e.preventDefault(); activeIndex = Math.max(activeIndex-1, 0); setActive(activeIndex); return; }
if (e.key === 'Enter') {
e.preventDefault();
const q = normalize(input().value);
const resultsNow = indexed
.map(it => ({ ...it, s: score(it, q) }))
.filter(it => it.s >= 0)
.sort((a,b) => b.s - a.s)
.slice(0, MAX_RESULTS);
openResult(resultsNow, activeIndex, e.ctrlKey || e.metaKey);
}
};
overlay.onclick = (e) => { if (e.target === overlay) closeOverlay(); };
}
function openResult(results, i, newTab) {
const r = results[i];
if (!r) return;
closeOverlay();
window.open(r.url, newTab ? '_blank' : '_self');
}
function closeOverlay() {
overlay.style.display = 'none';
list().innerHTML = '';
activeIndex = 0;
}
function escapeHtml(str) {
return (str || '').replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
// Global key handler: "just start typing"
window.addEventListener('keydown', (e) => {
if (overlay.style.display === 'flex') return;
// ignore when typing in inputs/textareas or using modifier shortcuts
const tag = (document.activeElement && document.activeElement.tagName || '').toLowerCase();
const typingIntoField = tag === 'input' || tag === 'textarea' || document.activeElement?.isContentEditable;
if (typingIntoField) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
// Printable character opens overlay with that character
if (e.key.length === 1 && !e.repeat) {
openOverlay(e.key);
e.preventDefault();
return;
}
// Optional: slash to open empty
if (e.key === '/') {
openOverlay('');
e.preventDefault();
}
});
})();
</script>
branding:
logo-url: https://web.dooplex.hu/static/DooPlex_logo_3.png
favicon-url: https://web.dooplex.hu/static/DooPlex_favicon_3.png