UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

176 lines (156 loc) 5.69 kB
import { parseBracketedKeyValueHash, createBracketedKeyValueHash, } from "../../../text/bracketed-key-value-hash.mjs"; /** * Synchronizes a <monster-tabs> instance with the URL hash, * including active tab and all existing tab IDs. * * @param {HTMLElement} tabsEl - The monster-tabs element * @param {string} selector - Hash selector name (e.g. "tabs", "tabs2") * @param {string} activeKey - Key for the active tab (default: "active") * @param {string} allTabsKey - Key for all tab IDs (default: "all") */ export function attachTabsHashSync( tabsEl, selector = "tabs", activeKey = "active", allTabsKey = "all", ) { if (!(tabsEl instanceof HTMLElement)) { throw new TypeError("Expected a monster-tabs HTMLElement"); } let lastKnownActiveTabId = null; let lastKnownAllTabIds = []; /** * Reads active and all tab IDs from the URL hash. * @returns {{activeTabId: string|null, allTabIds: string[]}} */ function getTabStateFromHash() { const hashObj = parseBracketedKeyValueHash(location.hash); const tabsData = hashObj?.[selector] ?? {}; const activeTabId = tabsData[activeKey] ?? null; const allTabIdsString = tabsData[allTabsKey] ?? ""; const allTabIds = allTabIdsString .split(",") .filter((id) => id.trim() !== ""); return { activeTabId, allTabIds }; } /** * Synchronizes tab state from hash on page load and hash changes. */ function syncFromHash() { const { activeTabId, allTabIds } = getTabStateFromHash(); // Sync active tab if (activeTabId && activeTabId !== lastKnownActiveTabId) { tabsEl.activeTab(activeTabId); lastKnownActiveTabId = activeTabId; } // Sync all tabs (add/remove tabs based on hash) const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id")); // Add tabs that are in hash but not in DOM for (const tabId of allTabIds) { if (!currentTabs.includes(tabId)) { // You'll need a way to get the label and content for new tabs // This is a placeholder. You might fetch it or have a default. // For existing tabs in the HTML, we're just ensuring they are present. // For truly new tabs from the hash, you'd need their content/label. // For this example, we'll assume the initial HTML already has all potential tabs, // and we're just making sure they are displayed if their IDs are in the hash. // If you truly want to create new tabs from scratch based on the hash, // you'd need more information in the hash or a lookup mechanism. console.warn( `Tab with ID '${tabId}' found in hash but not in DOM. Add logic to create it if necessary.`, ); } } // Remove tabs that are in DOM but not in hash for (const tabId of currentTabs) { if (!allTabIds.includes(tabId)) { tabsEl.removeTab(tabId); } } lastKnownAllTabIds = allTabIds; } window.addEventListener("hashchange", syncFromHash); syncFromHash(); // initial load /** * Writes the current active tab and all tab IDs to the URL hash. * @param {string|null} activeTabId - The ID of the currently active tab. * @param {string[]} allTabIds - An array of all current tab IDs. */ function writeHash(activeTabId, allTabIds) { const hashObj = parseBracketedKeyValueHash(location.hash); hashObj[selector] = { ...(hashObj[selector] ?? {}) }; if (activeTabId) { hashObj[selector][activeKey] = activeTabId; } else { delete hashObj[selector][activeKey]; } if (allTabIds.length > 0) { hashObj[selector][allTabsKey] = allTabIds.join(","); } else { delete hashObj[selector][allTabsKey]; } const newHash = createBracketedKeyValueHash(hashObj); if (location.hash !== newHash) { const scrollX = window.scrollX; const scrollY = window.scrollY; history.replaceState(null, "", newHash); window.scrollTo(scrollX, scrollY); } lastKnownActiveTabId = activeTabId; lastKnownAllTabIds = allTabIds; } // Listen for tab changes (active tab) tabsEl.addEventListener("monster-tab-changed", (e) => { if (e.target !== tabsEl) return; // Ignore bubbled events const newActiveTabId = e.detail?.reference; const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id")); writeHash(newActiveTabId, currentTabs); }); // Listen for tab additions const observer = new MutationObserver((mutationsList) => { let tabsChanged = false; for (const mutation of mutationsList) { if ( mutation.type === "childList" && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) ) { // Filter for actual tab elements const hasTabNodes = Array.from(mutation.addedNodes).some( (node) => node.nodeType === 1 && node.hasAttribute("data-monster-button-label"), ) || Array.from(mutation.removedNodes).some( (node) => node.nodeType === 1 && node.hasAttribute("data-monster-button-label"), ); if (hasTabNodes) { tabsChanged = true; break; } } } if (tabsChanged) { const currentActiveTabId = tabsEl.getActiveTab(); const currentTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id")); // Only update hash if the list of tabs has actually changed if ( currentTabs.length !== lastKnownAllTabIds.length || !currentTabs.every((id) => lastKnownAllTabIds.includes(id)) ) { writeHash(currentActiveTabId, currentTabs); } } }); // Observe the tabsEl for direct child additions/removals observer.observe(tabsEl, { childList: true }); // Initial write of all existing tabs to the hash const initialActiveTab = tabsEl.getActiveTab(); const initialTabs = tabsEl.getTabs().map((tab) => tab.getAttribute("id")); writeHash(initialActiveTab, initialTabs); }