summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src
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
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')
-rw-r--r--addons/web_editor/static/src/js/backend/fields.js12
-rw-r--r--addons/web_editor/static/src/js/base.js71
-rw-r--r--addons/web_editor/static/src/js/content/body_manager.js33
-rw-r--r--addons/web_editor/static/src/js/editor/editor.js274
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js602
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js101
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js2042
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js2086
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js287
-rw-r--r--addons/web_editor/static/src/js/editor/transcoder.js166
-rw-r--r--addons/web_editor/static/src/js/editor/translator.js420
-rw-r--r--addons/web_editor/static/src/js/iframe.js138
-rw-r--r--addons/web_editor/static/src/js/inline.js51
-rw-r--r--addons/web_editor/static/src/js/root_widget.js155
-rw-r--r--addons/web_editor/static/src/js/snippets.animation.js119
-rw-r--r--addons/web_editor/static/src/js/tours/rte.js638
-rw-r--r--addons/web_editor/static/src/js/widgets/ace.js804
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js361
-rw-r--r--addons/web_editor/static/src/less/web_editor.ui.components.less3
-rw-r--r--addons/web_editor/static/src/less/web_editor.ui.less4
-rw-r--r--addons/web_editor/static/src/xml/editor.xml2
-rw-r--r--addons/web_editor/static/src/xml/snippets.xml17
-rw-r--r--addons/web_editor/static/src/xml/translator.xml15
23 files changed, 4714 insertions, 3687 deletions
diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js
index 6b167c59edd..ab4828fe89d 100644
--- a/addons/web_editor/static/src/js/backend/fields.js
+++ b/addons/web_editor/static/src/js/backend/fields.js
@@ -7,7 +7,6 @@ var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var field_registry = require('web.field_registry');
-
var transcoder = require('web_editor.transcoder');
var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;
@@ -107,8 +106,8 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
*/
_getValue: function () {
if (this.nodeOptions['style-inline']) {
- transcoder.class_to_style(this.$content);
- transcoder.font_to_img(this.$content);
+ transcoder.classToStyle(this.$content);
+ transcoder.fontToImg(this.$content);
}
return this.$content.html();
},
@@ -125,7 +124,7 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
// trigger a mouseup to refresh the editor toolbar
this.$content.trigger('mouseup');
if (this.nodeOptions['style-inline']) {
- transcoder.style_to_class(this.$content);
+ transcoder.styleToClass(this.$content);
}
// reset the history (otherwise clicking on undo before editing the
// value will empty the editor)
@@ -352,7 +351,7 @@ var FieldTextHtml = AbstractField.extend({
return;
}
if (this.mode === "edit") {
- if(window.odoo[this.callback+"_updown"]) {
+ if (window.odoo[this.callback+"_updown"]) {
// FIXME
// window.odoo[this.callback+"_updown"](value, this.view.get_fields_values(), this.name);
this.resize();
@@ -391,7 +390,7 @@ var FieldTextHtml = AbstractField.extend({
var layoutInfo = this.editor.rte.editable().data('layoutInfo');
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
- this.editor.buildingBlock.clean_for_save();
+ this.editor.snippetsMenu.cleanForSave();
this._setValue(this.$content.html());
return this._super.apply(this, arguments);
},
@@ -405,5 +404,4 @@ return {
FieldTextHtmlSimple: FieldTextHtmlSimple,
FieldTextHtml: FieldTextHtml,
};
-
});
diff --git a/addons/web_editor/static/src/js/base.js b/addons/web_editor/static/src/js/base.js
index a241905c631..012068776e7 100644
--- a/addons/web_editor/static/src/js/base.js
+++ b/addons/web_editor/static/src/js/base.js
@@ -1,35 +1,64 @@
odoo.define('web_editor.base', function (require) {
-"use strict";
+'use strict';
var ajax = require('web.ajax');
var session = require('web.session');
-var get_context = function (dict) {
- var html = document.documentElement;
- return _.extend({
- 'lang': (html.getAttribute('lang') || '').replace('-', '_'),
- }, dict);
+var domReady = $.Deferred();
+$(domReady.resolve.bind(domReady));
+
+return {
+ /**
+ * If a widget needs to be instantiated on page loading, it needs to wait
+ * for appropriate resources to be loaded. This function returns a Deferred
+ * which is resolved when the dom is ready, the session is bound
+ * (translations loaded) and the XML is loaded. This should however not be
+ * necessary anymore as widgets should not be parentless and should then be
+ * instantiated (directly or not) by the page main component (webclient,
+ * website root, editor bar, ...). The DOM will be ready then, the main
+ * component is in charge of waiting for the session and the XML can be
+ * lazy loaded thanks to the @see Widget.xmlDependencies key.
+ *
+ * @returns {Deferred}
+ */
+ ready: function () {
+ return $.when(domReady, session.is_bound, ajax.loadXML());
+ },
};
+});
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
+//==============================================================================
-var data = {
- 'get_context': get_context,
-};
+odoo.define('web_editor.context', function (require) {
+'use strict';
-var dom_ready = $.Deferred();
-$(document).ready(function () {
- dom_ready.resolve(data);
- // fix for ie
- if ($.fn.placeholder) $('input, textarea').placeholder();
-});
-data.dom_ready = dom_ready;
+function getContext(context) {
+ var html = document.documentElement;
+ return _.extend({
+ lang: (html.getAttribute('lang') || 'en_US').replace('-', '_'),
+ }, context || {});
+}
+function getExtraContext(context) {
+ var html = document.documentElement;
+ return _.extend(getContext(), {
+ editable: !!(html.dataset.editable || $('[data-oe-model]').length), // temporary hack, this should be done in python
+ translatable: !!html.dataset.translatable,
+ edit_translations: !!html.dataset.edit_translations,
+ }, context || {});
+}
-// todo: remove and load a bundle of translated templates
-data.ready = function () {
- return $.when(dom_ready, session.is_bound, ajax.loadXML());
+return {
+ get: getContext,
+ getExtra: getExtraContext,
};
+});
+
+//==============================================================================
+
+odoo.define('web_editor.ready', function (require) {
+'use strict';
-return dom_ready;
+var base = require('web_editor.base');
+return base.ready();
});
diff --git a/addons/web_editor/static/src/js/content/body_manager.js b/addons/web_editor/static/src/js/content/body_manager.js
new file mode 100644
index 00000000000..1ba2d219770
--- /dev/null
+++ b/addons/web_editor/static/src/js/content/body_manager.js
@@ -0,0 +1,33 @@
+odoo.define('web_editor.BodyManager', function (require) {
+'use strict';
+
+var mixins = require('web.mixins');
+var session = require('web.session');
+var rootWidget = require('web_editor.root_widget');
+
+/**
+ * Element which is designed to be unique and that will be the top-most element
+ * in the widget hierarchy. So, all other widgets will be indirectly linked to
+ * this Class instance. Its main role will be to retrieve RPC demands from its
+ * children and handle them.
+ */
+var BodyManager = rootWidget.RootWidget.extend(mixins.ServiceProvider, {
+ /**
+ * @constructor
+ */
+ init: function () {
+ mixins.ServiceProvider.init.call(this);
+ this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ willStart: function () {
+ return $.when(
+ this._super.apply(this, arguments),
+ session.is_bound
+ );
+ },
+});
+return BodyManager;
+});
diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js
index 240a0a36121..353104c2c93 100644
--- a/addons/web_editor/static/src/js/editor/editor.js
+++ b/addons/web_editor/static/src/js/editor/editor.js
@@ -1,86 +1,80 @@
odoo.define('web_editor.editor', function (require) {
-"use strict";
+'use strict';
-var ajax = require('web.ajax');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var core = require('web.core');
-var base = require('web_editor.base');
var rte = require('web_editor.rte');
+var snippetsEditor = require('web_editor.snippet.editor');
-var qweb = core.qweb;
var _t = core._t;
-var editor = {};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-editor.dummy = function () {return true;}; // used for snippets, options...
-
-editor.editable = !!($('html').data('editable') || $("[data-oe-model]").length); // temporary hack, this should be done in python
-
-ajax.loadXML('/web_editor/static/src/xml/editor.xml', qweb);
-
-$(document).on('click', '.note-editable', function (ev) {
- ev.preventDefault();
-});
-
-$(document).on('submit', '.note-editable form .btn', function (ev) {
- ev.preventDefault(); // Disable form submition in editable mode
-});
-
-$(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
- // Prevent dropdown closing when a contenteditable children is focused
- if (ev.originalEvent
- && $(ev.target).has(ev.originalEvent.target).length
- && $(ev.originalEvent.target).is('[contenteditable]')) {
- ev.preventDefault();
- }
-});
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-editor.reload = function () {
- location.hash = "scrollTop=" + window.document.body.scrollTop;
- if (location.search.indexOf("enable_editor") > -1) {
- window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');
- } else {
- window.location.reload(true);
- }
-};
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-/* ----- TOP EDITOR BAR FOR ADMIN ---- */
-
-base.ready().then(function () {
- if (editor.editable && location.search.indexOf("enable_editor") >= 0) {
- editor.editor_bar = new editor.Class();
- editor.editor_bar.prependTo(document.body);
- }
-});
-
-editor.Class = Widget.extend({
+var EditorMenuBar = Widget.extend({
template: 'web_editor.editorbar',
+ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events: {
- 'click button[data-action=save]': 'save',
- 'click button[data-action=cancel]': 'cancel',
+ 'click button[data-action=save]': '_onSaveClick',
+ 'click button[data-action=cancel]': '_onCancelClick',
},
+ custom_events: {
+ request_history_undo_record: '_onHistoryUndoRecordRequest',
+ request_save: '_onSaveRequest',
+ },
+
+ /**
+ * Initializes RTE and snippets menu.
+ *
+ * @constructor
+ */
init: function (parent) {
var self = this;
var res = this._super.apply(this, arguments);
- this.parent = parent;
this.rte = new rte.Class(this);
this.rte.on('rte:start', this, function () {
self.trigger('rte:start');
});
+
+ // Snippets edition
+ var $editable = this.rte.editable();
+ this.snippetsMenu = new snippetsEditor.Class(this, $editable);
+
return res;
},
+ /**
+ * @override
+ */
start: function () {
+ var self = this;
+ var defs = [this._super.apply(this, arguments)];
+
+ core.bus.on('editor_save_request', this, this._onSaveRequest);
+ core.bus.on('editor_discard_request', this, this._onDiscardRequest);
+
$('.dropdown-toggle').dropdown();
- this.display_placeholder();
+ $(document).on('keyup', function (event) {
+ if ((event.keyCode === 8 || event.keyCode === 46)) {
+ var $target = $(event.target).closest('.o_editable');
+ if (!$target.is(':has(*:not(p):not(br))') && !$target.text().match(/\S/)) {
+ $target.empty();
+ }
+ }
+ });
+ $(document).on('click', '.note-editable', function (ev) {
+ ev.preventDefault();
+ });
+ $(document).on('submit', '.note-editable form .btn', function (ev) {
+ ev.preventDefault(); // Disable form submition in editable mode
+ });
+ $(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
+ // Prevent dropdown closing when a contenteditable children is focused
+ if (ev.originalEvent
+ && $(ev.target).has(ev.originalEvent.target).length
+ && $(ev.originalEvent.target).is('[contenteditable]')) {
+ ev.preventDefault();
+ }
+ });
- this.rte.on('change', this, this.proxy('rte_changed'));
this.rte.start();
var flag = false;
@@ -92,58 +86,144 @@ editor.Class = Widget.extend({
}
};
- return this._super.apply(this, arguments);
- },
- display_placeholder: function () {
- var $area = $("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]")
- .filter(".oe_not_editable")
- .filter(".oe_no_empty");
+ // Snippets menu
+ defs.push(this.snippetsMenu.insertAfter(this.$el));
+ this.rte.editable().find('*').off('mousedown mouseup click');
- this.on('rte:start', this, function () {
- $area.attr("data-oe-placeholder", _t("Write Your Text Here"));
- });
- this.on("snippets:ready", this, function () {
- if ($("body").hasClass("editor_has_snippets")) {
- $area.attr("data-oe-placeholder", _t("Write Your Text or Drag a Block Here"));
- }
+ return $.when.apply($, defs).then(function () {
+ self.trigger_up('edit_mode');
});
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ this._super.apply(this, arguments);
+ core.bus.off('editor_save_request', this, this._onSaveRequest);
+ core.bus.off('editor_discard_request', this, this._onDiscardRequest);
+ },
- $(document).on("keyup", function (event) {
- if((event.keyCode === 8 || event.keyCode === 46)) {
- var $target = $(event.target).closest(".o_editable");
- if(!$target.is(":has(*:not(p):not(br))") && !$target.text().match(/\S/)) {
- $target.empty();
- }
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Asks the user if he really wants to discard its changes (if there are
+ * some of them), then simply reload the page if he wants to.
+ *
+ * @param {boolean} [reload=true]
+ * true if the page has to be reloaded when the user answers yes
+ * (do nothing otherwise but add this to allow class extension)
+ * @returns {Deferred}
+ */
+ cancel: function (reload) {
+ var self = this;
+ var def = $.Deferred();
+ if (!rte.history.getEditableHasUndo().length) {
+ def.resolve();
+ } else {
+ var confirm = Dialog.confirm(this, _t("If you discard the current edition, all unsaved changes will be lost. You can cancel to return to the edition mode."), {
+ confirm_callback: def.resolve.bind(def),
+ });
+ confirm.on('closed', def, def.reject);
+ }
+ return def.then(function () {
+ if (reload !== false) {
+ window.onbeforeunload = null;
+ return self._reload();
}
});
},
- rte_changed: function () {},
- save: function () {
+ /**
+ * Asks the snippets to clean themself, then saves the page, then reloads it
+ * if asked to.
+ *
+ * @param {boolean} [reload=true]
+ * true if the page has to be reloaded after the save
+ * @returns {Deferred}
+ */
+ save: function (reload) {
+ var self = this;
+ this.snippetsMenu.cleanForSave();
return this.rte.save().then(function () {
- editor.reload();
+ if (reload !== false) {
+ return self._reload();
+ }
});
},
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
/**
- * Saves an RTE content, which always corresponds to a view section (?).
+ * Reloads the page in non-editable mode, with the right scrolling.
+ *
+ * @private
+ * @returns {Deferred} (never resolved, the page is reloading anyway)
*/
- save_without_reload: function () {
- return this.rte.save();
+ _reload: function () {
+ window.location.hash = 'scrollTop=' + window.document.body.scrollTop;
+ if (window.location.search.indexOf('enable_editor') >= 0) {
+ window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');
+ } else {
+ window.location.reload(true);
+ }
+ return $.Deferred();
},
- cancel: function () {
- return new $.Deferred(function (d) {
- if (!rte.history.getEditableHasUndo().length) {
- return d.resolve();
- }
- var confirm = Dialog.confirm(null, _t("If you discard the current edition, all unsaved changes will be lost. You can cancel to return to the edition mode."), {
- confirm_callback: d.resolve.bind(d)
- });
- confirm.on("closed", d, d.reject);
- }).then(function () {
- window.onbeforeunload = null;
- editor.reload();
- });
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the "Discard" button is clicked -> discards the changes.
+ *
+ * @private
+ */
+ _onCancelClick: function () {
+ this.cancel();
+ },
+ /**
+ * Called when an element askes to record an history undo -> records it.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onHistoryUndoRecordRequest: function (ev) {
+ this.rte.historyRecordUndo(ev.data.$target, ev.data.event);
+ },
+ /**
+ * Called when the "Save" button is clicked -> saves the changes.
+ *
+ * @private
+ */
+ _onSaveClick: function () {
+ this.save();
+ },
+ /**
+ * Called when a discard request is received -> discard the page content
+ * changes.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onDiscardRequest: function (ev) {
+ this.cancel(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
+ },
+ /**
+ * Called when a save request is received -> saves the page content.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onSaveRequest: function (ev) {
+ ev.stopPropagation();
+ this.save(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
},
});
-return editor;
+return {
+ Class: EditorMenuBar,
+};
});
diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js
index 92ab133263a..fffe8786f91 100644
--- a/addons/web_editor/static/src/js/editor/rte.js
+++ b/addons/web_editor/static/src/js/editor/rte.js
@@ -1,27 +1,24 @@
odoo.define('web_editor.rte', function (require) {
'use strict';
-var ajax = require('web.ajax');
-var core = require('web.core');
var concurrency = require('web.concurrency');
+var core = require('web.core');
var Widget = require('web.Widget');
+var weContext = require('web_editor.context');
var summernote = require('web_editor.summernote');
-var base = require('web_editor.base');
+var weWidgets = require('web_editor.widget');
var _t = core._t;
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* Summernote Lib (neek change to make accessible: method and object) */
-
+// Summernote Lib (neek change to make accessible: method and object)
var dom = summernote.core.dom;
var range = summernote.core.range;
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* Change History to have a global History for all summernote instances */
-
-var History = function History ($editable) {
+// Change History to have a global History for all summernote instances
+var History = function History($editable) {
var aUndo = [];
var pos = 0;
+ var toSnap;
this.makeSnap = function (event, rng) {
rng = rng || range.create();
@@ -42,27 +39,27 @@ var History = function History ($editable) {
var $editable = $(oSnap.editable);
if (document.documentMode) {
- $editable.removeAttr("contentEditable").removeProp("contentEditable");
+ $editable.removeAttr('contentEditable').removeProp('contentEditable');
}
$editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
- $(".oe_overlay").remove();
- $(".note-control-selection").hide();
+ $('.oe_overlay').remove();
+ $('.note-control-selection').hide();
- $editable.trigger("content_changed");
+ $editable.trigger('content_changed');
try {
- var r = oSnap.editable.innerHTML === "" ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
+ var r = oSnap.editable.innerHTML === '' ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
r.select();
- } catch(e) {
+ } catch (e) {
console.error(e);
return;
}
- $(document).trigger("click");
- $(".o_editable *").filter(function () {
+ $(document).trigger('click');
+ $('.o_editable *').filter(function () {
var $el = $(this);
- if($el.data('snippet-editor')) {
+ if ($el.data('snippet-editor')) {
$el.removeData();
}
});
@@ -74,13 +71,13 @@ var History = function History ($editable) {
return;
}
- $editable.trigger("applySnap");
+ $editable.trigger('applySnap');
- var evt = document.createEvent("MouseEvents");
- evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
+ var evt = document.createEvent('MouseEvents');
+ evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
target.dispatchEvent(evt);
- $editable.trigger("keyup");
+ $editable.trigger('keyup');
});
};
@@ -101,24 +98,25 @@ var History = function History ($editable) {
pos--;
}
this.applySnap(aUndo[Math.max(--pos,0)]);
- while (pos && (aUndo[pos].event === "blur" || (aUndo[pos+1].editable === aUndo[pos].editable && aUndo[pos+1].contents === aUndo[pos].contents))) {
+ while (pos && (aUndo[pos].event === 'blur' || (aUndo[pos+1].editable === aUndo[pos].editable && aUndo[pos+1].contents === aUndo[pos].contents))) {
this.applySnap(aUndo[--pos]);
}
};
+
this.hasUndo = function () {
- return (toSnap && (toSnap.event !== "blur" && toSnap.event !== "activate" && toSnap.event !== "undo")) ||
+ return (toSnap && (toSnap.event !== 'blur' && toSnap.event !== 'activate' && toSnap.event !== 'undo')) ||
!!_.find(aUndo.slice(0, pos+1), function (undo) {
- return undo.event !== "blur" && undo.event !== "activate" && undo.event !== "undo";
+ return undo.event !== 'blur' && undo.event !== 'activate' && undo.event !== 'undo';
});
};
this.getEditableHasUndo = function () {
var editable = [];
- if ((toSnap && (toSnap.event !== "blur" && toSnap.event !== "activate" && toSnap.event !== "undo"))) {
+ if ((toSnap && (toSnap.event !== 'blur' && toSnap.event !== 'activate' && toSnap.event !== 'undo'))) {
editable.push(toSnap.editable);
}
_.each(aUndo.slice(0, pos+1), function (undo) {
- if (undo.event !== "blur" && undo.event !== "activate" && undo.event !== "undo") {
+ if (undo.event !== 'blur' && undo.event !== 'activate' && undo.event !== 'undo') {
editable.push(undo.editable);
}
});
@@ -128,31 +126,31 @@ var History = function History ($editable) {
this.redo = function () {
if (!aUndo[pos+1]) { return; }
this.applySnap(aUndo[++pos]);
- while (aUndo[pos+1] && aUndo[pos].event === "active") {
+ while (aUndo[pos+1] && aUndo[pos].event === 'active') {
this.applySnap(aUndo[pos++]);
}
};
+
this.hasRedo = function () {
return aUndo.length > pos+1;
};
- var toSnap;
this.recordUndo = function ($editable, event, internal_history) {
var self = this;
if (!$editable) {
var rng = range.create();
- if(!rng) return;
- $editable = $(rng.sc).closest(".o_editable");
+ if (!rng) return;
+ $editable = $(rng.sc).closest('.o_editable');
}
- if (aUndo[pos] && (event === "applySnap" || event === "activate")) {
+ if (aUndo[pos] && (event === 'applySnap' || event === 'activate')) {
return;
}
if (!internal_history) {
- if (!event || !toSnap || !aUndo[pos-1] || toSnap.event === "activate") { // don't trigger change for all keypress
+ if (!event || !toSnap || !aUndo[pos-1] || toSnap.event === 'activate') { // don't trigger change for all keypress
setTimeout(function () {
- $editable.trigger("content_changed");
+ $editable.trigger('content_changed');
},0);
}
}
@@ -199,11 +197,9 @@ var History = function History ($editable) {
};
var history = new History();
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-// add focusIn to jQuery to allow to move caret into a div of a contentEditable area
-
-$.extend($.expr[':'],{
- o_editable: function (node,i,m) {
+// jQuery extensions
+$.extend($.expr[':'], {
+ o_editable: function (node, i, m) {
while (node) {
if (node.className && _.isString(node.className)) {
if (node.className.indexOf('o_not_editable')!==-1 ) {
@@ -216,9 +212,8 @@ $.extend($.expr[':'],{
node = node.parentNode;
}
return false;
- }
+ },
});
-
$.fn.extend({
focusIn: function () {
if (this.length) {
@@ -239,18 +234,16 @@ $.fn.extend({
range.create(dom.firstChild(this[0]), 0, next, next.textContent.length).select();
}
return this;
- }
+ },
});
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-/* ----- RICH TEXT EDITOR ---- */
-
-var RTE = Widget.extend({
- init: function (EditorBar, config) {
+// RTE
+var RTEWidget = Widget.extend({
+ /**
+ * @constructor
+ */
+ init: function (parent, getConfig) {
var self = this;
- this.EditorBar = EditorBar;
- data.rte = this;
this._super.apply(this, arguments);
this.init_bootstrap_carousel = $.fn.carousel;
@@ -262,32 +255,12 @@ var RTE = Widget.extend({
return res;
};
- if (config) {
- this.config = config;
- }
- },
- /**
- * Add a record undo to history
- * @param {DOM} target where the dom is changed is editable zone
- */
- historyRecordUndo: function ($target, event, internal_history) {
- var rng = range.create();
- var $editable = $(rng && rng.sc).closest(".o_editable");
- if (!rng || !$editable.length) {
- $editable = $($target).closest(".o_editable");
- rng = range.create($target.closest("*")[0],0);
- } else {
- rng = $editable.data('range') || rng;
- }
- rng.select();
- history.recordUndo($editable, event, internal_history);
+ this._getConfig = getConfig || this._getDefaultConfig;
+
+ weWidgets.computeFonts();
},
/**
- * Makes the page editable
- *
- * @param {Boolean} [restart=false] in case the edition was already set
- * up once and is being re-enabled.
- * @returns {$.Deferred} deferred indicating when the RTE is ready
+ * @override
*/
start: function () {
var self = this;
@@ -296,11 +269,14 @@ var RTE = Widget.extend({
$.fn.carousel = this.edit_bootstrap_carousel;
- $(document).on("keydown.rte", this, this.onKeydown.bind(this));
- $(document).on("mousedown.rte activate.rte", this, this.onMousedown.bind(this));
- $(document).on("mouseup.rte", this, this.onMouseup.bind(this));
+ core.bus.on('alt_dialog_demand', this, this._onAltDialogDemand);
+ core.bus.on('link_dialog_demand', this, this._onLinkDialogDemand);
+ core.bus.on('media_dialog_demand', this, this._onMediaDialogDemand);
+
+ $(document).on('mousedown.rte activate.rte', this, this._onMousedown.bind(this));
+ $(document).on('mouseup.rte', this, this._onMouseup.bind(this));
- $('.o_not_editable').attr("contentEditable", false);
+ $('.o_not_editable').attr('contentEditable', false);
var $editable = this.editable();
@@ -310,28 +286,27 @@ var RTE = Widget.extend({
var $node = $(this);
// add class to display inline-block for empty t-field
- if (window.getComputedStyle(this).display === "inline" && $node.data('oe-type') !== "image") {
+ if (window.getComputedStyle(this).display === 'inline' && $node.data('oe-type') !== 'image') {
$node.addClass('o_is_inline_editable');
}
- $node.data('initInnerHTML', $node.html());
});
// start element observation
$(document).on('content_changed', '.o_editable', function (event) {
- self.trigger('change', this);
+ self.trigger_up('rte_change', {target: event.target});
$(this).addClass('o_dirty');
});
- $('#wrapwrap, .o_editable').on('click.rte', '*', this, this.onClick.bind(this));
+ $('#wrapwrap, .o_editable').on('click.rte', '*', this, this._onClick.bind(this));
- $('body').addClass("editor_enable");
+ $('body').addClass('editor_enable');
$(document)
.tooltip({
selector: '[data-oe-readonly]',
container: 'body',
trigger: 'hover',
- delay: { "show": 1000, "hide": 100 },
+ delay: { 'show': 1000, 'hide': 100 },
placement: 'bottom',
title: _t("Readonly field")
})
@@ -341,161 +316,325 @@ var RTE = Widget.extend({
$(document).trigger('mousedown');
this.trigger('rte:start');
+
+ return this._super.apply(this, arguments);
},
+ /**
+ * @override
+ */
+ destroy: function () {
+ this.cancel();
+ this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Stops the RTE.
+ */
+ cancel: function () {
+ if (this.$last) {
+ this.$last.destroy();
+ this.$last = null;
+ }
+
+ $.fn.carousel = this.init_bootstrap_carousel;
+ core.bus.off('alt_dialog_demand', this, this._onAltDialogDemand);
+ core.bus.off('link_dialog_demand', this, this._onLinkDialogDemand);
+ core.bus.off('media_dialog_demand', this, this._onMediaDialogDemand);
+
+ $(document).off('.rte');
+ $('#wrapwrap, .o_editable').off('.rte');
+
+ $('.o_not_editable').removeAttr('contentEditable');
+ $(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
+ $(document).tooltip('destroy');
+ $('body').removeClass('editor_enable');
+ this.trigger('rte:stop');
+ },
+ /**
+ * Returns the editable areas on the page.
+ *
+ * @returns {jQuery}
+ */
+ editable: function () {
+ return $('#wrapwrap [data-oe-model]')
+ .not('.o_not_editable')
+ .filter(function () {
+ return !$(this).closest('.o_not_editable').length;
+ })
+ .not('link, script')
+ .not('[data-oe-readonly]')
+ .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]')
+ .not('.oe_snippet_editor')
+ .add('.o_editable');
+ },
+ /**
+ * Records the current state of the given $target to be able to undo future
+ * changes.
+ *
+ * @see History.recordUndo
+ * @param {jQuery} $target
+ * @param {string} event
+ * @param {boolean} internal_history
+ */
+ historyRecordUndo: function ($target, event, internal_history) {
+ var rng = range.create();
+ var $editable = $(rng && rng.sc).closest('.o_editable');
+ if (!rng || !$editable.length) {
+ $editable = $($target).closest('.o_editable');
+ rng = range.create($target.closest('*')[0],0);
+ } else {
+ rng = $editable.data('range') || rng;
+ }
+ rng.select();
+ history.recordUndo($editable, event, internal_history);
+ },
+ /**
+ * Searches all the dirty element on the page and saves them one by one. If
+ * one cannot be saved, this notifies it to the user and restarts rte
+ * edition.
+ *
+ * @param {Object} [context] - the context to use for saving rpc, default to
+ * the editor context found on the page
+ * @return {Deferred} rejected if the save cannot be done
+ */
save: function (context) {
var self = this;
- this.__saved = {}; // list of allready saved views and data
-
- var defs = $('.o_dirty')
+ var $dirty = $('.o_dirty');
+ $dirty
.removeAttr('contentEditable')
- .removeClass('o_dirty oe_carlos_danger o_is_inline_editable')
- .map(function () {
- var $el = $(this);
+ .removeClass('o_dirty oe_carlos_danger o_is_inline_editable');
+ var defs = _.map($dirty, function (el) {
+ var $el = $(el);
- $el.find('[class]').filter(function () {
- if (!this.className.match(/\S/)) {
- this.removeAttribute("class");
- }
- });
+ $el.find('[class]').filter(function () {
+ if (!this.className.match(/\S/)) {
+ this.removeAttribute('class');
+ }
+ });
- // TODO: Add a queue with concurrency limit in webclient
- // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
- return self.saving_mutex.exec(function () {
- return self.saveElement($el, context)
- .then(undefined, function (thing, response) {
- // because ckeditor regenerates all the dom,
- // we can't just setup the popover here as
- // everything will be destroyed by the DOM
- // regeneration. Add markings instead, and
- // returns a new rejection with all relevant
- // info
- var id = _.uniqueId('carlos_danger_');
- $el.addClass('o_dirty oe_carlos_danger ' + id);
- return $.Deferred().reject({
- id: id,
- error: response.data,
- });
- });
+ // TODO: Add a queue with concurrency limit in webclient
+ // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
+ return self.saving_mutex.exec(function () {
+ return self._saveElement($el, context || weContext.get())
+ .then(function () {
+ $el.removeClass('o_dirty');
+ }, function (response) {
+ // because ckeditor regenerates all the dom, we can't just
+ // setup the popover here as everything will be destroyed by
+ // the DOM regeneration. Add markings instead, and returns a
+ // new rejection with all relevant info
+ var id = _.uniqueId('carlos_danger_');
+ $el.addClass('o_dirty oe_carlos_danger ' + id);
+ var html = (response.data.exception_type === 'except_osv');
+ if (html) {
+ var msg = $('<div/>', {text: response.data.message}).html();
+ var data = msg.substring(3, msg.length -2).split(/', u'/);
+ response.data.message = '<b>' + data[0] + '</b>' + data[1];
+ }
+ $('.o_editable.' + id)
+ .removeClass(id)
+ .popover({
+ html: html,
+ trigger: 'hover',
+ content: response.data.message,
+ placement: 'auto top',
+ })
+ .popover('show');
});
- }).get();
+ });
+ });
- return $.when.apply(null, defs).then(function () {
+ return $.when.apply($, defs).then(function () {
window.onbeforeunload = null;
}, function (failed) {
// If there were errors, re-enable edition
- self.stop();
+ self.cancel();
self.start();
- // jquery's deferred being a pain in the ass
- if (!_.isArray(failed)) { failed = [failed]; }
-
- _(failed).each(function (failure) {
- var html = failure.error.exception_type === "except_osv";
- if (html) {
- var msg = $("<div/>").text(failure.error.message).html();
- var data = msg.substring(3,msg.length-2).split(/', u'/);
- failure.error.message = '<b>' + data[0] + '</b>' + data[1];
- }
- $('.o_editable.' + failure.id)
- .removeClass(failure.id)
- .popover({
- html: html,
- trigger: 'hover',
- content: failure.error.message,
- placement: 'auto top',
- })
- // Force-show popovers so users will notice them.
- .popover('show');
- });
});
},
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
/**
- * Get HTML cloned element with text nodes escaped for XML storage
+ * When the users clicks on an editable element, this function allows to add
+ * external behaviors.
+ *
+ * @private
+ * @param {jQuery} $editable
*/
- getEscapedElement: function ($el) {
+ _enableEditableArea: 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();
+ }
+ },
+ /**
+ * When an element enters edition, summernote is initialized on it. This
+ * function returns the default configuration for the summernote instance.
+ *
+ * @see _getConfig
+ * @private
+ * @param {jQuery} $editable
+ * @returns {Object}
+ */
+ _getDefaultConfig: function ($editable) {
+ return {
+ 'airMode' : true,
+ 'focus': false,
+ 'airPopover': [
+ ['style', ['style']],
+ ['font', ['bold', 'italic', 'underline', 'clear']],
+ ['fontsize', ['fontsize']],
+ ['color', ['color']],
+ ['para', ['ul', 'ol', 'paragraph']],
+ ['table', ['table']],
+ ['insert', ['link', 'picture']],
+ ['history', ['undo', 'redo']],
+ ],
+ 'styleWithSpan': false,
+ 'inlinemedia' : ['p'],
+ 'lang': 'odoo',
+ 'onChange': function (html, $editable) {
+ $editable.trigger('content_changed');
+ }
+ };
+ },
+ /**
+ * Gets jQuery cloned element with internal text nodes escaped for XML
+ * storage.
+ *
+ * @private
+ * @param {jQuery} $el
+ * @return {jQuery}
+ */
+ _getEscapedElement: function ($el) {
var escaped_el = $el.clone();
var to_escape = escaped_el.find('*').addBack();
to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack());
to_escape.contents().each(function () {
- if(this.nodeType === 3) {
+ if (this.nodeType === 3) {
this.nodeValue = $('<div />').text(this.nodeValue).html();
}
});
return escaped_el;
},
-
- saveElement: function ($el, context) {
- // remove multi edition
- if ($el.data('oe-model')) {
- var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression');
- if (this.__saved[key]) return $.when();
- this.__saved[key] = true;
- }
- var markup = this.getEscapedElement($el).prop('outerHTML');
-
- return ajax.jsonRpc('/web/dataset/call', 'call', {
+ /**
+ * Saves one (dirty) element of the page.
+ *
+ * @private
+ * @param {jQuery} $el - the element to save
+ * @param {Object} context - the context to use for the saving rpc
+ * @param {boolean} [withLang=false]
+ * false if the lang must be omitted in the context (saving "master"
+ * page element)
+ */
+ _saveElement: function ($el, context, withLang) {
+ return this._rpc({
model: 'ir.ui.view',
method: 'save',
args: [
$el.data('oe-id'),
- markup,
+ this._getEscapedElement($el).prop('outerHTML'),
$el.data('oe-xpath') || null,
- _.omit(context || base.get_context(), 'lang')
+ withLang ? context : _.omit(context, 'lang')
],
});
},
- stop: function () {
- if (this.$last) {
- this.$last.destroy();
- this.$last = null;
- }
-
- $.fn.carousel = this.init_bootstrap_carousel;
-
- $(document).off(".rte");
- $('#wrapwrap, .o_editable').off('.rte');
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
- $('.o_not_editable').removeAttr("contentEditable");
- $(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
- $(document).tooltip('destroy');
- $('body').removeClass("editor_enable");
- this.trigger('rte:stop');
+ /**
+ * Called when any editable element is clicked -> Prevents default browser
+ * action for the element.
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onClick: function (e) {
+ e.preventDefault();
},
-
- cancel: function () {
- $('.o_editable').each(function () {
- var $node = $(this);
- $node.data('initInnerHTML', $node.html());
- });
- this.stop();
+ /**
+ * Called when a demand to open a alt dialog is received on the bus.
+ *
+ * @private
+ * @param {Object} data
+ */
+ _onAltDialogDemand: function (data) {
+ var altDialog = new weWidgets.alt(this,
+ data.options || {},
+ data.$editable,
+ data.media
+ );
+ if (data.onSave) {
+ altDialog.on('save', this, data.onSave);
+ }
+ if (data.onCancel) {
+ altDialog.on('cancel', this, data.onCancel);
+ }
+ altDialog.open();
},
-
- onClick: function (e) {
- e.preventDefault();
+ /**
+ * Called when a demand to open a link dialog is received on the bus.
+ *
+ * @private
+ * @param {Object} data
+ */
+ _onLinkDialogDemand: function (data) {
+ var linkDialog = new weWidgets.LinkDialog(this,
+ data.options || {},
+ data.$editable,
+ data.linkInfo
+ );
+ if (data.onSave) {
+ linkDialog.on('save', this, data.onSave);
+ }
+ if (data.onCancel) {
+ linkDialog.on('cancel', this, data.onCancel);
+ }
+ linkDialog.open();
},
-
- // handler for cancel editor
- onKeydown: function (event) {
- if (event.keyCode === 27 && !$('.modal-content:visible').length) {
- _.defer(function () {
- $('#editor-top-navbar [data-action="cancel"]').click();
- var $modal = $('.modal-content > .modal-body').parents(".modal:first");
- $modal.off('keyup.dismiss.bs.modal');
- _.delay(function () {
- $modal.on('keyup.dismiss.bs.modal', function () {
- $(this).modal('hide');
- });
- }, 500);
- });
- }
+ /**
+ * Called when a demand to open a media dialog is received on the bus.
+ *
+ * @private
+ * @param {Object} data
+ */
+ _onMediaDialogDemand: function (data) {
+ var mediaDialog = new weWidgets.MediaDialog(this,
+ data.options || {},
+ data.$editable,
+ data.media
+ );
+ if (data.onSave) {
+ mediaDialog.on('save', this, data.onSave);
+ }
+ if (data.onCancel) {
+ mediaDialog.on('cancel', this, data.onCancel);
+ }
+ mediaDialog.open();
},
-
- // activate editor
- onMousedown: function (event) {
- var $target = $(event.target);
+ /**
+ * Called when the mouse is pressed on the document -> activate element
+ * edition.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMousedown: function (ev) {
+ var $target = $(ev.target);
var $editable = $target.closest('.o_editable');
if (!$editable.length) {
@@ -534,8 +673,8 @@ var RTE = Widget.extend({
this.$last = null;
}
if ($editable.length && (!this.$last || this.$last[0] !== $editable[0]) &&
- ($target.closest('[contenteditable]').attr('contenteditable') || "").toLowerCase() !== 'false') {
- $editable.summernote(this.config($editable));
+ ($target.closest('[contenteditable]').attr('contenteditable') || '').toLowerCase() !== 'false') {
+ $editable.summernote(this._getConfig($editable));
$editable.data('NoteHistory', history);
this.$last = $editable;
@@ -545,7 +684,7 @@ var RTE = Widget.extend({
document.execCommand('enableObjectResizing', false, false);
document.execCommand('enableInlineTableEditing', false, false);
document.execCommand('2D-position', false, false);
- } catch (e) {}
+ } catch (e) { /* */ }
document.body.addEventListener('resizestart', function (evt) {evt.preventDefault(); return false;});
document.body.addEventListener('movestart', function (evt) {evt.preventDefault(); return false;});
document.body.addEventListener('dragstart', function (evt) {evt.preventDefault(); return false;});
@@ -558,15 +697,17 @@ var RTE = Widget.extend({
$target.trigger('mousedown'); // for activate selection on picture
}
- this.onEnableEditableArea($editable);
+ this._enableEditableArea($editable);
}
},
-
- onEnableEditableArea: function ($editable) {
- },
-
- onMouseup: function (event) {
- var $target = $(event.target);
+ /**
+ * Called when the mouse is unpressed on the document.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onMouseup: function (ev) {
+ var $target = $(ev.target);
var $editable = $target.closest('.o_editable');
if (!$editable.length) {
@@ -582,56 +723,17 @@ var RTE = Widget.extend({
// whitespace characters (so Tripleclicking then typing text will remove
// the whole paragraph instead of its content).
// http://stackoverflow.com/questions/38467334/why-does-google-chrome-always-add-space-after-selected-text
- if ($.browser.chrome === true && event.originalEvent.detail === 3) {
+ if ($.browser.chrome === true && ev.originalEvent.detail === 3) {
var currentSelection = range.create();
if (currentSelection.sc.parentNode === currentSelection.ec) {
range.create(currentSelection.sc, currentSelection.so, currentSelection.sc, currentSelection.sc.length).select();
}
}
},
-
- editable: function () {
- return $('#wrapwrap [data-oe-model]')
- .not('.o_not_editable')
- .filter(function () {
- return !$(this).closest('.o_not_editable').length;
- })
- .not('link, script')
- .not('[data-oe-readonly]')
- .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]')
- .not('.oe_snippet_editor')
- .add('.o_editable');
- },
-
- config: function ($editable) {
- return {
- 'airMode' : true,
- 'focus': false,
- 'airPopover': [
- ['style', ['style']],
- ['font', ['bold', 'italic', 'underline', 'clear']],
- ['fontsize', ['fontsize']],
- ['color', ['color']],
- ['para', ['ul', 'ol', 'paragraph']],
- ['table', ['table']],
- ['insert', ['link', 'picture']],
- ['history', ['undo', 'redo']],
- ],
- 'styleWithSpan': false,
- 'inlinemedia' : ['p'],
- 'lang': "odoo",
- 'onChange': function (html, $editable) {
- $editable.trigger("content_changed");
- }
- };
- },
});
-
-var data = {
+return {
+ Class: RTEWidget,
history: history,
- Class: RTE
};
-return data;
-
});
diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js
index 979211cd4d7..7067ab28af6 100644
--- a/addons/web_editor/static/src/js/editor/rte.summernote.js
+++ b/addons/web_editor/static/src/js/editor/rte.summernote.js
@@ -3,39 +3,32 @@ odoo.define('web_editor.rte.summernote', function (require) {
var core = require('web.core');
var ajax = require('web.ajax');
-var base = require('web_editor.base');
+var weContext = require('web_editor.context');
var widgets = require('web_editor.widget');
var rte = require('web_editor.rte');
var QWeb = core.qweb;
var _t = core._t;
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
ajax.jsonRpc('/web/dataset/call', 'call', {
'model': 'ir.ui.view',
'method': 'read_template',
- 'args': ['web_editor.colorpicker', base.get_context()]
+ 'args': ['web_editor.colorpicker', weContext.get()]
}).done(function (data) {
QWeb.add_template(data);
});
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* Summernote Lib (neek change to make accessible: method and object) */
-
+// Summernote Lib (neek change to make accessible: method and object)
var dom = $.summernote.core.dom;
var range = $.summernote.core.range;
var eventHandler = $.summernote.eventHandler;
var renderer = $.summernote.renderer;
-// var options = $.summernote.options;
var tplButton = renderer.getTemplate().button;
var tplIconButton = renderer.getTemplate().iconButton;
var tplDropdown = renderer.getTemplate().dropdown;
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* update and change the popovers content, and add history button */
-
+// Update and change the popovers content, and add history button
var fn_createPalette = renderer.createPalette;
renderer.createPalette = function ($container, options) {
fn_createPalette.call(this, $container, options);
@@ -322,10 +315,8 @@ eventHandler.modules.handle.update = function ($handle, oStyle, isAirMode) {
}
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* hack for image and link editor */
-
-function getImgTarget ($editable) {
+// Hack for image and link editor
+function getImgTarget($editable) {
var $handle = $editable ? dom.makeLayoutInfo($editable).handle() : undefined;
return $(".note-control-selection", $handle).data('target');
}
@@ -375,18 +366,18 @@ eventHandler.modules.linkDialog.showLinkDialog = function ($editable, $dialog, l
$editable.data('range').select();
$editable.data('NoteHistory').recordUndo();
- var editor = new widgets.LinkDialog(null, {}, $editable, linkInfo).open();
-
var def = new $.Deferred();
- editor.on("save", this, function (linkInfo) {
- linkInfo.range.select();
- $editable.data('range', linkInfo.range);
- def.resolve(linkInfo);
- $editable.trigger('keyup');
- $('.note-popover .note-link-popover').show();
- });
- editor.on("cancel", this, function () {
- def.reject();
+ core.bus.trigger('link_dialog_demand', {
+ $editable: $editable,
+ linkInfo: linkInfo,
+ onSave: function (linkInfo) {
+ linkInfo.range.select();
+ $editable.data('range', linkInfo.range);
+ def.resolve(linkInfo);
+ $editable.trigger('keyup');
+ $('.note-popover .note-link-popover').show();
+ },
+ onCancel: def.reject.bind(def),
});
return def;
};
@@ -395,18 +386,22 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
if (r.sc.tagName && r.sc.childNodes.length) {
r.sc = r.sc.childNodes[r.so];
}
- new widgets.MediaDialog(null, {}, $editable, dom.isImg(r.sc) ? r.sc : null).open();
+ core.bus.trigger('media_dialog_demand', {
+ $editable: $editable,
+ media: dom.isImg(r.sc) ? r.sc : null,
+ });
return new $.Deferred().reject();
};
$.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) {
var $editable = layoutInfo.editable();
var $selection = layoutInfo.handle().find('.note-control-selection');
- var media = $selection.data('target');
- new widgets.alt(null, {}, $editable, media).open();
+ core.bus.trigger('alt_dialog_demand', {
+ $editable: $editable,
+ media: $selection.data('target'),
+ });
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
+// Utils
var fn_is_void = dom.isVoid || function () {};
dom.isVoid = function (node) {
return fn_is_void(node) || dom.isImgFont(node) || (node && node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i));
@@ -440,13 +435,11 @@ dom.isFont = function (node) {
return fn_is_font(node) || dom.isImgFont(node);
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
var fn_visible = $.summernote.pluginEvents.visible;
$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
var res = fn_visible.apply(this, arguments);
var rng = range.create();
- if(!rng) return res;
+ if (!rng) return res;
var $node = $(dom.node(rng.sc));
if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) &&
$node.hasClass("o_editable") &&
@@ -582,11 +575,12 @@ $.summernote.pluginEvents.codeview = function (event, editor, layoutInfo, enable
}
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* fix ie and re-range to don't break snippet*/
-
+// Fix ie and re-range to don't break snippet
+var last_div;
+var last_div_change;
+var last_editable;
var initial_data = {};
-function reRangeSelectKey (event) {
+function reRangeSelectKey(event) {
initial_data.range = null;
if (event.shiftKey && event.keyCode >= 37 && event.keyCode <= 40 && !$(event.target).is("input, textarea, select")) {
var r = range.create();
@@ -598,7 +592,7 @@ function reRangeSelectKey (event) {
}
}
}
-function reRangeSelect (event, dx, dy) {
+function reRangeSelect(event, dx, dy) {
var r = range.create();
if (!r || r.isCollapsed()) return;
@@ -615,7 +609,7 @@ function reRangeSelect (event, dx, dy) {
$(data.sc).closest('.o_editable').data('range', r);
return r;
}
-function summernote_mouseup (event) {
+function summernote_mouseup(event) {
if ($(event.target).closest("#web_editor-top-navbar, .note-popover").length) {
return;
}
@@ -638,7 +632,7 @@ function summernote_mouseup (event) {
}
}
var remember_selection;
-function summernote_mousedown (event) {
+function summernote_mousedown(event) {
rte.history.splitNext();
var $editable = $(event.target).closest(".o_editable, .note-editor");
@@ -663,19 +657,19 @@ function summernote_mousedown (event) {
var r_editable = editables.has((r||{}).sc);
if (!r_editable.closest('.note-editor').is($editable) && !r_editable.filter('.o_editable').is(editables)) {
var saved_editable = editables.has((remember_selection||{}).sc);
- if($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) {
+ if ($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) {
remember_selection = range.create(dom.firstChild($editable[0]), 0);
- } else if(!saved_editable.length) {
+ } else if (!saved_editable.length) {
remember_selection = undefined;
}
- if(remember_selection) {
+ if (remember_selection) {
try {
remember_selection.select();
} catch (e) {
console.warn(e);
}
}
- } else if(r_editable.length) {
+ } else if (r_editable.length) {
remember_selection = r;
}
@@ -691,18 +685,15 @@ function summernote_mousedown (event) {
}
}
-var last_div;
-var last_div_change;
-var last_editable;
-function summernote_ie_fix (event, pred) {
+function summernote_ie_fix(event, pred) {
var editable;
var div;
var node = event.target;
- while(node.parentNode) {
+ while (node.parentNode) {
if (!div && pred(node)) {
div = node;
}
- if(last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) {
+ if (last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) {
break;
}
if (node.className && node.className.indexOf('o_editable') !== -1) {
@@ -772,13 +763,13 @@ eventHandler.attach = function (oLayoutInfo, options) {
eventHandler.modules.linkDialog.show(oLayoutInfo);
});
- if(oLayoutInfo.editor().is('[data-oe-model][data-oe-type="image"]')) {
+ if (oLayoutInfo.editor().is('[data-oe-model][data-oe-type="image"]')) {
oLayoutInfo.editor().on('click', 'img', function (event) {
$(event.target).trigger("dblclick");
});
}
oLayoutInfo.editable().on('mousedown', function (e) {
- if(dom.isImg(e.target)) {
+ if (dom.isImg(e.target)) {
range.createFromNode(e.target).select();
}
});
@@ -875,9 +866,7 @@ eventHandler.detach = function (oLayoutInfo, options) {
$(document).off("keyup", reRangeSelectKey);
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* Translation for odoo */
-
+// Translation for odoo
$.summernote.lang.odoo = {
font: {
bold: _t('Bold'),
diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js
index a0bfb8b300c..c0299fc2969 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -1,612 +1,656 @@
odoo.define('web_editor.snippet.editor', function (require) {
'use strict';
-var Class = require('web.Class');
-var ajax = require('web.ajax');
var core = require('web.core');
var dom = require('web.dom');
var Widget = require('web.Widget');
-var editor = require('web_editor.editor');
-var animation = require('web_editor.snippets.animation');
var options = require('web_editor.snippets.options');
-var qweb = core.qweb;
var _t = core._t;
-ajax.loadXML('/web_editor/static/src/xml/snippets.xml', qweb);
-
-var data = {};
-
-/* ----- SNIPPET SELECTOR ---- */
-
-data.globalSelector = {
+var globalSelector = {
closest: function () { return $(); },
all: function () { return $(); },
is: function () { return false; },
};
/**
- * Snippet editor main class.
- * Management of drag&drop and focus/blur of snippets.
- * Previously named "BuildingBlock".
+ * Management of the overlay and option list for a snippet.
*/
-data.Class = Widget.extend({
- id: "oe_snippets",
- activeSnippets: [],
- init: function (parent, $editable) {
- this.$editable = $editable;
+var SnippetEditor = Widget.extend({
+ template: 'web_editor.snippet_overlay',
+ xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
+ events: {
+ 'click .oe_snippet_parent': '_onParentButtonClick',
+ 'click .oe_snippet_clone': '_onCloneClick',
+ 'click .oe_snippet_remove': '_onRemoveClick',
+ },
+ custom_events: {
+ cover_update: '_onCoverUpdate',
+ option_update: '_onOptionUpdate',
+ },
+ /**
+ * @constructor
+ * @param {Widget} parent
+ * @param {DOMElement} target
+ */
+ init: function (parent, target, templateOptions) {
this._super.apply(this, arguments);
- if(!$('#oe_manipulators').length) {
- $("<div/>", {id: "oe_manipulators", 'class': "o_css_editor"}).appendTo('body');
- }
- this.$active_snipped_id = false;
- this.snippets = [];
-
- data.instance = this;
+ this.$target = $(target);
+ this.$target.data('snippet-editor', this);
+ this.templateOptions = templateOptions;
},
+ /**
+ * @override
+ */
start: function () {
var self = this;
+ var defs = [this._super.apply(this, arguments)];
- $(window).resize(function () {
- _.defer(function () { $(document).click(); });
- });
+ // Hide parent button if no parent
+ var $parent = globalSelector.closest(this.$target.parent());
+ if (!$parent.length) {
+ this.$('.oe_snippet_parent').remove();
+ }
- this.fetch_snippet_templates();
- this.bind_snippet_click_editor();
+ // Initialize the associated options (see snippets.options.js)
+ defs.push(this._initializeOptions());
- $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) {
- e.preventDefault();
- });
+ // Initialize move/clone/remove buttons
+ if (!this.$target.parent().is(':o_editable')) {
+ this.$el.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
+ } else {
+ this.dropped = false;
+ this.$el.draggable({
+ greedy: true,
+ appendTo: 'body',
+ cursor: 'move',
+ handle: '.oe_snippet_move',
+ cursorAt: {
+ left: 18,
+ top: 14
+ },
+ helper: function () {
+ var $clone = $(this).clone().css({width: '24px', height: '24px', border: 0});
+ $clone.find('.oe_overlay_options >:not(:contains(.oe_snippet_move)), .oe_handle').remove();
+ $clone.find(':not(.glyphicon)').css({position: 'absolute', top: 0, left: 0});
+ $clone.appendTo('body').removeClass('hidden');
+ return $clone;
+ },
+ start: _.bind(self._onDragAndDropStart, self),
+ stop: _.bind(self._onDragAndDropStop, self)
+ });
+ }
- var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
- $.summernote.core.dom.isNotBreakable = function (node) {
- return _isNotBreakable(node) || $(node).is('div') || data.globalSelector.is($(node));
- };
+ return $.when.apply($, defs);
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ this._super.apply(this, arguments);
+ this.$target.removeData('snippet-editor');
+ },
- $(window).on('resize', function () {
- if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
- self.cover_target(self.$active_snipped_id.data("snippet-editor").$overlay, self.$active_snipped_id);
- }
- });
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
- $(document).on('mousemove', function () {
- if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
- self.$active_snipped_id.data("snippet-editor").$overlay.removeClass('o_keypress');
- }
+ /**
+ * Notifies all the associated snippet options that the snippet has just
+ * been dropped in the page.
+ */
+ buildSnippet: function () {
+ for (var i in this.styles) {
+ this.styles[i].onBuilt();
+ }
+ },
+ /**
+ * Notifies all the associated snippet options that the template which
+ * contains the snippet is about to be saved.
+ */
+ cleanForSave: function () {
+ if (this.isDestroyed()) {
+ return;
+ }
+ _.each(this.styles, function (option) {
+ option.cleanForSave();
});
- $(document).on('keydown', function (event) {
- if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
- self.$active_snipped_id.data("snippet-editor").$overlay.addClass('o_keypress');
- }
- if ((event.metaKey || (event.ctrlKey && !event.altKey)) && event.shiftKey && event.keyCode >= 48 && event.keyCode <= 57) {
- self.$('.scroll:first > ul li:eq('+(event.keyCode-49)+') a').trigger("click");
- event.preventDefault();
- }
+ },
+ /**
+ * Makes the editor overlay cover the associated snippet.
+ */
+ cover: function () {
+ var mt = parseInt(this.$target.css('margin-top') || 0);
+ var offset = this.$target.offset();
+ var manipulatorOffset = this.$el.parent().offset();
+ offset.top -= (manipulatorOffset.top + mt);
+ offset.left -= manipulatorOffset.left;
+ this.$el.css({
+ width: this.$target.outerWidth(),
+ left: offset.left,
+ top: offset.top,
});
+ this.$('.oe_handles').css('height', this.$target.outerHeight(true));
+ this.$el.toggleClass('o_top_cover', offset.top < 15);
},
+ /**
+ * Displays/Hides the editor overlay and notifies the associated snippet
+ * options. Note: when it is displayed, this is here that the parent
+ * snippet options are moved to the editor overlay.
+ *
+ * @param {boolean} focus - true to display, false to hide
+ */
+ toggleFocus: function (focus) {
+ var do_action = (focus ? _do_action_focus : _do_action_blur);
- _get_snippet_url: function () {
- return '/web_editor/snippets';
- },
- _add_check_selector : function (selector, no_check, is_children) {
- var self = this;
- selector = selector.split(/\s*,/).join(":not(.o_snippet_not_selectable), ") + ":not(.o_snippet_not_selectable)";
+ // Attach own and parent options on the current overlay
+ var $style_button = this.$el.find('.oe_options');
+ var $ul = $style_button.find('ul:first');
+ var $headers = $ul.find('.dropdown-header:data(editor)');
+ _.each($headers, (function (el) {
+ var $el = $(el);
+ var styles = _.values($el.data('editor').styles);
+ if ($el.data('editor') !== this) {
+ styles = _.filter(styles, function (option) { return !option.preventChildPropagation; });
+ }
- if (no_check) {
- return {
- closest: function ($from, parentNode) {
- return $from.closest(selector, parentNode);
- },
- all: function ($from) {
- return $from ? dom.cssFind($from, selector) : $(selector);
- },
- is: function ($from) {
- return $from.is(selector);
- }
- };
- } else {
- selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable";
- return {
- closest: function ($from, parentNode) {
- var parents = self.$editable.get();
- return $from.closest(selector, parentNode).filter(function () {
- var node = this;
- while (node.parentNode) {
- if (parents.indexOf(node)!==-1) {
- return true;
- }
- node = node.parentNode;
- }
- return false;
- });
- },
- all: is_children ? function ($from) {
- return dom.cssFind($from || self.$editable, selector);
- } : function ($from) {
- $from = $from || self.$editable;
- return $from.filter(selector).add(dom.cssFind($from, selector));
- },
- is: function ($from) {
- return $from.is(selector);
+ var count = 0;
+ _.each(_.sortBy(styles, '__order').reverse(), function (style) {
+ if (do_action(style, $el)) {
+ count++;
}
- };
+ });
+ $el.toggleClass('hidden', count === 0);
+ }).bind(this));
+
+ // Activate the overlay
+ $style_button.toggleClass('hidden', $ul.children(':not(.o_main_header):not(.divider):not(.hidden)').length === 0);
+ this.cover();
+ this.$el.toggleClass('oe_active', !!focus);
+
+ function _do_action_focus(style, $dest) {
+ style.$el.insertAfter($dest);
+ style.onFocus();
+ return (style.$el.length > 0);
+ }
+ function _do_action_blur(style, $dest) {
+ style.$el.detach();
+ style.onBlur();
+ return false;
}
},
- fetch_snippet_templates: function () {
- var self = this;
- var url = this._get_snippet_url();
- if (!url || !url.length) {
- this.$el.detach();
- return;
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * DOMElements have a default name which appears in the overlay when they
+ * are being edited. This method retrieves this name; it can be defined
+ * directly in the DOM thanks to the `data-name` attribute.
+ *
+ * @private
+ */
+ _getName: function () {
+ if (this.$target.data('name') !== undefined) {
+ return this.$target.data('name');
}
- return ajax.jsonRpc(url, 'call', {}).then(function (html) {
- self.compute_snippet_templates(html);
- self.trigger("snippets:ready");
- }, function () {
- console.warn('Snippets template not found:', url);
- });
+ if (this.$target.parent('.row').length) {
+ return _t("Column");
+ }
+ return _t("Block");
},
- compute_snippet_templates: function (html) {
+ /**
+ * Instantiates the snippet's options.
+ *
+ * @private
+ */
+ _initializeOptions: function () {
var self = this;
- var $html = $(html);
- var $scroll = $html.siblings("#o_scroll");
-
- if (!$scroll.length) {
- throw new Error("Wrong snippets xml definition");
- }
-
- // t-snippet
- $html.find('[data-oe-type="snippet"]').each(function () {
- $(this).children().attr('data-oe-type', "snippet").attr('data-oe-thumbnail', $(this).data('oe-thumbnail'));
- });
- // end
+ var $styles = this.$el.find('.oe_options');
+ var $ul = $styles.find('ul:first');
+ this.styles = {};
+ this.selectorSiblings = [];
+ this.selectorChildren = [];
- self.templateOptions = [];
- var selector = [];
- var $styles = $html.find("[data-js], [data-selector]");
- $styles.each(function () {
- var $style = $(this);
- var no_check = $style.data('no-check');
- var option_id = $style.data('js');
- var option = {
- 'option' : option_id,
- 'base_selector': $style.data('selector'),
- 'selector': self._add_check_selector($style.data('selector'), no_check),
- '$el': $style,
- 'drop-near': $style.data('drop-near') && self._add_check_selector($style.data('drop-near'), no_check, true),
- 'drop-in': $style.data('drop-in') && self._add_check_selector($style.data('drop-in'), no_check),
- 'data': $style.data()
- };
- self.templateOptions.push(option);
- selector.push(option.selector);
+ var i = 0;
+ $ul.append($('<li/>', {class: 'dropdown-header o_main_header', text: this._getName()}).data('editor', this));
+ var defs = _.map(this.templateOptions, function (val, option_id) {
+ if (!val.selector.is(self.$target)) {
+ return;
+ }
+ if (val['drop-near']) self.selectorSiblings.push(val['drop-near']);
+ if (val['drop-in']) self.selectorChildren.push(val['drop-in']);
+
+ var optionName = val.option;
+ var $el = val.$el.children('li').clone(true).addClass('snippet-option-' + optionName);
+ var option = new (options.registry[optionName] || options.Class)(self, self.$target, self.$el);
+ self.styles[optionName || _.uniqueId('option')] = option;
+ option.__order = i++;
+ return option.attachTo($el);
});
+ $ul.append($('<li/>', {class: 'divider'}));
- $styles.addClass("hidden");
- data.globalSelector.closest = function ($from) {
- var $temp;
- var $target;
- var len = selector.length;
- for (var i = 0; i<len; i++) {
- $temp = selector[i].closest($from, $target && $target[0]);
- if (!$target || $temp.length) {
- $target = $temp;
- }
- }
- return $target;
- };
- data.globalSelector.all = function ($from) {
- var $target;
- var len = selector.length;
- for (var i = 0; i<len; i++) {
- if (!$target) $target = selector[i].all($from);
- else $target = $target.add(selector[i].all($from));
- }
- return $target;
- };
- data.globalSelector.is = function ($from) {
- var len = selector.length;
- for (var i = 0; i<len; i++) {
- if (selector[i].is($from)) {
- return true;
+ var $parents = this.$target.parents();
+ _.each($parents, function (parent) {
+ var parentEditor = $(parent).data('snippet-editor');
+ if (parentEditor) {
+ for (var styleName in parentEditor.styles) {
+ if (!parentEditor.styles[styleName].preventChildPropagation) {
+ $ul.append($('<li/>', {class: 'dropdown-header o_parent_editor_header', text: parentEditor._getName()}).data('editor', parentEditor));
+ break;
+ }
}
}
- return false;
- };
+ });
- var number = 0;
+ if (!this.selectorSiblings.length && !this.selectorChildren.length) {
+ this.$el.find('.oe_snippet_move, .oe_snippet_clone').addClass('hidden');
+ }
- // oe_snippet_body
- self.$snippets = $scroll.find(".o_panel_body").children()
- .addClass("oe_snippet")
- .each(function () {
- var $snippet = $(this);
- if (!$('.oe_snippet_thumbnail', this).size()) {
- var $div = $(
- '<div class="oe_snippet_thumbnail">'+
- '<div class="oe_snippet_thumbnail_img"/>'+
- '<span class="oe_snippet_thumbnail_title"></span>'+
- '</div>');
- $div.find('span').text($snippet.attr("name"));
- $snippet.prepend($div);
-
- // from t-snippet
- var thumbnail = $("[data-oe-thumbnail]", this).data("oe-thumbnail");
- if (thumbnail) {
- $div.find('.oe_snippet_thumbnail_img').css('background-image', 'url(' + thumbnail + ')');
- }
- // end
-
- var moduleID = $snippet.data('moduleId');
- if (moduleID) {
- $snippet.addClass('o_snippet_install');
- var $installBtn = $('<a/>', {
- class: 'btn btn-primary btn-sm o_install_btn',
- target: '_blank',
- href: '/web#id=' + moduleID + '&view_type=form&model=ir.module.module&action=base.open_module_tree',
- text: _t("Install"),
- });
- $div.append($installBtn);
- }
- }
- if (!$snippet.data("selector")) {
- $("> *:not(.oe_snippet_thumbnail)", this).addClass('oe_snippet_body');
+ this.$el.find('[data-toggle="dropdown"]').dropdown();
+
+ return $.when.apply($, defs);
+ },
+ /**
+ * Removes the associated snippet from the DOM and destroys the associated
+ * editor (itself).
+ *
+ * @private
+ */
+ _removeSnippet: function () {
+ this.toggleFocus(false);
+
+ this.trigger_up('call_for_each_child_snippet', {
+ $snippet: this.$target,
+ callback: function (editor, $snippet) {
+ for (var i in editor.styles) {
+ editor.styles[i].onRemove();
}
- number++;
- })
- .not('[data-module-id]');
+ },
+ });
- // hide scroll if no snippets defined
- if (!number) {
- this.$el.detach();
- }
- $("body").toggleClass("editor_has_snippets", !!number);
+ var $parent = this.$target.parent();
+ this.$target.find('*').andSelf().tooltip('destroy');
+ this.$target.remove();
+ this.$el.remove();
- // select all default text to edit (if snippet default text)
- this.add_default_snippet_text_classes();
- $(document).on("click", ".o_default_snippet_text", function (event) {
- $(event.target).selectContent();
- });
- $(document).on("keyup", function (event) {
- var r = $.summernote.core.range.create();
- $(r && r.sc).closest(".o_default_snippet_text").removeClass("o_default_snippet_text");
- });
+ var node = $parent[0];
+ if (node && node.firstChild) {
+ $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
+ if (!node.firstChild.tagName && node.firstChild.textContent === ' ') {
+ node.removeChild(node.firstChild);
+ }
+ }
- // clean t-oe
- $html.find('[data-oe-model], [data-oe-type]').each(function () {
- for (var k=0; k<this.attributes.length; k++) {
- if (this.attributes[k].name.indexOf('data-oe-') === 0) {
- $(this).removeAttr(this.attributes[k].name);
- k--;
+ if ($parent.closest(':data("snippet-editor")').length) {
+ while (!$parent.data('snippet-editor')) {
+ var $nextParent = $parent.parent();
+ if ($parent.children().length === 0 && $parent.text().trim() === '' && !$parent.hasClass('oe_structure')) {
+ $parent.remove();
}
+ $parent = $nextParent;
}
- });
+ if ($parent.children().length === 0 && $parent.text().trim() === '' && !$parent.hasClass('oe_structure')) {
+ _.defer(function () {
+ $parent.data('snippet-editor')._removeSnippet();
+ });
+ }
+ }
- $html.find('.o_not_editable').attr("contentEditable", false);
+ // clean editor if they are image or table in deleted content
+ $('.note-control-selection').hide();
+ $('.o_table_handler').remove();
- this.$el.html($html);
+ this.trigger_up('snippet_removed');
+ this.destroy();
+ },
- self.make_snippet_draggable(self.$snippets);
- this.associate_snippet_names(this.$snippets);
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
- this.show_blocks();
- this.$el.on("snippet-dropped snippet-removed", this.show_blocks.bind(this));
- },
+ /**
+ * Called when the 'clone' button is clicked.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onCloneClick: function (ev) {
+ ev.preventDefault();
+ var $clone = this.$target.clone(false);
- associate_snippet_names: function ($snippets) {
- _.each($snippets, function (snippet) {
- var $snippet = $(snippet);
- var $sbody = $snippet.find(".oe_snippet_body");
- var snippet_classes = $sbody.attr("class").match(/s_[^ ]+/g);
- if (snippet_classes && snippet_classes.length) {
- snippet_classes = snippet_classes.join(".");
- }
- $("#wrapwrap ." + snippet_classes).data("name", $snippet.find(".oe_snippet_thumbnail_title").text());
+ this.trigger_up('request_history_undo_record', {$target: this.$target});
+
+ this.$target.after($clone);
+ this.trigger_up('call_for_each_child_snippet', {
+ $snippet: $clone,
+ callback: function (editor, $snippet) {
+ for (var i in editor.styles) {
+ editor.styles[i].onClone({
+ isCurrent: ($snippet.is($clone)),
+ });
+ }
+ },
});
},
-
- add_default_snippet_text_classes: function ($in) {
- if ($in === undefined) {
- $in = this.$snippets.find(".oe_snippet_body");
- }
-
- $in.find("*").addBack()
- .contents()
- .filter(function () {
- return this.nodeType === 3 && this.textContent.match(/\S/);
- }).parent().addClass("o_default_snippet_text");
+ /**
+ * Called when the overlay dimensions/positions should be recomputed.
+ *
+ * @private
+ */
+ _onCoverUpdate: function () {
+ this.cover();
},
+ /**
+ * Called when the snippet is starting to be dragged thanks to the 'move'
+ * button.
+ *
+ * @private
+ */
+ _onDragAndDropStart: function () {
+ var self = this;
+ self.size = {
+ width: self.$target.width(),
+ height: self.$target.height()
+ };
+ self.$target.after('<div class="oe_drop_clone" style="display: none;"/>');
+ self.$target.detach();
+ self.$el.addClass('hidden');
- cover_target: function ($el, $target) {
- if ($el.data('not-cover_target')) {
- return;
+ var $selectorSiblings;
+ for (var i = 0 ; i < self.selectorSiblings.length ; i++) {
+ if (!$selectorSiblings) $selectorSiblings = self.selectorSiblings[i].all();
+ else $selectorSiblings = $selectorSiblings.add(self.selectorSiblings[i].all());
}
- var pos = $target.offset();
- var mt = parseInt($target.css("margin-top") || 0);
- $el.css({
- width: $target.outerWidth(),
- left: pos.left,
- top: pos.top - mt,
- });
- $el.find('.oe_handles').css({
- height: $target.outerHeight(true),
+ var $selectorChildren;
+ for (i = 0 ; i < self.selectorChildren.length ; i++) {
+ if (!$selectorChildren) $selectorChildren = self.selectorChildren[i].all();
+ else $selectorChildren = $selectorChildren.add(self.selectorChildren[i].all());
+ }
+
+ this.trigger_up('go_to_parent', {$snippet: this.$target});
+ this.trigger_up('activate_insertion_zones', {
+ $selectorSiblings: $selectorSiblings,
+ $selectorChildren: $selectorChildren,
});
- $el.toggleClass('o_top_cover', pos.top <= (this.$('#o_scroll').position().top + 15));
- },
+ $('body').addClass('move-important');
- show_blocks: function () {
+ $('.oe_drop_zone').droppable({
+ over: function () {
+ $('.oe_drop_zone.hide').removeClass('hide');
+ $(this).addClass('hide').first().after(self.$target);
+ self.dropped = true;
+ },
+ out: function () {
+ $(this).removeClass('hide');
+ self.$target.detach();
+ self.dropped = false;
+ },
+ });
+ },
+ /**
+ * Called when the snippet is dropped after being dragged thanks to the
+ * 'move' button.
+ *
+ * @private
+ */
+ _onDragAndDropStop: function () {
var self = this;
- var cache = {};
- this.$snippets.each(function () {
- var $snippet = $(this);
- var $snippet_body = $snippet.find(".oe_snippet_body");
- var check = false;
- _.each(self.templateOptions, function (option, k) {
- if (check || !$snippet_body.is(option.base_selector)) return;
+ $('.oe_drop_zone').droppable('destroy').remove();
- cache[k] = cache[k] || {
- 'drop-near': option['drop-near'] ? option['drop-near'].all().length : 0,
- 'drop-in': option['drop-in'] ? option['drop-in'].all().length : 0
- };
- check = (cache[k]['drop-near'] || cache[k]['drop-in']);
- });
+ var prev = this.$target.first()[0].previousSibling;
+ var next = this.$target.last()[0].nextSibling;
+ var $parent = this.$target.parent();
- $snippet.toggleClass("disable", !check);
- });
- },
- bind_snippet_click_editor: function () {
- var self = this;
- var snipped_event_flag;
- $(document).on('click', '*', function (event) {
- var srcElement = event.srcElement || (event.originalEvent && (event.originalEvent.originalTarget || event.originalEvent.target) || event.target);
- if (self.editor_busy || snipped_event_flag===srcElement || !srcElement) {
- return;
- }
- snipped_event_flag = srcElement;
+ var $clone = $('.oe_drop_clone');
+ if (prev === $clone[0]) {
+ prev = $clone[0].previousSibling;
+ } else if (next === $clone[0]) {
+ next = $clone[0].nextSibling;
+ }
+ $clone.after(this.$target);
- setTimeout(function () {snipped_event_flag = false;}, 0);
- var $target = $(srcElement);
+ this.$el.removeClass('hidden');
+ $('body').removeClass('move-important');
+ $clone.remove();
- if ($target.closest(".oe_overlay, .note-popover").length) {
- return;
- }
+ if (this.dropped) {
+ this.trigger_up('request_history_undo_record', {$target: this.$target});
- if (!data.globalSelector.is($target)) {
- $target = data.globalSelector.closest($target);
+ if (prev) {
+ this.$target.insertAfter(prev);
+ } else if (next) {
+ this.$target.insertBefore(next);
+ } else {
+ $parent.prepend(this.$target);
}
- if (self.$active_snipped_id && self.$active_snipped_id.is($target)) {
- return;
- }
- self.make_active($target);
- });
- },
- snippet_blur: function ($snippet) {
- if ($snippet) {
- if ($snippet.data("snippet-editor")) {
- $snippet.data("snippet-editor").on_blur();
+ for (var i in this.styles) {
+ this.styles[i].onMove();
}
}
+
+ self.trigger_up('drag_and_drop_stop', {
+ $snippet: self.$target,
+ });
},
- snippet_focus: function ($snippet) {
- if ($snippet) {
- if ($snippet.data("snippet-editor")) {
- $snippet.data("snippet-editor").on_focus();
- }
+ /**
+ * Called when a child editor/option asks for another option to perform a
+ * specific action/react to a specific event.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onOptionUpdate: function (ev) {
+ // If multiple option names are given, we suppose it should not be
+ // propagated to parent editor
+ if (ev.data.optionNames) {
+ ev.stopPropagation();
+ var self = this;
+ _.each(ev.data.optionNames, function (name) {
+ var option = self.styles[name];
+ if (option) {
+ option.notify(ev.data.name, ev.data.data);
+ }
+ });
}
- },
- clean_for_save: function () {
- var self = this;
- var opt = options.registry;
- var template = self.templateOptions;
- for (var k in template) {
- var Option = opt[template[k]['option']];
- if (Option && Option.prototype.clean_for_save !== editor.dummy) {
- template[k].selector.all().filter(function () {
- var node = this;
- while (!/o_dirty|o_editable/.test(node.className) && node !== document) {
- node = node.parentNode;
- }
- return node.className.indexOf("o_dirty") !== -1;
- }).each(function () {
- new Option(self, null, $(this), k).clean_for_save();
- });
+ // If one option name is given, we suppose it should be handle by the
+ // first parent editor which can do it
+ if (ev.data.optionName) {
+ var option = this.styles[ev.data.optionName];
+ if (option) {
+ ev.stopPropagation();
+ option.notify(ev.data.name, ev.data.data);
}
}
- this.$editable.find("*[contentEditable], *[attributeEditable]")
- .removeAttr('contentEditable')
- .removeProp('contentEditable')
- .removeAttr('attributeEditable')
- .removeProp('attributeEditable');
},
- make_active: function ($snippet) {
- if ($snippet && this.$active_snipped_id && this.$active_snipped_id.get(0) === $snippet.get(0)) {
- return;
- }
- if (this.$active_snipped_id) {
- this.snippet_blur(this.$active_snipped_id);
- this.$active_snipped_id = false;
- }
- if ($snippet && $snippet.length) {
- if(_.indexOf(this.snippets, $snippet.get(0)) === -1) {
- this.snippets.push($snippet.get(0));
- }
- this.$active_snipped_id = $snippet;
- this.create_snippet_editor(this.$active_snipped_id);
- this.cover_target($snippet.data('overlay'), $snippet);
- this.snippet_focus($snippet);
- }
- this.$el.trigger('snippet-activated', $snippet);
- if ($snippet) {
- $snippet.trigger('snippet-activated', $snippet);
- }
+ /**
+ * Called when the 'parent' button is clicked.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onParentButtonClick: function (ev) {
+ ev.preventDefault();
+ this.trigger_up('go_to_parent', {
+ $snippet: this.$target,
+ });
},
- create_snippet_editor: function ($snippet) {
- if (typeof $snippet.data("snippet-editor") === 'undefined') {
- if (!this.activate_overlay_zones($snippet).length) return;
- $snippet.data("snippet-editor", new data.Editor(this, $snippet));
- }
+ /**
+ * Called when the 'remove' button is clicked.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onRemoveClick: function (ev) {
+ ev.preventDefault();
+ this.trigger_up('request_history_undo_record', {$target: this.$target});
+ this._removeSnippet();
},
+});
- // activate drag and drop for the snippets in the snippet toolbar
- make_snippet_draggable: function ($snippets) {
- var self = this;
- var $tumb = $snippets.find(".oe_snippet_thumbnail_img:first");
- var left = $tumb.outerWidth()/2;
- var top = $tumb.outerHeight()/2;
- var $toInsert, dropped, $snippet;
-
- $snippets.draggable({
- greedy: true,
- helper: 'clone',
- zIndex: '1000',
- appendTo: 'body',
- cursor: "move",
- handle: ".oe_snippet_thumbnail",
- distance: 30,
- cursorAt: {
- 'left': left,
- 'top': top
- },
- start: function () {
- dropped = false;
- // snippet_selectors => to get drop-near, drop-in
- $snippet = $(this);
- var $base_body = $snippet.find('.oe_snippet_body');
- var $selector_siblings = $();
- var $selector_children = $();
- var temp = self.templateOptions;
- for (var k in temp) {
- if ($base_body.is(temp[k].base_selector)) {
- if (temp[k]['drop-near']) {
- if (!$selector_siblings) $selector_siblings = temp[k]['drop-near'].all();
- else $selector_siblings = $selector_siblings.add(temp[k]['drop-near'].all());
- }
- if (temp[k]['drop-in']) {
- if (!$selector_children) $selector_children = temp[k]['drop-in'].all();
- else $selector_children = $selector_children.add(temp[k]['drop-in'].all());
- }
- }
- }
-
- $toInsert = $base_body.clone().data("name", $snippet.find(".oe_snippet_thumbnail_title").text());
-
- if (!$selector_siblings.length && !$selector_children.length) {
- console.warn($snippet.find(".oe_snippet_thumbnail_title").text() + " have not insert action: data-drop-near or data-drop-in");
- return;
- }
-
- self.make_active(false);
- self.activate_insertion_zones($selector_siblings, $selector_children);
-
- $('.oe_drop_zone').droppable({
- over: function () {
- dropped = true;
- $(this).first().after($toInsert).addClass('hidden');
- },
- out: function () {
- var prev = $toInsert.prev();
- if(this === prev[0]) {
- dropped = false;
- $toInsert.detach();
- $(this).removeClass('hidden');
- }
- },
- });
- },
- stop: function (ev, ui) {
- $toInsert.removeClass('oe_snippet_body');
-
- if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3 && ui.position.left + 50 > self.$el.outerWidth()) {
- var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first();
- if (el.length) {
- el.after($toInsert);
- dropped = true;
- }
- }
-
- self.$editable.find('.oe_drop_zone').droppable('destroy').remove();
-
- if (dropped) {
-
- var prev = $toInsert.first()[0].previousSibling;
- var next = $toInsert.last()[0].nextSibling;
- var rte = self.getParent().rte;
+/**
+ * Management of drag&drop menu and snippet related behaviors in the page.
+ */
+var SnippetsMenu = Widget.extend({
+ id: 'oe_snippets',
+ activeSnippets: [],
+ custom_events: {
+ activate_insertion_zones: '_onActivateInsertionZones',
+ call_for_each_child_snippet: '_onCallForEachChildSnippet',
+ deactivate_snippet: '_onDeactivateSnippet',
+ drag_and_drop_stop: '_onDragAndDropStop',
+ go_to_parent: '_onGoToParent',
+ snippet_removed: '_onSnippetRemoved',
+ },
- if (prev) {
- $toInsert.detach();
- rte.historyRecordUndo($(prev));
- $toInsert.insertAfter(prev);
- } else if (next) {
- $toInsert.detach();
- rte.historyRecordUndo($(next));
- $toInsert.insertBefore(next);
- } else {
- var $parent = $toInsert.parent();
- $toInsert.detach();
- rte.historyRecordUndo($parent);
- $parent.prepend($toInsert);
- }
+ /**
+ * @constructor
+ */
+ init: function (parent, $editable) {
+ this._super.apply(this, arguments);
- $toInsert.closest(".o_editable").trigger("content_changed");
+ this.$editable = $editable;
+ this.$activeSnippet = false;
+ this.snippetEditors = [];
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+ var defs = [this._super.apply(this, arguments)];
+ var $document = $(document);
+ var $window = $(window);
+
+ // Fetch snippet templates and compute it
+ var url = this._getSnippetURL();
+ defs.push(this._rpc({route: url}).then(function (html) {
+ return self._computeSnippetTemplates(html);
+ }));
+
+ // Prepare snippets editor environment
+ this.$snippetEditorArea = $('<div/>', {
+ id: 'oe_manipulators',
+ }).insertAfter(this.$el);
+
+ // Active snippet editor on click in the page
+ var lastClickedElement;
+ $document.on('click.snippets_menu', '*', function (ev) {
+ var srcElement = ev.srcElement || (ev.originalEvent && (ev.originalEvent.originalTarget || ev.originalEvent.target) || ev.target);
+ if (lastClickedElement === srcElement || !srcElement) {
+ return;
+ }
+ lastClickedElement = srcElement;
+ _.defer(function () {
+ lastClickedElement = false;
+ });
- var $target = false;
- $target = $toInsert;
+ var $target = $(srcElement);
+ if ($target.closest('.oe_overlay, .note-popover').length) {
+ return;
+ }
+ self._activateSnippet($target);
+ });
- setTimeout(function () {
- self.$el.trigger('snippet-dropped', $target);
+ core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet);
+ core.bus.on('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
- animation.start(true, $target);
+ // Some summernote customization
+ var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
+ $.summernote.core.dom.isNotBreakable = function (node) {
+ return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node));
+ };
- self.call_for_all_snippets($target, function (editor, $snippet) {
- _.defer(function () { editor.drop_and_build_snippet(); });
- });
- self.create_snippet_editor($target);
- self.cover_target($target.data('overlay'), $target);
- $target.closest(".o_editable").trigger("content_changed");
+ // Adapt overlay covering when the window is resized / content changes
+ var debouncedCoverUpdate = _.debounce(function () {
+ self._updateCurrentSnippetEditorOverlay();
+ }, 200);
+ $window.on('resize.snippets_menu', debouncedCoverUpdate);
+ $window.on('content_changed.snippets_menu', debouncedCoverUpdate);
+
+ // On keydown add a class on the active overlay to hide it and show it
+ // again when the mouse moves
+ $document.on('keydown.snippets_menu', function () {
+ if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) {
+ self.$activeSnippet.data('snippet-editor').$el.addClass('o_keypress');
+ }
+ });
+ $document.on('mousemove.snippets_menu', function () {
+ if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) {
+ self.$activeSnippet.data('snippet-editor').$el.removeClass('o_keypress');
+ }
+ });
- self.make_active($target);
- },0);
- } else {
- $toInsert.remove();
- }
- },
+ // Auto-selects text elements with a specific class and remove this
+ // on text changes
+ $document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
+ $(ev.target).selectContent();
+ });
+ $document.on('keyup.snippets_menu', function () {
+ var r = $.summernote.core.range.create();
+ $(r && r.sc).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
});
+
+ return $.when.apply($, defs);
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ this._super.apply(this, arguments);
+ this.$snippetEditorArea.remove();
+ $(window).off('.snippets_menu');
+ $(document).off('.snippets_menu');
+ core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet);
+ core.bus.off('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
},
- // call a method on a snippet and all his children
- call_for_all_snippets: function ($snippet, callback) {
- var self = this;
- $snippet.add(data.globalSelector.all($snippet)).each(function () {
- var $snippet = $(this);
- self.create_snippet_editor($snippet);
- if ($snippet.data("snippet-editor")) {
- callback.call(self, $snippet.data("snippet-editor"), $snippet);
- }
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prepares the page so that it may be saved:
+ * - Asks the snippet editors to clean their associated snippet
+ * - Remove the 'contentEditable' attributes
+ */
+ cleanForSave: function () {
+ _.each(this.snippetEditors, function (snippetEditor) {
+ snippetEditor.cleanForSave();
});
+ this.$editable.find('[contentEditable]')
+ .removeAttr('contentEditable')
+ .removeProp('contentEditable');
},
- // return the original snippet in the editor bar from a snippet id (string)
- get_snippet_from_id: function (id) {
- return $('.oe_snippet').filter(function () {
- return $(this).data('option') === id;
- }).first();
- },
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- // Create element insertion drop zones. two css selectors can be provided
- // selector.children -> will insert drop zones as direct child of the selected elements
- // in case the selected elements have children themselves, dropzones will be interleaved
- // with them.
- // selector.siblings -> will insert drop zones after and before selected elements
- activate_insertion_zones: function ($selector_siblings, $selector_children) {
- var self = this;
- var zone_template = $("<div class='oe_drop_zone oe_insert'/>");
+ /**
+ * Creates drop zones in the DOM (locations where snippets may be dropped).
+ * Those locations are determined thanks to the two types of given DOM.
+ *
+ * @private
+ * @param {jQuery} [$selectorSiblings]
+ * elements which must have siblings drop zones
+ * @param {jQuery} [$selectorSiblings]
+ * elements which must have child drop zones between each of existing
+ * child
+ */
+ _activateInsertionZones: function ($selectorSiblings, $selectorChildren) {
+ var zone_template = $('<div/>', {
+ class: 'oe_drop_zone oe_insert',
+ });
- if ($selector_children) {
- $selector_children.each(function () {
+ if ($selectorChildren) {
+ $selectorChildren.each(function () {
var $zone = $(this);
var css = window.getComputedStyle(this);
var float = css.float || css.cssFloat;
@@ -614,30 +658,30 @@ data.Class = Widget.extend({
$zone.append($drop);
var node = $drop[0].previousSibling;
- var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR"));
+ var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR'));
if (test) {
- $drop.addClass("oe_vertical").css({
- 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight),
- 'float': 'none',
- 'display': 'inline-block'
+ $drop.addClass('oe_vertical').css({
+ height: parseInt(window.getComputedStyle($zone[0]).lineHeight),
+ float: 'none',
+ display: 'inline-block',
});
- } else if (float === "left" || float === "right") {
- $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30));
+ } else if (float === 'left' || float === 'right') {
+ $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30));
}
$drop = $drop.clone();
$zone.prepend($drop);
node = $drop[0].nextSibling;
- test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR"));
+ test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR'));
if (test) {
- $drop.addClass("oe_vertical").css({
- 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight),
- 'float': 'none',
- 'display': 'inline-block'
+ $drop.addClass('oe_vertical').css({
+ height: parseInt(window.getComputedStyle($zone[0]).lineHeight),
+ float: 'none',
+ display: 'inline-block'
});
- } else if (float === "left" || float === "right") {
- $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30));
+ } else if (float === 'left' || float === 'right') {
+ $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30));
}
if (test) {
$drop.css({'float': 'none', 'display': 'inline-block'});
@@ -645,27 +689,27 @@ data.Class = Widget.extend({
});
// add children near drop zone
- $selector_siblings = $(_.uniq(($selector_siblings || $()).add($selector_children.children()).get()));
+ $selectorSiblings = $(_.uniq(($selectorSiblings || $()).add($selectorChildren.children()).get()));
}
- if ($selector_siblings) {
- $selector_siblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function () {
+ if ($selectorSiblings) {
+ $selectorSiblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function () {
var $zone = $(this);
var $drop;
var css = window.getComputedStyle(this);
var float = css.float || css.cssFloat;
- if($zone.prev('.oe_drop_zone:visible').length === 0) {
+ if ($zone.prev('.oe_drop_zone:visible').length === 0) {
$drop = zone_template.clone();
- if (float === "left" || float === "right") {
- $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.prev().outerHeight() || Infinity), 30));
+ if (float === 'left' || float === 'right') {
+ $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.prev().outerHeight() || Infinity), 30));
}
$zone.before($drop);
}
- if($zone.next('.oe_drop_zone:visible').length === 0) {
+ if ($zone.next('.oe_drop_zone:visible').length === 0) {
$drop = zone_template.clone();
- if (float === "left" || float === "right") {
- $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.next().outerHeight() || Infinity), 30));
+ if (float === 'left' || float === 'right') {
+ $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.next().outerHeight() || Infinity), 30));
}
$zone.after($drop);
}
@@ -673,15 +717,17 @@ data.Class = Widget.extend({
}
var count;
+ var $zones;
do {
count = 0;
- $zones = self.$editable.find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
+ $zones = this.$editable.find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
count += $zones.length;
$zones.remove();
} while (count > 0);
- // Cleaning consecutive zone and up zones placed between floating or inline elements. We do not like these kind of zones.
- var $zones = self.$editable.find('.oe_drop_zone:not(.oe_vertical)');
+ // Cleaning consecutive zone and up zones placed between floating or
+ // inline elements. We do not like these kind of zones.
+ $zones = this.$editable.find('.oe_drop_zone:not(.oe_vertical)');
$zones.each(function () {
var zone = $(this);
var prev = zone.prev();
@@ -695,460 +741,620 @@ data.Class = Widget.extend({
var float_next = next.css('float') || 'none';
var disp_prev = prev.css('display') || null;
var disp_next = next.css('display') || null;
- if( (float_prev === 'left' || float_prev === 'right')
- && (float_next === 'left' || float_next === 'right') ) {
+ if ((float_prev === 'left' || float_prev === 'right')
+ && (float_next === 'left' || float_next === 'right')) {
zone.remove();
- }else if( !( disp_prev === null
- || disp_next === null
- || disp_prev === 'block'
- || disp_next === 'block' )) {
+ } else if (disp_prev !== null && disp_next !== null
+ && disp_prev !== 'block' && disp_next !== 'block') {
zone.remove();
}
});
},
-
- // generate drop zones covering the elements selected by the selector
- // we generate overlay drop zones only to get an idea of where the snippet are, the drop
- activate_overlay_zones: function ($targets) {
- var self = this;
-
- // filter out invisible elements
- $targets = $targets.filter(':visible:hasVisibility:hasOpacity');
-
- $targets.each(function () {
- var $target = $(this);
- if (!$target.data('overlay')) {
- var $zone = $(qweb.render('web_editor.snippet_overlay'));
-
- // fix for pointer-events: none with ie9
- if (document.body && document.body.addEventListener) {
- $zone.on("click mousedown mousedown", function passThrough(event) {
- event.preventDefault();
- $target.each(function () {
- // check if clicked point (taken from event) is inside element
- event.srcElement = this;
- $(this).trigger(event.type);
- });
- return false;
- });
- }
-
- $zone.appendTo('#oe_manipulators');
- $zone.data('target', $target);
- $target.data('overlay', $zone);
-
- var timer;
- $target.closest('.o_editable').on("content_changed", function (event) {
- clearTimeout(timer);
- timer = setTimeout(function () {
- if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) {
- self.cover_target($target.data('overlay'), $target);
- }
- }, 50);
- });
-
- var resize = function () {
- if ($zone.parent().length) {
- self.cover_target($zone, $target);
- } else {
- $('body').off("resize", resize);
- }
- };
- $('body').on("resize", resize);
+ /**
+ * Disable the overlay editor of the active snippet and activate the new one
+ * if given.
+ * Note 1: if the snippet editor associated to the given snippet is not
+ * created yet, this method will create it.
+ * Note 2: if the given DOM element is not a snippet (no editor option), the
+ * first parent which is one is used instead.
+ *
+ * @param {jQuery|false} $snippet
+ * The DOM element whose editor need to be enabled. Only disable the
+ * current one if false is given.
+ * @returns {Deferred} (might be async when an editor must be created)
+ */
+ _activateSnippet: function ($snippet) {
+ if ($snippet) {
+ if (!globalSelector.is($snippet)) {
+ $snippet = globalSelector.closest($snippet);
+ }
+ if (this.$activeSnippet && this.$activeSnippet[0] === $snippet[0]) {
+ return $.when();
}
- self.cover_target($target.data('overlay'), $target);
- });
- return $targets;
- }
-});
-
-/**
- * Snippet editor Editor class.
- * Management of the overlay and option list for a snippet.
- */
-data.Editor = Class.extend({
- init: function (BuildingBlock, dom) {
- this.buildingBlock = BuildingBlock;
- this.$target = $(dom);
- this.$overlay = this.$target.data('overlay');
-
- // Initialize parent button
- this.init_parent_options();
-
- // Load overlay options content
- this.load_style_options();
-
- // Initialize move/clone/remove buttons
- if (!this.$target.parent().is(':o_editable')) {
- this.$overlay.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
- } else {
- this.$overlay.on('click', '.oe_snippet_clone', _.bind(this.on_clone, this));
- this.$overlay.on('click', '.oe_snippet_remove', _.bind(this.on_remove, this));
- this._drag_and_drop();
}
- },
-
- getName: function () {
- if (this.$target.data("name") !== undefined) {
- return this.$target.data("name");
+ if (this.$activeSnippet) {
+ if (this.$activeSnippet.data('snippet-editor')) {
+ this.$activeSnippet.data('snippet-editor').toggleFocus(false);
+ }
+ this.$activeSnippet = false;
}
- if (this.$target.parent(".row").length) {
- return _t("Column");
+ if ($snippet && $snippet.length) {
+ var self = this;
+ return this._createSnippetEditor($snippet).then(function () {
+ self.$activeSnippet = $snippet;
+ if (self.$activeSnippet.data('snippet-editor')) {
+ self.$activeSnippet.data('snippet-editor').toggleFocus(true);
+ }
+ });
}
- return _t("Block");
+ return $.when();
},
-
- // activate drag and drop for the snippets in the snippet toolbar
- _drag_and_drop: function () {
- var self = this;
- this.dropped = false;
- this.$overlay.draggable({
- greedy: true,
- appendTo: 'body',
- cursor: "move",
- handle: ".oe_snippet_move",
- cursorAt: {
- left: 18,
- top: 14
- },
- helper: function () {
- var $clone = $(this).clone().css({width: "24px", height: "24px", border: 0});
- $clone.find(".oe_overlay_options >:not(:contains(.oe_snippet_move)), .oe_handle").remove();
- $clone.find(":not(.glyphicon)").css({position: 'absolute', top: 0, left: 0});
- $clone.appendTo("body").removeClass("hidden");
- return $clone;
- },
- start: _.bind(self._drag_and_drop_start, self),
- stop: _.bind(self._drag_and_drop_stop, self)
- });
+ /**
+ * Updates the cover dimensions of the current snippet editor.
+ *
+ * @private
+ */
+ _updateCurrentSnippetEditorOverlay: function () {
+ if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) {
+ this.$activeSnippet.data('snippet-editor').cover();
+ }
},
- _drag_and_drop_after_insert_dropzone: function () {},
- _drag_and_drop_active_drop_zone: function ($zones) {
+ /**
+ * Calls a given callback 'on' the given snippet and all its child ones if
+ * any (DOM element with options).
+ *
+ * Note: the method creates the snippet editors if they do not exist yet.
+ *
+ * @private
+ * @param {jQuery} $snippet
+ * @param {function} callback
+ * Given two arguments: the snippet editor associated to the snippet
+ * being managed and the DOM element of this snippet.
+ * @returns {Deferred} (might be async if snippet editors need to be created
+ * and/or the callback is async)
+ */
+ _callForEachChildSnippet: function ($snippet, callback) {
var self = this;
- $zones.droppable({
- over: function () {
- $(".oe_drop_zone.hide").removeClass("hide");
- $(this).addClass("hide").first().after(self.$target);
- self.dropped = true;
- },
- out: function () {
- $(this).removeClass("hide");
- self.$target.detach();
- self.dropped = false;
- },
+ var defs = _.map($snippet.add(globalSelector.all($snippet)), function (el) {
+ var $snippet = $(el);
+ return self._createSnippetEditor($snippet).then(function (editor) {
+ if (editor) {
+ return callback.call(self, editor, $snippet);
+ }
+ });
});
+ return $.when.apply($, defs);
},
- _drag_and_drop_start: function () {
+ /**
+ * Creates and returns a set of helper functions which can help finding
+ * snippets in the DOM which match some parameters (typically parameters
+ * given by a snippet option). The functions are:
+ *
+ * - `is`: to determine if a given DOM is a snippet that matches the
+ * parameters
+ *
+ * - `closest`: find closest parent (or itself) of a given DOM which is a
+ * snippet that matches the parameters
+ *
+ * - `all`: find all snippets in the DOM that match the parameters
+ *
+ * See implementation for function details.
+ *
+ * @private
+ * @param {string} include
+ * jQuery selector that DOM elements must match to be considered as
+ * potential snippet.
+ * @param {string} exclude
+ * jQuery selector that DOM elements must *not* match the be
+ * considered as potential snippet.
+ * @param {boolean} noCheck
+ * true if DOM elements which are technically not in an editable
+ * environment may be considered.
+ * @param {boolean} isChildren
+ * when the DOM elements must be in an editable environment to be
+ * considered (@see noCheck), this is true if the DOM elements'
+ * parent must also be in an editable environment to be considered.
+ */
+ _computeSelectorFunctions : function (include, exclude, noCheck, isChildren) {
var self = this;
- self.buildingBlock.editor_busy = true;
- self.size = {
- width: self.$target.width(),
- height: self.$target.height()
- };
- self.$target.after("<div class='oe_drop_clone' style='display: none;'/>");
- self.$target.detach();
- self.$overlay.addClass("hidden");
- var $selector_siblings;
- for (var i = 0 ; i < self.selector_siblings.length ; i++) {
- if (!$selector_siblings) $selector_siblings = self.selector_siblings[i].all();
- else $selector_siblings = $selector_siblings.add(self.selector_siblings[i].all());
- }
- var $selector_children;
- for (i = 0 ; i < self.selector_children.length ; i++) {
- if (!$selector_children) $selector_children = self.selector_children[i].all();
- else $selector_children = $selector_children.add(self.selector_children[i].all());
+ // Convert the selector for elements to include into a list
+ var selectorList = _.compact(include.split(/\s*,\s*/));
+
+ // Convert the selector for elements to exclude into a list
+ var excludeList = _.compact(exclude.split(/\s*,\s*/));
+ excludeList.push('.o_snippet_not_selectable');
+
+ // Prepare the condition that will be added to each subselector for
+ // elements to include: 'not the elements to exclude and only the
+ // editable ones if needed'
+ var selectorConditions = _.map(excludeList, function (exc) {
+ return ':not(' + exc + ')';
+ }).join('');
+ if (!noCheck) {
+ selectorConditions = ':o_editable' + selectorConditions;
}
- self.buildingBlock.make_active(false);
- self.buildingBlock.activate_insertion_zones($selector_siblings, $selector_children);
-
- $("body").addClass('move-important');
+ // (Re)join the subselectors
+ var selector =_.map(selectorList, function (s) {
+ return s + selectorConditions;
+ }).join(', ');
- self._drag_and_drop_after_insert_dropzone();
- self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
+ // Prepare the functions
+ var functions = {
+ is: function ($from) {
+ return $from.is(selector);
+ },
+ };
+ if (noCheck) {
+ functions.closest = function ($from, parentNode) {
+ return $from.closest(selector, parentNode);
+ };
+ functions.all = function ($from) {
+ return $from ? dom.cssFind($from, selector) : $(selector);
+ };
+ } else {
+ functions.closest = function ($from, parentNode) {
+ var parents = self.$editable.get();
+ return $from.closest(selector, parentNode).filter(function () {
+ var node = this;
+ while (node.parentNode) {
+ if (parents.indexOf(node)!==-1) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+ });
+ };
+ functions.all = isChildren ? function ($from) {
+ return dom.cssFind($from || self.$editable, selector);
+ } : function ($from) {
+ $from = $from || self.$editable;
+ return $from.filter(selector).add(dom.cssFind($from, selector));
+ };
+ }
+ return functions;
},
- _drag_and_drop_stop: function () {
+ /**
+ * Processes the given snippet template to register snippet options, creates
+ * draggable thumbnail, etc.
+ *
+ * @private
+ * @param {string} html
+ */
+ _computeSnippetTemplates: function (html) {
var self = this;
+ var $html = $(html);
+ var $scroll = $html.siblings('#o_scroll');
- $(".oe_drop_zone").droppable('destroy').remove();
-
- var prev = this.$target.first()[0].previousSibling;
- var next = this.$target.last()[0].nextSibling;
- var $parent = this.$target.parent();
-
- var $clone = $(".oe_drop_clone");
- if (prev === $clone[0]) {
- prev = $clone[0].previousSibling;
- } else if (next === $clone[0]) {
- next = $clone[0].nextSibling;
- }
- $clone.after(this.$target);
-
- this.$overlay.removeClass("hidden");
- $("body").removeClass('move-important');
- $clone.remove();
+ $html.find('[data-oe-type="snippet"]').each(function () {
+ $(this).children()
+ .attr('data-oe-type', 'snippet')
+ .attr('data-oe-thumbnail', $(this).data('oe-thumbnail'));
+ });
- if (this.dropped) {
- this.buildingBlock.getParent().rte.historyRecordUndo(this.$target);
+ this.templateOptions = [];
+ var selectors = [];
+ var $styles = $html.find('[data-selector]');
+ $styles.each(function () {
+ var $style = $(this);
+ var selector = $style.data('selector');
+ var exclude = $style.data('exclude') || '';
+ var noCheck = $style.data('no-check');
+ var option_id = $style.data('js');
+ var option = {
+ 'option': option_id,
+ 'base_selector': selector,
+ 'base_exclude': exclude,
+ 'selector': self._computeSelectorFunctions(selector, exclude, noCheck),
+ '$el': $style,
+ 'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', noCheck, true),
+ 'drop-in': $style.data('drop-in') && self._computeSelectorFunctions($style.data('drop-in'), '', noCheck),
+ 'data': $style.data(),
+ };
+ self.templateOptions.push(option);
+ selectors.push(option.selector);
+ });
+ $styles.addClass('hidden');
- if (prev) {
- this.$target.insertAfter(prev);
- } else if (next) {
- this.$target.insertBefore(next);
- } else {
- $parent.prepend(this.$target);
+ globalSelector.closest = function ($from) {
+ var $temp;
+ var $target;
+ for (var i = 0, len = selectors.length ; i < len ; i++) {
+ $temp = selectors[i].closest($from, $target && $target[0]);
+ if ($temp.length) {
+ $target = $temp;
+ }
}
-
- for (var i in this.styles) {
- this.styles[i].on_move();
+ return $target || $();
+ };
+ globalSelector.all = function ($from) {
+ var $target = $();
+ for (var i = 0, len = selectors.length ; i < len ; i++) {
+ $target.add(selectors[i].all($from));
}
- }
+ return $target;
+ };
+ globalSelector.is = function ($from) {
+ for (var i = 0, len = selectors.length ; i < len ; i++) {
+ if (selectors[i].is($from)) {
+ return true;
+ }
+ }
+ return false;
+ };
- self.buildingBlock.editor_busy = false;
+ this.$snippets = $scroll.find('.o_panel_body').children()
+ .addClass('oe_snippet')
+ .each(function () {
+ var $snippet = $(this);
+ var name = $snippet.attr('name');
+ var $sbody = $snippet.children(':not(.oe_snippet_thumbnail)').addClass('oe_snippet_body');
+
+ // Associate in-page snippets to their name
+ if ($sbody.length) {
+ var snippetClasses = $sbody.attr('class').match(/s_[^ ]+/g);
+ if (snippetClasses && snippetClasses.length) {
+ snippetClasses = '.' + snippetClasses.join('.');
+ }
+ $(snippetClasses).data('name', name);
+ $sbody.data('name', name);
+ }
- self.init_parent_options();
- _.defer(function () {
- self.buildingBlock.cover_target(self.$target.data('overlay'), self.$target);
- });
- },
+ // Create the thumbnail
+ if ($snippet.find('.oe_snippet_thumbnail').length) {
+ return; // Compatibility with elements which do not use 't-snippet'
+ }
+ var $thumbnail = $(_.str.sprintf(
+ '<div class="oe_snippet_thumbnail">'+
+ '<div class="oe_snippet_thumbnail_img" style="background-image: url(%s);"/>'+
+ '<span class="oe_snippet_thumbnail_title">%s</span>'+
+ '</div>',
+ $snippet.find('[data-oe-thumbnail]').data('oeThumbnail'),
+ name
+ ));
+ $snippet.prepend($thumbnail);
+
+ // Create the install button (t-install feature) if necessary
+ var moduleID = $snippet.data('moduleId');
+ if (moduleID) {
+ $snippet.addClass('o_snippet_install');
+ var $installBtn = $('<a/>', {
+ class: 'btn btn-primary btn-sm o_install_btn',
+ target: '_blank',
+ href: '/web#id=' + moduleID + '&view_type=form&model=ir.module.module&action=base.open_module_tree',
+ text: _t("Install"),
+ });
+ $thumbnail.append($installBtn);
+ }
+ })
+ .not('[data-module-id]');
- load_style_options: function () {
- var self = this;
- var $styles = this.$overlay.find('.oe_options');
- var $ul = $styles.find('ul:first');
- this.styles = {};
- this.selector_siblings = [];
- this.selector_children = [];
+ // Hide scroll if no snippets defined
+ if (!this.$snippets.length) {
+ this.$el.detach();
+ }
+ $('body').toggleClass('editor_has_snippets', this.$snippets.length > 0);
- var i = 0;
- $ul.append($("<li/>", {"class": "dropdown-header o_main_header", text: this.getName()}).data("editor", this));
- _.each(this.buildingBlock.templateOptions, function (val, option_id) {
- if (!val.selector.is(self.$target)) {
- return;
- }
- if (val['drop-near']) self.selector_siblings.push(val['drop-near']);
- if (val['drop-in']) self.selector_children.push(val['drop-in']);
-
- var option = val['option'];
- self.styles[option] = new (options.registry[option] || options.Class)(self.buildingBlock, self, self.$target, option_id);
- $ul.append(self.styles[option].$el.addClass("snippet-option-" + option));
- self.styles[option].start();
- self.styles[option].__order = i++;
- });
- $ul.append($("<li/>", {"class": "divider"}));
+ // Register the text nodes that needs to be auto-selected on click
+ this._registerDefaultTexts();
- var $parents = this.$target.parents();
- _.each($parents, function (parent) {
- var parentEditor = $(parent).data("snippet-editor");
- if (parentEditor) {
- for (var styleName in parentEditor.styles) {
- if (!parentEditor.styles[styleName].preventChildPropagation) {
- $ul.append($("<li/>", {"class": "dropdown-header o_parent_editor_header", text: parentEditor.getName()}).data("editor", parentEditor));
- break;
- }
+ // Remove branding from template
+ _.each($html.find('[data-oe-model], [data-oe-type]'), function (el) {
+ for (var k = 0 ; k < el.attributes.length ; k++) {
+ if (el.attributes[k].name.indexOf('data-oe-') === 0) {
+ $(el).removeAttr(el.attributes[k].name);
+ k--;
}
}
});
- if (!this.selector_siblings.length && !this.selector_children.length) {
- this.$overlay.find(".oe_snippet_move, .oe_snippet_clone").addClass('hidden');
- }
+ // Force non editable part to contentEditable=false
+ $html.find('.o_not_editable').attr('contentEditable', false);
- this.$overlay.find('[data-toggle="dropdown"]').dropdown();
+ // Add the computed template and make elements draggable
+ this.$el.html($html);
+ this._makeSnippetDraggable(this.$snippets);
+ this._disableUndroppableSnippets();
},
-
/**
- * The init_parent_options method initializes the "go to parent" button and create the editor options
- * management for them if they do not already have one.
+ * Creates a snippet editor to associated to the given snippet. If the given
+ * snippet already has a linked snippet editor, the function only returns
+ * that one.
+ * The function also instantiates a snippet editor for all snippet parents
+ * as a snippet editor must be able to display the parent snippet options.
+ *
+ * @private
+ * @param {jQuery} $snippet
+ * @returns {Deferred<SnippetEditor>}
*/
- init_parent_options: function () {
+ _createSnippetEditor: function ($snippet) {
var self = this;
- var $button = this.$overlay.find('.oe_snippet_parent');
- var $parent = data.globalSelector.closest(this.$target.parent());
- if (!$parent.data("snippet-editor")) {
- this.buildingBlock.create_snippet_editor($parent);
+ var snippetEditor = $snippet.data('snippet-editor');
+ if (snippetEditor) {
+ return $.when(snippetEditor);
}
- $button.toggleClass("hidden", $parent.length === 0);
- $button.off("click").on('click', function (event) {
- event.preventDefault();
- _.defer(function () {
- self.buildingBlock.make_active($parent);
- });
+ var def;
+ var $parent = globalSelector.closest($snippet.parent());
+ if ($parent.length) {
+ def = this._createSnippetEditor($parent);
+ }
+
+ return $.when(def).then(function (parentEditor) {
+ snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions);
+ self.snippetEditors.push(snippetEditor);
+ return snippetEditor.appendTo(self.$snippetEditorArea);
+ }).then(function () {
+ return snippetEditor;
});
},
+ /**
+ * There may be no location where some snippets might be dropped. This mades
+ * them appear disabled in the menu.
+ *
+ * @todo make them undraggable
+ * @private
+ */
+ _disableUndroppableSnippets: function () {
+ var self = this;
+ var cache = {};
+ this.$snippets.each(function () {
+ var $snippet = $(this);
+ var $snippet_body = $snippet.find('.oe_snippet_body');
- on_clone: function (event) {
- event.preventDefault();
- var $clone = this.$target.clone(false);
+ var check = false;
+ _.each(self.templateOptions, function (option, k) {
+ if (check || !($snippet_body.is(option.base_selector) && !$snippet_body.is(option.base_exclude))) return;
- this.buildingBlock.getParent().rte.historyRecordUndo(this.$target);
+ cache[k] = cache[k] || {
+ 'drop-near': option['drop-near'] ? option['drop-near'].all().length : 0,
+ 'drop-in': option['drop-in'] ? option['drop-in'].all().length : 0
+ };
+ check = (cache[k]['drop-near'] || cache[k]['drop-in']);
+ });
- this.$target.after($clone);
- this.buildingBlock.call_for_all_snippets($clone, function (editor, $snippet) {
- for (var i in editor.styles) {
- editor.styles[i].on_clone($snippet, {
- isCurrent: ($snippet.is($clone)),
- });
- }
+ $snippet.toggleClass('o_disabled', !check);
});
- return false;
},
+ /**
+ * Returns the URL where to find the snippets template. This URL might have
+ * been set in the global 'snippetsURL' variable, otherwise this function
+ * returns a default one.
+ *
+ * @private
+ * @returns {string}
+ */
+ _getSnippetURL: function () {
+ return odoo.snippetsURL || '/web_editor/snippets';
+ },
+ /**
+ * Make given snippets be draggable/droppable thanks to their thumbnail.
+ *
+ * @private
+ * @param {jQuery} $snippets
+ */
+ _makeSnippetDraggable: function ($snippets) {
+ var self = this;
+ var $tumb = $snippets.find('.oe_snippet_thumbnail_img:first');
+ var left = $tumb.outerWidth()/2;
+ var top = $tumb.outerHeight()/2;
+ var $toInsert, dropped, $snippet;
- on_remove: function (event) {
- if (event !== undefined) {
- event.preventDefault();
- this.buildingBlock.getParent().rte.historyRecordUndo(this.$target);
- }
-
- this.on_blur();
+ $snippets.draggable({
+ greedy: true,
+ helper: 'clone',
+ zIndex: '1000',
+ appendTo: 'body',
+ cursor: 'move',
+ handle: '.oe_snippet_thumbnail',
+ distance: 30,
+ cursorAt: {
+ left: left,
+ top: top,
+ },
+ start: function () {
+ dropped = false;
+ $snippet = $(this);
+ var $base_body = $snippet.find('.oe_snippet_body');
+ var $selectorSiblings = $();
+ var $selectorChildren = $();
+ var temp = self.templateOptions;
+ for (var k in temp) {
+ if ($base_body.is(temp[k].base_selector) && !$base_body.is(temp[k].base_exclude)) {
+ if (temp[k]['drop-near']) {
+ if (!$selectorSiblings) $selectorSiblings = temp[k]['drop-near'].all();
+ else $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all());
+ }
+ if (temp[k]['drop-in']) {
+ if (!$selectorChildren) $selectorChildren = temp[k]['drop-in'].all();
+ else $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
+ }
+ }
+ }
- var index = _.indexOf(this.buildingBlock.snippets, this.$target.get(0));
- this.buildingBlock.call_for_all_snippets(this.$target, function (editor, $snippet) {
- for (var i in editor.styles) {
- editor.styles[i].on_remove();
- }
- });
- delete this.buildingBlock.snippets[index];
+ $toInsert = $base_body.clone().data('name', $base_body.data('name'));
- var $parent = this.$target.parent();
- this.$target.find("*").andSelf().tooltip("destroy");
- this.$target.remove();
- this.$overlay.remove();
+ if (!$selectorSiblings.length && !$selectorChildren.length) {
+ console.warn($snippet.find('.oe_snippet_thumbnail_title').text() + " have not insert action: data-drop-near or data-drop-in");
+ return;
+ }
- var node = $parent[0];
- if (node && node.firstChild) {
- $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
- if (!node.firstChild.tagName && node.firstChild.textContent === " ") {
- node.removeChild(node.firstChild);
- }
- }
+ self._activateSnippet(false);
+ self._activateInsertionZones($selectorSiblings, $selectorChildren);
- if($parent.closest(":data(\"snippet-editor\")").length) {
- while (!$parent.data("snippet-editor")) {
- var $nextParent = $parent.parent();
- if ($parent.children().length === 0 && $parent.text().trim() === "" && !$parent.hasClass("oe_structure")) {
- $parent.remove();
- }
- $parent = $nextParent;
- }
- if ($parent.children().length === 0 && $parent.text().trim() === "" && !$parent.hasClass("oe_structure")) {
- _.defer(function () {
- $parent.data("snippet-editor").on_remove();
+ $('.oe_drop_zone').droppable({
+ over: function () {
+ dropped = true;
+ $(this).first().after($toInsert).addClass('hidden');
+ },
+ out: function () {
+ var prev = $toInsert.prev();
+ if (this === prev[0]) {
+ dropped = false;
+ $toInsert.detach();
+ $(this).removeClass('hidden');
+ }
+ },
});
- }
- }
-
- // clean editor if they are image or table in deleted content
- $(".note-control-selection").hide();
- $('.o_table_handler').remove();
+ },
+ stop: function (ev, ui) {
+ $toInsert.removeClass('oe_snippet_body');
- this.buildingBlock.$el.trigger("snippet-removed");
+ if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3 && ui.position.left + 50 > self.$el.outerWidth()) {
+ var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first();
+ if (el.length) {
+ el.after($toInsert);
+ dropped = true;
+ }
+ }
- return false;
- },
+ self.$editable.find('.oe_drop_zone').droppable('destroy').remove();
- /*
- * drop_and_build_snippet
- * This method is called just after that a thumbnail is drag and dropped into a drop zone
- * (after the insertion of this.$body, if this.$body exists)
- */
- drop_and_build_snippet: function () {
- for (var i in this.styles) {
- this.styles[i].drop_and_build_snippet();
- }
- },
+ if (dropped) {
+ var prev = $toInsert.first()[0].previousSibling;
+ var next = $toInsert.last()[0].nextSibling;
- /* on_focus
- * This method is called when the user click inside the snippet in the dom
- */
- on_focus: function () {
- this._on_focus_blur(true);
- },
+ if (prev) {
+ $toInsert.detach();
+ self.trigger_up('request_history_undo_record', {$target: $(prev)});
+ $toInsert.insertAfter(prev);
+ } else if (next) {
+ $toInsert.detach();
+ self.trigger_up('request_history_undo_record', {$target: $(next)});
+ $toInsert.insertBefore(next);
+ } else {
+ var $parent = $toInsert.parent();
+ $toInsert.detach();
+ self.trigger_up('request_history_undo_record', {$target: $parent});
+ $parent.prepend($toInsert);
+ }
- /* on_focus
- * This method is called when the user click outside the snippet in the dom, after a focus
- */
- on_blur: function () {
- this._on_focus_blur(false);
- },
+ $toInsert.closest('.o_editable').trigger('content_changed');
- _on_focus_blur: function (focus) {
- var do_action = (focus ? _do_action_focus : _do_action_blur);
+ var $target = $toInsert;
- // Attach own and parent options on the current overlay
- var $style_button = this.$overlay.find(".oe_options");
- var $ul = $style_button.find("ul:first");
- var $headers = $ul.find(".dropdown-header:data(editor)");
- _.each($headers, (function (el) {
- var $el = $(el);
- var styles = _.values($el.data("editor").styles);
- if ($el.data("editor") !== this) {
- styles = _.filter(styles, function (option) { return !option.preventChildPropagation; });
- }
+ _.defer(function () {
+ self.trigger_up('snippet_dropped', {$target: $target});
+ self._disableUndroppableSnippets();
- var count = 0;
- _.each(_.sortBy(styles, "__order").reverse(), function (style) {
- if (do_action(style, $el)) {
- count++;
+ self._callForEachChildSnippet($target, function (editor, $snippet) {
+ _.defer(function () {
+ editor.buildSnippet();
+ });
+ }).then(function () {
+ $target.closest('.o_editable').trigger('content_changed');
+ self._activateSnippet($target);
+ });
+ });
+ } else {
+ $toInsert.remove();
}
- });
- $el.toggleClass("hidden", count === 0);
- }).bind(this));
-
- // Activate the overlay
- $style_button.toggleClass("hidden", $ul.children(":not(.o_main_header):not(.divider):not(.hidden)").length === 0);
- this.$overlay.toggleClass("oe_active", !!focus);
-
- function _do_action_focus(style, $dest) {
- style.$el.insertAfter($dest);
- style.on_focus();
- return (style.$el.length > 0);
- }
- function _do_action_blur(style, $dest) {
- style.$el.detach();
- style.on_blur();
- return false;
+ },
+ });
+ },
+ /**
+ * Adds the 'o_default_snippet_text' class on nodes which contain only
+ * non-empty text nodes. Those nodes are then auto-selected by the editor
+ * when they are clicked.
+ *
+ * @private
+ * @param {jQuery} [$in] - the element in which to search, default to the
+ * snippet bodies in the menu
+ */
+ _registerDefaultTexts: function ($in) {
+ if ($in === undefined) {
+ $in = this.$snippets.find('.oe_snippet_body');
}
+
+ $in.find('*').addBack()
+ .contents()
+ .filter(function () {
+ return this.nodeType === 3 && this.textContent.match(/\S/);
+ }).parent().addClass('o_default_snippet_text');
},
-});
-/**
- * Add the ability on the main editor class to instantiate the snippet editor component
- */
-editor.Class.include({
- init: function () {
- var self = this;
- var res = this._super.apply(this, arguments);
- var $editable = this.rte.editable();
- this.buildingBlock = new data.Class(this, $editable);
- this.buildingBlock.on("snippets:ready", this, function () {
- self.trigger("snippets:ready");
- });
- return res;
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when a child editor asks for insertion zones to be enabled.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onActivateInsertionZones: function (ev) {
+ this._activateInsertionZones(ev.data.$selectorSiblings, ev.data.$selectorChildren);
},
- start: function () {
- this.buildingBlock.insertAfter(this.$el);
- this.rte.editable().find("*").off('mousedown mouseup click');
- return this._super.apply(this, arguments);
+ /**
+ * Called when a child editor asks to operate some operation on all child
+ * snippet of a DOM element.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onCallForEachChildSnippet: function (ev) {
+ this._callForEachChildSnippet(ev.data.$snippet, ev.data.callback);
},
- save: function () {
- this.buildingBlock.clean_for_save();
- return this._super.apply(this, arguments);
+ /**
+ * Called when asked to clean the DOM for save. Should technically not be
+ * used but used by tests.
+ *
+ * @private
+ */
+ _onCleanForSaveDemand: function (ev) {
+ this.cleanForSave();
},
-});
-
-/**
- * Add the ability the restart the animations
- */
-editor.Class.include({
- start: function () {
- return this._super.apply(this, arguments).then(function () {
- animation.start(true);
- });
+ /**
+ * Called when a child editor asks to deactivate the current snippet
+ * overlay.
+ *
+ * @private
+ */
+ _onDeactivateSnippet: function () {
+ this._activateSnippet(false);
+ },
+ /**
+ * Called when a snippet has moved in the page.
+ *
+ * @todo technically, as a snippet has been moved, all editors should be
+ * destroyed as their snippet options may not correspond to their
+ * selector anymore. However this should rarely (maybe never ?) be the case,
+ * so we might not want to do this as it would slow the editor.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onDragAndDropStop: function (ev) {
+ this._activateSnippet(ev.data.$snippet);
+ },
+ /**
+ * Called when a snippet editor asked to disable itself and to enable its
+ * parent instead.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onGoToParent: function (ev) {
+ ev.stopPropagation();
+ this._activateSnippet(ev.data.$snippet.parent());
+ },
+ /**
+ * Called when a snippet is removed -> checks if there is draggable snippets
+ * to enable/disable as the DOM changed.
+ *
+ * @private
+ */
+ _onSnippetRemoved: function () {
+ this._disableUndroppableSnippets();
},
});
-return data;
+return {
+ Class: SnippetsMenu,
+ Editor: SnippetEditor,
+ globalSelector: globalSelector,
+};
});
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 68301312cd5..ba4ab0b60ed 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -1,993 +1,1311 @@
odoo.define('web_editor.snippets.options', 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 base = require('web_editor.base');
- var editor = require('web_editor.editor');
- var widget = require('web_editor.widget');
-
- var qweb = core.qweb;
- var _t = core._t;
-
+'use strict';
+
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var Widget = require('web.Widget');
+var weContext = require('web_editor.context');
+var widget = require('web_editor.widget');
+
+var qweb = core.qweb;
+var _t = core._t;
+
+/**
+ * Handles a set of options for one snippet. The registry returned by this
+ * module contains the names of the specialized SnippetOption which can be
+ * referenced thanks to the data-js key in the web_editor options template.
+ */
+var SnippetOption = Widget.extend({
+ events: {
+ 'mouseenter a': '_onLinkEnter',
+ 'click a': '_onLinkClick',
+ 'mouseleave': '_onMouseleave',
+ 'mouseleave ul': '_onMouseleave',
+ },
/**
- * The SnippetOption class handles one option for one snippet. The registry returned by
- * this module contains the names of the specialized SnippetOption which can be reference
- * thanks to the data-js key of the full options web_editor template.
+ * When editing a snippet, its options are shown alongside the ones of its
+ * parent snippets. The parent options are only shown if the following flag
+ * is set to false (default).
*/
- var SnippetOption = Class.extend({
- init: function (BuildingBlock, editor, $target, option_id) {
- this.buildingBlock = BuildingBlock;
- this.editor = editor;
- this.$target = $target;
+ preventChildPropagation: false,
- this.option = option_id;
- var option = this.buildingBlock.templateOptions[option_id];
- this.$el = option.$el.children("li").clone();
- this.data = option.$el.data();
+ /**
+ * The option `$el` is supposed to be the associated <li/> element in the
+ * options dropdown. The option controls another DOM element: the snippet it
+ * customizes, which can be found at `$target`. Access to the whole edition
+ * overlay is possible with `$overlay` (this is not recommended though).
+ *
+ * @constructor
+ */
+ init: function (parent, $target, $overlay) {
+ this._super.apply(this, arguments);
+ this.$target = $target;
+ this.$overlay = $overlay;
+ this.__methodNames = [];
+ },
+ /**
+ * Called when the option is initialized (i.e. the parent edition overlay is
+ * shown for the first time).
+ *
+ * @override
+ */
+ start: function () {
+ this.data = this.$el.data();
+ this._setActive();
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * Called when the parent edition overlay is covering the associated snippet
+ * (the first time, this follows the call to the @see start method).
+ *
+ * @abstract
+ */
+ onFocus : function () {},
+ /**
+ * Called when the parent edition overlay is covering the associated snippet
+ * for the first time, when it is a new snippet dropped from the d&d snippet
+ * menu. Note: this is called after the start and onFocus methods.
+ *
+ * @abstract
+ */
+ onBuilt: function () {},
+ /**
+ * Called when the parent edition overlay is removed from the associated
+ * snippet (another snippet enters edition for example).
+ *
+ * @abstract
+ */
+ onBlur : function () {},
+ /**
+ * Called when the associated snippet is the result of the cloning of
+ * another snippet (so `this.$target` is a cloned element).
+ *
+ * @abstract
+ * @param {Object} options
+ * @param {boolean} options.isCurrent
+ * true if the associated snippet is a clone of the main element that
+ * was cloned (so not a clone of a child of this main element that
+ * was cloned)
+ */
+ onClone: function (options) {},
+ /**
+ * Called when the associated snippet is moved to another DOM location.
+ *
+ * @abstract
+ */
+ onMove: function () {},
+ /**
+ * Called when the associated snippet is about to be removed from the DOM.
+ *
+ * @abstract
+ */
+ onRemove: function () {},
+ /**
+ * Called when the template which contains the associated snippet is about
+ * to be saved.
+ *
+ * @abstract
+ */
+ cleanForSave: function () {},
- this.$overlay = this.$target.data('overlay') || $();
- },
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
- // helper for this.$target.find
- $: function () {
- return this.$target.find.apply(this.$target, arguments);
- },
+ /**
+ * Default option method which allows to select one and only one class in
+ * the option classes set and set it on the associated snippet. The common
+ * case is having a subdropdown with each <li/> having a `data-select-class`
+ * value allowing to choose the associated class.
+ *
+ * @param {boolean|string} previewMode
+ * - truthy if the option is enabled for preview or if leaving it (in
+ * that second case, the value is 'reset')
+ * - false if the option should be activated for good
+ * @param {*} value - the class to activate ($li.data('selectClass'))
+ * @param {jQuery} $li - the related DOMElement option
+ */
+ selectClass: function (previewMode, value, $li) {
+ var $lis = this.$el.find('[data-select-class]').addBack('[data-select-class]');
+ var classes = $lis.map(function () {return $(this).data('selectClass');}).get().join(' ');
- _bind_li_menu: function () {
- this.$el.filter("li:hasData").find('a:first')
- .off('mouseenter click')
- .on('mouseenter click', _.bind(_mouse, this));
+ this.$target.removeClass(classes);
+ if (value) {
+ this.$target.addClass(value);
+ }
+ },
+ /**
+ * Default option method which allows to select one or multiple classes in
+ * the option classes set and set it on the associated snippet. The common
+ * case is having a subdropdown with each <li/> having a `data-toggle-class`
+ * value allowing to toggle the associated class.
+ *
+ * @see this.selectClass
+ */
+ toggleClass: function (previewMode, value, $li) {
+ var $lis = this.$el.find('[data-toggle-class]').addBack('[data-toggle-class]');
+ var classes = $lis.map(function () {return $(this).data('toggleClass');}).get().join(' ');
+ var activeClasses = $lis.filter('.active, :has(.active)').map(function () {return $(this).data('toggleClass');}).get().join(' ');
+
+ this.$target.removeClass(classes).addClass(activeClasses);
+ if (value && previewMode !== 'reset') {
+ this.$target.toggleClass(value);
+ }
+ },
- this.$el
- .off('mouseenter click', "li:hasData a")
- .on('mouseenter click', "li:hasData a", _.bind(_mouse, this));
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
- this.$el.off('mouseleave')
- .on('mouseleave', _.bind(this.reset, this));
+ /**
+ * Override the helper method to search inside the $target element instead
+ * of the dropdown <li/> element.
+ *
+ * @override
+ */
+ $: function () {
+ return this.$target.find.apply(this.$target, arguments);
+ },
+ /**
+ * Sometimes, options may need to notify other options, even in parent
+ * editors. This can be done thanks to the 'option_update' event, which
+ * will then be handled by this function.
+ *
+ * @param {string} name - an identifier for a type of update
+ * @param {*} data
+ */
+ notify: function (name, data) {
+ if (name === 'target') {
+ this.setTarget(data);
+ }
+ },
+ /**
+ * Sometimes, an option is binded on an element but should in fact apply on
+ * another one. For example, elements which contain slides: we want all the
+ * per-slide options to be in the main menu of the whole snippet. This
+ * function allows to set the option's target.
+ *
+ * @param {jQuery} $target - the new target element
+ */
+ setTarget: function ($target) {
+ this.$target = $target;
+ this._setActive();
+ this.$target.trigger('snippet-option-change', [this]);
+ },
- this.$el.off('mouseleave', "ul")
- .on('mouseleave', "ul", _.bind(this.reset, this));
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- this.methodsToReset = [];
+ /**
+ * Reactivate the options that were activated before previews.
+ */
+ _reset: function () {
+ var self = this;
+ var $actives = this.$el.find('.active').addBack('.active');
+ _.each($actives, function (activeElement) {
+ var $activeElement = $(activeElement);
+ self.__methodNames = _.without.apply(_, [self.__methodNames].concat(_.keys($activeElement.data())));
+ self._select('reset', $activeElement);
+ });
+ _.each(this.__methodNames, function (methodName) {
+ self[methodName]('reset');
+ });
+ this.__methodNames = [];
+ },
+ /**
+ * Activates the option associated to the given DOM element.
+ *
+ * @private
+ * @param {boolean|string} previewMode
+ * - truthy if the option is enabled for preview or if leaving it (in
+ * that second case, the value is 'reset')
+ * - false if the option should be activated for good
+ * @param {jQuery} $li - the related DOMElement option
+ */
+ _select: function (previewMode, $li) {
+ var self = this;
- function _mouse(event) {
- var $next = $(event.currentTarget).parent();
+ // Options can say they respond to strong choice
+ if (previewMode && ($li.data('noPreview') || $li.parent().data('noPreview'))) {
+ return;
+ }
+ // If it is not preview mode, the user selected the option for good
+ // (so record the action)
+ if (!previewMode) {
+ this._reset();
+ this.trigger_up('request_history_undo_record', {$target: this.$target});
+ }
- // triggers preview or apply methods if a menu item has been clicked
- this.select(event.type === "click" ? "click" : "over", $next);
- if (event.type === 'click') {
- this.set_active();
- this.$target.trigger("snippet-option-change", [this]);
- } else {
- this.$target.trigger("snippet-option-preview", [this]);
- }
- }
- },
-
- /**
- * The set_active method tweaks the option dropdown to show the selected value
- * according to the state of the $target the option customizes.
- */
- set_active: function () {
- var self = this;
- this.$el.find('[data-toggle_class], [data-select_class]')
- .addBack('[data-toggle_class], [data-select_class]')
- .removeClass("active")
- .filter(function () {
- var $elem = $(this);
- var className = $elem.data("toggle_class") || $elem.data("select_class");
- return self.$target.hasClass(className);
- })
- .addClass("active");
- },
-
- start: function () {
- this.set_active();
- this.$target.on('snippet-option-reset', _.bind(this.set_active, this));
- this._bind_li_menu();
- },
-
- on_focus : function () {
- this._bind_li_menu();
- },
-
- on_blur : function () {},
- on_clone: function ($clone) {},
- on_move: function () {},
- on_remove: function () {},
-
- drop_and_build_snippet: function () {},
-
- reset: function () {
- var self = this;
- var lis = this.$el.find("li.active").addBack('.active').get();
- _.each(lis.reverse(), function (li) {
- var $li = $(li);
- for (var k in self.methodsToReset) {
- var method = self.methodsToReset[k];
- if ($li.closest('[data-' + method + ']').length) {
- delete self.methodsToReset[k];
+ // Search for methods (data-...) (i.e. data-toggle-class) on the
+ // selected (sub)option and its parents
+ var el = $li[0];
+ var methods = [];
+ do {
+ methods.push([el, el.dataset]);
+ el = el.parentNode;
+ } while (this.$el.parent().has(el).length);
+
+ // Call the found method in the right order (parents -> child)
+ _.each(methods.reverse(), function (data) {
+ var $el = $(data[0]);
+ var methods = data[1];
+
+ _.each(methods, function (value, methodName) {
+ if (self[methodName]) {
+ if (previewMode === true) {
+ self.__methodNames.push(methodName);
}
+ self[methodName](previewMode, value, $el);
}
- self.select("reset", $li);
});
+ });
+ this.__methodNames = _.uniq(this.__methodNames);
- for (var k in this.methodsToReset) {
- var method = this.methodsToReset[k];
- if (method) {
- this[method]("reset", null);
- }
- }
- this.methodsToReset = [];
- this.$target.trigger("snippet-option-reset", [this]);
- },
-
- /**
- * Handle action type on a particuler $li (sub)option of the option.
- * @param type action type to handle ("click", "over", "reset")
- * @param $li the suboption DOM element which is concerned by the action
- */
- select: function (type, $li) {
- // (sub)options can say they respond to only one action type
- if ($li.data('only') && type !== $li.data('only')) {
- return;
- }
- // If the type is click, the user selected the option for good (so reset and record action)
- if (type === "click") {
- this.reset();
- this.buildingBlock.getParent().rte.historyRecordUndo(this.$target);
- }
-
- // Search for methods (data-...) (i.e. data-toggle_class) on the selected (sub)option and its parents
- var el = $li[0];
- var methods = [];
- do {
- var m = _.omit(el.dataset, ["oeId", "oeModel", "oeField", "oeXpath", "oeSourceId", "only"]);
- if (_.keys(m).length) {
- methods.push([el, m]);
- }
- el = el.parentNode;
- } while (this.$el.parent().has(el).length);
-
- // Call the found method in the right order (parents -> child) and save them for later reset
- var self = this;
- _.each(methods.reverse(), function (data) {
- var $el = $(data[0]);
- var methods = data[1];
-
- for (var k in methods) {
- if (self[k]) {
- if (type !== "reset" && self.methodsToReset.indexOf(k) === -1) {
- self.methodsToReset.push(k);
- }
- self[k](type, methods[k], $el);
- } else {
- console.error("'" + self.option + "' snippet option have not method '" + k + "'");
- }
- }
- });
- },
+ if (!previewMode) {
+ this._setActive();
+ }
+ },
+ /**
+ * Tweaks the option DOM elements to show the selected value according to
+ * the state of the $target the option customizes.
+ *
+ * @todo should be extendable in a more easy way
+ * @private
+ */
+ _setActive: function () {
+ var self = this;
+ this.$el.find('[data-toggle-class], [data-select-class]')
+ .addBack('[data-toggle-class], [data-select-class]')
+ .removeClass('active')
+ .filter(function () {
+ var $elem = $(this);
+ var className = $elem.data('toggleClass') || $elem.data('selectClass');
+ return self.$target.hasClass(className);
+ })
+ .addClass('active');
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
- /**
- * Default option methods toggle_class and selected_class handle the two behavior of having a list
- * of option classes to toggle or not and having a list of option classes where only one must be chosen.
- */
- toggle_class: function (type, value, $li) {
- var $lis = this.$el.find("[data-toggle_class]").addBack("[data-toggle_class]");
+ /**
+ * Called when a option link is entered -> activates the related option in
+ * preview mode.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onLinkEnter: function (ev) {
+ var $li = $(ev.currentTarget).parent(':hasData');
+ if (!$li.length) {
+ return;
+ }
+ this.__click = false;
+ this._select(true, $li);
+ this.$target.trigger('snippet-option-preview', [this]);
+ },
+ /**
+ * Called when an option link is clicked -> activates the related option.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onLinkClick: function (ev) {
+ var $li = $(ev.currentTarget).parent(':hasData');
+ if (!$li.length) {
+ return;
+ }
+ ev.preventDefault();
+ this.__click = true;
+ this._select(false, $li);
+ this.$target.trigger('snippet-option-change', [this]);
+ },
+ /**
+ * Called when an option link/menu is left -> reactivate the options that
+ * were activated before previews.
+ *
+ * @private
+ */
+ _onMouseleave: function () {
+ if (this.__click) {
+ return;
+ }
+ this._reset();
+ },
+});
- function map($lis) {
- return $lis.map(function () {return $(this).data("toggle_class");}).get().join(" ");
- }
- var classes = map($lis);
- var active_classes = map($lis.filter('.active, :has(.active)'));
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
- this.$target.removeClass(classes).addClass(active_classes);
- if (type !== 'reset') {
- this.$target.toggleClass(value);
- }
- },
- select_class: function (type, value, $li) {
- var $lis = this.$el.find("[data-select_class]").addBack("[data-select_class]");
+/**
+ * The registry object contains the list of available options.
+ */
+var registry = {};
- var classes = $lis.map(function () {return $(this).data("select_class");}).get().join(" ");
+registry.marginAndResize = SnippetOption.extend({
+ preventChildPropagation: true,
- this.$target.removeClass(classes);
- if (value) {
- this.$target.addClass(value);
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+ this._super();
+
+ var resize_values = this._getSize();
+ if (resize_values.n) this.$overlay.find('.oe_handle.n').removeClass('readonly');
+ if (resize_values.s) this.$overlay.find('.oe_handle.s').removeClass('readonly');
+ if (resize_values.e) this.$overlay.find('.oe_handle.e').removeClass('readonly');
+ if (resize_values.w) this.$overlay.find('.oe_handle.w').removeClass('readonly');
+ if (resize_values.size) this.$overlay.find('.oe_handle.size').removeClass('readonly');
+
+ var $auto_size = this.$overlay.find('.oe_handle.size .auto_size');
+ var $fixed_size = this.$overlay.find('.oe_handle.size .size');
+
+ var auto_sized = isNaN(parseInt(self.$target.prop('style').height));
+ $fixed_size.toggleClass('active', !auto_sized);
+ $auto_size.toggleClass('active', auto_sized);
+
+ $fixed_size.add(this.$overlay.find('.oe_handle:not(.size)')).on('mousedown', function (event) {
+ event.preventDefault();
+
+ var $handle = $(this);
+
+ var resize_values = self._getSize();
+ var compass = false;
+ var XY = false;
+ if ($handle.hasClass('n')) {
+ compass = 'n';
+ XY = 'Y';
+ }
+ else if ($handle.hasClass('s')) {
+ compass = 's';
+ XY = 'Y';
+ }
+ else if ($handle.hasClass('e')) {
+ compass = 'e';
+ XY = 'X';
+ }
+ else if ($handle.hasClass('w')) {
+ compass = 'w';
+ XY = 'X';
+ }
+ else if ($handle.hasClass('size')) {
+ compass = 'size';
+ XY = 'Y';
}
- },
-
- /**
- * Helper method to evaluate the value expression directly as a function
- */
- eval: function (type, value, $li) {
- var fn = new Function("node", "type", "value", "$li", value);
- fn.call(this, this, type, value, $li);
- },
-
- clean_for_save: editor.dummy
- });
- var registry = {};
-
- /* ----- default options ---- */
-
- registry.marginAndResize = SnippetOption.extend({
- preventChildPropagation: true,
- start: function () {
- var self = this;
- this._super();
+ var resize = resize_values[compass];
+ if (!resize) return;
- var resize_values = this.getSize();
- if (resize_values.n) this.$overlay.find(".oe_handle.n").removeClass("readonly");
- if (resize_values.s) this.$overlay.find(".oe_handle.s").removeClass("readonly");
- if (resize_values.e) this.$overlay.find(".oe_handle.e").removeClass("readonly");
- if (resize_values.w) this.$overlay.find(".oe_handle.w").removeClass("readonly");
- if (resize_values.size) this.$overlay.find(".oe_handle.size").removeClass("readonly");
- var $auto_size = this.$overlay.find(".oe_handle.size .auto_size");
- var $fixed_size = this.$overlay.find(".oe_handle.size .size");
+ if (compass === 'size') {
+ var offset = self.$target.offset().top;
+ } else {
+ var xy = event['page'+XY];
+ var current = resize[2] || 0;
+ var margin_dir = {s:'bottom', n: 'top', w: 'left', e: 'right'}[compass];
+ var real_margin = parseInt(self.$target.css('margin-'+margin_dir));
+ _.each(resize[0], function (val, key) {
+ if (self.$target.hasClass(val)) {
+ current = key;
+ } else if (resize[1][key] === real_margin) {
+ current = key;
+ }
+ });
+ var begin = current;
+ var beginClass = self.$target.attr('class');
+ var regClass = new RegExp('\\s*' + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
+ }
- var auto_sized = isNaN(parseInt(self.$target.prop("style").height));
- $fixed_size.toggleClass("active", !auto_sized);
- $auto_size.toggleClass("active", auto_sized);
+ var cursor = $handle.css('cursor')+'-important';
+ var $body = $(document.body);
+ $body.addClass(cursor);
- $fixed_size.add(this.$overlay.find(".oe_handle:not(.size)")).on('mousedown', function (event) {
+ var body_mousemove = function (event) {
event.preventDefault();
-
- var $handle = $(this);
-
- var resize_values = self.getSize();
- var compass = false;
- var XY = false;
- if ($handle.hasClass('n')) {
- compass = 'n';
- XY = 'Y';
- }
- else if ($handle.hasClass('s')) {
- compass = 's';
- XY = 'Y';
- }
- else if ($handle.hasClass('e')) {
- compass = 'e';
- XY = 'X';
+ if (compass === 'size') {
+ var dy = event.pageY-offset;
+ dy = dy - dy%resize;
+ if (dy <= 0) dy = resize;
+ self.$target.css('height', dy+'px');
+ self.$target.css('overflow', 'hidden');
+ self._onResize(compass, null, dy);
+ self.trigger_up('cover_update');
+ return;
}
- else if ($handle.hasClass('w')) {
- compass = 'w';
- XY = 'X';
+ var dd = event['page'+XY] - xy + resize[1][begin];
+ var next = current+1 === resize[1].length ? current : (current+1);
+ var prev = current ? (current-1) : 0;
+
+ var change = false;
+ if (dd > (2*resize[1][next] + resize[1][current])/3) {
+ self.$target.attr('class', (self.$target.attr('class')||'').replace(regClass, ''));
+ self.$target.addClass(resize[0][next]);
+ current = next;
+ change = true;
}
- else if ($handle.hasClass('size')) {
- compass = 'size';
- XY = 'Y';
+ if (prev !== current && dd < (2*resize[1][prev] + resize[1][current])/3) {
+ self.$target.attr('class', (self.$target.attr('class')||'').replace(regClass, ''));
+ self.$target.addClass(resize[0][prev]);
+ current = prev;
+ change = true;
}
- var resize = resize_values[compass];
- if (!resize) return;
+ if (change) {
+ self._onResize(compass, beginClass, current);
+ self.trigger_up('cover_update');
+ self._adaptMarginsPreviews();
+ $handle.addClass('oe_handle_change');
+ }
+ };
+ var body_mouseup = function () {
+ $body.unbind('mousemove', body_mousemove);
+ $body.unbind('mouseup', body_mouseup);
+ $body.removeClass(cursor);
+ setTimeout(function () {
+ if (begin !== current) {
+ self.trigger_up('request_history_undo_record', {
+ $target: self.$target,
+ event: 'resize_' + XY,
+ });
+ }
+ },0);
+ $handle.removeClass('oe_handle_change');
if (compass === 'size') {
- var offset = self.$target.offset().top;
- if (self.$target.css("background").match(/rgba\(0, 0, 0, 0\)/)) {
- self.$target.addClass("resize_editor_busy");
- }
+ $fixed_size.addClass('active');
+ $auto_size.removeClass('active');
} else {
- var xy = event['page'+XY];
- var current = resize[2] || 0;
- var margin_dir = {s:'bottom', n: 'top', w: 'left', e: 'right'}[compass];
- var real_margin = parseInt(self.$target.css('margin-'+margin_dir));
- _.each(resize[0], function (val, key) {
- if (self.$target.hasClass(val)) {
- current = key;
- } else if (resize[1][key] === real_margin) {
- current = key;
- }
- });
- var begin = current;
- var beginClass = self.$target.attr("class");
- var regClass = new RegExp("\\s*" + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
+ self._highlightMarginsPreviews();
}
+ };
+ $body.mousemove(body_mousemove);
+ $body.mouseup(body_mouseup);
+ });
+ $auto_size.on('click', function () {
+ self.$target.css('height', '');
+ self.$target.css('overflow', '');
+ self.trigger_up('request_history_undo_record', {
+ $target: self.$target,
+ event: 'resize_Y',
+ });
+ self.trigger_up('cover_update');
- self.buildingBlock.editor_busy = true;
-
- var cursor = $handle.css("cursor")+'-important';
- var $body = $(document.body);
- $body.addClass(cursor);
-
- var body_mousemove = function (event) {
- event.preventDefault();
- if (compass === 'size') {
- var dy = event.pageY-offset;
- dy = dy - dy%resize;
- if (dy <= 0) dy = resize;
- self.$target.css("height", dy+"px");
- self.$target.css("overflow", "hidden");
- self.on_resize(compass, null, dy);
- self.buildingBlock.cover_target(self.$overlay, self.$target);
- return;
- }
- var dd = event['page'+XY] - xy + resize[1][begin];
- var next = current+1 === resize[1].length ? current : (current+1);
- var prev = current ? (current-1) : 0;
-
- var change = false;
- if (dd > (2*resize[1][next] + resize[1][current])/3) {
- self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
- self.$target.addClass(resize[0][next]);
- current = next;
- change = true;
- }
- if (prev !== current && dd < (2*resize[1][prev] + resize[1][current])/3) {
- self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
- self.$target.addClass(resize[0][prev]);
- current = prev;
- change = true;
- }
+ $fixed_size.removeClass('active');
+ $auto_size.addClass('active');
- if (change) {
- self.on_resize(compass, beginClass, current);
- self.buildingBlock.cover_target(self.$overlay, self.$target);
- self.adapt_margin_preview_size();
- $handle.addClass('oe_handle_change');
- }
- };
+ return false;
+ });
+ },
+ /**
+ * @override
+ */
+ onFocus : function () {
+ this._updateCursor();
+ this._adaptMarginsPreviews();
+ },
- var body_mouseup = function () {
- $body.unbind('mousemove', body_mousemove);
- $body.unbind('mouseup', body_mouseup);
- $body.removeClass(cursor);
- setTimeout(function () {
- self.buildingBlock.editor_busy = false;
- if (begin !== current) {
- self.buildingBlock.getParent().rte.historyRecordUndo(self.$target, 'resize_' + XY);
- }
- },0);
- self.$target.removeClass("resize_editor_busy");
- $handle.removeClass('oe_handle_change');
-
- if (compass === "size") {
- $fixed_size.addClass("active");
- $auto_size.removeClass("active");
- } else {
- self._highlight_margins_preview(300);
- }
- };
- $body.mousemove(body_mousemove);
- $body.mouseup(body_mouseup);
- });
- $auto_size.on('click', function () {
- self.$target.css("height", "");
- self.$target.css("overflow", "");
- self.buildingBlock.getParent().rte.historyRecordUndo(self.$target, 'resize_Y');
- self.buildingBlock.cover_target(self.$overlay, self.$target);
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- $fixed_size.removeClass("active");
- $auto_size.addClass("active");
+ /**
+ * Adapts the margin handles to fit the left, top and bottom margins.
+ *
+ * @private
+ */
+ _adaptMarginsPreviews: function () {
+ var self = this;
+ var ml = this.$target.css('margin-left');
+ _.each(this.$overlay.find(".oe_handle.n, .oe_handle.s"), function (handle) {
+ var $handle = $(handle);
+ var direction = $handle.hasClass('n') ? 'top': 'bottom';
+ $handle.height(self.$target.css('margin-' + direction));
+ });
+ this.$overlay.find(".oe_handle.w").css({
+ width: ml,
+ left: '-' + ml,
+ });
+ },
+ /**
+ * Returns an object whose keys indicate the different editable dimensions.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _getSize: function () {
+ this.grid = {};
+ return this.grid;
+ },
+ /**
+ * Highlights the margins previews for a while.
+ *
+ * @private
+ */
+ _highlightMarginsPreviews: function () {
+ var $handlers = this.$overlay.find(".oe_handle.n, .oe_handle.s, .oe_handle.w");
+ $handlers.addClass('oe_handle_active').delay(300).queue(function () {
+ $handlers.removeClass('oe_handle_active').dequeue();
+ });
+ },
+ /**
+ * Called when the snippet is being resized and its classes changes.
+ *
+ * @private
+ * @param {string} compass - resize direction ('n', 's', 'e' or 'w')
+ * @param {?} beginClass - attributes class at the beginning
+ * @param {?} current - curent increment in this.grid
+ */
+ _onResize: function (compass, beginClass, current) {
+ this._updateCursor();
+ },
+ /**
+ * Adapts the resize handles according to the classes and dimensions.
+ *
+ * @private
+ */
+ _updateCursor : function () {
+ var _class = this.$target.attr('class') || '';
+ var $handle_s = this.$overlay.find('.oe_handle.s');
+ var $handle_n = this.$overlay.find('.oe_handle.n');
+ var $handle_w = this.$overlay.find('.oe_handle.w');
+
+ var col = _class.match(/col-md-([0-9-]+)/i);
+ col = col ? +col[1] : 0;
+
+ var offset = _class.match(/col-md-offset-([0-9-]+)/i);
+ offset = offset ? +offset[1] : 0;
+
+ var overlay_class = this.$overlay.attr('class').replace(/(^|\s+)block-[^\s]*/gi, '');
+ if (col+offset >= 12) overlay_class+= ' block-e-right';
+ if (col === 1) overlay_class+= ' block-w-right block-e-left';
+ if (offset === 0) overlay_class+= ' block-w-left';
+ $handle_w.toggleClass('oe_handle_centered', offset > 0).toggleClass('o_handle_edited', offset >= 1 );
+
+ var mb = _class.match(/mb([0-9-]+)/i);
+ mb = mb ? +mb[1] : parseInt(this.$target.css('margin-bottom'));
+ if (mb >= 128) overlay_class+= ' block-s-bottom';
+ else if (!mb) overlay_class+= ' block-s-top';
+ $handle_s.toggleClass('oe_handle_centered', mb >= 32).toggleClass('o_handle_edited', mb > 0 );
+
+ var mt = _class.match(/mt([0-9-]+)/i);
+ mt = mt ? +mt[1] : parseInt(this.$target.css('margin-top'));
+ if (mt >= 128) overlay_class+= ' block-n-top';
+ else if (!mt) overlay_class+= ' block-n-bottom';
+ $handle_n.toggleClass('oe_handle_centered', mt >= 32).toggleClass('o_handle_edited', mt > 0 );
+
+ this.$overlay.attr('class', overlay_class);
+ },
+});
- return false;
- });
- },
- getSize: function () {
- this.grid = {};
- return this.grid;
- },
-
- on_focus : function () {
- this._super();
- this.change_cursor();
- this.adapt_margin_preview_size();
- },
-
- change_cursor : function () {
- var _class = this.$target.attr("class") || "";
- var $handle_s = this.$overlay.find('.oe_handle.s');
- var $handle_n = this.$overlay.find('.oe_handle.n');
- var $handle_w = this.$overlay.find('.oe_handle.w');
-
- var col = _class.match(/col-md-([0-9-]+)/i);
- col = col ? +col[1] : 0;
-
- var offset = _class.match(/col-md-offset-([0-9-]+)/i);
- offset = offset ? +offset[1] : 0;
-
- var overlay_class = this.$overlay.attr("class").replace(/(^|\s+)block-[^\s]*/gi, '');
- if (col+offset >= 12) overlay_class+= " block-e-right";
- if (col === 1) overlay_class+= " block-w-right block-e-left";
- if (offset === 0) overlay_class+= " block-w-left";
- $handle_w.toggleClass('oe_handle_centered', offset > 0).toggleClass('o_handle_edited', offset >= 1 );
-
- var mb = _class.match(/mb([0-9-]+)/i);
- mb = mb ? +mb[1] : parseInt(this.$target.css('margin-bottom'));
- if (mb >= 128) overlay_class+= " block-s-bottom";
- else if (!mb) overlay_class+= " block-s-top";
- $handle_s.toggleClass('oe_handle_centered', mb >= 32).toggleClass('o_handle_edited', mb > 0 );
-
- var mt = _class.match(/mt([0-9-]+)/i);
- mt = mt ? +mt[1] : parseInt(this.$target.css('margin-top'));
- if (mt >= 128) overlay_class+= " block-n-top";
- else if (!mt) overlay_class+= " block-n-bottom";
- $handle_n.toggleClass('oe_handle_centered', mt >= 32).toggleClass('o_handle_edited', mt > 0 );
-
- this.$overlay.attr("class", overlay_class);
- },
-
- adapt_margin_preview_size : function () {
- var self = this,
- ml = this.$target.css('margin-left');
- _.each(this.$overlay.find(".oe_handle.n, .oe_handle.s"), function (handle) {
- var $handle = $(handle);
- var direction = $handle.hasClass('n') ? 'top': 'bottom';
- $handle.height(self.$target.css('margin-' + direction ));
- });
- this.$overlay.find(".oe_handle.w").css({
- width: ml,
- left: '-' + ml
- });
- },
+/**
+ * Handles the edition of margin-top and margin-bottom.
+ */
+registry['margin-y'] = registry.marginAndResize.extend({
- _highlight_margins_preview: function (timing) {
- var handlers = this.$overlay.find(".oe_handle.n, .oe_handle.s, .oe_handle.w");
- handlers.addClass('oe_handle_active').delay(timing).queue(function(){
- handlers.removeClass('oe_handle_active').dequeue();
- });
- },
-
- /* on_resize
- * called when the box is resizing and the class change, before the cover_target
- * @compass: resize direction : 'n', 's', 'e', 'w'
- * @beginClass: attributes class at the begin
- * @current: curent increment in this.grid
- */
- on_resize: function () {
- this.change_cursor();
- }
- });
-
- registry["margin-y"] = registry.marginAndResize.extend({
- preventChildPropagation: true,
-
- getSize: function () {
- this.grid = this._super();
- var grid = [0,4,8,16,32,48,64,92,128];
- this.grid = {
- // list of class (Array), grid (Array), default value (INT)
- n: [_.map(grid, function (v) {return 'mt'+v;}), grid],
- s: [_.map(grid, function (v) {return 'mb'+v;}), grid],
- // INT if the user can resize the snippet (resizing per INT px)
- size: null
- };
- return this.grid;
- },
- });
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- registry.resize = registry.marginAndResize.extend({
- preventChildPropagation: true,
+ /**
+ * @override
+ */
+ _getSize: function () {
+ this.grid = this._super();
+ var grid = [0,4,8,16,32,48,64,92,128];
+ this.grid = {
+ // list of class (Array), grid (Array), default value (INT)
+ n: [_.map(grid, function (v) {return 'mt'+v;}), grid],
+ s: [_.map(grid, function (v) {return 'mb'+v;}), grid],
+ // INT if the user can resize the snippet (resizing per INT px)
+ size: null
+ };
+ return this.grid;
+ },
+});
+
+/**
+ * Handles the edition of snippet's height.
+ */
+registry.resize = registry.marginAndResize.extend({
- getSize: function () {
- this.grid = this._super();
- this.grid.size = 8;
- return this.grid;
- },
- });
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
/**
- * The colorpicker option is designed to change the background color class of a snippet. This class change the
- * default background color and text color of the snippet content.
+ * @override
*/
- registry.colorpicker = SnippetOption.extend({
- start: function () {
- var self = this;
- var res = this._super.apply(this, arguments);
+ _getSize: function () {
+ this.grid = this._super();
+ this.grid.size = 8;
+ return this.grid;
+ },
+});
- if (!this.$el.find('.colorpicker').length) {
- var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
- var $clpicker = $(qweb.render('web_editor.colorpicker'));
+/**
+ * Handles the edition of snippet's background color classes.
+ */
+registry.colorpicker = SnippetOption.extend({
+ xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
+ events: _.extend({}, SnippetOption.prototype.events || {}, {
+ 'click .colorpicker button': '_onColorButtonClick',
+ 'mouseenter .colorpicker button': '_onColorButtonEnter',
+ 'mouseleave .colorpicker button': '_onColorButtonLeave',
+ 'click .note-color-reset': '_onColorResetButtonClick',
+ }),
- // Retrieve excluded palettes list
- var excluded = [];
- if (this.data.paletteExclude) {
- excluded = this.data.paletteExclude.replace(/ /g, '').split(',');
- }
- // Apply a custom title if specified
- if (this.data.paletteTitle) {
- $pt.find('.note-palette-title').text(this.data.paletteTitle);
- }
-
- var $toggles = $pt.find('.o_colorpicker_section_menu');
- var $tabs = $pt.find('.o_colorpicker_section_tabs');
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+ var res = this._super.apply(this, arguments);
+
+ if (!this.$el.find('.colorpicker').length) {
+ var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
+ var $clpicker = $(qweb.render('web_editor.colorpicker'));
+
+ // Retrieve excluded palettes list
+ var excluded = [];
+ if (this.data.paletteExclude) {
+ excluded = this.data.paletteExclude.replace(/ /g, '').split(',');
+ }
+ // Apply a custom title if specified
+ if (this.data.paletteTitle) {
+ $pt.find('.note-palette-title').text(this.data.paletteTitle);
+ }
- // Remove excluded palettes
- _.each(excluded, function (exc) {
- $clpicker.find('[data-name="' + exc + '"]').remove();
- });
+ var $toggles = $pt.find('.o_colorpicker_section_menu');
+ var $tabs = $pt.find('.o_colorpicker_section_tabs');
- var $sections = $clpicker.find('.o_colorpicker_section');
+ // Remove excluded palettes
+ _.each(excluded, function (exc) {
+ $clpicker.find('[data-name="' + exc + '"]').remove();
+ });
- if ($sections.length > 1) { // Multi-palette layout
- $sections.each(function () {
- var $section = $(this);
- var id = 'o_palette_' + $section.data('name') + _.uniqueId();
+ var $sections = $clpicker.find('.o_colorpicker_section');
- var $li = $('<li/>')
- .append($('<a/>', {href: '#' + id})
- .append($('<i/>', {'class': $section.data('iconClass') || '', html: $section.data('iconContent') || ''})));
- $toggles.append($li);
+ if ($sections.length > 1) { // Multi-palette layout
+ $sections.each(function () {
+ var $section = $(this);
+ var id = 'o_palette_' + $section.data('name') + _.uniqueId();
- $tabs.append($section.addClass('tab-pane').attr('id', id));
- });
+ var $li = $('<li/>')
+ .append($('<a/>', {href: '#' + id})
+ .append($('<i/>', {'class': $section.data('iconClass') || '', html: $section.data('iconContent') || ''})));
+ $toggles.append($li);
- // If a default palette is defined, make it active
- if (this.data.paletteDefault) {
- var $palette_def = $tabs.find('div[data-name="' + self.data.paletteDefault + '"]');
- var pos = $tabs.find('> div').index($palette_def);
+ $tabs.append($section.addClass('tab-pane').attr('id', id));
+ });
- $toggles.children('li').eq(pos).addClass('active');
- $palette_def.addClass('active');
- } else {
- $toggles.find('li').first().addClass('active');
- $tabs.find('div').first().addClass('active');
- }
+ // If a default palette is defined, make it active
+ if (this.data.paletteDefault) {
+ var $palette_def = $tabs.find('div[data-name="' + self.data.paletteDefault + '"]');
+ var pos = $tabs.find('> div').index($palette_def);
- $toggles.on('click mouseover', '> li > a', function (e) {
- e.preventDefault();
- e.stopPropagation();
- $(this).tab('show');
- });
- } else if ($sections.length === 1) { // Unique palette layout
- $tabs.addClass('o_unique_palette').append($sections.addClass('tab-pane active'));
+ $toggles.children('li').eq(pos).addClass('active');
+ $palette_def.addClass('active');
} else {
- $toggles.parent().empty().append($clpicker);
+ $toggles.find('li').first().addClass('active');
+ $tabs.find('div').first().addClass('active');
}
- this.$el.find('li').append($pt);
+ $toggles.on('click mouseover', '> li > a', function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ $(this).tab('show');
+ });
+ } else if ($sections.length === 1) { // Unique palette layout
+ $tabs.addClass('o_unique_palette').append($sections.addClass('tab-pane active'));
+ } else {
+ $toggles.parent().empty().append($clpicker);
}
- if (this.$el.data('area')) {
- this.$target = this.$target.find(this.$el.data('area'));
- this.$el.removeData('area').removeAttr('area');
+
+ this.$el.find('li').append($pt);
+ }
+ if (this.$el.data('area')) {
+ this.$target = this.$target.find(this.$el.data('area'));
+ this.$el.removeData('area').removeAttr('area');
+ }
+
+ var classes = [];
+ this.$el.find('.colorpicker button').each(function () {
+ var $color = $(this);
+ if (!$color.data('color')) {
+ return;
}
- var classes = [];
- this.$el.find(".colorpicker button").each(function () {
- var $color = $(this);
- if (!$color.data("color")) {
- return;
- }
+ var className = 'bg-' + $color.data('color');
+ $color.addClass(className);
+ if (self.$target.hasClass(className)) {
+ self.color = className;
+ $color.addClass('selected');
+ }
+ classes.push(className);
+ });
+ this.classes = classes.join(' ');
- var className = 'bg-' + $color.data('color');
- $color.addClass(className);
- if (self.$target.hasClass(className)) {
- self.color = className;
- $color.addClass("selected");
- }
- classes.push(className);
- });
- this.classes = classes.join(" ");
-
- this.bind_events();
- return res;
- },
- bind_events: function () {
- var self = this;
- var $colors = this.$el.find(".colorpicker button");
- $colors
- .mouseenter(function (e) {
- self.$target.removeClass(self.classes);
- var color = $(this).data("color");
- if (color) {
- self.$target.addClass('bg-' + color);
- }
- self.$target.trigger("background-color-event", e.type);
- })
- .mouseleave(function (e) {
- self.$target.removeClass(self.classes);
- var $selected = $colors.filter(".selected");
- var color = $selected.length && $selected.data("color");
- if (color) {
- self.$target.addClass('bg-' + color);
- }
- self.$target.trigger("background-color-event", e.type);
- })
- .click(function (e) {
- $colors.removeClass("selected");
- $(this).addClass("selected");
- self.$target.closest(".o_editable").trigger("content_changed");
- self.$target.trigger("background-color-event", e.type);
- });
+ return res;
+ },
- this.$el.find('.note-color-reset').on('click', function () {
- self.$target.removeClass(self.classes);
- $colors.removeClass("selected");
- });
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when a color button is clicked -> confirm the preview.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onColorButtonClick: function (ev) {
+ this.$el.find('.colorpicker button.selected').removeClass('selected');
+ $(ev.currentTarget).addClass('selected');
+ this.$target.closest('.o_editable').trigger('content_changed');
+ this.$target.trigger('background-color-event', ev.type);
+ },
+ /**
+ * Called when a color button is entered -> preview the background color.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onColorButtonEnter: function (ev) {
+ this.$target.removeClass(this.classes);
+ var color = $(ev.currentTarget).data('color');
+ if (color) {
+ this.$target.addClass('bg-' + color);
}
- });
+ this.$target.trigger('background-color-event', ev.type);
+ },
+ /**
+ * Called when a color button is left -> cancel the preview.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onColorButtonLeave: function (ev) {
+ this.$target.removeClass(this.classes);
+ var $selected = this.$el.find('.colorpicker button.selected');
+ var color = $selected.length && $selected.data('color');
+ if (color) {
+ this.$target.addClass('bg-' + color);
+ }
+ this.$target.trigger('background-color-event', ev.type);
+ },
+ /**
+ * Called when the color reset button is clicked -> remove all background
+ * color classes.
+ *
+ * @private
+ */
+ _onColorResetButtonClick: function () {
+ this.$target.removeClass(this.classes);
+ this.$el.find('.colorpicker button.selected').removeClass('selected');
+ },
+});
+/**
+ * Handles the edition of snippet's background image.
+ */
+registry.background = SnippetOption.extend({
/**
- * The background option is designed to change the background image of a snippet.
+ * @override
*/
- registry.background = SnippetOption.extend({
- start: function () {
- var res = this._super.apply(this, arguments);
- this.bind_bg_events();
- return res;
- },
+ start: function () {
+ var res = this._super.apply(this, arguments);
+ this.bindBackgroundEvents();
+ return res;
+ },
- bind_bg_events: function () {
- this.$target.off(".background-option")
- .on("background-color-event.background-option", (function (e, type) {
- e.stopPropagation();
- if (e.currentTarget !== e.target) return;
- this.$el.find("li[data-background=''] > a").trigger(type);
- }).bind(this));
- },
- background: function (type, value, $li) {
- if (value && value.length) {
- this.$target.css("background-image", "url(" + value + ")");
- this.$target.addClass("oe_img_bg");
- } else {
- this.$target.css("background-image", "");
- this.$target.removeClass("oe_img_bg oe_custom_bg");
- }
- },
- select_class: function (type, value, $li) {
- this.background(type, '', $li);
- this._super(type, value ? (value + " oe_img_bg") : value, $li);
- },
- choose_image: function (type, value, $li) {
- if(type !== "click") {
- return;
- }
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
- // Put fake image in the DOM, edit it and use it as background-image
- var $image = $("<img/>", {"class": "hidden", "src": value}).appendTo(this.$target);
+ /**
+ * Handles a background change.
+ *
+ * @see this.selectClass for parameters
+ */
+ background: function (previewMode, value, $li) {
+ if (value && value.length) {
+ this.$target.css('background-image', 'url(' + value + ')');
+ this.$target.addClass('oe_img_bg');
+ } else {
+ this.$target.css('background-image', '');
+ this.$target.removeClass('oe_img_bg oe_custom_bg');
+ }
+ },
+ /**
+ * @override
+ */
+ selectClass: function (previewMode, value, $li) {
+ this.background(previewMode, '', $li);
+ this._super(previewMode, value ? (value + ' oe_img_bg') : value, $li);
+ },
+ /**
+ * Opens a media dialog to add a custom background image.
+ *
+ * @see this.selectClass for parameters
+ */
+ chooseImage: function (previewMode, value, $li) {
+ // Put fake image in the DOM, edit it and use it as background-image
+ var $image = $('<img/>', {class: 'hidden', src: value}).appendTo(this.$target);
+
+ var _editor = new widget.MediaDialog(this, {}, null, $image[0]).open();
+ _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+
+ _editor.on('save', this, function () {
+ var value = $image.attr('src');
+ this.background(previewMode, value, $li);
+ this.$target.addClass('oe_custom_bg');
+ this._setActive();
+ this.$target.trigger('snippet-option-change', [this]);
+ });
+ _editor.on('closed', this, function () {
+ $image.remove();
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
- var _editor = new widget.MediaDialog(null, {}, null, $image[0]).open();
- _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+ /**
+ * Attaches events so that when a background-color is set, the background
+ * image is removed.
+ */
+ bindBackgroundEvents: function () {
+ this.$target.off('.background-option')
+ .on('background-color-event.background-option', (function (e, type) {
+ e.stopPropagation();
+ if (e.currentTarget !== e.target) return;
+ this.$el.find('li[data-background=""] > a').trigger(type);
+ }).bind(this));
+ },
+ /**
+ * @override
+ */
+ setTarget: function () {
+ this._super.apply(this, arguments);
+ this.bindBackgroundEvents();
+ },
- _editor.on('save', this, function () {
- var value = $image.attr("src");
- this.background(type, value, $li);
- this.$target.addClass("oe_custom_bg");
- this.set_active();
- this.$target.trigger("snippet-option-change", [this]);
- });
- _editor.on('closed', this, function () {
- $image.remove();
- });
- },
- set_active: function () {
- this._super.apply(this, arguments);
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- var src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
- if (this.$target.hasClass('oe_custom_bg')) {
- this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src);
- }
+ /**
+ * @override
+ */
+ _setActive: function () {
+ this._super.apply(this, arguments);
- this.$el.find('li[data-background]')
- .removeClass("active")
- .filter(function () {
- var bgOption = $(this).data("background");
- return (bgOption === "" && src === "" || bgOption !== "" && src.indexOf(bgOption) >= 0);
- })
- .addClass("active");
+ var src = this.$target.css('background-image').replace(/url\(['"]*|['"]*\)|^none$/g, '');
+ if (this.$target.hasClass('oe_custom_bg')) {
+ this.$el.find('li[data-choose-image]').data('background', src).attr('data-background', src);
}
- });
+
+ this.$el.find('li[data-background]')
+ .removeClass('active')
+ .filter(function () {
+ var bgOption = $(this).data('background');
+ return (bgOption === '' && src === '' || bgOption !== '' && src.indexOf(bgOption) >= 0);
+ })
+ .addClass('active');
+ }
+});
+
+/**
+ * Handles the edition of snippet's background image position.
+ */
+registry.background_position = SnippetOption.extend({
+ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
/**
- * The background_position option is designed to change the background image position of a snippet.
+ * @override
*/
- registry.background_position = SnippetOption.extend({
- start: function () {
- this._super.apply(this, arguments);
- this.on_focus();
- var self = this;
- this.$target.on("snippet-option-change", function () {
- self.on_focus();
- });
- },
- on_focus: function () {
- this._super.apply(this, arguments);
- this.$el.toggleClass('hidden', this.$target.css('background-image') === 'none');
- },
- background_position: function (type, value, $li) {
- if (type != 'click') { return; }
- var self = this;
-
- this.previous_state = [this.$target.attr('class'), this.$target.css('background-size'), this.$target.css('background-position')];
-
- this.bg_pos = self.$target.css('background-position').split(' ');
- this.bg_siz = self.$target.css('background-size').split(' ');
-
- this.modal = new Dialog(null, {
- title: _t("Background Image Sizing"),
- $content: $(qweb.render('web_editor.dialog.background_position')),
- buttons: [
- {text: _t("Ok"), classes: "btn-primary", close: true, click: _.bind(this.save, this)},
- {text: _t("Discard"), close: true, click: _.bind(this.discard, this)},
- ],
- }).open();
-
- this.modal.opened().then(function () {
- // Fetch data form $target
- var value = ((self.$target.hasClass('o_bg_img_opt_contain'))? 'contain' : ((self.$target.hasClass('o_bg_img_opt_custom'))? 'custom' : 'cover'));
- self.modal.$("> label > input[value=" + value + "]").prop('checked', true);
-
- if(self.$target.hasClass("o_bg_img_opt_repeat")) {
- self.modal.$("#o_bg_img_opt_contain_repeat").prop('checked', true);
- self.modal.$("#o_bg_img_opt_custom_repeat").val('o_bg_img_opt_repeat');
- } else if (self.$target.hasClass("o_bg_img_opt_repeat_x")) {
- self.modal.$("#o_bg_img_opt_custom_repeat").val('o_bg_img_opt_repeat_x');
- } else if (self.$target.hasClass("o_bg_img_opt_repeat_y")) {
- self.modal.$("#o_bg_img_opt_custom_repeat").val('o_bg_img_opt_repeat_y');
- }
+ start: function () {
+ this._super.apply(this, arguments);
+ var self = this;
+ this.$target.on('snippet-option-change', function () {
+ self.onFocus();
+ });
+ },
+ /**
+ * @override
+ */
+ onFocus: function () {
+ this.$el.toggleClass('hidden', this.$target.css('background-image') === 'none');
+ },
- if(self.bg_pos.length > 1) {
- self.bg_pos = {
- x: self.bg_pos[0],
- y: self.bg_pos[1],
- };
- self.modal.$("#o_bg_img_opt_custom_pos_x").val(self.bg_pos.x.replace('%', ''));
- self.modal.$("#o_bg_img_opt_custom_pos_y").val(self.bg_pos.y.replace('%', ''));
- }
- if(self.bg_siz.length > 1) {
- self.modal.$("#o_bg_img_opt_custom_size_x").val(self.bg_siz[0].replace('%', ''));
- self.modal.$("#o_bg_img_opt_custom_size_y").val(self.bg_siz[1].replace('%', ''));
- }
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
- // Focus Point
- self.$focus = self.modal.$(".o_focus_point");
- self.update_pos_information();
+ /**
+ * Opens a Dialog to edit the snippet's backgroung image position.
+ *
+ * @see this.selectClass for parameters
+ */
+ backgroundPosition: function (previewMode, value, $li) {
+ var self = this;
+
+ this.previous_state = [this.$target.attr('class'), this.$target.css('background-size'), this.$target.css('background-position')];
+
+ this.bg_pos = self.$target.css('background-position').split(' ');
+ this.bg_siz = self.$target.css('background-size').split(' ');
+
+ this.modal = new Dialog(null, {
+ title: _t("Background Image Sizing"),
+ $content: $(qweb.render('web_editor.dialog.background_position')),
+ buttons: [
+ {text: _t("Ok"), classes: 'btn-primary', close: true, click: _.bind(this._saveChanges, this)},
+ {text: _t("Discard"), close: true, click: _.bind(this._discardChanges, this)},
+ ],
+ }).open();
+
+ this.modal.opened().then(function () {
+ // Fetch data form $target
+ var value = ((self.$target.hasClass('o_bg_img_opt_contain'))? 'contain' : ((self.$target.hasClass('o_bg_img_opt_custom'))? 'custom' : 'cover'));
+ self.modal.$('> label > input[value=' + value + ']').prop('checked', true);
+
+ if (self.$target.hasClass('o_bg_img_opt_repeat')) {
+ self.modal.$('#o_bg_img_opt_contain_repeat').prop('checked', true);
+ self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat');
+ } else if (self.$target.hasClass('o_bg_img_opt_repeat_x')) {
+ self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat_x');
+ } else if (self.$target.hasClass('o_bg_img_opt_repeat_y')) {
+ self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat_y');
+ }
- var img_url = /\(['"]?([^'"]+)['"]?\)/g.exec(self.$target.css('background-image'));
- img_url = (img_url && img_url[1]) || '';
- var $img = $('<img/>', {'class': 'img img-responsive', src: img_url});
- $img.on('load', function () {
- self.bind_img_events($img);
- });
- $img.prependTo(self.modal.$(".o_bg_img_opt_object"));
+ if (self.bg_pos.length > 1) {
+ self.bg_pos = {
+ x: self.bg_pos[0],
+ y: self.bg_pos[1],
+ };
+ self.modal.$('#o_bg_img_opt_custom_pos_x').val(self.bg_pos.x.replace('%', ''));
+ self.modal.$('#o_bg_img_opt_custom_pos_y').val(self.bg_pos.y.replace('%', ''));
+ }
+ if (self.bg_siz.length > 1) {
+ self.modal.$('#o_bg_img_opt_custom_size_x').val(self.bg_siz[0].replace('%', ''));
+ self.modal.$('#o_bg_img_opt_custom_size_y').val(self.bg_siz[1].replace('%', ''));
+ }
- // Bind events
- self.modal.$el.on('change', '> label > input', function (e) {
- self.modal.$('> .o_bg_img_opt').addClass('o_hidden')
- .filter("[data-value=" + e.target.value + "]")
- .removeClass('o_hidden');
- });
- self.modal.$el.on('change', 'input, select', function (e) {
- self.save();
- });
- self.modal.$("> label > input:checked").trigger('change');
- });
- },
- bind_img_events: function ($img) {
- var self = this;
+ // Focus Point
+ self.$focus = self.modal.$('.o_focus_point');
+ self._updatePosInformation();
- var mousedown = false;
- $img.on('mousedown', function (e) {
- mousedown = true;
+ var img_url = /\(['"]?([^'"]+)['"]?\)/g.exec(self.$target.css('background-image'));
+ img_url = (img_url && img_url[1]) || '';
+ var $img = $('<img/>', {class: 'img img-responsive', src: img_url});
+ $img.on('load', function () {
+ self._bindImageEvents($img);
});
- $img.on('mousemove', function (e) {
- if (mousedown) {
- _update(e);
- }
+ $img.prependTo(self.modal.$('.o_bg_img_opt_object'));
+
+ // Bind events
+ self.modal.$el.on('change', '> label > input', function (e) {
+ self.modal.$('> .o_bg_img_opt').addClass('o_hidden')
+ .filter('[data-value=' + e.target.value + ']')
+ .removeClass('o_hidden');
});
- $img.on('mouseup', function (e) {
- self.$focus.addClass('o_with_transition');
- _update(e);
- setTimeout(function () {
- self.$focus.removeClass('o_with_transition');
- }, 200);
- mousedown = false;
+ self.modal.$el.on('change', 'input, select', function (e) {
+ self._saveChanges();
});
+ self.modal.$('> label > input:checked').trigger('change');
+ });
+ },
- function _update(e) {
- var posX = e.pageX - $(e.target).offset().left;
- var posY = e.pageY - $(e.target).offset().top;
- self.bg_pos = {
- x: clip_value(posX/$img.width()*100).toFixed(2) + '%',
- y: clip_value(posY/$img.height()*100).toFixed(2) + '%',
- };
- self.update_pos_information();
- self.save();
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Bind events on the given image so that the users can adapt the focus
+ * point.
+ *
+ * @private
+ * @param {jQuery} $img
+ */
+ _bindImageEvents: function ($img) {
+ var self = this;
+
+ var mousedown = false;
+ $img.on('mousedown', function (e) {
+ mousedown = true;
+ });
+ $img.on('mousemove', function (e) {
+ if (mousedown) {
+ _update(e);
}
+ });
+ $img.on('mouseup', function (e) {
+ self.$focus.addClass('o_with_transition');
+ _update(e);
+ setTimeout(function () {
+ self.$focus.removeClass('o_with_transition');
+ }, 200);
+ mousedown = false;
+ });
+
+ function _update(e) {
+ var posX = e.pageX - $(e.target).offset().left;
+ var posY = e.pageY - $(e.target).offset().top;
+ self.bg_pos = {
+ x: clipValue(posX/$img.width()*100).toFixed(2) + '%',
+ y: clipValue(posY/$img.height()*100).toFixed(2) + '%',
+ };
+ self._updatePosInformation();
+ self._saveChanges();
- function clip_value(value) {
+ function clipValue(value) {
return Math.max(0, Math.min(value, 100));
}
- },
- update_pos_information: function () {
- this.modal.$(".o_bg_img_opt_ui_info .o_x").text(this.bg_pos.x);
- this.modal.$(".o_bg_img_opt_ui_info .o_y").text(this.bg_pos.y);
- this.$focus.css({
- left: this.bg_pos.x,
- top: this.bg_pos.y,
+ }
+ },
+ /**
+ * Removes all option-related classes and style on the target element.
+ *
+ * @private
+ */
+ _clean: function () {
+ this.$target.removeClass('o_bg_img_opt_contain o_bg_img_opt_custom o_bg_img_opt_repeat o_bg_img_opt_repeat_x o_bg_img_opt_repeat_y')
+ .css({
+ 'background-size': '',
+ 'background-position': '',
+ });
+ },
+ /**
+ * Restores the target style before last edition made with the option.
+ *
+ * @private
+ */
+ _discardChanges: function () {
+ this._clean();
+ if (this.previous_state) {
+ this.$target.addClass(this.previous_state[0]).css({
+ 'background-size': this.previous_state[1],
+ 'background-position': this.previous_state[2],
});
- },
- save: function () {
- this.clean();
-
- var bg_img_size = this.modal.$('> :not(label):not(.o_hidden)').data('value') || 'cover';
- switch (bg_img_size) {
- case "cover":
- this.$target.css('background-position', this.bg_pos.x + ' ' + this.bg_pos.y);
- break;
- case "contain":
- this.$target.addClass('o_bg_img_opt_contain');
- this.$target.toggleClass('o_bg_img_opt_repeat', this.modal.$("#o_bg_img_opt_contain_repeat").prop("checked"));
- break;
- case "custom":
- this.$target.addClass('o_bg_img_opt_custom');
- var sizeX = this.modal.$("#o_bg_img_opt_custom_size_x").val();
- var sizeY = this.modal.$("#o_bg_img_opt_custom_size_y").val();
- var posX = this.modal.$("#o_bg_img_opt_custom_pos_x").val();
- var posY = this.modal.$("#o_bg_img_opt_custom_pos_y").val();
- this.$target.addClass(this.modal.$("#o_bg_img_opt_custom_repeat").val())
- .css({
- 'background-size': ((sizeX)? sizeX + '%' : 'auto') + " " + ((sizeY)? sizeY + '%' : 'auto'),
- 'background-position': ((posX)? posX + '%' : 'auto') + " " + ((posY)? posY + '%' : 'auto'),
- });
- break;
- }
- },
- discard: function () {
- this.clean();
- if (this.previous_state) {
- this.$target.addClass(this.previous_state[0]).css({
- 'background-size': this.previous_state[1],
- 'background-position': this.previous_state[2],
+ }
+ },
+ /**
+ * Updates the visual representation of the chosen background position.
+ *
+ * @private
+ */
+ _updatePosInformation: function () {
+ this.modal.$('.o_bg_img_opt_ui_info .o_x').text(this.bg_pos.x);
+ this.modal.$('.o_bg_img_opt_ui_info .o_y').text(this.bg_pos.y);
+ this.$focus.css({
+ left: this.bg_pos.x,
+ top: this.bg_pos.y,
+ });
+ },
+ /**
+ * Updates the target element to match the chosen options.
+ *
+ * @private
+ */
+ _saveChanges: function () {
+ this._clean();
+
+ var bg_img_size = this.modal.$('> :not(label):not(.o_hidden)').data('value') || 'cover';
+ switch (bg_img_size) {
+ case 'cover':
+ this.$target.css('background-position', this.bg_pos.x + ' ' + this.bg_pos.y);
+ break;
+ case 'contain':
+ this.$target.addClass('o_bg_img_opt_contain');
+ this.$target.toggleClass('o_bg_img_opt_repeat', this.modal.$('#o_bg_img_opt_contain_repeat').prop('checked'));
+ break;
+ case 'custom':
+ this.$target.addClass('o_bg_img_opt_custom');
+ var sizeX = this.modal.$('#o_bg_img_opt_custom_size_x').val();
+ var sizeY = this.modal.$('#o_bg_img_opt_custom_size_y').val();
+ var posX = this.modal.$('#o_bg_img_opt_custom_pos_x').val();
+ var posY = this.modal.$('#o_bg_img_opt_custom_pos_y').val();
+ this.$target.addClass(this.modal.$('#o_bg_img_opt_custom_repeat').val())
+ .css({
+ 'background-size': ((sizeX)? sizeX + '%' : 'auto') + ' ' + ((sizeY)? sizeY + '%' : 'auto'),
+ 'background-position': ((posX)? posX + '%' : 'auto') + ' ' + ((posY)? posY + '%' : 'auto'),
+ });
+ break;
+ }
+ },
+});
+
+/**
+ * Allows to replace a text value with the name of a database record.
+ * @todo replace this mechanism with real backend m2o field ?
+ */
+registry.many2one = SnippetOption.extend({
+ xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
+ /**
+ * @override
+ */
+ start: function () {
+ var self = this;
+
+ this.Model = this.$target.data('oe-many2one-model');
+ this.ID = +this.$target.data('oe-many2one-id');
+
+ // create search button and bind search bar
+ this.$btn = $(qweb.render('web_editor.many2one.button'))
+ .insertAfter(this.$overlay.find('.oe_options'));
+
+ this.$ul = this.$btn.find('ul');
+ this.$search = this.$ul.find('li:first');
+ this.$search.find('input').on('mousedown click mouseup keyup keydown', function (e) {
+ e.stopPropagation();
+ });
+
+ // move menu item
+ setTimeout(function () {
+ if (self.$overlay.find('.oe_options').hasClass('hidden')) {
+ self.$btn.css('height', '0').find('> a').addClass('hidden');
+ self.$ul.show().css({
+ 'top': '-24px', 'margin': '0', 'padding': '2px 0', 'position': 'relative'
+ });
+ } else {
+ self.$btn.find('a').on('click', function (e) {
+ self._clear();
});
}
- },
- clean: function () {
- this.$target.removeClass('o_bg_img_opt_contain o_bg_img_opt_custom o_bg_img_opt_repeat o_bg_img_opt_repeat_x o_bg_img_opt_repeat_y')
- .css({
- 'background-size': '',
- 'background-position': '',
- });
- },
- });
-
- /* t-field options */
+ },0);
- registry.many2one = SnippetOption.extend({
- start: function () {
- var self = this;
-
- this.Model = this.$target.data('oe-many2one-model');
- this.ID = +this.$target.data('oe-many2one-id');
-
- // create search button and bind search bar
- this.$btn = $(qweb.render("web_editor.many2one.button"))
- .insertAfter(this.$overlay.find('.oe_options'));
-
- this.$ul = this.$btn.find("ul");
- this.$search = this.$ul.find('li:first');
- this.$search.find('input').on('mousedown click mouseup keyup keydown', function (e) {
- e.stopPropagation();
+ // bind search input
+ this.$search.find('input')
+ .focus()
+ .on('keyup', function (e) {
+ self._findExisting($(this).val());
});
- // move menu item
- setTimeout(function () {
- if (self.$overlay.find('.oe_options').hasClass('hidden')) {
- self.$btn.css('height', '0').find('> a').addClass('hidden');
- self.$ul.show().css({
- 'top': '-24px', 'margin': '0', 'padding': '2px 0', 'position': 'relative'
- });
- } else {
- self.$btn.find('a').on('click', function (e) {
- self.clear();
- });
- }
- },0);
+ // bind result
+ this.$ul.on('click', 'li:not(:first) a', function (e) {
+ self._selectRecord($(e.currentTarget));
+ });
- // bind search input
- this.$search.find('input')
- .focus()
- .on('keyup', function (e) {
- self.find_existing($(this).val());
- });
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ onFocus: function () {
+ this.$target.attr('contentEditable', 'false');
+ this._clear();
+ },
- // bind result
- this.$ul.on('click', "li:not(:first) a", function (e) {
- self.select_record(this);
- });
- },
-
- on_focus: function () {
- this.$target.attr('contentEditable', 'false');
- this.clear();
- this._super.apply(this, arguments);
- },
-
- clear: function () {
- var self = this;
- this.$search.siblings().remove();
- self.$search.find('input').val("");
- setTimeout(function () {
- self.$search.find('input').focus();
- },0);
- },
-
- find_existing: function (name) {
- var self = this;
- var domain = [];
- if (!name || !name.length) {
- self.$search.siblings().remove();
- return;
- }
- if (isNaN(+name)) {
- if (this.Model !== "res.partner") {
- domain.push(['name', 'ilike', name]);
- } else {
- domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]);
- }
- } else {
- domain.push(['id', '=', name]);
- }
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- ajax.jsonRpc('/web/dataset/call_kw', 'call', {
- model: this.Model,
- method: 'search_read',
- args: [domain, this.Model === "res.partner" ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']],
- kwargs: {
- order: 'name DESC',
- limit: 5,
- context: base.get_context(),
- }
- }).then(function (result) {
- self.$search.siblings().remove();
- self.$search.after(qweb.render("web_editor.many2one.search",{contacts:result}));
- });
- },
-
- get_contact_rendering: function (options) {
- return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
- model: 'ir.qweb.field.contact',
- method: 'get_record_to_html',
- args: [[this.ID]],
- kwargs: {
- options: options,
- context: base.get_context(),
- }
- });
- },
-
- select_record: function (li) {
- var self = this;
-
- this.ID = +$(li).data("id");
- this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID);
-
- this.buildingBlock.getParent().rte.historyRecordUndo(this.$target);
- this.$target.trigger('content_changed');
-
- if (self.$target.data('oe-type') === "contact") {
- $('[data-oe-contact-options]')
- .filter('[data-oe-model="'+self.$target.data('oe-model')+'"]')
- .filter('[data-oe-id="'+self.$target.data('oe-id')+'"]')
- .filter('[data-oe-field="'+self.$target.data('oe-field')+'"]')
- .filter('[data-oe-contact-options!="'+self.$target.data('oe-contact-options')+'"]')
- .add(self.$target)
- .attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID)
- .each(function () {
- var $node = $(this);
- self.get_contact_rendering($node.data('oe-contact-options'))
- .then(function (html) {
- $node.html(html);
- });
- });
+ /**
+ * Removes the input value and suggestions.
+ *
+ * @private
+ */
+ _clear: function () {
+ var self = this;
+ this.$search.siblings().remove();
+ self.$search.find('input').val('');
+ setTimeout(function () {
+ self.$search.find('input').focus();
+ }, 0);
+ },
+ /**
+ * Find existing record with the given name and suggest them.
+ *
+ * @private
+ * @param {string} name
+ * @returns {Deferred}
+ */
+ _findExisting: function (name) {
+ var self = this;
+ var domain = [];
+ if (!name || !name.length) {
+ self.$search.siblings().remove();
+ return;
+ }
+ if (isNaN(+name)) {
+ if (this.Model !== 'res.partner') {
+ domain.push(['name', 'ilike', name]);
} else {
- self.$target.html($(li).data("name"));
+ domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]);
}
+ } else {
+ domain.push(['id', '=', name]);
+ }
- setTimeout(function () {
- self.buildingBlock.make_active(false);
- },0);
+ return this._rpc({
+ model: this.Model,
+ method: 'search_read',
+ args: [domain, this.Model === 'res.partner' ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']],
+ kwargs: {
+ order: 'name DESC',
+ limit: 5,
+ context: weContext.get(),
+ },
+ }).then(function (result) {
+ self.$search.siblings().remove();
+ self.$search.after(qweb.render('web_editor.many2one.search',{contacts:result}));
+ });
+ },
+ /**
+ * Selects the given suggestion and displays it the proper way.
+ *
+ * @private
+ * @param {jQuery} $li
+ */
+ _selectRecord: function ($li) {
+ var self = this;
+
+ this.ID = +$li.data('id');
+ this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID);
+
+ this.trigger_up('request_history_undo_record', {$target: this.$target});
+ this.$target.trigger('content_changed');
+
+ if (self.$target.data('oe-type') === 'contact') {
+ $('[data-oe-contact-options]')
+ .filter('[data-oe-model="'+self.$target.data('oe-model')+'"]')
+ .filter('[data-oe-id="'+self.$target.data('oe-id')+'"]')
+ .filter('[data-oe-field="'+self.$target.data('oe-field')+'"]')
+ .filter('[data-oe-contact-options!="'+self.$target.data('oe-contact-options')+'"]')
+ .add(self.$target)
+ .attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID)
+ .each(function () {
+ var $node = $(this);
+ var options = $node.data('oe-contact-options');
+ self._rpc({
+ model: 'ir.qweb.field.contact',
+ method: 'get_record_to_html',
+ args: [[self.ID]],
+ kwargs: {
+ options: options,
+ context: weContext.get(),
+ },
+ }).then(function (html) {
+ $node.html(html);
+ });
+ });
+ } else {
+ self.$target.html($li.data('name'));
}
- });
- return {
- Class: SnippetOption,
- registry: registry,
- };
+ _.defer(function () {
+ self.trigger_up('deactivate_snippet');
+ });
+ }
+});
+
+return {
+ Class: SnippetOption,
+ registry: registry,
+};
});
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
index c2e2ea54dca..4860931ea6b 100644
--- a/addons/web_editor/static/src/js/editor/summernote.js
+++ b/addons/web_editor/static/src/js/editor/summernote.js
@@ -6,25 +6,21 @@ require('summernote/summernote'); // wait that summernote is loaded
var _t = core._t;
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* Summernote Lib (neek hack to make accessible: method and object) */
-
-var agent = $.summernote.core.agent;
+// Summernote Lib (neek hack to make accessible: method and object)
+// var agent = $.summernote.core.agent;
var dom = $.summernote.core.dom;
var range = $.summernote.core.range;
var list = $.summernote.core.list;
var key = $.summernote.core.key;
var eventHandler = $.summernote.eventHandler;
-var editor = eventHandler.modules.editor;
+// var editor = eventHandler.modules.editor;
var renderer = $.summernote.renderer;
var options = $.summernote.options;
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* Add method to Summernote*/
-
+// Add methods to summernote
dom.hasContentAfter = function (node) {
var next;
- if(dom.isEditable(node)) return;
+ if (dom.isEditable(node)) return;
while (node.nextSibling) {
next = node.nextSibling;
if (next.tagName || dom.isVisibleText(next) || dom.isBR(next)) return next;
@@ -33,7 +29,7 @@ dom.hasContentAfter = function (node) {
};
dom.hasContentBefore = function (node) {
var prev;
- if(dom.isEditable(node)) return;
+ if (dom.isEditable(node)) return;
while (node.previousSibling) {
prev = node.previousSibling;
if (prev.tagName || dom.isVisibleText(prev) || dom.isBR(prev)) return prev;
@@ -97,12 +93,12 @@ dom.isEqual = function (prev, cur) {
}
var att, att2;
loop_prev:
- for(var a in prev.attributes) {
+ for (var a in prev.attributes) {
att = prev.attributes[a];
- for(var b in cur.attributes) {
+ for (var b in cur.attributes) {
att2 = cur.attributes[b];
if (att.name === att2.name) {
- if (strip(att.value) != strip(att2.value)) return false;
+ if (strip(att.value) !== strip(att2.value)) return false;
continue loop_prev;
}
}
@@ -110,9 +106,6 @@ dom.isEqual = function (prev, cur) {
}
return true;
};
-/**
- * Checks that the node only has a @style, not e.g. @class or whatever
- */
dom.hasOnlyStyle = function (node) {
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
@@ -199,7 +192,7 @@ dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
if (!begin) {
begin = node;
- while(begin.firstChild) {begin = begin.firstChild;}
+ while (begin.firstChild) {begin = begin.firstChild;}
so = 0;
} else if (begin.tagName && begin.childNodes[so]) {
begin = begin.childNodes[so];
@@ -207,10 +200,8 @@ dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
}
if (!end) {
end = node;
- while(end.lastChild) {end = end.lastChild;}
+ while (end.lastChild) {end = end.lastChild;}
eo = end.textContent.length-1;
- } else if (end.tagName) {
-
} else if (end.tagName && end.childNodes[so]) {
end = end.childNodes[so];
so = 0;
@@ -225,7 +216,7 @@ dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
eo = 0;
}
- function __merge (node) {
+ function __merge(node) {
var merged = false;
var prev;
for (var k=0; k<node.childNodes.length; k++) {
@@ -330,7 +321,8 @@ dom.autoMerge = function (target, previous) {
while (node) {
nodes.push(node);
- if (temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node))) {
+ temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node));
+ if (temp) {
if (!dom.isText(node) && !dom.isMergable(node) && temp.tagName !== node.tagName) {
nodes = [];
}
@@ -369,7 +361,7 @@ dom.removeSpace = function (node, begin, so, end, eo) {
};
}
- (function __remove_space (node) {
+ (function __remove_space(node) {
if (!node) return;
var t_begin, t_end;
for (var k=0; k<node.childNodes.length; k++) {
@@ -389,14 +381,14 @@ dom.removeSpace = function (node, begin, so, end, eo) {
removed = true;
if (cur === begin) {
t_begin = dom.hasContentBefore(dom.ancestorHavePreviousSibling(cur));
- if(t_begin) {
+ if (t_begin) {
so = 0;
begin = dom.lastChild(t_begin);
}
}
if (cur === end) {
t_end = dom.hasContentAfter(dom.ancestorHaveNextSibling(cur));
- if(t_end) {
+ if (t_end) {
eo = 1;
end = dom.firstChild(t_end);
if (dom.isText(end)) {
@@ -414,19 +406,21 @@ dom.removeSpace = function (node, begin, so, end, eo) {
// convert HTML space
if (dom.isText(cur)) {
var text;
+ var temp;
+ var _temp;
var exp1 = /[\t\n\r ]+/g;
var exp2 = /(?!([ ]|\u00A0)|^)\u00A0(?!([ ]|\u00A0)|$)/g;
if (cur === begin) {
- var temp = cur.textContent.substr(0, so);
- var _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
+ temp = cur.textContent.substr(0, so);
+ _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
so -= temp.length - _temp.length;
}
if (cur === end) {
- var temp = cur.textContent.substr(0, eo);
- var _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
+ temp = cur.textContent.substr(0, eo);
+ _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
eo -= temp.length - _temp.length;
}
- var text = cur.textContent.replace(exp1, ' ').replace(exp2, ' ');
+ text = cur.textContent.replace(exp1, ' ').replace(exp2, ' ');
removed = removed || cur.textContent.length !== text.length;
cur.textContent = text;
}
@@ -441,10 +435,8 @@ dom.removeSpace = function (node, begin, so, end, eo) {
eo: dom.isBR(end) ? 0 : eo
};
};
-dom.node = function (node) {
- return node.tagName ? node : node.parentNode;
-};
dom.removeBetween = function (sc, so, ec, eo, towrite) {
+ var text;
if (ec.tagName) {
if (ec.childNodes[eo]) {
ec = ec.childNodes[eo];
@@ -529,22 +521,19 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) {
so = 0;
$(ancestor).prepend(sc);
} else if (dom.isText(sc)) {
- var text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
+ text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
so = Math.min(so, text.length);
sc.textContent = text;
}
-
} else {
-
- var text = ancestor.textContent;
+ text = ancestor.textContent;
ancestor.textContent = text.slice(0, so) + text.slice(eo, Infinity).replace(/^[ \t\n\r]+/, '\u00A0');
-
}
eo = so;
- if(!dom.isBR(sc) && !dom.isVisibleText(sc) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
+ if (!dom.isBR(sc) && !dom.isVisibleText(sc) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
ancestor = dom.node(sc);
- var text = document.createTextNode('\u00A0');
+ text = document.createTextNode('\u00A0');
$(sc).before(text);
sc = text;
so = 0;
@@ -570,7 +559,7 @@ dom.outdent = function (node) {
return margin;
};
dom.scrollIntoViewIfNeeded = function (node) {
- var node = dom.node(node);
+ node = dom.node(node);
var $span;
if (dom.isBR(node)) {
@@ -593,7 +582,7 @@ dom.scrollIntoViewIfNeeded = function (node) {
// get if a parent have a scrollbar
parent = node.parentNode;
- while (parent != offsetParent &&
+ while (parent !== offsetParent &&
(parent.tagName === "BODY" || ["auto", "scroll"].indexOf(dom.getComputedStyle(parent).overflowY) === -1)) {
parent = parent.parentNode;
}
@@ -659,14 +648,9 @@ dom.isNotBreakable = function (node) {
// avoid triple click => crappy dom
return !dom.isText(node) && !dom.isBR(dom.firstChild(node)) && dom.isVoid(dom.firstChild(node));
};
-
dom.isContentEditable = function (node) {
return $(node).closest('[contenteditable]').is('[contenteditable="true"]');
};
-
-dom.isPre = function (node) {
- return node.tagName === "PRE";
-};
dom.isFont = function (node) {
var nodeName = node && node.nodeName.toUpperCase();
return node && (nodeName === "FONT" ||
@@ -678,19 +662,10 @@ dom.isFont = function (node) {
dom.isVisibleText = function (textNode) {
return !!textNode.textContent.match(/\S|\u00A0/);
};
-
-/* avoid thinking HTML comment are visible */
var old_isVisiblePoint = dom.isVisiblePoint;
dom.isVisiblePoint = function (point) {
return point.node.nodeType !== 8 && old_isVisiblePoint.apply(this, arguments);
};
-
-/**
- * order the style of the node to compare 2 nodes and remove attribute if empty
- *
- * @param {Node} node
- * @return {String} className
- */
dom.orderStyle = function (node) {
var style = node.getAttribute('style');
if (!style) return null;
@@ -705,12 +680,6 @@ dom.orderStyle = function (node) {
node.setAttribute('style', style);
return style;
};
-/**
- * order the class of the node to compare 2 nodes and remove attribute if empty
- *
- * @param {Node} node
- * @return {String} className
- */
dom.orderClass = function (node) {
var className = node.getAttribute && node.getAttribute('class');
if (!className) return null;
@@ -725,21 +694,9 @@ dom.orderClass = function (node) {
node.setAttribute('class', className);
return className;
};
-/**
- * return the current node (if text node, return parentNode)
- *
- * @param {Node} node
- * @return {Node} node
- */
dom.node = function (node) {
return dom.isText(node) ? node.parentNode : node;
};
-/**
- * move all childNode to an other node
- *
- * @param {Node} from
- * @param {Node} to
- */
dom.moveContent = function (from, to) {
if (from === to) {
return;
@@ -749,18 +706,16 @@ dom.moveContent = function (from, to) {
dom.insertAfter(from.lastChild, from);
}
} else {
- while (from.firstChild && from.firstChild != to) {
+ while (from.firstChild && from.firstChild !== to) {
to.appendChild(from.firstChild);
}
}
};
-
dom.getComputedStyle = function (node) {
- return node.nodeType == Node.COMMENT_NODE ? {} : window.getComputedStyle(node);
+ return node.nodeType === Node.COMMENT_NODE ? {} : window.getComputedStyle(node);
};
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) {
var sc = this.sc;
@@ -894,7 +849,7 @@ range.WrappedRange.prototype.isContentEditable = function () {
return dom.isContentEditable(this.sc) && (this.sc === this.ec || dom.isContentEditable(this.ec));
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
renderer.tplButtonInfo.fontsize = function (lang, options) {
var items = options.fontSizes.reduce(function (memo, v) {
@@ -910,8 +865,7 @@ renderer.tplButtonInfo.fontsize = function (lang, options) {
});
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* add some text commands */
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
key.nameFromCode[46] = 'DELETE';
key.nameFromCode[27] = 'ESCAPE';
@@ -960,7 +914,7 @@ $.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
var $editable = layoutInfo.editable();
$editable.data('NoteHistory').recordUndo($editable, 'tab');
var r = range.create();
- var outdent = outdent || false;
+ outdent = outdent || false;
event.preventDefault();
if (r && (dom.ancestor(r.sc, dom.isCell) || dom.ancestor(r.ec, dom.isCell))) {
@@ -986,11 +940,12 @@ $.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
$.summernote.pluginEvents.indent(event, editor, layoutInfo);
}
} else {
+ var next;
if (!outdent) {
- if(dom.isText(r.sc)) {
- var next = r.sc.splitText(r.so);
+ if (dom.isText(r.sc)) {
+ next = r.sc.splitText(r.so);
} else {
- var next = document.createTextNode('')
+ next = document.createTextNode('');
$(r.sc.childNodes[r.so]).before(next);
}
editor.typing.insertTab($editable, r, options.tabsize);
@@ -1000,7 +955,7 @@ $.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
} else {
r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
r = range.create(r.sc, r.so, r.ec, r.eo);
- var next = r.sc.splitText(r.so);
+ next = r.sc.splitText(r.so);
r.sc.textContent = r.sc.textContent.replace(/(\u00A0)+$/g, '');
next.textContent = next.textContent.replace(/^(\u00A0)+/g, '');
range.create(r.sc, r.sc.textContent.length, r.sc, r.sc.textContent.length).select();
@@ -1062,27 +1017,30 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
var br = $("<br/>")[0];
+ var node;
+ var $node;
+ var $clone;
var contentBefore = r.sc.textContent.slice(0,r.so).match(/\S|\u00A0/);
if (!contentBefore && dom.isText(r.sc)) {
- var node = r.sc.previousSibling;
+ node = r.sc.previousSibling;
while (!contentBefore && node && dom.isText(node)) {
contentBefore = dom.isVisibleText(node);
node = node.previousSibling;
}
}
- var node = dom.node(r.sc);
+ node = dom.node(r.sc);
var exist = r.sc.childNodes[r.so] || r.sc;
exist = dom.isVisibleText(exist) || dom.isBR(exist) ? exist : dom.hasContentAfter(exist) || (dom.hasContentBefore(exist) || exist);
// table: add a tr
var td = dom.ancestor(node, dom.isCell);
if (td && !dom.nextElementSibling(node) && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode) && (!dom.isText(r.sc) || !r.sc.textContent.slice(r.so).match(/\S|\u00A0/))) {
- var $node = $(td.parentNode);
- var $clone = $node.clone();
+ $node = $(td.parentNode);
+ $clone = $node.clone();
$clone.children().html(dom.blank);
$node.after($clone);
- var node = dom.firstElementChild($clone[0]) || $clone[0];
+ node = dom.firstElementChild($clone[0]) || $clone[0];
range.create(node, 0, node, 0).select();
dom.scrollIntoViewIfNeeded(br);
event.preventDefault();
@@ -1125,8 +1083,8 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
if (dom.isBR(last)) {
last = last.parentNode;
}
- var $node = $(last);
- var $clone = $node.clone().text("");
+ $node = $(last);
+ $clone = $node.clone().text("");
$node.after($clone);
node = dom.node(dom.firstElementChild($clone[0]) || $clone[0]);
$(node).html(br);
@@ -1197,22 +1155,7 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
return true;
};
-function summernote_keydown_clean (field) {
- setTimeout(function () {
- var r = range.create();
- if (!r) return;
- var node = r[field];
- while (dom.isText(node)) {node = node.parentNode;}
- node = node.parentNode;
- var data = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
- data = dom.removeSpace(node.parentNode, data.sc, data.so, data.sc, data.so);
-
- range.create(data.sc, data.so, data.sc, data.so).select();
- },0);
-}
-
function remove_table_content(r) {
- var ancestor = r.commonAncestor();
var nodes = dom.listBetween(r.sc, r.ec);
if (dom.isText(r.sc)) {
r.sc.textContent = r.sc.textContent.slice(0, r.so);
@@ -1226,12 +1169,12 @@ function remove_table_content(r) {
continue;
} else if (dom.isCell(node)) {
$(node).html("<br/>");
- } else if(node.parentNode) {
+ } else if (node.parentNode) {
do {
var parent = node.parentNode;
parent.removeChild(node);
node = parent;
- } while(!dom.isVisibleText(node) && !dom.firstElementChild(node) &&
+ } while (!dom.isVisibleText(node) && !dom.firstElementChild(node) &&
!dom.isCell(node) &&
node.parentNode && !$(node.parentNode).hasClass('o_editable'));
}
@@ -1282,12 +1225,12 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
var content = target.textContent.replace(/[ \t\r\n]+$/, '');
var temp;
var temp2;
+ var next;
// media
if (dom.isImg(node) || (!contentAfter && dom.isImg(dom.hasContentAfter(node)))) {
var parent;
var index;
- var r;
if (!dom.isImg(node)) {
node = dom.hasContentAfter(node);
}
@@ -1295,7 +1238,7 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
parent = node.parentNode;
index = dom.position(node);
if (index>0) {
- var next = node.previousSibling;
+ next = node.previousSibling;
r = range.create(next, next.textContent.length);
} else {
r = range.create(parent, 0);
@@ -1315,7 +1258,7 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
return false;
}
var before = false;
- var next = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
+ next = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
if (!dom.isContentEditable(next)) {
before = true;
next = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
@@ -1343,9 +1286,9 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
!dom.isNotBreakable(temp) &&
!dom.isNotBreakable(temp2)) {
dom.autoMerge(target, false);
- var next = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(target)));
+ next = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(target)));
if (dom.isBR(next)) {
- if(dom.position(next) == 0) {
+ if (dom.position(next) === 0) {
range.create(next.parentNode, 0).select();
}
else {
@@ -1426,6 +1369,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
var content = target.textContent.replace(/[ \t\r\n]+$/, '');
var temp;
var temp2;
+ var prev;
// delete media
if (dom.isImg(node) || (!contentBefore && dom.isImg(dom.hasContentBefore(node)))) {
@@ -1450,7 +1394,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
node = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(tr)) || $editable.get(0));
}
$(tr).empty();
- if(!$(tr).closest('table').has('td, th').length) {
+ if (!$(tr).closest('table').has('td, th').length) {
$(tr).closest('table').remove();
}
$(tr).remove();
@@ -1472,7 +1416,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
return false;
}
var before = true;
- var prev = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
+ prev = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
if (!dom.isContentEditable(prev)) {
before = false;
prev = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
@@ -1499,7 +1443,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
(temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
!dom.isNotBreakable(temp) &&
!dom.isNotBreakable(temp2)) {
- var prev = dom.firstChild(target);
+ prev = dom.firstChild(target);
dom.autoMerge(target, true);
range.create(prev, 0).select();
}
@@ -1521,7 +1465,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
}
}
- var r = range.create();
+ r = range.create();
if (r) {
$(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
dom.scrollIntoViewIfNeeded(r.sc.parentNode.previousElementSibling || r.sc);
@@ -1531,9 +1475,6 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
return false;
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* add list command (create a uggly dom for chrome) */
-
function isFormatNode(node) {
return node.tagName && options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
}
@@ -1603,13 +1544,13 @@ $.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutI
return;
}
- var parent = p0.parentNode;
- var ul = document.createElement(sorted ? "ol" : "ul");
+ parent = p0.parentNode;
+ ul = document.createElement(sorted ? "ol" : "ul");
parent.insertBefore(ul, p0);
var childNodes = parent.childNodes;
var brs = [];
var begin = false;
- for (var i=0; i<childNodes.length; i++) {
+ for (i = 0; i < childNodes.length; i++) {
if (begin && dom.isBR(childNodes[i])) {
parent.removeChild(childNodes[i]);
i--;
@@ -1631,7 +1572,7 @@ $.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutI
parent.removeChild(childNodes[i]);
}
- for (var i=0; i<brs.length; i++) {
+ for (i = 0; i < brs.length ; i++) {
parent.removeChild(brs[i]);
}
r.clean().select();
@@ -1648,11 +1589,10 @@ $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent)
if (!r) return;
var flag = false;
- function indentUL (UL, start, end) {
+ function indentUL(UL, start, end) {
var next;
var tagName = UL.tagName;
var node = UL.firstChild;
- var parent = UL.parentNode;
var ul = document.createElement(tagName);
var li = document.createElement("li");
li.style.listStyle = "none";
@@ -1686,15 +1626,14 @@ $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent)
li = prev;
li.parentNode.removeChild(dom.nextElementSibling(li));
}
- var next = dom.nextElementSibling(li);
+ next = dom.nextElementSibling(li);
if (next && next.tagName === "LI" && (temp = dom.firstElementChild(next)) && temp.tagName === tagName && (dom.firstElementChild(li) !== dom.firstElementChild(next))) {
dom.doMerge(dom.firstElementChild(li), dom.firstElementChild(next));
li.parentNode.removeChild(dom.nextElementSibling(li));
}
}
- function outdenttUL (UL, start, end) {
+ function outdenttUL(UL, start, end) {
var next;
- var tagName = UL.tagName;
var node = UL.firstChild;
var parent = UL.parentNode;
var li = UL.parentNode.tagName === "LI" ? UL.parentNode : UL;
@@ -1733,7 +1672,7 @@ $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent)
dom.merge(parent, start, 0, end, 1, null, true);
}
- function indentOther (p, start, end) {
+ function indentOther(p, start, end) {
if (p === start || $.contains(p, start) || $.contains(start, p)) {
flag = true;
}
@@ -1815,8 +1754,6 @@ $.summernote.pluginEvents.outdent = function (event, editor, layoutInfo) {
return $.summernote.pluginEvents.indent(event, editor, layoutInfo, true);
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
$.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTagName) {
var $editable = layoutInfo.editable();
$editable.data('NoteHistory').recordUndo($editable);
@@ -1858,11 +1795,11 @@ $.summernote.pluginEvents.removeFormat = function (event, editor, layoutInfo, va
var $editable = layoutInfo.editable();
$editable.data('NoteHistory').recordUndo($editable);
var r = range.create();
- if(!r) return;
+ if (!r) return;
var node = range.create().sc.parentNode;
document.execCommand('removeFormat');
document.execCommand('removeFormat');
- var r = range.create();
+ r = range.create();
if (!r) return;
r = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
range.create(r.sc, r.so, r.ec, r.eo).select();
@@ -1875,15 +1812,16 @@ eventHandler.modules.toolbar.button.updateRecentColor = function (elBtn, sEvent,
var $recentcolor = $.find('.note-recent-color i');
var $recentcolorbtn = $.find('.note-recent-color');
- //find last used color for fonts or for icons
+ //find last used color for fonts or for icons
//set this color into recentcolor button of both font and icon
for (var i in $recentcolor) {
var $font = $recentcolor[i];
var $button = $($recentcolorbtn[i]);
var className = $font.className.split(/\s+/);
+ var k;
if (sEvent === "foreColor") {
//set class for forecolor to recentcolor button font-icon
- for (var k=2; k<className.length; k++) {
+ for (k=2; k<className.length; k++) {
if (className[k].length && className[k].slice(0,5) === "text-") {
className.splice(k,1);
k--;
@@ -1898,7 +1836,7 @@ eventHandler.modules.toolbar.button.updateRecentColor = function (elBtn, sEvent,
}
} else {
//set class for backcolor to recentcolor button font-icon
- for (var k=2; k<className.length; k++) {
+ for (k=2; k<className.length; k++) {
if (className[k].length && className[k].slice(0,3) === "bg-") {
className.splice(k,1);
k--;
@@ -1925,17 +1863,17 @@ eventHandler.modules.toolbar.button.updateRecentColor = function (elBtn, sEvent,
$(document).on('click keyup', function () {
var $popover = $((range.create()||{}).sc).closest('[contenteditable]');
var popover_history = ($popover.data()||{}).NoteHistory;
- if(!popover_history || popover_history == history) return;
+ if (!popover_history || popover_history === history) return;
var editor = $popover.parent('.note-editor');
$('button[data-event="undo"]', editor).attr('disabled', !popover_history.hasUndo());
$('button[data-event="redo"]', editor).attr('disabled', !popover_history.hasRedo());
});
eventHandler.modules.editor.undo = function ($popover) {
- if(!$popover.attr('disabled')) $popover.data('NoteHistory').undo();
+ if (!$popover.attr('disabled')) $popover.data('NoteHistory').undo();
};
eventHandler.modules.editor.redo = function ($popover) {
- if(!$popover.attr('disabled')) $popover.data('NoteHistory').redo();
+ if (!$popover.attr('disabled')) $popover.data('NoteHistory').redo();
};
// use image toolbar if current range is on image
@@ -1943,19 +1881,19 @@ var fn_editor_currentstyle = eventHandler.modules.editor.currentStyle;
eventHandler.modules.editor.currentStyle = function (target) {
var styleInfo = fn_editor_currentstyle.apply(this, arguments);
// with our changes for inline editor, the targeted element could be a button of the editor
- if(!styleInfo.image || !dom.isEditable(styleInfo.image)) {
+ if (!styleInfo.image || !dom.isEditable(styleInfo.image)) {
styleInfo.image = undefined;
var r = range.create();
- if(r)
+ if (r)
styleInfo.image = r.isOnImg();
}
return styleInfo;
-}
+};
options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62];
$.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) {
var r = range.create();
- if(!r) return;
+ if (!r) return;
var startPoint = r.getStartPoint();
var endPoint = r.getEndPoint();
@@ -1978,13 +1916,15 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
// get first and last point
- if (endPoint.offset && endPoint.offset != dom.nodeLength(endPoint.node)) {
- var ancestor = dom.ancestor(endPoint.node, dom.isFont) || endPoint.node;
+ var ancestor;
+ var node;
+ if (endPoint.offset && endPoint.offset !== dom.nodeLength(endPoint.node)) {
+ ancestor = dom.ancestor(endPoint.node, dom.isFont) || endPoint.node;
dom.splitTree(ancestor, endPoint);
}
- if (startPoint.offset && startPoint.offset != dom.nodeLength(startPoint.node)) {
- var ancestor = dom.ancestor(startPoint.node, dom.isFont) || startPoint.node;
- var node = dom.splitTree(ancestor, startPoint);
+ if (startPoint.offset && startPoint.offset !== dom.nodeLength(startPoint.node)) {
+ ancestor = dom.ancestor(startPoint.node, dom.isFont) || startPoint.node;
+ node = dom.splitTree(ancestor, startPoint);
if (endPoint.node === startPoint.node) {
endPoint.node = node;
endPoint.offset = dom.nodeLength(node);
@@ -1999,7 +1939,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
var node = point.node;
if (((dom.isText(node) && dom.isVisibleText(node)) ||
(dom.isFont(node) && !dom.isVisibleText(node))) &&
- (node != endPoint.node || endPoint.offset)) {
+ (node !== endPoint.node || endPoint.offset)) {
nodes.push(point.node);
@@ -2013,9 +1953,10 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
// apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
- var node, font, $font, fonts = [], className;
+ var font, $font, fonts = [], className;
+ var i;
if (color || bgcolor || size) {
- for (var i=0; i<nodes.length; i++) {
+ for (i=0; i<nodes.length; i++) {
node = nodes[i];
font = dom.ancestor(node, dom.isFont);
@@ -2033,8 +1974,9 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
className = font.className.split(/\s+/);
+ var k;
if (color) {
- for (var k=0; k<className.length; k++) {
+ for (k=0; k<className.length; k++) {
if (className[k].length && className[k].slice(0,5) === "text-") {
className.splice(k,1);
k--;
@@ -2050,7 +1992,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
}
if (bgcolor) {
- for (var k=0; k<className.length; k++) {
+ for (k=0; k<className.length; k++) {
if (className[k].length && className[k].slice(0,3) === "bg-") {
className.splice(k,1);
k--;
@@ -2077,7 +2019,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
// remove empty values
// we must remove the value in 2 steps (applay inherit then remove) because some
// browser like chrome have some time an error for the rendering and/or keep inherit
- for (var i=0; i<fonts.length; i++) {
+ for (i=0; i<fonts.length; i++) {
font = fonts[i];
if (font.style.backgroundColor === "inherit") {
font.style.backgroundColor = "";
@@ -2100,13 +2042,13 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
// select nodes to clean (to remove empty font and merge same nodes)
- var nodes = [];
+ nodes = [];
dom.walkPoint(startPoint, endPoint, function (point) {
nodes.push(point.node);
});
nodes = list.unique(nodes);
- function remove (node, to) {
+ function remove(node, to) {
if (node === endPoint.node) {
endPoint = dom.prevPoint(endPoint);
}
@@ -2118,8 +2060,8 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
// remove node without attributes (move content), and merge the same nodes
if (!dom.isImgFont(node)) {
- var className2, style, style2, $prev;
- for (var i=0; i<nodes.length; i++) {
+ var className2, style, style2;
+ for (i=0; i<nodes.length; i++) {
node = nodes[i];
if ((dom.isText(node) || dom.isBR(node)) && !dom.isVisibleText(node)) {
@@ -2150,7 +2092,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
if (i>0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) {
className2 = font.getAttribute('class');
style2 = font.getAttribute('style');
- if (node !== font && className == className2 && style == style2) {
+ if (node !== font && className === className2 && style === style2) {
remove(node, font);
nodes.splice(i,1);
i--;
@@ -2169,7 +2111,6 @@ $.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value)
editor.afterCommand($editable);
};
$.summernote.pluginEvents.color = function (event, editor, layoutInfo, sObjColor) {
- var $editable = layoutInfo.editable();
var oColor = JSON.parse(sObjColor);
var foreColor = oColor.foreColor, backColor = oColor.backColor;
@@ -2200,8 +2141,6 @@ $.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backC
editor.afterCommand($editable);
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
options.onCreateLink = function (sLinkUrl) {
if (sLinkUrl.indexOf('mailto:') === 0) {
// pass
@@ -2213,16 +2152,13 @@ options.onCreateLink = function (sLinkUrl) {
return sLinkUrl;
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-/* table */
-
-function summernote_table_scroll (event) {
+function summernote_table_scroll(event) {
var r = range.create();
if (r && r.isOnCell()) {
$('.o_table_handler').remove();
}
}
-function summernote_table_update (oStyle) {
+function summernote_table_update(oStyle) {
var r = range.create();
if (!oStyle.range || !r || !r.isOnCell() || !r.isOnCellFirst()) {
$('.o_table_handler').remove();
@@ -2334,15 +2270,15 @@ function summernote_table_update (oStyle) {
var fn_popover_update = eventHandler.modules.popover.update;
eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
fn_popover_update.call(this, $popover, oStyle, isAirMode);
- if(!!(isAirMode ? $popover : $popover.parent()).find('.note-table').length) {
+ if ((isAirMode ? $popover : $popover.parent()).find('.note-table').length) {
summernote_table_update(oStyle);
}
};
// override summernote clipboard functionality
-eventHandler.modules.clipboard.attach = function(layoutInfo) {
+eventHandler.modules.clipboard.attach = function (layoutInfo) {
var $editable = layoutInfo.editable();
- $editable.on('paste', function(e) {
+ $editable.on('paste', function (e) {
var clipboardData = ((e.originalEvent || e).clipboardData || window.clipboardData);
// Change nothing if pasting html (copy from text editor / web / ...) or
// if clipboardData is not available (IE / ...)
@@ -2359,8 +2295,6 @@ eventHandler.modules.clipboard.attach = function(layoutInfo) {
});
};
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
var fn_attach = eventHandler.attach;
eventHandler.attach = function (oLayoutInfo, options) {
var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
@@ -2379,5 +2313,4 @@ options.icons.image.image = "file-image-o";
$.summernote.lang['en-US'].image.image = "File / Image";
return $.summernote;
-
});
diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js
index 6b42e028b3a..11d23bf603b 100644
--- a/addons/web_editor/static/src/js/editor/transcoder.js
+++ b/addons/web_editor/static/src/js/editor/transcoder.js
@@ -4,39 +4,49 @@ odoo.define('web_editor.transcoder', function (require) {
var widget = require('web_editor.widget');
var rulesCache = [];
-var getMatchedCSSRules = function (a) {
+
+/**
+ * Returns the css rules which applies on an element, tweaked so that they are
+ * browser/mail client ok.
+ *
+ * @param {DOMElement} a
+ * @returns {Object} css property name -> css property value
+ */
+function getMatchedCSSRules(a) {
+ var i, r, k;
if (!rulesCache.length) {
var sheets = document.styleSheets;
- for(var i = sheets.length-1; i >= 0 ; i--) {
+ for (i = sheets.length-1 ; i >= 0 ; i--) {
var rules;
if (sheets[i].rules) {
rules = sheets[i].rules;
} else {
- //try...catch because Firefox not able to enumerate document.styleSheets[].cssRules[] for cross-domain stylesheets.
+ // try...catch because Firefox not able to enumerate
+ // document.styleSheets[].cssRules[] for cross-domain sheets.
try {
rules = sheets[i].cssRules;
- } catch(e) {
+ } catch (e) {
console.warn("Can't read the css rules of: " + sheets[i].href, e);
continue;
}
rules = sheets[i].cssRules;
}
if (rules) {
- for(var r = rules.length-1; r >= 0; r--) {
+ for (r = rules.length-1; r >= 0; r--) {
var selectorText = rules[r].selectorText;
if (selectorText &&
rules[r].cssText &&
- selectorText !== "*" &&
- selectorText.indexOf(":hover") === -1 &&
- selectorText.indexOf(":before") === -1 &&
- selectorText.indexOf(":after") === -1 &&
- selectorText.indexOf(":active") === -1 &&
- selectorText.indexOf(":link") === -1 &&
- selectorText.indexOf("::") === -1 &&
- selectorText.indexOf("\"") === -1 &&
+ selectorText !== '*' &&
+ selectorText.indexOf(':hover') === -1 &&
+ selectorText.indexOf(':before') === -1 &&
+ selectorText.indexOf(':after') === -1 &&
+ selectorText.indexOf(':active') === -1 &&
+ selectorText.indexOf(':link') === -1 &&
+ selectorText.indexOf('::') === -1 &&
+ selectorText.indexOf('"') === -1 &&
selectorText.indexOf("'") === -1) {
var st = selectorText.split(/\s*,\s*/);
- for (var k=0; k<st.length; k++) {
+ for (k = 0 ; k < st.length ; k++) {
rulesCache.push({ 'selector': st[k], 'style': rules[r].style });
}
}
@@ -47,13 +57,15 @@ var getMatchedCSSRules = function (a) {
}
var css = [];
+ var style;
a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector;
- for(var r = 0; r < rulesCache.length; r++) {
+ for (r = 0; r < rulesCache.length; r++) {
if (a.matches(rulesCache[r].selector)) {
- var style = rulesCache[r].style;
+ style = rulesCache[r].style;
if (style.parentRule) {
var style_obj = {};
- for (var k=0, len=style.length; k<len; k++) {
+ var len;
+ for (k = 0, len = style.length ; k < len ; k++) {
if (style[k].indexOf('animation') !== -1) {
continue;
}
@@ -68,19 +80,19 @@ var getMatchedCSSRules = function (a) {
}
}
- function specificity (selector) {
+ function specificity(selector) {
// http://www.w3.org/TR/css3-selectors/#specificity
var a = 0;
- selector = selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ""; });
+ selector = selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ''; });
var b = 0;
- selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ""; });
+ selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ''; });
var c = 0;
- selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, function (a) { if(a.indexOf(':not(')===-1) c++; return ""; });
+ selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, function (a) { if (a.indexOf(':not(')===-1) c++; return ''; });
return a*100 + b*10 + c;
}
css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); });
- var style = {};
+ style = {};
_.each(css, function (v,k) {
_.each(v[1], function (v,k) {
if (v && _.isString(v) && k.indexOf('-webkit') === -1 && (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1)) {
@@ -99,7 +111,7 @@ var getMatchedCSSRules = function (a) {
delete style.display;
}
- _.each(['margin', 'padding'], function(p) {
+ _.each(['margin', 'padding'], function (p) {
if (style[p+'-top'] || style[p+'-right'] || style[p+'-bottom'] || style[p+'-left']) {
if (style[p+'-top'] === style[p+'-right'] && style[p+'-top'] === style[p+'-bottom'] && style[p+'-top'] === style[p+'-left']) {
// keep => property: [top/right/bottom/left value];
@@ -132,103 +144,125 @@ var getMatchedCSSRules = function (a) {
}
return style;
-};
+}
-// convert font awsome into image
-var font_to_img = function ($editable) {
- $editable.find(".fa").each(function () {
+/**
+ * Converts font icons to images.
+ *
+ * @param {jQuery} $editable - the element in which the font icons have to be
+ * converted to images
+ */
+function fontToImg($editable) {
+ $editable.find('.fa').each(function () {
var $font = $(this);
var icon, content;
_.find(widget.fontIcons, function (font) {
return _.find(widget.getCssSelectors(font.parser), function (css) {
if ($font.is(css[0].replace(/::?before/g, ''))) {
- icon = css[2].split("-").shift();
+ icon = css[2].split('-').shift();
content = css[1].match(/content:\s*['"]?(.)['"]?/)[1];
return true;
}
});
});
if (content) {
- var color = $font.css("color").replace(/\s/g, '');
- $font.replaceWith($("<img/>", {
- "src": _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, $font.height())),
- "data-class": $font.attr("class"),
- "data-style": $font.attr("style"),
- "class": $font.attr("class").replace(new RegExp("(^|\\s+)" + icon + "(-[^\\s]+)?", "gi"), ''), // remove inline font-awsome style
- "style": $font.attr("style")
- }).css({height: "auto", width: "auto"}));
+ var color = $font.css('color').replace(/\s/g, '');
+ $font.replaceWith($('<img/>', {
+ src: _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, $font.height())),
+ 'data-class': $font.attr('class'),
+ 'data-style': $font.attr('style'),
+ class: $font.attr('class').replace(new RegExp('(^|\\s+)' + icon + '(-[^\\s]+)?', 'gi'), ''), // remove inline font-awsome style
+ style: $font.attr('style'),
+ }).css({height: 'auto', width: 'auto'}));
} else {
$font.remove();
}
});
-};
-// convert image into font awsome
-var img_to_font = function ($editable) {
- $editable.find("img[src*='/web_editor/font_to_img/']").each(function () {
+}
+
+/**
+ * Converts images which were the result of a font icon convertion to a font
+ * icon again.
+ *
+ * @param {jQuery} $editable - the element in which the images will be converted
+ * back to font icons
+ */
+function imgToFont($editable) {
+ $editable.find('img[src*="/web_editor/font_to_img/"]').each(function () {
var $img = $(this);
- $img.replaceWith($("<span/>", {
- "class": $img.data("class"),
- "style": $img.data("style")
+ $img.replaceWith($('<span/>', {
+ class: $img.data('class'),
+ style: $img.data('style')
}));
});
-};
+}
-// convert class into inline style to send by mail
-var class_to_style = function ($editable) {
+/**
+ * Converts css style to inline style (leave the classes on elements but forces
+ * the style they give as inline style).
+ *
+ * @param {jQuery} $editable
+ */
+function classToStyle($editable) {
if (!rulesCache.length) {
getMatchedCSSRules($editable[0]);
}
$editable.find('*').each(function () {
var $target = $(this);
var css = getMatchedCSSRules(this);
- var style = $target.attr("style") || "";
+ var style = $target.attr('style') || '';
_.each(css, function (v,k) {
if (!(new RegExp('(^|;)\s*' + k).test(style))) {
- style = k+":"+v+";"+style;
+ style = k+':'+v+';'+style;
}
});
if (_.isEmpty(style)) {
- $target.removeAttr("style");
+ $target.removeAttr('style');
} else {
- $target.attr("style", style);
+ $target.attr('style', style);
}
});
-};
-// convert style into inline class from mail
-var style_to_class = function ($editable) {
+}
+
+/**
+ * Removes the inline style which is not necessary (because, for example, a
+ * class on an element will induce the same style).
+ *
+ * @param {jQuery} $editable
+ */
+function styleToClass($editable) {
getMatchedCSSRules($editable[0]);
- var $c = $('<span/>').appendTo("body");
+ var $c = $('<span/>').appendTo(document.body);
$editable.find('*').each(function () {
var $target = $(this);
var css = getMatchedCSSRules(this);
- var style = "";
+ var style = '';
_.each(css, function (v,k) {
if (!(new RegExp('(^|;)\s*' + k).test(style))) {
- style = k+":"+v+";"+style;
+ style = k+':'+v+';'+style;
}
});
- css = ($c.attr("style", style).attr("style") || "").split(/\s*;\s*/);
- style = $target.attr("style") || "";
+ css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/);
+ style = $target.attr('style') || '';
_.each(css, function (v) {
style = style.replace(v, '');
});
style = style.replace(/;+(\s;)*/g, ';').replace(/^;/g, '');
if (style !== '') {
- $target.attr("style", style);
+ $target.attr('style', style);
} else {
- $target.removeAttr("style");
+ $target.removeAttr('style');
}
});
$c.remove();
-};
+}
return {
- 'font_to_img': font_to_img,
- 'img_to_font': img_to_font,
- 'class_to_style': class_to_style,
- 'style_to_class': style_to_class,
+ fontToImg: fontToImg,
+ imgToFont: imgToFont,
+ classToStyle: classToStyle,
+ styleToClass: styleToClass,
};
-
});
diff --git a/addons/web_editor/static/src/js/editor/translator.js b/addons/web_editor/static/src/js/editor/translator.js
index e82ebbb5b81..c28a578fac0 100644
--- a/addons/web_editor/static/src/js/editor/translator.js
+++ b/addons/web_editor/static/src/js/editor/translator.js
@@ -2,70 +2,65 @@ odoo.define('web_editor.translate', function (require) {
'use strict';
var core = require('web.core');
-var ajax = require('web.ajax');
+var Dialog = require('web.Dialog');
+var localStorage = require('web.local_storage');
var Widget = require('web.Widget');
-var base = require('web_editor.base');
+var weContext = require('web_editor.context');
var rte = require('web_editor.rte');
-var editor_widget = require('web_editor.widget');
+var weWidgets = require('web_editor.widget');
var _t = core._t;
-var translatable = !!$('html').data('translatable');
-var edit_translations = !!$('html').data('edit_translations');
+var localStorageNoDialogKey = 'website_translator_nodialog';
-var RTE_Translate = rte.Class.extend({
- saveElement: function ($el, context) {
- // remove multi edition
- var key = $el.data('oe-translation-id') ? 'translation:'+$el.data('oe-translation-id') :
- ($el.data('oe-model') ? $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression') : false);
- if (!key || this.__saved[key]) return true;
- this.__saved[key] = true;
+var RTETranslatorWidget = rte.Class.extend({
- if ($el.data('oe-translation-id')) {
- var translation_content = this.getEscapedElement($el).html();
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- return ajax.jsonRpc('/web/dataset/call', 'call', {
+ /**
+ * If the element holds a translation, saves it. Otherwise, fallback to the
+ * standard saving but with the lang kept.
+ *
+ * @override
+ */
+ _saveElement: function ($el, context, withLang) {
+ if ($el.data('oe-translation-id')) {
+ return this._rpc({
model: 'ir.translation',
method: 'save_html',
args: [
[+$el.data('oe-translation-id')],
- translation_content,
- context || base.get_context()
- ],
- });
- } else {
- var markup = this.getEscapedElement($el).prop('outerHTML');
-
- return ajax.jsonRpc('/web/dataset/call', 'call', {
- model: 'ir.ui.view',
- method: 'save',
- args: [
- $el.data('oe-id'),
- markup,
- $el.data('oe-xpath') || null,
- context || base.get_context()
+ this._getEscapedElement($el).html(),
+ context || weContext.get()
],
});
}
+ return this._super($el, context, withLang === undefined ? true : withLang);
},
});
-var Translate_Modal = editor_widget.Dialog.extend({
- init: function (p, options, parent, node) {
- this._super(p, _.extend({}, {
+var AttributeTranslateDialog = weWidgets.Dialog.extend({
+ /**
+ * @constructor
+ */
+ init: function (parent, options, node) {
+ this._super(parent, _.extend({
title: _t("Translate Attribute"),
buttons: [
- {text: _t("Close"), classes: "btn-primary o_save_button", close: true, click: this.save}
- ]
+ {text: _t("Close"), classes: 'btn-primary o_save_button', click: this.save}
+ ],
}, options || {}));
- this.parent = parent;
this.$target = $(node);
this.translation = $(node).data('translation');
},
+ /**
+ * @override
+ */
start: function () {
var self = this;
- var def = this._super.apply(this, arguments);
- var $group = $("<div/>", {"class": "form-group"}).appendTo(this.$el);
+ var $group = $('<div/>', {class: 'form-group'}).appendTo(this.$el);
_.each(this.translation, function (node, attr) {
var $node = $(node);
var $label = $('<label class="control-label"></label>').text(attr);
@@ -74,42 +69,68 @@ var Translate_Modal = editor_widget.Dialog.extend({
var value = $input.val();
$node.html(value).trigger('change', node);
$node.data('$node').attr($node.data('attribute'), value).trigger('translate');
- self.parent.rte_changed(node);
+ self.trigger_up('rte_change', {target: node});
});
$group.append($label).append($input);
});
- return def;
+ return this._super.apply(this, arguments);
}
});
-var Translate = Widget.extend({
- events: {
- 'click [data-action="save"]': 'save_and_reload',
- 'click [data-action="cancel"]': 'cancel',
+var TranslatorInfoDialog = Dialog.extend({
+ template: 'web_editor.TranslatorInfoDialog',
+ xmlDependencies: Dialog.prototype.xmlDependencies.concat(
+ ['/web_editor/static/src/xml/translator.xml']
+ ),
+
+ /**
+ * @constructor
+ */
+ init: function (parent, options) {
+ this._super(parent, _.extend({
+ title: _t("Translation Info"),
+ buttons: [
+ {text: _t("Ok, never show me this again"), classes: 'btn-primary', close: true, click: this._onStrongOk.bind(this)},
+ {text: _t("Ok"), close: true}
+ ],
+ }, options || {}));
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the "strong" ok is clicked -> adapt localstorage to make sure
+ * the dialog is never displayed again.
+ *
+ * @private
+ */
+ _onStrongOk: function () {
+ localStorage.setItem(localStorageNoDialogKey, true);
},
+});
+
+var TranslatorMenuBar = Widget.extend({
template: 'web_editor.editorbar',
+ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
+ events: {
+ 'click [data-action="save"]': '_onSaveClick',
+ 'click [data-action="cancel"]': '_onCancelClick',
+ },
+ custom_events: {
+ 'rte_change': '_onRTEChange',
+ },
+
+ /**
+ * @constructor
+ */
init: function (parent, $target, lang) {
- this.parent = parent;
- this.lang = lang || base.get_context().lang;
- this.setTarget($target);
this._super.apply(this, arguments);
- this.rte = new RTE_Translate(this, this.config);
- this.rte.on('change', this, this.rte_changed);
- },
- start: function () {
- this._super.apply(this, arguments);
- this.$('#web_editor-toolbars').remove();
- return this.edit();
- },
- setTarget: function ($target) {
var $edit = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
$edit.filter(':has([data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field])').attr('data-oe-readonly', true);
-
this.$target = $edit.not('[data-oe-readonly]');
-
- // attributes
-
var attrs = ['placeholder', 'title', 'alt'];
_.each(attrs, function (attr) {
$target.find('['+attr+'*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
@@ -135,39 +156,99 @@ var Translate = Widget.extend({
});
this.$target_attr = $target.find('.o_translatable_attribute');
this.$target_attribute = $('.o_editable_translatable_attribute');
+
+ this.lang = lang || weContext.get().lang;
+
+ this.rte = new RTETranslatorWidget(this, this._getRTEConfig);
},
- find: function (selector) {
- return selector ? this.$target.find(selector).addBack().filter(selector) : this.$target;
- },
- edit: function () {
+ /**
+ * @override
+ */
+ start: function () {
+ this.$('#web_editor-toolbars').remove();
+
var flag = false;
window.onbeforeunload = function (event) {
if ($('.o_editable.o_dirty').length && !flag) {
flag = true;
- setTimeout(function () {flag=false;},0);
+ setTimeout(function () {
+ flag = false;
+ }, 0);
return _t('This document is not saved!');
}
};
- this.$target.addClass("o_editable");
+ this.$target.addClass('o_editable');
this.rte.start();
this.translations = [];
- this.markTranslatableNodes();
- this.onTranslateReady();
+ this._markTranslatableNodes();
+ this.$el.show(); // TODO seems useless
+ this.trigger('edit');
+
+ if (!localStorage.getItem(localStorageNoDialogKey)) {
+ new TranslatorInfoDialog(this).open();
+ }
+
+ return this._super.apply(this, arguments);
},
- onTranslateReady: function () {
- this.$el.show();
- this.trigger("edit");
+ /**
+ * @override
+ */
+ destroy: function () {
+ this._cancel();
+ this._super.apply(this, arguments);
},
- rte_changed: function (node) {
- var $node = $(node);
- $node.find("p").each(function () { // remove <p/> element which might have been inserted because of copy-paste
- var $p = $(this);
- $p.after($p.html()).remove();
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Leaves translation mode by hiding the translator bar.
+ *
+ * @todo should be destroyed?
+ * @private
+ */
+ _cancel: function () {
+ var self = this;
+ this.rte.cancel();
+ this.$target.each(function () {
+ $(this).html(self._getTranlationObject(this).value);
});
- var trans = this.getTranlationObject($node[0]);
- $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
+ this._unmarkTranslatableNode();
+ this.trigger('cancel');
+ this.$el.hide();
+ window.onbeforeunload = null;
+ },
+ /**
+ * Returns the RTE summernote configuration for translation mode.
+ *
+ * @private
+ * @param {jQuery} $editable
+ */
+ _getRTEConfig: function ($editable) {
+ return {
+ airMode : true,
+ focus: false,
+ airPopover: $editable.data('oe-model') ? [
+ ['history', ['undo', 'redo']],
+ ] : [
+ ['font', ['bold', 'italic', 'underline', 'clear']],
+ ['fontsize', ['fontsize']],
+ ['color', ['color']],
+ ['history', ['undo', 'redo']],
+ ],
+ styleWithSpan: false,
+ inlinemedia : ['p'],
+ lang: 'odoo',
+ onChange: function (html, $editable) {
+ $editable.trigger('content_changed');
+ },
+ };
},
- getTranlationObject: function (node) {
+ /**
+ * @private
+ */
+ _getTranlationObject: function (node) {
var $node = $(node);
var id = +$node.data('oe-translation-id');
if (!id) {
@@ -181,33 +262,23 @@ var Translate = Widget.extend({
}
return trans;
},
- __unbind_click: function (event) {
- if (event.ctrlKey || !$(event.target).is(':o_editable')) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- },
- __translate_attribute: function (event) {
- if (event.ctrlKey) {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- if (event.type !== 'mousedown') {
- return;
- }
-
- new Translate_Modal(null, {}, event.data, event.target).open();
- },
- markTranslatableNodes: function (node) {
+ /**
+ * @private
+ */
+ _markTranslatableNodes: function () {
var self = this;
this.$target.add(this.$target_attribute).each(function () {
var $node = $(this);
- var trans = self.getTranlationObject(this);
+ var trans = self._getTranlationObject(this);
trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
});
- this.$target.parent().prependEvent('click.translator', this, this.__unbind_click);
+ this.$target.parent().prependEvent('click.translator', function (ev) {
+ if (ev.ctrlKey || !$(ev.target).is(':o_editable')) {
+ return;
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ });
// attributes
@@ -215,105 +286,86 @@ var Translate = Widget.extend({
var $node = $(this);
var translation = $node.data('translation');
_.each(translation, function (node, attr) {
- var trans = self.getTranlationObject(node);
+ var trans = self._getTranlationObject(node);
trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
$node.attr('data-oe-translation-state', (trans.state || 'to_translate'));
});
});
- this.$target_attr.prependEvent('mousedown.translator click.translator mouseup.translator', this, this.__translate_attribute);
+ this.$target_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
+ if (ev.ctrlKey) {
+ return;
+ }
+ ev.preventDefault();
+ ev.stopPropagation();
+ if (ev.type !== 'mousedown') {
+ return;
+ }
- console.info('Click on CTRL when you click in an translatable area to have the default behavior');
+ new AttributeTranslateDialog(self, {}, ev.target).open();
+ });
},
- unarkTranslatableNode: function () {
+ /**
+ * Saves the translation and reloads the page to leave edit mode.
+ *
+ * @private
+ * @returns {Deferred} (never resolved as the page is reloading anyway)
+ */
+ _save: function () {
+ return this.rte.save(weContext.get({lang: this.lang})).then(function () {
+ window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
+ return $.Deferred();
+ });
+ },
+ /**
+ * @private
+ */
+ _unmarkTranslatableNode: function () {
this.$target.removeClass('o_editable').removeAttr('contentEditable');
this.$target.parent().off('.translator');
this.$target_attr.off('.translator');
},
- save_and_reload: function () {
- return this.save().then(function () {
- window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
- });
- },
- save: function () {
- var context = base.get_context();
- context.lang = this.lang;
- return this.rte.save(context);
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the "cancel" button is clicked -> undo changes and leaves
+ * edition.
+ *
+ * @private
+ */
+ _onCancelClick: function () {
+ this._cancel();
},
- cancel: function () {
- var self = this;
- this.rte.cancel();
- this.$target.each(function () {
- $(this).html(self.getTranlationObject(this).value);
+ /**
+ * Called when text is edited -> make sure text is not messed up and mark
+ * the element as dirty.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onRTEChange: function (ev) {
+ var $node = $(ev.data.target);
+ $node.find('p').each(function () { // remove <p/> element which might have been inserted because of copy-paste
+ var $p = $(this);
+ $p.after($p.html()).remove();
});
- this.unarkTranslatableNode();
- this.trigger("cancel");
- this.$el.hide();
- window.onbeforeunload = null;
+ var trans = this._getTranlationObject($node[0]);
+ $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
},
- destroy: function () {
- this.cancel();
- this.$el.remove();
- this._super.apply(this, arguments);
+ /**
+ * Called when the "save" button is clicked -> saves the translations.
+ *
+ * @private
+ */
+ _onSaveClick: function () {
+ this._save();
},
-
- config: function ($editable) {
- if ($editable.data('oe-model')) {
- return {
- 'airMode' : true,
- 'focus': false,
- 'airPopover': [
- ['history', ['undo', 'redo']],
- ],
- 'styleWithSpan': false,
- 'inlinemedia' : ['p'],
- 'lang': "odoo",
- 'onChange': function (html, $editable) {
- $editable.trigger("content_changed");
- }
- };
- }
- return {
- 'airMode' : true,
- 'focus': false,
- 'airPopover': [
- ['font', ['bold', 'italic', 'underline', 'clear']],
- ['fontsize', ['fontsize']],
- ['color', ['color']],
- ['history', ['undo', 'redo']],
- ],
- 'styleWithSpan': false,
- 'inlinemedia' : ['p'],
- 'lang': "odoo",
- 'onChange': function (html, $editable) {
- $editable.trigger("content_changed");
- }
- };
- }
});
-
-if (edit_translations) {
- base.ready().then(function () {
- data.instance = new Translate(this, $('#wrapwrap'));
- data.instance.prependTo(document.body);
-
- $('a[href*=edit_translations]').each(function () {
- this.href = this.href.replace(/[$?]edit_translations[^&?]+/, '');
- });
- $('form[action*=edit_translations]').each(function () {
- this.action = this.action.replace(/[$?]edit_translations[^&?]+/, '');
- });
-
- $('title').html($('title').html().replace(/&lt;span data-oe-model.+?&gt;(.+?)&lt;\/span&gt;/, '\$1'));
- });
-}
-
-var data = {
- 'translatable': translatable,
- 'edit_translations': edit_translations,
- 'Class': Translate,
+return {
+ Class: TranslatorMenuBar,
};
-return data;
-
});
diff --git a/addons/web_editor/static/src/js/iframe.js b/addons/web_editor/static/src/js/iframe.js
index 8b4fd385e81..c2727daf52a 100644
--- a/addons/web_editor/static/src/js/iframe.js
+++ b/addons/web_editor/static/src/js/iframe.js
@@ -1,29 +1,29 @@
odoo.define('web_editor.iframe', function (require) {
'use strict';
-var ajax = require("web.ajax");
-var core = require("web.core");
+var core = require('web.core');
var editor = require('web_editor.editor');
var translator = require('web_editor.translate');
var rte = require('web_editor.rte');
-var snippet_editor = require('web_editor.snippet.editor');
-
-var callback = window ? window["callback"] : undefined;
-window.top.odoo[callback+"_updown"] = function (value, fields_values, field_name) {
- var $editable = $("#editable_area");
- if(value !== $editable.prop("innerHTML")) {
- if ($('body').hasClass('editor_enable')) {
- if (value !== fields_values[field_name]) {
- rte.history.recordUndo($editable);
- }
- snippet_editor.instance.make_active(false);
- }
- $editable.html(value);
+var callback = window ? window['callback'] : undefined;
+window.top.odoo[callback + '_updown'] = function (value, fields_values, field_name) {
+ var $editable = $('#editable_area');
+ if (value === $editable.prop('innerHTML')) {
+ return;
+ }
- if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
- $editable.trigger("content_changed");
+ if ($('body').hasClass('editor_enable')) {
+ if (value !== fields_values[field_name]) {
+ rte.history.recordUndo($editable);
}
+ core.bus.trigger('deactivate_snippet');
+ }
+
+ $editable.html(value);
+
+ if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
+ $editable.trigger('content_changed');
}
};
@@ -32,33 +32,26 @@ editor.Class.include({
this.on('rte:start', this, function () {
this.$('form').hide();
- if (window.top.odoo[callback+"_editor"]) {
- window.top.odoo[callback+"_editor"](this);
+ if (window.top.odoo[callback + '_editor']) {
+ window.top.odoo[callback + '_editor'](this);
}
- var $editable = $("#editable_area");
+ var $editable = $('#editable_area');
setTimeout(function () {
- $($editable.find("*").filter(function () {return !this.children.length;}).first()[0] || $editable)
- .focusIn().trigger("mousedown").trigger("keyup");
+ $($editable.find('*').filter(function () {return !this.children.length;}).first()[0] || $editable)
+ .focusIn().trigger('mousedown').trigger('keyup');
},0);
$editable.on('content_changed', this, function () {
- if (window.top.odoo[callback+"_downup"]) {
- window.top.odoo[callback+"_downup"]($editable.prop('innerHTML'));
+ if (window.top.odoo[callback + '_downup']) {
+ window.top.odoo[callback + '_downup']($editable.prop('innerHTML'));
}
});
});
- this.on("snippets:ready", this, function () {
- $(window.top).trigger("resize");
+ return this._super.apply(this, arguments).then(function () {
+ $(window.top).trigger('resize'); // TODO check, probably useless
});
- return this._super.apply(this, arguments);
- }
-});
-
-snippet_editor.Class.include({
- _get_snippet_url: function () {
- return snippets_url;
}
});
@@ -69,18 +62,89 @@ rte.Class.include({
config.airPopover.splice(7, 0, ['view', ['codeview']]);
}
return config;
- }
+ },
});
translator.Class.include({
start: function () {
var res = this._super.apply(this, arguments);
$('button[data-action=save]').hide();
- if (window.top.odoo[callback+"_editor"]) {
- window.top.odoo[callback+"_editor"](this);
+ if (window.top.odoo[callback + '_editor']) {
+ window.top.odoo[callback + '_editor'](this);
}
return res;
- }
+ },
+});
+});
+
+//==============================================================================
+
+odoo.define('web_editor.IframeRoot.instance', function (require) {
+'use strict';
+
+require('web.dom_ready');
+var iframeRootData = require('web_editor.IframeRoot');
+
+var iframeRoot = new iframeRootData.IframeRoot(null);
+return iframeRoot.attachTo(document.body).then(function () {
+ return iframeRoot;
+});
});
+//==============================================================================
+
+odoo.define('web_editor.IframeRoot', function (require) {
+'use strict';
+
+var BodyManager = require('web_editor.BodyManager');
+var weContext = require('web_editor.context');
+var editor = require('web_editor.editor');
+var rootWidget = require('web_editor.root_widget');
+var translate = require('web_editor.translate');
+
+var iframeRootRegistry = new rootWidget.RootWidgetRegistry();
+
+var IframeRoot = BodyManager.extend({
+ /**
+ * @override
+ * @todo this is somehow a duplicate of website features
+ */
+ start: function () {
+ var defs = [this._super.apply(this, arguments)];
+
+ var ctx = weContext.getExtra();
+
+ if (ctx.editable && window.location.search.indexOf('enable_editor') >= 0) {
+ var editorInstance = new (editor.Class)(this);
+ defs.push(editorInstance.prependTo(this.$el));
+ }
+
+ if (ctx.edit_translations) {
+ var translator = new (translate.Class)(this, this.$('#wrapwrap'));
+ defs.push(translator.prependTo(this.$el));
+ }
+
+ return $.when.apply($, defs);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * As the IframeRoot instance is designed to be unique, the associated
+ * registry has been instantiated outside of the class and is simply
+ * returned here.
+ *
+ * @override
+ */
+ _getRegistry: function () {
+ return iframeRootRegistry;
+ },
+});
+
+return {
+ IframeRoot: IframeRoot,
+ iframeRootRegistry: iframeRootRegistry,
+};
});
diff --git a/addons/web_editor/static/src/js/inline.js b/addons/web_editor/static/src/js/inline.js
index 765ce7682df..ac0dee435ae 100644
--- a/addons/web_editor/static/src/js/inline.js
+++ b/addons/web_editor/static/src/js/inline.js
@@ -1,6 +1,7 @@
odoo.define('web_editor.inline', function (require) {
'use strict';
+var core = require('web.core');
var editor = require('web_editor.editor');
var rte = require('web_editor.rte');
var widget = require('web_editor.widget');
@@ -9,22 +10,22 @@ var snippet_editor = require('web_editor.snippet.editor');
widget.MediaDialog.include({
start: function () {
- this._super.apply(this, arguments);
this.$('[href="#editor-media-video"]').addClass('hidden');
- }
+ return this._super.apply(this, arguments);
+ },
});
editor.Class.include({
start: function () {
- if (location.search.indexOf("enable_editor") !== -1) {
+ if (window.location.search.indexOf('enable_editor') !== -1) {
this.on('rte:start', this, function () {
// move the caret at the end of the text when click after all content
- $("#wrapwrap").on('click', function (event) {
- if ($(event.target).is("#wrapwrap") || $(event.target).is("#editable_area:empty")) {
+ $('#wrapwrap').on('click', function (event) {
+ if ($(event.target).is('#wrapwrap') || $(event.target).is('#editable_area:empty')) {
_.defer(function () {
- var node = $("#editable_area *")
+ var node = $('#editable_area *')
.filter(function () { return this.textContent.match(/\S|\u00A0/); })
- .add($("#editable_area"))
+ .add($('#editable_area'))
.last()[0];
$.summernote.core.range.create(node, $.summernote.core.dom.nodeLength(node)).select();
});
@@ -38,21 +39,22 @@ editor.Class.include({
snippet_editor.Class.include({
start: function () {
- this._super.apply(this, arguments);
- setTimeout(function () {
- var $editable = $("#editable_area");
- transcoder.img_to_font($editable);
- transcoder.style_to_class($editable);
+ _.defer(function () {
+ var $editable = $('#editable_area');
+ transcoder.imgToFont($editable);
+ transcoder.styleToClass($editable);
// fix outlook image rendering bug
$editable.find('img[style*="width"], img[style*="height"]').removeAttr('height width');
});
+ return this._super.apply(this, arguments);
},
- clean_for_save: function () {
+ cleanForSave: function () {
this._super.apply(this, arguments);
- var $editable = $("#editable_area");
- transcoder.font_to_img($editable);
- transcoder.class_to_style($editable);
+
+ var $editable = $('#editable_area');
+ transcoder.fontToImg($editable);
+ transcoder.classToStyle($editable);
// fix outlook image rendering bug
_.each(['width', 'height'], function (attribute) {
@@ -65,24 +67,23 @@ snippet_editor.Class.include({
},
});
-window.top.odoo[callback+"_updown"] = function (value, fields_values) {
- var $editable = $("#editable_area");
- value = value || "";
+var callback = window ? window['callback'] : undefined;
+window.top.odoo[callback + '_updown'] = function (value, fields_values) {
+ var $editable = $('#editable_area');
+ value = value || '';
if (value.indexOf('on_change_model_and_list') === -1 && value !== $editable.html()) {
rte.history.recordUndo($editable, null, true);
- if (snippet_editor.instance) {
- snippet_editor.instance.make_active(false);
- }
+ core.bus.trigger('deactivate_snippet');
$editable.html(value);
- transcoder.img_to_font($editable);
- transcoder.style_to_class($editable);
+ transcoder.imgToFont($editable);
+ transcoder.styleToClass($editable);
// fix outlook image rendering bug
$editable.find('img[style*="width"], img[style*="height"]').removeAttr('height width');
} else {
- $editable.trigger("content_changed");
+ $editable.trigger('content_changed');
}
};
});
diff --git a/addons/web_editor/static/src/js/root_widget.js b/addons/web_editor/static/src/js/root_widget.js
new file mode 100644
index 00000000000..98a14908f10
--- /dev/null
+++ b/addons/web_editor/static/src/js/root_widget.js
@@ -0,0 +1,155 @@
+odoo.define('web_editor.root_widget', function (require) {
+'use strict';
+
+var Class = require('web.Class');
+var dom = require('web.dom');
+var mixins = require('web.mixins');
+var Widget = require('web.Widget');
+
+/**
+ * Specialized Widget which automatically instantiates child widgets to attach
+ * to internal DOM elements once it is started. The widgets to instantiate are
+ * known thanks to a linked registry which contains info about the widget
+ * classes and jQuery selectors to use to find the elements to attach them to.
+ */
+var RootWidget = Widget.extend({
+ custom_events: _.extend({}, Widget.prototype.custom_events || {}, {
+ registry_update: '_onRegistryUpdate',
+ }),
+ /**
+ * @constructor
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+
+ this._widgets = [];
+ this._listenToUpdates = false;
+ this._getRegistry().setParent(this);
+ },
+ /**
+ * @override
+ * @see _attachComponents
+ */
+ start: function () {
+ var defs = [this._super.apply(this, arguments)];
+
+ defs.push(this._attachComponents());
+ this._listenToUpdates = true;
+
+ return $.when.apply($, defs);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Instantiates a child widget according to the given registry data.
+ *
+ * @private
+ * @param {Object} childInfo
+ * @param {function} childInfo.Widget - the widget class to instantiate
+ * @param {string} childInfo.selector
+ * the jQuery selector to use to find the internal DOM element which
+ * needs to be attached to the instantiated widget
+ * @param {jQuery} [$from] - only check DOM elements which are descendant of
+ * the given one. If not given, use this.$el.
+ * @returns {Deferred}
+ */
+ _attachComponent: function (childInfo, $from) {
+ var self = this;
+ var $elements = dom.cssFind($from || this.$el, childInfo.selector);
+ var defs = _.map($elements, function (element) {
+ var w = new childInfo.Widget(self);
+ self._widgets.push(w);
+ return w.attachTo(element);
+ });
+ return $.when.apply($, defs);
+ },
+ /**
+ * Instantiates the child widgets that need to be according to the linked
+ * registry.
+ *
+ * @private
+ * @param {jQuery} [$from] - only check DOM elements which are descendant of
+ * the given one. If not given, use this.$el.
+ * @returns {Deferred}
+ */
+ _attachComponents: function ($from) {
+ var self = this;
+ var childInfos = this._getRegistry().get();
+ var defs = _.map(childInfos, function (childInfo) {
+ return self._attachComponent(childInfo, $from);
+ });
+ return $.when.apply($, defs);
+ },
+ /**
+ * Returns the `RootWidgetRegistry` instance that is linked to this
+ * `RootWidget` instance.
+ *
+ * @abstract
+ * @private
+ * @returns {RootWidgetRegistry}
+ */
+ _getRegistry: function () {},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the linked registry is updated after this `RootWidget`
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onRegistryUpdate: function (ev) {
+ ev.stopPropagation();
+ if (this._listenToUpdates) {
+ this._attachComponent(ev.data);
+ }
+ },
+});
+
+var RootWidgetRegistry = Class.extend(mixins.EventDispatcherMixin, {
+ /**
+ * @constructor
+ */
+ init: function () {
+ mixins.EventDispatcherMixin.init.call(this);
+ this._registry = [];
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Adds an element to the registry (info of what and how to instantiate).
+ *
+ * @param {function} Widget - the widget class to instantiate
+ * @param {string} selector
+ * the jQuery selector to use to find the internal DOM element which
+ * needs to be attached to the instantiated widget
+ */
+ add: function (Widget, selector) {
+ var registryInfo = {
+ Widget: Widget,
+ selector: selector,
+ };
+ this._registry.push(registryInfo);
+ this.trigger_up('registry_update', registryInfo);
+ },
+ /**
+ * Retrieves all the registry elements.
+ */
+ get: function () {
+ return this._registry;
+ },
+});
+
+return {
+ RootWidget: RootWidget,
+ RootWidgetRegistry: RootWidgetRegistry,
+};
+});
diff --git a/addons/web_editor/static/src/js/snippets.animation.js b/addons/web_editor/static/src/js/snippets.animation.js
deleted file mode 100644
index 47d62dda821..00000000000
--- a/addons/web_editor/static/src/js/snippets.animation.js
+++ /dev/null
@@ -1,119 +0,0 @@
-odoo.define('web_editor.snippets.animation', function (require) {
-'use strict';
-
-var Class = require('web.Class');
-var base = require('web_editor.base');
-
-var registry = {};
-var ready = [];
-
-/**
- * The Animation class provides a way for executing code once a website DOM element is loaded in the dom
- * and handle the case where the website edit mode is triggered.
- */
-var Animation = Class.extend({
- /**
- * The selector attribute, if defined, allows to automatically create an instance of this animation on
- * page load for each DOM element which matches this selector. The Animation $target element will then
- * be that particular DOM element. This should be the main way of instantiating Animation elements.
- */
- selector: false,
- /**
- * The $ method is a helper to search for DOM elements inside the animation DOM element target.
- * @param selector of the element to search in the DOM element target
- */
- $: function () {
- return this.$el.find.apply(this.$el, arguments);
- },
- /**
- * The init method starts the animation, waiting for its dependencies to be initialized.
- * The init method is called when instantiating the Animation thanks to the "new" keyword.
- * @param dom - the DOM element the animation is targeting
- * @param editable_mode - A boolean which is true if the page is in edition mode
- */
- init: function (dom, editable_mode) {
- this.$el = this.$target = $(dom);
- this._start(editable_mode);
- },
- /**
- * The willStart method initializes the resources the start method will need to properly work.
- * @param editable_mode - A boolean which is true if the page is in edition mode
- * @return A deferred which is resolved once all the resources are loaded
- */
- willStart: function (editable_mode) {
- return $.when();
- },
- /**
- * The abstract start method is intended to initialize the animation.
- * The method should not be called directly as called automatically on animation initialization and
- * on restart (@see Animation.init, @see Animation.restart).
- * @param editable_mode - A boolean which is true if the page is in edition mode
- * @param next argument will be the resources given by the willStart deferred (@see Animation.willStart)
- */
- start: function (editable_mode) {},
- /**
- * The abstract stop method is intended to destroy the animation and should basically restore the
- * target to the way it was before the start method was called.
- * Note: this method should be callable even if the start method was not called earlier.
- */
- stop: function () {},
- /**
- * The restart method stops the animation then starts it again, waiting for the eventual willStart
- * deferred.
- * @param editable_mode - A boolean which is true if the page is in edition mode
- * @return A deferred which is resolved once the Animation resources/elements are (re)loaded/(re)initialized.
- */
- restart: function (editable_mode) {
- return $.when(this.stop()).then(this._start.bind(this, editable_mode));
- },
- /**
- * The private _start method basically calls the start method after waiting for the end
- * of the willStart method.
- * @param editable_mode - A boolean which is true if the page is in edition mode
- * @return A deferred which is resolved once the Animation resources/elements are loaded/initialized.
- */
- _start: function (editable_mode) {
- return this.willStart(editable_mode).then(this.start.bind(this, editable_mode));
- },
-});
-
-/**
- * Start animations when the website loading is finished
- */
-base.ready().always(function () {
- _.defer(start);
-});
-
-return {
- Class: Animation,
- registry: registry,
- start: start,
-};
-
-/**
- * The start function allows to create an Animation instance for each registered one for each DOM element
- * which matches the "selector" key of these registered animations (@see Animation.selector).
- * @param editable_mode - A boolean which is true if the page is in edition mode
- * @param $init_target (optional) - a jQuery DOM element ; if given, only initialize the animations whose
- * "selector" matches it
- */
-function start(editable_mode, $init_target) {
- _.each(registry, function (Animation) {
- var selector = Animation.prototype.selector || "";
- var $target = $init_target ? $init_target.filter(selector) : $(selector);
-
- $target.each(function () {
- var $snippet = $(this);
- var animation = $snippet.data("snippet-view");
- if (!$snippet.parents("#oe_snippets").length
- && !$snippet.parent("body").length
- && !animation) {
- ready.push($snippet);
- $snippet.data("snippet-view", new Animation($snippet, editable_mode));
- } else if (animation) {
- animation.restart(editable_mode);
- }
- });
- });
-}
-});
diff --git a/addons/web_editor/static/src/js/tours/rte.js b/addons/web_editor/static/src/js/tours/rte.js
index da4780202d3..f80704bf878 100644
--- a/addons/web_editor/static/src/js/tours/rte.js
+++ b/addons/web_editor/static/src/js/tours/rte.js
@@ -1,322 +1,328 @@
odoo.define('web_editor.tour', function (require) {
- "use strict";
+'use strict';
- var tour = require("web_tour.tour");
- var base = require("web_editor.base");
- var snippet_editor = require('web_editor.snippet.editor');
+var core = require('web.core');
+var tour = require('web_tour.tour');
+var base = require('web_editor.base');
- var click_event = function (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);
- };
+/**
+ * Simulates a click event of given type on the given element.
+ *
+ * @param {DOMElement} el
+ * @param {string} type - 'click', 'mouseup', ...
+ */
+function simulateClickEvent(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);
+}
- tour.register("rte", {
- url: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
- test: true,
- wait_for: base.ready(),
- }, [{
- content: "Change html for this test",
- trigger: "#editable_area",
- run: function () {
- var html = '\n'+
- '<section>\n'+
- ' <div class="container" style="width: 600px;">\n'+
- ' <div class="row">\n'+
- ' <div class="col-md-6 mt16">\n'+
- '<h1 id="text_content_id">Batnae municipium in Anthemusia</h1> \n'+
- ' <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>\n'+
- ' <p> Quam <img style="width: 25%" src="/web/static/src/img/logo.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+
- ' <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+
- '<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+
- ' </div>\n'+
- ' <div class="col-md-6 mt16">\n'+
- ' <img class="img img-responsive shadow mb16" src="/web/static/src/img/logo.png" alt="Odoo text and image block">\n'+
- ' </div>\n'+
- ' </div>\n'+
- '</section>\n';
- this.$anchor.html(html);
- }
- }, {
- content: "simulate triple click and change text bg-color",
- trigger: '#editable_area > section .row > div:first',
- run: function () {
- var $h1 = $('h1', this.$anchor);
- $.summernote.core.range.create($h1[0].firstChild, 0, $('p', this.$anchor)[0], 0).select();
- click_event($h1[0], 'mouseup');
- }
- }, {
- content: "change text bg-color after triple click",
- trigger: '.note-popover button[data-event="color"]',
- extra_trigger: '#editable_area > section .row > div:first',
- }, {
- content: "change selection to change text color",
- trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1 font',
- run: function () {
- $.summernote.core.range.create(this.$anchor[0].firstChild, 5, this.$anchor[0].firstChild, 10).select();
- click_event(this.$anchor[0], 'mouseup');
- }
- }, {
- content: "open color dropdown",
- trigger: ".note-color button.dropdown-toggle",
- }, {
- content: "change text color",
- trigger: ".btn-group.open button[data-event=foreColor]:first",
- }, {
- content: "change selection to change text bg-color again",
- trigger: '#editable_area > section .row > div:first h1 font:eq(2)',
- run: function () {
- $.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor[0].firstChild, 10).select();
- click_event(this.$anchor.prev()[0], 'mouseup');
- }
- }, {
- content: "open color dropdown",
- trigger: ".note-color button.dropdown-toggle",
- }, {
- content: "change text backColor again",
- trigger: "button[data-event=backColor]:visible:first",
- }, {
- content: "change selection (h1 and p) to change text color with class",
- trigger: '#editable_area > section .row > div:first h1 font:eq(4)',
- run: function () {
- $.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor.parent("h1").next("p")[0].firstChild, 30).select();
- click_event(this.$anchor.prev()[0], 'mouseup');
- }
- }, {
- content: "open color dropdown",
- trigger: ".note-color button.dropdown-toggle",
- }, {
- content: "change text color again",
- trigger: "button[data-event=foreColor]:visible:eq(3)",
- }, {
- content: "delete selection",
- trigger: '.o_editable.note-editable.o_dirty',
- extra_trigger: '#editable_area > section .row > div:first p font',
- run: "keydown 46", // delete
- }, {
- content: "create an other selection to delete",
- trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
- extra_trigger: '.o_editable.note-editable.o_dirty',
- run: function () {
- $.summernote.core.range.createFromNode(this.$anchor.next("p")[0]).clean();
- $.summernote.core.range.create(this.$anchor.find('font:containsExact(ici)')[0].firstChild, 1, this.$anchor.next().next()[0].firstChild, 5).select();
- click_event(this.$anchor.find('font:last')[0], 'mouseup');
- },
- }, {
- content: "clean and delete (backspace) an other selection",
- trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
- run: "keydown 8", // backspace
- }, {
- content: "an other selection",
- trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
- extra_trigger: '.o_editable.note-editable.o_dirty',
- run: function () {
- $.summernote.core.range.create(this.$anchor.find('font:first')[0].firstChild, 3, this.$anchor.next("p")[0].childNodes[2], 8).select();
- click_event(this.$anchor.find('font:first')[0], 'mouseup');
- },
- }, {
- content: "delete an other selection",
- trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
- run: "keydown 46",
- }, {
- content: "undo",
- trigger: '.note-image-popover button[data-event="undo"]',
- extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )',
- }, {
- content: "undo again",
- trigger: '.note-air-popover button[data-event="undo"]',
- extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )',
- }, {
- content: "delete (backspace) after undo",
- trigger: '.o_editable.note-editable.o_dirty',
- extra_trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
- run: "keydown 8", // backspace
- }, {
- content: "click on image",
- trigger: '#editable_area > section .row > div:first img[style*="25%"]',
- extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
- }, {
- content: "Click on resize half",
- trigger: '.note-image-popover:visible button[data-event="resize"][data-value="0.5"]',
- }, {
- content: "Click on edit picture",
- trigger: '.note-image-popover:visible button[data-event="showImageDialog"]',
- extra_trigger: '#editable_area > section .row > div:first img[style*="50%"]',
- }, {
- content: "Click on pictogram tab",
- trigger: 'a[data-toggle="tab"]:contains(Pictogram)',
- extra_trigger: '#editor-media-image',
- }, {
- content: "select a pictogram",
- trigger: '#editor-media-icon.active span.fa:first',
- }, {
- content: "save pictogram",
- trigger: 'button.o_save_button',
- extra_trigger: '#editor-media-icon.active span.o_selected',
- }, {
- content: "select a size for the pictogram",
- trigger: '.note-image-popover button[data-event="resizefa"][data-value="3"]',
- }, {
- content: "click on float right",
- trigger: '.note-image-popover:visible button[data-event="floatMe"][data-value="right"]',
- extra_trigger: '#wrapwrap span.fa-3x',
- }, {
- content: "click on create link",
- trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
- extra_trigger: '#editable_area > section .row > div:first span.fa.pull-right',
- }, {
- content: "click on color style",
- trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
- extra_trigger: 'a#link-preview:containsRegex(/^<span [^>]+><\\/span>$/) > span.fa.fa-3x.pull-right',
- }, {
- content: "insert a link url",
- trigger: '#o_link_dialog_url_input',
- extra_trigger: 'a#link-preview.btn',
- run: "text http://www.odoo.com",
- }, {
- content: "change text label",
- trigger: '#o_link_dialog_label_input',
- run: "text ABC[IMG] DEF",
- }, {
- content: "save link",
- trigger: 'button.o_save_button',
- extra_trigger: 'a#link-preview.btn:containsRegex(/^ABC<span [^>]+><\\/span> DEF$/)',
- }, {
- content: "click on other picture",
- trigger: '#editable_area > section .row > div:last img',
- extra_trigger: 'body:not(:has(#link-preview)) a.btn[href^="http://"]:has(span.fa.fa-3x.pull-right)',
- }, {
- content: "click on create link again",
- trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
- extra_trigger: '#editable_area > section .row > div:first span.fa.pull-right',
- }, {
- content: "click on color style again",
- trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
- extra_trigger: 'a#link-preview:containsRegex(/^<img [^>]+>$/) img',
- }, {
- content: "insert an email",
- trigger: '#o_link_dialog_url_input',
- extra_trigger: 'a#link-preview.btn',
- run: "text test@test.test",
- }, {
- content: "save link",
- trigger: 'button.o_save_button',
- extra_trigger: 'a#link-preview.btn[href="mailto:test@test.test"]',
- }, {
- content: "select for triple enter then double backspace",
- trigger: '#editable_area > section .row > div:first p:eq(2)',
- extra_trigger: 'body:not(:has(#link-preview)) #editable_area > section .row > div:eq(1) > a > img',
- run: function () {
- var p = this.$anchor[0].firstChild;
- $.summernote.core.range.create(p, p.textContent.length, p, p.textContent.length).select();
- click_event(p, 'mouseup');
- },
- }, {
- content: "triple enter then double backspace",
- trigger: '#editable_area > section .row > div:first p:eq(2)',
- run: "keydown 66,13,66,13,13,8,8", // B enter B enter enter backspace backspace
- }, {
- content: "add ul content",
- trigger: '#editable_area > section .row > div:first',
- extra_trigger: 'body:not(:has(#editable_area > section .row > div:first p:eq(5), #editable_area > section .row > div:eq(3))) #editable_area > section .row > div:first p:eq(3)',
- run: function () {
- var html = ' <ul> '+
- '\n <li> <p>Batnae municipium. </p></li>'+
- '\n <li> Seleucia praeter.</li>'+
- '\n <li><p>Et hanc quidem.</p></li>'+
- '\n </ul>';
- this.$anchor.append(html);
- var node = this.$anchor.find('ul li p:last')[0].firstChild;
- $.summernote.core.range.create(node, 6).select();
- click_event(node, 'mouseup');
- }
- }, {
- content: "click on style dropdown",
- trigger: '.note-air-popover .note-style button.dropdown-toggle',
- extra_trigger: '#editable_area > section .row > div:first ul li p:first',
- }, {
- content: "select h3",
- trigger: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
- }, {
- content: "select h3",
- trigger: '#editable_area > section .row > div:first > ul > li > h3',
- run: function () {
- var node = this.$anchor[0].firstChild;
- $.summernote.core.range.create(node, 0).select();
- click_event(node, 'mouseup');
- }
- }, {
- content: "double tabulation",
- trigger: '#editable_area > section .row > div:first > ul > li > h3',
- run: "keydown 9,9", // tabulation
- }, {
- content: "click on order list",
- trigger: '.note-air-popover button[data-event="insertOrderedList"]',
- extra_trigger: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
- }, {
- content: "select for enter in ul",
- trigger: '#editable_area > section .row > div:first ul li > p:last',
- run: function () {
- this.$anchor[0].firstChild.textContent += "";
- $.summernote.core.range.create(this.$anchor[0].firstChild, 7).select();
- click_event(this.$anchor[0], 'mouseup');
- }
- }, {
- content: "enter in ul",
- trigger: '#editable_area > section .row > div:first ul li > p:last',
- run: "keydown 66,13", // enter
- }, {
- trigger: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
- content: "backspace in list",
- run: "keydown 8",
- }, {
- content: "end",
- trigger: '#editable_area > section .row > div:first ul li p:eq(0):containsRegex(/^Batnae Bmunicipium.$/)',
- }]);
+tour.register('rte', {
+ url: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
+ test: true,
+ wait_for: base.ready(),
+}, [{
+ content: "Change html for this test",
+ trigger: "#editable_area",
+ run: function () {
+ var html = '\n'+
+ '<section>\n'+
+ ' <div class="container" style="width: 600px;">\n'+
+ ' <div class="row">\n'+
+ ' <div class="col-md-6 mt16">\n'+
+ '<h1 id="text_content_id">Batnae municipium in Anthemusia</h1> \n'+
+ ' <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>\n'+
+ ' <p> Quam <img style="width: 25%" src="/web/static/src/img/logo.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+
+ ' <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+
+ '<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+
+ ' </div>\n'+
+ ' <div class="col-md-6 mt16">\n'+
+ ' <img class="img img-responsive shadow mb16" src="/web/static/src/img/logo.png" alt="Odoo text and image block">\n'+
+ ' </div>\n'+
+ ' </div>\n'+
+ '</section>\n';
+ this.$anchor.html(html);
+ }
+}, {
+ content: "simulate triple click and change text bg-color",
+ trigger: '#editable_area > section .row > div:first',
+ run: function () {
+ var $h1 = $('h1', this.$anchor);
+ $.summernote.core.range.create($h1[0].firstChild, 0, $('p', this.$anchor)[0], 0).select();
+ simulateClickEvent($h1[0], 'mouseup');
+ }
+}, {
+ content: "change text bg-color after triple click",
+ trigger: '.note-popover button[data-event="color"]',
+ extra_trigger: '#editable_area > section .row > div:first',
+}, {
+ content: "change selection to change text color",
+ trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1 font',
+ run: function () {
+ $.summernote.core.range.create(this.$anchor[0].firstChild, 5, this.$anchor[0].firstChild, 10).select();
+ simulateClickEvent(this.$anchor[0], 'mouseup');
+ }
+}, {
+ content: "open color dropdown",
+ trigger: ".note-color button.dropdown-toggle",
+}, {
+ content: "change text color",
+ trigger: ".btn-group.open button[data-event=foreColor]:first",
+}, {
+ content: "change selection to change text bg-color again",
+ trigger: '#editable_area > section .row > div:first h1 font:eq(2)',
+ run: function () {
+ $.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor[0].firstChild, 10).select();
+ simulateClickEvent(this.$anchor.prev()[0], 'mouseup');
+ }
+}, {
+ content: "open color dropdown",
+ trigger: ".note-color button.dropdown-toggle",
+}, {
+ content: "change text backColor again",
+ trigger: "button[data-event=backColor]:visible:first",
+}, {
+ content: "change selection (h1 and p) to change text color with class",
+ trigger: '#editable_area > section .row > div:first h1 font:eq(4)',
+ run: function () {
+ $.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor.parent("h1").next("p")[0].firstChild, 30).select();
+ simulateClickEvent(this.$anchor.prev()[0], 'mouseup');
+ }
+}, {
+ content: "open color dropdown",
+ trigger: ".note-color button.dropdown-toggle",
+}, {
+ content: "change text color again",
+ trigger: "button[data-event=foreColor]:visible:eq(3)",
+}, {
+ content: "delete selection",
+ trigger: '.o_editable.note-editable.o_dirty',
+ extra_trigger: '#editable_area > section .row > div:first p font',
+ run: "keydown 46", // delete
+}, {
+ content: "create an other selection to delete",
+ trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
+ extra_trigger: '.o_editable.note-editable.o_dirty',
+ run: function () {
+ $.summernote.core.range.createFromNode(this.$anchor.next("p")[0]).clean();
+ $.summernote.core.range.create(this.$anchor.find('font:containsExact(ici)')[0].firstChild, 1, this.$anchor.next().next()[0].firstChild, 5).select();
+ simulateClickEvent(this.$anchor.find('font:last')[0], 'mouseup');
+ },
+}, {
+ content: "clean and delete (backspace) an other selection",
+ trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
+ run: "keydown 8", // backspace
+}, {
+ content: "an other selection",
+ trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
+ extra_trigger: '.o_editable.note-editable.o_dirty',
+ run: function () {
+ $.summernote.core.range.create(this.$anchor.find('font:first')[0].firstChild, 3, this.$anchor.next("p")[0].childNodes[2], 8).select();
+ simulateClickEvent(this.$anchor.find('font:first')[0], 'mouseup');
+ },
+}, {
+ content: "delete an other selection",
+ trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
+ run: "keydown 46",
+}, {
+ content: "undo",
+ trigger: '.note-image-popover button[data-event="undo"]',
+ extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )',
+}, {
+ content: "undo again",
+ trigger: '.note-air-popover button[data-event="undo"]',
+ extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )',
+}, {
+ content: "delete (backspace) after undo",
+ trigger: '.o_editable.note-editable.o_dirty',
+ extra_trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
+ run: "keydown 8", // backspace
+}, {
+ content: "click on image",
+ trigger: '#editable_area > section .row > div:first img[style*="25%"]',
+ extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
+}, {
+ content: "Click on resize half",
+ trigger: '.note-image-popover:visible button[data-event="resize"][data-value="0.5"]',
+}, {
+ content: "Click on edit picture",
+ trigger: '.note-image-popover:visible button[data-event="showImageDialog"]',
+ extra_trigger: '#editable_area > section .row > div:first img[style*="50%"]',
+}, {
+ content: "Click on pictogram tab",
+ trigger: 'a[data-toggle="tab"]:contains(Pictogram)',
+ extra_trigger: '#editor-media-image',
+}, {
+ content: "select a pictogram",
+ trigger: '#editor-media-icon.active span.fa:first',
+}, {
+ content: "save pictogram",
+ trigger: 'button.o_save_button',
+ extra_trigger: '#editor-media-icon.active span.o_selected',
+}, {
+ content: "select a size for the pictogram",
+ trigger: '.note-image-popover button[data-event="resizefa"][data-value="3"]',
+}, {
+ content: "click on float right",
+ trigger: '.note-image-popover:visible button[data-event="floatMe"][data-value="right"]',
+ extra_trigger: '#wrapwrap span.fa-3x',
+}, {
+ content: "click on create link",
+ trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
+ extra_trigger: '#editable_area > section .row > div:first span.fa.pull-right',
+}, {
+ content: "click on color style",
+ trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
+ extra_trigger: 'a#link-preview:containsRegex(/^<span [^>]+><\\/span>$/) > span.fa.fa-3x.pull-right',
+}, {
+ content: "insert a link url",
+ trigger: '#o_link_dialog_url_input',
+ extra_trigger: 'a#link-preview.btn',
+ run: "text http://www.odoo.com",
+}, {
+ content: "change text label",
+ trigger: '#o_link_dialog_label_input',
+ run: "text ABC[IMG] DEF",
+}, {
+ content: "save link",
+ trigger: 'button.o_save_button',
+ extra_trigger: 'a#link-preview.btn:containsRegex(/^ABC<span [^>]+><\\/span> DEF$/)',
+}, {
+ content: "click on other picture",
+ trigger: '#editable_area > section .row > div:last img',
+ extra_trigger: 'body:not(:has(#link-preview)) a.btn[href^="http://"]:has(span.fa.fa-3x.pull-right)',
+}, {
+ content: "click on create link again",
+ trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
+ extra_trigger: '#editable_area > section .row > div:first span.fa.pull-right',
+}, {
+ content: "click on color style again",
+ trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
+ extra_trigger: 'a#link-preview:containsRegex(/^<img [^>]+>$/) img',
+}, {
+ content: "insert an email",
+ trigger: '#o_link_dialog_url_input',
+ extra_trigger: 'a#link-preview.btn',
+ run: "text test@test.test",
+}, {
+ content: "save link",
+ trigger: 'button.o_save_button',
+ extra_trigger: 'a#link-preview.btn[href="mailto:test@test.test"]',
+}, {
+ content: "select for triple enter then double backspace",
+ trigger: '#editable_area > section .row > div:first p:eq(2)',
+ extra_trigger: 'body:not(:has(#link-preview)) #editable_area > section .row > div:eq(1) > a > img',
+ run: function () {
+ var p = this.$anchor[0].firstChild;
+ $.summernote.core.range.create(p, p.textContent.length, p, p.textContent.length).select();
+ simulateClickEvent(p, 'mouseup');
+ },
+}, {
+ content: "triple enter then double backspace",
+ trigger: '#editable_area > section .row > div:first p:eq(2)',
+ run: "keydown 66,13,66,13,13,8,8", // B enter B enter enter backspace backspace
+}, {
+ content: "add ul content",
+ trigger: '#editable_area > section .row > div:first',
+ extra_trigger: 'body:not(:has(#editable_area > section .row > div:first p:eq(5), #editable_area > section .row > div:eq(3))) #editable_area > section .row > div:first p:eq(3)',
+ run: function () {
+ var html = ' <ul> '+
+ '\n <li> <p>Batnae municipium. </p></li>'+
+ '\n <li> Seleucia praeter.</li>'+
+ '\n <li><p>Et hanc quidem.</p></li>'+
+ '\n </ul>';
+ this.$anchor.append(html);
+ var node = this.$anchor.find('ul li p:last')[0].firstChild;
+ $.summernote.core.range.create(node, 6).select();
+ simulateClickEvent(node, 'mouseup');
+ }
+}, {
+ content: "click on style dropdown",
+ trigger: '.note-air-popover .note-style button.dropdown-toggle',
+ extra_trigger: '#editable_area > section .row > div:first ul li p:first',
+}, {
+ content: "select h3",
+ trigger: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
+}, {
+ content: "select h3",
+ trigger: '#editable_area > section .row > div:first > ul > li > h3',
+ run: function () {
+ var node = this.$anchor[0].firstChild;
+ $.summernote.core.range.create(node, 0).select();
+ simulateClickEvent(node, 'mouseup');
+ }
+}, {
+ content: "double tabulation",
+ trigger: '#editable_area > section .row > div:first > ul > li > h3',
+ run: "keydown 9,9", // tabulation
+}, {
+ content: "click on order list",
+ trigger: '.note-air-popover button[data-event="insertOrderedList"]',
+ extra_trigger: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
+}, {
+ content: "select for enter in ul",
+ trigger: '#editable_area > section .row > div:first ul li > p:last',
+ run: function () {
+ this.$anchor[0].firstChild.textContent += "";
+ $.summernote.core.range.create(this.$anchor[0].firstChild, 7).select();
+ simulateClickEvent(this.$anchor[0], 'mouseup');
+ }
+}, {
+ content: "enter in ul",
+ trigger: '#editable_area > section .row > div:first ul li > p:last',
+ run: "keydown 66,13", // enter
+}, {
+ trigger: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
+ content: "backspace in list",
+ run: "keydown 8",
+}, {
+ content: "end",
+ trigger: '#editable_area > section .row > div:first ul li p:eq(0):containsRegex(/^Batnae Bmunicipium.$/)',
+}]);
- tour.register('rte_inline', {
- url: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
- test: true,
- wait_for: base.ready(),
- }, [{
- content: "Change html for this test",
- trigger: "#editable_area",
- run: function () {
- var html = '\n'+
- '<div>\n'+
- ' <table cellspacing="0" cellpadding="0" width="100%">\n'+
- ' <tbody>\n'+
- ' <tr>\n'+
- ' <td valign="center" width="270">\n'+
- ' <img src="/logo.png" alt="Your Logo" class="img-circle img-thumbnail">\n'+
- ' </td>\n'+
- ' <td valign="center" width="270">\n'+
- ' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+
- ' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square pull-right"></span>\n'+
- ' </td>\n'+
- ' </tr>\n'+
- ' </tbody>\n'+
- ' </table>\n'+
- '</div>';
- this.$anchor.html(html);
- }
- }, {
- content: "call clean for save",
- trigger: '#wrapwrap table',
- run: function () {
- snippet_editor.instance.clean_for_save();
- }
- }, {
- content: "check the image style",
- trigger: '#wrapwrap img:first[width][height][style*="-radius"][style*="1px"][style*="padding"]',
- }, {
- content: "check the font image src",
- trigger: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(51,122,183)/28"]',
- }, {
- content: "check the font class to css",
- trigger: '#wrapwrap img:eq(1)[height]:not([class*="fa"])',
- }, {
- content: "check the second font class to css",
- trigger: '#wrapwrap img:eq(2)[style*="float: right"]',
- }]);
+tour.register('rte_inline', {
+ url: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
+ test: true,
+ wait_for: base.ready(),
+}, [{
+ content: "Change html for this test",
+ trigger: "#editable_area",
+ run: function () {
+ var html = '\n'+
+ '<div>\n'+
+ ' <table cellspacing="0" cellpadding="0" width="100%">\n'+
+ ' <tbody>\n'+
+ ' <tr>\n'+
+ ' <td valign="center" width="270">\n'+
+ ' <img src="/logo.png" alt="Your Logo" class="img-circle img-thumbnail">\n'+
+ ' </td>\n'+
+ ' <td valign="center" width="270">\n'+
+ ' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+
+ ' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square pull-right"></span>\n'+
+ ' </td>\n'+
+ ' </tr>\n'+
+ ' </tbody>\n'+
+ ' </table>\n'+
+ '</div>';
+ this.$anchor.html(html);
+ }
+}, {
+ content: "call clean for save",
+ trigger: '#wrapwrap table',
+ run: function () {
+ core.bus.trigger('snippet_editor_clean_for_save');
+ }
+}, {
+ content: "check the image style",
+ trigger: '#wrapwrap img:first[width][height][style*="-radius"][style*="1px"][style*="padding"]',
+}, {
+ content: "check the font image src",
+ trigger: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(51,122,183)/28"]',
+}, {
+ content: "check the font class to css",
+ trigger: '#wrapwrap img:eq(1)[height]:not([class*="fa"])',
+}, {
+ content: "check the second font class to css",
+ trigger: '#wrapwrap img:eq(2)[style*="float: right"]',
+}]);
});
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,
};
});
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 ac93545c87b..5ec0dfdc697 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
@@ -476,7 +476,6 @@ body .modal {
}
}
-// FIXME bootstrap modal : 1050
.ui-autocomplete {
- z-index: 1051;
+ z-index: @zindex-modal + 1;
}
diff --git a/addons/web_editor/static/src/less/web_editor.ui.less b/addons/web_editor/static/src/less/web_editor.ui.less
index 1e44204c906..a434f07fe54 100644
--- a/addons/web_editor/static/src/less/web_editor.ui.less
+++ b/addons/web_editor/static/src/less/web_editor.ui.less
@@ -166,7 +166,7 @@ html[lang] > body.editor_enable [data-oe-translation-state] {
}
}
- &.disable {
+ &.o_disabled {
.oe_snippet_thumbnail {
background-color: rgba(255, 0, 0, 0.75);
}
@@ -302,6 +302,8 @@ html[lang] > body.editor_enable [data-oe-translation-state] {
@odoo-handle-edge-size: 1px;
@odoo-handle-border-width: 1px;
+ position: relative;
+
// SNIPPET MANIPULATORS
.oe_overlay {
.o-position-absolute();
diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml
index 39bb389f1de..3fdbc462110 100644
--- a/addons/web_editor/static/src/xml/editor.xml
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -6,7 +6,7 @@
<!-- Editor top bar which contains the summernote tools and save/discard buttons -->
<t t-name="web_editor.editorbar">
- <div id="web_editor-top-edit" class="o_css_editor">
+ <div id="web_editor-top-edit">
<div id="web_editor-toolbars"/>
<form class="navbar-form text-muted">
<button type="button" class="btn btn-sm btn-default" data-action="cancel"><i class="fa fa-times"/> Discard</button>
diff --git a/addons/web_editor/static/src/xml/snippets.xml b/addons/web_editor/static/src/xml/snippets.xml
index d9e8c83a495..ccf56129ad1 100644
--- a/addons/web_editor/static/src/xml/snippets.xml
+++ b/addons/web_editor/static/src/xml/snippets.xml
@@ -1,18 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
-
- <!-- Snippet loader -->
- <t t-name="web_editor.snippets_style">
- <li class="navbar-form">
- <div class="btn-group">
- <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
- Block style <span class="caret"></span>
- </button>
- <ul class="dropdown-menu" id="oe_snippets_style" role="menu"></ul>
- </div>
- </li>
- </t>
-
<t t-name="web_editor.snippet_overlay">
<div class="oe_overlay">
<div class="oe_handles">
@@ -40,10 +27,6 @@
</div>
</t>
- <t t-name="web_editor.snippet_manipulator">
- <div class='oe_snippet_manipulator'></div>
- </t>
-
<!-- options -->
<div t-name="web_editor.snippet.option.colorpicker" class="colorpicker">
<div class="note-palette-title">Background Color</div>
diff --git a/addons/web_editor/static/src/xml/translator.xml b/addons/web_editor/static/src/xml/translator.xml
new file mode 100644
index 00000000000..dbd6c8287f6
--- /dev/null
+++ b/addons/web_editor/static/src/xml/translator.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates id="template" xml:space="preserve">
+<div t-name="web_editor.TranslatorInfoDialog">
+ <p>You are about to enter the translation mode.</p>
+ <p>Here are the visuals used to help you translate efficiently:</p>
+ <ul class="oe_translate_examples">
+ <li data-oe-translation-state="to_translate">Content to translate</li>
+ <li data-oe-translation-state="translated">Translated content</li>
+ </ul>
+ <p>
+ In this mode, you can only translate texts. To change the structure of the page, you must edit the master page.
+ Each modification on the master page is automatically applied to all translated versions.
+ </p>
+</div>
+</templates>