[MERGE] website, *: improve customize dialog + add logo sizing
* portal, web, web_editor See sub-commits for details. - Many improvements to the customize dialog 'framework' (toggle switches, new widgets, ...) - Optimization of RPCs (including ace editor which shares the same code) - Move navbar options of the customize menu to the customize dialog - Add the possibility to control the header logo size - Add the possibility to control the header font size task-1904244 closes odoo/odoo#29624
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
///
|
||||
|
||||
// ====== Variables =========
|
||||
$-navbar-height: $nav-link-height !default;
|
||||
$o-theme-navbar-logo-height: $nav-link-height !default;
|
||||
|
||||
// Portal toolbar (filters, search bar)
|
||||
$o-portal-mobile-toolbar: true; // Enable/Disable custom design
|
||||
@@ -42,15 +42,23 @@ $o-portal-use-default-colors: $body-bg == $o-portal-default-body-bg;
|
||||
header {
|
||||
.navbar-brand {
|
||||
flex: 0 0 auto;
|
||||
max-width: 75%;
|
||||
|
||||
&.logo {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
img {
|
||||
// object-fit does not work on IE but is only used as a fallback
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
width: auto;
|
||||
height: $-navbar-height;
|
||||
max-width: none;
|
||||
height: $o-theme-navbar-logo-height;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
height: auto;
|
||||
max-height: min($o-theme-navbar-logo-height, 5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +72,6 @@ header {
|
||||
ul.nav > li {
|
||||
&.divider {
|
||||
display: none;
|
||||
height: $-navbar-height;
|
||||
border-right: 1px solid $nav-divider-color;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
$o-portal-default-body-bg: #FCFCFC;
|
||||
|
||||
$o-theme-navbar-logo-height: null;
|
||||
|
||||
$o-theme-btn-icon-hover-decoration: none;
|
||||
|
||||
@@ -177,11 +177,11 @@
|
||||
// Mixin which allows to extend the BS4 bg-variant mixin
|
||||
@mixin o-bg-color-extension($color, $text-color, $with-muted) {}
|
||||
|
||||
// Function to remove all falsy values of a map
|
||||
// Function to remove all null values of a map
|
||||
@function o-map-omit($map) {
|
||||
$-map: ();
|
||||
@each $key, $value in $map {
|
||||
@if $value {
|
||||
@if $value != null {
|
||||
$-map: map-merge($-map, (
|
||||
$key: $value,
|
||||
));
|
||||
|
||||
@@ -4,20 +4,18 @@ import base64
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
import werkzeug.wrappers
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
from lxml import etree, html
|
||||
from lxml import etree
|
||||
|
||||
from odoo.http import request
|
||||
from odoo import http, tools
|
||||
from odoo.modules.module import get_resource_path, get_module_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Web_Editor(http.Controller):
|
||||
#------------------------------------------------------
|
||||
# convert font into picture
|
||||
@@ -296,40 +294,57 @@ class Web_Editor(http.Controller):
|
||||
result['originalSrc'] = record.url
|
||||
return result
|
||||
|
||||
def _get_view_fields_to_read(self):
|
||||
return ['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id']
|
||||
|
||||
## The get_assets_editor_resources route is in charge of transmitting the resources the assets
|
||||
## editor needs to work.
|
||||
## @param key - the xml_id or id of the view the resources are related to
|
||||
## @param get_views - True if the views must be fetched (default to True)
|
||||
## @param get_scss - True if the style must be fetched (default to True)
|
||||
## @param get_js - True if the javascript must be fetched (default to True)
|
||||
## @param bundles - True if the bundles views must be fetched (default to False)
|
||||
## @param bundles_restriction - Names of the bundle in which to look for scss files (if empty, search in all of them)
|
||||
## @param only_user_custom_files - True if only user custom files are needed (default to True)
|
||||
## @returns a dictionary with views info in the views key and style info in the scss key
|
||||
@http.route("/web_editor/get_assets_editor_resources", type="json", auth="user", website=True)
|
||||
def get_assets_editor_resources(self, key, get_views=True, get_scss=True, get_js=True, bundles=False, bundles_restriction=[], only_user_custom_files=True):
|
||||
"""
|
||||
Transmit the resources the assets editor needs to work.
|
||||
|
||||
Params:
|
||||
key (str): the key of the view the resources are related to
|
||||
|
||||
get_views (bool, default=True):
|
||||
True if the views must be fetched
|
||||
|
||||
get_scss (bool, default=True):
|
||||
True if the style must be fetched
|
||||
|
||||
get_js (bool, default=True):
|
||||
True if the javascript must be fetched
|
||||
|
||||
bundles (bool, default=False):
|
||||
True if the bundles views must be fetched
|
||||
|
||||
bundles_restriction (list, default=[]):
|
||||
Names of the bundles in which to look for scss files
|
||||
(if empty, search in all of them)
|
||||
|
||||
only_user_custom_files (bool, default=True):
|
||||
True if only user custom files must be fetched
|
||||
|
||||
Returns:
|
||||
dict: views, scss, js
|
||||
"""
|
||||
# Related views must be fetched if the user wants the views and/or the style
|
||||
views = request.env["ir.ui.view"].get_related_views(key, bundles=bundles)
|
||||
views = views.read(self._get_view_fields_to_read())
|
||||
views = views.read(request.env['web_editor.assets'].get_view_fields_to_read())
|
||||
|
||||
js_files_data_by_bundle = []
|
||||
scss_files_data_by_bundle = []
|
||||
js_files_data_by_bundle = []
|
||||
|
||||
if get_js:
|
||||
js_files_data_by_bundle = self.load_resources('js', views, bundles_restriction, only_user_custom_files)
|
||||
if get_scss:
|
||||
scss_files_data_by_bundle = self.load_resources('scss', views, bundles_restriction, only_user_custom_files)
|
||||
scss_files_data_by_bundle = self._load_resources('scss', views, bundles_restriction, only_user_custom_files)
|
||||
if get_js:
|
||||
js_files_data_by_bundle = self._load_resources('js', views, bundles_restriction, only_user_custom_files)
|
||||
|
||||
return dict(
|
||||
views=get_views and views or [],
|
||||
scss=get_scss and scss_files_data_by_bundle or [],
|
||||
js=get_js and js_files_data_by_bundle or [],
|
||||
)
|
||||
return {
|
||||
'views': get_views and views or [],
|
||||
'scss': get_scss and scss_files_data_by_bundle or [],
|
||||
'js': get_js and js_files_data_by_bundle or [],
|
||||
}
|
||||
|
||||
def _load_resources(self, file_type, views, bundles_restriction, only_user_custom_files):
|
||||
AssetsUtils = request.env['web_editor.assets']
|
||||
|
||||
def load_resources(self, file_type, views, bundles_restriction, only_user_custom_files):
|
||||
files_data_by_bundle = []
|
||||
resources_type_info = {'t_call_assets_attribute': 't-js', 'mimetype': 'text/javascript'}
|
||||
if file_type == 'scss':
|
||||
@@ -339,10 +354,6 @@ class Web_Editor(http.Controller):
|
||||
# This will used to exclude library scss files from the result
|
||||
excluded_url_matcher = re.compile("^(.+/lib/.+)|(.+import_bootstrap.+\.scss)$")
|
||||
|
||||
# Load already customized files attachments
|
||||
custom_url = self._make_custom_scss_or_js_file_url("%%.%%", "%%")
|
||||
custom_attachments = self.get_custom_attachment(custom_url, op='=like')
|
||||
|
||||
# First check the t-call-assets used in the related views
|
||||
url_infos = dict()
|
||||
for v in views:
|
||||
@@ -363,7 +374,7 @@ class Web_Editor(http.Controller):
|
||||
continue
|
||||
|
||||
# Check if the file is customized and get bundle/path info
|
||||
file_data = self._match_scss_or_js_file_url(url)
|
||||
file_data = AssetsUtils.get_asset_info(url)
|
||||
if not file_data:
|
||||
continue
|
||||
|
||||
@@ -375,13 +386,19 @@ class Web_Editor(http.Controller):
|
||||
or file_type == 'scss' and not only_user_custom_files:
|
||||
files_data.append(url)
|
||||
|
||||
# File data is returned sorted by bundle, with the bundles names and xmlids
|
||||
# scss data is returned sorted by bundle, with the bundles
|
||||
# names and xmlids
|
||||
if len(files_data):
|
||||
files_data_by_bundle.append([dict(xmlid=asset_name, name=request.env.ref(asset_name).name), files_data])
|
||||
files_data_by_bundle.append([
|
||||
{'xmlid': asset_name, 'name': request.env.ref(asset_name).name},
|
||||
files_data
|
||||
])
|
||||
|
||||
# Filter bundles/files:
|
||||
# - A file which appears in multiple bundles only appears in the first one (the first in the DOM)
|
||||
# - Only keep bundles with files which appears in the asked bundles and only keep those files
|
||||
# - A file which appears in multiple bundles only appears in the
|
||||
# first one (the first in the DOM)
|
||||
# - Only keep bundles with files which appears in the asked bundles
|
||||
# and only keep those files
|
||||
for i in range(0, len(files_data_by_bundle)):
|
||||
bundle_1 = files_data_by_bundle[i]
|
||||
for j in range(0, len(files_data_by_bundle)):
|
||||
@@ -393,7 +410,8 @@ class Web_Editor(http.Controller):
|
||||
bundle_1 = files_data_by_bundle[i]
|
||||
for j in range(i + 1, len(files_data_by_bundle)):
|
||||
bundle_2 = files_data_by_bundle[j]
|
||||
# In every bundle, keep only the files which were not found in previous bundles
|
||||
# In every bundle, keep only the files which were not found
|
||||
# in previous bundles
|
||||
bundle_2[1] = [item_2 for item_2 in bundle_2[1] if item_2 not in bundle_1[1]]
|
||||
|
||||
# Only keep bundles which still have files and that were requested
|
||||
@@ -403,146 +421,57 @@ class Web_Editor(http.Controller):
|
||||
]
|
||||
|
||||
# Fetch the arch of each kept file, in each bundle
|
||||
urls = []
|
||||
for bundle_data in files_data_by_bundle:
|
||||
urls += bundle_data[1]
|
||||
custom_attachments = AssetsUtils.get_all_custom_attachments(urls)
|
||||
|
||||
for bundle_data in files_data_by_bundle:
|
||||
for i in range(0, len(bundle_data[1])):
|
||||
url = bundle_data[1][i]
|
||||
url_info = url_infos[url]
|
||||
|
||||
content = None
|
||||
if url_info["customized"]:
|
||||
# If the file is already customized, the content is found in the corresponding attachment
|
||||
content = base64.b64decode(custom_attachments.filtered(lambda a: a.url == url).datas)
|
||||
else:
|
||||
# If the file is not yet customized, the content is found by reading the local scss file
|
||||
module = url_info["module"]
|
||||
module_path = get_module_path(module)
|
||||
module_resource_path = get_resource_path(module, url_info["resource_path"])
|
||||
if module_path and module_resource_path:
|
||||
module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/'
|
||||
module_resource_path = os.path.normpath(module_resource_path)
|
||||
if module_resource_path.startswith(module_path):
|
||||
with open(module_resource_path, "rb") as f:
|
||||
content = f.read()
|
||||
content = AssetsUtils.get_asset_content(url, url_info, custom_attachments)
|
||||
|
||||
bundle_data[1][i] = {
|
||||
'url': "/%s/%s" % (url_info["module"], url_info["resource_path"]),
|
||||
'arch': content,
|
||||
'customized': url_info["customized"],
|
||||
}
|
||||
|
||||
bundle_data[1][i] = dict(
|
||||
url="/%s/%s" % (url_info["module"], url_info["resource_path"]),
|
||||
arch=content,
|
||||
customized=url_info["customized"],
|
||||
)
|
||||
return files_data_by_bundle
|
||||
|
||||
def save_scss_or_js_view_hook(self):
|
||||
return {}
|
||||
@http.route("/web_editor/save_asset", type="json", auth="user", website=True)
|
||||
def save_asset(self, url, bundle_xmlid, content, file_type):
|
||||
"""
|
||||
Save a given modification of a scss/js file.
|
||||
|
||||
def save_scss_or_js_attachment_hook(self):
|
||||
return {}
|
||||
Params:
|
||||
url (str):
|
||||
the original url of the scss/js file which has to be modified
|
||||
|
||||
## The save_scss_or_js route is in charge of saving a given modification of a scss/js file.
|
||||
## @param url - the original url of the scss/js file which has to be modified
|
||||
## @param bundle_xmlid - the xmlid of the bundle in which the scss/js file addition can be found
|
||||
## @param content - the new content of the scss/js file
|
||||
## @param file_type - scss or js
|
||||
@http.route("/web_editor/save_scss_or_js", type="json", auth="user", website=True)
|
||||
def save_scss_or_js(self, url, bundle_xmlid, content, file_type):
|
||||
IrAttachment = request.env["ir.attachment"]
|
||||
bundle_xmlid (str):
|
||||
the xmlid of the bundle in which the scss/js file addition can
|
||||
be found
|
||||
|
||||
custom_url = self._make_custom_scss_or_js_file_url(url, bundle_xmlid)
|
||||
content (str): the new content of the scss/js file
|
||||
|
||||
# Check if the file to save had already been modified
|
||||
custom_attachment = self.get_custom_attachment(custom_url)
|
||||
datas = base64.b64encode((content or "\n").encode("utf-8"))
|
||||
if custom_attachment:
|
||||
# If it was already modified, simply override the corresponding attachment content
|
||||
custom_attachment.write({"datas": datas})
|
||||
else:
|
||||
# If not, create a new attachment to copy the original scss/js file content, with its modifications
|
||||
new_attach = {
|
||||
'name': custom_url,
|
||||
'type': "binary",
|
||||
'mimetype': (file_type == 'js' and 'text/javascript' or 'text/scss'),
|
||||
'datas': datas,
|
||||
'datas_fname': url.split("/")[-1],
|
||||
'url': custom_url,
|
||||
}
|
||||
new_attach.update(self.save_scss_or_js_attachment_hook())
|
||||
IrAttachment.create(new_attach)
|
||||
file_type (str): 'scss' or 'js'
|
||||
"""
|
||||
request.env['web_editor.assets'].save_asset(url, bundle_xmlid, content, file_type)
|
||||
|
||||
# Create a view to extend the template which adds the original file to link the new modified version instead
|
||||
IrUiView = request.env["ir.ui.view"]
|
||||
file_type_info = {
|
||||
'tag': 'link' if file_type == 'scss' else 'script',
|
||||
'attribute': 'href' if file_type == 'scss' else 'src',
|
||||
}
|
||||
@http.route("/web_editor/reset_asset", type="json", auth="user", website=True)
|
||||
def reset_asset(self, url, bundle_xmlid):
|
||||
"""
|
||||
The reset_asset route is in charge of reverting all the changes that
|
||||
were done to a scss/js file.
|
||||
|
||||
def views_linking_url(view):
|
||||
"""
|
||||
Returns whether the view arch has some html link tag linked to the url.
|
||||
Params:
|
||||
url (str):
|
||||
the original URL of the scss/js file to reset
|
||||
|
||||
(note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.)
|
||||
"""
|
||||
tree = etree.XML(view.arch)
|
||||
return bool(tree.xpath("//%%(tag)s[@%%(attribute)s='%(url)s']" % {
|
||||
'url': url,
|
||||
} % file_type_info))
|
||||
|
||||
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url)
|
||||
|
||||
new_view = {
|
||||
'name': custom_url,
|
||||
'key': 'web_editor.%s_%s' % (file_type, str(uuid.uuid4())[:6]),
|
||||
'mode': "extension",
|
||||
'inherit_id': view_to_xpath.id,
|
||||
'arch': """
|
||||
<data inherit_id="%(inherit_xml_id)s" name="%(name)s">
|
||||
<xpath expr="//%%(tag)s[@%%(attribute)s='%(url_to_replace)s']" position="attributes">
|
||||
<attribute name="%%(attribute)s">%(new_url)s</attribute>
|
||||
</xpath>
|
||||
</data>
|
||||
""" % {
|
||||
'inherit_xml_id': view_to_xpath.xml_id,
|
||||
'name': custom_url,
|
||||
'url_to_replace': url,
|
||||
'new_url': custom_url,
|
||||
} % file_type_info
|
||||
}
|
||||
new_view.update(self.save_scss_or_js_view_hook())
|
||||
IrUiView.create(new_view)
|
||||
|
||||
request.env["ir.qweb"].clear_caches()
|
||||
|
||||
## The reset_scss_or_js route is in charge of reverting all the changes that were done to a scss/js file.
|
||||
## @param url - the original URL of the scss file to reset
|
||||
## @param bundle_xmlid - the xmlid of the bundle in which the scss file addition can be found
|
||||
@http.route("/web_editor/reset_scss_or_js", type="json", auth="user", website=True)
|
||||
def reset_scss_or_js(self, url, bundle_xmlid):
|
||||
custom_url = self._make_custom_scss_or_js_file_url(url, bundle_xmlid)
|
||||
|
||||
# Simply delete the attachement which contains the modified scss/js file and the xpath view which links it
|
||||
self.get_custom_attachment(custom_url).unlink()
|
||||
self.get_custom_view(custom_url).unlink()
|
||||
|
||||
def get_custom_attachment(self, custom_url, op='='):
|
||||
assert op in ('=like', '='), 'Invalid operator'
|
||||
IrAttachment = request.env["ir.attachment"]
|
||||
return IrAttachment.search([("url", op, custom_url)])
|
||||
|
||||
def get_custom_view(self, custom_url, op='='):
|
||||
assert op in ('=like', '='), 'Invalid operator'
|
||||
IrUiView = request.env["ir.ui.view"]
|
||||
return IrUiView.search([("name", op, custom_url)])
|
||||
|
||||
def _make_custom_scss_or_js_file_url(self, url, bundle):
|
||||
parts = url.rsplit(".", 1)
|
||||
return "%s.custom.%s.%s" % (parts[0], bundle, parts[1])
|
||||
|
||||
_match_file_url_regex = re.compile("^/(\w+)/(.+?)(\.custom\.(.+))?\.(\w+)$")
|
||||
def _match_scss_or_js_file_url(self, url):
|
||||
m = self._match_file_url_regex.match(url)
|
||||
if not m:
|
||||
return False
|
||||
return {
|
||||
'module': m.group(1),
|
||||
'resource_path': "%s.%s" % (m.group(2), m.group(5)),
|
||||
'customized': bool(m.group(3)),
|
||||
'bundle': m.group(4) or False
|
||||
}
|
||||
bundle_xmlid (str):
|
||||
the xmlid of the bundle in which the scss/js file addition can
|
||||
be found
|
||||
"""
|
||||
request.env['web_editor.assets'].reset_asset(url, bundle_xmlid)
|
||||
|
||||
@@ -7,4 +7,6 @@ from . import ir_ui_view
|
||||
from . import ir_http
|
||||
from . import ir_translation
|
||||
|
||||
from . import assets
|
||||
|
||||
from . import test_models
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo import models
|
||||
from odoo.modules.module import get_resource_path, get_module_path
|
||||
|
||||
_match_asset_file_url_regex = re.compile("^/(\w+)/(.+?)(\.custom\.(.+))?\.(\w+)$")
|
||||
|
||||
|
||||
class Assets(models.AbstractModel):
|
||||
_name = 'web_editor.assets'
|
||||
_description = 'Assets Utils'
|
||||
|
||||
def get_view_fields_to_read(self):
|
||||
return ['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id']
|
||||
|
||||
def get_all_custom_attachments(self, urls):
|
||||
"""
|
||||
Fetch all the ir.attachment records related to given URLs.
|
||||
|
||||
Params:
|
||||
urls (str[]): list of urls
|
||||
|
||||
Returns:
|
||||
ir.attachment(): attachment records related to the given URLs.
|
||||
"""
|
||||
return self._get_custom_attachment(urls, op='in')
|
||||
|
||||
def get_asset_content(self, url, url_info=None, custom_attachments=None):
|
||||
"""
|
||||
Fetch the content of an asset (scss / js) file. That content is either
|
||||
the one of the related file on the disk or the one of the corresponding
|
||||
custom ir.attachment record.
|
||||
|
||||
Params:
|
||||
url (str): the URL of the asset (scss / js) file/ir.attachment
|
||||
|
||||
url_info (dict, optional):
|
||||
the related url info (see get_asset_info) (allows to optimize
|
||||
some code which already have the info and do not want this
|
||||
function to re-get it)
|
||||
|
||||
custom_attachments (ir.attachment(), optional):
|
||||
the related custom ir.attachment records the function might need
|
||||
to search into (allows to optimize some code which already have
|
||||
that info and do not want this function to re-get it)
|
||||
|
||||
Returns:
|
||||
utf-8 encoded content of the asset (scss / js)
|
||||
"""
|
||||
if url_info is None:
|
||||
url_info = self.get_asset_info(url)
|
||||
|
||||
if url_info["customized"]:
|
||||
# If the file is already customized, the content is found in the
|
||||
# corresponding attachment
|
||||
attachment = None
|
||||
if custom_attachments is None:
|
||||
attachment = self._get_custom_attachment(url)
|
||||
else:
|
||||
attachment = custom_attachments.filtered(lambda r: r.url == url)
|
||||
return attachment and base64.b64decode(attachment.datas) or False
|
||||
|
||||
# If the file is not yet customized, the content is found by reading
|
||||
# the local scss file
|
||||
module = url_info["module"]
|
||||
module_path = get_module_path(module)
|
||||
module_resource_path = get_resource_path(module, url_info["resource_path"])
|
||||
if module_path and module_resource_path:
|
||||
module_path = os.path.join(os.path.normpath(module_path), '') # join ensures the path ends with '/'
|
||||
module_resource_path = os.path.normpath(module_resource_path)
|
||||
if module_resource_path.startswith(module_path):
|
||||
with open(module_resource_path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
def get_asset_info(self, url):
|
||||
"""
|
||||
Return information about an asset (scss / js) file/ir.attachment just by
|
||||
looking at its URL.
|
||||
|
||||
Params:
|
||||
url (str): the url of the asset (scss / js) file/ir.attachment
|
||||
|
||||
Returns:
|
||||
dict:
|
||||
module (str): the original asset's related app
|
||||
|
||||
resource_path (str):
|
||||
the relative path to the original asset from the related app
|
||||
|
||||
customized (bool): whether the asset is a customized one or not
|
||||
|
||||
bundle (str):
|
||||
the name of the bundle the asset customizes (False if this
|
||||
is not a customized asset)
|
||||
"""
|
||||
m = _match_asset_file_url_regex.match(url)
|
||||
if not m:
|
||||
return False
|
||||
return {
|
||||
'module': m.group(1),
|
||||
'resource_path': "%s.%s" % (m.group(2), m.group(5)),
|
||||
'customized': bool(m.group(3)),
|
||||
'bundle': m.group(4) or False
|
||||
}
|
||||
|
||||
def make_custom_asset_file_url(self, url, bundle_xmlid):
|
||||
"""
|
||||
Return the customized version of an asset URL, that is the URL the asset
|
||||
would have if it was customized.
|
||||
|
||||
Params:
|
||||
url (str): the original asset's url
|
||||
bundle_xmlid (str): the name of the bundle the asset would customize
|
||||
|
||||
Returns:
|
||||
str: the URL the given asset would have if it was customized in the
|
||||
given bundle
|
||||
"""
|
||||
parts = url.rsplit(".", 1)
|
||||
return "%s.custom.%s.%s" % (parts[0], bundle_xmlid, parts[1])
|
||||
|
||||
def reset_asset(self, url, bundle_xmlid):
|
||||
"""
|
||||
Delete the potential customizations made to a given (original) asset.
|
||||
|
||||
Params:
|
||||
url (str): the URL of the original asset (scss / js) file
|
||||
|
||||
bundle_xmlid (str):
|
||||
the name of the bundle in which the customizations to delete
|
||||
were made
|
||||
"""
|
||||
custom_url = self.make_custom_asset_file_url(url, bundle_xmlid)
|
||||
|
||||
# Simply delete the attachement which contains the modified scss/js file
|
||||
# and the xpath view which links it
|
||||
self._get_custom_attachment(custom_url).unlink()
|
||||
self._get_custom_view(custom_url).unlink()
|
||||
|
||||
def save_asset(self, url, bundle_xmlid, content, file_type):
|
||||
"""
|
||||
Customize the content of a given asset (scss / js).
|
||||
|
||||
Params:
|
||||
url (src):
|
||||
the URL of the original asset to customize (whether or not the
|
||||
asset was already customized)
|
||||
|
||||
bundle_xmlid (src):
|
||||
the name of the bundle in which the customizations will take
|
||||
effect
|
||||
|
||||
content (src): the new content of the asset (scss / js)
|
||||
|
||||
file_type (src):
|
||||
either 'scss' or 'js' according to the file being customized
|
||||
"""
|
||||
custom_url = self.make_custom_asset_file_url(url, bundle_xmlid)
|
||||
datas = base64.b64encode((content or "\n").encode("utf-8"))
|
||||
|
||||
# Check if the file to save had already been modified
|
||||
custom_attachment = self._get_custom_attachment(custom_url)
|
||||
if custom_attachment:
|
||||
# If it was already modified, simply override the corresponding
|
||||
# attachment content
|
||||
custom_attachment.write({"datas": datas})
|
||||
else:
|
||||
# If not, create a new attachment to copy the original scss/js file
|
||||
# content, with its modifications
|
||||
new_attach = {
|
||||
'name': custom_url,
|
||||
'type': "binary",
|
||||
'mimetype': (file_type == 'js' and 'text/javascript' or 'text/scss'),
|
||||
'datas': datas,
|
||||
'datas_fname': url.split("/")[-1],
|
||||
'url': custom_url,
|
||||
}
|
||||
new_attach.update(self._save_asset_attachment_hook())
|
||||
self.env["ir.attachment"].create(new_attach)
|
||||
|
||||
# Create a view to extend the template which adds the original file
|
||||
# to link the new modified version instead
|
||||
file_type_info = {
|
||||
'tag': 'link' if file_type == 'scss' else 'script',
|
||||
'attribute': 'href' if file_type == 'scss' else 'src',
|
||||
}
|
||||
|
||||
def views_linking_url(view):
|
||||
"""
|
||||
Returns whether the view arch has some html tag linked to
|
||||
the url. (note: searching for the URL string is not enough as it
|
||||
could appear in a comment or an xpath expression.)
|
||||
"""
|
||||
tree = etree.XML(view.arch)
|
||||
return bool(tree.xpath("//%%(tag)s[@%%(attribute)s='%(url)s']" % {
|
||||
'url': url,
|
||||
} % file_type_info))
|
||||
|
||||
IrUiView = self.env["ir.ui.view"]
|
||||
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url)
|
||||
new_view = {
|
||||
'name': custom_url,
|
||||
'key': 'web_editor.%s_%s' % (file_type, str(uuid.uuid4())[:6]),
|
||||
'mode': "extension",
|
||||
'inherit_id': view_to_xpath.id,
|
||||
'arch': """
|
||||
<data inherit_id="%(inherit_xml_id)s" name="%(name)s">
|
||||
<xpath expr="//%%(tag)s[@%%(attribute)s='%(url_to_replace)s']" position="attributes">
|
||||
<attribute name="%%(attribute)s">%(new_url)s</attribute>
|
||||
</xpath>
|
||||
</data>
|
||||
""" % {
|
||||
'inherit_xml_id': view_to_xpath.xml_id,
|
||||
'name': custom_url,
|
||||
'url_to_replace': url,
|
||||
'new_url': custom_url,
|
||||
} % file_type_info
|
||||
}
|
||||
new_view.update(self._save_asset_view_hook())
|
||||
IrUiView.create(new_view)
|
||||
|
||||
self.env["ir.qweb"].clear_caches()
|
||||
|
||||
def _get_custom_attachment(self, custom_url, op='='):
|
||||
"""
|
||||
Fetch the ir.attachment record related to the given customized asset.
|
||||
|
||||
Params:
|
||||
custom_url (str): the URL of the customized asset
|
||||
op (str, default: '='): the operator to use to search the records
|
||||
|
||||
Returns:
|
||||
ir.attachment()
|
||||
"""
|
||||
assert op in ('in', '='), 'Invalid operator'
|
||||
return self.env["ir.attachment"].search([("url", op, custom_url)])
|
||||
|
||||
def _get_custom_view(self, custom_url, op='='):
|
||||
"""
|
||||
Fetch the ir.ui.view record related to the given customized asset (the
|
||||
inheriting view which replace the original asset by the customized one).
|
||||
|
||||
Params:
|
||||
custom_url (str): the URL of the customized asset
|
||||
op (str, default: '='): the operator to use to search the records
|
||||
|
||||
Returns:
|
||||
ir.ui.view()
|
||||
"""
|
||||
assert op in ('='), 'Invalid operator'
|
||||
return self.env["ir.ui.view"].search([("name", op, custom_url)])
|
||||
|
||||
def _save_asset_attachment_hook(self):
|
||||
"""
|
||||
Returns the additional values to use to write the DB on customized
|
||||
attachment creation.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
return {}
|
||||
|
||||
def _save_asset_view_hook(self):
|
||||
"""
|
||||
Returns the additional values to use to write the DB on customized
|
||||
asset's related view creation.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
return {}
|
||||
@@ -535,7 +535,7 @@ var ViewEditor = Widget.extend({
|
||||
} else {
|
||||
var resource = type === 'scss' ? this.scss[resID] : this.js[resID];
|
||||
return this._rpc({
|
||||
route: '/web_editor/reset_scss_or_js',
|
||||
route: '/web_editor/reset_asset',
|
||||
params: {
|
||||
url: resID,
|
||||
bundle_xmlid: resource.bundle_xmlid,
|
||||
@@ -560,7 +560,7 @@ var ViewEditor = Widget.extend({
|
||||
var bundleXmlID = sessionIdEndsWithJS ? this.js[session.id].bundle_xmlid : this.scss[session.id].bundle_xmlid;
|
||||
var fileType = sessionIdEndsWithJS ? 'js' : 'scss';
|
||||
self._rpc({
|
||||
route: '/web_editor/save_scss_or_js',
|
||||
route: '/web_editor/save_asset',
|
||||
params: {
|
||||
url: session.id,
|
||||
bundle_xmlid: bundleXmlID,
|
||||
|
||||
@@ -3,4 +3,3 @@
|
||||
|
||||
from . import backend
|
||||
from . import main
|
||||
from . import web_editor
|
||||
|
||||
@@ -4,6 +4,7 @@ import base64
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import requests
|
||||
import werkzeug.utils
|
||||
@@ -331,45 +332,27 @@ class Website(Home):
|
||||
# Themes
|
||||
# ------------------------------------------------------
|
||||
|
||||
def get_view_ids(self, xml_ids):
|
||||
ids = []
|
||||
def _get_customize_views(self, xml_ids):
|
||||
View = request.env["ir.ui.view"].with_context(active_test=False)
|
||||
for xml_id in xml_ids:
|
||||
if "." in xml_id:
|
||||
# Get website-specific view if possible
|
||||
record_id = View.search([
|
||||
("website_id", "=", request.website.id),
|
||||
("key", "=", xml_id),
|
||||
], limit=1).id or request.env.ref(xml_id).id
|
||||
else:
|
||||
record_id = int(xml_id)
|
||||
ids.append(record_id)
|
||||
return ids
|
||||
if not xml_ids:
|
||||
return View
|
||||
domain = [("key", "in", xml_ids)] + request.website.website_domain()
|
||||
return View.search(domain).filter_duplicate()
|
||||
|
||||
@http.route(['/website/theme_customize_get'], type='json', auth="public", website=True)
|
||||
def theme_customize_get(self, xml_ids):
|
||||
enable = []
|
||||
disable = []
|
||||
ids = self.get_view_ids(xml_ids)
|
||||
for view in request.env['ir.ui.view'].browse(ids):
|
||||
if view.active:
|
||||
enable.append(view.key)
|
||||
else:
|
||||
disable.append(view.key)
|
||||
return [enable, disable]
|
||||
views = self._get_customize_views(xml_ids)
|
||||
return {
|
||||
'enabled': views.filtered('active').mapped('key'),
|
||||
'names': {view.key: view.name for view in views},
|
||||
}
|
||||
|
||||
@http.route(['/website/theme_customize'], type='json', auth="public", website=True)
|
||||
def theme_customize(self, enable=None, disable=None, get_bundle=False):
|
||||
""" enable or Disable lists of ``xml_id`` of the inherit templates """
|
||||
def set_active(xml_ids, active):
|
||||
if xml_ids:
|
||||
real_ids = self.get_view_ids(xml_ids)
|
||||
request.env['ir.ui.view'].browse(real_ids).write({'active': active})
|
||||
|
||||
if disable:
|
||||
set_active(disable, False)
|
||||
if enable:
|
||||
set_active(enable, True)
|
||||
self._get_customize_views(disable).write({'active': False})
|
||||
self._get_customize_views(enable).write({'active': True})
|
||||
|
||||
if get_bundle:
|
||||
context = dict(request.context)
|
||||
@@ -386,6 +369,42 @@ class Website(Home):
|
||||
self.theme_customize(enable and enable.split(",") or [], disable and disable.split(",") or [])
|
||||
return request.redirect(href + ("&theme=true" if "#" in href else "#theme=true") + ("&tab=" + tab))
|
||||
|
||||
@http.route(['/website/make_scss_custo'], type='json', auth='user', website=True)
|
||||
def make_scss_custo(self, url, values):
|
||||
"""
|
||||
Makes a scss customization of the given file. That file must
|
||||
contain a scss map including a line comment containing the word 'hook',
|
||||
to indicate the location where to write the new key,value pairs.
|
||||
|
||||
Params:
|
||||
url (str):
|
||||
the URL of the scss file to customize (supposed to be a variable
|
||||
file which will appear in the assets_common bundle)
|
||||
|
||||
values (dict):
|
||||
key,value mapping to integrate in the file's map (containing the
|
||||
word hook). If a key is already in the file's map, its value is
|
||||
overridden.
|
||||
"""
|
||||
AssetsUtils = request.env['web_editor.assets']
|
||||
|
||||
custom_url = AssetsUtils.make_custom_asset_file_url(url, 'web.assets_common')
|
||||
updatedFileContent = AssetsUtils.get_asset_content(custom_url) or AssetsUtils.get_asset_content(url)
|
||||
updatedFileContent = updatedFileContent.decode('utf-8')
|
||||
for name, value in values.items():
|
||||
pattern = "'%s': %%s,\n" % name
|
||||
regex = re.compile(pattern % ".+")
|
||||
replacement = pattern % value
|
||||
if regex.search(updatedFileContent):
|
||||
updatedFileContent = re.sub(regex, replacement, updatedFileContent)
|
||||
else:
|
||||
updatedFileContent = re.sub(r'( *)(.*hook.*)', r'\1%s\1\2' % replacement, updatedFileContent)
|
||||
|
||||
# Bundle is 'assets_common' as this route is only meant to update
|
||||
# variables scss files
|
||||
AssetsUtils.save_asset(url, 'web.assets_common', updatedFileContent, 'scss')
|
||||
return True
|
||||
|
||||
@http.route(['/website/multi_render'], type='json', auth="public", website=True)
|
||||
def multi_render(self, ids_or_xml_ids, values=None):
|
||||
View = request.env['ir.ui.view']
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.addons.web_editor.controllers.main import Web_Editor
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class Web_Editor(Web_Editor):
|
||||
|
||||
def _get_view_fields_to_read(self):
|
||||
res = super(Web_Editor, self)._get_view_fields_to_read()
|
||||
res.append('website_id')
|
||||
return res
|
||||
|
||||
def save_scss_or_js_view_hook(self):
|
||||
res = super(Web_Editor, self).save_scss_or_js_view_hook()
|
||||
|
||||
website = request.env['website'].get_current_website()
|
||||
if website:
|
||||
res['website_id'] = website.id
|
||||
return res
|
||||
|
||||
def save_scss_or_js_attachment_hook(self):
|
||||
res = super(Web_Editor, self).save_scss_or_js_attachment_hook()
|
||||
|
||||
website = request.env['website'].get_current_website()
|
||||
if website:
|
||||
res['website_id'] = website.id
|
||||
return res
|
||||
|
||||
def get_custom_attachment(self, custom_url, op='='):
|
||||
website = request.env['website'].get_current_website()
|
||||
res = super(Web_Editor, self).get_custom_attachment(custom_url, op=op)
|
||||
return res.with_context(website_id=website.id).filtered(lambda x: not x.website_id or x.website_id == website)
|
||||
|
||||
def get_custom_view(self, custom_url, op='='):
|
||||
website = request.env['website'].get_current_website()
|
||||
res = super(Web_Editor, self).get_custom_view(custom_url, op=op)
|
||||
return res.with_context(website_id=website.id).filter_duplicate()
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import assets
|
||||
from . import ir_actions
|
||||
from . import ir_attachment
|
||||
from . import ir_http
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Assets(models.AbstractModel):
|
||||
_inherit = 'web_editor.assets'
|
||||
|
||||
def get_view_fields_to_read(self):
|
||||
res = super(Assets, self).get_view_fields_to_read()
|
||||
res.append('website_id')
|
||||
return res
|
||||
|
||||
def _get_custom_attachment(self, custom_url, op='='):
|
||||
"""
|
||||
See web_editor.Assets._get_custom_attachment
|
||||
Extend to only return the attachments related to the current website.
|
||||
"""
|
||||
website = self.env['website'].get_current_website()
|
||||
res = super(Assets, self)._get_custom_attachment(custom_url, op=op)
|
||||
return res.with_context(website_id=website.id).filtered(lambda x: not x.website_id or x.website_id == website)
|
||||
|
||||
def _get_custom_view(self, custom_url, op='='):
|
||||
"""
|
||||
See web_editor.Assets._get_custom_view
|
||||
Extend to only return the views related to the current website.
|
||||
"""
|
||||
website = self.env['website'].get_current_website()
|
||||
res = super(Assets, self)._get_custom_view(custom_url, op=op)
|
||||
return res.with_context(website_id=website.id).filter_duplicate()
|
||||
|
||||
def _save_asset_attachment_hook(self):
|
||||
"""
|
||||
See web_editor.Assets._save_asset_attachment_hook
|
||||
Extend to add website ID at attachment creation.
|
||||
"""
|
||||
res = super(Assets, self)._save_asset_attachment_hook()
|
||||
|
||||
website = self.env['website'].get_current_website()
|
||||
if website:
|
||||
res['website_id'] = website.id
|
||||
return res
|
||||
|
||||
def _save_asset_view_hook(self):
|
||||
"""
|
||||
See web_editor.Assets._save_asset_view_hook
|
||||
Extend to add website ID at view creation.
|
||||
"""
|
||||
res = super(Assets, self)._save_asset_view_hook()
|
||||
|
||||
website = self.env['website'].get_current_website()
|
||||
if website:
|
||||
res['website_id'] = website.id
|
||||
return res
|
||||
@@ -3,6 +3,7 @@ odoo.define('website.content.menu', function (require) {
|
||||
|
||||
var dom = require('web.dom');
|
||||
var sAnimation = require('website.content.snippets.animation');
|
||||
var wUtils = require('website.utils');
|
||||
|
||||
sAnimation.registry.affixMenu = sAnimation.Class.extend({
|
||||
selector: 'header.o_affix_enabled',
|
||||
@@ -23,6 +24,11 @@ sAnimation.registry.affixMenu = sAnimation.Class.extend({
|
||||
this.$dropdowns = this.$headers.find('.dropdown');
|
||||
this.$navbarCollapses = this.$headers.find('.navbar-collapse');
|
||||
|
||||
this._adaptDefaultOffset();
|
||||
wUtils.onceAllImagesLoaded(this.$headerClone).then(function () {
|
||||
self._adaptDefaultOffset();
|
||||
});
|
||||
|
||||
// Handle events for the collapse menus
|
||||
_.each(this.$headerClone.find('[data-toggle="collapse"]'), function (el) {
|
||||
var $source = $(el);
|
||||
@@ -49,6 +55,24 @@ sAnimation.registry.affixMenu = sAnimation.Class.extend({
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_adaptDefaultOffset: function () {
|
||||
var bottom = this.$target.offset().top + this._getHeaderHeight();
|
||||
this.$headerClone.css('margin-top', Math.min(-200, -bottom) + 'px');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getHeaderHeight: function () {
|
||||
return this.$headerClone.outerHeight();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -92,16 +116,7 @@ sAnimation.registry.autohideMenu = sAnimation.Class.extend({
|
||||
this.noAutohide = this.$el.closest('.o_no_autohide_menu').length;
|
||||
if (!this.noAutohide) {
|
||||
var $navbar = this.$el.closest('.navbar');
|
||||
_.each($navbar.find('img'), function (img) {
|
||||
if (img.complete) {
|
||||
return; // Already loaded
|
||||
}
|
||||
var def = $.Deferred();
|
||||
defs.push(def);
|
||||
$(img).one('load', function () {
|
||||
def.resolve();
|
||||
});
|
||||
});
|
||||
defs.push(wUtils.onceAllImagesLoaded($navbar));
|
||||
|
||||
// The previous code will make sure we wait for images to be fully
|
||||
// loaded before initializing the auto more menu. But in some cases,
|
||||
|
||||
@@ -81,6 +81,12 @@ var CustomizeMenu = Widget.extend({
|
||||
},
|
||||
}).then(function (result) {
|
||||
var currentGroup = '';
|
||||
if (result.length) {
|
||||
$menu.append($('<div/>', {
|
||||
class: 'dropdown-divider',
|
||||
role: 'separator',
|
||||
}));
|
||||
}
|
||||
_.each(result, function (item) {
|
||||
if (currentGroup !== item.inherit_id[1]) {
|
||||
currentGroup = item.inherit_id[1];
|
||||
|
||||
@@ -60,6 +60,24 @@ function autocompleteWithPages(self, $input) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {jQuery} $element
|
||||
*/
|
||||
function onceAllImagesLoaded($element) {
|
||||
var defs = _.map($element.find('img').addBack('img'), function (img) {
|
||||
if (img.complete) {
|
||||
return; // Already loaded
|
||||
}
|
||||
var def = $.Deferred();
|
||||
$(img).one('load', function () {
|
||||
def.resolve();
|
||||
});
|
||||
return def;
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @todo create Dialog.prompt instead of this
|
||||
@@ -168,6 +186,7 @@ function prompt(options, _qweb) {
|
||||
return {
|
||||
loadAnchors: loadAnchors,
|
||||
autocompleteWithPages: autocompleteWithPages,
|
||||
onceAllImagesLoaded: onceAllImagesLoaded,
|
||||
prompt: prompt,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,26 +3,118 @@ odoo.define('website.theme', function (require) {
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
|
||||
var Dialog = require('web.Dialog');
|
||||
var Widget = require('web.Widget');
|
||||
var weWidgets = require('wysiwyg.widgets');
|
||||
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
|
||||
var websiteNavbarData = require('website.navbar');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
var templateDef = null;
|
||||
|
||||
var QuickEdit = Widget.extend({
|
||||
xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
|
||||
template: 'website.theme_customize_active_input',
|
||||
events: {
|
||||
'keydown input': '_onInputKeydown',
|
||||
'click .btn-primary': '_onSaveClick',
|
||||
'click .btn-secondary': '_onResetClick',
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, value, unit) {
|
||||
this._super.apply(this, arguments);
|
||||
this.value = value;
|
||||
this.unit = unit;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$input = this.$('input');
|
||||
this.$input.select();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} [value]
|
||||
*/
|
||||
_save: function (value) {
|
||||
if (value === undefined) {
|
||||
value = parseFloat(this.$input.val());
|
||||
value = isNaN(value) ? 'null' : (value + this.unit);
|
||||
}
|
||||
this.trigger_up('QuickEdit:save', {
|
||||
value: value,
|
||||
});
|
||||
this.destroy();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onInputKeydown: function (ev) {
|
||||
var inputValue = this.$input.val();
|
||||
var value = 0;
|
||||
if (inputValue !== '') {
|
||||
value = parseFloat(this.$input.val());
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (ev.which) {
|
||||
case $.ui.keyCode.UP:
|
||||
this.$input.val(value + 1);
|
||||
break;
|
||||
case $.ui.keyCode.DOWN:
|
||||
this.$input.val(value - 1);
|
||||
break;
|
||||
case $.ui.keyCode.ENTER:
|
||||
// Do not listen to change events, we want the user to be able
|
||||
// to confirm in all cases.
|
||||
this._save();
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onSaveClick: function () {
|
||||
this._save();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onResetClick: function () {
|
||||
this._save('null');
|
||||
},
|
||||
});
|
||||
|
||||
var ThemeCustomizeDialog = Dialog.extend({
|
||||
xmlDependencies: (Dialog.prototype.xmlDependencies || [])
|
||||
.concat(['/website/static/src/xml/website.editor.xml']),
|
||||
|
||||
template: 'website.theme_customize',
|
||||
events: {
|
||||
'change [data-xmlid], [data-enable], [data-disable]': '_onChange',
|
||||
'click .checked [data-xmlid], .checked [data-enable], .checked [data-disable]': '_onChange',
|
||||
'click .o_theme_customize_color': '_onColorClick',
|
||||
'change .o_theme_customize_option_input': '_onChange',
|
||||
'click .checked .o_theme_customize_option_input[type="radio"]': '_onChange',
|
||||
},
|
||||
|
||||
CUSTOM_BODY_IMAGE_XML_ID: 'option_custom_body_image',
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
@@ -58,29 +150,53 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
this._generateDialogHTML();
|
||||
|
||||
this.PX_BY_REM = parseFloat($(document.documentElement).css('font-size'));
|
||||
|
||||
this.$modal.addClass('o_theme_customize_modal');
|
||||
|
||||
this.style = window.getComputedStyle(document.documentElement);
|
||||
|
||||
var $tabs;
|
||||
var loadDef = this._loadViews().then(function (data) {
|
||||
self._generateDialogHTML(data);
|
||||
$tabs = self.$('[data-toggle="tab"]');
|
||||
|
||||
// Hide the tab navigation if only one tab
|
||||
if ($tabs.length <= 1) {
|
||||
$tabs.closest('.nav').addClass('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
// Enable the first option tab or the given default tab
|
||||
var $tabs = this.$('[data-toggle="tab"]');
|
||||
this.opened().then(function () {
|
||||
$tabs.eq(self.defaultTab).tab('show');
|
||||
|
||||
var $colorPreview = self.$('.o_theme_customize_color_previews:visible');
|
||||
var $primary = $colorPreview.find('.o_theme_customize_color[data-color="primary"]');
|
||||
var $alpha = $colorPreview.find('.o_theme_customize_color[data-color="alpha"]');
|
||||
var $secondary = $colorPreview.find('.o_theme_customize_color[data-color="secondary"]');
|
||||
var $beta = $colorPreview.find('.o_theme_customize_color[data-color="beta"]');
|
||||
var sameAlphaPrimary = $primary.find('.o_color_preview').css('background-color') === $alpha.find('.o_color_preview').css('background-color');
|
||||
var sameBetaSecondary = $secondary.find('.o_color_preview').css('background-color') === $beta.find('.o_color_preview').css('background-color');
|
||||
// Hack to hide primary/secondary if they are equal to alpha/beta
|
||||
// (this is the case with default values but not in some themes).
|
||||
var $primary = self.$('.o_theme_customize_color[data-color="primary"]');
|
||||
var $alpha = self.$('.o_theme_customize_color[data-color="alpha"]');
|
||||
var $secondary = self.$('.o_theme_customize_color[data-color="secondary"]');
|
||||
var $beta = self.$('.o_theme_customize_color[data-color="beta"]');
|
||||
|
||||
var sameAlphaPrimary = $primary.css('background-color') === $alpha.css('background-color');
|
||||
var sameBetaSecondary = $secondary.css('background-color') === $beta.css('background-color');
|
||||
|
||||
if (!sameAlphaPrimary) {
|
||||
$alpha.find('.o_color_name').text(_t("Extra Color"));
|
||||
$primary.removeClass('d-none').addClass('d-flex');
|
||||
$alpha.prev().text(_t("Extra Color"));
|
||||
}
|
||||
if (!sameBetaSecondary) {
|
||||
$beta.find('.o_color_name').text(_t("Extra Color"));
|
||||
$secondary.removeClass('d-none').addClass('d-flex');
|
||||
$beta.prev().text(_t("Extra Color"));
|
||||
}
|
||||
|
||||
$primary = $primary.closest('.o_theme_customize_option');
|
||||
$alpha = $alpha.closest('.o_theme_customize_option');
|
||||
$secondary = $secondary.closest('.o_theme_customize_option');
|
||||
$beta = $beta.closest('.o_theme_customize_option');
|
||||
|
||||
$primary.toggleClass('d-none', sameAlphaPrimary);
|
||||
$secondary.toggleClass('d-none', sameBetaSecondary);
|
||||
|
||||
if (!sameAlphaPrimary && sameBetaSecondary) {
|
||||
$beta.insertBefore($alpha);
|
||||
} else if (sameAlphaPrimary && !sameBetaSecondary) {
|
||||
@@ -88,17 +204,7 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
}
|
||||
});
|
||||
|
||||
// Hide the tab navigation if only one tab
|
||||
if ($tabs.length <= 1) {
|
||||
$tabs.closest('.nav').addClass('d-none');
|
||||
}
|
||||
|
||||
this.$inputs = this.$('[data-xmlid], [data-enable], [data-disable]');
|
||||
|
||||
return $.when(
|
||||
this._super.apply(this, arguments),
|
||||
this._loadViews()
|
||||
);
|
||||
return $.when(this._super.apply(this, arguments), loadDef);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -108,7 +214,47 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_generateDialogHTML: function () {
|
||||
_chooseBodyCustomImage: function () {
|
||||
var self = this;
|
||||
var def = $.Deferred();
|
||||
var $image = $('<img/>');
|
||||
var editor = new weWidgets.MediaDialog(this, {
|
||||
onlyImages: true,
|
||||
firstFilters: ['background'],
|
||||
}, $image[0]);
|
||||
|
||||
editor.on('save', this, function (media) { // TODO use scss customization instead (like for user colors)
|
||||
var src = $(media).attr('src');
|
||||
self._rpc({
|
||||
model: 'ir.model.data',
|
||||
method: 'get_object_reference',
|
||||
args: ['website', this.CUSTOM_BODY_IMAGE_XML_ID],
|
||||
}).then(function (data) {
|
||||
return self._rpc({
|
||||
model: 'ir.ui.view',
|
||||
method: 'save',
|
||||
args: [
|
||||
data[1],
|
||||
'#wrapwrap { background-image: url("' + src + '"); }',
|
||||
'//style',
|
||||
],
|
||||
});
|
||||
}).always(def.resolve.bind(def));
|
||||
});
|
||||
editor.on('cancel', this, function () {
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
editor.open();
|
||||
|
||||
return def;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} data - @see this._loadViews
|
||||
*/
|
||||
_generateDialogHTML: function (data) {
|
||||
var self = this;
|
||||
var $contents = this.$el.children('content');
|
||||
if ($contents.length === 0) {
|
||||
return;
|
||||
@@ -148,8 +294,21 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
|
||||
this.$('[title]').tooltip();
|
||||
|
||||
this.$inputs = self.$('.o_theme_customize_option_input');
|
||||
// Enable data-xmlid="" inputs if none of their neighbors were enabled
|
||||
_.each(this.$inputs.filter('[data-xmlid=""]'), function (input) {
|
||||
var $input = $(input);
|
||||
var $neighbors = self.$inputs.filter('[name="' + $input.attr('name') + '"]').not($input);
|
||||
if ($neighbors.length && !$neighbors.filter(':checked').length) {
|
||||
$input.prop('checked', true);
|
||||
}
|
||||
});
|
||||
this._setActive();
|
||||
this._updateValues();
|
||||
|
||||
function _processItems($items, $container) {
|
||||
var optionsName = _.uniqueId('option-');
|
||||
var alone = ($items.length === 1);
|
||||
|
||||
_.each($items, function (item) {
|
||||
var $item = $(item);
|
||||
@@ -159,37 +318,47 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
case 'OPT':
|
||||
var widgetName = $item.data('widget');
|
||||
|
||||
// Build the options template
|
||||
var $option = $(core.qweb.render('website.theme_customize_modal_option', {
|
||||
name: optionsName,
|
||||
id: $item.attr('id') || _.uniqueId('o_theme_customize_input_id_'),
|
||||
var xmlid = $item.data('xmlid');
|
||||
|
||||
string: $item.attr('string'),
|
||||
var renderingOptions = _.extend({
|
||||
string: $item.attr('string') || data.names[xmlid.split(',')[0].trim()],
|
||||
icon: $item.data('icon'),
|
||||
font: $item.data('font'),
|
||||
}, $item.data());
|
||||
|
||||
xmlid: $item.data('xmlid'),
|
||||
enable: $item.data('enable'),
|
||||
disable: $item.data('disable'),
|
||||
reload: $item.data('reload'),
|
||||
|
||||
// Build the options template
|
||||
var $option = $(core.qweb.render('website.theme_customize_modal_option', _.extend({
|
||||
alone: alone,
|
||||
name: xmlid === undefined ? _.uniqueId('option-') : optionsName,
|
||||
id: $item.attr('id') || _.uniqueId('o_theme_customize_input_id_'),
|
||||
checked: xmlid === undefined || xmlid && (!_.difference(self._getXMLIDs($item), data.enabled).length),
|
||||
widget: widgetName,
|
||||
}));
|
||||
}, renderingOptions)));
|
||||
$option.find('input')
|
||||
.addClass('o_theme_customize_option_input')
|
||||
.attr({
|
||||
'data-xmlid': xmlid,
|
||||
'data-enable': $item.data('enable'),
|
||||
'data-disable': $item.data('disable'),
|
||||
'data-reload': $item.data('reload'),
|
||||
});
|
||||
|
||||
if (widgetName) {
|
||||
var $widget = $(core.qweb.render('website.theme_customize_' + widgetName));
|
||||
$option.append($widget);
|
||||
var $widget = $(core.qweb.render('website.theme_customize_widget_' + widgetName, renderingOptions));
|
||||
$option.find('label').append($widget);
|
||||
}
|
||||
|
||||
var $final;
|
||||
if ($container.hasClass('form-row')) {
|
||||
$col = $('<div/>', {
|
||||
$final = $('<div/>', {
|
||||
class: _.str.sprintf('col-%s', $item.data('col') || 6),
|
||||
});
|
||||
$col.append($option);
|
||||
$container.append($col);
|
||||
$final.append($option);
|
||||
} else {
|
||||
$container.append($option);
|
||||
$final = $option;
|
||||
}
|
||||
$final.attr('data-depends', $item.data('depends'));
|
||||
$container.append($final);
|
||||
break;
|
||||
|
||||
case 'LIST':
|
||||
@@ -201,6 +370,10 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
$container.append($col);
|
||||
_processItems($item.children(), $listContainer);
|
||||
break;
|
||||
|
||||
default:
|
||||
_processItems($item.children(), $container);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -209,29 +382,11 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
* @private
|
||||
*/
|
||||
_loadViews: function () {
|
||||
var self = this;
|
||||
return this._rpc({
|
||||
route: '/website/theme_customize_get',
|
||||
params: {
|
||||
xml_ids: this._getXMLIDs(this.$inputs),
|
||||
'xml_ids': this._getXMLIDs(this.$inputs || this.$('[data-xmlid]')),
|
||||
},
|
||||
}).done(function (data) {
|
||||
self.$inputs.prop('checked', false);
|
||||
_.each(self.$inputs.filter('[data-xmlid]:not([data-xmlid=""])'), function (input) {
|
||||
var $input = $(input);
|
||||
if (!_.difference(self._getXMLIDs($input), data[0]).length) {
|
||||
$input.prop('checked', true);
|
||||
}
|
||||
});
|
||||
_.each(self.$inputs.filter('[data-xmlid=""]'), function (input) {
|
||||
var $input = $(input);
|
||||
if (!self.$inputs.filter('[name="' + $input.attr('name') + '"]:checked').length) {
|
||||
$input.prop('checked', true);
|
||||
}
|
||||
});
|
||||
self._setActive();
|
||||
}).fail(function (d, error) {
|
||||
Dialog.alert(this, error.data.message);
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -260,49 +415,112 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_processChange: function ($inputs) {
|
||||
_makeSCSSCusto: function (url, values) {
|
||||
return this._rpc({
|
||||
route: '/website/make_scss_custo',
|
||||
params: {
|
||||
'url': url,
|
||||
'values': values,
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_pickColor: function (colorElement) {
|
||||
var self = this;
|
||||
this.$modal.addClass('o_theme_customize_loading');
|
||||
|
||||
var bodyCustomImageXMLID = 'option_custom_body_image';
|
||||
var $inputBodyCustomImage = $inputs.filter('[data-xmlid*="website.' + bodyCustomImageXMLID + '"]:checked');
|
||||
if (!$inputBodyCustomImage.length) {
|
||||
return $.when();
|
||||
}
|
||||
var $color = $(colorElement);
|
||||
var colorName = $color.data('color');
|
||||
var colorType = $color.data('colorType');
|
||||
|
||||
var def = $.Deferred();
|
||||
var $image = $('<img/>');
|
||||
var editor = new weWidgets.MediaDialog(this, {
|
||||
onlyImages: true,
|
||||
firstFilters: ['background'],
|
||||
}, $image[0]);
|
||||
|
||||
editor.on('save', this, function (media) { // TODO use scss customization instead (like for user colors)
|
||||
var src = $(media).attr('src');
|
||||
self._rpc({
|
||||
model: 'ir.model.data',
|
||||
method: 'get_object_reference',
|
||||
args: ['website', bodyCustomImageXMLID],
|
||||
}).then(function (data) {
|
||||
return self._rpc({
|
||||
model: 'ir.ui.view',
|
||||
method: 'save',
|
||||
args: [
|
||||
data[1],
|
||||
'#wrapwrap { background-image: url("' + src + '"); }',
|
||||
'//style',
|
||||
],
|
||||
});
|
||||
}).then(function () {
|
||||
var colorpicker = new ColorpickerDialog(this, {
|
||||
defaultColor: $color.css('background-color'),
|
||||
});
|
||||
var chosenColor = undefined;
|
||||
colorpicker.on('colorpicker:saved', this, function (ev) {
|
||||
ev.stopPropagation();
|
||||
chosenColor = ev.data.cssColor;
|
||||
});
|
||||
colorpicker.on('closed', this, function (ev) {
|
||||
if (chosenColor === undefined) {
|
||||
def.resolve();
|
||||
});
|
||||
});
|
||||
editor.on('cancel', this, function () {
|
||||
def.resolve();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
editor.open();
|
||||
var baseURL = '/website/static/src/scss/options/colors/';
|
||||
var url = _.str.sprintf('%suser_%scolor_palette.scss', baseURL, (colorType ? (colorType + '_') : ''));
|
||||
|
||||
var colors = {};
|
||||
colors[colorName] = chosenColor;
|
||||
if (colorName === 'alpha') {
|
||||
colors['beta'] = 'null';
|
||||
colors['gamma'] = 'null';
|
||||
colors['delta'] = 'null';
|
||||
colors['epsilon'] = 'null';
|
||||
}
|
||||
|
||||
self._makeSCSSCusto(url, colors).always(def.resolve.bind(def));
|
||||
});
|
||||
colorpicker.open();
|
||||
|
||||
return def;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_processChange: function ($inputs) {
|
||||
var self = this;
|
||||
var defs = [];
|
||||
|
||||
var $options = $inputs.closest('.o_theme_customize_option');
|
||||
|
||||
// Handle body image changes
|
||||
var $bodyImageInputs = $inputs.filter('[data-xmlid*="website.' + this.CUSTOM_BODY_IMAGE_XML_ID + '"]:checked');
|
||||
defs = defs.concat(_.map($bodyImageInputs, function () {
|
||||
return self._chooseBodyCustomImage();
|
||||
}));
|
||||
|
||||
// Handle color changes
|
||||
var $colors = $options.find('.o_theme_customize_color');
|
||||
defs = defs.concat(_.map($colors, function (colorElement) {
|
||||
return self._pickColor($(colorElement));
|
||||
}));
|
||||
|
||||
// Handle input changes
|
||||
var $inputsData = $options.find('.o_theme_customize_input');
|
||||
defs = defs.concat(_.map($inputsData, function (inputData, i) {
|
||||
return self._quickEdit($(inputData));
|
||||
}));
|
||||
|
||||
return $.when.apply($, defs);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_quickEdit: function ($inputData) {
|
||||
var text = $inputData.text().trim();
|
||||
var value = parseFloat(text) || '';
|
||||
var unit = text.match(/([^\s\d]+)$/)[1];
|
||||
|
||||
var def = $.Deferred();
|
||||
var qEdit = new QuickEdit(this, value, unit);
|
||||
qEdit.on('QuickEdit:save', this, function (ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
var value = ev.data.value;
|
||||
// Convert back to rem if needed
|
||||
if ($inputData.data('unit') === 'rem' && unit === 'px' && value !== 'null') {
|
||||
value = parseFloat(value) / this.PX_BY_REM + 'rem';
|
||||
}
|
||||
|
||||
var values = {};
|
||||
values[$inputData.data('value')] = value;
|
||||
this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', values)
|
||||
.always(def.resolve.bind(def));
|
||||
});
|
||||
qEdit.appendTo($inputData.closest('.o_theme_customize_option'));
|
||||
return def;
|
||||
},
|
||||
/**
|
||||
@@ -312,11 +530,11 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
var self = this;
|
||||
|
||||
// Look at all options to see if they are enabled or disabled
|
||||
var $enable = this.$inputs.filter('[data-xmlid]:checked');
|
||||
var $enable = this.$inputs.filter(':checked');
|
||||
|
||||
// Mark the labels as checked accordingly
|
||||
this.$('label').removeClass('checked');
|
||||
$enable.closest('label').addClass('checked');
|
||||
$enable.closest('label:not(.o_switch)').addClass('checked');
|
||||
|
||||
// Mark the option sets as checked if all their option are checked/unchecked
|
||||
var $sets = this.$inputs.filter('[data-enable], [data-disable]').not('[data-xmlid]');
|
||||
@@ -329,15 +547,21 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
if (self._getInputs($set.data('disable')).filter(':checked').length) {
|
||||
checked = false;
|
||||
}
|
||||
$set.prop('checked', checked).closest('label').toggleClass('checked', checked);
|
||||
$set.prop('checked', checked).closest('label:not(.o_switch)').toggleClass('checked', checked);
|
||||
});
|
||||
|
||||
// Make the hidden sections visible if their dependencies are met
|
||||
_.each(this.$('[data-depends]'), function (hidden) {
|
||||
var $hidden = $(hidden);
|
||||
var depends = $hidden.data('depends');
|
||||
var nbDependencies = depends ? depends.split(',').length : 0;
|
||||
var enabled = self._getInputs(depends).filter(':checked').length === nbDependencies;
|
||||
var dependencies = depends ? depends.split(/\s*,\s*/g) : [];
|
||||
var enabled = _.all(dependencies, function (dep) {
|
||||
var toBeChecked = (dep[0] !== '!');
|
||||
if (!toBeChecked) {
|
||||
dep = dep.substr(1);
|
||||
}
|
||||
return self._getInputs(dep).is(':checked') === toBeChecked;
|
||||
});
|
||||
$hidden.toggleClass('d-none', !enabled);
|
||||
});
|
||||
},
|
||||
@@ -345,6 +569,11 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
* @private
|
||||
*/
|
||||
_updateStyle: function (enable, disable, reload) {
|
||||
var self = this;
|
||||
|
||||
var $loading = $('<i/>', {class: 'fa fa-refresh fa-spin'});
|
||||
this.$modal.find('.modal-title').append($loading);
|
||||
|
||||
if (reload || config.debug === 'assets') {
|
||||
window.location.href = $.param.querystring('/website/theme_customize_reload', {
|
||||
href: window.location.href,
|
||||
@@ -355,18 +584,19 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
return $.Deferred();
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return this._rpc({
|
||||
route: '/website/theme_customize',
|
||||
params: {
|
||||
enable: enable,
|
||||
disable: disable,
|
||||
get_bundle: true,
|
||||
'enable': enable,
|
||||
'disable': disable,
|
||||
'get_bundle': true,
|
||||
},
|
||||
}).then(function (bundles) {
|
||||
var $allLinks = $();
|
||||
var defs = _.map(bundles, function (bundleContent, bundleName) {
|
||||
var linkSelector = 'link[href*="' + bundleName + '"]';
|
||||
var $links = $(linkSelector);
|
||||
$allLinks = $allLinks.add($links);
|
||||
var $newLinks = $(bundleContent).filter(linkSelector);
|
||||
|
||||
var linksLoaded = $.Deferred();
|
||||
@@ -382,14 +612,51 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
window.location.reload();
|
||||
});
|
||||
$links.last().after($newLinks);
|
||||
return linksLoaded.then(function () {
|
||||
$links.remove();
|
||||
});
|
||||
return linksLoaded;
|
||||
});
|
||||
return $.when.apply($, defs).always(function () {
|
||||
$loading.remove();
|
||||
$allLinks.remove();
|
||||
});
|
||||
}).then(function () {
|
||||
// Some animations may depend on the variables that were
|
||||
// customized, so we have to restart them.
|
||||
self.trigger_up('animation_start_demand');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_updateValues: function () {
|
||||
var self = this;
|
||||
// Put user values
|
||||
_.each(this.$('.o_theme_customize_color'), function (el) {
|
||||
var $el = $(el);
|
||||
var value = self.style.getPropertyValue('--' + $el.data('color')).trim();
|
||||
$el.css('background-color', value);
|
||||
});
|
||||
_.each(this.$('.o_theme_customize_input'), function (el) {
|
||||
var $el = $(el);
|
||||
var value = self.style.getPropertyValue('--' + $el.data('value')).trim();
|
||||
|
||||
return $.when.apply($, defs).then(function () {
|
||||
self.$modal.removeClass('o_theme_customize_loading');
|
||||
});
|
||||
// Convert rem values to px values
|
||||
if (_.str.endsWith(value, 'rem')) {
|
||||
value = parseFloat(value) * self.PX_BY_REM + 'px';
|
||||
}
|
||||
|
||||
var $span = $el.find('span');
|
||||
$span.removeClass().text('');
|
||||
switch (value) {
|
||||
case '':
|
||||
case 'false':
|
||||
case 'true':
|
||||
// When null or a boolean value, shows an icon which tells
|
||||
// the user that there is no numeric/text value
|
||||
$span.addClass('fa fa-ban text-danger');
|
||||
break;
|
||||
default:
|
||||
$span.text(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -406,6 +673,11 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
|
||||
// Checkout the option that changed
|
||||
var $option = $(ev.currentTarget);
|
||||
if ($option.is(':disabled')) {
|
||||
return;
|
||||
}
|
||||
this.$inputs.prop('disabled', true);
|
||||
|
||||
var $options = $option;
|
||||
var checked = $option.is(':checked');
|
||||
|
||||
@@ -437,91 +709,21 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
|
||||
// Update the style according to the whole set of options
|
||||
self._processChange($options).then(function () {
|
||||
self._updateStyle(
|
||||
return self._updateStyle(
|
||||
self._getXMLIDs($enable),
|
||||
self._getXMLIDs($disable),
|
||||
$option.data('reload') && window.location.href.match(new RegExp($option.data('reload')))
|
||||
);
|
||||
}).then(function () {
|
||||
self._updateValues();
|
||||
self.$inputs.prop('disabled', false);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
_onColorClick: function (ev) {
|
||||
var self = this;
|
||||
var $color = $(ev.currentTarget);
|
||||
var colorName = $color.data('color');
|
||||
var colorType = $color.data('colorType');
|
||||
|
||||
var colorpicker = new ColorpickerDialog(this, {
|
||||
defaultColor: $color.find('.o_color_preview').css('background-color'),
|
||||
});
|
||||
colorpicker.on('colorpicker:saved', this, function (ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO improve to be more efficient
|
||||
self._rpc({
|
||||
route: '/web_editor/get_assets_editor_resources',
|
||||
params: {
|
||||
key: 'website.layout',
|
||||
get_views: false,
|
||||
get_scss: true,
|
||||
get_js: false,
|
||||
bundles: false,
|
||||
bundles_restriction: [],
|
||||
only_user_custom_files: false,
|
||||
},
|
||||
}).then(function (data) {
|
||||
var files = data.scss[0][1];
|
||||
var file = _.find(files, function (file) {
|
||||
var baseURL = '/website/static/src/scss/options/colors/';
|
||||
return file.url === _.str.sprintf('%suser_%scolor_palette.scss', baseURL, (colorType ? (colorType + '_') : ''));
|
||||
});
|
||||
|
||||
var colors = {};
|
||||
colors[colorName] = ev.data.cssColor;
|
||||
if (colorName === 'alpha') {
|
||||
colors['beta'] = 'null';
|
||||
colors['gamma'] = 'null';
|
||||
colors['delta'] = 'null';
|
||||
colors['epsilon'] = 'null';
|
||||
}
|
||||
|
||||
var updatedFileContent = file.arch;
|
||||
_.each(colors, function (colorValue, colorName) {
|
||||
var pattern = _.str.sprintf("'%s': %%s,\n", colorName);
|
||||
var regex = new RegExp(_.str.sprintf(pattern, ".+"));
|
||||
var replacement = _.str.sprintf(pattern, colorValue);
|
||||
if (regex.test(updatedFileContent)) {
|
||||
updatedFileContent = updatedFileContent
|
||||
.replace(regex, replacement);
|
||||
} else {
|
||||
updatedFileContent = updatedFileContent
|
||||
.replace(/( *)(.*hook.*)/, _.str.sprintf('$1%s$1$2', replacement));
|
||||
}
|
||||
});
|
||||
|
||||
return self._rpc({
|
||||
route: '/web_editor/save_scss_or_js',
|
||||
params: {
|
||||
url: file.url,
|
||||
bundle_xmlid: 'web.assets_common',
|
||||
content: updatedFileContent,
|
||||
file_type: 'scss',
|
||||
},
|
||||
});
|
||||
}).then(function () {
|
||||
return self._updateStyle();
|
||||
});
|
||||
});
|
||||
colorpicker.open();
|
||||
},
|
||||
});
|
||||
|
||||
var ThemeCustomizeMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
|
||||
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
|
||||
customize_theme: '_openThemeCustomizeDialog',
|
||||
'customize_theme': '_openThemeCustomizeDialog',
|
||||
}),
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file is meant to be edited automatically by the user. The variables it
|
||||
// contains should not be renamed otherwise it would break existing customers
|
||||
// customizations.
|
||||
|
||||
$o-user-website-values: map-merge($o-user-website-values, o-map-omit((
|
||||
// -- hook --
|
||||
)));
|
||||
@@ -40,6 +40,22 @@ $o-social-colors: (
|
||||
|
||||
$o-theme-figcaption-opacity: 0.6;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Website customizations
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
$o-website-values-palettes: (
|
||||
(
|
||||
'logo-height': null, // Default to navbar height (see portal)
|
||||
'header-font-size': null, // Default to BS (normal font-size)
|
||||
),
|
||||
) !default;
|
||||
$o-website-values-palette-number: 1 !default;
|
||||
|
||||
// By default, all user website values are null. Each null value is
|
||||
// automatically replaced with corresponsing value of chosen values palette.
|
||||
$o-user-website-values: () !default;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Fonts
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -34,6 +34,20 @@ $o-theme-color-palettes: append($o-theme-color-palettes, map-merge($-palette-def
|
||||
|
||||
|
||||
// Enable last color and theme color palettes, which are now the user customized
|
||||
// color palettes
|
||||
// color palettes.
|
||||
$o-color-palette-number: length($o-color-palettes);
|
||||
$o-theme-color-palette-number: length($o-theme-color-palettes);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Website customizations
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
$-website-values-default: nth($o-website-values-palettes, $o-website-values-palette-number);
|
||||
$o-website-values: map-merge($-website-values-default, o-map-omit($o-user-website-values)) !default;
|
||||
$o-website-values-palettes: append($o-website-values-palettes, $o-website-values);
|
||||
$o-website-values-palette-number: length($o-website-values-palettes);
|
||||
@function o-website-value($key) {
|
||||
@return map-get($o-website-values, $key);
|
||||
}
|
||||
|
||||
$o-theme-navbar-logo-height: o-website-value('logo-height') !default;
|
||||
|
||||
@@ -6,17 +6,6 @@
|
||||
}
|
||||
|
||||
.modal.o_theme_customize_modal {
|
||||
|
||||
&.o_theme_customize_loading {
|
||||
pointer-events: none;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
@include o-position-absolute(0, 2%);
|
||||
width: 96%;
|
||||
@@ -71,25 +60,40 @@
|
||||
+ label {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
width: auto;
|
||||
background: transparent;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 24px;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
border: 2px solid transparent;
|
||||
background-color: $o-brand-lightsecondary;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
background-color: rgba(0, 32, 64, 0.03);
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border-color: $o-brand-odoo;
|
||||
color: $o-brand-odoo;
|
||||
}
|
||||
|
||||
&.o_switch {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
$i: 1;
|
||||
@@ -103,57 +107,68 @@
|
||||
}
|
||||
$i: $i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
.o_theme_customize_color_previews {
|
||||
h6, a {
|
||||
font-size: 12px;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
&.o_theme_customize_with_widget {
|
||||
label {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
|
||||
.o_theme_customize_color {
|
||||
> .o_color_preview {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 27px;
|
||||
border: 1px solid black;
|
||||
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
}
|
||||
&::before {
|
||||
background-image: url('/web_editor/static/src/img/transparent.png');
|
||||
}
|
||||
&::after {
|
||||
background-color: inherit;
|
||||
&.checked {
|
||||
border-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$palette: nth($o-color-palettes, $o-color-palette-number);
|
||||
$-text-color: if(map-get($palette, 'text'), map-get($palette, 'text'), #212529); // BS default as fallback (TODO find real value)
|
||||
$-h1-color: if(map-get($palette, 'h1'), map-get($palette, 'h1'), $-text-color);
|
||||
$-h2-color: if(map-get($palette, 'h2'), map-get($palette, 'h2'), $-h1-color);
|
||||
$-h3-color: if(map-get($palette, 'h3'), map-get($palette, 'h3'), $-h2-color);
|
||||
$-h4-color: if(map-get($palette, 'h4'), map-get($palette, 'h4'), $-h3-color);
|
||||
$-h5-color: if(map-get($palette, 'h5'), map-get($palette, 'h5'), $-h4-color);
|
||||
$-h6-color: if(map-get($palette, 'h6'), map-get($palette, 'h6'), $-h5-color);
|
||||
$-defaults: (
|
||||
'body': white, // BS default as fallback (TODO find real value)
|
||||
'text': $-text-color,
|
||||
'h1': $-h1-color,
|
||||
'h2': $-h2-color,
|
||||
'h3': $-h3-color,
|
||||
'h4': $-h4-color,
|
||||
'h5': $-h5-color,
|
||||
'h6': $-h6-color,
|
||||
);
|
||||
.o_theme_customize_color {
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
margin-left: auto;
|
||||
border: 1px solid black;
|
||||
|
||||
@each $name, $value in map-merge($-defaults, o-map-omit($palette)) {
|
||||
&.o_theme_customize_color_#{$name} > .o_color_preview {
|
||||
background-color: $value;
|
||||
}
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
}
|
||||
&::before {
|
||||
background-image: url('/web_editor/static/src/img/transparent.png');
|
||||
}
|
||||
&::after {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.o_theme_customize_input {
|
||||
flex: 1 1 auto;
|
||||
text-align: right;
|
||||
|
||||
.fa-edit {
|
||||
visibility: hidden;
|
||||
color: $o-brand-primary;
|
||||
}
|
||||
}
|
||||
.o_theme_customize_option:hover .o_theme_customize_input .fa-edit {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.o_theme_customize_active_input {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
width: auto;
|
||||
padding: 3px;
|
||||
|
||||
> * {
|
||||
height: 100% !important;
|
||||
|
||||
&.form-control {
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
&.form-control, .btn {
|
||||
padding: 2px 8px !important;
|
||||
text-align: right !important;
|
||||
font-size: $font-size-sm !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,44 @@ $-font-numbers: (
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
// The theme customize modal JS will need to know the value of some scss
|
||||
// variables used to render the user website, and those may have been
|
||||
// customized by themes, the user or anything else (so there is no file to
|
||||
// parse to get them). Those will be printed here as CSS variables.
|
||||
|
||||
// 1) Handle default values
|
||||
--header-font-size: #{$font-size-base};
|
||||
|
||||
// 2) The values in the $theme-colors map are already printed by Bootstrap.
|
||||
|
||||
// 3) The values in the $colors map are also printed by Bootstrap.
|
||||
|
||||
// 4) The Odoo values map, $o-website-values, must be printed.
|
||||
@each $key, $value in $o-website-values {
|
||||
--#{$key}: #{$value};
|
||||
}
|
||||
|
||||
// 5) Use final value used by the theme
|
||||
$-h1-color: if($headings-color == inherit, $body-color, $headings-color);
|
||||
$-h2-color: if(color('h2'), color('h2'), $-h1-color);
|
||||
$-h3-color: if(color('h3'), color('h3'), $-h2-color);
|
||||
$-h4-color: if(color('h4'), color('h4'), $-h3-color);
|
||||
$-h5-color: if(color('h5'), color('h5'), $-h4-color);
|
||||
$-h6-color: if(color('h6'), color('h6'), $-h5-color);
|
||||
|
||||
--body: #{$body-bg};
|
||||
--text: #{$body-color};
|
||||
--h1: #{$-h1-color};
|
||||
--h2: #{$-h2-color};
|
||||
--h3: #{$-h3-color};
|
||||
--h4: #{$-h4-color};
|
||||
--h5: #{$-h5-color};
|
||||
--h6: #{$-h6-color};
|
||||
|
||||
--logo-height: #{$o-theme-navbar-logo-height};
|
||||
}
|
||||
|
||||
#wrapwrap {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
@@ -64,6 +102,31 @@ $-font-numbers: (
|
||||
}
|
||||
}
|
||||
|
||||
$-header-nav-link-height: $nav-link-height;
|
||||
@if o-website-value('header-font-size') {
|
||||
$-header-nav-link-height: o-website-value('header-font-size') * $line-height-base + $nav-link-padding-y * 2;
|
||||
header {
|
||||
font-size: o-website-value('header-font-size');
|
||||
|
||||
.dropdown-menu {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@if $o-theme-navbar-logo-height {
|
||||
// With default values, this makes it slightly bigger than standard
|
||||
// navbar-brand, which is what we want
|
||||
header .navbar-brand {
|
||||
font-size: $o-theme-navbar-logo-height / $line-height-base;
|
||||
|
||||
$-logo-padding-y: max(0, $-header-nav-link-height - $o-theme-navbar-logo-height) / 2;
|
||||
&, &.logo {
|
||||
padding-top: $-logo-padding-y;
|
||||
padding-bottom: $-logo-padding-y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_footer {
|
||||
@if color('footer') {
|
||||
@include o-bg-color(color('footer'));
|
||||
@@ -985,10 +1048,10 @@ table.table_desc tr td {
|
||||
position: fixed;
|
||||
z-index: $zindex-modal - 20;
|
||||
background: $light;
|
||||
margin-top: -200px;
|
||||
margin-top: -999px;
|
||||
transition: margin-top 500ms ease 0s;
|
||||
&.affixed {
|
||||
margin-top: 0px;
|
||||
margin-top: 0px !important; // the default margin-top is adapted in JS
|
||||
}
|
||||
}
|
||||
#oe_main_menu_navbar + #wrapwrap .o_header_affix {
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
<!-- Custom checkbox (material-design-like toggle) -->
|
||||
<t t-name="website.components.switch">
|
||||
<label class="o_switch" t-att-for="id">
|
||||
<input type="checkbox" t-att-id="id"/>
|
||||
<input type="checkbox" t-att-id="id" t-att-checked="checked ? 'checked' : undefined"/>
|
||||
<span/>
|
||||
<t t-if="label"><t t-esc="label"/></t>
|
||||
<div t-if="label"><t t-esc="label"/></div>
|
||||
</label>
|
||||
</t>
|
||||
|
||||
@@ -37,54 +37,39 @@
|
||||
<div class="form-row justify-content-between o_options_container"/>
|
||||
</div>
|
||||
<t t-name="website.theme_customize_modal_option">
|
||||
<div t-attf-class="o_theme_customize_option my-1 flex-grow-0 #{font ? 'o_theme_customize_option_font_' + font : ''}">
|
||||
<div t-attf-class="o_theme_customize_option my-1 flex-grow-0 #{font ? 'o_theme_customize_option_font_' + font : ''} #{widget ? 'o_theme_customize_with_widget' : ''}">
|
||||
<img t-if="icon" t-att-src="icon"/>
|
||||
<label t-attf-class="#{widget ? 'd-none' : 'd-flex'} align-items-center justify-content-center text-center">
|
||||
<input t-att-id="id" t-att-name="name" type="radio" class="o_theme_customize_option_input" t-att-data-xmlid="xmlid" t-att-data-enable="enable" t-att-data-disable="disable" t-att-data-reload="reload"/>
|
||||
<span><t t-esc="string"/></span>
|
||||
|
||||
<t t-set="label" t-value="font ? '' : string"/>
|
||||
<t t-if="alone and !widget" t-call="website.components.switch"/>
|
||||
<label t-else="">
|
||||
<input t-att-id="id" t-att-name="name" type="radio" t-att-checked="checked ? 'checked' : undefined"/>
|
||||
<span><t t-esc="label"/></span>
|
||||
</label>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="website.theme_customize_color_palette">
|
||||
<div class="o_theme_customize_color_previews">
|
||||
<div>
|
||||
<h6>Main colors</h6>
|
||||
<t t-foreach="{'primary': _t('Primary'), 'secondary': _t('Secondary'), 'alpha': _t('Primary'), 'beta': _t('Secondary'), 'gamma': _t('Extra Color'), 'delta': _t('Extra Color'), 'epsilon': _t('Extra Color')}" t-as="color">
|
||||
<t t-call="website.theme_customize_color_palette_button">
|
||||
<t t-set="color_type" t-value="'theme'"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Background colors</h6>
|
||||
<t t-foreach="{'body': _t('Body'), 'menu': _t('Menus'), 'footer': _t('Footer')}" t-as="color">
|
||||
<t t-call="website.theme_customize_color_palette_button"/>
|
||||
</t>
|
||||
</div>
|
||||
<div>
|
||||
<h6>Text colors</h6>
|
||||
<t t-foreach="{'text': _t('Text'), 'h1': _t('Headings 1'), 'h2': _t('Headings 2'), 'h3': _t('Headings 3'), 'h4': _t('Headings 4'), 'h5': _t('Headings 5'), 'h6': _t('Headings 6')}" t-as="color">
|
||||
<t t-call="website.theme_customize_color_palette_button"/>
|
||||
</t>
|
||||
</div>
|
||||
<div>
|
||||
<h6>State colors</h6>
|
||||
<t t-foreach="{'success': _t('Success'), 'info': _t('Info'), 'warning': _t('Warning'), 'danger': _t('Error')}" t-as="color">
|
||||
<t t-call="website.theme_customize_color_palette_button">
|
||||
<t t-set="color_type" t-value="'theme'"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<t t-name="website.theme_customize_widget_color">
|
||||
<div t-attf-class="o_theme_customize_color o_theme_customize_color_#{color}"
|
||||
t-att-data-color="color"
|
||||
t-att-data-color-type="colorType"/>
|
||||
</t>
|
||||
<t t-name="website.theme_customize_widget_input">
|
||||
<div t-attf-class="o_theme_customize_input o_theme_customize_input_#{value}"
|
||||
t-att-data-value="value"
|
||||
t-att-data-unit="unit">
|
||||
<i class="fa fa-edit"/>
|
||||
<span/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="website.theme_customize_color_palette_button">
|
||||
<button type="button"
|
||||
t-attf-class="btn btn-secondary #{color == 'primary' or color == 'secondary' ? 'd-none' : 'd-flex'} align-items-center w-100 mt-2 justify-content-between o_theme_customize_color o_theme_customize_color_#{color}"
|
||||
t-att-data-color="color"
|
||||
t-att-data-color-type="color_type">
|
||||
<span class="o_color_name"><t t-esc="color_value"/></span>
|
||||
<span t-attf-class="o_color_preview bg-#{color}"/>
|
||||
</button>
|
||||
<t t-name="website.theme_customize_active_input">
|
||||
<div class="input-group input-group-sm align-items-center o_theme_customize_active_input">
|
||||
<input type="text" class="form-control" t-att-value="widget.value"/>
|
||||
<div class="input-group-append">
|
||||
<div t-if="widget.unit" class="input-group-text"><t t-esc="widget.unit"/></div>
|
||||
<button type="button" class="btn btn-primary fa fa-check" title="Save"/>
|
||||
<button type="button" class="btn btn-secondary fa fa-undo" title="Reset"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-extend="wysiwyg.widgets.link">
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
<a role="menuitem" href="#" data-action="customize_theme" class="dropdown-item" id="theme_customize">Customize Theme</a>
|
||||
<a role="menuitem" href="#" data-action="ace" class="dropdown-item" id="html_editor">HTML/CSS/JS Editor</a>
|
||||
<a role="menuitem" href="/web#action=website.action_website_add_features" class="dropdown-item" id="install_apps" context="{'search_default_web_features' : 1}">Add Features</a>
|
||||
<div role="separator" class="dropdown-divider"/>
|
||||
</div>
|
||||
</li>
|
||||
<!-- Promote -->
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
<template id="_assets_primary_variables" inherit_id="portal._assets_primary_variables">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/primary_variables.scss"/>
|
||||
|
||||
<!-- Those files will be automatically edited by users -->
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/options/user_values.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/options/colors/user_color_palette.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/options/colors/user_theme_color_palette.scss"/>
|
||||
</xpath>
|
||||
@@ -297,7 +300,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="layout_logo_show" inherit_id="website.layout" customize_show="True" name="Show Logo">
|
||||
<template id="layout_logo_show" inherit_id="website.layout" name="Show Logo">
|
||||
<xpath expr="//header//a[hasclass('navbar-brand')]" position="replace">
|
||||
<a href="/" class="navbar-brand logo">
|
||||
<span t-field="res_company.logo" t-options="{'widget': 'image'}" role="img" t-att-aria-label="'Logo of %s' % res_company.name" t-att-title="res_company.name" />
|
||||
@@ -305,7 +308,7 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="affix_top_menu" inherit_id="website.layout" customize_show="True" name="Fixed Top Menu">
|
||||
<template id="affix_top_menu" inherit_id="website.layout" name="Fixed Top Menu">
|
||||
<xpath expr="//header" position="attributes">
|
||||
<attribute name="t-attf-class" add="#{'o_affix_enabled'}" separator=" "/>
|
||||
</xpath>
|
||||
@@ -317,10 +320,6 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<record id="portal.portal_show_sign_in" model="ir.ui.view">
|
||||
<field name="customize_show" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Features template -->
|
||||
<template id="login_layout" inherit_id="web.login_layout" name="Website Login Layout" priority="20">
|
||||
<xpath expr="t" position="replace">
|
||||
@@ -448,7 +447,7 @@
|
||||
-> string = tab's nav text
|
||||
-> title = tab's heading
|
||||
|
||||
Allowed tags in the <content/> elements:
|
||||
In the <content/> elements:
|
||||
|
||||
<opt>: Declares a new toggle option
|
||||
-> id (optional) = an ID to associate to the option
|
||||
@@ -476,12 +475,61 @@
|
||||
<list>: Declares a subset of options showed as a list
|
||||
-> string = list's title
|
||||
-> data-col (optional, default 6) = bootstrap column size for the list
|
||||
|
||||
Other elements will be ignored and their children will be processed as if
|
||||
that element was omitted, except that those children will be considered as
|
||||
in a new set of option. That allows to wrap an option in a fake element to
|
||||
indicate it is a standalone option for example.
|
||||
|
||||
Note: if an option is the only one of its group, a toggle element will be
|
||||
rendered.
|
||||
-->
|
||||
<template id="theme_customize">
|
||||
<div>
|
||||
<!-- Color options -->
|
||||
<content id="theme_customize_content_colors" string="Colors" title="Choose the theme colors">
|
||||
<opt data-widget="color_palette" data-col="12"/>
|
||||
<list string="Main colors">
|
||||
<opt data-widget="color" data-color-type="theme" data-color="primary" string="Primary"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="secondary" string="Secondary"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="alpha" string="Primary"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="beta" string="Secondary"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="gamma" string="Extra Color"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="delta" string="Extra Color"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="epsilon" string="Extra Color"/>
|
||||
</list>
|
||||
<list string="Status colors">
|
||||
<opt data-widget="color" data-color-type="theme" data-color="success" string="Success"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="info" string="Info"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="warning" string="Warning"/>
|
||||
<opt data-widget="color" data-color-type="theme" data-color="danger" string="Error"/>
|
||||
</list>
|
||||
<list string="Text colors">
|
||||
<opt data-widget="color" data-color="text" string="Text"/>
|
||||
<opt data-widget="color" data-color="h1" string="Headings 1"/>
|
||||
<opt data-widget="color" data-color="h2" string="Headings 2"/>
|
||||
<opt data-widget="color" data-color="h3" string="Headings 3"/>
|
||||
<opt data-widget="color" data-color="h4" string="Headings 4"/>
|
||||
<opt data-widget="color" data-color="h5" string="Headings 5"/>
|
||||
<opt data-widget="color" data-color="h6" string="Headings 6"/>
|
||||
</list>
|
||||
<list string="Background colors">
|
||||
<opt data-widget="color" data-color="body" string="Body"/>
|
||||
<opt data-widget="color" data-color="menu" string="Menus"/>
|
||||
<opt data-widget="color" data-color="footer" string="Footer"/>
|
||||
</list>
|
||||
</content>
|
||||
|
||||
<!-- Menu options -->
|
||||
<content id="theme_customize_content_navbar" string="Nav Bar" title="Choose your nav bar">
|
||||
<list string="Main Layout">
|
||||
<checkbox><opt data-xmlid="website.affix_top_menu" data-reload="/"/></checkbox>
|
||||
<checkbox><opt data-xmlid="portal.portal_show_sign_in" data-reload="/"/></checkbox>
|
||||
<opt data-widget="input" data-unit="rem" data-value="header-font-size" string="Font Size"/>
|
||||
</list>
|
||||
<list string="Logo">
|
||||
<checkbox><opt data-xmlid="website.layout_logo_show" data-reload="/"/></checkbox>
|
||||
<opt data-widget="input" data-unit="rem" data-value="logo-height" string="Logo Height"/>
|
||||
</list>
|
||||
</content>
|
||||
|
||||
<!-- Layout Options -->
|
||||
|
||||
Reference in New Issue
Block a user