[tabs] Fix conflict with anchor links / hashes

This commit is contained in:
2026-03-18 16:32:42 -04:00
parent 97a7cabba3
commit a4fc62ca5c

View File

@@ -1,6 +1,6 @@
/* DS2 core (c) 2024 Alexander McIlwraith /* DS2 core (c) 20242026 Alexander McIlwraith
Released under Creative Commons Attribution-ShareAlike 4.0 International Released under Creative Commons Attribution-ShareAlike 4.0 International
*/ */
// create a pure JS mouse click event // create a pure JS mouse click event
const click = new MouseEvent('click', { const click = new MouseEvent('click', {
@@ -9,48 +9,56 @@ const click = new MouseEvent('click', {
cancelable: true cancelable: true
}); });
// wait for an element to appear in the DOM
const waitForElement = (selector) => { const waitForElement = (selector) => {
return new Promise(resolve => { return new Promise(resolve => {
if (document.querySelector(selector)) { if (document.querySelector(selector)) {
return resolve(document.querySelector(selector)); return resolve(document.querySelector(selector));
} }
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
};
const observer = new MutationObserver(mutations => { // Tab logic
if (document.querySelector(selector)) { const tabNamespace = "#tab:";
observer.disconnect();
resolve(document.querySelector(selector));
}
});
// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
const chooseTab = (tab) => { const chooseTab = (tab) => {
const siblings = Array.from(tab.parentNode.children); const siblings = Array.from(tab.parentNode.children);
siblings.forEach(sibling => sibling.classList.remove("selected")); siblings.forEach(sibling => sibling.classList.remove("selected"));
tab.classList.add("selected"); tab.classList.add("selected");
const tabPanels = Array.from(tab.parentNode.parentNode.children) const tabPanels = Array.from(tab.parentNode.parentNode.children);
tabPanels.forEach(panel => panel.classList.remove("open")); tabPanels.forEach(panel => panel.classList.remove("open"));
const tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel"); const tabPanelId = tab
document.getElementById(tabPanelId).classList.add("open"); .getAttribute("id")
} .replace("tab", "tab-panel");
let pushState = 0; document
.getElementById(tabPanelId)
.classList.add("open");
};
let pushStateCount = 0;
let tabsetCount = 0; let tabsetCount = 0;
export function init(container = document, spacer = true, args = {}) { export function init(container = document, spacer = true) {
container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => { container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => {
if (tabGroup.querySelector("[role=tablist]") === null) {
if (tabGroup.querySelector('[role="tablist"]') === null) {
if (tabGroup.getAttribute("id") == null) { if (tabGroup.getAttribute("id") == null) {
tabGroup.setAttribute("id", "tab-group-" + tabsetCount); tabGroup.setAttribute("id", `tab-group-${tabsetCount++}`);
tabsetCount++;
} }
const tabgroup = tabGroup.getAttribute("id"); const tabgroup = tabGroup.getAttribute("id");
@@ -58,24 +66,22 @@ export function init(container = document, spacer = true, args = {}) {
Array.from(tabGroup.children).forEach(child => { Array.from(tabGroup.children).forEach(child => {
// is details? const isDetails = child.nodeName === "DETAILS";
let dtls = child.nodeName == "DETAILS" ? true : false; const tabLabel =
isDetails
? child.querySelector("summary")?.innerHTML
: child.getAttribute("tab") || child.getAttribute("data-tab");
// get the tab text if (tabLabel !== null) {
let tab = dtls ? child.querySelector("summary").innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");
// if the tab text is not blank const tabID = tabLabel.replace(/\W+/g, "-").toLowerCase();
if (tab !== null) { let tabPanel;
const tabID = tab.replace(/\W+/g, "-").toLowerCase();
// define the tab panel content if (isDetails) {
let tabPanel = null;
if (dtls) {
tabPanel = child; tabPanel = child;
tabPanel.setAttribute("open", ""); tabPanel.setAttribute("open", "");
} else { } else {
tabPanel = document.createElement('div'); tabPanel = document.createElement("div");
tabPanel.appendChild(child.cloneNode(true)); tabPanel.appendChild(child.cloneNode(true));
} }
@@ -83,76 +89,104 @@ export function init(container = document, spacer = true, args = {}) {
tabPanel.className = tablist === "" ? "open" : ""; tabPanel.className = tablist === "" ? "open" : "";
tabPanel.setAttribute("role", "tabpanel"); tabPanel.setAttribute("role", "tabpanel");
tabPanel.setAttribute("tabindex", "0"); tabPanel.setAttribute("tabindex", "0");
tabPanel.setAttribute("aria-labelledby", `tab-${tabgroup}-${tabID}`); tabPanel.setAttribute(
"aria-labelledby",
`tab-${tabgroup}-${tabID}`
);
child.parentNode.replaceChild(tabPanel, child); child.parentNode.replaceChild(tabPanel, child);
tablist += `<li tabindex="0" role="tab" id="tab-${tabgroup}-${tabID}"><span>${tab}</span></li>`;
tablist += `
<li
tabindex="0"
role="tab"
id="tab-${tabgroup}-${tabID}"
>
<span>${tabLabel}</span>
</li>
`;
} else { } else {
child.classList.add("tab-hidden"); child.classList.add("tab-hidden");
} }
}); });
const ul = document.createElement('ul'); const ul = document.createElement("ul");
ul.setAttribute("role", "tablist"); ul.setAttribute("role", "tablist");
tabGroup.insertBefore(ul, tabGroup.firstChild); tabGroup.insertBefore(ul, tabGroup.firstChild);
ul.innerHTML = spacer != true ? `${tablist}` : `${tablist}<li role="separator" class="separator"></li>`;
if ( tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order") ) { ul.innerHTML = spacer !== true
let order = (tabGroup.getAttribute("order") || tabGroup.getAttribute("data-order")).split(","); ? tablist
: `${tablist}<li role="separator" class="separator"></li>`;
// Tab ordering
if (tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order")) {
const order = (
tabGroup.getAttribute("order") ||
tabGroup.getAttribute("data-order")
).split(",");
const items = Array.from(ul.getElementsByTagName("li")); const items = Array.from(ul.getElementsByTagName("li"));
items.sort((a, b) => { items.sort((a, b) => {
const aa = order.indexOf(a.textContent.trim()); const aa = order.indexOf(a.textContent.trim());
const bb = order.indexOf(b.textContent.trim()); const bb = order.indexOf(b.textContent.trim());
if (aa === -1) return 1;
// Check if both items exist in orderArray if (bb === -1) return -1;
if (aa === -1) return 1; // Move to the end if not found return aa - bb;
if (bb === -1) return -1; // Move to the end if not found
return aa - bb; // Sort based on the defined order
}); });
ul.innerHTML = ''; ul.innerHTML = "";
items.forEach(item => ul.appendChild(item)); items.forEach(item => ul.appendChild(item));
chooseTab(items[0]); chooseTab(items[0]);
} }
// Tab event handlers
tabGroup.querySelectorAll('[role="tab"]').forEach(tab => { tabGroup.querySelectorAll('[role="tab"]').forEach(tab => {
tab.addEventListener("click", (evt) => { tab.addEventListener("click", (evt) => {
if (pushState == 0) {
window.history.pushState({rand: Math.random(), pushState: pushState, tab: tab.parentNode.firstChild.getAttribute("id")}, "", `#${tab.parentNode.firstChild.getAttribute("id")}`); const tabId = tab.getAttribute("id");
pushState++; const hash = `${tabNamespace}${tabId}`;
}
chooseTab(evt.currentTarget); chooseTab(evt.currentTarget);
window.history.pushState({rand: Math.random(), pushState: pushState, tab: tab.getAttribute("id")}, "", `#${tab.getAttribute("id")}`);
pushState++; window.history.pushState(
{ tab: tabId },
"",
hash
);
pushStateCount++;
}); });
tab.addEventListener("keypress", (e) => { tab.addEventListener("keypress", (e) => {
e.preventDefault(); if (e.which === 32 || e.which === 13) {
if( e.which == 32 || e.which == 13 ) { e.preventDefault();
e.currentTarget.dispatchEvent(click); e.currentTarget.dispatchEvent(click);
} }
}) });
}); });
ul.querySelector("li").classList.add("selected");
}
if (document.location.hash != "" && document.location.hash.substring(0,5) == "#tab-") { ul.querySelector("li")?.classList.add("selected");
waitForElement(document.location.hash).then((el) => {
//el.scrollIntoView();
el.focus();
el.dispatchEvent(click);
});
} }
}); });
window.addEventListener("popstate", function (e) { // Initial hash handling (tabs only)
e.preventDefault(); if (location.hash.startsWith(tabNamespace)) {
if (e.state != null) { const tabId = location.hash.replace(tabNamespace, "");
chooseTab(document.querySelector(`#${e.state.tab}`)); waitForElement(`#${tabId}`).then(el => {
} else { el.focus();
history.go(-1); el.dispatchEvent(click);
});
}
// History navigation (tabs only)
window.addEventListener("popstate", (e) => {
if (!location.hash.startsWith(tabNamespace)) return;
if (e.state?.tab) {
const tab = document.querySelector(`#${e.state.tab}`);
if (tab) chooseTab(tab);
} }
}); });
} }