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 qweb = core.qweb; var _t = core._t; ajax.loadXML('/web_editor/static/src/xml/ace.xml', qweb); 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 } }; } } 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 { isValid: true }; } 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({ template: 'web_editor.ace_view_editor', events: { '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', }, /** * 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 = _.defaults({}, options, {position: 'right'}); this.views = {}; 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 () { this.$viewEditor = this.$('#ace-view-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"); this.$editor = this.$(".ace_editor"); var refX = 0; var resizing = false; var minWidth = 400; var debounceStoreEditorWidth = _.debounce(storeEditorWidth, 500); this._updateViewSelectDOM(); var initResID; if (this.options.initialViewID) { initResID = this.options.initialViewID; } else { if (typeof this.viewKey === "number") { initResID = this.viewKey; } else { var view = _.findWhere(this.views, {xml_id: this.viewKey}); if (!view) { view = _.findWhere(this.views, {key: this.viewKey}); } initResID = view.id; } } this.displayView(initResID); $(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 () { 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"); // 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); }); }).bind(this)); }, /** * 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; this.$viewList.empty(); _.each(this.sorted_views, function (view) { self.$viewList.append($("