UNPKG

solid-panes

Version:

Solid-compatible Panes: applets and views for the mashlib and databrowser

409 lines (408 loc) 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setActiveMenuPane = exports.refreshMenu = exports.createLeftSideMenu = void 0; require("./menu.css"); var _solidLogic = require("solid-logic"); var _rdflib = require("rdflib"); var _ownerProfile = require("../profileUtils/ownerProfile"); var _menu2 = _interopRequireDefault(require("../icons/menu.svg?raw")); var _iconHelper = require("../icons/iconHelper"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const MENU_ICON = (0, _iconHelper.createUiIcon)(_menu2.default, 'Menu Icon', '#ffffff'); const MENU_COLLAPSED_KEY = 'solid-panes-menu-collapsed'; let menuCollapsed = false; const loadMenuCollapsedState = () => { try { return localStorage.getItem(MENU_COLLAPSED_KEY) === 'true'; } catch (error) { return false; } }; const saveMenuCollapsedState = collapsed => { try { localStorage.setItem(MENU_COLLAPSED_KEY, String(collapsed)); } catch (error) { // ignore storage errors } }; const updateMenuCollapseButton = () => { const collapseBtn = document.getElementById('MenuCollapseBtn'); if (!collapseBtn) return; collapseBtn.textContent = menuCollapsed ? '\u203A' : '\u2039'; collapseBtn.setAttribute('aria-label', menuCollapsed ? 'Expand navigation menu' : 'Collapse navigation menu'); }; const updateCollapseButtonPosition = (navMenu, collapseBtn) => { if (!navMenu || !collapseBtn) return; const navRect = navMenu.getBoundingClientRect(); const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16; const buttonWidth = parseFloat(getComputedStyle(collapseBtn).width) || 1.25 * rootFontSize; const offsetRem = (navRect.width - buttonWidth / 2) / rootFontSize; collapseBtn.style.setProperty('left', `${offsetRem.toFixed(3)}rem`, 'important'); }; const applyMenuCollapsedState = navMenu => { if (!navMenu) return; navMenu.classList.toggle('collapsed', menuCollapsed); updateMenuCollapseButton(); const collapseBtn = document.getElementById('MenuCollapseBtn'); updateCollapseButtonPosition(navMenu, collapseBtn); }; const isLoggedIn = () => Boolean(_solidLogic.authSession?.info?.isLoggedIn); const ensureMenuSkeleton = () => { menuCollapsed = loadMenuCollapsedState(); const root = document.querySelector('[role="main"]') || document.body; let navMenu = document.getElementById('NavMenu'); if (!navMenu) { navMenu = document.createElement('nav'); navMenu.id = 'NavMenu'; navMenu.className = 'app-nav'; navMenu.setAttribute('aria-label', 'App navigation'); navMenu.hidden = true; navMenu.style.display = 'none'; const headerEl = document.createElement('div'); headerEl.className = 'menu-header'; const closeBtn = document.createElement('button'); closeBtn.id = 'MenuCloseBtn'; closeBtn.className = 'menu-close'; closeBtn.type = 'button'; closeBtn.setAttribute('aria-label', 'Close menu'); closeBtn.textContent = '✕'; headerEl.appendChild(closeBtn); const title = document.createElement('span'); title.className = 'menu-header-title'; title.textContent = 'Menu'; headerEl.appendChild(title); navMenu.appendChild(headerEl); const authStateEl = document.createElement('div'); authStateEl.id = 'AuthState'; authStateEl.className = 'menu-auth-state'; navMenu.appendChild(authStateEl); const contentEl = document.createElement('div'); contentEl.id = 'NavMenuContent'; contentEl.className = 'menu-content'; navMenu.appendChild(contentEl); root.insertBefore(navMenu, root.firstChild); } else if (!navMenu.querySelector('.menu-header')) { const headerEl = document.createElement('div'); headerEl.className = 'menu-header'; const closeBtn = document.createElement('button'); closeBtn.id = 'MenuCloseBtn'; closeBtn.className = 'menu-close'; closeBtn.type = 'button'; closeBtn.setAttribute('aria-label', 'Close menu'); closeBtn.textContent = '✕'; headerEl.appendChild(closeBtn); const title = document.createElement('span'); title.className = 'menu-header-title'; title.textContent = 'Menu'; headerEl.appendChild(title); navMenu.insertBefore(headerEl, navMenu.firstChild); } let toggle = document.getElementById('MenuToggleBtn'); if (!toggle) { toggle = document.createElement('button'); toggle.id = 'MenuToggleBtn'; toggle.className = 'menu-toggle'; toggle.type = 'button'; toggle.setAttribute('aria-label', 'Toggle navigation menu'); toggle.hidden = true; const toggleImg = document.createElement('img'); toggleImg.src = MENU_ICON; toggleImg.alt = ''; toggleImg.className = 'menu-toggle-icon'; toggleImg.setAttribute('aria-hidden', 'true'); toggle.appendChild(toggleImg); const toggleLabel = document.createElement('span'); toggleLabel.id = 'MenuToggleLabel'; toggleLabel.className = 'menu-toggle-label'; toggle.appendChild(toggleLabel); root.insertBefore(toggle, root.firstChild); } let overlay = document.getElementById('MenuOverlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'MenuOverlay'; overlay.className = 'menu-overlay'; overlay.hidden = true; document.body.appendChild(overlay); } let collapseBtn = document.getElementById('MenuCollapseBtn'); if (!collapseBtn) { collapseBtn = document.createElement('button'); collapseBtn.id = 'MenuCollapseBtn'; collapseBtn.className = 'menu-collapse'; collapseBtn.type = 'button'; collapseBtn.setAttribute('aria-label', 'Collapse navigation menu'); collapseBtn.textContent = '\u2039'; collapseBtn.hidden = true; document.body.appendChild(collapseBtn); } }; const getMenuItems = async (subject, outliner) => { try { const items = await outliner.getDashboardItems(subject); return items.map(element => { const targetSubject = element.subject || _solidLogic.authn.currentUser() || subject; return { icon: element.icon, subject: targetSubject, paneName: element.tabName || element.paneName, label: element.label, onclick: () => openDashboardPane(targetSubject, outliner, element.tabName || element.paneName) }; }); } catch (error) { console.error('Unable to load navigation menu items', error); return []; } }; const createMenuButton = item => { const button = document.createElement('button'); button.className = 'menu-item'; button.type = 'button'; if (item.paneName) { button.dataset.paneName = item.paneName; } if (item.icon) { const icon = document.createElement('img'); icon.className = 'menu-item-icon'; icon.src = item.icon; icon.alt = ''; icon.setAttribute('aria-hidden', 'true'); button.appendChild(icon); } const label = document.createElement('span'); label.className = 'menu-item-label'; label.textContent = item.label; button.appendChild(label); if (item.id) { button.id = item.id; } button.addEventListener('click', async () => { await item.onclick(); }); return button; }; const setActiveMenuItem = (container, paneName) => { const menuItems = Array.from(container.querySelectorAll('.menu-item')); let activeItem; menuItems.forEach(item => { const isActive = Boolean(paneName) && item.dataset.paneName === paneName; item.classList.toggle('menu-item-active', isActive); if (isActive) { item.setAttribute('aria-current', 'page'); activeItem = item; } else { item.removeAttribute('aria-current'); } }); if (paneName) { if (!activeItem && menuItems[0]) { // If an explicit pane name is provided but does not match any menu // item, fall back to clearing selection rather than selecting the first item. container.dataset.activePaneName = ''; } else { container.dataset.activePaneName = paneName; } } else { // If there is no active pane, do not auto-select the first menu item. container.dataset.activePaneName = ''; } updateToggleLabel(activeItem); }; const setActiveMenuPane = paneName => { const navMenuContent = document.getElementById('NavMenuContent'); if (!navMenuContent) return; setActiveMenuItem(navMenuContent, paneName); }; exports.setActiveMenuPane = setActiveMenuPane; const updateToggleLabel = activeItem => { const toggleLabel = document.getElementById('MenuToggleLabel'); if (!toggleLabel) return; toggleLabel.textContent = activeItem?.querySelector('.menu-item-label')?.textContent || ''; }; const renderMenuItems = async (subject, outliner, container) => { const menuItems = await getMenuItems(subject, outliner); container.replaceChildren(...menuItems.map(createMenuButton)); setActiveMenuItem(container, container.dataset.activePaneName); }; const refreshMenu = layout => { const navMenu = document.getElementById('NavMenu'); const toggle = document.getElementById('MenuToggleBtn'); const collapseBtn = document.getElementById('MenuCollapseBtn'); const overlay = document.getElementById('MenuOverlay'); if (!navMenu || !toggle || !overlay || !collapseBtn) return; const loggedIn = isLoggedIn(); if (!loggedIn) { navMenu.hidden = true; navMenu.style.display = 'none'; toggle.hidden = true; toggle.style.display = 'none'; collapseBtn.hidden = true; collapseBtn.style.display = 'none'; overlay.hidden = true; overlay.style.display = 'none'; return; } if (layout === 'mobile') { navMenu.classList.add('mobile-hidden'); navMenu.classList.remove('mobile-visible'); toggle.hidden = false; collapseBtn.hidden = true; overlay.hidden = true; navMenu.hidden = false; navMenu.classList.remove('collapsed'); toggle.setAttribute('aria-expanded', 'false'); } else { navMenu.classList.remove('mobile-hidden', 'mobile-visible'); toggle.hidden = true; collapseBtn.hidden = false; overlay.hidden = true; navMenu.hidden = false; applyMenuCollapsedState(navMenu); updateCollapseButtonPosition(navMenu, collapseBtn); toggle.setAttribute('aria-expanded', 'false'); } }; exports.refreshMenu = refreshMenu; const createLeftSideMenu = async (subject, outliner) => { ensureMenuSkeleton(); const navMenu = document.getElementById('NavMenu'); const menuToggle = document.getElementById('MenuToggleBtn'); const menuOverlay = document.getElementById('MenuOverlay'); const navMenuContent = document.getElementById('NavMenuContent'); const closeMobileMenu = () => { if (!navMenu || !menuToggle || !menuOverlay) return; navMenu.classList.remove('mobile-visible'); navMenu.classList.add('mobile-hidden'); menuToggle.setAttribute('aria-expanded', 'false'); menuOverlay.hidden = true; }; const openMobileMenu = () => { if (!navMenu || !menuToggle || !menuOverlay) return; navMenu.classList.remove('mobile-hidden'); navMenu.classList.add('mobile-visible'); menuToggle.setAttribute('aria-expanded', 'true'); menuOverlay.hidden = false; }; const collapseBtn = document.getElementById('MenuCollapseBtn'); const expandDesktopMenu = () => { if (!navMenu || !collapseBtn) return; if (!menuCollapsed) return; menuCollapsed = false; saveMenuCollapsedState(menuCollapsed); applyMenuCollapsedState(navMenu); }; if (navMenu) { navMenu.addEventListener('click', event => { const isMobile = outliner.context?.environment?.layout === 'mobile'; const clickedCollapseButton = event.target.closest('#MenuCollapseBtn'); if (!isMobile && menuCollapsed && !clickedCollapseButton) { expandDesktopMenu(); } }); } if (menuToggle) { menuToggle.addEventListener('click', () => { if (navMenu?.classList.contains('mobile-visible')) { closeMobileMenu(); } else { openMobileMenu(); } }); } if (collapseBtn) { collapseBtn.addEventListener('click', () => { menuCollapsed = !menuCollapsed; saveMenuCollapsedState(menuCollapsed); applyMenuCollapsedState(navMenu); }); } const updateMenuVisibility = () => { const loggedIn = isLoggedIn(); if (navMenu) { navMenu.hidden = !loggedIn; navMenu.style.display = loggedIn ? '' : 'none'; } if (menuToggle) { menuToggle.hidden = !loggedIn; menuToggle.style.display = loggedIn ? '' : 'none'; } if (collapseBtn) { collapseBtn.hidden = !loggedIn; collapseBtn.style.display = loggedIn ? '' : 'none'; } if (menuOverlay) { menuOverlay.hidden = !loggedIn; menuOverlay.style.display = loggedIn ? '' : 'none'; } }; updateMenuVisibility(); if (menuOverlay) { menuOverlay.addEventListener('click', closeMobileMenu); } const closeBtn = document.getElementById('MenuCloseBtn'); if (closeBtn) { closeBtn.addEventListener('click', closeMobileMenu); } if (navMenuContent) { updateMenuVisibility(); const loggedIn = isLoggedIn(); if (loggedIn) { await renderMenuItems(subject, outliner, navMenuContent); } if (!navMenuContent.dataset.authEventsBound) { const refreshMenuItems = async () => { await renderMenuItems(subject, outliner, navMenuContent); }; _solidLogic.authSession.events.on('login', () => { updateMenuVisibility(); refreshMenu(outliner.context?.environment?.layout === 'mobile' ? 'mobile' : 'desktop'); refreshMenuItems(); }); _solidLogic.authSession.events.on('logout', () => { updateMenuVisibility(); refreshMenu(outliner.context?.environment?.layout === 'mobile' ? 'mobile' : 'desktop'); refreshMenuItems(); }); _solidLogic.authSession.events.on('sessionRestore', () => { updateMenuVisibility(); refreshMenu(outliner.context?.environment?.layout === 'mobile' ? 'mobile' : 'desktop'); refreshMenuItems(); }); navMenuContent.dataset.authEventsBound = 'true'; } navMenuContent.addEventListener('click', event => { const item = event.target.closest('.menu-item'); if (item instanceof HTMLButtonElement) { setActiveMenuItem(navMenuContent, item.dataset.paneName); } const isMobile = outliner.context?.environment?.layout === 'mobile'; if (item && isMobile) { closeMobileMenu(); } }); } refreshMenu(outliner.context?.environment?.layout === 'mobile' ? 'mobile' : 'desktop'); }; exports.createLeftSideMenu = createLeftSideMenu; async function openDashboardPane(subject, outliner, pane) { const me = _solidLogic.authn.currentUser(); if (!subject) { if (me) { subject = me; } else { const store = outliner?.context?.store || outliner?.context?.session?.store || outliner?.kb; const fetcher = outliner?.context?.fetcher || store?.fetcher; if (!store || !fetcher) { throw new Error('Unable to load profile: missing RDF store or fetcher'); } const fallbackUri = (0, _rdflib.sym)(window.location.href); subject = await (0, _ownerProfile.loadProfileFromURI)(subject || fallbackUri, store, fetcher); } } outliner.showDashboard(subject, { pane }); }