summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js/widgets
diff options
context:
space:
mode:
authorqsm-odoo <qsm@odoo.com>2017-06-29 16:38:34 +0200
committerqsm-odoo <qsm@odoo.com>2017-08-16 11:04:14 +0200
commit2972976962617d4b8a0113bae58c640ab41cdff8 (patch)
treec27d107b597dec85ad1ecd02553982dc0da657de /addons/web_editor/static/src/js/widgets
parent4f925af06fd3cebfcfb56eb947ab68c8a2ca4490 (diff)
[REF] web_editor, website, *: complete refactoring
* mass_mailing, payment, point_of_sale, portal, survey, web, web_tour, website_blog, website_crm_partner_assign, website_event, website_event_questions, website_form, website_forum, website_gengo, website_hr_recruitment, website_links, website_mail, website_mail_channel, website_mass_mailing, website_quote, website_sale, website_sale_options, website_slides, website_twitter This commit reviews the whole "JS side" of the web_editor and website apps. This is a first step to be able to improve them with new and better functionnalities; this commit is not supposed to change any visual behavior. The main goal was to achieve a structure similar to the backend one. Now, the frontend side also has a root widget (like the WebClient) and all other widgets are attached to it one way or another. This allows the benefits of using the 'trigger_up' functionnality for example. As RPC are now mainly done with the `this._rpc` functionnality (being possible thanks to the parent hierarchy), the frontend will also be possible to test thanks to QUnit in a future update (besides the "text" editor side which still requires a refactoring to be able to do that). --- Here are some of the changes: (-) conventions and documentation The code has been updated to follow JS conventions and a lot of code has been commented (around +2000 lines of comment). This also means that lots of functions have been renamed to use camelCase or simply to make their name understandable. See https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines. (-) deprecated: web_editor.base The "web_editor.base" module has been split and does not force the modules which require it to wait for DOM ready anymore. This was indeed slowing loading times, but also prevented to use some modules in some contexts (see the LESS editor use in web_studio which is the subject of another task). Now the "editor context" can be got thanks to the "web_editor.context" JS module with its "get" function. The "web_editor.base" module should probably not be used anymore (see its code and recent updates). (-) new: web.dom_ready If a JS module should wait for the DOM to be ready to be executed, a new JS module has been created: "web.dom_ready". This should always be used in a module which only want to instantiate stuff. Do not extend (or worst, include) classes after DOM ready. (-) website.website The "website.website" module has been split. "website.website" does not return anything useful anymore, it just initialize some miscellaneous stuff, without waiting for the DOM to be ready. You might want to check "website.utils", "website.content.compatibility" or `WebsiteRoot`. Also `website.form` has been deleted (use `this._rpc`), so has been `website.error`. `website.prompt` will be removed in a future update to be replaced by `Dialog.prompt`. (-) widgets are great Many classes which were not widgets are now widgets. This allows them to use the 'events', the 'xmlDependencies' and the 'this._rpc' features for example. Here are some of the main ones: - Snippet options: these were classes with a `$el` for the menu element and `$target` for the customized element. This is still the case but following standard `Widget` structure (one exception: using `this.$(...)` searches in the `$target` as before this update). - Snippet animations: instead of class instances with a `$target` element which can be `start` and `stop`, these are now standard widgets which can be `start` and `destroy`. `this.$target` is an alias to `this.$el` for ease of compatibility. - Snippet editors: instead of class instances in charge of an editor overlay, these are now widgets. Each "child" snippet editor is properly attached as a "child", which allows editors to communicate and to be properly destroyed. (-) root widgets and website navbar The frontend is different of the backend. In the backend, the page has an empty <body/> element and all the components are instantiated from parent to children (i.e. the `WebClient` is instantiated and is in charge of instantiating the `ControlPanel`, etc). The frontend cannot work like that on page loadings as they are way more frequent than in the backend and we do not want them to flicker. A frontend page is loaded as a <body/> element which already contains the website navbar and its menus and the whole content page. JS code has to be "attached" to these existing elements. This is possible thanks to the `RootWidget` instances and the specialized `WebsiteRoot`, `IframeRoot` and `WebsiteNavbar` (see code for details). (-) lazy loading No more (or at least a lot less) XML/JS has to be loaded on page loading, thanks to the use of the `Widget.xmlDependencies` feature. XML which have to be lazy loaded is loaded only on related Widget instantiation if necessary, which allows to execute a lot of JS code before the DOM is ready and to start many widgets on DOM ready (not later). A visual benefit of this is clicking on the 'edit' button as soon as it is possible: before this commit, this was sometimes not doing anything as event handlers were not binded yet. Still a possible exception: loading the session and locales. This may be asynchronous stuff which is still required before widget instantiations but this will be the subject of another task. (-) deprecated code and code location More than reviewing code and organizing it, many apparent dead code was removed. More importantly, mislocated code was put in the right app. This is the case for snippet animations which is a concept for website apps but was defined in the web_editor app, or some translation concepts which were part of website but should have been part of web_editor. --- There are probably more things to say about this commit but I will let the comments speak for those.
Diffstat (limited to 'addons/web_editor/static/src/js/widgets')
-rw-r--r--addons/web_editor/static/src/js/widgets/ace.js804
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js361
2 files changed, 656 insertions, 509 deletions
diff --git a/addons/web_editor/static/src/js/widgets/ace.js b/addons/web_editor/static/src/js/widgets/ace.js
index b1b28d78e83..a1fbd0753a6 100644
--- a/addons/web_editor/static/src/js/widgets/ace.js
+++ b/addons/web_editor/static/src/js/widgets/ace.js
@@ -3,17 +3,22 @@ odoo.define('web_editor.ace', function (require) {
var ajax = require('web.ajax');
var core = require('web.core');
-var Dialog = require("web.Dialog");
+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 weContext = require('web_editor.context');
+var localStorage = 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);
-
+/**
+ * Formats a content-check result (@see checkXML, checkLESS).
+ *
+ * @param {boolean} isValid
+ * @param {integer} [errorLine] needed if isValid is false
+ * @param {string} [errorMessage] needed if isValid is false
+ * @returns {Object}
+ */
function _getCheckReturn(isValid, errorLine, errorMessage) {
return {
isValid: isValid,
@@ -23,17 +28,22 @@ function _getCheckReturn(isValid, errorLine, errorMessage) {
},
};
}
-
+/**
+ * Checks the syntax validity of some XML.
+ *
+ * @param {string} xml
+ * @returns {Object} @see _getCheckReturn
+ */
function checkXML(xml) {
- if (typeof window.DOMParser != "undefined") {
- var xmlDoc = (new window.DOMParser()).parseFromString(xml, "text/xml");
- var error = xmlDoc.getElementsByTagName("parsererror");
+ 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";
+ } 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);
@@ -41,11 +51,21 @@ function checkXML(xml) {
}
return _getCheckReturn(true);
}
-
+/**
+ * Formats some XML so that it has proper indentation and structure.
+ *
+ * @param {string} xml
+ * @returns {string} formatted xml
+ */
function formatXML(xml) {
return window.vkbeautify.xml(xml, 4);
}
-
+/**
+ * Checks the syntax validity of some LESS.
+ *
+ * @param {string} less
+ * @returns {Object} @see _getCheckReturn
+ */
var checkLESS = (function () {
var mapping = {
'{': '}', '}': '{',
@@ -75,55 +95,52 @@ var checkLESS = (function () {
return _getCheckReturn(true);
};
})();
-
+/**
+ * Formats some LESS so that it has proper indentation and structure.
+ *
+ * @todo Right now, this does return the given LESS content, untouched.
+ * @param {string} less
+ * @returns {string} formatted less
+ */
function formatLESS(less) {
return less;
}
/**
- * The ViewEditor Widget allow to visualize resources (by default, XML views) and edit them.
+ * Allows to visualize resources (by default, XML views) and edit them.
*/
var ViewEditor = Widget.extend({
template: 'web_editor.ace_view_editor',
+ xmlDependencies: ['/web_editor/static/src/xml/ace.xml'],
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',
+ 'click .o_ace_type_switcher_choice': '_onTypeChoice',
+ 'change .o_res_list': '_onResChange',
+ 'click .js_include_bundles': '_onIncludeBundlesChange',
+ 'click .js_include_all_less': '_onIncludeAllLessChange',
+ 'click button[data-action=save]': '_onSaveClick',
+ 'click button[data-action=reset]': '_onResetClick',
+ 'click button[data-action=format]': '_onFormatClick',
+ 'click button[data-action=close]': '_onCloseClick',
},
+
/**
- * 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.
+ * Initializes the parameters so that the ace editor knows which information
+ * it has to load.
+ *
+ * @constructor
+ * @param {Widget} parent
+ * @param {string|integer} viewKey
+ * xml_id or id of the view whose linked resources have to be loaded.
+ * @param {Object} [options]
+ * @param {string|integer} [options.initialResID]
+ * a specific view ID / LESS URL to load on start (otherwise the main
+ * view ID associated with the specified viewKey will be used)
+ * @param {string} [options.position=right]
+ * @param {boolean} [options.doNotLoadViews=false]
+ * @param {boolean} [options.doNotLoadLess=false]
+ * @param {boolean} [options.includeBundles=false]
+ * @param {boolean} [options.includeAllLess=false]
+ * @param {string[]} [options.defaultBundlesRestriction]
*/
init: function (parent, viewKey, options) {
this._super.apply(this, arguments);
@@ -140,15 +157,17 @@ var ViewEditor = Widget.extend({
this.resources = {xml: {}, less: {}};
this.editingSessions = {xml: {}, less: {}};
- this.currentType = "xml";
+ 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.
+ * Loads everything the ace library needs to work.
+ * It also loads the resources to visualize (@see _loadResources).
+ *
+ * @override
*/
willStart: function () {
var js_def = ajax.loadJS('/web/static/lib/ace/ace.odoo-custom.js').then(function () {
@@ -158,33 +177,34 @@ var ViewEditor = Widget.extend({
ajax.loadJS('/web/static/lib/ace/theme-monokai.js')
);
});
- return $.when(this._super.apply(this, arguments), js_def, this.loadResources());
+ 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.
+ * Initializes the library and initial view once the DOM is ready. It also
+ * initializes the resize feature of the ace editor.
+ *
+ * @override
*/
start: function () {
- this.$viewEditor = this.$("#ace-view-editor");
- this.$editor = this.$(".ace_editor");
+ 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.$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")
+ 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.$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.$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");
+ this.aceEditor.setTheme('ace/theme/monokai');
var refX = 0;
var resizing = false;
@@ -197,35 +217,35 @@ var ViewEditor = Widget.extend({
var initType;
if (this.options.initialResID) {
initResID = this.options.initialResID;
- initType = (_.isString(initResID) && initResID[0] === '/') ? "less" : "xml";
+ 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";
+ initType = 'less';
}
if (!this.options.doNotLoadViews) {
- initResID = (typeof this.viewKey === "number" ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id);
- initType = "xml";
+ initResID = (typeof this.viewKey === 'number' ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id);
+ initType = 'xml';
}
}
if (initResID) {
- this.displayResource(initResID, initType);
+ 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");
+ 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));
+ $(document).on('mouseup.ViewEditor', stopResizing.bind(this)).on('mousemove.ViewEditor', updateWidth.bind(this));
if (this.options.position === 'left') {
this.$('.ace_scroller').after($('<div>').addClass('ace_resize_bar'));
this.$('.ace_gutter').css({'cursor': 'default'});
- this.$el.on("mousedown.ViewEditor", ".ace_resize_bar", startResizing.bind(this));
+ this.$el.on('mousedown.ViewEditor', '.ace_resize_bar', startResizing.bind(this));
} else {
- this.$el.on("mousedown.ViewEditor", ".ace_gutter", startResizing.bind(this));
+ this.$el.on('mousedown.ViewEditor', '.ace_gutter', startResizing.bind(this));
}
resizeEditor.call(this, readEditorWidth.call(this));
@@ -239,10 +259,10 @@ var ViewEditor = Widget.extend({
this.$el.width(width);
}
function storeEditorWidth() {
- local_storage.setItem('ace_editor_width', this.$el.width());
+ localStorage.setItem('ace_editor_width', this.$el.width());
}
function readEditorWidth() {
- var width = local_storage.getItem('ace_editor_width');
+ var width = localStorage.getItem('ace_editor_width');
return parseInt(width || 720, 10);
}
function startResizing(e) {
@@ -266,20 +286,102 @@ var ViewEditor = Widget.extend({
}
},
/**
- * The destroy method unbinds custom events binded to the document element.
+ * @override
*/
destroy: function () {
this._super.apply(this, arguments);
- this.$el.off(".ViewEditor");
- $(document).off(".ViewEditor");
+ this.$el.off('.ViewEditor');
+ $(document).off('.ViewEditor');
},
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
/**
- * 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.
+ * Initializes a text editor for the specified resource.
+ *
+ * @private
+ * @param {integer|string} resID - the ID/URL of the view/less file
+ * @param {string} [type] (default to the currently selected one)
+ * @returns {ace.EditSession}
*/
- loadResources: function () {
+ _buildEditingSession: function (resID, type) {
+ var self = this;
+ type = type || this.currentType;
+ var editingSession = new window.ace.EditSession(this.resources[type][resID].arch);
+ editingSession.setUseWorker(false);
+ editingSession.setMode('ace/mode/' + (type || this.currentType));
+ editingSession.setUndoManager(new window.ace.UndoManager());
+ editingSession.on('change', function () {
+ _.defer(function () {
+ self._toggleDirtyInfo(resID);
+ self._showErrorLine();
+ });
+ });
+ return editingSession;
+ },
+ /**
+ * 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 if
+ * necessary.
+ *
+ * @private
+ * @param {integer|string} resID
+ * @param {string} [type] - the type of resource (either 'xml' or 'less')
+ */
+ _displayResource: function (resID, type) {
+ if (type) {
+ this._switchType(type);
+ }
+
+ var editingSession = this.editingSessions[this.currentType][resID];
+ if (!editingSession) {
+ editingSession = this.editingSessions[this.currentType][resID] = this._buildEditingSession(resID);
+ }
+ this.aceEditor.setSession(editingSession);
+
+ 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].select2('val', resID);
+
+ this.$resetButton.toggleClass('hidden', this.currentType === 'xml' || !this.less[resID].customized);
+ },
+ /**
+ * Formats the current resource being vizualized.
+ * (@see formatXML, formatLESS)
+ *
+ * @private
+ */
+ _formatResource: function () {
+ var res = this.aceEditor.getValue();
+ var check = (this.currentType === 'xml' ? checkXML : checkLESS)(res);
+ if (check.isValid) {
+ this.aceEditor.setValue((this.currentType === 'xml' ? formatXML : formatLESS)(res));
+ } else {
+ this._showErrorLine(check.error.line, check.error.message, this._getSelectedResource());
+ }
+ },
+ /**
+ * Returns the currently selected resource data.
+ *
+ * @private
+ * @returns {integer|string} view ID or less file URL
+ */
+ _getSelectedResource: function () {
+ return this.$lists[this.currentType].select2('val');
+ },
+ /**
+ * Loads data the ace editor will vizualize and process it. Default behavior
+ * is loading the activate views, index them and build their hierarchy.
+ *
+ * @private
+ * @returns {Deferred}
+ */
+ _loadResources: function () {
// Reset resources
this.resources = {xml: {}, less: {}};
this.editingSessions = {xml: {}, less: {}};
@@ -288,7 +390,7 @@ var ViewEditor = Widget.extend({
// Load resources
return this._rpc({
- route: "/web_editor/get_assets_editor_resources",
+ route: '/web_editor/get_assets_editor_resources',
params: {
key: this.viewKey,
get_views: !this.options.doNotLoadViews,
@@ -305,7 +407,7 @@ var ViewEditor = Widget.extend({
// Only keep the active views and index them by ID.
_.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;
@@ -347,145 +449,30 @@ var ViewEditor = Widget.extend({
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($("<option/>", {
- value: view.id,
- text: view.name,
- selected: currentId === view.id,
- "data-level": view.level,
- "data-debug": view.xml_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) {
- var name = lessInfo.url.substring(_.lastIndexOf(lessInfo.url, "/") + 1, lessInfo.url.length - 5);
- $optgroup.append($("<option/>", {
- value: lessInfo.url,
- text: name,
- selected: currentId === lessInfo.url,
- "data-debug": lessInfo.url,
- "data-customized": lessInfo.customized
- }));
+ _.extend(self.less, _.indexBy(bundleInfos[1], 'url'));
});
- });
-
- this.$lists.xml.select2("destroy");
- this.$lists.xml.select2({
- formatResult: _formatDisplay,
- formatSelection: _formatDisplay,
- });
- this.$lists.less.select2("destroy");
- this.$lists.less.select2({
- formatResult: _formatDisplay,
- formatSelection: _formatDisplay,
- });
-
- function _formatDisplay(data) {
- var $elem = $(data.element);
-
- var $div = $("<div/>", {
- text: data.text || "",
- style: "padding: 0 0 0 " + (24 * $elem.data("level")) + "px",
- });
-
- if ($elem.data("dirty") || $elem.data("customized")) {
- $div.prepend($("<span/>", {
- class: "fa fa-floppy-o " + ($elem.data("dirty") ? "text-warning" : "text-success"),
- style: "margin-right: 8px;",
- }));
- }
-
- if (session.debug && $elem.data("debug")) {
- $div.append($("<span/>", {
- text: " (" + $elem.data("debug") + ")",
- class: "text-muted",
- style: "font-size: 80%",
- }));
- }
-
- return $div;
- }
- },
- /**
- * 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 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)
- */
- selectedResource: function () {
- return this.$lists[this.currentType].select2("val");
- },
- /**
- * 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")
- */
- displayResource: function (resID, type) {
- if (type) this.switchType(type);
-
- var editingSession = this.editingSessions[this.currentType][resID];
- if (!editingSession) {
- editingSession = this.editingSessions[this.currentType][resID] = this._buildEditingSession(resID);
- }
- this.aceEditor.setSession(editingSession);
-
- 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].select2("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
+ * 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 views reset is not supported yet
+ *
+ * @private
+ * @param {integer|string} [resID] (default to the currently selected one)
+ * @param {string} [type] (default to the currently selected one)
+ * @returns {Deferred}
*/
- resetResource: function (resID, type) {
- resID = resID || this.selectedResource();
+ _resetResource: function (resID, type) {
+ resID = resID || this._getSelectedResource();
type = type || this.currentType;
- if (this.currentType === "xml") {
- return $.Defered().reject(_t("Reseting views is not supported yet")); // TODO
+ if (this.currentType === 'xml') {
+ return $.Defered().reject(_t("Reseting views is not supported yet"));
} else {
return this._rpc({
- route: "/web_editor/reset_less",
+ route: '/web_editor/reset_less',
params: {
url: resID,
bundle_xmlid: this.less[resID].bundle_xmlid,
@@ -494,63 +481,43 @@ var ViewEditor = Widget.extend({
}
},
/**
- * 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.
+ * Saves an unique LESS file.
+ *
+ * @private
+ * @param {Object} session - contains the 'id' (url) and the 'text' of the
+ * LESS file to save.
+ * @return {Deferred} status indicates if the save is finished or if an
+ * error occured.
*/
- _buildEditingSession: function(resID, type) {
+ _saveLess: function (session) {
+ var def = $.Deferred();
+
var self = this;
- type = type || this.currentType;
- var editingSession = new window.ace.EditSession(this.resources[type][resID].arch);
- editingSession.setUseWorker(false);
- editingSession.setMode("ace/mode/" + (type || this.currentType));
- editingSession.setUndoManager(new window.ace.UndoManager());
- editingSession.on("change", function () {
- _.defer(function () {
- self._toggleDirtyInfo(resID);
- self._showErrorLine();
- });
+ this._rpc({
+ route: '/web_editor/save_less',
+ params: {
+ 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);
});
- return editingSession;
- },
- /**
- * The private _toggleDirtyInfo method update the select option DOM element associated with
- * 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 (resID, type, isDirty) {
- type = type || this.currentType;
- if (!resID || !this.editingSessions[type][resID]) return;
-
- var $option = this.$lists[type].find("[value='" + resID + "']");
- if (isDirty === undefined) {
- isDirty = this.editingSessions[type][resID].getUndoManager().hasUndo();
- }
- $option.data("dirty", isDirty);
- },
- /**
- * The formatResource method formats the current resource being vizualized.
- * TODO formatting LESS files is not supported yet.
- */
- formatResource: function () {
- var res = this.aceEditor.getValue();
- var check = (this.currentType === "xml" ? checkXML : checkLESS)(res);
- if (check.isValid) {
- this.aceEditor.setValue((this.currentType === "xml" ? formatXML : formatLESS)(res));
- } else {
- this._showErrorLine(check.error.line, check.error.message, this.selectedResource());
- }
+ return def;
},
/**
- * 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.
+ * Saves every resource that has been modified. If one cannot be saved, none
+ * is saved and an error message is displayed.
+ *
+ * @private
+ * @return {Deferred} status indicates if the save is finished or if an
+ * error occured.
*/
- saveResources: function () {
+ _saveResources: function () {
var toSave = {};
var errorFound = false;
_.each(this.editingSessions, (function (editingSessions, type) {
@@ -568,7 +535,7 @@ var ViewEditor = Widget.extend({
this._showErrorLine();
for (var i = 0 ; i < toSave[type].length && !errorFound ; i++) {
- var check = (type === "xml" ? checkXML : checkLESS)(toSave[type][i].text);
+ 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];
@@ -579,24 +546,27 @@ var ViewEditor = Widget.extend({
var defs = [];
_.each(toSave, (function (_toSave, type) {
- defs = defs.concat(_.map(_toSave, (type === "xml" ? this._saveView : this._saveLess).bind(this)));
+ defs = defs.concat(_.map(_toSave, (type === 'xml' ? this._saveView : this._saveLess).bind(this)));
}).bind(this));
return $.when.apply($, defs).fail((function (session, error) {
- Dialog.alert(this, "", {
+ Dialog.alert(this, '', {
title: _t("Server error"),
- $content: $("<div/>").html(
+ $content: $('<div/>').html(
_t("A server error occured. Please check you correctly signed in and that the file you are saving is well-formed.")
- + "<br/>"
+ + '<br/>'
+ error
)
});
}).bind(this));
},
/**
- * 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.
+ * Saves an unique XML view.
+ *
+ * @private
+ * @param {Object} session - the 'id' and the 'text' of the view to save.
+ * @returns {Deferred} status indicates if the save is finished or if an
+ * error occured.
*/
_saveView: function (session) {
var def = $.Deferred();
@@ -605,9 +575,9 @@ var ViewEditor = Widget.extend({
this._rpc({
model: 'ir.ui.view',
method: 'write',
- args: [[session.id], {arch: session.text}, _.extend(base.get_context(), {lang: null})],
+ args: [[session.id], {arch: session.text}, _.extend(weContext.get(), {lang: null})],
}).then(function () {
- self._toggleDirtyInfo(session.id, "xml", false);
+ self._toggleDirtyInfo(session.id, 'xml', false);
def.resolve();
}, function (source, error) {
def.reject(session, error);
@@ -616,77 +586,253 @@ var ViewEditor = Widget.extend({
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;
- this._rpc({
- route: "/web_editor/save_less",
- params: {
- 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);
- });
-
- return def;
- },
- /**
- * 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 resID: the ID/URL of the view/less file whose line is to highlight
- * @param type: the type of the given resource
+ * Shows 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 called without argument, the effects are removed.
+ *
+ * @private
+ * @param {integer} [line] - the line number to highlight
+ * @param {string} [message] - to show on click on the line number
+ * @param {integer|string} [resID]
+ * @param {string} [type]
*/
_showErrorLine: function (line, message, resID, type) {
if (line === undefined || line <= 0) {
if (this.$errorLine) {
- this.$errorLine.removeClass("o_error");
- this.$errorLine.off(".o_error");
+ this.$errorLine.removeClass('o_error');
+ this.$errorLine.off('.o_error');
this.$errorLine = undefined;
- this.$errorContent.removeClass("o_error");
+ this.$errorContent.removeClass('o_error');
this.$errorContent = undefined;
}
return;
}
- if (type) this.switchType(type);
+ if (type) this._switchType(type);
- if (this.selectedResource() === resID) {
+ if (this._getSelectedResource() === resID) {
__showErrorLine.call(this, line);
} else {
var onChangeSession = (function () {
- this.aceEditor.off("changeSession", onChangeSession);
+ this.aceEditor.off('changeSession', onChangeSession);
_.delay(__showErrorLine.bind(this, line), 400);
}).bind(this);
this.aceEditor.on('changeSession', onChangeSession);
- this.displayResource(resID, this.currentType);
+ this._displayResource(resID, this.currentType);
}
function __showErrorLine(line) {
this.aceEditor.gotoLine(line);
- this.$errorLine = this.$viewEditor.find(".ace_gutter-cell").filter(function () {
+ 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);
+ }).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");
+ this.$errorContent = this.$viewEditor.find('.ace_scroller').addClass('o_error');
+ }
+ },
+ /**
+ * Switches to the LESS or XML edition. Calling this method will adapt all
+ * DOM elements to keep the editor consistent.
+ *
+ * @private
+ * @param {string} 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');
+ },
+ /**
+ * Updates the select option DOM element associated with a particular resID
+ * to indicate if the option is dirty or not.
+ *
+ * @private
+ * @param {integer|string} resID
+ * @param {string} [type] (default to the currently selected one)
+ * @param {boolean} [isDirty] true if the view is dirty, default to content
+ * of UndoManager
+ */
+ _toggleDirtyInfo: function (resID, type, isDirty) {
+ type = type || this.currentType;
+
+ if (!resID || !this.editingSessions[type][resID]) return;
+
+ var $option = this.$lists[type].find('[value=' + resID + ']');
+ if (isDirty === undefined) {
+ isDirty = this.editingSessions[type][resID].getUndoManager().hasUndo();
+ }
+ $option.data('dirty', isDirty);
+ },
+ /**
+ * Renders the content of the view/file <select/> DOM element according to
+ * current widget data.
+ *
+ * @private
+ */
+ _updateViewSelectDOM: function () {
+ var currentId = this._getSelectedResource();
+
+ var self = this;
+ this.$lists.xml.empty();
+ _.each(this.sorted_views, function (view) {
+ self.$lists.xml.append($('<option/>', {
+ value: view.id,
+ text: view.name,
+ selected: currentId === view.id,
+ 'data-level': view.level,
+ 'data-debug': view.xml_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) {
+ var name = lessInfo.url.substring(_.lastIndexOf(lessInfo.url, '/') + 1, lessInfo.url.length - 5);
+ $optgroup.append($('<option/>', {
+ value: lessInfo.url,
+ text: name,
+ selected: currentId === lessInfo.url,
+ 'data-debug': lessInfo.url,
+ 'data-customized': lessInfo.customized
+ }));
+ });
+ });
+
+ this.$lists.xml.select2('destroy');
+ this.$lists.xml.select2({
+ formatResult: _formatDisplay,
+ formatSelection: _formatDisplay,
+ });
+ this.$lists.less.select2('destroy');
+ this.$lists.less.select2({
+ formatResult: _formatDisplay,
+ formatSelection: _formatDisplay,
+ });
+
+ function _formatDisplay(data) {
+ var $elem = $(data.element);
+
+ var $div = $('<div/>', {
+ text: data.text || '',
+ style: 'padding: 0 0 0 ' + (24 * $elem.data('level')) + 'px',
+ });
+
+ if ($elem.data('dirty') || $elem.data('customized')) {
+ $div.prepend($('<span/>', {
+ class: 'fa fa-floppy-o ' + ($elem.data('dirty') ? 'text-warning' : 'text-success'),
+ style: 'margin-right: 8px;',
+ }));
+ }
+
+ if (session.debug && $elem.data('debug')) {
+ $div.append($('<span/>', {
+ text: ' (' + $elem.data('debug') + ')',
+ class: 'text-muted',
+ style: 'font-size: 80%',
+ }));
+ }
+
+ return $div;
}
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the close button is clicked -> hides the ace editor.
+ *
+ * @private
+ */
+ _onCloseClick: function () {
+ this.do_hide();
+ },
+ /**
+ * Called when the format button is clicked -> format the current resource.
+ *
+ * @private
+ */
+ _onFormatClick: function () {
+ this._formatResource();
+ },
+ /**
+ * Called when the checkbox which indicates if all LESS should be included
+ * is toggled -> reloads the resources accordingly.
+ *
+ * @private
+ * @param {Event}
+ */
+ _onIncludeAllLessChange: function (ev) {
+ this.options.includeAllLess = $(ev.target).prop('checked');
+ this._loadResources().then(this._updateViewSelectDOM.bind(this));
+ },
+ /**
+ * Called when the checkbox which indicates if assets bundles should be
+ * included is toggled -> reloads the resources accordingly.
+ *
+ * @private
+ * @param {Event}
+ */
+ _onIncludeBundlesChange: function (ev) {
+ this.options.includeBundles = $(ev.target).prop('checked');
+ this._loadResources().then(this._updateViewSelectDOM.bind(this));
+ },
+ /**
+ * Called when another resource is selected -> displays it.
+ *
+ * @private
+ */
+ _onResChange: function () {
+ this._displayResource(this._getSelectedResource());
+ },
+ /**
+ * Called when the reset button is clicked -> resets the resources to its
+ * original standard odoo state.
+ *
+ * @private
+ */
+ _onResetClick: 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._getSelectedResource());
+ },
+ });
+ },
+ /**
+ * Called when the save button is clicked -> saves the dirty resources and
+ * reloads.
+ *
+ * @private
+ */
+ _onSaveClick: function () {
+ this._saveResources();
+ },
+ /**
+ * Called when the user wants to switch from xml to less or vice-versa ->
+ * adapt resources choices and displays a resource of that type.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onTypeChoice: function (ev) {
+ ev.preventDefault();
+ this._switchType($(ev.target).data('type'));
+ },
});
return ViewEditor;
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js
index 3bfae6ce240..20b67828fad 100644
--- a/addons/web_editor/static/src/js/widgets/widgets.js
+++ b/addons/web_editor/static/src/js/widgets/widgets.js
@@ -2,11 +2,9 @@ odoo.define('web_editor.widget', function (require) {
'use strict';
var core = require('web.core');
-var ajax = require('web.ajax');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
-var base = require('web_editor.base');
-var rte = require('web_editor.rte');
+var weContext = require("web_editor.context");
var QWeb = core.qweb;
var range = $.summernote.core.range;
@@ -48,6 +46,10 @@ Dialog = Dialog.extend({
*/
var alt = Dialog.extend({
template: 'web_editor.dialog.alt',
+ xmlDependencies: Dialog.prototype.xmlDependencies.concat(
+ ['/web_editor/static/src/xml/editor.xml']
+ ),
+
init: function (parent, options, $editable, media) {
this._super(parent, _.extend({}, {
title: _t("Change media description and tooltip")
@@ -70,158 +72,11 @@ var alt = Dialog.extend({
},
});
-var click_event = function (el, type) {
+function click_event(el, type) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
el.dispatchEvent(evt);
-};
-
-/**
- * MediaDialog widget. Lets users change a media, including uploading a
- * new image, font awsome or video and can change a media into an other
- * media
- *
- * options: select_images: allow the selection of more of one image
- */
-var MediaDialog = Dialog.extend({
- template: 'web_editor.dialog.media',
- events : _.extend({}, Dialog.prototype.events, {
- 'input input#icon-search': 'search',
- }),
- init: function (parent, options, $editable, media) {
- this._super(parent, _.extend({}, {
- title: _t("Select a Media"),
- }, options));
- if ($editable) {
- this.$editable = $editable;
- this.rte = this.$editable.rte || this.$editable.data('rte');
- }
- this.options = options || {};
- this.old_media = media;
- this.media = media;
- this.isNewMedia = !media;
- this.range = range.create();
- },
- start: function () {
- var self = this;
-
- this.$modal.addClass('note-image-dialog');
- this.$modal.find('.modal-dialog').addClass('o_select_media_dialog');
-
- this.only_images = this.options.only_images || this.options.select_images || (this.media && ($(this.media).parent().data("oe-field") === "image" || $(this.media).parent().data("oe-type") === "image"));
- if (this.only_images) {
- this.$('[href="#editor-media-document"], [href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
- }
-
- this.opened((function () {
- if (this.media) {
- if (this.media.nodeName === "IMG") {
- this.$('[href="#editor-media-image"]').tab('show');
- } else if ($(this.media).is('a.o_image')) {
- this.$('[href="#editor-media-document"]').tab('show');
- } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) {
- this.$('[href="#editor-media-video"]').tab('show');
- } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) {
- this.media = this.media.parentNode;
- this.$('[href="#editor-media-video"]').tab('show');
- } else if (this.media.className.match(/(^|\s)fa($|\s)/)) {
- this.$('[href="#editor-media-icon"]').tab('show');
- }
- }
- }).bind(this));
-
- this.imageDialog = new ImageDialog(this, this.media, this.options);
- this.imageDialog.appendTo(this.$("#editor-media-image"));
- this.documentDialog = new ImageDialog(this, this.media, _.extend({'document': true}, this.options));
- this.documentDialog.appendTo(this.$("#editor-media-document"));
- if (!this.only_images) {
- this.iconDialog = new fontIconsDialog(this, this.media, this.options);
- this.iconDialog.appendTo(this.$("#editor-media-icon"));
- this.videoDialog = new VideoDialog(this, this.media, this.options);
- this.videoDialog.appendTo(this.$("#editor-media-video"));
- }
-
- this.active = this.imageDialog;
-
- this.$('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
- if ($(event.target).is('[href="#editor-media-image"]')) {
- self.active = self.imageDialog;
- self.$('li.search, li.previous, li.next').removeClass("hidden");
- } else if ($(event.target).is('[href="#editor-media-document"]')) {
- self.active = self.documentDialog;
- self.$('li.search, li.previous, li.next').removeClass("hidden");
- } else if ($(event.target).is('[href="#editor-media-icon"]')) {
- self.active = self.iconDialog;
- self.$('li.search, li.previous, li.next').removeClass("hidden");
- self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
- } else if ($(event.target).is('[href="#editor-media-video"]')) {
- self.active = self.videoDialog;
- self.$('.nav-tabs li.search').addClass("hidden");
- }
- });
-
- return this._super.apply(this, arguments);
- },
- save: function () {
- if (this.options.select_images) {
- this.final_data = this.active.save();
- this._super.apply(this, arguments);
- return;
- }
- if(this.rte) {
- this.range.select();
- this.rte.historyRecordUndo(this.media);
- }
-
- var self = this;
- if (self.media) {
- this.media.innerHTML = "";
- if (this.active !== this.imageDialog && this.active !== this.documentDialog) {
- this.imageDialog.clear();
- }
- // if not mode only_images
- if (this.iconDialog && this.active !== this.iconDialog) {
- this.iconDialog.clear();
- }
- if (this.videoDialog && this.active !== this.videoDialog) {
- this.videoDialog.clear();
- }
- } else {
- this.media = document.createElement("img");
- this.range.insertNode(this.media, true);
- this.active.media = this.media;
- }
- this.active.save();
-
- if (this.active.add_class) {
- $(this.active.media).addClass(this.active.add_class);
- }
- var media = this.active.media;
-
- this.final_data = [media, self.old_media];
- $(document.body).trigger("media-saved", this.final_data);
- $(self.old_media).trigger("save", this.final_data);
-
- // Update editor bar after image edition (in case the image change to icon or other)
- _.defer(function () {
- if (!media.parentNode) return;
- range.createFromNode(media).select();
- click_event(media, "mousedown");
- click_event(media, "mouseup");
- });
-
- this._super.apply(this, arguments);
- },
- searchTimer: null,
- search: function () {
- var self = this;
- var needle = this.$("input#icon-search").val();
- clearTimeout(this.searchTimer);
- this.searchTimer = setTimeout(function () {
- self.active.search(needle || "");
- },250);
- }
-});
+}
/**
* ImageDialog widget. Let users change an image, including uploading a
@@ -230,6 +85,7 @@ var MediaDialog = Dialog.extend({
*/
var ImageDialog = Widget.extend({
template: 'web_editor.dialog.image',
+ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
IMAGES_PER_ROW: 6,
IMAGES_ROWS: 2,
events: _.extend({}, Dialog.prototype.events, {
@@ -284,7 +140,7 @@ var ImageDialog = Widget.extend({
o.id = +o.url.match(/\/web\/content\/([0-9]*)/, '')[1];
}
this.parent.$(".pager > li").click(function (e) {
- if(!self.$el.is(':visible')) {
+ if (!self.$el.is(':visible')) {
return;
}
e.preventDefault();
@@ -331,7 +187,7 @@ var ImageDialog = Widget.extend({
this.add_class = "pull-left";
this.style = {"width": "100%"};
}
- if(this.media.tagName !== "IMG") {
+ if (this.media.tagName !== "IMG") {
media = document.createElement('img');
$(this.media).replaceWith(media);
this.media = media;
@@ -431,15 +287,15 @@ var ImageDialog = Widget.extend({
if (needle && needle.length) {
domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
}
- return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+ return this._rpc({
model: 'ir.attachment',
method: 'search_read',
args: [],
kwargs: {
domain: domain,
fields: ['name', 'mimetype', 'checksum', 'url', 'type'],
- order: 'id desc',
- context: base.get_context()
+ order: [{name: 'id', asc: false}],
+ context: weContext.get(),
}
}).then(this.proxy('fetched_existing'));
},
@@ -516,7 +372,12 @@ var ImageDialog = Widget.extend({
$both.css({borderWidth: "5px", borderColor: "#f00"});
- return ajax.jsonRpc('/web_editor/attachment/remove', 'call', {'ids': [id]}).then(function (prevented) {
+ return this._rpc({
+ route: '/web_editor/attachment/remove',
+ params: {
+ ids: [id],
+ },
+ }).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.records = _.without(self.records, attachment);
self.display_attachments();
@@ -545,7 +406,7 @@ var getCssSelectors = function (filter) {
return cacheCssSelectors[filter];
}
var sheets = document.styleSheets;
- for(var i = 0; i < sheets.length; i++) {
+ for (var i = 0; i < sheets.length; i++) {
var rules;
if (sheets[i].rules) {
rules = sheets[i].rules;
@@ -553,18 +414,18 @@ var getCssSelectors = function (filter) {
//try...catch because Firefox not able to enumerate document.styleSheets[].cssRules[] for cross-domain stylesheets.
try {
rules = sheets[i].cssRules;
- } catch(e) {
+ } catch (e) {
console.warn("Can't read the css rules of: " + sheets[i].href, e);
continue;
}
}
if (rules) {
- for(var r = 0; r < rules.length; r++) {
+ for (var r = 0; r < rules.length; r++) {
var selectorText = rules[r].selectorText;
if (selectorText) {
selectorText = selectorText.split(/\s*,\s*/);
var data = null;
- for(var s = 0; s < selectorText.length; s++) {
+ for (var s = 0; s < selectorText.length; s++) {
var match = selectorText[s].match(filter);
if (match) {
var clean = match[1].slice(1).replace(/::?before$/, '');
@@ -596,28 +457,13 @@ var computeFonts = _.once(function () {
});
});
-rte.Class.include({
- init: function () {
- this._super.apply(this, arguments);
- computeFonts();
- },
- onEnableEditableArea: function ($editable) {
- if ($editable.data('oe-type') === "monetary") {
- $editable.attr('contenteditable', false);
- $editable.find('.oe_currency_value').attr('contenteditable', true);
- }
- if ($editable.is('[data-oe-model]') && !$editable.is('[data-oe-model="ir.ui.view"]') && !$editable.is('[data-oe-type="html"]')) {
- $editable.data('layoutInfo').popover().find('.btn-group:not(.note-history)').remove();
- }
- },
-});
-
/**
* FontIconsDialog widget. Lets users change a font awesome, support all
* font awesome loaded in the css files.
*/
var fontIconsDialog = Widget.extend({
template: 'web_editor.dialog.font-icons',
+ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events : _.extend({}, Dialog.prototype.events, {
'click .font-icons-icon': function (e) {
e.preventDefault();
@@ -863,6 +709,7 @@ function createVideoNode(url, options) {
*/
var VideoDialog = Widget.extend({
template: 'web_editor.dialog.video',
+ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events : _.extend({}, Dialog.prototype.events, {
'click input#urlvideo ~ button': 'get_video',
'click input#embedvideo ~ button': 'get_embed_video',
@@ -936,7 +783,7 @@ var VideoDialog = Widget.extend({
if (this.media.dataset.src) {
try {
delete this.media.dataset.src;
- } catch(e) {
+ } catch (e) {
this.media.dataset.src = undefined;
}
}
@@ -945,10 +792,163 @@ var VideoDialog = Widget.extend({
});
/**
+ * MediaDialog widget. Lets users change a media, including uploading a
+ * new image, font awsome or video and can change a media into an other
+ * media
+ *
+ * options: select_images: allow the selection of more of one image
+ */
+var MediaDialog = Dialog.extend({
+ template: 'web_editor.dialog.media',
+ xmlDependencies: Dialog.prototype.xmlDependencies.concat(
+ ['/web_editor/static/src/xml/editor.xml']
+ ),
+ events : _.extend({}, Dialog.prototype.events, {
+ 'input input#icon-search': 'search',
+ }),
+ init: function (parent, options, $editable, media) {
+ this._super(parent, _.extend({}, {
+ title: _t("Select a Media"),
+ }, options));
+ if ($editable) {
+ this.$editable = $editable;
+ this.rte = this.$editable.rte || this.$editable.data('rte');
+ }
+ this.options = options || {};
+ this.old_media = media;
+ this.media = media;
+ this.isNewMedia = !media;
+ this.range = range.create();
+ },
+ start: function () {
+ var self = this;
+
+ this.$modal.addClass('note-image-dialog');
+ this.$modal.find('.modal-dialog').addClass('o_select_media_dialog');
+
+ this.only_images = this.options.only_images || this.options.select_images || (this.media && ($(this.media).parent().data("oe-field") === "image" || $(this.media).parent().data("oe-type") === "image"));
+ if (this.only_images) {
+ this.$('[href="#editor-media-document"], [href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+ }
+
+ this.opened((function () {
+ if (this.media) {
+ if (this.media.nodeName === "IMG") {
+ this.$('[href="#editor-media-image"]').tab('show');
+ } else if ($(this.media).is('a.o_image')) {
+ this.$('[href="#editor-media-document"]').tab('show');
+ } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) {
+ this.$('[href="#editor-media-video"]').tab('show');
+ } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) {
+ this.media = this.media.parentNode;
+ this.$('[href="#editor-media-video"]').tab('show');
+ } else if (this.media.className.match(/(^|\s)fa($|\s)/)) {
+ this.$('[href="#editor-media-icon"]').tab('show');
+ }
+ }
+ }).bind(this));
+
+ this.imageDialog = new ImageDialog(this, this.media, this.options);
+ this.imageDialog.appendTo(this.$("#editor-media-image"));
+ this.documentDialog = new ImageDialog(this, this.media, _.extend({'document': true}, this.options));
+ this.documentDialog.appendTo(this.$("#editor-media-document"));
+ if (!this.only_images) {
+ this.iconDialog = new fontIconsDialog(this, this.media, this.options);
+ this.iconDialog.appendTo(this.$("#editor-media-icon"));
+ this.videoDialog = new VideoDialog(this, this.media, this.options);
+ this.videoDialog.appendTo(this.$("#editor-media-video"));
+ }
+
+ this.active = this.imageDialog;
+
+ this.$('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
+ if ($(event.target).is('[href="#editor-media-image"]')) {
+ self.active = self.imageDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-document"]')) {
+ self.active = self.documentDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-icon"]')) {
+ self.active = self.iconDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-video"]')) {
+ self.active = self.videoDialog;
+ self.$('.nav-tabs li.search').addClass("hidden");
+ }
+ });
+
+ return this._super.apply(this, arguments);
+ },
+ save: function () {
+ if (this.options.select_images) {
+ this.final_data = this.active.save();
+ this._super.apply(this, arguments);
+ return;
+ }
+ if (this.rte) {
+ this.range.select();
+ this.rte.historyRecordUndo(this.media);
+ }
+
+ var self = this;
+ if (self.media) {
+ this.media.innerHTML = "";
+ if (this.active !== this.imageDialog && this.active !== this.documentDialog) {
+ this.imageDialog.clear();
+ }
+ // if not mode only_images
+ if (this.iconDialog && this.active !== this.iconDialog) {
+ this.iconDialog.clear();
+ }
+ if (this.videoDialog && this.active !== this.videoDialog) {
+ this.videoDialog.clear();
+ }
+ } else {
+ this.media = document.createElement("img");
+ this.range.insertNode(this.media, true);
+ this.active.media = this.media;
+ }
+ this.active.save();
+
+ if (this.active.add_class) {
+ $(this.active.media).addClass(this.active.add_class);
+ }
+ var media = this.active.media;
+
+ this.final_data = [media, self.old_media];
+ $(document.body).trigger("media-saved", this.final_data);
+ $(self.old_media).trigger("save", this.final_data);
+
+ // Update editor bar after image edition (in case the image change to icon or other)
+ _.defer(function () {
+ if (!media.parentNode) return;
+ range.createFromNode(media).select();
+ click_event(media, "mousedown");
+ click_event(media, "mouseup");
+ });
+
+ this._super.apply(this, arguments);
+ },
+ searchTimer: null,
+ search: function () {
+ var self = this;
+ var needle = this.$("input#icon-search").val();
+ clearTimeout(this.searchTimer);
+ this.searchTimer = setTimeout(function () {
+ self.active.search(needle || "");
+ },250);
+ }
+});
+
+/**
* The Link Dialog allows to customize link content and style.
*/
var LinkDialog = Dialog.extend({
template: 'web_editor.dialog.link',
+ xmlDependencies: Dialog.prototype.xmlDependencies.concat(
+ ['/web_editor/static/src/xml/editor.xml']
+ ),
events: _.extend({}, Dialog.prototype.events, {
'change :input.url-source': 'changed',
'keyup :input.url': 'onkeyup',
@@ -1064,7 +1064,7 @@ var LinkDialog = Dialog.extend({
var label = this.$('#o_link_dialog_label_input').val() || val;
if (label && this.data.images) {
- for(var i=0; i<this.data.images.length; i++) {
+ for (var i=0; i<this.data.images.length; i++) {
label = label.replace(/</, "&lt;").replace(/>/, "&gt;").replace(/\[IMG\]/, this.data.images[i].outerHTML);
}
}
@@ -1173,5 +1173,6 @@ return {
LinkDialog: LinkDialog,
getCssSelectors: getCssSelectors,
fontIcons: fontIcons,
+ computeFonts: computeFonts,
};
});