|
| 1 | +/** |
| 2 | + * MIT License |
| 3 | + * |
| 4 | + * Copyright (c) 2025 Mickaël Canouil |
| 5 | + * |
| 6 | + * Permission is hereby granted, free of charge, to any person obtaining a copy |
| 7 | + * of this software and associated documentation files (the "Software"), to deal |
| 8 | + * in the Software without restriction, including without limitation the rights |
| 9 | + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 10 | + * copies of the Software, and to permit persons to whom the Software is |
| 11 | + * furnished to do so, subject to the following conditions: |
| 12 | + * |
| 13 | + * The above copyright notice and this permission notice shall be included in all |
| 14 | + * copies or substantial portions of the Software. |
| 15 | + * |
| 16 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 17 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 18 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 19 | + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 20 | + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 | + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 22 | + * SOFTWARE. |
| 23 | + */ |
| 24 | + |
| 25 | +window.RevealJsTabset = function () { |
| 26 | + return { |
| 27 | + id: "RevealJsTabset", |
| 28 | + init: function (deck) { |
| 29 | + const TAB_SELECTOR = "ul.panel-tabset-tabby > li"; |
| 30 | + const TAB_LINK_SELECTOR = "ul.panel-tabset-tabby > li a"; |
| 31 | + |
| 32 | + /** |
| 33 | + * Get all tab panes for a given tabset element. |
| 34 | + * @param {Element} tabset - The tabset container element |
| 35 | + * @returns {HTMLCollection} Collection of tab pane elements |
| 36 | + */ |
| 37 | + function getTabPanes(tabset) { |
| 38 | + const tabContent = tabset.querySelector(".tab-content"); |
| 39 | + return tabContent ? tabContent.children : []; |
| 40 | + } |
| 41 | + |
| 42 | + /** |
| 43 | + * Initialise tabset fragments on ready. |
| 44 | + * This sets up fragment indices for tab content and creates invisible |
| 45 | + * fragment triggers for tab navigation. |
| 46 | + */ |
| 47 | + deck.on("ready", function () { |
| 48 | + const tabsetSlides = document.querySelectorAll(".reveal .slides section .panel-tabset"); |
| 49 | + |
| 50 | + tabsetSlides.forEach(function (tabset) { |
| 51 | + const tabs = tabset.querySelectorAll(TAB_SELECTOR); |
| 52 | + const tabCount = tabs.length; |
| 53 | + if (tabCount <= 1) return; |
| 54 | + |
| 55 | + const tabPanes = getTabPanes(tabset); |
| 56 | + const parentNode = tabset.parentNode; |
| 57 | + let currentIndex = 0; |
| 58 | + |
| 59 | + // Process each tab |
| 60 | + for (let i = 0; i < tabCount; i++) { |
| 61 | + if (tabPanes[i]) { |
| 62 | + // Assign fragment indices to any fragments within the tab pane |
| 63 | + const fragmentsInPane = tabPanes[i].querySelectorAll(".fragment"); |
| 64 | + fragmentsInPane.forEach(function (fragment) { |
| 65 | + fragment.setAttribute("data-fragment-index", currentIndex); |
| 66 | + currentIndex++; |
| 67 | + }); |
| 68 | + } |
| 69 | + |
| 70 | + // Create invisible fragment triggers for tab switching (except after last tab) |
| 71 | + if (i < tabCount - 1) { |
| 72 | + const fragmentDiv = document.createElement("div"); |
| 73 | + fragmentDiv.className = "panel-tabset-fragment fragment"; |
| 74 | + fragmentDiv.dataset.tabIndex = i + 1; |
| 75 | + fragmentDiv.setAttribute("data-fragment-index", currentIndex); |
| 76 | + fragmentDiv.style.display = "none"; |
| 77 | + parentNode.appendChild(fragmentDiv); |
| 78 | + currentIndex++; |
| 79 | + } |
| 80 | + } |
| 81 | + }); |
| 82 | + }); |
| 83 | + |
| 84 | + /** |
| 85 | + * Handle fragment shown events. |
| 86 | + * When a tabset fragment is shown, click the corresponding tab. |
| 87 | + */ |
| 88 | + deck.on("fragmentshown", function (event) { |
| 89 | + if (!event.fragment.classList.contains("panel-tabset-fragment")) return; |
| 90 | + |
| 91 | + const tabIndex = parseInt(event.fragment.dataset.tabIndex, 10); |
| 92 | + const tabset = deck.getCurrentSlide().querySelector(".panel-tabset"); |
| 93 | + if (!tabset) return; |
| 94 | + |
| 95 | + const tabLinks = tabset.querySelectorAll(TAB_LINK_SELECTOR); |
| 96 | + if (tabLinks[tabIndex]) { |
| 97 | + tabLinks[tabIndex].click(); |
| 98 | + } |
| 99 | + }); |
| 100 | + |
| 101 | + /** |
| 102 | + * Handle fragment hidden events. |
| 103 | + * When a tabset fragment is hidden (going backwards), click the previous tab. |
| 104 | + */ |
| 105 | + deck.on("fragmenthidden", function (event) { |
| 106 | + if (!event.fragment.classList.contains("panel-tabset-fragment")) return; |
| 107 | + |
| 108 | + const tabIndex = parseInt(event.fragment.dataset.tabIndex, 10); |
| 109 | + const tabset = deck.getCurrentSlide().querySelector(".panel-tabset"); |
| 110 | + if (!tabset) return; |
| 111 | + |
| 112 | + const tabLinks = tabset.querySelectorAll(TAB_LINK_SELECTOR); |
| 113 | + const targetIndex = tabIndex > 0 ? tabIndex - 1 : 0; |
| 114 | + if (tabLinks[targetIndex]) { |
| 115 | + tabLinks[targetIndex].click(); |
| 116 | + } |
| 117 | + }); |
| 118 | + |
| 119 | + /** |
| 120 | + * Handle PDF export mode. |
| 121 | + * Ensures the correct tab is visible based on fragment state. |
| 122 | + */ |
| 123 | + deck.on("pdf-ready", function () { |
| 124 | + const slides = document.querySelectorAll(".reveal .slides section"); |
| 125 | + |
| 126 | + slides.forEach(function (slide) { |
| 127 | + const tabset = slide.querySelector(".panel-tabset"); |
| 128 | + if (!tabset) return; |
| 129 | + |
| 130 | + const fragments = slide.querySelectorAll(".panel-tabset-fragment"); |
| 131 | + let activeTabIndex = 0; |
| 132 | + |
| 133 | + // Find the highest visible tab index |
| 134 | + fragments.forEach(function (fragment) { |
| 135 | + if (fragment.classList.contains("visible")) { |
| 136 | + const tabIndex = parseInt(fragment.dataset.tabIndex, 10); |
| 137 | + if (tabIndex > activeTabIndex) { |
| 138 | + activeTabIndex = tabIndex; |
| 139 | + } |
| 140 | + } |
| 141 | + }); |
| 142 | + |
| 143 | + // Update tab states |
| 144 | + const tabLinks = tabset.querySelectorAll(TAB_LINK_SELECTOR); |
| 145 | + const tabPanes = getTabPanes(tabset); |
| 146 | + const tabPanesArray = Array.from(tabPanes); |
| 147 | + |
| 148 | + tabLinks.forEach(function (link, index) { |
| 149 | + const li = link.parentElement; |
| 150 | + const isActive = index === activeTabIndex; |
| 151 | + |
| 152 | + li.classList.toggle("active", isActive); |
| 153 | + link.setAttribute("aria-selected", isActive ? "true" : "false"); |
| 154 | + link.setAttribute("tabindex", isActive ? "0" : "-1"); |
| 155 | + }); |
| 156 | + |
| 157 | + // Update pane visibility |
| 158 | + tabPanesArray.forEach(function (panel, index) { |
| 159 | + const isActive = index === activeTabIndex; |
| 160 | + |
| 161 | + panel.classList.toggle("active", isActive); |
| 162 | + panel.style.display = isActive ? "block" : "none"; |
| 163 | + }); |
| 164 | + }); |
| 165 | + }); |
| 166 | + }, |
| 167 | + }; |
| 168 | +}; |
0 commit comments