@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
176 lines (156 loc) • 5.69 kB
JavaScript
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);
}