added js code for quicklaunch
This commit is contained in:
@@ -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 => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||
}[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
|
||||
|
||||
Reference in New Issue
Block a user