[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#2837 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
1691e8970b
commit
4d7a88639a
3
conf.py
3
conf.py
@ -124,6 +124,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;
|
||||
|
||||
|
@ -1087,6 +1087,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