[tabs] Fix conflict with anchor links / hashes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
/* DS2 core (c) 2024 Alexander McIlwraith
|
/* DS2 core (c) 2024–2026 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user