diff --git a/layouts/shortcodes/tabpane.html b/layouts/shortcodes/tabpane.html index d67567b08a..ad58f413c7 100644 --- a/layouts/shortcodes/tabpane.html +++ b/layouts/shortcodes/tabpane.html @@ -25,9 +25,9 @@ {{ $_persist := .Get "persist" -}} {{ with $_persist -}} - {{ $matched := findRE "^(header|lang|none)$" . -}} + {{ $matched := findRE "^(header|lang|disabled)$" . -}} {{ if not $matched -}} - {{ errorf "Shortcode %q: parameter %q should be one of 'header', 'lang', or 'none'; but got %s. Error position: %s" $.Name "persist" $_persist $.Position -}} + {{ errorf "Shortcode %q: parameter %q should be one of 'header', 'lang', or 'disabled'; but got %s. Error position: %s" $.Name "persist" $_persist $.Position -}} {{ end -}} {{ end -}} @@ -44,7 +44,7 @@ {{ $langEqualsHeader := default false ($.Get "langEqualsHeader") -}} {{ $deprecatedPersistLang := $_persistLang | default true -}} {{ $persistKeyKind := $_persist | default (cond (eq $langPane "") "lang" "header") -}} -{{ $persistTab := and $deprecatedPersistLang (ne $persistKeyKind "none") -}} +{{ $persistTab := and $deprecatedPersistLang (ne $persistKeyKind "disabled") -}} {{ $rightPane := default false ($.Get "right") -}} {{ $activeSet := false -}} {{/* Scratchpad gets populated through call to .Inner */ -}} @@ -106,7 +106,6 @@ {{ if $disabled }} disabled{{ end -}}" id="{{ $tabid }}" data-bs-toggle="tab" data-bs-target="#{{ $entryid }}" role="tab" {{ if and $persistTab $persistKey -}} - onclick="tdPersistActiveTab({{ $persistKey }});" {{/* */ -}} {{ printf "%s=%q " $tpPersistAttrName $persistKey | safeHTMLAttr -}} {{ end -}} aria-controls="{{- $entryid -}}" aria-selected="{{- cond ( and ( not $activeSet ) ( not $disabled ) ) "true" "false" -}}"> diff --git a/static/js/tabpane-persist.js b/static/js/tabpane-persist.js index cdddfc5aee..2362130754 100644 --- a/static/js/tabpane-persist.js +++ b/static/js/tabpane-persist.js @@ -1,30 +1,116 @@ -// Storage key name also used as a data-* attribute suffix: -const storageKeyName = 'td-tp-persist'; +// Storage key names and data attribute name: +const td_persistStorageKeyNameBase = 'td-tp-persist'; +const td_persistCounterStorageKeyName = `${td_persistStorageKeyNameBase}-count`; +const td_persistDataAttrName = `data-${td_persistStorageKeyNameBase}`; -function tdActivateTabsWithKey(key) { - if (!key) return; - document - .querySelectorAll(`[data-${storageKeyName}="${key}"]`) - .forEach((element) => { - new bootstrap.Tab(element).show(); - }); -} +// Utilities -function tdPersistActiveTab(activeTabKey) { - if (!tdSupportsLocalStorage()) return; +const _tdPersistCssSelector = (attrValue) => + attrValue + ? `[${td_persistDataAttrName}="${attrValue}"]` + : `[${td_persistDataAttrName}]`; + +const _tdStoragePersistKey = (tabKey) => + td_persistStorageKeyNameBase + ':' + (tabKey || ''); + +const _tdSupportsLocalStorage = () => typeof Storage !== 'undefined'; + +// Helpers + +function tdPersistKey(key, value) { + // @requires: tdSupportsLocalStorage(); try { - localStorage.setItem(storageKeyName, activeTabKey); - tdActivateTabsWithKey(activeTabKey); + if (value) { + localStorage.setItem(key, value); + } else { + localStorage.removeItem(key); + } } catch (error) { - console.error(`Unable to save active tab '${activeTabKey}' to localStorage:`, error); + const action = value ? 'add' : 'remove'; + console.error( + `Docsy tabpane: unable to ${action} localStorage key '${key}': `, + error + ); } } -const tdSupportsLocalStorage = () => typeof Storage !== 'undefined'; +// Retrieve, increment, and store tab-select event count, then returns it. +function tdGetTabSelectEventCountAndInc() { + // @requires: tdSupportsLocalStorage(); + + const storedCount = localStorage.getItem(td_persistCounterStorageKeyName); + let numTabSelectEvents = parseInt(storedCount) || 0; + numTabSelectEvents++; + tdPersistKey(td_persistCounterStorageKeyName, numTabSelectEvents.toString()); + return numTabSelectEvents; +} + +// Main functions -// On page load, activate tabs -if (tdSupportsLocalStorage()) { - const activeTabKey = localStorage.getItem(storageKeyName); +function tdActivateTabsWithKey(key) { + if (!key) return; + + document.querySelectorAll(_tdPersistCssSelector(key)).forEach((element) => { + new bootstrap.Tab(element).show(); + }); +} + +function tdPersistActiveTab(activeTabKey) { + if (!_tdSupportsLocalStorage()) return; + + tdPersistKey( + _tdStoragePersistKey(activeTabKey), + tdGetTabSelectEventCountAndInc() + ); tdActivateTabsWithKey(activeTabKey); } + +// Handlers + +function tdGetAndActivatePersistedTabs(tabs) { + // Get unique persistence keys of tabs in this page + var keyOfTabsInThisPage = [ + ...new Set( + Array.from(tabs).map((el) => el.getAttribute(td_persistDataAttrName)) + ), + ]; + + // Create a list of active tabs with their age: + let key_ageList = keyOfTabsInThisPage + // Map to [tab-key, last-activated-age] + .map((k) => [ + k, + parseInt(localStorage.getItem(_tdStoragePersistKey(k))) || 0, + ]) + // Exclude tabs that have never been activated + .filter(([k, v]) => v) + // Sort from oldest selected to most recently selected + .sort((a, b) => a[1] - b[1]); + + // Activate tabs from the oldest to the newest + key_ageList.forEach(([key]) => { + tdActivateTabsWithKey(key); + }); + + return key_ageList; +} + +function tdRegisterTabClickHandler(tabs) { + tabs.forEach((tab) => { + tab.addEventListener('click', () => { + const activeTabKey = tab.getAttribute(td_persistDataAttrName); + tdPersistActiveTab(activeTabKey); + }); + }); +} + +// Register listeners and activate tabs + +window.addEventListener('DOMContentLoaded', () => { + if (!_tdSupportsLocalStorage()) return; + + var allTabsInThisPage = document.querySelectorAll(_tdPersistCssSelector()); + tdRegisterTabClickHandler(allTabsInThisPage); + tdGetAndActivatePersistedTabs(allTabsInThisPage); +});