mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-11 18:52:21 +02:00
* refactor(settings): add modular shell primitives * refactor(settings): wire modular shell * test(settings): exercise real coordinator ESM boundary * refactor(settings): add registry-backed settings finder * fix(settings): harden registry navigation behavior
173 lines
4.5 KiB
JavaScript
173 lines
4.5 KiB
JavaScript
import {
|
|
SETTINGS_GROUPS,
|
|
getSettingsPanel,
|
|
searchSettingsPanels,
|
|
} from './registry.js';
|
|
|
|
const _boundModals = new WeakSet();
|
|
|
|
function groupLabelFor(panel) {
|
|
const group = SETTINGS_GROUPS.find(candidate => candidate.id === panel.group);
|
|
return group?.label || '';
|
|
}
|
|
|
|
function clearResults(resultsEl) {
|
|
if (!resultsEl) return;
|
|
resultsEl.replaceChildren();
|
|
resultsEl.classList.add('hidden');
|
|
}
|
|
|
|
function getResultButtons(resultsEl) {
|
|
if (!resultsEl) return [];
|
|
return Array.from(resultsEl.querySelectorAll('[data-settings-search-result]'));
|
|
}
|
|
|
|
function setActiveResult(resultsEl, index) {
|
|
const buttons = getResultButtons(resultsEl);
|
|
if (!buttons.length) return -1;
|
|
|
|
let next = index;
|
|
if (next < 0) next = buttons.length - 1;
|
|
if (next >= buttons.length) next = 0;
|
|
|
|
buttons.forEach((button, buttonIndex) => {
|
|
const active = buttonIndex === next;
|
|
button.classList.toggle('active', active);
|
|
button.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
});
|
|
|
|
if (typeof buttons[next].scrollIntoView === 'function') {
|
|
buttons[next].scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function bindSettingsSearch(modalEl, options = {}) {
|
|
if (!modalEl || _boundModals.has(modalEl)) return;
|
|
|
|
const input = modalEl.querySelector('#settings-nav-search');
|
|
const resultsEl = modalEl.querySelector('#settings-nav-search-results');
|
|
|
|
if (!input || !resultsEl) return;
|
|
_boundModals.add(modalEl);
|
|
|
|
const isAdmin = typeof options.isAdmin === 'function'
|
|
? options.isAdmin
|
|
: () => false;
|
|
|
|
const openPanel = typeof options.openPanel === 'function'
|
|
? options.openPanel
|
|
: () => {};
|
|
|
|
let activeIndex = -1;
|
|
|
|
function reset() {
|
|
input.value = '';
|
|
activeIndex = -1;
|
|
clearResults(resultsEl);
|
|
}
|
|
|
|
function activateResult(button) {
|
|
const panelId = button?.dataset?.settingsSearchResult;
|
|
if (!panelId || !getSettingsPanel(panelId)) return;
|
|
|
|
openPanel(panelId);
|
|
reset();
|
|
}
|
|
|
|
function render() {
|
|
const query = input.value.trim();
|
|
activeIndex = -1;
|
|
resultsEl.replaceChildren();
|
|
|
|
if (!query) {
|
|
resultsEl.classList.add('hidden');
|
|
return;
|
|
}
|
|
|
|
const matches = searchSettingsPanels(query, {
|
|
isAdmin: isAdmin(),
|
|
});
|
|
|
|
if (!matches.length) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'settings-search-empty';
|
|
empty.textContent = 'No settings found';
|
|
resultsEl.appendChild(empty);
|
|
resultsEl.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
for (const panel of matches) {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'settings-search-result';
|
|
button.setAttribute('data-settings-search-result', panel.id);
|
|
button.setAttribute('role', 'option');
|
|
button.setAttribute('aria-selected', 'false');
|
|
|
|
const label = document.createElement('span');
|
|
label.className = 'settings-search-result-label';
|
|
label.textContent = panel.label;
|
|
|
|
const group = document.createElement('span');
|
|
group.className = 'settings-search-result-group';
|
|
group.textContent = groupLabelFor(panel);
|
|
|
|
button.append(label, group);
|
|
button.addEventListener('click', () => activateResult(button));
|
|
resultsEl.appendChild(button);
|
|
}
|
|
|
|
resultsEl.classList.remove('hidden');
|
|
}
|
|
|
|
input.addEventListener('input', render);
|
|
|
|
input.addEventListener('focus', () => {
|
|
if (input.value.trim()) render();
|
|
});
|
|
|
|
input.addEventListener('keydown', event => {
|
|
const buttons = getResultButtons(resultsEl);
|
|
|
|
if (event.key === 'Escape') {
|
|
if (!input.value && resultsEl.classList.contains('hidden')) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
reset();
|
|
return;
|
|
}
|
|
|
|
if (!buttons.length) return;
|
|
|
|
if (event.key === 'ArrowDown') {
|
|
event.preventDefault();
|
|
activeIndex = setActiveResult(resultsEl, activeIndex + 1);
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'ArrowUp') {
|
|
event.preventDefault();
|
|
activeIndex = setActiveResult(resultsEl, activeIndex - 1);
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Enter') {
|
|
const target = buttons[activeIndex >= 0 ? activeIndex : 0];
|
|
if (!target) return;
|
|
event.preventDefault();
|
|
activateResult(target);
|
|
}
|
|
});
|
|
|
|
modalEl.addEventListener('mousedown', event => {
|
|
if (event.target === input || resultsEl.contains(event.target)) return;
|
|
clearResults(resultsEl);
|
|
activeIndex = -1;
|
|
});
|
|
|
|
return { reset, render };
|
|
}
|