[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: 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>
This commit is contained in:
parent
ece2d4c081
commit
a154e01cee
3
conf.py
3
conf.py
@ -145,6 +145,9 @@ extensions = [
|
||||
# Content tabs
|
||||
'sphinx_tabs.tabs',
|
||||
|
||||
# Spoilers
|
||||
'spoilers',
|
||||
|
||||
# Strange html domain logic used in memento pages
|
||||
'html_domain',
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
//------------------------------------------------------------------------------
|
||||
|
105
extensions/spoilers/__init__.py
Normal file
105
extensions/spoilers/__init__.py
Normal file
@ -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'</{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,
|
||||
}
|
Loading…
Reference in New Issue
Block a user