Helm/assets/js/helm.js
2026-01-23 10:30:48 -05:00

1610 lines
No EOL
65 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* CaptainCore Helm JS */
(() => {
const CFG = window.CCHELM_CONFIG || {};
const CORE_IDS = new Set(CFG.coreIds || []);
const TOOLBAR_KEEP = new Set(CFG.toolbarKeepIds || []);
const UPDATES_COUNT = Number(CFG.updatesCount || 0);
const VIEW_KEY = 'cch:view'; // 'cards' | 'expanded'
const THEME_KEY = 'cch:theme'; // 'light' | 'dark'
const qs = (sel, ctx = document) => ctx.querySelector(sel);
const qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
const isMac = () => {
const ua = navigator.userAgent || '';
const plt = navigator.platform || '';
return /Mac|iPhone|iPad|iPod/i.test(ua) || /Mac/i.test(plt);
};
const applyExternalLinkStyles = (element, href) => {
try {
const linkUrl = new URL(href, window.location.origin);
if (linkUrl.hostname !== window.location.hostname) {
element.target = '_blank';
element.rel = 'noopener noreferrer';
return true;
}
} catch (e) {
// Invalid URL
}
return false;
};
// --- SHORTCUT CONFIGURATION START ---
const getShortcutLabel = () => (isMac() ? '⌘⌥K' : 'Ctrl+Alt+K');
const isOpenShortcut = (e) => {
// Check for 'k' or 'K'
const isK = (e.key && e.key.toLowerCase() === 'k') ||
(e.code && e.code.toLowerCase() === 'keyk') ||
e.keyCode === 75;
if (!isK) return false;
if (isMac()) {
// Mac: Cmd + Option + K
return e.metaKey && e.altKey && !e.ctrlKey && !e.shiftKey;
} else {
// Windows/Linux: Ctrl + Alt + K
return e.ctrlKey && e.altKey && !e.metaKey && !e.shiftKey;
}
};
// --- SHORTCUT CONFIGURATION END ---
const visibleText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
qsa(
'.screen-reader-text, .awaiting-mod, .update-plugins, .count, ' +
'.plugin-count, .wp-ui-notification',
clone
).forEach((n) => n.remove());
return (clone.textContent || '').trim();
};
const sanitizeLabel = (s) => {
if (!s) return '';
s = String(s).replace(/\s+/g, ' ').trim();
s = s.replace(/(?:[\s\u00a0]*[()\[\]•·|:\-\u2013\u2014]*)?\d+(?:\+)?$/u, '');
return s.trim();
};
const escHtml = (s) =>
String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const highlightInto = (el, raw, q) => {
if (!el) return;
if (!q) {
el.textContent = raw;
return;
}
const src = String(raw);
const hay = src.toLowerCase();
const needle = q.toLowerCase();
if (!needle) {
el.textContent = src;
return;
}
let i = 0;
let html = '';
while (true) {
const j = hay.indexOf(needle, i);
if (j === -1) {
html += escHtml(src.slice(i));
break;
}
html += escHtml(src.slice(i, j));
html +=
'<mark class="cch-hl">' +
escHtml(src.slice(j, j + needle.length)) +
'</mark>';
i = j + needle.length;
}
el.innerHTML = html;
};
/* --- Icon Helpers --- */
const copyIcon = (li) => {
const src = qs('.wp-menu-image', li);
const span = document.createElement('span');
span.className = 'cch-icon';
if (!src) return span;
const classes = Array.from(src.classList).filter((c) => c.startsWith('dashicons'));
if (classes.length) {
span.classList.add(...classes);
if (!span.classList.contains('dashicons-before')) span.classList.add('dashicons-before');
}
const bg = src.getAttribute('style') || '';
const m = /background-image:\s*url\(([^)]+)\)/i.exec(bg);
if (m) {
span.style.backgroundImage = 'url(' + m[1] + ')';
span.style.backgroundSize = 'cover';
}
const svg = qs('svg', src);
if (svg) span.appendChild(svg.cloneNode(true));
return span;
};
const makeDashiconIcon = (dash) => {
const span = document.createElement('span');
span.className = 'cch-icon';
if (typeof dash === 'string' && dash.startsWith('dashicons-')) {
span.classList.add('dashicons-before', dash);
} else {
span.classList.add('dashicons-before', 'dashicons-admin-generic');
}
return span;
};
const addSafeClasses = (el, list) => {
(list || [])
.map((c) => String(c).trim())
.filter((c) => c && /^[A-Za-z0-9_\-:]+$/.test(c) && c.length < 128)
.forEach((c) => el.classList.add(c));
};
const makeIconFromSnapshot = (it) => {
const span = document.createElement('span');
span.className = 'cch-icon';
const bg = (it.iconBg || '').trim();
const svg = (it.iconSvg || '').trim();
let sig = (it.iconClass || '').trim();
if (!bg && /^data:image\//i.test(sig)) {
span.style.backgroundImage = `url(${sig})`;
span.style.backgroundSize = 'cover';
return span;
}
if (!bg && /^url\(/i.test(sig)) {
span.style.backgroundImage = sig;
span.style.backgroundSize = 'cover';
return span;
}
if (!bg && /\.(svg|png|jpe?g|gif)(\?.*)?$/i.test(sig)) {
span.style.backgroundImage = `url(${sig})`;
span.style.backgroundSize = 'cover';
return span;
}
if (bg) {
if (/^url\(/i.test(bg)) span.style.backgroundImage = bg;
else span.style.backgroundImage = `url(${bg})`;
span.style.backgroundSize = 'cover';
return span;
}
if (svg) {
span.innerHTML = svg;
return span;
}
if (sig.startsWith('dashicons')) {
addSafeClasses(span, ['dashicons-before', sig]);
return span;
}
addSafeClasses(span, ['dashicons-before', 'dashicons-admin-generic']);
return span;
};
/* --- Loading State Logic --- */
const triggerLoading = (href) => {
const toggle = document.getElementById('cch-island-toggle');
const popout = document.getElementById('cch-popout');
const label = toggle ? toggle.querySelector('.cch-island-label') : null;
const icon = toggle ? toggle.querySelector('.cch-island-icon') : null;
const loader = toggle ? toggle.querySelector('.cch-island-loader') : null;
// Close popout
if (popout) popout.setAttribute('aria-hidden', 'true');
document.body.classList.remove('cch-lock-scroll');
// Set Island to loading state
if (toggle && label) {
toggle.classList.add('cch-loading');
// 1. Swap icon for loader
if(icon) icon.style.display = 'none';
if(loader) loader.style.display = 'block';
// 2. Format nice text for the label
try {
const url = new URL(href, window.location.origin);
const isExternal = url.origin !== window.location.origin;
let displayText = '';
if (isExternal) {
// Show hostname for external links
displayText = url.hostname + url.pathname;
} else {
// Internal: show pathname + search
displayText = url.pathname + url.search;
// Only simplify if there's more content after the prefix
const simplified = displayText
.replace(/^\/wp-admin\/(?=.+)/, '')
.replace(/^admin\.php\?page=/, '');
if (simplified && simplified !== displayText) {
displayText = simplified;
}
}
// Truncate if too long
if (displayText.length > 58) displayText = displayText.substring(0, 55) + '...';
label.textContent = 'Loading ' + displayText;
} catch(e) {
label.textContent = 'Loading...';
}
}
};
const handleLinkClick = (e) => {
const a = e.target.closest('a');
if (!a || !a.href || a.getAttribute('href') === '#') return;
if (a.target === '_blank' || e.metaKey || e.ctrlKey || e.shiftKey) return;
e.preventDefault();
triggerLoading(a.href);
// Check if we are in Divi (et_fb=1) or inside an iframe
const isDiviBuilder = window.location.search.includes('et_fb=1');
const isIframe = window.self !== window.top;
if (isDiviBuilder || isIframe) {
// Force navigation on the parent window to break out of the builder
window.top.location.href = a.href;
} else {
// Standard navigation
window.location.href = a.href;
}
};
/* --- Collectors --- */
const isSystemMenu = (li) => {
if (!li || !li.id) return false;
if (CORE_IDS.has(li.id)) return true;
try {
const a = qs('a', li);
const href = a ? new URL(a.href, location.origin).pathname : '';
return [
'/wp-admin/index.php',
'/wp-admin/upload.php',
'/wp-admin/edit.php',
'/wp-admin/edit.php?post_type=page',
'/wp-admin/themes.php',
'/wp-admin/plugins.php',
'/wp-admin/users.php',
'/wp-admin/tools.php',
'/wp-admin/options-general.php',
].some((p) => href.startsWith(p));
} catch { return false; }
};
const collectLeftMenus = () => {
const byLabel = (a, b) => a.label.localeCompare(b.label);
const system = [];
const extensions = [];
const hasSnap = Array.isArray(CFG.menuSnapshot) && CFG.menuSnapshot.length > 0;
if (hasSnap) {
const snap = CFG.menuSnapshot;
snap.forEach((it) => {
const item = {
label: sanitizeLabel(it.label || ''),
href: it.href || '#',
subs: Array.isArray(it.subs) ?
it.subs.map((s) => ({
label: sanitizeLabel(s.label || ''),
href: s.href || '#',
})) : [],
icon: makeIconFromSnapshot(it),
id: it.id || '',
};
if (item.id && CORE_IDS.has(item.id)) system.push(item);
else extensions.push(item);
});
system.sort(byLabel);
extensions.sort(byLabel);
return { system, extensions };
}
const adminMenuRoot = document.querySelector('#adminmenu');
if (adminMenuRoot) {
const items = Array.from(document.querySelectorAll('#adminmenu > li.menu-top')).filter(
(li) => !li.classList.contains('wp-menu-separator') && li.id !== 'collapse-menu'
);
items.forEach((li) => {
const a = li.querySelector('a');
if (!a) return;
const nameEl = li.querySelector('.wp-menu-name') || a;
const label = sanitizeLabel(visibleText(nameEl));
const href = a.getAttribute('href') || '#';
const subs = Array.from(li.querySelectorAll('ul.wp-submenu-wrap > li > a'))
.filter((x) => !x.classList.contains('wp-submenu-head'))
.map((x) => ({
label: sanitizeLabel(visibleText(x)),
href: x.getAttribute('href') || '#',
}));
const icon = copyIcon(li);
const item = { label, href, icon, subs, id: li.id || '' };
if (isSystemMenu(li)) system.push(item);
else extensions.push(item);
});
system.sort(byLabel);
extensions.sort(byLabel);
}
return { system, extensions };
};
const collectToolbar = () => {
const roots = [
qs('#wp-admin-bar-root-default'),
qs('#wp-admin-bar-top-secondary'),
].filter(Boolean);
const extras = [];
const system = [];
const SKIP_IDS = new Set(
Array.isArray(CFG.toolbarSkipIds) && CFG.toolbarSkipIds.length ?
CFG.toolbarSkipIds :
['wp-admin-bar-menu-toggle', 'wp-admin-bar-search', 'wp-admin-bar-cch-popout-toggle']
);
const ICON_MAP = Object.assign({
'wp-admin-bar-site-name': 'dashicons-admin-home',
'wp-admin-bar-new-content': 'dashicons-plus',
'wp-admin-bar-comments': 'dashicons-admin-comments',
'wp-admin-bar-updates': 'dashicons-update',
'wp-admin-bar-customize': 'dashicons-admin-customize',
'wp-admin-bar-search': 'dashicons-search',
'wp-admin-bar-my-account': 'dashicons-admin-users',
'wp-admin-bar-edit': 'dashicons-edit',
}, CFG.toolbarIconMap || {});
roots.forEach((root) => {
qsa(':scope > li', root).forEach((li) => {
const id = li.id || '';
if (SKIP_IDS.has(id)) return;
const anchor = qs('a.ab-item', li) || qs('.ab-item', li) || qs('[role="menuitem"]', li);
if (!anchor) return;
const subs = qsa('.ab-submenu a', li).map((x) => {
let label = sanitizeLabel(visibleText(x));
const href = x.getAttribute('href') || '#';
if (href.includes('profile.php') || href.includes('user-edit.php')) label = 'Edit Profile';
return { label: label, href: href };
});
let label = sanitizeLabel(visibleText(anchor));
if (!label) {
const aria = (anchor.getAttribute('aria-label') || '').trim();
const title = (anchor.getAttribute('title') || '').trim();
label = sanitizeLabel(aria || title);
}
if (!label) {
const sr = qs('.screen-reader-text', anchor);
if (sr) label = sanitizeLabel((sr.textContent || '').trim());
}
if (!label && subs.length) label = subs[0].label || '';
let href = anchor.getAttribute('href') || '';
if (!href && subs.length) href = subs[0].href || '#';
if (!href) href = '#';
if (!label && subs.length === 0) return;
const item = { id, label, href, subs, icon: null };
const mapped = ICON_MAP[id];
if (mapped) item.icon = makeDashiconIcon(mapped);
if (!item.icon) {
const candidates = [ anchor, anchor.querySelector('.dashicons'), anchor.querySelector("[class*='dashicons-']") ].filter(Boolean);
for (const el of candidates) {
const found = Array.from(el.classList).find((c) => c.startsWith('dashicons-'));
if (found) { item.icon = makeDashiconIcon(found); break; }
}
}
if (TOOLBAR_KEEP.has(id)) system.push(item);
else extras.push(item);
});
});
const byLabel = (a, b) => a.label.localeCompare(b.label);
system.sort(byLabel);
extras.sort(byLabel);
return { system, extras };
};
/* --- Builders --- */
const buildCard = (item) => {
// Card is a container div, main link is inside
const card = document.createElement('div');
card.className = 'cch-card';
card.setAttribute('data-label', (item.label || '').toLowerCase());
// Main link wrapper (header)
const mainLink = document.createElement('a');
mainLink.className = 'cch-card-main-link';
mainLink.href = item.href || '#';
const isExternalCard = applyExternalLinkStyles(mainLink, item.href);
const iconSlot = item.icon || document.createElement('span');
const content = document.createElement('div');
const title = document.createElement('div');
const meta = document.createElement('div');
title.className = 'cch-label';
title.textContent = item.label || '';
title.dataset.cchRaw = item.label || '';
if (isExternalCard) {
const icon = document.createElement('span');
icon.className = 'dashicons dashicons-external cch-external-link-icon';
title.appendChild(icon);
}
meta.className = 'cch-meta';
if (typeof item.meta === 'string') {
meta.textContent = item.meta;
} else if (typeof item.updateCount === 'number') {
meta.textContent = item.updateCount + ' updates';
} else {
meta.textContent = item.subs && item.subs.length ? item.subs.length + ' shortcuts' : 'Open';
}
content.appendChild(title);
content.appendChild(meta);
// Build main link
mainLink.addEventListener('click', handleLinkClick);
mainLink.appendChild(iconSlot);
mainLink.appendChild(content);
// Add main link to card
card.appendChild(mainLink);
if (item.subs && item.subs.length) {
const ul = document.createElement('ul');
ul.className = 'cch-submenu';
item.subs.forEach((s) => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = s.href || '#';
a.textContent = s.label || '';
a.dataset.cchRaw = s.label || '';
const isExternalSub = applyExternalLinkStyles(a, s.href);
if (isExternalSub) {
const icon = document.createElement('span');
icon.className = 'dashicons dashicons-external cch-external-link-icon';
a.appendChild(icon);
}
a.addEventListener('click', (e) => {
e.stopPropagation(); // prevent card click
handleLinkClick(e);
});
li.appendChild(a);
ul.appendChild(li);
});
card.appendChild(ul);
// Prevent main card click when clicking into the UL white space
ul.addEventListener('click', (e) => e.stopPropagation());
}
return card;
};
const buildSection = (title, items) => {
if (!items.length) return null;
const section = document.createElement('section');
section.className = 'cch-section';
const h = document.createElement('h3');
h.textContent = title;
const grid = document.createElement('div');
grid.className = 'cch-grid';
items.forEach((it) => grid.appendChild(buildCard(it)));
section.appendChild(h);
section.appendChild(grid);
return section;
};
const buildPopout = () => {
const pop = document.createElement('div');
pop.id = 'cch-popout';
pop.setAttribute('role', 'dialog');
pop.setAttribute('aria-modal', 'true');
pop.setAttribute('aria-hidden', 'true');
const wrap = document.createElement('div');
wrap.className = 'cch-popout-inner';
const header = document.createElement('div');
header.className = 'cch-popout-header';
const title = document.createElement('h2');
title.id = 'cch-popout-title';
title.textContent = 'Quick Menu';
const search = document.createElement('input');
search.type = 'search';
search.id = 'cch-popout-search';
search.placeholder = 'Filter apps and actions...';
const viewToggle = document.createElement('div');
viewToggle.className = 'cch-view-toggle';
viewToggle.setAttribute('role', 'group');
viewToggle.setAttribute('aria-label', 'Layout');
const btnCards = document.createElement('button');
btnCards.type = 'button';
btnCards.setAttribute('aria-label', 'Cards view');
btnCards.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="9" y="1" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="1" y="9" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="9" y="9" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/></svg><span>Cards</span>';
const btnExpanded = document.createElement('button');
btnExpanded.type = 'button';
btnExpanded.setAttribute('aria-label', 'Expanded view');
btnExpanded.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="14" height="4" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="1" y="7" width="14" height="4" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="1" y="13" width="14" height="2" rx="1" stroke="currentColor" stroke-width="1.5"/></svg><span>Expanded</span>';
viewToggle.appendChild(btnCards);
viewToggle.appendChild(btnExpanded);
const modeBtn = document.createElement('button');
modeBtn.type = 'button';
modeBtn.className = 'cch-btn-icon cch-mode-toggle';
modeBtn.title = 'Toggle Dark Mode';
// Icon set via function below
const helpBtn = document.createElement('button');
helpBtn.type = 'button';
helpBtn.className = 'cch-btn-icon cch-help';
helpBtn.setAttribute('aria-haspopup', 'dialog');
helpBtn.setAttribute('aria-expanded', 'false');
helpBtn.title = 'Help';
helpBtn.textContent = '?';
const close = document.createElement('button');
close.type = 'button';
close.className = 'cch-btn-icon cch-close';
close.setAttribute('aria-label', 'Close menu');
close.innerHTML = '<span class="dashicons dashicons-no-alt"></span>';
header.appendChild(title);
header.appendChild(search);
header.appendChild(viewToggle);
header.appendChild(modeBtn);
header.appendChild(helpBtn);
header.appendChild(close);
const sections = document.createElement('div');
sections.className = 'cch-sections';
// Data collection
const { system, extensions } = collectLeftMenus();
const { system: toolbarSystem, extras: toolbarExtras } = collectToolbar();
// Integrate Updates
const updatesIdx = toolbarExtras.findIndex((it) => it.id === 'wp-admin-bar-updates');
if (updatesIdx > -1) {
const u = toolbarExtras.splice(updatesIdx, 1)[0];
const icon = makeDashiconIcon('dashicons-update');
const hasUpdates = UPDATES_COUNT > 0;
const updatesItem = {
label: hasUpdates ? 'Updates available' : 'Up to date',
href: u.href || '#',
subs: u.subs || [],
icon,
updateCount: UPDATES_COUNT,
};
system.unshift(updatesItem);
}
// Integrate WooCommerce
const wcIdx = toolbarExtras.findIndex((it) => it.id === 'wp-admin-bar-woocommerce-site-visibility-badge');
if (wcIdx > -1) {
const w = toolbarExtras.splice(wcIdx, 1)[0];
const iconW = makeDashiconIcon('dashicons-cart');
extensions.unshift({ label: w.label || 'WooCommerce', href: w.href || '#', subs: w.subs || [], icon: iconW });
}
// Fallback Edit link when no homepage is assigned or on archive pages
const editIdx = toolbarSystem.findIndex((it) => it.id === 'wp-admin-bar-edit');
const editItem = editIdx > -1 ? toolbarSystem[editIdx] : null;
const hasValidEdit = editItem && editItem.href && editItem.href !== '#' && editItem.href.includes('post.php');
if (!hasValidEdit) {
// Remove invalid edit item if present
if (editIdx > -1) toolbarSystem.splice(editIdx, 1);
const editIcon = makeDashiconIcon('dashicons-edit');
toolbarSystem.push({
id: 'wp-admin-bar-edit-fallback',
label: 'Edit',
href: '/wp-admin/',
subs: [],
icon: editIcon,
});
}
const s1 = buildSection('System', system);
const s2 = buildSection('Extensions', extensions);
const s3 = toolbarExtras.length ? buildSection('Toolbar Extras', toolbarExtras) : null;
const s4 = buildSection('Toolbar', toolbarSystem);
if (s1) sections.appendChild(s1);
if (s2) sections.appendChild(s2);
if (s3) sections.appendChild(s3);
if (s4) sections.appendChild(s4);
wrap.appendChild(header);
wrap.appendChild(sections);
pop.appendChild(wrap);
// Help Overlay
const help = document.createElement('div');
help.id = 'cch-help';
help.setAttribute('aria-hidden', 'true');
const helpPanel = document.createElement('div');
helpPanel.className = 'cch-help-panel';
const helpTitle = document.createElement('h3');
helpTitle.textContent = 'Keyboard shortcuts';
const shortcut = getShortcutLabel();
const helplist = document.createElement('ul');
helplist.className = 'cch-help-list';
const items = [
`Open / Close menu: ${shortcut}`,
'Navigate: Arrow keys',
'Open selection: Enter',
'Filter: Type to search',
'Close: Esc',
];
items.forEach((txt) => {
const li = document.createElement('li');
li.textContent = txt;
helplist.appendChild(li);
});
const helpClose = document.createElement('button');
helpClose.type = 'button';
helpClose.className = 'cch-help-close';
helpClose.textContent = 'Close help';
helpPanel.appendChild(helpTitle);
helpPanel.appendChild(helplist);
helpPanel.appendChild(helpClose);
help.appendChild(helpPanel);
pop.appendChild(help);
document.body.appendChild(pop);
/* --- Helper Functions --- */
const showHelp = () => {
help.setAttribute('aria-hidden', 'false');
helpBtn.setAttribute('aria-expanded', 'true');
const btn = qs('.cch-help-close', help) || help;
btn.focus();
};
const hideHelp = () => {
help.setAttribute('aria-hidden', 'true');
helpBtn.setAttribute('aria-expanded', 'false');
helpBtn.focus();
};
const hide = () => {
pop.setAttribute('aria-hidden', 'true');
document.body.classList.remove('cch-lock-scroll');
setKeyboardMode(false);
hideHelp();
};
const show = () => {
pop.setAttribute('aria-hidden', 'false');
document.body.classList.add('cch-lock-scroll');
setKeyboardMode(false);
// Autofocus Search
requestAnimationFrame(() => {
setTimeout(() => { search.focus(); }, 10);
});
};
// --- Event Listeners ---
helpBtn.addEventListener('click', (e) => {
e.preventDefault();
if (help.getAttribute('aria-hidden') === 'false') hideHelp();
else showHelp();
});
help.addEventListener('click', (e) => {
if (e.target === help) hideHelp();
});
helpClose.addEventListener('click', hideHelp);
close.addEventListener('click', hide);
pop.addEventListener('click', (e) => {
if (e.target === pop || e.target === wrap) hide();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && pop.getAttribute('aria-hidden') === 'false') {
if (help.getAttribute('aria-hidden') === 'false') hideHelp();
else hide();
}
});
// Dark Mode
const sunIcon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
const moonIcon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
const updateModeIcon = (isDark) => {
modeBtn.innerHTML = isDark ? sunIcon : moonIcon;
modeBtn.title = isDark ? "Switch to Light Mode" : "Switch to Dark Mode";
};
const applyTheme = (theme) => {
const isDark = theme === 'dark';
document.body.classList.toggle('cch-dark-mode', isDark);
localStorage.setItem(THEME_KEY, theme);
updateModeIcon(isDark);
};
const currentTheme = localStorage.getItem(THEME_KEY) || 'dark';
applyTheme(currentTheme);
modeBtn.addEventListener('click', () => {
const current = document.body.classList.contains('cch-dark-mode') ? 'dark' : 'light';
applyTheme(current === 'dark' ? 'light' : 'dark');
});
// View Mode
const applyView = (view) => {
const expanded = view === 'expanded';
pop.classList.toggle('cch-view-expanded', expanded);
pop.classList.toggle('cch-view-cards', !expanded);
btnCards.classList.toggle('is-selected', !expanded);
btnExpanded.classList.toggle('is-selected', expanded);
localStorage.setItem(VIEW_KEY, expanded ? 'expanded' : 'cards');
// search.focus();
};
const initialView = localStorage.getItem(VIEW_KEY) || 'cards';
applyView(initialView);
btnCards.addEventListener('click', () => applyView('cards'));
btnExpanded.addEventListener('click', () => applyView('expanded'));
// --- Navigation Logic ---
qsa('.cch-section', pop).forEach((section) => {
const grid = qs('.cch-grid', section);
if (!grid) return;
qsa(':scope > .cch-card', grid).forEach((card, idx) => {
card.dataset.cchIdx = String(idx);
qsa(':scope .cch-submenu > li', card).forEach((li, sidx) => {
li.dataset.cchIdx = String(sidx);
});
});
});
let activeCard = null;
let activeSubIndex = -1;
let keyboardMode = false;
let lastHoverCard = null;
const setKeyboardMode = (on) => {
keyboardMode = !!on;
pop.classList.toggle('cch-keynav', keyboardMode);
};
const isVisible = (el) => !!(el && el.getClientRects && el.getClientRects().length);
const candidateCards = (root) => {
const list = qsa('.cch-card', root).filter(isVisible);
if (!root.classList.contains('cch-has-query')) return list;
return list.filter((c) => !c.classList.contains('cch-dim'));
};
const candidateSubs = (card, root) => {
const subs = qsa('.cch-submenu a', card);
// If searching, only return non-dimmed (matching) sub-items
if (root.classList.contains('cch-has-query')) {
return subs.filter((a) => !a.classList.contains('cch-dim'));
}
return subs;
};
const clearSubHighlight = () => { qsa('.cch-submenu a.cch-sub-active', pop).forEach((a) => a.classList.remove('cch-sub-active')); };
const syncSubmenuOpenState = () => {
qsa('.cch-card.cch-sub-open', pop).forEach((c) =>
c.classList.remove('cch-sub-open')
);
if (activeCard && activeSubIndex >= 0) {
activeCard.classList.add('cch-sub-open');
}
};
const updateSubHighlight = () => {
clearSubHighlight();
syncSubmenuOpenState();
if (!activeCard) return;
const subs = candidateSubs(activeCard, pop);
if (!subs.length) return;
if (activeSubIndex >= 0 && subs[activeSubIndex]) {
subs[activeSubIndex].classList.add('cch-sub-active');
subs[activeSubIndex].scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
};
const setActiveCard = (card) => {
if (activeCard === card) return;
if (activeCard) activeCard.classList.remove('cch-active');
activeCard = card && isVisible(card) ? card : null;
activeSubIndex = -1;
clearSubHighlight();
syncSubmenuOpenState();
if (activeCard) {
activeCard.classList.add('cch-active');
activeCard.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
};
const clearActiveByMouse = () => {
if (activeCard) activeCard.classList.remove('cch-active');
activeCard = null;
activeSubIndex = -1;
clearSubHighlight();
syncSubmenuOpenState();
setKeyboardMode(false);
};
const firstVisibleCard = () => candidateCards(pop)[0] || null;
const allCards = () => candidateCards(pop);
const pickStartCard = () => {
if (lastHoverCard && isVisible(lastHoverCard)) return lastHoverCard;
if (activeCard && isVisible(activeCard)) return activeCard;
return firstVisibleCard();
};
/* Spatial Navigation Logic */
const measureGlobalModel = () => {
const vis = candidateCards(pop);
const entries = vis.map((el) => {
const r = el.getBoundingClientRect();
return {
el, left: r.left, top: r.top, right: r.right, bottom: r.bottom,
cx: r.left + r.width / 2, cy: r.top + r.height / 2,
};
});
if (!entries.length) return { rows: [], byCol: [] };
entries.sort((a, b) => a.top !== b.top ? a.top - b.top : a.left - b.left);
const rows = [];
const ROW_TOL = 10;
entries.forEach((e) => {
const last = rows[rows.length - 1];
if (!last) { rows.push([e]); return; }
const rowTop = last[0].top;
if (Math.abs(e.top - rowTop) <= ROW_TOL) last.push(e);
else rows.push([e]);
});
rows.forEach((row) => row.sort((a, b) => a.left - b.left));
const firstRow = rows[0] || [];
const columns = firstRow.map((e) => e.cx);
const byCol = columns.map(() => []);
entries.forEach((e) => {
if (!columns.length) return;
let best = 0; let bestd = Infinity;
columns.forEach((cx, i) => {
const d = Math.abs(e.cx - cx);
if (d < bestd) { bestd = d; best = i; }
});
byCol[best].push(e);
});
byCol.forEach((col) => col.sort((a, b) => a.top - b.top));
return { rows, byCol };
};
const moveHorizontal = (delta) => {
const start = pickStartCard();
if (!start) return;
if (!activeCard) setActiveCard(start);
const { rows } = measureGlobalModel();
if (!rows.length) return;
let rowIdx = -1; let colIdx = -1;
for (let r = 0; r < rows.length; r++) {
const c = rows[r].findIndex((e) => e.el === activeCard);
if (c !== -1) { rowIdx = r; colIdx = c; break; }
}
if (rowIdx === -1) { setActiveCard(rows[0][0].el); return; }
const row = rows[rowIdx];
let nextRowIdx = rowIdx;
let nextColIdx = colIdx + (delta > 0 ? 1 : -1);
if (nextColIdx >= row.length) {
nextRowIdx = (rowIdx + 1) % rows.length;
nextColIdx = 0;
} else if (nextColIdx < 0) {
nextRowIdx = (rowIdx - 1 + rows.length) % rows.length;
const prevRow = rows[nextRowIdx];
nextColIdx = prevRow.length - 1;
}
setActiveCard(rows[nextRowIdx][nextColIdx].el);
};
const moveVerticalGlobal = (dir) => {
const start = pickStartCard();
if (!start) return;
if (!activeCard) setActiveCard(start);
const { byCol } = measureGlobalModel();
if (!byCol.length) return;
let colIdx = -1; let rowIdx = -1;
for (let c = 0; c < byCol.length; c++) {
const r = byCol[c].findIndex((e) => e.el === activeCard);
if (r !== -1) { colIdx = c; rowIdx = r; break; }
}
if (colIdx === -1) return;
const step = (c, r, d) => {
if (d > 0) {
if (r < byCol[c].length - 1) return { c, r: r + 1 };
return { c: (c + 1) % byCol.length, r: 0 };
} else {
if (r > 0) return { c, r: r - 1 };
const pc = (c - 1 + byCol.length) % byCol.length;
return { c: pc, r: byCol[pc].length - 1 };
}
};
const next = step(colIdx, rowIdx, dir);
setActiveCard(byCol[next.c][next.r].el);
};
const moveVertical = (dir) => {
const start = pickStartCard();
if (!start) return;
if (!activeCard) setActiveCard(start);
const subs = candidateSubs(activeCard, pop);
if (subs.length) {
if (activeSubIndex === -1) {
if (dir > 0) { activeSubIndex = 0; updateSubHighlight(); return; }
moveVerticalGlobal(-1);
const newSubs = candidateSubs(activeCard, pop);
if (newSubs.length) { activeSubIndex = newSubs.length - 1; updateSubHighlight(); }
return;
}
const nextIndex = activeSubIndex + dir;
if (nextIndex >= 0 && nextIndex < subs.length) {
activeSubIndex = nextIndex; updateSubHighlight(); return;
}
if (nextIndex < 0) { activeSubIndex = -1; updateSubHighlight(); return; }
if (nextIndex >= subs.length) {
activeSubIndex = -1; updateSubHighlight(); moveVerticalGlobal(+1); return;
}
} else {
if (dir < 0) {
moveVerticalGlobal(-1);
const newSubs = candidateSubs(activeCard, pop);
if (newSubs.length) { activeSubIndex = newSubs.length - 1; updateSubHighlight(); }
} else {
moveVerticalGlobal(+1);
}
return;
}
moveVerticalGlobal(dir);
};
const clickSelection = () => {
if (!activeCard) return;
const subs = candidateSubs(activeCard, pop);
if (subs.length && activeSubIndex >= 0 && subs[activeSubIndex]) {
subs[activeSubIndex].click();
return;
}
// Click the main link inside the card
const mainLink = qs('.cch-card-main-link', activeCard);
if (mainLink) mainLink.click();
};
const ensureActiveAfterFilter = () => {
const first = firstVisibleCard();
if (!first) { setActiveCard(null); return; }
if (!activeCard || !isVisible(activeCard)) { setActiveCard(first); }
};
// --- Search Logic ---
search.addEventListener('input', () => {
const q = search.value.trim().toLowerCase();
pop.classList.toggle('cch-has-query', q.length > 0);
// --- EASTER EGG START ---
const easterEggId = 'cch-easter-egg-card';
let eggCard = document.getElementById(easterEggId);
if (q === 'dark mode') {
if (!eggCard) {
// Create the Magic Card
const isActive = CFG.darkModeActive;
const eggItem = {
label: isActive ? 'Disable Dark Mode' : 'Enable Dark Mode',
href: '#',
icon: makeDashiconIcon('dashicons-art'), // Paintbrush icon
meta: 'Experimental Theme'
};
eggCard = buildCard(eggItem);
eggCard.id = easterEggId;
eggCard.classList.add('cch-card-easter-egg'); // For specific styling if needed
// Click Handler
eggCard.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const labelEl = eggCard.querySelector('.cch-label');
if(labelEl) labelEl.textContent = 'Toggling...';
// Toggle CSS immediately for feedback
const linkId = 'cch-dark-overrides-css';
let linkTag = document.getElementById(linkId);
if (linkTag) {
// Disable
linkTag.remove();
CFG.darkModeActive = false;
} else {
// Enable
linkTag = document.createElement('link');
linkTag.id = linkId;
linkTag.rel = 'stylesheet';
linkTag.href = CFG.darkCssUrl;
linkTag.media = 'all';
document.head.appendChild(linkTag);
CFG.darkModeActive = true;
}
// Persist via AJAX
const formData = new FormData();
formData.append('action', 'cch_toggle_dark_mode');
formData.append('nonce', CFG.nonce);
fetch(window.ajaxurl || '/wp-admin/admin-ajax.php', {
method: 'POST',
body: formData
}).then(() => {
// Update label after save
if(labelEl) labelEl.textContent = CFG.darkModeActive ? 'Disable Dark Mode' : 'Enable Dark Mode';
});
});
// Prepend to the first visible grid
const firstGrid = qs('.cch-grid', pop);
if (firstGrid) firstGrid.insertBefore(eggCard, firstGrid.firstChild);
}
} else if (q === 'refresh menu') {
if (!eggCard) {
const eggItem = {
label: 'Refresh Menu',
href: '#',
icon: makeDashiconIcon('dashicons-update'),
meta: 'Purge menu cache'
};
eggCard = buildCard(eggItem);
eggCard.id = easterEggId;
eggCard.classList.add('cch-card-easter-egg');
eggCard.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const labelEl = eggCard.querySelector('.cch-label');
if (labelEl) labelEl.textContent = 'Refreshing...';
const formData = new FormData();
formData.append('action', 'cch_purge_menu_cache');
formData.append('nonce', CFG.nonce);
fetch(window.ajaxurl || '/wp-admin/admin-ajax.php', {
method: 'POST',
body: formData
}).then(() => {
if (labelEl) labelEl.textContent = 'Done! Reloading...';
setTimeout(() => window.location.reload(), 500);
});
});
const firstGrid = qs('.cch-grid', pop);
if (firstGrid) firstGrid.insertBefore(eggCard, firstGrid.firstChild);
}
} else {
// Remove egg if search doesn't match exactly
if (eggCard) eggCard.remove();
}
// --- EASTER EGG END ---
qsa('.cch-card', pop).forEach((card) => {
const titleEl = qs('.cch-label', card);
const rawTitle = (titleEl && titleEl.dataset && titleEl.dataset.cchRaw) || (titleEl && titleEl.textContent) || '';
const titleMatch = q.length === 0 || rawTitle.toLowerCase().includes(q.toLowerCase());
highlightInto(titleEl, rawTitle, q);
const ul = qs('.cch-submenu', card);
let matchCount = 0;
if (ul) {
const lis = qsa(':scope > li', ul);
const matches = [];
const nonMatches = [];
lis.forEach((li) => {
const a = qs('a', li);
if (!a) return;
const raw = (a.dataset && a.dataset.cchRaw) || a.textContent || '';
const isMatch = q.length === 0 || raw.toLowerCase().includes(q.toLowerCase());
highlightInto(a, raw, q);
if (isMatch) { a.classList.remove('cch-dim'); matchCount++; matches.push(li); }
else { a.classList.add('cch-dim'); nonMatches.push(li); }
});
const byIdx = (a, b) => (+a.dataset.cchIdx || 0) - (+b.dataset.cchIdx || 0);
matches.sort(byIdx);
nonMatches.sort(byIdx);
[...matches, ...nonMatches].forEach((li) => ul.appendChild(li));
}
const isCardMatch = titleMatch || matchCount > 0;
card.classList.toggle('cch-dim', q.length > 0 && !isCardMatch);
card.classList.toggle('cch-sub-open', q.length > 0 && matchCount > 0 && !titleMatch);
});
// Reorder sections
qsa('.cch-section', pop).forEach((section) => {
const grid = qs('.cch-grid', section);
if (!grid) return;
const cards = qsa(':scope > .cch-card', grid);
const matches = [];
const nonMatches = [];
cards.forEach((card) => {
if (card.classList.contains('cch-dim')) nonMatches.push(card);
else matches.push(card);
});
const byIdx = (a, b) => (+a.dataset.cchIdx || 0) - (+b.dataset.cchIdx || 0);
matches.sort(byIdx);
nonMatches.sort(byIdx);
[...matches, ...nonMatches].forEach((c) => grid.appendChild(c));
const matchCount = section.querySelectorAll('.cch-card:not(.cch-dim)').length;
const hide = search.value.trim().length > 0 && matchCount === 0;
section.classList.toggle('cch-hidden', hide);
});
activeSubIndex = -1;
updateSubHighlight();
const first = allCards()[0] || null;
if (!first) setActiveCard(null);
else if (!activeCard || activeCard.classList.contains('cch-dim')) setActiveCard(first);
});
// Mouse behavior
pop.addEventListener('mouseover', (e) => {
const hovered = e.target.closest('.cch-card');
if (!hovered) return;
lastHoverCard = hovered;
if (keyboardMode && activeCard && hovered !== activeCard) {
clearActiveByMouse();
}
});
// --- Keyboard Handler ---
const keyIs = (e, names, codes) => {
const k = (e.key || '').toLowerCase();
const c = (e.code || '').toLowerCase();
const kc = e.keyCode || e.which || 0;
return (
names.some((n) => k === n.toLowerCase()) ||
names.some((n) => c === n.toLowerCase()) ||
(codes && codes.includes(kc))
);
};
// Helper to check if active card is in top row of grid
const isAtTopRow = () => {
if (!activeCard) return false;
const { rows } = measureGlobalModel();
if (!rows.length) return false;
return rows[0].some(r => r.el === activeCard);
};
const handleNavKey = (e) => {
if (pop.getAttribute('aria-hidden') === 'true') return;
if (e.defaultPrevented || e.isComposing) return;
const isSearchFocused = document.activeElement === search;
// ENTER
if (keyIs(e, ['Enter'], [13])) {
e.preventDefault();
clickSelection();
return;
}
// ARROW DOWN
if (keyIs(e, ['ArrowDown', 'Down'], [40])) {
// CASE 1: Starting navigation from Search Bar or no selection
if (isSearchFocused || !activeCard) {
e.preventDefault();
setKeyboardMode(true);
// Simple Logic: Just grab the very first visible card in the DOM
// Do not use pickStartCard() or spatial logic here.
const first = firstVisibleCard();
if (first) {
search.blur();
setActiveCard(first);
// If the first card has search matches inside (pills),
// automatically select the first matching pill.
const subs = candidateSubs(first, pop);
if (subs.length > 0) {
activeSubIndex = 0;
updateSubHighlight();
}
}
return;
}
// CASE 2: Already navigating the grid
e.preventDefault();
setKeyboardMode(true);
// If we are on a card, check if we should enter its sub-menu
const subs = candidateSubs(activeCard, pop);
if (subs.length && activeSubIndex === -1) {
activeSubIndex = 0; updateSubHighlight();
} else {
// Otherwise move to the card below visually
moveVertical(+1);
}
return;
}
// ARROW UP
if (keyIs(e, ['ArrowUp', 'Up'], [38])) {
if (isSearchFocused) return; // Allow normal cursor movement
e.preventDefault();
setKeyboardMode(true);
// If inside submenu, move up
if (activeSubIndex > 0) {
activeSubIndex--; updateSubHighlight();
return;
}
// If at top of submenu, return to Card
if (activeSubIndex === 0) {
activeSubIndex = -1; updateSubHighlight();
return;
}
// If at Card, check if we should jump to Search
if (isAtTopRow()) {
clearActiveByMouse();
search.focus();
search.select(); // Select text so typing replaces it
return;
}
// Otherwise move card up
moveVertical(-1);
return;
}
// ARROW LEFT / RIGHT
if (keyIs(e, ['ArrowLeft', 'Left', 'ArrowRight', 'Right'], [37, 39])) {
if (isSearchFocused) return; // Allow text nav
e.preventDefault();
setKeyboardMode(true);
activeSubIndex = -1;
updateSubHighlight();
const dir = keyIs(e, ['ArrowRight', 'Right'], [39]) ? 1 : -1;
moveHorizontal(dir);
return;
}
// TYPE TO SEARCH (Auto-focus)
// If user types a letter while grid is focused, jump to search
if (!isSearchFocused && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
search.focus();
// do not prevent default, allow char to be typed
}
};
document.addEventListener('keydown', handleNavKey);
return { pop, show, hide };
};
const init = () => {
// 1. Detect if we are inside the Site Editor (FSE) Iframe
// If so, remove this instance of the island to prevent double loading.
try {
if (window.self !== window.top && window.parent.location.href.includes('site-editor.php')) {
const island = document.getElementById('cch-island-container');
if (island) island.remove();
// Also remove the admin bar shim if it exists to prevent spacing issues
const html = document.documentElement;
if (html) html.style.marginTop = '0px';
return;
}
} catch (e) {
// Cross-origin errors or other issues; ignore.
}
document.body.classList.add('cch-hide-admin-menu');
let pop, show, hide;
try {
const result = buildPopout();
pop = result.pop;
show = result.show;
hide = result.hide;
} catch (e) {
console.error('CaptainCore Helm: Error building popout', e);
return;
}
// Attach listeners to context menu if it exists (rendered by PHP)
const contextMenu = document.getElementById('cch-context-menu');
if (contextMenu) {
const label = contextMenu.querySelector('.cch-context-label');
const links = Array.from(contextMenu.querySelectorAll('.cch-context-item'));
let focusIndex = -1;
const updateFocus = () => {
links.forEach((link, idx) => {
if (idx === focusIndex) {
link.classList.add('cch-focus');
link.scrollIntoView({ block: 'nearest' });
} else {
link.classList.remove('cch-focus');
}
});
};
const resetFocus = () => {
focusIndex = -1;
updateFocus();
};
const closeContextMenu = () => {
contextMenu.classList.remove('cch-context-open');
resetFocus();
};
if (label) {
label.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = contextMenu.classList.contains('cch-context-open');
if (isOpen) {
closeContextMenu();
} else {
contextMenu.classList.add('cch-context-open');
}
});
}
links.forEach(link => {
// Mouse interaction should clear keyboard focus to prevent confusion
link.addEventListener('mouseenter', () => {
focusIndex = -1;
updateFocus();
});
link.addEventListener('click', (e) => {
closeContextMenu();
handleLinkClick(e);
});
});
document.addEventListener('click', (e) => {
if (!contextMenu.contains(e.target)) {
closeContextMenu();
}
});
// Keyboard Navigation for Context Menu
document.addEventListener('keydown', (e) => {
if (!contextMenu.classList.contains('cch-context-open')) return;
if (e.key === 'Escape') {
e.preventDefault();
closeContextMenu();
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
focusIndex++;
if (focusIndex >= links.length) focusIndex = 0;
updateFocus();
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
if (focusIndex === -1) focusIndex = links.length;
focusIndex--;
if (focusIndex < 0) focusIndex = links.length - 1;
updateFocus();
return;
}
if (e.key === 'Enter') {
// If an item is focused via keyboard
if (focusIndex > -1 && links[focusIndex]) {
e.preventDefault();
e.stopPropagation();
links[focusIndex].click();
return;
}
// If focus is on the label itself (e.g. user tabbed to it)
// we allow default behavior or toggle logic, but if menu is open
// and no item selected, we might want to close it or do nothing.
}
});
}
const islandContainer = document.getElementById('cch-island-container');
const islandToggle = document.getElementById('cch-island-toggle');
const hideBtn = document.getElementById('cch-ui-hide-btn');
const toast = document.getElementById('cch-ui-toast');
// --- Hide Logic ---
if (hideBtn && islandContainer) {
hideBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Close context menu if open
if (contextMenu) contextMenu.classList.remove('cch-context-open');
// Hide Island (non-persistent, reloading restores)
islandContainer.classList.add('cch-ui-hidden');
// Show Toast
if (toast) {
toast.classList.add('cch-show');
setTimeout(() => {
toast.classList.remove('cch-show');
}, 4000);
}
});
}
// --- Main Toggle Logic ---
if (islandToggle) {
islandToggle.addEventListener('click', (e) => {
e.preventDefault();
if(islandToggle.classList.contains('cch-loading')) return;
const isOpen = pop.getAttribute('aria-hidden') === 'false';
if (isOpen) hide();
else show();
});
}
// Handle Edit/View action buttons
const actionButtons = document.querySelectorAll('.cch-island-action');
actionButtons.forEach((btn) => {
btn.addEventListener('click', (e) => {
const href = btn.getAttribute('href');
if (href && href !== '#') {
e.preventDefault();
triggerLoading(href);
window.location.href = href;
}
});
});
// --- Query Monitor Integration ---
const qmBtn = document.getElementById('cch-island-qm');
if (qmBtn) {
// 1. Toggle Logic
qmBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// QM attaches click listeners to the admin bar link.
// We simulate a click on the hidden admin bar link if possible,
// or toggle the class directly if we know the ID.
const qmBarLink = document.querySelector('#wp-admin-bar-query-monitor > a');
const qmPanel = document.getElementById('query-monitor-main');
if (qmPanel) {
if (qmPanel.classList.contains('qm-show')) {
qmPanel.classList.remove('qm-show');
document.body.classList.remove('qm-show'); // QM sometimes adds this
} else {
qmPanel.classList.add('qm-show');
// Ensure it has height
if (qmPanel.offsetHeight < 50) qmPanel.style.height = '400px';
}
} else if (qmBarLink) {
qmBarLink.click();
}
});
// 2. Status Sync (Color)
// Query Monitor applies classes like .qm-error, .qm-warning to the admin bar item.
// We want to copy those to our Helm button.
const syncQmStatus = () => {
const qmBarItem = document.getElementById('wp-admin-bar-query-monitor');
if (!qmBarItem) return;
const classes = ['qm-error', 'qm-warning', 'qm-alert'];
classes.forEach(cls => {
if (qmBarItem.classList.contains(cls)) {
qmBtn.classList.add(cls);
} else {
qmBtn.classList.remove(cls);
}
});
};
// Run on load
syncQmStatus();
// Observe changes (QM updates via AJAX sometimes)
const qmBarItem = document.getElementById('wp-admin-bar-query-monitor');
if (qmBarItem) {
const observer = new MutationObserver(syncQmStatus);
observer.observe(qmBarItem, { attributes: true, attributeFilter: ['class'] });
}
}
// --- Keyboard Logic ---
document.addEventListener('keydown', (e) => {
if (!isOpenShortcut(e)) return;
e.preventDefault();
// Special Case: If UI is hidden, restore it first.
if (islandContainer && islandContainer.classList.contains('cch-ui-hidden')) {
islandContainer.classList.remove('cch-ui-hidden');
// Proceed to open the menu for immediate feedback.
}
const isOpen = pop.getAttribute('aria-hidden') === 'false';
if (isOpen) hide();
else show();
});
// Global link click listener for loading state
document.addEventListener('click', (e) => {
const a = e.target.closest('a');
if (!a || !a.href || e.defaultPrevented) return;
// Ignore if inside popout (handled specifically)
if (pop.contains(a)) return;
// Ignore modifiers
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
if (a.target === '_blank') return;
// Ignore anchors, js, etc
const href = a.getAttribute('href');
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
if (a.classList.contains('thickbox') || a.classList.contains('button-disabled') || a.hasAttribute('download')) return;
// Check for same-page hash navigation
try {
const url = new URL(a.href, window.location.origin);
if (url.origin === window.location.origin &&
url.pathname === window.location.pathname &&
url.search === window.location.search &&
url.hash) {
return;
}
} catch (err) { /* ignore invalid URLs */ }
// Trigger visual loading state
triggerLoading(a.href);
});
// Clear loading state when navigating back (bfcache)
window.addEventListener('pageshow', (e) => {
if (e.persisted) {
const toggle = document.getElementById('cch-island-toggle');
if (toggle) {
toggle.classList.remove('cch-loading');
const label = toggle.querySelector('.cch-island-label');
const icon = toggle.querySelector('.cch-island-icon');
const loader = toggle.querySelector('.cch-island-loader');
if (label) label.textContent = 'Menu';
if (icon) icon.style.display = '';
if (loader) loader.style.display = 'none';
}
}
});
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();