From f13b1869b6509f5a8b73627a014b8350a69c8bb7 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Thu, 10 Feb 2022 14:36:35 -0800 Subject: [PATCH 1/3] Add button to toggle all elements --- docs/reference/index.md | 12 ++++ docs/use.md | 66 +++++++++++++++++++ sphinx_togglebutton/__init__.py | 39 ++++++----- sphinx_togglebutton/_static/togglebutton.css | 44 ++++++++----- sphinx_togglebutton/_static/togglebutton.js | 14 +++- sphinx_togglebutton/directive.py | 68 ++++++++++++++++++++ 6 files changed, 210 insertions(+), 33 deletions(-) create mode 100644 sphinx_togglebutton/directive.py diff --git a/docs/reference/index.md b/docs/reference/index.md index 87a993f..d9ce2c4 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -3,6 +3,18 @@ This page shows the most common ways that `sphinx-togglebutton` is used as a reference. This is a benchmark for styling, and also helps demonstrate the behavior of this extension. +## Toggle all button + +Here's a button that will toggle all items of a particular type + +```{toggle-all-button} +``` + +With a custom title: + +```{toggle-all-button} A test! +``` + ## Use amongst text Here's a paragraph, it's just here to provide some text context for the togglebuttons in this section. diff --git a/docs/use.md b/docs/use.md index 4612b3d..94b7af6 100644 --- a/docs/use.md +++ b/docs/use.md @@ -171,6 +171,72 @@ button.toggle-button { } ``` +## Toggle all buttons at once + +There are two ways that you can toggle all elements on the screen at once. +Both are covered below. + +For reference, we'll show two kinds of toggle-able content so you can see the behavior. + +```{toggle} +**Here's a toggle-able content block!** +``` + +```{admonition} And this is a toggleable admonition! +:class: dropdown + +Here's some toggle-able content! +``` + +### The `{toggle-all-button}` directive + +Using the `{toggle-all-button}` directive will add a button that, when clicked, will toggle all of the content on the page. +Each item that is closed will be opened, and each item that is open will be closed. + +For example: + +```` +```{toggle-all-button} +``` +```` +results in: + +```{toggle-all-button} +``` + +### Toggle all items that match a selector + +If you'd like to restrict your `toggle-all-button` to only toggle a subset of items on your page, use the `:selector:` option. +This should map on to elements that were selected by the `togglebutton_selector` configuration. + +For example, to only toggle admonition blocks, use this selector: + +```` +```{toggle-all-button} Toggle admonitions +:selector: .admonition.dropdown +``` +```` + +This results in: + +```{toggle-all-button} Toggle admonitions +:selector: .admonition.dropdown +``` + +### Customize the title + +You may optionally provide a title as well. +For example: + +```` +```{toggle-all-button} Toggle all the things! +``` +```` +results in: + +```{toggle-all-button} Toggle all the things! +``` + ## Printing behavior with toggle buttons By default `sphinx-togglebutton` will **open all toggle-able content when you print**. diff --git a/sphinx_togglebutton/__init__.py b/sphinx_togglebutton/__init__.py index aeac7b5..a2dd70e 100644 --- a/sphinx_togglebutton/__init__.py +++ b/sphinx_togglebutton/__init__.py @@ -1,8 +1,9 @@ """A small sphinx extension to add "toggle" buttons to items.""" import os -from docutils.parsers.rst import Directive, directives from docutils import nodes +from .directive import Toggle, ToggleAllButton, ToggleAllNode + __version__ = "0.3.0" @@ -28,24 +29,16 @@ def insert_custom_selection_config(app): app.add_js_file(None, body=js_text) -class Toggle(Directive): - """Hide a block of markup text by wrapping it in a container.""" - - optional_arguments = 1 - final_argument_whitespace = True - has_content = True - - option_spec = {"id": directives.unchanged, "show": directives.flag} +# Helper functions for controlling our toggle node's behavior +def visit_element_html(self, node): + """Render an element node as HTML.""" + self.body.append(node.html()) + raise nodes.SkipNode - def run(self): - self.assert_has_content() - classes = ["toggle"] - if "show" in self.options: - classes.append("toggle-shown") - parent = nodes.container(classes=classes) - self.state.nested_parse(self.content, self.content_offset, parent) - return [parent] +def skip(self, node): + """Skip the node on a particular builder type.""" + raise nodes.SkipNode # We connect this function to the step after the builder is initialized @@ -66,7 +59,19 @@ def setup(app): # Run the function after the builder is initialized app.connect("builder-inited", insert_custom_selection_config) app.connect("config-inited", initialize_js_assets) + + # Register nodes and directives app.add_directive("toggle", Toggle) + app.add_directive("toggle-all-button", ToggleAllButton) + app.add_node( + ToggleAllNode, + html=(visit_element_html, None), + latex=(skip, None), + textinfo=(skip, None), + text=(skip, None), + man=(skip, None), + override=True, + ) return { "version": __version__, "parallel_read_safe": True, diff --git a/sphinx_togglebutton/_static/togglebutton.css b/sphinx_togglebutton/_static/togglebutton.css index 3560ceb..c3eb44f 100644 --- a/sphinx_togglebutton/_static/togglebutton.css +++ b/sphinx_togglebutton/_static/togglebutton.css @@ -1,3 +1,32 @@ +/** + * General button styles + */ +.toggle-button-style { + display: flex; + align-items: center; + width: fit-content; + + border-radius: .4em; + border: 1px solid #ccc; + background: #f8f8f8; + padding: 0.5em 1em; + font-size: .9em; + margin-bottom: 1em; +} + +.toggle-button-style:hover { + background: #f6f6f6; +} + +.toggle-button-style:active { + background: #eee; +} + +.toggle-button-style:focus, .toggle-button-style:focus-visible { + outline: none; +} + + /** * Admonition-based toggles */ @@ -84,24 +113,9 @@ details.toggle-details { } details.toggle-details summary { - display: flex; - align-items: center; - width: fit-content; cursor: pointer; list-style: none; - border-radius: .4em; - border: 1px solid #ccc; - background: #f8f8f8; padding: 0.4em 1em 0.4em 0.5em; /* Less padding on left because the SVG has left margin */ - font-size: .9em; -} - -details.toggle-details summary:hover { - background: #f6f6f6; -} - -details.toggle-details summary:active { - background: #eee; } details.toggle-details[open] summary { diff --git a/sphinx_togglebutton/_static/togglebutton.js b/sphinx_togglebutton/_static/togglebutton.js index 0d15d0c..2f75c6c 100644 --- a/sphinx_togglebutton/_static/togglebutton.js +++ b/sphinx_togglebutton/_static/togglebutton.js @@ -50,7 +50,7 @@ var initToggleItems = () => { // Define the structure of the details block and insert it as a sibling var detailsBlock = `
- + ${toggleChevron} ${toggleHintShow} @@ -170,3 +170,15 @@ if (toggleOpenOnPrint == "true") { }); }); } + + +const toggleAllBySelector = (selector) => { + document.querySelectorAll(selector).forEach((el) => { + if (el.classList.contains("admonition")) { + el.querySelector("button.toggle-button").click(); + } else { + // We have a details tag, the parent is the `
` block + el.parentElement.open = !(el.parentElement.open === true) + } + }); +} \ No newline at end of file diff --git a/sphinx_togglebutton/directive.py b/sphinx_togglebutton/directive.py new file mode 100644 index 0000000..6bc8765 --- /dev/null +++ b/sphinx_togglebutton/directive.py @@ -0,0 +1,68 @@ +import email +from docutils import nodes +from docutils.parsers.rst import directives +from sphinx.util.docutils import SphinxDirective + + +class ToggleAllNode(nodes.Element): + """Appended to the doctree by the ToggleAll directive + + Renders as a button to enable thebe on the page. + + If no ToggleAll directive is found in the document but thebe + is enabled, the node is added at the bottom of the document. + """ + + def __init__(self, rawsource="", *children, selector=None, text="Toggle All", **attributes): + super().__init__("", text=text, selector=selector) + + def html(self): + text = self["text"] + selector = self["selector"] + return (f"""\ + """) + + +class ToggleAllButton(SphinxDirective): + """Trigger toggle on all elements that match a selector.""" + + optional_arguments = 1 + final_argument_whitespace = True + option_spec = { + "selector": directives.unchanged + } + has_content = False + + def run(self): + kwargs = { + "text": "Toggle all buttons", + "selector": self.env.config.togglebutton_selector + } + if self.arguments: + kwargs["text"] = self.arguments[0] + if self.options.get("selector"): + kwargs["selector"] = self.options["selector"] + return [ToggleAllNode(**kwargs)] + + +class Toggle(SphinxDirective): + """Hide a block of markup text by wrapping it in a container.""" + + optional_arguments = 1 + final_argument_whitespace = True + has_content = True + + option_spec = {"id": directives.unchanged, "show": directives.flag} + + def run(self): + self.assert_has_content() + classes = ["toggle"] + if "show" in self.options: + classes.append("toggle-shown") + + parent = nodes.container(classes=classes) + self.state.nested_parse(self.content, self.content_offset, parent) + return [parent] From 770505ec8b0f803868ff5bb742b05c0a8227f8ef Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Thu, 10 Feb 2022 15:01:37 -0800 Subject: [PATCH 2/3] NEW: Add toggle-all button --- docs/conf.py | 2 +- docs/index.md | 1 + docs/reference/notebooks.md | 73 ++++++++++++++ docs/use.md | 168 ++++++++++++++++++++------------ setup.py | 2 +- sphinx_togglebutton/__init__.py | 2 +- 6 files changed, 183 insertions(+), 65 deletions(-) create mode 100644 docs/reference/notebooks.md diff --git a/docs/conf.py b/docs/conf.py index 106229f..c242761 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,7 +38,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["myst_parser", "sphinx_design", "sphinx_togglebutton"] +extensions = ["myst_nb", "sphinx_copybutton", "sphinx_design", "sphinx_togglebutton"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/index.md b/docs/index.md index 759109c..518ccb4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -81,5 +81,6 @@ See {ref}`usage` for information about how to use `sphinx-togglebutton`. :maxdepth: 2 use reference/index +reference/notebooks changelog ``` \ No newline at end of file diff --git a/docs/reference/notebooks.md b/docs/reference/notebooks.md new file mode 100644 index 0000000..fa04d99 --- /dev/null +++ b/docs/reference/notebooks.md @@ -0,0 +1,73 @@ +--- +jupytext: + cell_metadata_filter: -all + formats: md:myst + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.11.5 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# MyST Notebooks + +`sphinx-togglebutton` is particularly useful with `MyST-NB` notebooks. +This is used to show and hide code cell inputs and outputs. + +Here is a demonstration of the functionality. + +## `{toggle-all-button}` usage + +The code below generated the buttons that follow: + +```` +```{toggle-all-button} +``` + +```{toggle-all-button} Toggle all inputs +:selector: .cell.tag_hide-input .cell_input +``` + +```{toggle-all-button} Toggle all outputs +:selector: .cell.tag_hide-output .cell_output +``` +```` + +```{toggle-all-button} +``` + +```{toggle-all-button} Toggle all inputs +:selector: .cell.tag_hide-input .cell_input +``` + +```{toggle-all-button} Toggle all outputs +:selector: .cell.tag_hide-output .cell_output +``` + +## Cell inputs + +```{code-cell} +:tags: [hide-input] +for ii in range(20): + print(f"Number: {ii}") +``` + +## Cell outputs + +```{code-cell} +:tags: [hide-output] +for ii in range(20): + print(f"Number: {ii}") +``` + +## Hide the whole cell + +```{code-cell} +:tags: [hide-cell] +for ii in range(20): + print(f"Number: {ii}") +``` diff --git a/docs/use.md b/docs/use.md index 94b7af6..ecabe92 100644 --- a/docs/use.md +++ b/docs/use.md @@ -15,7 +15,16 @@ Each is described below ## Collapse a block of content with a CSS selector You can hide any content and display a toggle button to show it by using certain CSS classes. -`sphinx-togglebutton` will wrap elements with these classes in a `
` block like so: + +By default, `sphinx-togglebutton` will use this selector: + +``` +.toggle, .admonition.dropdown +``` + +### Block-level toggles + +For most content, `sphinx-togglebutton` will wrap elements that match this selector in a `
` block like so: ```html
@@ -24,7 +33,7 @@ You can hide any content and display a toggle button to show it by using certain
``` -:::{admonition} example +:::{admonition} example block-level toggle :class: tip This MyST Markdown: @@ -39,45 +48,32 @@ results in: ``` ::: -### Configure the CSS selector used to insert toggle buttons - -By default, `sphinx-togglebutton` will use this selector: - -``` -.toggle, .admonition.dropdown -``` - -However, you can customize this behavior with the `togglebutton_selector` configuration value. -To specify the selector to use, pass a valid CSS selector as a string: - -:::{admonition} example -:class: tip -Configure `sphinx-togglebutton` to look for a `.toggle-this-element` class and an element with ID `#my-special-id` **instead of** `.toggle` and `.admonition.dropdown`. - -```python -sphinx_togglebutton_selector = ".toggle-this-element, #my-special-id" -``` -::: (dropdown-admonitions)= -## Collapse admonitions with the `dropdown` class +### Toggle admonitions -`sphinx-togglebutton` treats admonitions as a special case if they are selected. -If a Sphinx admonition matches the toggle button selector, then its title will be displayed with a button to reveal its content. +If the matched element **also has an `admonition` class** then `sphinx-togglebutton` will treat it as a Sphinx admonition, and will only toggle the _content_ of the admonition. -:::{admonition} example +:::{admonition} example admonition toggle :class: tip -````md -```{admonition} This will be shown +This MyST Markdown: + +`````md +````{admonition} Here's my admonition :class: dropdown -And this will be hidden! + +```{image} https://media.giphy.com/media/FaKV1cVKlVRxC/giphy.gif ``` ```` -results in -```{admonition} This will be shown +````` + +results in: +````{admonition} Here's my admonition :class: dropdown -And this will be hidden! + +```{image} https://media.giphy.com/media/FaKV1cVKlVRxC/giphy.gif ``` +```` ::: This works for any kind of Sphinx admoniton: @@ -93,6 +89,20 @@ A warning! ::: +### Configure the CSS selector used to insert toggle buttons + +You can customize this behavior with the `togglebutton_selector` configuration value. +To specify the selector to use, pass a valid CSS selector as a string: + +:::{admonition} example +:class: tip +Configure `sphinx-togglebutton` to look for a `.toggle-this-element` class and an element with ID `#my-special-id` **instead of** `.toggle` and `.admonition.dropdown`. + +```python +sphinx_togglebutton_selector = ".toggle-this-element, #my-special-id" +``` +::: + (toggle-directive)= ## Use the `{toggle}` directive to toggle blocks of content @@ -141,35 +151,6 @@ It results in the following: Here is my toggle-able content! ::: -## Change the button hint text - -You can control the "hint" text that is displayed next to togglebuttons. -To do so, use the following configuration variable in your `conf.py` file: - -```python -togglebutton_hint = "Displayed when the toggle is closed." -togglebutton_hint_hide = "Displayed when the toggle is open." -``` - -## Change the toggle icon color - -You can apply some extra styles to the toggle button to achieve the look you want. -This is particularly useful if the color of the toggle button does not contrast with the background of an admonition. - -To style the toggle button, [add a custom CSS file to your documentation](https://docs.readthedocs.io/en/stable/guides/adding-custom-css.html) and include a custom CSS selector like so: - -```scss -// Turn the color red... -// ...with admonition toggle buttons -button.toggle-button { - color: red; -} - -// ...with content block toggle buttons -.toggle-button summary { - color: red; -} -``` ## Toggle all buttons at once @@ -204,7 +185,7 @@ results in: ```{toggle-all-button} ``` -### Toggle all items that match a selector +#### Toggle all items that match a selector If you'd like to restrict your `toggle-all-button` to only toggle a subset of items on your page, use the `:selector:` option. This should map on to elements that were selected by the `togglebutton_selector` configuration. @@ -223,7 +204,7 @@ This results in: :selector: .admonition.dropdown ``` -### Customize the title +#### Customize the title You may optionally provide a title as well. For example: @@ -232,11 +213,74 @@ For example: ```{toggle-all-button} Toggle all the things! ``` ```` + results in: ```{toggle-all-button} Toggle all the things! ``` +### Use the JavaScript function to toggle all buttons + +There is also a JavaScript function you can call however you wish to trigger the same behavior as the directive described above. + +This function is called `toggleAllBySelector` and takes a single argument, which is a string selector for the elements that you wish to toggle. + +For example, here's a toggle-able admonition: + +:::{note} +:class: dropdown +This admonition can be toggled! +::: + +The following code embeds a `{raw}` block with custom HTML to trigger this function: + +```` +```{raw} html + +``` +```` + +results in: + +```{raw} html + +``` + +:::{tip} +This is particularly useful for theme developers who wish to embed toggle elements in their user interfaces. +::: + +## Change the button hint text + +You can control the "hint" text that is displayed next to togglebuttons. +To do so, use the following configuration variable in your `conf.py` file: + +```python +togglebutton_hint = "Displayed when the toggle is closed." +togglebutton_hint_hide = "Displayed when the toggle is open." +``` + +## Change the toggle icon color + +You can apply some extra styles to the toggle button to achieve the look you want. +This is particularly useful if the color of the toggle button does not contrast with the background of an admonition. + +To style the toggle button, [add a custom CSS file to your documentation](https://docs.readthedocs.io/en/stable/guides/adding-custom-css.html) and include a custom CSS selector like so: + +```scss +// Turn the color red... +// ...with admonition toggle buttons +button.toggle-button { + color: red; +} + +// ...with content block toggle buttons +.toggle-button summary { + color: red; +} +``` + + ## Printing behavior with toggle buttons By default `sphinx-togglebutton` will **open all toggle-able content when you print**. diff --git a/setup.py b/setup.py index 07e35d9..566f187 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,6 @@ "sphinx_togglebutton": ["_static/togglebutton.css", "_static/togglebutton.js", "_static/togglebutton-chevron.svg"] }, install_requires=["setuptools", "wheel", "sphinx", "docutils"], - extras_require={"sphinx": ["myst_parser", "sphinx_book_theme", "sphinx_design"]}, + extras_require={"sphinx": ["myst_nb", "sphinx_copybutton", "sphinx_book_theme", "sphinx_design"]}, classifiers=["License :: OSI Approved :: MIT License"], ) diff --git a/sphinx_togglebutton/__init__.py b/sphinx_togglebutton/__init__.py index a2dd70e..0b66de3 100644 --- a/sphinx_togglebutton/__init__.py +++ b/sphinx_togglebutton/__init__.py @@ -59,7 +59,7 @@ def setup(app): # Run the function after the builder is initialized app.connect("builder-inited", insert_custom_selection_config) app.connect("config-inited", initialize_js_assets) - + # Register nodes and directives app.add_directive("toggle", Toggle) app.add_directive("toggle-all-button", ToggleAllButton) From 5a6d6f99503b6ad30125aa3ca5c07366d3762e88 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Sat, 12 Feb 2022 14:49:22 -0600 Subject: [PATCH 3/3] Use groups instead of all --- docs/_static/logo-chevron.svg | 4 + docs/conf.py | 30 ++ docs/index.md | 2 +- docs/reference/notebooks.md | 12 +- docs/use.md | 360 +++++++++++++------- sphinx_togglebutton/__init__.py | 21 +- sphinx_togglebutton/_static/togglebutton.js | 163 +++++---- sphinx_togglebutton/directive.py | 38 ++- 8 files changed, 408 insertions(+), 222 deletions(-) create mode 100644 docs/_static/logo-chevron.svg diff --git a/docs/_static/logo-chevron.svg b/docs/_static/logo-chevron.svg new file mode 100644 index 0000000..cfc047e --- /dev/null +++ b/docs/_static/logo-chevron.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index c242761..1cdb144 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -89,6 +89,14 @@ myst_enable_extensions = ["colon_fence"] # To test behavior in JS +togglebutton_groups = { + "cell-inputs": ".cell.tag_hide-input .cell_input", +"cell-outputs": ".cell.tag_hide-output .cell_output", +"directive": ".toggle", +"admonitions": ".admonition.dropdown", +"group1": ".group-one", +"group2": ".group-two", +} # togglebutton_hint = "test show" # togglebutton_hint_hide = "test hide" # togglebutton_open_on_print = False @@ -97,6 +105,8 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] +html_logo = "_static/logo-chevron.svg" +html_title = "Sphinx Togglebutton" # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -176,3 +186,23 @@ "Miscellaneous", ) ] + +# -- Create an example directive for the docs --------------------------------- +from docutils.parsers.rst.directives.admonitions import Admonition + + +class Example(Admonition): + def run(self): + # Manually add a "tip" class to style it + if "class" not in self.options: + self.options["class"] = ["tip"] + else: + self.options["class"].append("tip") + # Add `Example` to the title so we don't have to type it + self.arguments[0] = f"Example: {self.arguments[0]}" + # Now run the Admonition logic so it behaves the same way + nodes = super().run() + return nodes + +def setup(app): + app.add_directive("example", Example) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 518ccb4..ab5f59d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,7 @@ Some content ```` ::: -See {ref}`dropdown-admonitions` for more information. +See {ref}`use:admonition-toggles` for more information. ## Hide any content behind a toggle button diff --git a/docs/reference/notebooks.md b/docs/reference/notebooks.md index fa04d99..d08c694 100644 --- a/docs/reference/notebooks.md +++ b/docs/reference/notebooks.md @@ -28,24 +28,20 @@ The code below generated the buttons that follow: ```{toggle-all-button} ``` -```{toggle-all-button} Toggle all inputs -:selector: .cell.tag_hide-input .cell_input +```{toggle-all-button} cell-inputs ``` -```{toggle-all-button} Toggle all outputs -:selector: .cell.tag_hide-output .cell_output +```{toggle-all-button} cell-outputs ``` ```` ```{toggle-all-button} ``` -```{toggle-all-button} Toggle all inputs -:selector: .cell.tag_hide-input .cell_input +```{toggle-all-button} cell-inputs ``` -```{toggle-all-button} Toggle all outputs -:selector: .cell.tag_hide-output .cell_output +```{toggle-all-button} cell-outputs ``` ## Cell inputs diff --git a/docs/use.md b/docs/use.md index ecabe92..9b95b5f 100644 --- a/docs/use.md +++ b/docs/use.md @@ -3,28 +3,110 @@ This page covers how to use and configure / customize `sphinx-togglebutton`. -There are three main ways to use `sphinx-togglebutton`: +**To toggle content**, there are two options: -- Wrap arbitrary objects in a toggle button via a CSS selector -- Collapse admonitions with the `dropdown` class -- Make arbitrary chunks of content "toggle-able" with the `toggle` directive +- Use [a CSS selector](use:css-selector) to identify and toggle content +- Use [a `{toggle}` directive](use:toggle-directive) to toggle chunks of content. -Each is described below +**To toggle multiple items at once** you may **group** elements to toggle, and [insert a button to toggle all items of a group](use:toggle-groups). + +Each is described below. (use:css-selector)= -## Collapse a block of content with a CSS selector +## Collapse content with a toggle button + +You can hide any content and display a toggle button to show it. +There are two ways to do this. -You can hide any content and display a toggle button to show it by using certain CSS classes. +### With CSS classes -By default, `sphinx-togglebutton` will use this selector: +`sphinx-togglebutton` will use a CSS selector to match elements on a page, and will convert each of them into toggle-able content. +By default, `sphinx-togglebutton` uses this selector: ``` .toggle, .admonition.dropdown ``` +This will match any items with `toggle` class, and Sphinx admonitions with a `dropdown` class. + +:::{example} A div with a `toggle` class +Create a `div` with a `toggle` class, so that `sphinx-togglebutton` will detect it with the default selector: + +```` +```{container} toggle +Some toggled content with a `toggle` class. +``` +```` + +results in: + +```{container} toggle +Some toggled content with a `toggle` class. +``` +::: + +:::{tip} +You can change the selector that is used by providing your own selector in `togglebutton_selector`. +However, a more flexible way to control which elements are toggled is [to use toggle button groups](use:toggle-groups). +::: + +(use:toggle-directive)= +### With the `{toggle}` directive + +As a short-hand for the above approach, there is a `{toggle}` directive to quickly wrap content in a Toggle button. +The content of the `{toggle}` directive will be wrapped in a `
` element (see [](use:block-level)). + +:::{example} Wrap an image with a `{toggle}` directive +To wrap an image use the toggle directive like so: + +```` +```{toggle} +Here is my toggle-able content! +``` +```` +results in + +```{toggle} +Here is my toggle-able content! +``` +::: + +#### Show the content by default + +To show the toggle-able content by default, use the `:show:` flag. + +:::{example} Show content by default + +```` +```{toggle} +:show: +Here is my toggle-able content! +``` +```` + +results in + +```{toggle} +:show: +Here is my toggle-able content! +``` +::: + +## Types of toggled content + +There are two types of toggle-able content: [block-level toggles](use:block-level) and [admonition toggles](use:admonition-toggle). + +(use:block-level)= ### Block-level toggles -For most content, `sphinx-togglebutton` will wrap elements that match this selector in a `
` block like so: +For most content, `sphinx-togglebutton` will wrap elements that match the selector in a `
` block. +The block looks like this: + +```{toggle} +Some toggle-able content! +``` + +These blocks have the following `
` structure: ```html
@@ -33,30 +115,40 @@ For most content, `sphinx-togglebutton` will wrap elements that match this selec
``` -:::{admonition} example block-level toggle -:class: tip -This MyST Markdown: +:::{example} Block-level toggles +Add a `toggle` class to an image directive so that it becomes toggled. ````md ```{image} https://media.giphy.com/media/FaKV1cVKlVRxC/giphy.gif :class: toggle ``` ```` + results in: + ```{image} https://media.giphy.com/media/FaKV1cVKlVRxC/giphy.gif :class: toggle ``` + ::: -(dropdown-admonitions)= -### Toggle admonitions +(use:admonition-toggles)= +### Admonition toggles If the matched element **also has an `admonition` class** then `sphinx-togglebutton` will treat it as a Sphinx admonition, and will only toggle the _content_ of the admonition. +For example: + +:::{warning} +:class: dropdown +A toggled warning! +::: + +The default selector will match admonitions with a `dropdown` class (the selector is `.admonitions.dropdown`). + +:::{example} Make an `{admonition}` toggled -:::{admonition} example admonition toggle -:class: tip -This MyST Markdown: +Create a toggled admonition by adding the `dropdown` class to it. `````md ````{admonition} Here's my admonition @@ -68,6 +160,7 @@ This MyST Markdown: ````` results in: + ````{admonition} Here's my admonition :class: dropdown @@ -76,181 +169,217 @@ results in: ```` ::: -This works for any kind of Sphinx admoniton: -:::{note} -:class: dropdown -A note! -::: +(use:toggle-groups)= +## Create groups of toggled content + +You can create groups of toggle content that can each be opened and closed as a group. +To do so, use the `togglebutton_groups` configuration. +This is a dictionary of `group-name: css-selector` pairs. +Each group name can be referenced by the [group toggle button](use:group-toggle-all). + +For example, the following configuration creates two toggle groups + +```python +togglebutton_groups = { + "group1": ".group-one", + "group2": ".group-two" +} +``` + +Any elements that match the selector `.group-one` will be assigned to `group1`, and any elements that match the selector `.group-two` will be assigned to `group2`. :::{warning} -:class: dropdown -A warning! +Using `togglebutton_groups` will **override** the value of `togglebutton_selector`. +If you want to manually keep the default selector for toggle-buttons, add a group with the `.toggle, .admonition.dropdown` selector to it. ::: +(use:group-toggle-all)= +### Toggle groups at once with the `{toggle-group}` directive -### Configure the CSS selector used to insert toggle buttons +To toggle all items that belong to the same group at once, use the `{toggle-group}` directive. +It will add a button that, when clicked, will toggle all of the content in a group. +Each item that is closed will be opened, and each item that is open will be closed. -You can customize this behavior with the `togglebutton_selector` configuration value. -To specify the selector to use, pass a valid CSS selector as a string: +:::{example} Toggle only buttons from one group -:::{admonition} example -:class: tip -Configure `sphinx-togglebutton` to look for a `.toggle-this-element` class and an element with ID `#my-special-id` **instead of** `.toggle` and `.admonition.dropdown`. +Below we define a `{toggle-group}` button that only toggles elements from toggled items that are part of `group1`. + +First, define `group1` and `group2` in `conf.py`: ```python -sphinx_togglebutton_selector = ".toggle-this-element, #my-special-id" +togglebutton_groups = { + "group1": ".group-one", + "group2": ".group-two" +} ``` -::: -(toggle-directive)= -## Use the `{toggle}` directive to toggle blocks of content +Next, create two blocks of toggled content, one for each group: -To add toggle-able content, use the **toggle directive**. This directive -will wrap its content in a toggle-able container. You can call it like so: +```` +```{container} group-one +This first content is for **Group One**. +``` +```{container} group-one +This second content is for **Group One**. +``` +```{container} group-two +This first content is for **Group Two**. +``` +```{container} group-two +This second content is for **Group Two**. +``` +```` +```{container} group-one +This first content is for **Group One**. +``` +```{container} group-one +This second content is for **Group One**. +``` +```{container} group-two +This first content is for **Group Two**. +``` +```{container} group-two +This second content is for **Group Two**. +``` -:::{tab-set-code} +Finally, add a button to toggle **only** group one: -````markdown -```{toggle} -Here is my toggle-able content! +```` +```{toggle-group} group1 ``` ```` -```rst -.. toggle:: +results in: - Here is my toggle-able content! +```{toggle-group} group1 ``` - ::: +#### Toggle all groups at once -The code above results in: +To toggle all groups with the button, simply do not provide a group name and it will selector all groups. -:::{toggle} +:::{example} Toggle all buttons regardless of group -Here is my toggle-able content! +The following code will toggle *all* buttons on the page: + +```` +```{toggle-group} +``` +```` + +```{toggle-group} +``` ::: -To show the toggle-able content by default, use the `:show:` flag. +#### Customize the title of `{toggle-group}` -````markdown -```{toggle} -:show: +You may optionally provide a title as well. -Here is my toggle-able content! +:::{example} Custom title with `{toggle-group}` + +```` +```{toggle-group} tips +:text: Toggle all of the buttons on the page! ``` ```` -It results in the following: - -:::{toggle} -:show: +results in: -Here is my toggle-able content! +```{toggle-group} tips +:text: Toggle all of the buttons on the page! +``` ::: +### Use the JavaScript function to toggle all buttons -## Toggle all buttons at once - -There are two ways that you can toggle all elements on the screen at once. -Both are covered below. - -For reference, we'll show two kinds of toggle-able content so you can see the behavior. +There is also a JavaScript function you can call to trigger the same behavior as the `{toggle-group}` directive described above. -```{toggle} -**Here's a toggle-able content block!** -``` +This function is called `toggleAllByGroup` and takes a single argument, which is a group name that you wish to toggle. -```{admonition} And this is a toggleable admonition! -:class: dropdown +You can call it like: -Here's some toggle-able content! +```javascript +toggleAllByGroup("groupname"); ``` -### The `{toggle-all-button}` directive +It is meant for designing your own buttons and UI elements that trigger toggle buttons on the page. -Using the `{toggle-all-button}` directive will add a button that, when clicked, will toggle all of the content on the page. -Each item that is closed will be opened, and each item that is open will be closed. +:::{example} Create your own `toggle-groups` button -For example: +Here's a toggle-able admonition matching **group1** from above: ```` -```{toggle-all-button} +```{note} +:class: group-one +Here's a toggle-admonition in `group-one`! ``` ```` results in: -```{toggle-all-button} +```{note} +:class: group-one +Here's a toggle-admonition in `group-one`! ``` -#### Toggle all items that match a selector - -If you'd like to restrict your `toggle-all-button` to only toggle a subset of items on your page, use the `:selector:` option. -This should map on to elements that were selected by the `togglebutton_selector` configuration. - -For example, to only toggle admonition blocks, use this selector: +And the following code embeds a `{raw}` block with custom HTML to trigger this function: ```` -```{toggle-all-button} Toggle admonitions -:selector: .admonition.dropdown +```{raw} html + ``` ```` -This results in: +results in: -```{toggle-all-button} Toggle admonitions -:selector: .admonition.dropdown +```{raw} html + ``` +::: -#### Customize the title +To **toggle all buttons at once** with JavaScript, regardless of group, pass `" "` to the function (i.e., `toggleAllByGroup("**")`). -You may optionally provide a title as well. -For example: +:::{example} Toggle all buttons with JavaScript +Below are two toggle-buttons from two different groups: ```` -```{toggle-all-button} Toggle all the things! +```{container} group-one +Group one text! +``` +```{container} group-two +Group two text! ``` ```` results in: -```{toggle-all-button} Toggle all the things! +```{container} group-one +Group one text! +``` +```{container} group-two +Group two text! ``` -### Use the JavaScript function to toggle all buttons - -There is also a JavaScript function you can call however you wish to trigger the same behavior as the directive described above. - -This function is called `toggleAllBySelector` and takes a single argument, which is a string selector for the elements that you wish to toggle. - -For example, here's a toggle-able admonition: - -:::{note} -:class: dropdown -This admonition can be toggled! -::: - -The following code embeds a `{raw}` block with custom HTML to trigger this function: +And the following JavaScript button opens both at once: ```` ```{raw} html - + ``` ```` - -results in: +results in ```{raw} html - + ``` - -:::{tip} -This is particularly useful for theme developers who wish to embed toggle elements in their user interfaces. ::: -## Change the button hint text +## Customize the toggle button style + +There are a few ways you can customize the toggle button style. + +### Change toggle button hint text You can control the "hint" text that is displayed next to togglebuttons. To do so, use the following configuration variable in your `conf.py` file: @@ -260,7 +389,7 @@ togglebutton_hint = "Displayed when the toggle is closed." togglebutton_hint_hide = "Displayed when the toggle is open." ``` -## Change the toggle icon color +### Change the toggle icon color You can apply some extra styles to the toggle button to achieve the look you want. This is particularly useful if the color of the toggle button does not contrast with the background of an admonition. @@ -280,8 +409,7 @@ button.toggle-button { } ``` - -## Printing behavior with toggle buttons +## Print behavior with toggle buttons By default `sphinx-togglebutton` will **open all toggle-able content when you print**. It will close them again when the printing operation is complete. diff --git a/sphinx_togglebutton/__init__.py b/sphinx_togglebutton/__init__.py index 0b66de3..d6f6302 100644 --- a/sphinx_togglebutton/__init__.py +++ b/sphinx_togglebutton/__init__.py @@ -1,11 +1,15 @@ """A small sphinx extension to add "toggle" buttons to items.""" import os +from typing import Dict, AnyStr from docutils import nodes +from sphinx.util import logging -from .directive import Toggle, ToggleAllButton, ToggleAllNode +from .directive import Toggle, ToggleAllInGroupButton, ToggleAllInGroupNode __version__ = "0.3.0" +SPHINX_LOGGER = logging.getLogger(__name__) + def st_static_path(app): static_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "_static")) @@ -23,9 +27,13 @@ def initialize_js_assets(app, config): # This function reads in a variable and inserts it into JavaScript def insert_custom_selection_config(app): - # This is a configuration that you've specified for users in `conf.py` - selector = app.config["togglebutton_selector"] - js_text = "var togglebuttonSelector = '%s';" % selector + if isinstance(app.config.togglebutton_selector, str): + selectors = {"default_selector": app.config.togglebutton_selector} + if app.config.togglebutton_groups: + if isinstance(app.config.togglebutton_selector, str): + SPHINX_LOGGER.info("toggle-button]: Selector groups over-riding selector string.") + selectors = app.config.togglebutton_groups + js_text = "var togglebuttonSelectors = %s;" % selectors app.add_js_file(None, body=js_text) @@ -52,6 +60,7 @@ def setup(app): # Add the string we'll use to select items in the JS # Tell Sphinx about this configuration variable app.add_config_value("togglebutton_selector", ".toggle, .admonition.dropdown", "html") + app.add_config_value("togglebutton_groups", {}, "html") app.add_config_value("togglebutton_hint", "Click to show", "html") app.add_config_value("togglebutton_hint_hide", "Click to hide", "html") app.add_config_value("togglebutton_open_on_print", True, "html") @@ -62,9 +71,9 @@ def setup(app): # Register nodes and directives app.add_directive("toggle", Toggle) - app.add_directive("toggle-all-button", ToggleAllButton) + app.add_directive("toggle-group", ToggleAllInGroupButton) app.add_node( - ToggleAllNode, + ToggleAllInGroupNode, html=(visit_element_html, None), latex=(skip, None), textinfo=(skip, None), diff --git a/sphinx_togglebutton/_static/togglebutton.js b/sphinx_togglebutton/_static/togglebutton.js index 2f75c6c..5d64193 100644 --- a/sphinx_togglebutton/_static/togglebutton.js +++ b/sphinx_togglebutton/_static/togglebutton.js @@ -9,83 +9,89 @@ let toggleChevron = ` `; var initToggleItems = () => { - var itemsToToggle = document.querySelectorAll(togglebuttonSelector); - console.log(`[togglebutton]: Adding toggle buttons to ${itemsToToggle.length} items`) - // Add the button to each admonition and hook up a callback to toggle visibility - itemsToToggle.forEach((item, index) => { - if (item.classList.contains("admonition")) { - // If it's an admonition block, then we'll add a button inside - // Generate unique IDs for this item - var toggleID = `toggle-${index}`; - var buttonID = `button-${toggleID}`; - - item.setAttribute('id', toggleID); - if (!item.classList.contains("toggle")){ - item.classList.add("toggle"); - } - // This is the button that will be added to each item to trigger the toggle - var collapseButton = ` - `; - - item.insertAdjacentHTML("afterbegin", collapseButton); - thisButton = document.getElementById(buttonID); - - // Add click handlers for the button + admonition title (if admonition) - thisButton.addEventListener('click', toggleClickHandler); - admonitionTitle = document.querySelector(`#${toggleID} > .admonition-title`) - if (admonitionTitle) { - admonitionTitle.addEventListener('click', toggleClickHandler); - admonitionTitle.dataset.target = toggleID - admonitionTitle.dataset.button = buttonID - } - - // Now hide the item for this toggle button unless explicitly noted to show - if (!item.classList.contains("toggle-shown")) { - toggleHidden(thisButton); - } - } else { - // If not an admonition, wrap the block in a
block - // Define the structure of the details block and insert it as a sibling - var detailsBlock = ` -
- - ${toggleChevron} - ${toggleHintShow} - -
`; - item.insertAdjacentHTML("beforebegin", detailsBlock); - - // Now move the toggle-able content inside of the details block - details = item.previousElementSibling - details.appendChild(item) - - // Set up a click trigger to change the text as needed - details.addEventListener('click', (click) => { - let parent = click.target.parentElement; - if (parent.tagName.toLowerCase() == "details") { - summary = parent.querySelector("summary"); - details = parent; - } else { - summary = parent; - details = parent.parentElement; + for (const [group, selector] of Object.entries(togglebuttonSelectors)) { + var itemsToToggle = document.querySelectorAll(selector); + + console.log(`[togglebutton]: Adding toggle button group: ${group} with ${itemsToToggle.length} items`) + // Add the button to each admonition and hook up a callback to toggle visibility + itemsToToggle.forEach((item, index) => { + // Attach togglebutton data entries + item.dataset.togglebuttonGroup = group; + item.dataset.togglebutton = "true"; + if (item.classList.contains("admonition")) { + // If it's an admonition block, then we'll add a button inside + // Generate unique IDs for this item + var toggleID = `toggle-${index}`; + var buttonID = `button-${toggleID}`; + + item.setAttribute('id', toggleID); + if (!item.classList.contains("toggle")){ + item.classList.add("toggle"); } - // Update the inner text for the proper hint - if (details.open) { - summary.querySelector("span").innerText = toggleHintShow; - } else { - summary.querySelector("span").innerText = toggleHintHide; + // This is the button that will be added to each item to trigger the toggle + var collapseButton = ` + `; + + item.insertAdjacentHTML("afterbegin", collapseButton); + thisButton = document.getElementById(buttonID); + + // Add click handlers for the button + admonition title (if admonition) + thisButton.addEventListener('click', toggleClickHandler); + admonitionTitle = document.querySelector(`#${toggleID} > .admonition-title`) + if (admonitionTitle) { + admonitionTitle.addEventListener('click', toggleClickHandler); + admonitionTitle.dataset.target = toggleID + admonitionTitle.dataset.button = buttonID } - - }); - // If we have a toggle-shown class, open details block should be open - if (item.classList.contains("toggle-shown")) { - details.click(); + // Now hide the item for this toggle button unless explicitly noted to show + if (!item.classList.contains("toggle-shown")) { + toggleHidden(thisButton); + } + } else { + // If not an admonition, wrap the block in a
block + // Define the structure of the details block and insert it as a sibling + var detailsBlock = ` +
+ + ${toggleChevron} + ${toggleHintShow} + +
`; + item.insertAdjacentHTML("beforebegin", detailsBlock); + + // Now move the toggle-able content inside of the details block + details = item.previousElementSibling + details.appendChild(item) + + // Set up a click trigger to change the text as needed + details.addEventListener('click', (click) => { + let parent = click.target.parentElement; + if (parent.tagName.toLowerCase() == "details") { + summary = parent.querySelector("summary"); + details = parent; + } else { + summary = parent; + details = parent.parentElement; + } + // Update the inner text for the proper hint + if (details.open) { + summary.querySelector("span").innerText = toggleHintShow; + } else { + summary.querySelector("span").innerText = toggleHintHide; + } + + }); + + // If we have a toggle-shown class, open details block should be open + if (item.classList.contains("toggle-shown")) { + details.click(); + } } - } - }) + }); + } }; // This should simply add / remove the collapsed class and change the button text @@ -149,7 +155,6 @@ if (toggleOpenOnPrint == "true") { // Open the admonitions document.querySelectorAll(".admonition.toggle.toggle-hidden").forEach((el) => { - console.log(el); el.querySelector("button.toggle-button").click(); el.dataset["toggle_after_print"] = "true"; }); @@ -172,7 +177,15 @@ if (toggleOpenOnPrint == "true") { } -const toggleAllBySelector = (selector) => { +const toggleAllByGroup = (group) => { + // Are we toggling all buttons or just a group? + if (group === "**") { + var selector = "*[data-togglebutton='true']"; + } else { + var selector = `*[data-togglebutton-group="${group}"]`; + } + + // Trigger a toggle for each button that has matched the selector document.querySelectorAll(selector).forEach((el) => { if (el.classList.contains("admonition")) { el.querySelector("button.toggle-button").click(); diff --git a/sphinx_togglebutton/directive.py b/sphinx_togglebutton/directive.py index 6bc8765..ac092a2 100644 --- a/sphinx_togglebutton/directive.py +++ b/sphinx_togglebutton/directive.py @@ -4,7 +4,7 @@ from sphinx.util.docutils import SphinxDirective -class ToggleAllNode(nodes.Element): +class ToggleAllInGroupNode(nodes.Element): """Appended to the doctree by the ToggleAll directive Renders as a button to enable thebe on the page. @@ -13,39 +13,45 @@ class ToggleAllNode(nodes.Element): is enabled, the node is added at the bottom of the document. """ - def __init__(self, rawsource="", *children, selector=None, text="Toggle All", **attributes): - super().__init__("", text=text, selector=selector) + def __init__(self, rawsource="", *children, group=None, text="Toggle All", **attributes): + super().__init__("", text=text, group=group) def html(self): text = self["text"] - selector = self["selector"] + group = self["group"] return (f"""\ """) -class ToggleAllButton(SphinxDirective): - """Trigger toggle on all elements that match a selector.""" +class ToggleAllInGroupButton(SphinxDirective): + """Trigger toggle on all elements that match a group.""" optional_arguments = 1 final_argument_whitespace = True option_spec = { - "selector": directives.unchanged + "text": directives.unchanged } has_content = False def run(self): - kwargs = { - "text": "Toggle all buttons", - "selector": self.env.config.togglebutton_selector - } + kwargs = {} if self.arguments: - kwargs["text"] = self.arguments[0] - if self.options.get("selector"): - kwargs["selector"] = self.options["selector"] - return [ToggleAllNode(**kwargs)] + kwargs["group"] = self.arguments[0] + else: + kwargs["group"] = "**" + + if self.options.get("text"): + kwargs["text"] = self.options["text"] + else: + if kwargs["group"] == "**": + msg = "Toggle all content" + else: + msg = f"Toggle all {kwargs['group']}" + kwargs["text"] = msg + return [ToggleAllInGroupNode(**kwargs)] class Toggle(SphinxDirective):