odoo.define('web_editor.ace', function (require) { 'use strict'; var ajax = require('web.ajax'); 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'); var session = require("web.session"); var qweb = core.qweb; 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 _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 _getCheckReturn(false, xmlDocIE.parseError.line, xmlDocIE.parseError.reason); } } 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: { "click .o_ace_type_switcher_choice": function (e) { e.preventDefault(); this.switchType($(e.target).data("type")); }, 'change .o_res_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', }, /** * 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 * - 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. */ init: function (parent, viewKey, options) { 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"; // 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. * 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/mode-less.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 () { this.$viewEditor = this.$("#ace-view-editor"); this.$editor = this.$(".ace_editor"); 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"); var refX = 0; var resizing = false; var minWidth = 400; var debounceStoreEditorWidth = _.debounce(storeEditorWidth, 500); this._updateViewSelectDOM(); 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') { this.$('.ace_scroller').after($('
').addClass('ace_resize_bar')); this.$('.ace_gutter').css({'cursor': 'default'}); this.$el.on("mousedown.ViewEditor", ".ace_resize_bar", startResizing.bind(this)); } else { 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', this.$el.width()); } function readEditorWidth() { var width = local_storage.getItem('ace_editor_width'); return parseInt(width || 720, 10); } function startResizing(e) { refX = e.pageX; resizing = true; } function stopResizing() { resizing = false; } function updateWidth(e) { if (!resizing) return; var offset = e.pageX - refX; if (this.options.position === 'left') { offset = - offset; } var width = this.$el.width() - offset; refX = e.pageX; resizeEditor.call(this, width); debounceStoreEditorWidth.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 () { // 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, 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. _.extend(this.views, _.indexBy(_.filter(views, function (view) { return view.active; }), "id")); // 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); } }); // 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); }); } _.each(roots, function (root) { visit(root, 0); }); } 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/file * select DOM element according to current widget data. */ _updateViewSelectDOM: function () { var currentId = this.selectedResource(); var self = this; this.$lists.xml.empty(); _.each(this.sorted_views, function (view) { self.$lists.xml.append($("", { label: bundleInfos[0].name, }).appendTo(self.$lists.less); _.each(bundleInfos[1], function (lessInfo) { var name = lessInfo.url.substring(_.lastIndexOf(lessInfo.url, "/") + 1, lessInfo.url.length - 5); $optgroup.append($("