From a154e01ceea0b43110e6bedf6a654b5893efe731 Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Fri, 21 Oct 2022 13:39:46 +0000 Subject: [PATCH] [IMP] extensions/spoilers: add an extension for spoilers It is often needed in developer tutorials to show excerpts of code for examples and solutions. This takes quite some page space and, for the latter, it is not always desired to show the final code result right after the exercise objective. This commit adds a spoiler feature to the documentation to allow hiding content until the reader wants to display it. The feature relies on the new `.. spoiler:: Button label` directive. task-3036845 closes odoo/documentation#2877 X-original-commit: 4d7a88639aa5effb288fa1c1ffcb2d607db19cca Signed-off-by: Antoine Vandevenne (anv) Co-authored-by: Morgan Meganck Co-authored-by: Stefano Rigano --- conf.py | 3 + .../documentation/rst_cheat_sheet.rst | 18 +++ .../odoo_theme/static/scss/_variables.scss | 7 ++ .../static/scss/bootstrap_overridden.scss | 14 +++ extensions/odoo_theme/static/style.scss | 36 ++++++ extensions/spoilers/__init__.py | 105 ++++++++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 extensions/spoilers/__init__.py diff --git a/conf.py b/conf.py index 967383e2f..c28c7d3d5 100644 --- a/conf.py +++ b/conf.py @@ -145,6 +145,9 @@ extensions = [ # Content tabs 'sphinx_tabs.tabs', + # Spoilers + 'spoilers', + # Strange html domain logic used in memento pages 'html_domain', ] diff --git a/content/contributing/documentation/rst_cheat_sheet.rst b/content/contributing/documentation/rst_cheat_sheet.rst index ee7689c2f..69c057883 100644 --- a/content/contributing/documentation/rst_cheat_sheet.rst +++ b/content/contributing/documentation/rst_cheat_sheet.rst @@ -714,6 +714,24 @@ Code blocks def main(): print("Hello world!") +.. _contributing/spoilers: + +Spoilers +======== + +.. list-table:: + :class: o-showcase-table + + * - .. spoiler:: Answer to the Ultimate Question of Life, the Universe, and Everything + + **42** + + * - .. code-block:: text + + .. spoiler:: Answer to the Ultimate Question of Life, the Universe, and Everything + + **42** + .. _contributing/tabs: Content tabs diff --git a/extensions/odoo_theme/static/scss/_variables.scss b/extensions/odoo_theme/static/scss/_variables.scss index dbec98217..d5797e8d6 100644 --- a/extensions/odoo_theme/static/scss/_variables.scss +++ b/extensions/odoo_theme/static/scss/_variables.scss @@ -241,3 +241,10 @@ $padding-l: 3rem; $margin-s: $padding-s; $margin-m: $padding-m; $margin-l: $padding-l; + +//------------------------------------------------------------------------------ +// Components +//------------------------------------------------------------------------------ + +// Accordion +$o-accordion-bg: tint-color($doc_exercise, 90%)!default; diff --git a/extensions/odoo_theme/static/scss/bootstrap_overridden.scss b/extensions/odoo_theme/static/scss/bootstrap_overridden.scss index 7094ece0c..42e8a5486 100644 --- a/extensions/odoo_theme/static/scss/bootstrap_overridden.scss +++ b/extensions/odoo_theme/static/scss/bootstrap_overridden.scss @@ -27,3 +27,17 @@ $dropdown-min-width: 4.5rem; // Modals $modal-backdrop-opacity: .7; + +// Accordion +$accordion-body-padding-y: 0 !default; +$accordion-body-padding-x: 0 !default; +$accordion-color: shade-color($doc_exercise, 35%) !default; +$accordion-bg: $o-accordion-bg; + +$accordion-button-color: $accordion-color !default; +$accordion-button-active-color: $accordion-button-color !default; +$accordion-button-bg: $accordion-bg !default; +$accordion-button-active-bg: $accordion-button-bg !default; + +$accordion-icon-transform: rotate(0deg) !default; + diff --git a/extensions/odoo_theme/static/style.scss b/extensions/odoo_theme/static/style.scss index afb64d7bc..40ba37368 100644 --- a/extensions/odoo_theme/static/style.scss +++ b/extensions/odoo_theme/static/style.scss @@ -1094,6 +1094,42 @@ header { } } } + +//------------------------------------------------------------------------------ +// Spoilers +//------------------------------------------------------------------------------ + +.o_spoiler { + border-color: tint-color($doc_exercise, 30%); + background-color: $o-accordion-bg; + + .accordion-button { + &::after { + margin: 0 .2rem 0 0; + } + + &.collapsed::after { + transform: rotate(-90deg); + } + } + + .accordion-body { + #wrap & { // FIXME: Temporary workaround to take priority over pre-existing rules. + > *:first-child { + margin-top: 1rem; + } + + code, div[class^="highlight"] { + border-color: tint-color($doc-exercise, 50%); + + &:first-child:last-child { + margin-bottom: 0; + } + } + } + } +} + //------------------------------------------------------------------------------ // Content Tabs //------------------------------------------------------------------------------ diff --git a/extensions/spoilers/__init__.py b/extensions/spoilers/__init__.py new file mode 100644 index 000000000..dfbca29ff --- /dev/null +++ b/extensions/spoilers/__init__.py @@ -0,0 +1,105 @@ +from docutils import nodes +from sphinx.locale import get_translation +from sphinx.util.docutils import SphinxDirective + + +class Spoiler(SphinxDirective): + """ Implement a `spoiler` directive with Bootstrap's accordion component. """ + required_arguments = 0 + optional_arguments = 1 + final_argument_whitespace = True + has_content = True + + def run(self): + """ Process the content of the directive. + + We use custom node classes to represent HTML elements (e.g., 'div') rather than the + corresponding sphinx.nodes.* class (e.g., sphinx.nodes.container) to prevent automatically + setting the name of the node as class (e.g., "container") on the element. + """ + _ = get_translation('spoilers') + self.assert_has_content() + spoiler_id = self.env.new_serialno("spoiler") + heading_id = f'o_spoiler_header_{spoiler_id}' + content_id = f'o_spoiler_content_{spoiler_id}' + + # Create the main accordion container. + accordion_container = Container( + classes=['accordion', 'accordion-flush', 'o_spoiler', 'alert'] + ) # Reuse the SCSS rules already defined for `alert`. Same thing for `alert-title`. + + # Create the accordion item container. + accordion_item_container = Container(classes=['accordion-item']) + accordion_container.append(accordion_item_container) + + # Create the header. + header = Header(ids=[heading_id], classes=['accordion-header']) + accordion_item_container.append(header) + + # Create the toggle button. + button = Button( + classes=[ + 'accordion-button', 'collapsed', 'flex-row-reverse', 'justify-content-end', + 'fw-bold', 'p-0', 'border-bottom-0' + ], + **{ + 'type': 'button', + 'data-bs-toggle': 'collapse', + 'data-bs-target': f'#{content_id}', + 'aria-expanded': 'false', + 'aria-controls': content_id, + } + ) + header.append(button) + + # Create the button label. + label = self.arguments[0] if self.arguments else _("Spoiler") + button_label = nodes.Text(label) + button.append(button_label) + + # Create the accordion collapse container. + accordion_collapse_container = Container( + ids=[content_id], + classes=['accordion-collapse', 'collapse', 'border-bottom-0'], + **{'aria-labelledby': heading_id}, + ) + accordion_item_container.append(accordion_collapse_container) + + # Create the accordion body container. + accordion_body_container = Container(classes=['accordion-body']) + self.state.nested_parse(self.content, self.content_offset, accordion_body_container) + accordion_collapse_container.append(accordion_body_container) + + return [accordion_container] + + +class Container(nodes.General, nodes.Element): + custom_tag_name = 'div' + + +class Header(nodes.General, nodes.Element): + custom_tag_name = 'span' + + +class Button(nodes.General, nodes.Element): + custom_tag_name = 'button' + + +def visit_node(translator, node): + custom_attr = {k: v for k, v in node.attributes.items() if k not in node.known_attributes} + translator.body.append(translator.starttag(node, node.custom_tag_name, **custom_attr).rstrip()) + + +def depart_node(translator, node): + translator.body.append(f'') + + +def setup(app): + app.add_directive('spoiler', Spoiler) + app.add_node(Container, html=(visit_node, depart_node)) + app.add_node(Header, html=(visit_node, depart_node)) + app.add_node(Button, html=(visit_node, depart_node)) + return { + 'parallel_read_safe': True, + 'parallel_write_safe': True, + }