summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web_editor/static/src')
-rw-r--r--addons/web_editor/static/src/js/ace.js416
-rw-r--r--addons/web_editor/static/src/less/web_editor.ui.components.less7
-rw-r--r--addons/web_editor/static/src/xml/ace.xml25
3 files changed, 342 insertions, 106 deletions
diff --git a/addons/web_editor/static/src/js/ace.js b/addons/web_editor/static/src/js/ace.js
index bbcc4fa4f74..7367c33eaa6 100644
--- a/addons/web_editor/static/src/js/ace.js
+++ b/addons/web_editor/static/src/js/ace.js
@@ -13,57 +13,103 @@ var _t = core._t;
ajax.loadXML('/web_editor/static/src/xml/ace.xml', qweb);
+function _getCheckReturn(isValid, errorLine, errorMessage) {
+ return {
+ isValid: isValid,
+ error: isValid ? null : {
+ line: errorLine,
+ message: errorMessage,
+ },
+ };
+}
+
function checkXML(xml) {
if (typeof window.DOMParser != "undefined") {
var xmlDoc = (new window.DOMParser()).parseFromString(xml, "text/xml");
var error = xmlDoc.getElementsByTagName("parsererror");
if (error.length > 0) {
- return {
- isValid: false,
- error: {
- line: parseInt(error[0].innerHTML.match(/[Ll]ine[^\d]+(\d+)/)[1], 10),
- message: error[0].innerHTML
- }
- };
+ return _getCheckReturn(false, parseInt(error[0].innerHTML.match(/[Ll]ine[^\d]+(\d+)/)[1], 10), error[0].innerHTML);
}
} else if (typeof window.ActiveXObject != "undefined" && new window.ActiveXObject("Microsoft.XMLDOM")) {
var xmlDocIE = new window.ActiveXObject("Microsoft.XMLDOM");
xmlDocIE.async = "false";
xmlDocIE.loadXML(xml);
if (xmlDocIE.parseError.line > 0) {
- return {
- isValid: false,
- error: {
- line: xmlDocIE.parseError.line,
- message: xmlDocIE.parseError.reason
- }
- };
+ return _getCheckReturn(false, xmlDocIE.parseError.line, xmlDocIE.parseError.reason);
}
}
-
- return {
- isValid: true
- };
+ return _getCheckReturn(true);
}
function formatXML(xml) {
return window.vkbeautify.xml(xml, 4);
}
+var checkLESS = (function () {
+ var mapping = {
+ '{': '}', '}': '{',
+ '(': ')', ')': '(',
+ '[': ']', ']': '[',
+ };
+ var openings = ['{', '(', '['];
+ var closings = ['}', ')', ']'];
+
+ return function (less) {
+ var stack = [];
+ var line = 1;
+ for (var i = 0 ; i < less.length ; i++) {
+ if (_.contains(openings, less[i])) {
+ stack.push(less[i]);
+ } else if (_.contains(closings, less[i])) {
+ if (stack.pop() !== mapping[less[i]]) {
+ return _getCheckReturn(false, line, _t("Unexpected ") + less[i]);
+ }
+ } else if (less[i] === '\n') {
+ line++;
+ }
+ }
+ if (stack.length > 0) {
+ return _getCheckReturn(false, line, _t("Expected ") + mapping[stack.pop()]);
+ }
+ return _getCheckReturn(true);
+ };
+})();
+
+function formatLESS(less) {
+ return less;
+}
+
/**
* The ViewEditor Widget allow to visualize resources (by default, XML views) and edit them.
*/
var ViewEditor = Widget.extend({
template: 'web_editor.ace_view_editor',
events: {
- 'change #ace-view-list': function () {
- this.displayView(this.selectedViewId());
+ "click .o_ace_type_switcher_choice": function (e) {
+ e.preventDefault();
+ this.switchType($(e.target).data("type"));
+ },
+ 'change .oe_view_list': function () {
+ this.displayResource(this.selectedResource());
},
'click .js_include_bundles': function (e) {
this.options.includeBundles = $(e.target).prop("checked");
this.loadResources().then(this._updateViewSelectDOM.bind(this));
},
+ 'click .js_include_all_less': function (e) {
+ this.options.includeAllLess = $(e.target).prop("checked");
+ this.loadResources().then(this._updateViewSelectDOM.bind(this));
+ },
'click button[data-action=save]': 'saveResources',
+ "click button[data-action=\"reset\"]": function () {
+ var self = this;
+ Dialog.confirm(this, _t("If you reset this file, all your customizations will be lost as it will be reverted to the default file."), {
+ title: _t("Careful !"),
+ confirm_callback: function () {
+ self.resetResource(self.selectedResource());
+ },
+ });
+ },
'click button[data-action=format]': 'formatResource',
'click button[data-action=close]': 'do_hide',
},
@@ -74,7 +120,7 @@ var ViewEditor = Widget.extend({
* @param viewKey: xml_id of the view whose linked resources are to be loaded.
Also allow to receive the id directly.
* @param options: an object containing some options
- * - initialViewID: a specific view ID to load on start (otherwise the main view ID
+ * - initialResID: a specific view ID to load on start (otherwise the main view ID
* associated with the specified viewKey will be used).
* - includeBundles: whether or not the assets bundles templates needs to be loaded.
*/
@@ -82,10 +128,22 @@ var ViewEditor = Widget.extend({
this._super.apply(this, arguments);
this.viewKey = viewKey;
+ this.options = _.defaults({}, options, {
+ position: 'right',
+ doNotLoadViews: false,
+ doNotLoadLess: false,
+ includeBundles: false,
+ includeAllLess: false,
+ defaultBundlesRestriction: [],
+ });
+
+ this.resources = {xml: {}, less: {}};
+ this.editingSessions = {xml: {}, less: {}};
+ this.currentType = "xml";
- this.options = _.defaults({}, options, {position: 'right'});
- this.views = {};
- this.buffers = {};
+ // Alias
+ this.views = this.resources.xml;
+ this.less = this.resources.less;
},
/**
* The willStart method is in charge of loading everything the ace library needs to work.
@@ -95,6 +153,7 @@ var ViewEditor = Widget.extend({
var js_def = ajax.loadJS('/web/static/lib/ace/ace.odoo-custom.js').then(function () {
return $.when(
ajax.loadJS('/web/static/lib/ace/mode-xml.js'),
+ ajax.loadJS('/web/static/lib/ace/mode-less.js'),
ajax.loadJS('/web/static/lib/ace/theme-monokai.js')
);
});
@@ -106,10 +165,22 @@ var ViewEditor = Widget.extend({
* @return a deferred which is resolved when the widget DOM content is fully loaded.
*/
start: function () {
- this.$viewEditor = this.$('#ace-view-editor');
+ this.$viewEditor = this.$("#ace-view-editor");
this.$editor = this.$(".ace_editor");
- this.$viewList = this.$('#ace-view-list');
- this.$viewID = this.$('#ace-view-id');
+
+ this.$typeSwitcherChoices = this.$(".o_ace_type_switcher_choice");
+ this.$typeSwitcherBtn = this.$(".o_ace_type_switcher > .dropdown-toggle");
+
+ this.$lists = {
+ xml: this.$("#ace-view-list"),
+ less: this.$("#ace-less-list")
+ };
+ this.$includeBundlesArea = this.$(".oe_include_bundles");
+ this.$includeAllLessArea = this.$(".o_include_all_less");
+ this.$viewID = this.$("#ace-view-id > span");
+
+ this.$formatButton = this.$("button[data-action=\"format\"]");
+ this.$resetButton = this.$("button[data-action=\"reset\"]");
this.aceEditor = window.ace.edit(this.$viewEditor[0]);
this.aceEditor.setTheme("ace/theme/monokai");
@@ -120,10 +191,32 @@ var ViewEditor = Widget.extend({
var debounceStoreEditorWidth = _.debounce(storeEditorWidth, 500);
this._updateViewSelectDOM();
- this.displayView(
- this.options.initialViewID
- || (typeof this.viewKey === "number" ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id)
- );
+
+ var initResID;
+ var initType;
+ if (this.options.initialResID) {
+ initResID = this.options.initialResID;
+ initType = (_.isString(initResID) && initResID[0] === '/') ? "less" : "xml";
+ } else {
+ if (!this.options.doNotLoadLess) {
+ initResID = this.sorted_less[0][1][0].url; // first bundle, less files, first one
+ initType = "less";
+ }
+ if (!this.options.doNotLoadViews) {
+ initResID = (typeof this.viewKey === "number" ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id);
+ initType = "xml";
+ }
+ }
+ if (initResID) {
+ this.displayResource(initResID, initType);
+ }
+
+ if (!this.sorted_views.length || !this.sorted_less.length) {
+ _.defer((function () {
+ this.switchType(this.sorted_views.length ? "xml" : "less");
+ this.$typeSwitcherBtn.parent(".btn-group").addClass("hidden");
+ }).bind(this));
+ }
$(document).on("mouseup.ViewEditor", stopResizing.bind(this)).on("mousemove.ViewEditor", updateWidth.bind(this));
if (this.options.position === 'left') {
@@ -186,14 +279,29 @@ var ViewEditor = Widget.extend({
* @return a deferred which is resolved once everything is loaded and processed.
*/
loadResources: function () {
+ // Reset resources
+ this.resources = {xml: {}, less: {}};
+ this.editingSessions = {xml: {}, less: {}};
+ this.views = this.resources.xml;
+ this.less = this.resources.less;
+
+ // Load resources
return ajax.jsonRpc("/web_editor/get_assets_editor_resources", "call", {
key: this.viewKey,
- bundles: this.options.includeBundles
- }).then((function (views) {
+ get_views: !this.options.doNotLoadViews,
+ get_less: !this.options.doNotLoadLess,
+ bundles: this.options.includeBundles,
+ bundles_restriction: this.options.includeAllLess ? [] : this.options.defaultBundlesRestriction,
+ }).then((function (resources) {
+ _process_views.call(this, resources.views);
+ _process_less.call(this, resources.less);
+ }).bind(this));
+
+ function _process_views(views) {
// Only keep the active views and index them by ID.
- this.views = _.indexBy(_.filter(views, function (view) {
+ _.extend(this.views, _.indexBy(_.filter(views, function (view) {
return view.active;
- }), "id");
+ }), "id"));
// Initialize a 0 level for each view and assign them an array containing their children.
var self = this;
@@ -225,63 +333,133 @@ var ViewEditor = Widget.extend({
_.each(roots, function (root) {
visit(root, 0);
});
- }).bind(this));
+ }
+
+ function _process_less(less) {
+ // The received less data is already sorted by bundle and DOM order
+ this.sorted_less = less;
+
+ // Store the URL ungrouped by bundle and use the URL as key (resource ID)
+ var self = this;
+ _.each(less, function (bundleInfos) {
+ _.each(bundleInfos[1], function (info) { info.bundle_xmlid = bundleInfos[0].xmlid; });
+ _.extend(self.less, _.indexBy(bundleInfos[1], "url"));
+ });
+ }
},
/**
- * The private _updateViewSelectDOM method purpose is to render the content of the view
+ * The private _updateViewSelectDOM method purpose is to render the content of the view/file
* select DOM element according to current widget data.
*/
_updateViewSelectDOM: function () {
- var currentId = this.selectedViewId();
+ var currentId = this.selectedResource();
var self = this;
- this.$viewList.empty();
+ this.$lists.xml.empty();
_.each(this.sorted_views, function (view) {
- self.$viewList.append($("<option/>", {
+ self.$lists.xml.append($("<option/>", {
value: view.id,
text: _.str.sprintf("%s%s", _.str.repeat("- ", view.level), view.name),
- selected: currentId === view.id
+ selected: currentId === view.id,
}));
});
+
+ this.$lists.less.empty();
+ _.each(this.sorted_less, function (bundleInfos) {
+ var $optgroup = $("<optgroup/>", {
+ label: bundleInfos[0].name,
+ }).appendTo(self.$lists.less);
+ _.each(bundleInfos[1], function (lessInfo) {
+ $optgroup.append($("<option/>", {
+ value: lessInfo.url,
+ text: lessInfo.url + (lessInfo.customized ? _.str.sprintf(" (%s)", _t("customized")) : ""),
+ selected: currentId === lessInfo.url,
+ }));
+ });
+ });
+ },
+ /**
+ * The switchType method switches to the LESS or XML edition. Calling this method will adapt all DOM elements to
+ * keep the editor consistent.
+ * @param type: either "xml" or "less"
+ */
+ switchType: function (type) {
+ this.currentType = type;
+ this.$typeSwitcherBtn.html(this.$typeSwitcherChoices.filter("[data-type=\"" + type + "\"]").html());
+ _.each(this.$lists, function ($list, _type) { $list.toggleClass("hidden", type !== _type); });
+ this.$lists[type].change();
+
+ this.$includeBundlesArea.toggleClass("hidden", this.currentType === "less" || !session.debug);
+ this.$includeAllLessArea.toggleClass("hidden", this.currentType === "xml" || !session.debug || this.options.defaultBundlesRestriction.length === 0);
+ this.$formatButton.toggleClass("hidden", this.currentType === "less");
},
/**
- * The selectedViewId method returns the currently selected view id.
- * @return the currently selected view id.
+ * The selectedResource method returns the currently selected resource id (view ID or less file URL).
+ * @return the currently resource id (view ID or less file URL)
*/
- selectedViewId: function () {
- return parseInt(this.$viewList.val(), 10);
+ selectedResource: function () {
+ return this.$lists[this.currentType].val();
},
/**
- * The displayView method forces the view identified by its ID to be displayed in the editor.
- * The method will update the view select DOM element as well.
- * @param viewID: the ID of the view to display.
+ * The displayResource method forces the view/less file identified by its ID/URL to be displayed in the editor.
+ * The method will update the resource select DOM element as well.
+ * @param resID: the ID/URL of the view/less file to display
+ * @param type: the type of resource (either "xml" or "less")
*/
- displayView: function (viewID) {
- var editingSession = this.buffers[viewID];
+ displayResource: function (resID, type) {
+ if (type) this.switchType(type);
+
+ var editingSession = this.editingSessions[this.currentType][resID];
if (!editingSession) {
- editingSession = this._buildEditingSession(viewID);
+ editingSession = this.editingSessions[this.currentType][resID] = this._buildEditingSession(resID);
}
this.aceEditor.setSession(editingSession);
- this.$viewID.text(_.str.sprintf(_t("Template ID: %s"), this.views[viewID].xml_id));
- if (viewID !== this.selectedViewId()) {
- this.$viewList.val(viewID);
+ if (this.currentType === "xml") {
+ this.$viewID.text(_.str.sprintf(_t("Template ID: %s"), this.views[resID].xml_id));
+ } else {
+ this.$viewID.text(_.str.sprintf(_t("Less file: %s"), resID));
+ }
+ this.$lists[this.currentType].val(resID);
+
+ this.$resetButton.toggleClass("hidden", this.currentType === "xml" || !this.less[resID].customized);
+ },
+ /**
+ * The resetResource method forces the view/less file identified by its ID/URL to be reset to the way it was before
+ * the user started editing it. TODO view reset is not supported yet
+ * @param resID: the ID/URL of the view/less file to reset (default to the currently selected one)
+ * @param type: the type of the resource to reset (default to the currently selected one)
+ * @return a deferred which is resolved once the resource has been reset
+ */
+ resetResource: function (resID, type) {
+ resID = resID || this.selectedResource();
+ type = type || this.currentType;
+
+ if (this.currentType === "xml") {
+ return $.Defered().reject(_t("Reseting views is not supported yet")); // TODO
+ } else {
+ return ajax.jsonRpc("/web_editor/reset_less", "call", {
+ url: resID,
+ bundle_xmlid: this.less[resID].bundle_xmlid,
+ });
}
},
/**
- * The private _buildEditingSession method initialize a text editor for the specified view.
- * @param viewID: the ID of the view to display.
- * @return an ace.EditSession object linked to the specified viewID.
+ * The private _buildEditingSession method initializes a text editor for the specified resource.
+ * @param resID: the ID/URL of the view/less file whose text editor it will be
+ * @param type: the type of the given resource (default to the currently selected one)
+ * @return an ace.EditSession object linked to the specified resID.
*/
- _buildEditingSession: function(viewID) {
+ _buildEditingSession: function(resID, type) {
var self = this;
- var editingSession = this.buffers[viewID] = new window.ace.EditSession(this.views[viewID].arch);
+ type = type || this.currentType;
+ var editingSession = new window.ace.EditSession(this.resources[type][resID].arch);
editingSession.setUseWorker(false);
- editingSession.setMode("ace/mode/xml");
+ editingSession.setMode("ace/mode/" + (type || this.currentType));
editingSession.setUndoManager(new window.ace.UndoManager());
editingSession.on("change", function () {
_.defer(function () {
- self._toggleDirtyInfo(viewID);
+ self._toggleDirtyInfo(resID);
self._showErrorLine();
});
});
@@ -289,18 +467,21 @@ var ViewEditor = Widget.extend({
},
/**
* The private _toggleDirtyInfo method update the select option DOM element associated with
- * a particular viewID to indicate if the option is dirty or not.
- * @param viewID: the view id whose option has to be updated
+ * a particular resID to indicate if the option is dirty or not.
+ * @param resID: the ID/URL of the view/less file whose option has to be updated
+ * @param type: the type of the given resource (default to the currently selected one)
* @param isDirty: a boolean to indicate if the view is dirty or not ; default to content of UndoManager
*/
- _toggleDirtyInfo: function (viewID, isDirty) {
- if (!viewID || !this.buffers[viewID]) return;
+ _toggleDirtyInfo: function (resID, type, isDirty) {
+ type = type || this.currentType;
+
+ if (!resID || !this.editingSessions[type][resID]) return;
- var $option = this.$viewList.find("[value=" + viewID + "]");
+ var $option = this.$lists[type].find("[value='" + resID + "']");
var optionName = $option.text();
var dirtyMarker = " (" + _t("unsaved changes") + ")";
if (isDirty === undefined) {
- isDirty = this.buffers[viewID].getUndoManager().hasUndo();
+ isDirty = this.editingSessions[type][resID].getUndoManager().hasUndo();
}
if (isDirty && optionName.indexOf(dirtyMarker) < 0) {
$option.text(optionName + dirtyMarker);
@@ -310,21 +491,15 @@ var ViewEditor = Widget.extend({
},
/**
* The formatResource method formats the current resource being vizualized.
+ * TODO formatting LESS files is not supported yet.
*/
formatResource: function () {
- this._formatXML();
- },
- /**
- * The private _formatXML method formats the current resource assuming it is an XML document.
- * If the xml is not valid, it shows the error instead.
- */
- _formatXML: function () {
- var xml = this.aceEditor.getValue();
- var check = checkXML(xml);
+ var res = this.aceEditor.getValue();
+ var check = (this.currentType === "xml" ? checkXML : checkLESS)(res);
if (check.isValid) {
- this.aceEditor.setValue(formatXML(xml));
+ this.aceEditor.setValue((this.currentType === "xml" ? formatXML : formatLESS)(res));
} else {
- this._showErrorLine(check.error.line, check.error.message, this.selectedViewId());
+ this._showErrorLine(check.error.line, check.error.message, this.selectedResource());
}
},
/**
@@ -333,35 +508,47 @@ var ViewEditor = Widget.extend({
* @return a deferred whose status indicates if the save is finished or if an error occured.
*/
saveResources: function () {
- var dirtyBuffers = _.pick(this.buffers, function (session) {
- return session.getUndoManager().hasUndo();
- });
- var toSave = _.map(dirtyBuffers, function (editingSession, viewID) {
- return {
- id: parseInt(viewID, 10),
- text: editingSession.getValue(),
- };
- });
+ var toSave = {};
+ var errorFound = false;
+ _.each(this.editingSessions, (function (editingSessions, type) {
+ if (errorFound) return;
+
+ var dirtySessions = _.pick(editingSessions, function (session) {
+ return session.getUndoManager().hasUndo();
+ });
+ toSave[type] = _.map(dirtySessions, function (session, resID) {
+ return {
+ id: parseInt(resID, 10) || resID,
+ text: session.getValue(),
+ };
+ });
- this._showErrorLine();
- for (var i = 0 ; i < toSave.length ; i++) {
- var check = checkXML(toSave[i].text);
- if (!check.isValid) {
- this._showErrorLine(check.error.line, check.error.message, toSave[i].id);
- return $.Deferred().reject(toSave[i]);
+ this._showErrorLine();
+ for (var i = 0 ; i < toSave[type].length && !errorFound ; i++) {
+ var check = (type === "xml" ? checkXML : checkLESS)(toSave[type][i].text);
+ if (!check.isValid) {
+ this._showErrorLine(check.error.line, check.error.message, toSave[type][i].id, type);
+ errorFound = toSave[type][i];
+ }
}
- }
+ }).bind(this));
+ if (errorFound) return $.Deferred().reject(errorFound);
+
+ var defs = [];
+ _.each(toSave, (function (_toSave, type) {
+ defs = defs.concat(_.map(_toSave, (type === "xml" ? this._saveView : this._saveLess).bind(this)));
+ }).bind(this));
- return $.when.apply($, _.map(toSave, this._saveView.bind(this))).fail(function (session, error) {
+ return $.when.apply($, defs).fail((function (session, error) {
Dialog.alert(this, "", {
title: _t("Server error"),
$content: $("<div/>").html(
- _t("A server error occured. Please check you correctly signed in and that the XML you are saving is well-formed.")
+ _t("A server error occured. Please check you correctly signed in and that the file you are saving is well-formed.")
+ "<br/>"
+ error
)
});
- });
+ }).bind(this));
},
/**
* The private _saveView method is in charge of saving an unique XML view.
@@ -377,7 +564,29 @@ var ViewEditor = Widget.extend({
method: 'write',
args: [[session.id], {arch: session.text}, _.extend(base.get_context(), {lang: null})],
}).then(function () {
- self._toggleDirtyInfo(session.id, false);
+ self._toggleDirtyInfo(session.id, "xml", false);
+ def.resolve();
+ }, function (source, error) {
+ def.reject(session, error);
+ });
+
+ return def;
+ },
+ /**
+ * The private _saveLess method is in charge of saving an unique LESS file.
+ * @param session: an object which contains the "id" (url) and the "text" of the view to save.
+ * @return a deferred whose status indicates if the save is finished or if an error occured.
+ */
+ _saveLess: function (session) {
+ var def = $.Deferred();
+
+ var self = this;
+ ajax.jsonRpc("/web_editor/save_less", "call", {
+ url: session.id,
+ bundle_xmlid: this.less[session.id].bundle_xmlid,
+ content: session.text,
+ }).then(function () {
+ self._toggleDirtyInfo(session.id, "less", false);
def.resolve();
}, function (source, error) {
def.reject(session, error);
@@ -391,9 +600,10 @@ var ViewEditor = Widget.extend({
* number. If the _showErrorLine is called without argument, the effects are removed.
* @param line: the line number to highlight
* @param message: the message to show on click on the line number
- * @param viewID: the id of the view whose line is to highlight
+ * @param resID: the ID/URL of the view/less file whose line is to highlight
+ * @param type: the type of the given resource
*/
- _showErrorLine: function (line, message, viewID) {
+ _showErrorLine: function (line, message, resID, type) {
if (line === undefined || line <= 0) {
if (this.$errorLine) {
this.$errorLine.removeClass("o_error");
@@ -405,7 +615,9 @@ var ViewEditor = Widget.extend({
return;
}
- if (this.selectedViewId() === viewID) {
+ if (type) this.switchType(type);
+
+ if (this.selectedResource() === resID) {
__showErrorLine.call(this, line);
} else {
var onChangeSession = (function () {
@@ -413,7 +625,7 @@ var ViewEditor = Widget.extend({
_.delay(__showErrorLine.bind(this, line), 400);
}).bind(this);
this.aceEditor.on('changeSession', onChangeSession);
- this.displayView(viewID);
+ this.displayResource(resID, this.currentType);
}
function __showErrorLine(line) {
diff --git a/addons/web_editor/static/src/less/web_editor.ui.components.less b/addons/web_editor/static/src/less/web_editor.ui.components.less
index 9d40da2a9f3..709e9b03b2e 100644
--- a/addons/web_editor/static/src/less/web_editor.ui.components.less
+++ b/addons/web_editor/static/src/less/web_editor.ui.components.less
@@ -135,11 +135,16 @@
.o-justify-content(space-between);
padding: @grid-gutter-width/4;
+ > .o_ace_type_switcher > button::after {
+ .o-caret-down();
+ margin-left: 4px;
+ }
+
> * {
.o-flex(0, 0, auto);
margin: 0 @grid-gutter-width/4;
- &.oe_include_bundles {
+ &.o_include_option {
.o-flex-display();
.o-align-items(center);
font-size: 11px;
diff --git a/addons/web_editor/static/src/xml/ace.xml b/addons/web_editor/static/src/xml/ace.xml
index 3451418a94b..5974003f443 100644
--- a/addons/web_editor/static/src/xml/ace.xml
+++ b/addons/web_editor/static/src/xml/ace.xml
@@ -4,20 +4,39 @@
<t t-name="web_editor.ace_view_editor">
<div class="o_ace_view_editor">
<div class="form-inline o_ace_view_editor_title">
+ <div class="btn-group o_ace_type_switcher">
+ <button type="button" class="btn btn-sm btn-info dropdown-toggle" data-toggle="dropdown">XML (HTML)</button>
+ <ul class="dropdown-menu" role="menu">
+ <li><a href="#" class="o_ace_type_switcher_choice" data-type="xml">XML (HTML)</a></li>
+ <li><a href="#" class="o_ace_type_switcher_choice" data-type="less">LESS (CSS)</a></li>
+ </ul>
+ </div>
<select id="ace-view-list" class="form-control oe_view_list"/>
- <label class="oe_include_bundles">
+ <select id="ace-less-list" class="form-control oe_view_list hidden"/>
+ <label class="o_include_option oe_include_bundles">
<div class="o_checkbox">
<input type="checkbox" class="js_include_bundles"/><span/>
</div>
Include Asset Bundles
</label>
+ <label class="o_include_option o_include_all_less hidden">
+ <div class="o_checkbox">
+ <input type="checkbox" class="js_include_all_less" t-att-checked="widget.options.includeAllLess ? 'checked' : undefined"/><span/>
+ </div>
+ Include All LESS Files
+ </label>
<div class="o_button_section">
<button data-action="save" type="submit" class="btn btn-sm btn-primary">Save</button>
- <button data-action="format" type="button" class="btn btn-sm btn-default">Format</button>
<button data-action="close" type="button" class="btn btn-sm btn-default">Close</button>
</div>
</div>
- <div id="ace-view-id"/>
+ <div id="ace-view-id">
+ <span/>
+ <div class="pull-right">
+ <button data-action="reset" type="button" class="btn btn-xs btn-danger"><i class="fa fa-undo"/> Reset</button>
+ <button data-action="format" type="button" class="btn btn-xs btn-link">Format</button>
+ </div>
+ </div>
<div id="ace-view-editor"/>
</div>
</t>