diff options
| author | qsm-odoo <qsm@odoo.com> | 2016-10-19 17:41:10 +0200 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2016-11-08 14:18:28 +0100 |
| commit | 5a030db3eb77b6d80187f8ae1a64303b2f50391d (patch) | |
| tree | 24e1ed9fd9862f8a1a9d255489a0f7abb461b866 /addons/web_editor/static/src | |
| parent | 42226de46ea0cb2ff93f8e34af323fb1d805fce0 (diff) | |
[REF] website, web_editor: refactor ace editor
Stop using ugly code for ace editor.
* Really close the ace editor by changing its display property thanks
to the dedicated do_show and do_hide method.
* Make the common part of ace editors (website, web_studio) consistent
so that the extensions are really small.
* Do not do RPC to retrieve data already retrieved (now, there is only
one unique RPC when the editor starts an one RPC per view changed
on save).
* Fix born-dead code so that the website editor starts automatically
if the URL contains its associated hash and loads the correct view
* Better handle errors (previous behavior only worked partially and
only on chrome):
- Jump to the error line and make it red.
- Do not open a Dialog with mostly incomprehensible message but
allow to open it on click of the error marker.
- Do not save any changed file if one contains an error.
- Do not allow formatting a file if it contains an error (show the
error instead).
* Do not use different routes to get the same content (remove the
/website/customize_template_get route as it returns the same
content as /web_editor/customize_template_get route). Note: in
a further commit this route will be refactored.
* In a general way, use consistent method names and behaviors /
comment the code / lint files / ...
* Open the code to extensions (so that it may later load other data
than XML).
Note: this refactoring allowed to fix bugs in previous odoo versions
but this commit message will not detail them.
Diffstat (limited to 'addons/web_editor/static/src')
| -rw-r--r-- | addons/web_editor/static/src/js/ace.js | 609 | ||||
| -rw-r--r-- | addons/web_editor/static/src/less/web_editor.ui.components.less | 29 | ||||
| -rw-r--r-- | addons/web_editor/static/src/xml/ace.xml | 6 |
3 files changed, 370 insertions, 274 deletions
diff --git a/addons/web_editor/static/src/js/ace.js b/addons/web_editor/static/src/js/ace.js index 7bd6ec36ba2..8b4ee8d7b43 100644 --- a/addons/web_editor/static/src/js/ace.js +++ b/addons/web_editor/static/src/js/ace.js @@ -2,8 +2,8 @@ odoo.define('web_editor.ace', function (require) { 'use strict'; var ajax = require('web.ajax'); -var Class = require('web.Class'); var core = require('web.core'); +var Dialog = require("web.Dialog"); var Widget = require('web.Widget'); var base = require('web_editor.base'); var local_storage = require('web.local_storage'); @@ -13,329 +13,414 @@ var _t = core._t; ajax.loadXML('/web_editor/static/src/xml/ace.xml', qweb); -var XmlDocument = Class.extend({ - init: function (text) { - this.xml = text; - }, - isWellFormed: function () { - var error; - if (document.implementation.createDocument) { - // use try catch for ie - try { - var dom = new DOMParser().parseFromString(this.xml, "text/xml"); - error = dom.getElementsByTagName("parsererror"); - return error.length === 0 || $(error).text(); - } catch (e) {} +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 + } + }; } - if (window.ActiveXObject) { - // IE - var msDom = new ActiveXObject("Microsoft.XMLDOM"); - msDom.async = false; - msDom.loadXML(this.xml); - return !msDom.parseError.errorCode || msDom.parseError.reason + "\nline " + msDom.parseError.line; + } 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 true; - }, - format: function () { - return vkbeautify.xml(this.xml, 4); - }, -}); + } -var ViewOption = Widget.extend({ - template: 'web_editor.ace_view_option', - init: function (parent, options) { - this.view_id = options.id; - this.view_name = options.name; + return { + isValid: true + }; +} - var indent = _.str.repeat("- ", options.level); - this.view_name = _.str.sprintf("%s%s", indent, options.name); - this._super(parent); - }, -}); +function formatXML(xml) { + return window.vkbeautify.xml(xml, 4); +} +/** + * The ViewEditor Widget allow to visualize resources (by default, XML views) and edit them. + */ var ViewEditor = Widget.extend({ - resizing: false, - refX: 0, - minWidth: 40, template: 'web_editor.ace_view_editor', events: { - 'change #ace-view-list': 'displaySelectedView', - 'click .js_include_bundles': 'loadTemplates', - 'click button[data-action=save]': 'saveViews', - 'click button[data-action=format]': 'formatXml', - 'click button[data-action=close]': 'close', + 'change #ace-view-list': function () { + this.displayView(this.selectedViewId()); + }, + 'click .js_include_bundles': function (e) { + this.options.includeBundles = $(e.target).prop("checked"); + this.loadResources().then(this._updateViewSelectDOM.bind(this)); + }, + 'click button[data-action=save]': 'saveResources', + 'click button[data-action=format]': 'formatResource', + 'click button[data-action=close]': 'do_hide', }, - init: function (parent) { - this.buffers = {}; + /** + * The init method should initialize the parameters of which information the ace editor will + * have to load. + * @param parent: the parent element of the editor widget. + * @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 + * associated with the specified viewKey will be used). + * - includeBundles: whether or not the assets bundles templates needs to be loaded. + */ + init: function (parent, viewKey, options) { + this._super.apply(this, arguments); + + this.viewKey = viewKey; + this.options = options || {}; + this.views = {}; - this._super(parent); + this.buffers = {}; }, + /** + * The willStart method is in charge of loading everything the ace library needs to work. + * It also loads the resources to visualize. See @loadResources. + */ + willStart: function () { + 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/theme-monokai.js') + ); + }); + return $.when(this._super.apply(this, arguments), js_def, this.loadResources()); + }, + /** + * The start method is in charge of initializing the library and initial view once the DOM is + * ready. It also initializes the resize feature of the ace editor. + * @return a deferred which is resolved when the widget DOM content is fully loaded. + */ start: function () { - var self = this; - self.aceEditor = ace.edit(self.$('#ace-view-editor')[0]); - self.aceEditor.setTheme("ace/theme/monokai"); - self.loadTemplates(); + this.$viewEditor = this.$('#ace-view-editor'); + this.$editor = this.$(".ace_editor"); + this.$viewList = this.$('#ace-view-list'); + this.$viewID = this.$('#ace-view-id'); + + this.aceEditor = window.ace.edit(this.$viewEditor[0]); + this.aceEditor.setTheme("ace/theme/monokai"); + + var refX = 0; + var resizing = false; + var minWidth = 400; + + this._updateViewSelectDOM(); + this.displayView( + this.options.initialViewID + || (typeof this.viewKey === "number" ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id) + ); - var $editor = self.$('.ace_editor'); - function resizeEditor (target) { - var width = Math.min(document.body.clientWidth, Math.max(parseInt(target, 10), self.minWidth)); - $editor.width(width); - self.aceEditor.resize(); - self.$el.width(width); + $(document).on("mouseup.ViewEditor", stopResizing.bind(this)).on("mousemove.ViewEditor", updateWidth.bind(this)); + this.$el.on("mousedown.ViewEditor", ".ace_gutter", startResizing.bind(this)); + + resizeEditor.call(this, readEditorWidth.call(this)); + + return this._super.apply(this, arguments); + + function resizeEditor(target) { + var width = Math.min(document.body.clientWidth, Math.max(parseInt(target, 10), minWidth)); + this.$editor.width(width); + this.aceEditor.resize(); + this.$el.width(width); } function storeEditorWidth() { - local_storage.setItem('ace_editor_width', self.$el.width()); + local_storage.setItem('ace_editor_width', this.$el.width()); } function readEditorWidth() { var width = local_storage.getItem('ace_editor_width'); return parseInt(width || 720, 10); } - function startResizing (e) { - self.refX = e.pageX; - self.resizing = true; - } - function stopResizing () { - self.resizing = false; + function startResizing(e) { + refX = e.pageX; + resizing = true; } - function updateWidth (e) { - if (self.resizing) { - var offset = e.pageX - self.refX; - var width = self.$el.width() - offset; - self.refX = e.pageX; - resizeEditor(width); - storeEditorWidth(); - } + function stopResizing() { + resizing = false; } - self.$('.ace_gutter').mouseup(stopResizing).mousedown(startResizing).click(stopResizing); - $(document).on("mousemove.ViewEditor", updateWidth).on("mouseup.ViewEditor", stopResizing); - $('button[data-action=edit]').click(function () { - self.close(); - }); - resizeEditor(readEditorWidth()); + function updateWidth(e) { + if (!resizing) return; - return this._super.apply(this, arguments); + var offset = e.pageX - refX; + var width = this.$el.width() - offset; + refX = e.pageX; + resizeEditor.call(this, width); + storeEditorWidth.call(this); + } }, + /** + * The destroy method unbinds custom events binded to the document element. + */ destroy: function () { this._super.apply(this, arguments); + this.$el.off(".ViewEditor"); $(document).off(".ViewEditor"); }, + /** + * The loadResources method is in charge of loading data the ace editor will vizualize + * and of processing them. + * Default behavior is loading the activate views, index them and build their hierarchy. + * @return a deferred which is resolved once everything is loaded and processed. + */ + loadResources: function () { + return ajax.jsonRpc('/web_editor/customize_template_get', 'call', { + key: this.viewKey, + full: true, + bundles: this.options.includeBundles + }).then((function (views) { + // Only keep the active views and index them by ID. + this.views = _.indexBy(_.filter(views, function (view) { + return view.active; + }), "id"); - // The 4 following methods are meant to be extended depending on the context - open: function () { - this.$el.removeClass('oe_ace_closed').addClass('oe_ace_open'); - }, - close: function () { - this.$el.removeClass('oe_ace_open').addClass('oe_ace_closed'); - }, - reloadPage: function () {}, - loadTemplates: function () {}, - - loadViews: function (views) { - var $viewList = this.$('#ace-view-list').empty(); - var viewGraph = this.buildViewGraph(views); - _(viewGraph).each(function (view) { - if (!view.id) { return; } + // Initialize a 0 level for each view and assign them an array containing their children. + var self = this; + var roots = []; + _.each(this.views, function (view) { + view.level = 0; + view.children = []; + }); + _.each(this.views, function (view) { + var parentId = view.inherit_id[0]; + var parent = parentId && self.views[parentId]; + if (parent) { + parent.children.push(view); + } else { + roots.push(view); + } + }); - this.views[view.id] = view; - new ViewOption(this, view).appendTo($viewList); - }.bind(this)); - return this.loadView(viewGraph[0].id); - }, - buildViewGraph: function (views) { - var activeViews = _.uniq(_.filter(views, function (view) { - return view.active; - }), false, function (view) { - return view.id; - }); - var index = {}; - var roots = []; - _.each(activeViews, function (view) { - index[view.id] = view; - view.children = []; - }); - _.each(index, function (view) { - var parentId = view.inherit_id[0]; - if (parentId && index[parentId]) { - index[parentId].children.push(view); - } else { - roots.push(view); + // Assign the correct level based on children key and save a sorted array where + // each view is followed by their children. + this.sorted_views = []; + function visit(view, level) { + view.level = level; + self.sorted_views.push(view); + _.each(view.children, function (child) { + visit(child, level + 1); + }); } - }); - var result = []; - function visit (node, level) { - node.level = level; - result.push(node); - _.each(node.children, function (child) { - visit(child, level + 1); + _.each(roots, function (root) { + visit(root, 0); }); - } - _.each(roots, function (node) { - visit(node, 0); - }); - return result; + }).bind(this)); }, - loadView: function (id) { - var viewId = parseInt(id, 10); + /** + * The private _updateViewSelectDOM method purpose is to render the content of the view + * select DOM element according to current widget data. + */ + _updateViewSelectDOM: function () { + var currentId = this.selectedViewId(); + var self = this; - return ajax.jsonRpc('/web/dataset/call', 'call', { - model: 'ir.ui.view', - method: 'read', - args: [[viewId], ['arch'], _.extend(base.get_context(), {'lang': null})], - }).then(function (result) { - self._displayArch(result[0].arch, viewId); + this.$viewList.empty(); + _.each(this.sorted_views, function (view) { + self.$viewList.append($("<option/>", { + value: view.id, + text: _.str.sprintf("%s%s", _.str.repeat("- ", view.level), view.name), + selected: currentId === view.id + })); }); }, - _displayArch: function(arch, viewId) { + /** + * The selectedViewId method returns the currently selected view id. + * @return the currently selected view id. + */ + selectedViewId: function () { + return parseInt(this.$viewList.val(), 10); + }, + /** + * 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. + */ + displayView: function (viewID) { + var editingSession = this.buffers[viewID]; + if (!editingSession) { + editingSession = this._buildEditingSession(viewID); + } + 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); + } + }, + /** + * 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. + */ + _buildEditingSession: function(viewID) { var self = this; - var editingSession = this.buffers[viewId] = new ace.EditSession(arch); + var editingSession = this.buffers[viewID] = new window.ace.EditSession(this.views[viewID].arch); editingSession.setUseWorker(false); editingSession.setMode("ace/mode/xml"); - editingSession.setUndoManager(new ace.UndoManager()); + editingSession.setUndoManager(new window.ace.UndoManager()); editingSession.on("change", function () { - setTimeout(function () { - var $option = self.$('#ace-view-list').find('[value='+viewId+']'); - var bufferName = $option.text(); - var dirtyMarker = " (unsaved changes)"; - var isDirty = editingSession.getUndoManager().hasUndo(); - if (isDirty && bufferName.indexOf(dirtyMarker) < 0) { - $option.text(bufferName + dirtyMarker); - } else if (!isDirty && bufferName.indexOf(dirtyMarker) > 0) { - $option.text(bufferName.substring(0, bufferName.indexOf(dirtyMarker))); - } - }, 1); + _.defer(function () { + self._toggleDirtyInfo(viewID); + self._showErrorLine(); + }); }); - if (viewId === self.selectedViewId()) { - self.displayView.call(self, viewId); - } + return editingSession; }, - selectedViewId: function () { - return parseInt(this.$('#ace-view-list').val(), 10); - }, - displayView: function (id) { - var viewId = parseInt(id, 10); - var editingSession = this.buffers[viewId]; - if (editingSession) { - this.aceEditor.setSession(editingSession); - this.$('#ace-view-id').text(_.str.sprintf( - _t("Template ID: %s"), - this.views[viewId].xml_id)); + /** + * 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 + * @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; + + var $option = this.$viewList.find("[value=" + viewID + "]"); + var optionName = $option.text(); + var dirtyMarker = " (" + _t("unsaved changes") + ")"; + if (isDirty === undefined) { + isDirty = this.buffers[viewID].getUndoManager().hasUndo(); + } + if (isDirty && optionName.indexOf(dirtyMarker) < 0) { + $option.text(optionName + dirtyMarker); + } else if (!isDirty && optionName.indexOf(dirtyMarker) > 0) { + $option.text(optionName.substring(0, optionName.indexOf(dirtyMarker))); } }, - displaySelectedView: function () { - var self = this; - var viewID = this.selectedViewId(); - if (this.buffers[viewID]) { - this.displayView(viewID); - this.updateHash(); + /** + * The formatResource method formats the current resource being vizualized. + */ + 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); + if (check.isValid) { + this.aceEditor.setValue(formatXML(xml)); } else { - this.loadView(viewID).then(function() { - self.updateHash(); - }); + this._showErrorLine(check.error.line, check.error.message, this.selectedViewId()); } }, - formatXml: function () { - var xml = new XmlDocument(this.aceEditor.getValue()); - this.aceEditor.setValue(xml.format()); - }, - saveViews: function () { - var self = this; - var toSave = _.filter(_.map(self.buffers, function (editingSession, viewId) { + /** + * The saveResources method is in charge of saving every resource that has been modified. + * If one cannot be saved, none is saved and an error message is displayed. + * @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), - isDirty: editingSession.getUndoManager().hasUndo(), + id: parseInt(viewID, 10), text: editingSession.getValue(), }; - }), function (session) { - return session.isDirty; - }); - this.clearError(); - var requests = _.map(toSave, function (session) { - return self.saveView(session); }); - $.when.apply($, requests).then(function () { - self.reloadPage.call(self); - }).fail(function (source, session, error) { - self.displayError.call(self, source, session, error); + + 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]); + } + } + + return $.when.apply($, _.map(toSave, this._saveView.bind(this))).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.") + + "<br/>" + + error + ) + }); }); }, - saveView: function (session) { - var xml = new XmlDocument(session.text); - var isWellFormed = xml.isWellFormed(); + /** + * The private _saveView method is in charge of saving an unique XML view. + * @param session: an object which contains the "id" and the "text" of the view to save. + * @return a deferred whose status indicates if the save is finished or if an error occured. + */ + _saveView: function (session) { var def = $.Deferred(); - if (isWellFormed === true) { - ajax.jsonRpc('/web/dataset/call', 'call', { - model: 'ir.ui.view', - method: 'write', - args: [[session.id], { 'arch': xml.xml }, _.extend(base.get_context(), {'lang': null})], - }).then(function () { - def.resolve(); - }).fail(function (source, error) { - def.reject("server", session, error); - }); - } else { - def.reject(null, session, isWellFormed); - } + + var self = this; + ajax.jsonRpc('/web/dataset/call', 'call', { + model: 'ir.ui.view', + method: 'write', + args: [[session.id], {arch: session.text}, _.extend(base.get_context(), {lang: null})], + }).then(function () { + self._toggleDirtyInfo(session.id, false); + def.resolve(); + }, function (source, error) { + def.reject(session, error); + }); + return def; }, - clearError: function () { - this.$(".ace_layer.ace_text-layer .ace_line").css("background", ""); - }, - displayError: function (source, session, error) { - var self = this; - var line, test; - // format error message - var message = _.isString(error) ? error - : (error && error.data && error.data.arguments && error.data.arguments[0] === "Access Denied") ? "Access denied: please sign in" - : (error && error.data && error.data.message) ? error.data.message - : (error && error.message) ? error.message - : "Unexpected error"; - if (source == "server") { - message = eval(message.replace(/^\(/g, '([') - .replace(/\)$/g, '])') - .replace(/u'/g, "'") - .replace(/<([^>]+)>/g, '<b style="color:#661100;"><\$1></b>'))[1]; - line = -1; - } else { - line = message.match(/line ([0-9]+)/i); - line = line ? parseInt(line[1],10) : -1; - test = new RegExp("^\\s*"+line+"\\s*$"); + /** + * The private _showErrorLine method is designed to show a line which produced an error. Red color + * is added to the editor, the cursor move to the line and a message is opened on click on the line + * 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 + */ + _showErrorLine: function (line, message, viewID) { + if (line === undefined || line <= 0) { + if (this.$errorLine) { + this.$errorLine.removeClass("o_error"); + this.$errorLine.off(".o_error"); + this.$errorLine = undefined; + this.$errorContent.removeClass("o_error"); + this.$errorContent = undefined; + } + return; } - function gotoline() { - self.aceEditor.gotoLine(line); - setTimeout(function () { - var $lines = self.$(".ace_editor .ace_gutter .ace_gutter-cell"); - var index = $lines.filter(function () { - return test.test($(this).text()); - }).index(); - if (index>0) { - self.$(".ace_layer.ace_text-layer .ace_line:eq(" + index + ")").css("background", "#661100"); - } - },100); - } - function onchangeSession () { - self.aceEditor.off('changeSession', onchangeSession); - gotoline(); + if (this.selectedViewId() === viewID) { + __showErrorLine.call(this, line); + } else { + var onChangeSession = (function () { + this.aceEditor.off("changeSession", onChangeSession); + _.delay(__showErrorLine.bind(this, line), 400); + }).bind(this); + this.aceEditor.on('changeSession', onChangeSession); + this.displayView(viewID); } - var $list = this.$("#ace-view-list"); - if (+$list.val() == session.id) { - if (line>-1) gotoline(); - } else { - if (line) self.aceEditor.on('changeSession', onchangeSession); - this.$("#ace-view-list").val(session.id).change(); + function __showErrorLine(line) { + this.aceEditor.gotoLine(line); + this.$errorLine = this.$viewEditor.find(".ace_gutter-cell").filter(function () { + return parseInt($(this).text()) === line; + }).addClass("o_error"); + this.$errorLine.addClass("o_error").on("click.o_error", function () { + var $message = $("<div/>").html(message); + $message.text($message.text()); + Dialog.alert(this, "", {$content: $message}); + }); + this.$errorContent = this.$viewEditor.find(".ace_scroller").addClass("o_error"); } - return { - title: session.text.match(/\s+name=['"]([^'"]+)['"]/i)[1], - message: "<b>Malformed XML document</b>:<br/>" + message - }; }, }); -return { - XmlDocument: XmlDocument, - ViewOption: ViewOption, - ViewEditor: ViewEditor, -}; - +return ViewEditor; }); 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 44f3923c35e..de2c49ea5d2 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 @@ -126,6 +126,7 @@ color: white; .o-flex-display(); .o-flex-flow(column, nowrap); + opacity: 0.97; .o_ace_view_editor_title { .o-flex(0, 0, auto); @@ -164,15 +165,29 @@ .o-flex(1, 1, auto); .ace_gutter { cursor: ew-resize; + + .ace_gutter-cell.o_error { + position: relative; + &::after { + .o-position-absolute(-100%, 0, -100%, 0); + .ace-line-error-mixin(); + } + } + } + .ace_scroller.o_error::after { + .o-position-absolute(0, auto, 0, 0); + width: 3px; + .ace-line-error-mixin(); } - } - &.oe_ace_open { - opacity: 0.97; - } - &.oe_ace_closed { - z-index: -1000; - opacity: 0; + .ace-line-error-mixin() { + content: ""; + z-index: 1000; + display: block; + background-color: @brand-danger; + opacity: 0.5; + pointer-events: none; + } } } diff --git a/addons/web_editor/static/src/xml/ace.xml b/addons/web_editor/static/src/xml/ace.xml index d4faf260c00..3451418a94b 100644 --- a/addons/web_editor/static/src/xml/ace.xml +++ b/addons/web_editor/static/src/xml/ace.xml @@ -2,7 +2,7 @@ <templates> <t t-name="web_editor.ace_view_editor"> - <div class="o_ace_view_editor oe_ace_closed"> + <div class="o_ace_view_editor"> <div class="form-inline o_ace_view_editor_title"> <select id="ace-view-list" class="form-control oe_view_list"/> <label class="oe_include_bundles"> @@ -22,8 +22,4 @@ </div> </t> -<t t-name="web_editor.ace_view_option"> - <option t-att-value="widget.view_id"><t t-esc="widget.view_name"/></option> -</t> - </templates> |
