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);
+});