Skip to content

Commit f1427ce

Browse files
✨ NEW: sync tabs by URL query parameters (#196)
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` Co-authored-by: Mike McKiernan <[email protected]>
1 parent a6f97b8 commit f1427ce

12 files changed

+179
-32
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,13 @@ repos:
4444
require_serial: true
4545
pass_filenames: false
4646
# args: [--style=compressed, --no-source-map, style/index.scss, sphinx_design/compiled/style.min.css]
47+
48+
- id: tsc
49+
name: tsc (jsdoc)
50+
entry: tsc
51+
language: node
52+
files: \.(js)$
53+
types_or: [javascript]
54+
args: [--allowJs, --noEmit, --strict]
55+
additional_dependencies:
56+
- typescript

docs/conf.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,13 @@
101101
}
102102

103103
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
104-
myst_enable_extensions = ["colon_fence", "deflist", "substitution", "html_image"]
104+
myst_enable_extensions = [
105+
"attrs_inline",
106+
"colon_fence",
107+
"deflist",
108+
"substitution",
109+
"html_image",
110+
]
105111

106112
myst_substitutions = {
107113
"loremipsum": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "

docs/get_started.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ sd_hide_title: true
4545

4646
### Creating custom directives
4747

48+
:::{versionadded} 0.6.0
49+
:::
50+
4851
You can use the `sd_custom_directives` configuration option in your `conf.py` to add custom directives, with default option values:
4952

5053
```python

docs/snippets/myst/tab-sync.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
::::{tab-set}
2+
:sync-group: category
23

34
:::{tab-item} Label1
45
:sync: key1
@@ -15,6 +16,7 @@ Content 2
1516
::::
1617

1718
::::{tab-set}
19+
:sync-group: category
1820

1921
:::{tab-item} Label1
2022
:sync: key1

docs/snippets/rst/tab-sync.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.. tab-set::
2+
:sync-group: category
23

34
.. tab-item:: Label1
45
:sync: key1
@@ -11,6 +12,7 @@
1112
Content 2
1213

1314
.. tab-set::
15+
:sync-group: category
1416

1517
.. tab-item:: Label1
1618
:sync: key1

docs/tabs.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,26 @@ See the [Material Design](https://material.io/components/tabs) description for f
3333

3434
## Synchronised Tabs
3535

36-
Use the `sync` option to synchronise the selected tab items across multiple tab-sets.
37-
Note, synchronisation requires that JavaScript is enabled.
36+
The Selection of tab items can be synchronised across multiple tab-sets.
37+
For a `tab-item` to be synchronisable, add the `sync` option to the `tab-item` directive with a key unique to that set.
38+
Now when you select a tab in one set, tabs in other sets with the same key will be selected.
39+
40+
:::{note}
41+
Synchronisation requires that JavaScript is enabled.
42+
:::
43+
44+
:::{versionadded} 0.6.0
45+
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`.
46+
47+
You can also add an [HTML query string](https://en.wikipedia.org/wiki/Query_string) to the end of the page's URL,
48+
to automatically select a tab with a specific key across all tab-sets of the group, for example:
49+
50+
- [`?category=key1#synchronised-tabs`](?category=key1#synchronised-tabs){.external}
51+
- [`?category=key2#synchronised-tabs`](?category=key2#synchronised-tabs){.external}
52+
:::
3853

3954
::::{tab-set}
55+
:sync-group: category
4056

4157
:::{tab-item} Label1
4258
:sync: key1
@@ -53,6 +69,7 @@ Content 2
5369
::::
5470

5571
::::{tab-set}
72+
:sync-group: category
5673

5774
:::{tab-item} Label1
5875
:sync: key1
@@ -86,7 +103,16 @@ The `tab-set-code` directive provides a shorthand for synced code examples.
86103
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`.
87104
Tabs will be labelled and synchronised by the `language` attribute (in upper-case).
88105

106+
:::{versionadded} 0.6.0
107+
You can also add an [HTML query string](https://en.wikipedia.org/wiki/Query_string) to the end of the page's URL,
108+
to automatically select a tab with a specific code across all tab-sets of the group, for example:
109+
110+
- [`?code=markdown#tabbed-code-examples`](?code=markdown#tabbed-code-examples){.external}
111+
- [`?code=rst#tabbed-code-examples`](?code=rst#tabbed-code-examples){.external}
112+
:::
113+
89114
```````{tab-set}
115+
:sync-group: code
90116
91117
``````{tab-item} Markdown
92118
:sync: markdown
@@ -202,9 +228,26 @@ Content 2
202228

203229
## `tab-set` options
204230

231+
sync-group
232+
: A group name for synchronised tab sets (default `tab`).
233+
205234
class
206235
: Additional CSS classes for the container element.
207236

237+
## `tab-set-code` options
238+
239+
no-sync
240+
: Disable synchronisation of tabs.
241+
242+
sync-group
243+
: A group name for synchronised tab sets (default `code`).
244+
245+
class-set
246+
: Additional CSS classes for the set container element.
247+
248+
class-item
249+
: Additional CSS classes for the item container element.
250+
208251
## `tab-item` options
209252

210253
selected

sphinx_design/compiled/sd_tabs.js

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,101 @@
1-
var sd_labels_by_text = {};
1+
// @ts-check
22

3+
// Extra JS capability for selected tabs to be synced
4+
// The selection is stored in local storage so that it persists across page loads.
5+
6+
/**
7+
* @type {Record<string, HTMLElement[]>}
8+
*/
9+
let sd_id_to_elements = {};
10+
const storageKeyPrefix = "sphinx-design-tab-id-";
11+
12+
/**
13+
* Create a key for a tab element.
14+
* @param {HTMLElement} el - The tab element.
15+
* @returns {[string, string, string] | null} - The key.
16+
*
17+
*/
18+
function create_key(el) {
19+
let syncId = el.getAttribute("data-sync-id");
20+
let syncGroup = el.getAttribute("data-sync-group");
21+
if (!syncId || !syncGroup) return null;
22+
return [syncGroup, syncId, syncGroup + "--" + syncId];
23+
}
24+
25+
/**
26+
* Initialize the tab selection.
27+
*
28+
*/
329
function ready() {
4-
const li = document.getElementsByClassName("sd-tab-label");
5-
for (const label of li) {
6-
syncId = label.getAttribute("data-sync-id");
7-
if (syncId) {
8-
label.onclick = onSDLabelClick;
9-
if (!sd_labels_by_text[syncId]) {
10-
sd_labels_by_text[syncId] = [];
30+
// Find all tabs with sync data
31+
32+
/** @type {string[]} */
33+
let groups = [];
34+
35+
document.querySelectorAll(".sd-tab-label").forEach((label) => {
36+
if (label instanceof HTMLElement) {
37+
let data = create_key(label);
38+
if (data) {
39+
let [group, id, key] = data;
40+
41+
// add click event listener
42+
// @ts-ignore
43+
label.onclick = onSDLabelClick;
44+
45+
// store map of key to elements
46+
if (!sd_id_to_elements[key]) {
47+
sd_id_to_elements[key] = [];
48+
}
49+
sd_id_to_elements[key].push(label);
50+
51+
if (groups.indexOf(group) === -1) {
52+
groups.push(group);
53+
// Check if a specific tab has been selected via URL parameter
54+
const tabParam = new URLSearchParams(window.location.search).get(
55+
group
56+
);
57+
if (tabParam) {
58+
console.log(
59+
"sphinx-design: Selecting tab id for group '" +
60+
group +
61+
"' from URL parameter: " +
62+
tabParam
63+
);
64+
window.sessionStorage.setItem(storageKeyPrefix + group, tabParam);
65+
}
66+
}
67+
68+
// Check is a specific tab has been selected previously
69+
let previousId = window.sessionStorage.getItem(
70+
storageKeyPrefix + group
71+
);
72+
if (previousId === id) {
73+
// console.log(
74+
// "sphinx-design: Selecting tab from session storage: " + id
75+
// );
76+
// @ts-ignore
77+
label.previousElementSibling.checked = true;
78+
}
1179
}
12-
sd_labels_by_text[syncId].push(label);
1380
}
14-
}
81+
});
1582
}
1683

84+
/**
85+
* Activate other tabs with the same sync id.
86+
*
87+
* @this {HTMLElement} - The element that was clicked.
88+
*/
1789
function onSDLabelClick() {
18-
// Activate other inputs with the same sync id.
19-
syncId = this.getAttribute("data-sync-id");
20-
for (label of sd_labels_by_text[syncId]) {
90+
let data = create_key(this);
91+
if (!data) return;
92+
let [group, id, key] = data;
93+
for (const label of sd_id_to_elements[key]) {
2194
if (label === this) continue;
95+
// @ts-ignore
2296
label.previousElementSibling.checked = true;
2397
}
24-
window.localStorage.setItem("sphinx-design-last-tab", syncId);
98+
window.sessionStorage.setItem(storageKeyPrefix + group, id);
2599
}
26100

27101
document.addEventListener("DOMContentLoaded", ready, false);

sphinx_design/tabs.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class TabSetDirective(SdDirective):
2424

2525
has_content = True
2626
option_spec = {
27+
"sync-group": directives.unchanged_required,
2728
"class": directives.class_option,
2829
}
2930

@@ -44,6 +45,8 @@ def run_with_defaults(self) -> list[nodes.Node]:
4445
subtype="tab",
4546
)
4647
break
48+
if "sync_id" in item.children[0]:
49+
item.children[0]["sync_group"] = self.options.get("sync-group", "tab")
4750
return [tab_set]
4851

4952

@@ -122,6 +125,7 @@ class TabSetCodeDirective(SdDirective):
122125
has_content = True
123126
option_spec = {
124127
"no-sync": directives.flag,
128+
"sync-group": directives.unchanged_required,
125129
"class-set": directives.class_option,
126130
"class-item": directives.class_option,
127131
}
@@ -151,7 +155,8 @@ def run_with_defaults(self) -> list[nodes.Node]:
151155
classes=["sd-tab-label", *self.options.get("class-label", [])],
152156
)
153157
if "no-sync" not in self.options:
154-
tab_label["sync_id"] = f"tabcode-{language}"
158+
tab_label["sync_group"] = self.options.get("sync-group", "code")
159+
tab_label["sync_id"] = language
155160
tab_content = create_component(
156161
"tab-content",
157162
children=[item],
@@ -190,8 +195,9 @@ def depart_tab_input(self, node):
190195

191196
def visit_tab_label(self, node):
192197
attributes = {"for": node["input_id"]}
193-
if "sync_id" in node:
198+
if "sync_id" in node and "sync_group" in node:
194199
attributes["data-sync-id"] = node["sync_id"]
200+
attributes["data-sync-group"] = node["sync_group"]
195201
self.body.append(self.starttag(node, "label", **attributes))
196202

197203

@@ -262,7 +268,8 @@ def run(self) -> None:
262268
)
263269
if tab_label.get("ids"):
264270
label_node["ids"] += tab_label["ids"]
265-
if "sync_id" in tab_label:
271+
if "sync_group" in tab_label and "sync_id" in tab_label:
272+
label_node["sync_group"] = tab_label["sync_group"]
266273
label_node["sync_id"] = tab_label["sync_id"]
267274
label_node.source, label_node.line = tab_item.source, tab_item.line
268275
children.append(label_node)

tests/test_snippets/snippet_post_tab-code-set.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
Heading
55
<container classes="sd-tab-set" design_component="tab-set" is_div="True">
66
<sd_tab_input checked="True" id="sd-tab-item-0" set_id="sd-tab-set-0" type="radio">
7-
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-0" sync_id="tabcode-python">
7+
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-0" sync_group="code" sync_id="python">
88
PYTHON
99
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
1010
<literal_block force="False" highlight_args="{'linenostart': 1}" language="python" linenos="False" source="snippet.py" xml:space="preserve">
1111
a = 1
1212
<sd_tab_input checked="False" id="sd-tab-item-1" set_id="sd-tab-set-0" type="radio">
13-
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-1" sync_id="tabcode-javascript">
13+
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-1" sync_group="code" sync_id="javascript">
1414
JAVASCRIPT
1515
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
1616
<literal_block force="False" highlight_args="{}" language="javascript" linenos="False" xml:space="preserve">

tests/test_snippets/snippet_post_tab-sync.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@
44
Heading
55
<container classes="sd-tab-set" design_component="tab-set" is_div="True">
66
<sd_tab_input checked="True" id="sd-tab-item-0" set_id="sd-tab-set-0" type="radio">
7-
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-0" sync_id="key1">
7+
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-0" sync_group="category" sync_id="key1">
88
Label1
99
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
1010
<paragraph>
1111
Content 1
1212
<sd_tab_input checked="False" id="sd-tab-item-1" set_id="sd-tab-set-0" type="radio">
13-
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-1" sync_id="key2">
13+
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-1" sync_group="category" sync_id="key2">
1414
Label2
1515
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
1616
<paragraph>
1717
Content 2
1818
<container classes="sd-tab-set" design_component="tab-set" is_div="True">
1919
<sd_tab_input checked="True" id="sd-tab-item-2" set_id="sd-tab-set-1" type="radio">
20-
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-2" sync_id="key1">
20+
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-2" sync_group="category" sync_id="key1">
2121
Label1
2222
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
2323
<paragraph>
2424
Content 1
2525
<sd_tab_input checked="False" id="sd-tab-item-3" set_id="sd-tab-set-1" type="radio">
26-
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-3" sync_id="key2">
26+
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-3" sync_group="category" sync_id="key2">
2727
Label2
2828
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
2929
<paragraph>

tests/test_snippets/snippet_pre_tab-code-set.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
Heading
55
<container classes="sd-tab-set" design_component="tab-set" is_div="True">
66
<container classes="sd-tab-item" design_component="tab-item" is_div="True">
7-
<rubric classes="sd-tab-label" sync_id="tabcode-python">
7+
<rubric classes="sd-tab-label" sync_group="code" sync_id="python">
88
PYTHON
99
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
1010
<literal_block force="False" highlight_args="{'linenostart': 1}" language="python" source="snippet.py" xml:space="preserve">
1111
a = 1
1212
<container classes="sd-tab-item" design_component="tab-item" is_div="True">
13-
<rubric classes="sd-tab-label" sync_id="tabcode-javascript">
13+
<rubric classes="sd-tab-label" sync_group="code" sync_id="javascript">
1414
JAVASCRIPT
1515
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
1616
<literal_block force="False" highlight_args="{}" language="javascript" xml:space="preserve">

0 commit comments

Comments
 (0)