Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
370 changes: 268 additions & 102 deletions public/js/admin-app-form.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/js/admin-auth.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ag, ap } from '/js/admin-shared.js?v=70ec6be1';
import { ag, ap } from '/js/admin-shared.js?v=0f36d0dc';
import { t } from '/js/i18n.js?v=e644a5c5';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { el, inp as inpById, qa } from '/js/utils.js?v=e8b2a9f7';
import { el, inp as inpById, qa } from '/js/utils.js?v=970a91b0';

export async function checkAuth(onLogin) {
try {
Expand Down
4 changes: 2 additions & 2 deletions public/js/admin-color-control.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=70ec6be1';
import { PE_SVG, initInlineEdit, toast } from '/js/admin-shared.js?v=0f36d0dc';
import { t } from '/js/i18n.js?v=e644a5c5';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { qa, q } from '/js/utils.js?v=e8b2a9f7';
import { qa, q } from '/js/utils.js?v=970a91b0';

const CC_SWATCHES = ['#1c1c1e', '#8e8e93', '#f2f2f7', '#ff393c', '#ffcd00', '#35c759', '#0289ff', '#cb30df'];
export const BADGE_DEFAULT = '#1e6ef4';
Expand Down
2 changes: 1 addition & 1 deletion public/js/admin-drag.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76';
import { state } from '/js/admin-state.js?v=7d68e98e';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
import { qa } from '/js/utils.js?v=e8b2a9f7';
import { qa } from '/js/utils.js?v=970a91b0';

/** @type {(before: unknown) => void} */
let _save = () => {};
Expand Down
10 changes: 5 additions & 5 deletions public/js/admin-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@

import { collapsedFolders, filter, state } from '/js/admin-state.js?v=7d68e98e';
import { snapshotItems } from '/js/admin-save-logic.js?v=4f71ef6c';
import { reorderItems } from '/js/admin-logic.js?v=dcf7c37d';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=f3b01482';
import { paintIcon } from '/js/admin-shared.js?v=70ec6be1';
import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=e8b2a9f7';
import { reorderItems } from '/js/admin-logic.js?v=74cb4272';
import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=3ffdf185';
import { paintIcon } from '/js/admin-shared.js?v=0f36d0dc';
import { clr as rc, el, qa, setUserText } from '/js/utils.js?v=970a91b0';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { t } from '/js/i18n.js?v=e644a5c5';
import { sizeLabel } from '/js/admin-widget-form.js?v=997ca7c6';
import { sizeLabel } from '/js/admin-widget-form.js?v=0cb044d0';
import { widgetGlyph } from '/js/widget-glyphs.js?v=12b0a947';

/** @type {{ openModal: (idx: number|null) => void,
Expand Down
12 changes: 12 additions & 0 deletions public/js/admin-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ export function collectFieldValues(reads, { includeTransient = false } = {}) {
return out;
}

/* Loose, so "AdGuard Home", "adguard_home" and "adguard-home" are one name. */
export function sameIconName(a, b) {
const norm = s =>
String(s ?? '')
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]/g, '');
const x = norm(a);
return !!x && x === norm(b);
}

export function nextActiveIndex(key, active, len) {
if (len <= 0) return null;
const clamp = i => Math.max(0, Math.min(i, len - 1));
Expand Down
6 changes: 3 additions & 3 deletions public/js/admin-settings.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { toast, ag, ap } from '/js/admin-shared.js?v=70ec6be1';
import { toast, ag, ap } from '/js/admin-shared.js?v=0f36d0dc';
import { pwStrength } from '/js/password-strength.js?v=42f45ac7';
import { t } from '/js/i18n.js?v=e644a5c5';
import { shouldWritePassword, settingsSaveBlocker, clearsStoredPassword, BLOCK } from '/js/admin-logic.js?v=dcf7c37d';
import { shouldWritePassword, settingsSaveBlocker, clearsStoredPassword, BLOCK } from '/js/admin-logic.js?v=74cb4272';
import { confirmText } from '/js/modal.js?v=11fa1eff';
import { el, inp, q, qa, setUserText } from '/js/utils.js?v=e8b2a9f7';
import { el, inp, q, qa, setUserText } from '/js/utils.js?v=970a91b0';

/* Mirrors the server's rule: auth cannot be switched on with no password. */
let _passwordSet = false;
Expand Down
9 changes: 6 additions & 3 deletions public/js/admin-shared.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/* Stateless helpers shared by the admin modules. Mutable state stays out. */
import { nextActiveIndex, recoversSession, toastHoldMs } from '/js/admin-logic.js?v=dcf7c37d';
import { el, qa, q } from '/js/utils.js?v=e8b2a9f7';
import { nextActiveIndex, recoversSession, toastHoldMs } from '/js/admin-logic.js?v=74cb4272';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
import { el, qa, q } from '/js/utils.js?v=970a91b0';
import { t } from '/js/i18n.js?v=e644a5c5';
import { iconChain } from '/js/icons.js?v=69c2b9bd';
import { iconChain } from '/js/icons.js?v=04e7796e';

export const API = '';

Expand Down Expand Up @@ -218,6 +219,7 @@ export function wireChecklist(dd, btn, list, onToggle) {
li.classList.toggle('kb-active', n === active);
});
o[active].focus();
fluidHoverKb(o[active]);
};
/* Scoped to the open state. A listener that outlives the list holds the
detached subtree it closes over, and the settings form rewires on every
Expand All @@ -244,6 +246,7 @@ export function wireChecklist(dd, btn, list, onToggle) {
list.hidden = true;
btn.setAttribute('aria-expanded', 'false');
opts().forEach(li => li.classList.remove('kb-active'));
fluidHoverClear(list);
if (focusBtn) btn.focus();
};
const toggle = li => {
Expand Down
8 changes: 4 additions & 4 deletions public/js/admin-widget-form.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { state } from '/js/admin-state.js?v=7d68e98e';
import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=70ec6be1';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=e6ec5157';
import { PE_SVG, CHEV_SVG, initInlineEdit } from '/js/admin-shared.js?v=0f36d0dc';
import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=1b131080';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=dcf7c37d';
import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=74cb4272';
import { t } from '/js/i18n.js?v=e644a5c5';
import { q, qi, qa } from '/js/utils.js?v=e8b2a9f7';
import { q, qi, qa } from '/js/utils.js?v=970a91b0';

const SIZE_ICONS = {
small:
Expand Down
27 changes: 17 additions & 10 deletions public/js/admin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=8c9ef961';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=1bb8584a';
import { initList, render, syncFilterUI } from '/js/admin-list.js?v=2067483a';
import { resolveAdminSection } from '/js/admin-logic.js?v=dcf7c37d';
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=92c86f9e';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=ee5255ba';
import { initList, render, syncFilterUI } from '/js/admin-list.js?v=0c33cd56';
import { resolveAdminSection } from '/js/admin-logic.js?v=74cb4272';
import {
buildAppItem,
claimFolderChildren,
Expand All @@ -10,13 +10,15 @@ import {
snapshotItems,
upsertItem,
} from '/js/admin-save-logic.js?v=4f71ef6c';
import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=9f7cf590';
import { ag, ap, initInlineEdit, paintIcon, setReauthHandler, toast } from '/js/admin-shared.js?v=70ec6be1';
import { loadSettings, showBgFields, showBgFit, showWallpaperFile } from '/js/admin-settings.js?v=23830fd0';
import { ag, ap, initInlineEdit, paintIcon, setReauthHandler, toast } from '/js/admin-shared.js?v=0f36d0dc';
import { collapsedFolders, filter, state } from '/js/admin-state.js?v=7d68e98e';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=997ca7c6';
import { buildWidgetForm } from '/js/admin-widget-form.js?v=0cb044d0';
import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86';
import { initGlideSelect, syncGlideSelect } from '/js/glide-select.js?v=8b39e9d0';
import { html, raw, setHtml } from '/js/html.js?v=c71f8903';
import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=e644a5c5';
import { loadLocalIcons } from '/js/icons.js?v=69c2b9bd';
import { loadLocalIcons } from '/js/icons.js?v=04e7796e';
import {
clearSkipTls,
convert,
Expand All @@ -29,8 +31,8 @@ import {
import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=9de1cb7d';
import { confirmModal, confirmText, openModal as openDialog, promptModal } from '/js/modal.js?v=11fa1eff';
import { readMode, watchSystemTheme, writeMode } from '/js/theme.js?v=00c011c9';
import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=e8b2a9f7';
import { normalizeColorInput } from '/js/admin-color-control.js?v=2d28867b';
import { el, inp, q, qa, clr as rc, sanitizeCssUrl, setUserText, tgt } from '/js/utils.js?v=970a91b0';
import { normalizeColorInput } from '/js/admin-color-control.js?v=d162431d';
import { parseYamlTolerant, YamlLiteError } from '/js/yaml-lite.js?v=6ebb564c';
import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6';

Expand All @@ -39,7 +41,10 @@ import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6'
dashboard, so the two screens cannot disagree about what mobile means. */
function _syncMobile(mobile) {
document.documentElement.classList.toggle('is-mobile', mobile);
syncGlideSelect();
}
initFluidHover();

const _mobileAtLoad = isMobileLayout();
_syncMobile(_mobileAtLoad);
onLayoutChange(_syncMobile, _mobileAtLoad);
Expand Down Expand Up @@ -606,11 +611,13 @@ function initNav() {
s.hidden = s.id !== 'sec-' + id;
});
links.forEach(l => l.classList.toggle('active', l.dataset.sec === id));
syncGlideSelect();
localStorage.setItem(STORE, id);
}
links.forEach(l => l.addEventListener('click', () => show(l.dataset.sec)));
addEventListener('hashchange', () => show(location.hash.slice(1)));
show(location.hash.slice(1) || localStorage.getItem(STORE));
initGlideSelect();
}

function initAllInlineEdits() {
Expand Down
16 changes: 10 additions & 6 deletions public/js/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { loadLocalIcons, iconChain } from '/js/icons.js?v=69c2b9bd';
import { loadLocalIcons, iconChain } from '/js/icons.js?v=04e7796e';
import {
WIDGET_HEIGHTS,
WIDGET_DESIGN,
Expand All @@ -24,13 +24,14 @@ import {
setUserText,
teardownWidgets,
titleWhenTruncated,
} from '/js/utils.js?v=e8b2a9f7';
import { initSpotlight } from '/js/spotlight.js?v=c81114db';
} from '/js/utils.js?v=970a91b0';
import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86';
import { initSpotlight } from '/js/spotlight.js?v=2b722c16';
import { html, setHtml, raw } from '/js/html.js?v=c71f8903';
import { initI18n, t, currentLang } from '/js/i18n.js?v=e644a5c5';
import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=42f45ac7';
import { sanitizeItemLinks } from '/js/link-url.js?v=54adb40f';
import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=3c2c7d0b';
import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=08ec9951';
import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=b3c8b6c2';
import { formatNumber } from '/js/format-number.js?v=4a5ccef4';
import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=aa52b1a3';
Expand Down Expand Up @@ -845,6 +846,7 @@ async function boot() {
};
_stateRef = state;
initUI(state);
initFluidHover();
initSpotlight({ getItems: () => items, isMob: () => MOB, CB, iconChain, openFolderDesktop, openFolderMobile });

/* Mobile measures the viewport as it builds, so it waits for layout. */
Expand Down Expand Up @@ -936,11 +938,13 @@ async function boot() {
document.body.appendChild(probe);
let _sat,
_saH = -1,
_saW = -1,
_saFirst = true;
new ResizeObserver(entries => {
const h = entries[0].contentRect.height;
if (h === _saH) return;
const { width: w, height: h } = entries[0].contentRect;
if (h === _saH && w === _saW) return;
_saH = h;
_saW = w;
/* Observing delivers the current size at once, and the layout being built
now already has it. */
if (_saFirst) {
Expand Down
133 changes: 133 additions & 0 deletions public/js/fluid-hover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/* A single highlight per list, moved instead of redrawn. It follows the
pointer, and falls back to the keyboard cursor when the pointer leaves. */

import { ensurePill, pillOf, placePill, restPill } from '/js/moving-pill.js?v=4e961dec';

const FINE = '(hover: hover) and (pointer: fine)';

/* Container element → { hover, kb, bound }. */
const states = new WeakMap();
/* Containers holding a live pill, so a resize can reposition them. Weakly
held: the admin rebuilds these lists on every render, and a strong set kept
every detached one alive until the window happened to be resized. */
const live = new Set();

function dropDetached() {
for (const c of live) if (!c.isConnected) live.delete(c);
}

const LISTS = [
{ container: '#sres', item: '.sr' },
{ container: '.row-dd-list', item: 'li[role="option"]' },
{ container: '#iprs', item: '.ipr' },
{ container: '.grp', item: '.row-link, .row.drow' },
];

export function fluidHoverSupported() {
return typeof matchMedia === 'function' && matchMedia(FINE).matches;
}

function selectable(item) {
if (item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true') return false;
return !item.classList.contains('d-none') && !item.classList.contains('dragging');
}

function stateFor(container) {
let s = states.get(container);
if (!s) {
s = { hover: null, kb: null, bound: false };
states.set(container, s);
}
return s;
}

function refresh(container) {
const s = stateFor(container);
/* Whichever moved last. The pointer holding it meant a keyboard cursor in a
hovered list had no highlight at all, because the row's own background is
suppressed while the pill is on, and Enter then acted on an invisible row. */
const target = s.hover || s.kb;
if (target && target.isConnected && selectable(target)) {
const pill = ensurePill(container, 'fh-hl');
live.add(container);
placePill(container, pill, target);
} else {
restPill(pillOf(container));
live.delete(container);
}
dropDetached();
}

function bind(container) {
const s = stateFor(container);
if (s.bound) return;
s.bound = true;
container.addEventListener('pointerleave', () => {
s.hover = null;
refresh(container);
});
}

function locate(item) {
if (!item || !item.matches) return null;
for (const cfg of LISTS) {
if (!item.matches(cfg.item)) continue;
const container = item.closest(cfg.container);
if (container) return container;
}
return null;
}

/* Moves the highlight to the keyboard cursor, and takes it off the pointer:
the last input to move is the one that owns it. */
export function fluidHoverKb(item) {
if (!fluidHoverSupported()) return;
const container = locate(item);
if (!container) return;
const s = stateFor(container);
s.hover = null;
s.kb = item;
bind(container);
refresh(container);
}

/* For the test: proves a detached container is not retained. */
export function _liveHolds(container) {
dropDetached();
return live.has(container);
}

export function fluidHoverClear(container) {
const s = states.get(container);
if (!s) return;
s.hover = null;
s.kb = null;
refresh(container);
}

export function initFluidHover(root = document) {
if (!fluidHoverSupported()) return;
root.addEventListener(
'pointerover',
e => {
if (e.pointerType && e.pointerType !== 'mouse') return;
const from = /** @type {Element} */ (e.target);
if (!from || !from.closest) return;
for (const cfg of LISTS) {
const item = from.closest(cfg.item);
if (!item) continue;
const container = item.closest(cfg.container);
if (!container || !selectable(item)) continue;
stateFor(container).hover = item;
bind(container);
refresh(container);
return;
}
},
true,
);
addEventListener('resize', () => {
dropDetached();
for (const c of live) refresh(c);
});
}
Loading