From a05b74453934fb76da08e58f6422854e4bd394a6 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Thu, 23 May 2024 00:05:35 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20NEW:=20sync=20tabs=20by=20URL=20que?= =?UTF-8?q?ry=20parameters=20(#196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synchronised tabs can now be selected by adding a query parameter to the URL, for that sync-group, such as `?code=python` for ```restructuredtext .. tab-set-code:: .. literalinclude:: snippet.py :language: python .. literalinclude:: snippet.js :language: javascript ``` The last selected tab key, per group, is also persisted to `SessionStroage` --- .pre-commit-config.yaml | 10 ++ docs/conf.py | 8 +- docs/get_started.md | 3 + docs/snippets/myst/tab-sync.txt | 2 + docs/snippets/rst/tab-sync.txt | 2 + docs/tabs.md | 47 +++++++- sphinx_design/compiled/sd_tabs.js | 102 +++++++++++++++--- sphinx_design/tabs.py | 13 ++- .../snippet_post_tab-code-set.xml | 4 +- tests/test_snippets/snippet_post_tab-sync.xml | 8 +- .../snippet_pre_tab-code-set.xml | 4 +- tests/test_snippets/snippet_pre_tab-sync.xml | 8 +- 12 files changed, 179 insertions(+), 32 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9374882..b812969 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,3 +44,13 @@ repos: require_serial: true pass_filenames: false # args: [--style=compressed, --no-source-map, style/index.scss, sphinx_design/compiled/style.min.css] + + - id: tsc + name: tsc (jsdoc) + entry: tsc + language: node + files: \.(js)$ + types_or: [javascript] + args: [--allowJs, --noEmit, --strict] + additional_dependencies: + - typescript diff --git a/docs/conf.py b/docs/conf.py index 438742c..ac2fc1f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -101,7 +101,13 @@ } exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -myst_enable_extensions = ["colon_fence", "deflist", "substitution", "html_image"] +myst_enable_extensions = [ + "attrs_inline", + "colon_fence", + "deflist", + "substitution", + "html_image", +] myst_substitutions = { "loremipsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " diff --git a/docs/get_started.md b/docs/get_started.md index 4394c5b..7189e28 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -45,6 +45,9 @@ sd_hide_title: true ### Creating custom directives +:::{versionadded} 0.6.0 +::: + You can use the `sd_custom_directives` configuration option in your `conf.py` to add custom directives, with default option values: ```python diff --git a/docs/snippets/myst/tab-sync.txt b/docs/snippets/myst/tab-sync.txt index d0a4788..d2fbe12 100644 --- a/docs/snippets/myst/tab-sync.txt +++ b/docs/snippets/myst/tab-sync.txt @@ -1,4 +1,5 @@ ::::{tab-set} +:sync-group: category :::{tab-item} Label1 :sync: key1 @@ -15,6 +16,7 @@ Content 2 :::: ::::{tab-set} +:sync-group: category :::{tab-item} Label1 :sync: key1 diff --git a/docs/snippets/rst/tab-sync.txt b/docs/snippets/rst/tab-sync.txt index 531bad1..cc4b114 100644 --- a/docs/snippets/rst/tab-sync.txt +++ b/docs/snippets/rst/tab-sync.txt @@ -1,4 +1,5 @@ .. tab-set:: + :sync-group: category .. tab-item:: Label1 :sync: key1 @@ -11,6 +12,7 @@ Content 2 .. tab-set:: + :sync-group: category .. tab-item:: Label1 :sync: key1 diff --git a/docs/tabs.md b/docs/tabs.md index 45aa2d7..25114d1 100644 --- a/docs/tabs.md +++ b/docs/tabs.md @@ -33,10 +33,26 @@ See the [Material Design](https://material.io/components/tabs) description for f ## Synchronised Tabs -Use the `sync` option to synchronise the selected tab items across multiple tab-sets. -Note, synchronisation requires that JavaScript is enabled. +The Selection of tab items can be synchronised across multiple tab-sets. +For a `tab-item` to be synchronisable, add the `sync` option to the `tab-item` directive with a key unique to that set. +Now when you select a tab in one set, tabs in other sets with the same key will be selected. + +:::{note} +Synchronisation requires that JavaScript is enabled. +::: + +:::{versionadded} 0.6.0 +To synchronise tabs only across certain tab-sets, add the `:sync-group:` option to each `tab-set` directive with the same group name, such as `:sync-group: category`. + +You can also add an [HTML query string](https://en.wikipedia.org/wiki/Query_string) to the end of the page's URL, +to automatically select a tab with a specific key across all tab-sets of the group, for example: + +- [`?category=key1#synchronised-tabs`](?category=key1#synchronised-tabs){.external} +- [`?category=key2#synchronised-tabs`](?category=key2#synchronised-tabs){.external} +::: ::::{tab-set} +:sync-group: category :::{tab-item} Label1 :sync: key1 @@ -53,6 +69,7 @@ Content 2 :::: ::::{tab-set} +:sync-group: category :::{tab-item} Label1 :sync: key1 @@ -86,7 +103,16 @@ The `tab-set-code` directive provides a shorthand for synced code examples. You can place any directives in a `tab-set-code` that produce a `literal_block` node with a `language` attribute, for example `code`, `code-block` and `literalinclude`. Tabs will be labelled and synchronised by the `language` attribute (in upper-case). +:::{versionadded} 0.6.0 +You can also add an [HTML query string](https://en.wikipedia.org/wiki/Query_string) to the end of the page's URL, +to automatically select a tab with a specific code across all tab-sets of the group, for example: + +- [`?code=markdown#tabbed-code-examples`](?code=markdown#tabbed-code-examples){.external} +- [`?code=rst#tabbed-code-examples`](?code=rst#tabbed-code-examples){.external} +::: + ```````{tab-set} +:sync-group: code ``````{tab-item} Markdown :sync: markdown @@ -202,9 +228,26 @@ Content 2 ## `tab-set` options +sync-group +: A group name for synchronised tab sets (default `tab`). + class : Additional CSS classes for the container element. +## `tab-set-code` options + +no-sync +: Disable synchronisation of tabs. + +sync-group +: A group name for synchronised tab sets (default `code`). + +class-set +: Additional CSS classes for the set container element. + +class-item +: Additional CSS classes for the item container element. + ## `tab-item` options selected diff --git a/sphinx_design/compiled/sd_tabs.js b/sphinx_design/compiled/sd_tabs.js index 97bf67c..b25bd6a 100644 --- a/sphinx_design/compiled/sd_tabs.js +++ b/sphinx_design/compiled/sd_tabs.js @@ -1,27 +1,101 @@ -var sd_labels_by_text = {}; +// @ts-check +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ function ready() { - const li = document.getElementsByClassName("sd-tab-label"); - for (const label of li) { - syncId = label.getAttribute("data-sync-id"); - if (syncId) { - label.onclick = onSDLabelClick; - if (!sd_labels_by_text[syncId]) { - sd_labels_by_text[syncId] = []; + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } } - sd_labels_by_text[syncId].push(label); } - } + }); } +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ function onSDLabelClick() { - // Activate other inputs with the same sync id. - syncId = this.getAttribute("data-sync-id"); - for (label of sd_labels_by_text[syncId]) { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { if (label === this) continue; + // @ts-ignore label.previousElementSibling.checked = true; } - window.localStorage.setItem("sphinx-design-last-tab", syncId); + window.sessionStorage.setItem(storageKeyPrefix + group, id); } document.addEventListener("DOMContentLoaded", ready, false); diff --git a/sphinx_design/tabs.py b/sphinx_design/tabs.py index 130241b..5a04f7e 100644 --- a/sphinx_design/tabs.py +++ b/sphinx_design/tabs.py @@ -24,6 +24,7 @@ class TabSetDirective(SdDirective): has_content = True option_spec = { + "sync-group": directives.unchanged_required, "class": directives.class_option, } @@ -44,6 +45,8 @@ def run_with_defaults(self) -> list[nodes.Node]: subtype="tab", ) break + if "sync_id" in item.children[0]: + item.children[0]["sync_group"] = self.options.get("sync-group", "tab") return [tab_set] @@ -122,6 +125,7 @@ class TabSetCodeDirective(SdDirective): has_content = True option_spec = { "no-sync": directives.flag, + "sync-group": directives.unchanged_required, "class-set": directives.class_option, "class-item": directives.class_option, } @@ -151,7 +155,8 @@ def run_with_defaults(self) -> list[nodes.Node]: classes=["sd-tab-label", *self.options.get("class-label", [])], ) if "no-sync" not in self.options: - tab_label["sync_id"] = f"tabcode-{language}" + tab_label["sync_group"] = self.options.get("sync-group", "code") + tab_label["sync_id"] = language tab_content = create_component( "tab-content", children=[item], @@ -190,8 +195,9 @@ def depart_tab_input(self, node): def visit_tab_label(self, node): attributes = {"for": node["input_id"]} - if "sync_id" in node: + if "sync_id" in node and "sync_group" in node: attributes["data-sync-id"] = node["sync_id"] + attributes["data-sync-group"] = node["sync_group"] self.body.append(self.starttag(node, "label", **attributes)) @@ -262,7 +268,8 @@ def run(self) -> None: ) if tab_label.get("ids"): label_node["ids"] += tab_label["ids"] - if "sync_id" in tab_label: + if "sync_group" in tab_label and "sync_id" in tab_label: + label_node["sync_group"] = tab_label["sync_group"] label_node["sync_id"] = tab_label["sync_id"] label_node.source, label_node.line = tab_item.source, tab_item.line children.append(label_node) diff --git a/tests/test_snippets/snippet_post_tab-code-set.xml b/tests/test_snippets/snippet_post_tab-code-set.xml index c5beb19..0ca7564 100644 --- a/tests/test_snippets/snippet_post_tab-code-set.xml +++ b/tests/test_snippets/snippet_post_tab-code-set.xml @@ -4,13 +4,13 @@ Heading - + PYTHON a = 1 - + JAVASCRIPT diff --git a/tests/test_snippets/snippet_post_tab-sync.xml b/tests/test_snippets/snippet_post_tab-sync.xml index f4f40cd..71d2ada 100644 --- a/tests/test_snippets/snippet_post_tab-sync.xml +++ b/tests/test_snippets/snippet_post_tab-sync.xml @@ -4,26 +4,26 @@ Heading - + Label1 Content 1 - + Label2 Content 2 - + Label1 Content 1 - + Label2 diff --git a/tests/test_snippets/snippet_pre_tab-code-set.xml b/tests/test_snippets/snippet_pre_tab-code-set.xml index aa362ed..c3e7192 100644 --- a/tests/test_snippets/snippet_pre_tab-code-set.xml +++ b/tests/test_snippets/snippet_pre_tab-code-set.xml @@ -4,13 +4,13 @@ Heading - + PYTHON a = 1 - + JAVASCRIPT diff --git a/tests/test_snippets/snippet_pre_tab-sync.xml b/tests/test_snippets/snippet_pre_tab-sync.xml index ae55670..bed0566 100644 --- a/tests/test_snippets/snippet_pre_tab-sync.xml +++ b/tests/test_snippets/snippet_pre_tab-sync.xml @@ -4,26 +4,26 @@ Heading - + Label1 Content 1 - + Label2 Content 2 - + Label1 Content 1 - + Label2