diff --git a/addons/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js b/addons/web_editor/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js similarity index 100% rename from addons/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js rename to addons/web_editor/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js diff --git a/addons/web_editor/static/src/js/ace.js b/addons/web_editor/static/src/js/ace.js new file mode 100644 index 00000000000..cd20815f49c --- /dev/null +++ b/addons/web_editor/static/src/js/ace.js @@ -0,0 +1,320 @@ +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 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); + +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) {} + } + 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; + } + 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; + + var indent = _.str.repeat("- ", options.level); + this.view_name = _.str.sprintf("%s%s", indent, options.name); + this._super(parent); + }, +}); + +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', + }, + init: function (parent) { + this.buffers = {}; + this.views = {}; + this._super(parent); + }, + start: function () { + var self = this; + self.aceEditor = ace.edit(self.$('#ace-view-editor')[0]); + self.aceEditor.setTheme("ace/theme/monokai"); + self.loadTemplates(); + + 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); + } + function storeEditorWidth() { + local_storage.setItem('ace_editor_width', self.$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 updateWidth (e) { + if (self.resizing) { + var offset = e.pageX - self.refX; + var width = self.$el.width() - offset; + self.refX = e.pageX; + resizeEditor(width); + storeEditorWidth(); + } + } + document.body.addEventListener('mouseup', stopResizing, true); + self.$('.ace_gutter').mouseup(stopResizing).mousedown(startResizing).click(stopResizing); + $(document).mousemove(updateWidth); + $('button[data-action=edit]').click(function () { + self.close(); + }); + resizeEditor(readEditorWidth()); + }, + + // 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(); + _(this.buildViewGraph(views)).each(function (view) { + if (!view.id) { return; } + + this.views[view.id] = view; + new ViewOption(this, view).appendTo($viewList); + this.loadView(view.id); + }.bind(this)); + }, + 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; + if (parentId && index[parentId]) { + index[parentId].children.push(view); + } else { + roots.push(view); + } + }); + var result = []; + function visit (node, level) { + node.level = level; + result.push(node); + _.each(node.children, function (child) { + visit(child, level + 1); + }); + } + _.each(roots, function (node) { + visit(node, 0); + }); + return result; + }, + loadView: function (id) { + var viewId = parseInt(id, 10); + var self = this; + ajax.jsonRpc('/web/dataset/call', 'call', { + model: 'ir.ui.view', + method: 'read', + args: [[viewId], ['arch'], _.extend(base.get_context(), {'lang': null})], + }).then(function (result) { + var editingSession = self.buffers[viewId] = new ace.EditSession(result[0].arch); + editingSession.setMode("ace/mode/xml"); + editingSession.setUndoManager(new ace.UndoManager()); + editingSession.setUseWorker(false); + 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); + }); + if (viewId === self.selectedViewId()) { + self.displayView.call(self, viewId); + } + }); + }, + 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)); + } + }, + displaySelectedView: function () { + this.displayView(this.selectedViewId()); + this.updateHash(); + }, + 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) { + return { + id: parseInt(viewId, 10), + isDirty: editingSession.getUndoManager().hasUndo(), + 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); + }); + }, + saveView: function (session) { + var xml = new XmlDocument(session.text); + var isWellFormed = xml.isWellFormed(); + 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); + } + 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, '<\$1>'))[1]; + line = -1; + } else { + line = message.match(/line ([0-9]+)/i); + line = line ? parseInt(line[1],10) : -1; + test = new RegExp("^\\s*"+line+"\\s*$"); + } + + 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(); + } + + 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(); + } + return session.text.match(/\s+name=['"]([^'"]+)['"]/i)[1], "Malformed XML document:
" + message; + }, +}); + +return { + XmlDocument: XmlDocument, + ViewOption: ViewOption, + ViewEditor: ViewEditor, +}; + +}); diff --git a/addons/web_editor/static/src/xml/ace.xml b/addons/web_editor/static/src/xml/ace.xml new file mode 100644 index 00000000000..2f18f60731f --- /dev/null +++ b/addons/web_editor/static/src/xml/ace.xml @@ -0,0 +1,26 @@ + + + + +
+
+ + + + + +
+
+
+
+ + + + + + + diff --git a/addons/web_editor/views/editor.xml b/addons/web_editor/views/editor.xml index 056b2df4169..6c7f23f8822 100644 --- a/addons/web_editor/views/editor.xml +++ b/addons/web_editor/views/editor.xml @@ -60,6 +60,8 @@