Skip to content
Snippets Groups Projects
Verified Commit 5985e645 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: Add dynamic tab management and URL hash synchronization

Summary of changes
- Added buttons for adding and removing tabs in "268.html" and "268.mjs" to enhance user interaction with the tabs functionality.
- Updated "271.html" and "271.mjs" files to assign unique IDs to multiple tab elements for easier management.
- Introduced a new utility function "attachTabsHashSync" in "attach-tabs-hash-sync.mjs" to allow tab states to synchronize with the URL hash.
- Enhanced the existing "Tabs" class in "tabs.mjs" by implementing methods for dynamic tab addition and removal.
parent a001d0d7
No related branches found
No related tags found
No related merge requests found
......@@ -14,6 +14,9 @@
<li><a href="/">Back to overview</a></li>
</ul>
<main>
<button id="addTab">Add Tab</button>
<button id="removeLastTab">Remove last Tab</button>
<monster-tabs data-monster-option-features-removeBehavior="auto" id="mainTabs">
<monster-tabs data-monster-option-features-removeBehavior="auto">
<div data-monster-button-label="A1" data-monster-removable>test1</div>
......
......@@ -13,3 +13,28 @@ import "../../../source/components/style/normalize.pcss";
import "../../../source/components/style/typography.pcss";
import "../../../source/components/layout/tabs.mjs";
import { attachTabsHashSync } from "../../../source/components/layout/utils/attach-tabs-hash-sync.mjs";
document.addEventListener("DOMContentLoaded", () => {
const mainTabs = document.querySelector("#mainTabs");
attachTabsHashSync(mainTabs, "tabs", "active");
console.log("Main Tabs:", mainTabs);
// const subTabs = document.querySelector("#subTabs");
// attachTabsHashSync(subTabs, "tabs2", "active");
});
const button = document.querySelector("#addTab");
button.addEventListener("click", () => {
mainTabs.addTab("HALLO")
});
const removeLastTab = document.querySelector("#removeLastTab");
removeLastTab.addEventListener("click", () => {
const tabs = mainTabs.getTabs();
console.log(tabs);
if (tabs.length > 0) {
mainTabs.removeTab(tabs[tabs.length - 1].getAttribute("id"));
}
});
......@@ -15,7 +15,7 @@
</ul>
<main style="width: 990px; border:1px solid red">
<monster-tabs>
<monster-tabs id="mainTabs">
<div>tab 1</div>
<div>tab 2</div>
<div data-monster-button-label="big" style="height: 1000px">
......@@ -23,7 +23,8 @@
</div>
<div data-monster-button-label="22">
<monster-tabs>
<monster-tabs id="subTabs">
<div>tab 11</div>
<div>tab 12</div>
<div>tab 13</div>
......
......@@ -12,4 +12,18 @@ import "../../../source/components/style/theme.pcss";
import "../../../source/components/style/normalize.pcss";
import "../../../source/components/style/typography.pcss";
import "../../../source/components/layout/tabs.mjs";
import { attachTabsHashSync } from "../../../source/components/layout/utils/attach-tabs-hash-sync.mjs";
document.addEventListener("DOMContentLoaded", () => {
const mainTabs = document.querySelector("#mainTabs");
attachTabsHashSync(mainTabs, "tabs", "active");
const subTabs = document.querySelector("#subTabs");
attachTabsHashSync(subTabs, "tabs2", "active");
});
//const tabs = document.querySelector("#subTabs");
//attachTabsHashSync(tabs, "tabs", "active");
//const tabs2 = document.querySelector("#mainTabs");
//attachTabsHashSync(tabs2, "tabs2", "active");
......@@ -164,11 +164,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1751211869,
"narHash": "sha256-1Cu92i1KSPbhPCKxoiVG5qnoRiKTgR5CcGSRyLpOd7Y=",
"lastModified": 1752308619,
"narHash": "sha256-pzrVLKRQNPrii06Rm09Q0i0dq3wt2t2pciT/GNq5EZQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b43c397f6c213918d6cfe6e3550abfe79b5d1c51",
"rev": "650e572363c091045cdbc5b36b0f4c1f614d3058",
"type": "github"
},
"original": {
......
{
"name": "@schukai/monster",
"version": "4.32.1",
"version": "4.33.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@schukai/monster",
"version": "4.32.1",
"version": "4.33.1",
"license": "AGPL 3.0",
"dependencies": {
"@floating-ui/dom": "^1.7.2",
......
......@@ -283,6 +283,99 @@ class Tabs extends CustomElement {
return "monster-tabs";
}
/**
* This method is called internal and should not be called directly.
* @param tabId
* @returns {Tabs}
*/
removeTab(tabId) {
const tabs = this.getTabs();
for (const tab of tabs) {
if ((tab.getAttribute("id") === tabId || tab.getAttribute("data-monster-name") === tabId) && tab.hasAttribute("data-monster-removable")) {
tab.remove();
initTabButtons.call(this);
return this;
}
}
return this;
}
/**
* This method is called internal and should not be called directly.
* @returns {[]}
*/
getTabs() {
const nodes = getSlottedElements.call(this);
const tabs = [];
for (const node of nodes) {
if (node instanceof HTMLElement) {
tabs.push(node);
}
}
return tabs;
}
/**
* This method is called internal and should not be called directly.
* @param content
* @param active
* @param label
* @param tabId
* @param removable
* @returns {Tabs}
*/
addTab(content, {
active = false,
label = null,
tabId = null,
removable = true
} = {} ) {
const tab = document.createElement("div");
if (!isString(label) || label.trim() === "") {
label = this.getOption("labels.new-tab-label");
}
tab.setAttribute("data-monster-button-label" ,label);
if (!isString(tabId) || tabId.trim() === "") {
let thisID = this.getAttribute("id");
if (!thisID) {
thisID = new ID("tab").toString();
}
tabId = new ID(thisID).toString();
}
// check if id is already used
const existingTabs = this.getTabs();
for (const existingTab of existingTabs) {
if (existingTab.getAttribute("id") === tabId || existingTab.getAttribute("data-monster-name") === tabId) {
throw new Error(`Tab with id "${tabId}" already exists.`);
}
}
tab.setAttribute("id", tabId);
if (active === true) {
tab.classList.add("active");
}
tab.setAttribute(ATTRIBUTE_ROLE, "tab");
if(removable === true) {
tab.setAttribute("data-monster-removable", "true");
}
if(content instanceof HTMLElement) {
tab.appendChild(content);
} else if (isString(content)) {
tab.innerHTML = content;
}
this.appendChild(tab);
return this
}
/**
* A function that activates a tab based on the provided name.
*
......
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) {
history.replaceState(null, "", newHash);
}
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);
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment