Skip to content

Commit

Permalink
✨ NEW: sync tabs by URL query parameters (#196)
Browse files Browse the repository at this point in the history
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 <mmckiernan@nvidia.com>
  • Loading branch information
chrisjsewell and mikemckiernan committed May 22, 2024
1 parent a6f97b8 commit f1427ce
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 32 deletions.
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 7 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. "
Expand Down
3 changes: 3 additions & 0 deletions docs/get_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/snippets/myst/tab-sync.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
::::{tab-set}
:sync-group: category

:::{tab-item} Label1
:sync: key1
Expand All @@ -15,6 +16,7 @@ Content 2
::::

::::{tab-set}
:sync-group: category

:::{tab-item} Label1
:sync: key1
Expand Down
2 changes: 2 additions & 0 deletions docs/snippets/rst/tab-sync.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.. tab-set::
:sync-group: category

.. tab-item:: Label1
:sync: key1
Expand All @@ -11,6 +12,7 @@
Content 2

.. tab-set::
:sync-group: category

.. tab-item:: Label1
:sync: key1
Expand Down
47 changes: 45 additions & 2 deletions docs/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,6 +69,7 @@ Content 2
::::

::::{tab-set}
:sync-group: category

:::{tab-item} Label1
:sync: key1
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
102 changes: 88 additions & 14 deletions sphinx_design/compiled/sd_tabs.js
Original file line number Diff line number Diff line change
@@ -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<string, HTMLElement[]>}
*/
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);
13 changes: 10 additions & 3 deletions sphinx_design/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class TabSetDirective(SdDirective):

has_content = True
option_spec = {
"sync-group": directives.unchanged_required,
"class": directives.class_option,
}

Expand All @@ -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]


Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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))


Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_snippets/snippet_post_tab-code-set.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
Heading
<container classes="sd-tab-set" design_component="tab-set" is_div="True">
<sd_tab_input checked="True" id="sd-tab-item-0" set_id="sd-tab-set-0" type="radio">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-0" sync_id="tabcode-python">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-0" sync_group="code" sync_id="python">
PYTHON
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<literal_block force="False" highlight_args="{'linenostart': 1}" language="python" linenos="False" source="snippet.py" xml:space="preserve">
a = 1
<sd_tab_input checked="False" id="sd-tab-item-1" set_id="sd-tab-set-0" type="radio">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-1" sync_id="tabcode-javascript">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-1" sync_group="code" sync_id="javascript">
JAVASCRIPT
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<literal_block force="False" highlight_args="{}" language="javascript" linenos="False" xml:space="preserve">
Expand Down
8 changes: 4 additions & 4 deletions tests/test_snippets/snippet_post_tab-sync.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@
Heading
<container classes="sd-tab-set" design_component="tab-set" is_div="True">
<sd_tab_input checked="True" id="sd-tab-item-0" set_id="sd-tab-set-0" type="radio">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-0" sync_id="key1">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-0" sync_group="category" sync_id="key1">
Label1
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<paragraph>
Content 1
<sd_tab_input checked="False" id="sd-tab-item-1" set_id="sd-tab-set-0" type="radio">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-1" sync_id="key2">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-1" sync_group="category" sync_id="key2">
Label2
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<paragraph>
Content 2
<container classes="sd-tab-set" design_component="tab-set" is_div="True">
<sd_tab_input checked="True" id="sd-tab-item-2" set_id="sd-tab-set-1" type="radio">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-2" sync_id="key1">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-2" sync_group="category" sync_id="key1">
Label1
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<paragraph>
Content 1
<sd_tab_input checked="False" id="sd-tab-item-3" set_id="sd-tab-set-1" type="radio">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-3" sync_id="key2">
<sd_tab_label classes="sd-tab-label" input_id="sd-tab-item-3" sync_group="category" sync_id="key2">
Label2
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<paragraph>
Expand Down
4 changes: 2 additions & 2 deletions tests/test_snippets/snippet_pre_tab-code-set.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
Heading
<container classes="sd-tab-set" design_component="tab-set" is_div="True">
<container classes="sd-tab-item" design_component="tab-item" is_div="True">
<rubric classes="sd-tab-label" sync_id="tabcode-python">
<rubric classes="sd-tab-label" sync_group="code" sync_id="python">
PYTHON
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<literal_block force="False" highlight_args="{'linenostart': 1}" language="python" source="snippet.py" xml:space="preserve">
a = 1
<container classes="sd-tab-item" design_component="tab-item" is_div="True">
<rubric classes="sd-tab-label" sync_id="tabcode-javascript">
<rubric classes="sd-tab-label" sync_group="code" sync_id="javascript">
JAVASCRIPT
<container classes="sd-tab-content" design_component="tab-content" is_div="True">
<literal_block force="False" highlight_args="{}" language="javascript" xml:space="preserve">
Expand Down
Loading

0 comments on commit f1427ce

Please sign in to comment.