
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#2876
X-original-commit: 4d7a88639a
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Morgan Meganck <morm@odoo.com>
Co-authored-by: Stefano Rigano <sri@odoo.com>
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
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'</{node.custom_tag_name}>')
|
|
|
|
|
|
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,
|
|
}
|