Merge branch '12.0-one-doc-edi' of github.com:odoo/documentation-user into 12.0-one-doc-edi

This commit is contained in:
Victor Feyens 2021-02-22 16:58:31 +01:00
commit 6091147656
14 changed files with 231 additions and 113 deletions

View File

@ -30,13 +30,16 @@ clean:
$(RM_CMD) extensions/odoo_theme/static/style.css $(RM_CMD) extensions/odoo_theme/static/style.css
@echo "Cleaning finished." @echo "Cleaning finished."
edi: SPHINXOPTS += -A collapse_menu=True #edi: SPHINXOPTS += -A collapse_menu=True # If needed, comment rather than setting False
edi: VERSIONS += 12.0,13.0,14.0 edi: VERSIONS += 12.0,13.0,14.0
edi: CANONICAL_VERSION += 14.0 edi: CANONICAL_VERSION += 14.0
edi: LANGUAGES += en,fr,es edi: LANGUAGES += en,fr,es
edi: CURRENT_LANG += fr edi: CURRENT_LANG += fr
edi: clean html edi: clean html
static: extensions/odoo_theme/static extensions/odoo_theme/static/style.css
cp -r extensions/odoo_theme/static/* _build/html/_static/
html: extensions/odoo_theme/static/style.css html: extensions/odoo_theme/static/style.css
@echo "Starting build..." @echo "Starting build..."
$(SPHINX_BUILD) -c $(CONFIG_DIR) -b html $(SPHINXOPTS) $(SOURCE_DIR) $(BUILD_DIR)/html $(SPHINX_BUILD) -c $(CONFIG_DIR) -b html $(SPHINXOPTS) $(SOURCE_DIR) $(BUILD_DIR)/html

View File

@ -1,3 +1,5 @@
:show_content:
============ ============
Applications Applications
============ ============

View File

@ -1,5 +1,5 @@
:empty_page: :show_content:
================================= =================================
Contributing to the documentation Contributing to the documentation

View File

@ -1,32 +1,27 @@
from . import pygments_override from docutils import nodes
from . import translator
import sphinx.builders.html
from sphinx import addnodes from sphinx import addnodes
from sphinx.environment.adapters import toctree from sphinx.environment.adapters import toctree
from docutils import nodes
from . import pygments_override, translator
def setup(app): def setup(app):
app.set_translator('html', translator.BootstrapTranslator) app.set_translator('html', translator.BootstrapTranslator)
# VFE TODO check if default meta initialization is necessary. app.connect('html-page-context', set_missing_meta)
# If not, remove update_meta method
app.connect('html-page-context', update_meta)
app.add_js_file('js/utils.js') # Keep in first position
app.add_js_file('js/layout.js') app.add_js_file('js/layout.js')
app.add_js_file('js/menu.js') app.add_js_file('js/menu.js')
app.add_js_file('js/page_toc.js') app.add_js_file('js/page_toc.js')
def update_meta(app, pagename, templatename, context, doctree): def set_missing_meta(app, pagename, templatename, context, doctree):
meta = context.get('meta') if context.get('meta') is None: # Pages without title (used with `include::`) have no meta
if meta is None: context['meta'] = {}
meta = context['meta'] = {}
# TODO VFE detailed explanation of the patch logic and use.
class Monkey(object): class Monkey(object):
""" Replace patched method of an object by a new method receiving the old one in argument. """
def __init__(self, obj): def __init__(self, obj):
self.obj = obj self.obj = obj
def __call__(self, fn): def __call__(self, fn):
@ -36,41 +31,67 @@ class Monkey(object):
@Monkey(toctree.TocTree) @Monkey(toctree.TocTree)
def resolve(old_resolve, tree, docname, *args, **kwargs): def resolve(old_resolve, tree, docname, *args, **kwargs):
def _update_toctree_nodes(_node) -> None:
""" Make necessary changes to Docutils' nodes of the toc.
Internal structure of toc nodes:
<ul>
<li>
<p><a/></p>
<ul>
...
</ul>
</li>
<li/>
<ul/>
"""
if isinstance(_node, nodes.reference): # The node is a reference (<a/>)
_node_docname = _get_docname(_node)
_clear_reference_if_empty_page(_node, _node_docname)
_set_docname_as_class(_node, _node_docname)
elif isinstance(_node, (addnodes.compact_paragraph, nodes.bullet_list, nodes.list_item)):
for _subnode in _node.children:
_update_toctree_nodes(_subnode)
def _get_docname(_node):
"""
docname = a/b/c/the_page_being_rendered
_ref = ../../contributing/documentation
_path_parts = ['..', '..', 'contributing', 'documentation']
_res = ['a', 'contributing', 'documentation']
_docname = a/contributing/documentation
"""
_ref = _node['refuri'].replace('.html', '')
_parent_directory_occurrences = _ref.count('..')
if not _parent_directory_occurrences: # The ref is already the docname
_docname = _ref
else:
_path_parts = _ref.split('/')
_res = docname.split('/')[:-(_parent_directory_occurrences+1)] \
+ _path_parts[_parent_directory_occurrences:]
_docname = '/'.join(_res)
return _docname
def _clear_reference_if_empty_page(_reference_node, _node_docname):
""" Clear reference of 'empty' toctree pages.
Inspect parent node's siblings to determine whether the node references a toc and, if so,
clear its reference URL. (<a href="#"/>)
If the page has the `show_content` metadata, don't clear the reference.
"""
if _node_docname and any(
isinstance(_subnode, nodes.bullet_list)
for _subnode in _reference_node.parent.parent.children
): # The node references a toc
if 'show_content' not in tree.env.metadata[_node_docname]:
_reference_node['refuri'] = '#' # The page must not be accessible
def _set_docname_as_class(_reference_node, _node_docname):
_node_docname = _node_docname or docname # refuri==None <-> href="#"
_reference_node.parent.parent['classes'].append(f'o_menu_{_node_docname.replace("/", "_")}')
resolved_toc = old_resolve(tree, docname, *args, **kwargs) resolved_toc = old_resolve(tree, docname, *args, **kwargs)
if resolved_toc: if resolved_toc: # `resolve` returns None if the depth of the TOC to resolve is too high
# Not sure set_class really does what we want. _update_toctree_nodes(resolved_toc)
_toctree_add_empty_class(tree, resolved_toc, docname)
resolved_toc['classes'].append('testtesttest')
return resolved_toc return resolved_toc
def _toctree_add_empty_class(tree, node, docname) -> None:
for subnode in node.children:
if isinstance(subnode, (
addnodes.compact_paragraph,
nodes.list_item,
nodes.bullet_list
)):
# for <p>, <li> and <ul> just recurse
_toctree_add_empty_class(tree, subnode, docname)
elif isinstance(subnode, nodes.reference):
toc_ref = get_reference(subnode, docname)
if toc_ref and 'empty_page' in tree.env.metadata[toc_ref]:
subnode['classes'].append('o_empty_page')
def get_reference(node, docname):
ref = node['refuri'].replace('.html', '') # applications.html
if ref.find('..') < 0:
# direct reference
return ref
splitted_refuri = ref.split('/')
count = 0 # Number of ../ in refuri
for split in splitted_refuri:
if split == "..":
count += 1
# ref = ../../../contributing/documentation
# docname = services/legal/terms/enterprise
# res = contributing/documentation
res = docname.split('/')[:-(count+1)] + splitted_refuri[count:]
return "/".join(
res
)

View File

@ -64,11 +64,11 @@
{% set main_classes = [] %} {% set main_classes = [] %}
{% if pagename == master_doc %} {# The current page is the homepage #} {% if pagename == master_doc %} {# The current page is the homepage #}
{% set main_classes = main_classes + ['index'] %} {% set main_classes = main_classes + ['index'] %} {# TODO ANVFE should be 'o_index' #}
{% endif %} {% endif %}
{% if 'code-column' in meta %} {# The page contains a 'memento' (side dynamic block) #} {% if 'code-column' in meta %} {# The page contains a 'memento' (side dynamic block) #}
{% set main_classes = main_classes + ['has_code_col'] %} {% set main_classes = main_classes + ['has_code_col'] %} {# TODO ANVFE see #}
{% endif %} {% endif %}
{% if 'classes' in meta %} {# The page source defines custom classes #} {% if 'classes' in meta %} {# The page source defines custom classes #}

View File

@ -1,6 +1,5 @@
<div class="o_logo_wrapper"> <div class="o_logo_wrapper">
<a href="{{ pathto(master_doc) }}" class="o_logo"> <a href="{{ pathto(master_doc) }}" class="o_logo">
{# TODO EDI/design make one unique image for Odoo docs s.t. when clicking on docs you are also redirected #}
<img src="{{ pathto('_static/img/logos/odoo_logo.svg', 1) }}" <img src="{{ pathto('_static/img/logos/odoo_logo.svg', 1) }}"
height="30" alt="Odoo"/> height="30" alt="Odoo"/>
<span class="text-dark fw_extralight">docs</span> <span class="text-dark fw_extralight">docs</span>

View File

@ -3,7 +3,7 @@
<button class="btn border dropdown-toggle" id="languages" data-bs-toggle="dropdown"> <button class="btn border dropdown-toggle" id="languages" data-bs-toggle="dropdown">
{{ language }} {# The current language #} {{ language }} {# The current language #}
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="languages"> <ul class="dropdown-menu" aria-labelledby="languages">
{% for alternate_language, language_code, url in alternate_languages %} {% for alternate_language, language_code, url in alternate_languages %}
<li><a class="dropdown-item" href="{{ url }}">{{ alternate_language }}</a></li> <li><a class="dropdown-item" href="{{ url }}">{{ alternate_language }}</a></li>
{% endfor %} {% endfor %}

View File

@ -5,5 +5,8 @@
title_only: Whether menu items for content pages (without toctree) should be hidden title_only: Whether menu items for content pages (without toctree) should be hidden
includehidden: Whether menu items of pages inside a hidden toctree should be rendered includehidden: Whether menu items of pages inside a hidden toctree should be rendered
#} #}
{{ toctree(collapse=collapse_menu, titles_only=True, includehidden=True)}} {#
{# TODO replace `collapse_menu` by `True` and remove Makefile entry 'light' #} `collapse_menu` is passed directly to Jinja with sphinx-build's option `-A collapse_menu=True`.
It it evaluated as a string, so what we're really evaluating here is `collapse_menu != None`.
#}
{{ toctree(collapse=collapse_menu, titles_only=True, includehidden=False)}}

View File

@ -27,6 +27,9 @@ class OdooStyle(Style):
Keyword.Constant: 'bold', Keyword.Constant: 'bold',
Name.Builtin: '#2c2cff', Name.Builtin: '#2c2cff',
Name.Function: 'bold italic', Name.Function: 'bold italic',
Name.Class: "bold #0000FF",
Name.Namespace: "bold #0000FF",
Name.Exception: 'bg:#e3d2d2 #a61717',
Name.Variable: 'bold #2c2cff', Name.Variable: 'bold #2c2cff',
Name.Attribute: '#2c2cff', Name.Attribute: '#2c2cff',
Name.Tag: "bold #008000", Name.Tag: "bold #008000",

View File

@ -1,26 +1,42 @@
(function ($) { (function ($) {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const navigationMenu = document.getElementById('o_main_toctree'); this.navigationMenu = document.getElementById('o_main_toctree');
// Add a class with the name of the file to each corresponding menu item // Allow to automatically collapse and expand TOC entries
_flagMenuItemsWithFileName(navigationMenu); _prepareAccordion(this.navigationMenu);
// Allow to respectively highlight and expand the TOC entries and their related TOC
// entry list whose page is displayed.
_flagActiveTocEntriesAndLists();
}); });
/** /**
* Add the name of the file as class of the corresponding menu item. * Add the relevant classes on the TOC entries (and lists) whose page is displayed.
* *
* @param {HTMLElement} navigationMenu - The navigation menu containing the global TOC * TOC entries (<li> elements) that are on the path of the displayed page receive the
* `o_active_toc_entry` class, and their related (parent) TOC entry list (<ul> elements) receive
* the `show` (Bootstrap) class. The child TOC entry list of the deepest TOC entry also
* receives the `show` class.
*/ */
const _flagMenuItemsWithFileName = (navigationMenu) => { const _flagActiveTocEntriesAndLists = () => {
navigationMenu.querySelectorAll('li').forEach(menuItem => { let deepestTocEntry = undefined;
let href = menuItem.querySelector('a').href; this.navigationMenu.querySelectorAll('.current').forEach(element => {
if (href === '#') { // Selected nodes don't have their file name in the href if (element.tagName === 'UL') {
href = window.location.href; // Get it from the current window location // Expand all related <ul>
element.classList.add('show');
} else if (element.tagName === 'LI') {
// Highlight all <li> in the active hierarchy
element.classList.add('o_active_toc_entry');
deepestTocEntry = element;
} }
const fileName = href.substring(href.lastIndexOf('/') + 1, href.lastIndexOf('.html')); })
menuItem.classList.add(`o_menu_${fileName}`); if (deepestTocEntry) {
}); const childTocEntryList = deepestTocEntry.querySelector('ul');
if (childTocEntryList) {
childTocEntryList.classList.add('show');
}
}
}; };
})(); })();

View File

@ -12,23 +12,28 @@
return; return;
} }
// Allow to hide the TOC entry referring the title (<h1> heading)
_flagFirstHeadingRef();
// Allow to automatically collapse and expand TOC entries // Allow to automatically collapse and expand TOC entries
_prepareAccordion(); _prepareAccordion(this.pageToc);
// Allow to respectively highlight and expand the TOC entries and their related TOC // Allow to respectively highlight and expand the TOC entries and their related TOC
// entry list whose section is focused. // entry list whose section is focused.
_flagActiveTocEntriesAndLists(); _flagActiveTocEntriesAndLists();
// Allow to hide the TOC entry referring the title (<h1> heading)
_flagFirstHeadingRef();
} }
}); });
/**
* Entirely hide the local tree of contents.
*/
const _hidePageToc = () => this.pageToc.style.visibility = 'hidden';
/** /**
* Add the relevant classes on the TOC entries (and lists) whose section is focused. * Add the relevant classes on the TOC entries (and lists) whose section is focused.
* *
* TOC entries whose section is focused (<li> elements) receive the `active` class and their * TOC entries whose section is focused (<li> elements) receive the `o_active_toc_entry` class
* related TOC entry list (<ul> elements) receive the `show` class. * and their related TOC entry list (<ul> elements) receive the `show` (Bootstrap) class.
*/ */
const _flagActiveTocEntriesAndLists = () => { const _flagActiveTocEntriesAndLists = () => {
@ -70,7 +75,7 @@
const _unflagAll = () => { const _unflagAll = () => {
this.pageToc.querySelectorAll('li,ul').forEach(element => { this.pageToc.querySelectorAll('li,ul').forEach(element => {
element.classList.remove('active', 'show'); element.classList.remove('o_active_toc_entry', 'show');
}); });
}; };
@ -78,10 +83,13 @@
let tocEntry = headingRef.parentElement; let tocEntry = headingRef.parentElement;
while (tocEntry !== this.pageToc) { while (tocEntry !== this.pageToc) {
if (tocEntry.tagName === 'LI') { if (tocEntry.tagName === 'LI') {
tocEntry.classList.add('active'); // Highlight all <li> in the active hierarchy // Highlight all <li> in the active hierarchy
tocEntry.classList.add('o_active_toc_entry');
// Expand all related <ul>
const relatedTocEntryList = tocEntry.querySelector('ul'); const relatedTocEntryList = tocEntry.querySelector('ul');
if (relatedTocEntryList) { if (relatedTocEntryList) {
relatedTocEntryList.classList.add('show'); // Expand all related <ul> relatedTocEntryList.classList.add('show');
} }
} }
tocEntry = tocEntry.parentElement; tocEntry = tocEntry.parentElement;
@ -94,10 +102,10 @@
}); });
let timeoutId = undefined; let timeoutId = undefined;
document.addEventListener('scroll', () => { document.addEventListener('scroll', () => {
clearTimeout(timeoutId); // For each scroll event, cancel the previous timeout callback
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
clickedHeadingRef = undefined; // Go back to highlighting the heading ref in view clickedHeadingRef = undefined; // Go back to highlighting the heading ref in view
}, 100); }, 100);
clearTimeout(timeoutId); // For each scroll event, cancel the previous timeout callback
_updateFlags(); _updateFlags();
}); });
@ -108,30 +116,8 @@
/** /**
* Add the class `o_page_toc_title` on the first heading reference. * Add the class `o_page_toc_title` on the first heading reference.
*/ */
const _flagFirstHeadingRef = () => this.headingRefs[0].classList.add('o_page_toc_title'); const _flagFirstHeadingRef = () => {
this.headingRefs[0].parentNode.classList.add('o_page_toc_title');
/** }
* Entirely hide the local tree of contents.
*/
const _hidePageToc = () => this.pageToc.style.visibility = 'hidden';
/**
* Update the page TOC entries and heading references to allow collapsing them.
*/
const _prepareAccordion = () => {
// Start at the second TOC entry list (<ul>) to avoid collapsing the entire TOC
const pageTocRoot = this.pageToc.querySelectorAll('ul')[1];
pageTocRoot.querySelectorAll('ul').forEach(tocEntryList => {
const relatedHeadingRef = tocEntryList.previousSibling; // The preceding <a> element
tocEntryList.id = `o_target_${relatedHeadingRef.getAttribute('href').replace('#', '')}`
tocEntryList.classList.add('collapse');
relatedHeadingRef.setAttribute('data-bs-target', `#${tocEntryList.id}`);
relatedHeadingRef.setAttribute('data-bs-toggle', 'collapse');
});
// TODO [ANV]
// current output: <a class="reference internal collapsed" href="#text" data-bs-target="#o_target_text" data-bs-toggle="collapse" aria-expanded="false">Text</a>
// desired output: <a class="reference internal" href="#text"><i class="i-chevron-right collapsed" data-bs-target="#o_target_text" data-bs-toggle="collapse" aria-expanded="false"></i> Text</a>
};
})(); })();

View File

@ -0,0 +1,64 @@
let tocEntryListId = 0; // Used to generate IDs of toc entry lists for both the menu and page TOC
/**
* Update the provided TOC to allow collapsing its entries with Bootstrap's accordion.
*
* The typical structure of a TOC menu is a follows:
* <ul><li>
* <a href="#"/>
* <ul>
* <li><a href="#heading_without_child"/></li>
* <li>
* <a href="#heading_with_children"/>
* <ul>...</ul>
* </li>
* </ul>
* </li></ul>
*
* Since a <ul> is always preceded by a <a>, and since we only need to make change to <a>
* elements followed by a <ul>, we simply loop on <ul> elements to access all parts of the DOM
* that need to be modified.
*
* The final structure must look like this:
* <ul><li>
* <!-- Only <a> element with empty href must expand/collapse on click -->
* <a href="#" data-bs-target="#o_target_{id}>" data-bs-toggle="collapse"/>
* <ul>
* <li><a href="#heading_without_child"/></li>
* <li>
* <div class="o_toc_entry_wrapper">
* <i class="i-chevron-right" data-bs-target="#o_target_{id}" data-bs-toggle="collapse"/>
* <a href="#heading_with_children"/>
* </div>
* <ul id="o_target_{id}" class="collapse">...</ul>
* </li>
* </ul>
* </li></ul>
*
* @param {HTMLElement} tocElement - The element containing the TOC
*/
const _prepareAccordion = (tocElement) => {
// Start at the second TOC entry list (<ul>) to avoid collapsing the entire TOC
const tocRoot = tocElement.querySelector('ul');
tocRoot.querySelectorAll('ul').forEach(tocEntryList => {
// Modify the <ul> element
tocEntryList.id = `o_target_${tocEntryListId++}`
tocEntryList.classList.add('collapse');
// Create and configure an <i> element
const arrowButton = document.createElement('I');
arrowButton.setAttribute('data-bs-target', `#${tocEntryList.id}`);
arrowButton.setAttribute('data-bs-toggle', 'collapse');
arrowButton.classList.add('i-chevron-right');
// Modify the <a> element (only if it has no href, otherwise let the redirection happen)
const relatedHeadingRef = tocEntryList.previousSibling;
if (relatedHeadingRef.getAttribute('href') === '#') {
relatedHeadingRef.setAttribute('data-bs-target', `#${tocEntryList.id}`);
relatedHeadingRef.setAttribute('data-bs-toggle', 'collapse');
}
// Create a <div> element
const tocEntryWrapper = document.createElement('DIV');
tocEntryWrapper.classList.add('o_toc_entry_wrapper');
// Insert the <i> and <a> elements inside the <div> and prepend the <div> to the <ul>
tocEntryWrapper.append(arrowButton, relatedHeadingRef);
tocEntryList.parentNode.insertBefore(tocEntryWrapper, tocEntryList);
});
};

View File

@ -59,6 +59,7 @@ $display-line-height: 1;
// For each of Bootstrap's buttons, define text, background, and border color. // For each of Bootstrap's buttons, define text, background, and border color.
$btn-font-weight: $fw_semibold; $btn-font-weight: $fw_semibold;
$btn-focus-box-shadow: 0 0 0 transparent;
// Badges // Badges
@ -106,3 +107,6 @@ $list-group-border-color: $card-border-color;
$nav-pills-link-active-color: #fff; $nav-pills-link-active-color: #fff;
$nav-pills-link-active-bg: o-color('o-color-2'); $nav-pills-link-active-bg: o-color('o-color-2');
// Dropdowns
$dropdown-min-width: 4.5rem;

View File

@ -115,25 +115,42 @@ header.o_main_header{
} }
> .toctree-l1 { > .toctree-l1 {
> a:before { &[class^="o_menu_"] > .o_toc_entry_wrapper > i:before {
@include inline-icomoon($i-doc-apps, 0 1rem 0 -30px); @include inline-icomoon($i-doc-apps, 0 1rem 0 -30px);
} }
&.o_menu_applications > a:before{ &.o_menu_applications > .o_toc_entry_wrapper > i:before{
content:'#{$i-doc-apps}'; content:'#{$i-doc-apps}';
} }
&.o_menu_administration > a:before{ &.o_menu_administration > .o_toc_entry_wrapper > i:before{
content:'#{$i-doc-admin}'; content:'#{$i-doc-admin}';
} }
&.o_menu_developer > a:before{ &.o_menu_developer > .o_toc_entry_wrapper > i:before{
content:'#{$i-doc-dev}'; content:'#{$i-doc-dev}';
} }
&.o_menu_services > a:before{ &.o_menu_services > .o_toc_entry_wrapper > i:before{
content:'#{$i-doc-services}'; content:'#{$i-doc-services}';
} }
&.o_menu_contributing > a:before{ &.o_menu_contributing > .o_toc_entry_wrapper > i:before{
content:'#{$i-doc-contribute}'; content:'#{$i-doc-contribute}';
} }
.o_toc_entry_wrapper {
display: flex;
align-items: center;
}
}
> .toctree-l2 {
.o_toc_entry_wrapper {
> i[class^="i-"] {
transition: rotate .3s;
&[aria-expanded="true"]{
transform: rotate(90deg);
}
}
}
} }
} }
@ -620,7 +637,7 @@ header.o_main_header{
} }
} }
} }
.active > a{ .o_active_toc_entry > a {
font-weight: $fw_bold; font-weight: $fw_bold;
} }
} }