diff --git a/addons/portal/static/src/js/portal.js b/addons/portal/static/src/js/portal.js
index edc5ae4c2dd..5842596db1b 100644
--- a/addons/portal/static/src/js/portal.js
+++ b/addons/portal/static/src/js/portal.js
@@ -1,10 +1,10 @@
-
-odoo.define('portal.portal', function(require) {
+odoo.define('portal.portal', function (require) {
'use strict';
-$(document).ready(function () {
+ require('web.dom_ready');
- if(!$('.o_portal').length) {
+
+ if (!$('.o_portal').length) {
return $.Deferred().reject("DOM doesn't contain '.o_portal'");
}
@@ -21,14 +21,14 @@ $(document).ready(function () {
}
if ($('.o_portal_search_panel').length) {
- $('.o_portal_search_panel .search-submit').click(function() {
+ $('.o_portal_search_panel .search-submit').click(function () {
var search = $.deparam(window.location.search.substring(1));
search.search_in = $(".o_portal_search_panel li.active a").attr("href").replace("#","");
search.search = $(".o_portal_search_panel input[name='search']").val();
window.location.search = $.param(search);
});
- $('.o_portal_search_panel .dropdown-menu').find('a').click(function(e) {
+ $('.o_portal_search_panel .dropdown-menu').find('a').click(function (e) {
e.preventDefault();
$(this).parents('.dropdown-menu').find('li').removeClass('active');
$(this).parent('li').addClass('active');
@@ -46,4 +46,3 @@ $(document).ready(function () {
});
}
});
-});
diff --git a/addons/portal/views/assets.xml b/addons/portal/views/assets.xml
index a40533c7876..af6569e150f 100644
--- a/addons/portal/views/assets.xml
+++ b/addons/portal/views/assets.xml
@@ -1,15 +1,14 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/addons/survey/static/src/js/survey.js b/addons/survey/static/src/js/survey.js
index d645411c694..b923235fd5c 100644
--- a/addons/survey/static/src/js/survey.js
+++ b/addons/survey/static/src/js/survey.js
@@ -1,6 +1,7 @@
odoo.define('survey.survey', function (require) {
'use strict';
+require('web.dom_ready');
var core = require('web.core');
var time = require('web.time');
var ajax = require('web.ajax');
diff --git a/addons/survey/static/src/js/survey_result.js b/addons/survey/static/src/js/survey_result.js
index 8f8ca83efc9..373296ba8ef 100644
--- a/addons/survey/static/src/js/survey_result.js
+++ b/addons/survey/static/src/js/survey_result.js
@@ -1,7 +1,7 @@
odoo.define('survey.result', function (require) {
'use strict';
-var website = require('website.website');
+require('web.dom_ready');
if(!$('.js_surveyresult').length) {
return $.Deferred().reject("DOM doesn't contain '.js_surveyresult'");
diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js
index 8ad094b0ae8..6650022c486 100644
--- a/addons/web/static/src/js/core/dom.js
+++ b/addons/web/static/src/js/core/dom.js
@@ -1,3 +1,13 @@
+odoo.define('web.dom_ready', function (require) {
+'use strict';
+
+var def = $.Deferred();
+$(def.resolve.bind(def));
+return def;
+});
+
+//==============================================================================
+
odoo.define('web.dom', function (require) {
"use strict";
@@ -245,5 +255,4 @@ return {
return $container;
},
};
-
});
diff --git a/addons/web/static/src/js/report/report.editor.js b/addons/web/static/src/js/report/report.editor.js
index a36b1596ca2..0059e39be02 100644
--- a/addons/web/static/src/js/report/report.editor.js
+++ b/addons/web/static/src/js/report/report.editor.js
@@ -1,7 +1,6 @@
odoo.define('report.editor', function (require) {
'use strict';
-var ajax = require('web.ajax');
var core = require('web.core');
var utils = require('report.utils');
var editor = require('web_editor.editor');
@@ -16,23 +15,18 @@ var trusted_origin = utils.build_origin(trusted_protocol, trusted_host);
if (window.self !== window.top) {
$(document.body).addClass('o_in_iframe'); // in order to apply css rules
- // As `reload` is called after `save` and `cancel`, we nullify this function
- // if the editor in order to be able to chain some deferred to `save` and
- // `cancel`.
- editor.reload = function () {
- return $.when();
- };
-
// And now we chain some deferred to `save` and `cancel` in order to inform
// the report's client action that the actions are done.
editor.Class.include({
save: function () {
- return this._super.apply(this, arguments).then(function () {
+ // Force to not reload
+ return this._super(false).then(function () {
window.parent.postMessage('report.editor:save_ok', trusted_origin);
});
},
cancel: function () {
- return this._super.apply(this, arguments).then(function () {
+ // Force to not reload
+ return this._super(false).then(function () {
window.parent.postMessage('report.editor:discard_ok', trusted_origin);
});
},
@@ -54,12 +48,12 @@ if (window.self !== window.top) {
return;
}
- switch(message) {
+ switch (message) {
case 'report.editor:ask_save':
- editor.editor_bar.save();
+ core.bus.trigger('editor_save_request');
break;
case 'report.editor:ask_discard':
- editor.editor_bar.cancel();
+ core.bus.trigger('editor_discard_request');
break;
default:
}
@@ -91,16 +85,19 @@ if (window.self !== window.top) {
}
options.registry.many2one.include({
- select_record: function (li) {
- var self = this;
- this._super(li);
- if (this.$target.data('oe-field') === "partner_id") {
- var $img = $('.header .row img:first');
- var css = window.getComputedStyle($img[0]);
- $img.css("max-height", css.height+'px');
- $img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
- setTimeout(function () { $img.removeClass('o_dirty'); },0);
+ _selectRecord: function ($li) {
+ this._super.apply(this, arguments);
+ if (this.$target.data('oe-field') !== 'partner_id') {
+ return;
}
+
+ var $img = $('.header .row img:first');
+ var css = window.getComputedStyle($img[0]);
+ $img.css('max-height', css.height+'px');
+ $img.attr('src', '/web/image/res.partner/' + this.ID + '/image');
+ _.defer(function () {
+ $img.removeClass('o_dirty');
+ });
}
});
diff --git a/addons/web/static/src/less/report.editor.less b/addons/web/static/src/less/report.editor.less
index 206ddb87037..544787ff6b2 100644
--- a/addons/web/static/src/less/report.editor.less
+++ b/addons/web/static/src/less/report.editor.less
@@ -14,7 +14,7 @@
body.o_in_iframe {
margin-top: 40px!important;
- div.o_css_editor {
+ div#web_editor-top-edit {
> form {
display: none;
}
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 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());
+ },
+};
+});
+
+//==============================================================================
+
+odoo.define('web_editor.context', function (require) {
+'use strict';
+
+function getContext(context) {
var html = document.documentElement;
return _.extend({
- 'lang': (html.getAttribute('lang') || '').replace('-', '_'),
- }, dict);
+ 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 || {});
+}
+
+return {
+ get: getContext,
+ getExtra: getExtraContext,
};
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-var data = {
- 'get_context': get_context,
-};
-
-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;
-// todo: remove and load a bundle of translated templates
-data.ready = function () {
- return $.when(dom_ready, session.is_bound, ajax.loadXML());
-};
+//==============================================================================
-return dom_ready;
+odoo.define('web_editor.ready', function (require) {
+'use strict';
+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"));
- }
- });
-
- $(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();
- }
- }
- });
- },
- rte_changed: function () {},
- save: function () {
- return this.rte.save().then(function () {
- editor.reload();
+ return $.when.apply($, defs).then(function () {
+ self.trigger_up('edit_mode');
});
},
/**
- * Saves an RTE content, which always corresponds to a view section (?).
+ * @override
*/
- save_without_reload: function () {
- return this.rte.save();
+ 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);
},
- 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)
+
+ //--------------------------------------------------------------------------
+ // 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", d, d.reject);
- }).then(function () {
- window.onbeforeunload = null;
- editor.reload();
+ confirm.on('closed', def, def.reject);
+ }
+ return def.then(function () {
+ if (reload !== false) {
+ window.onbeforeunload = null;
+ return self._reload();
+ }
});
},
+ /**
+ * 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 () {
+ if (reload !== false) {
+ return self._reload();
+ }
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Reloads the page in non-editable mode, with the right scrolling.
+ *
+ * @private
+ * @returns {Deferred} (never resolved, the page is reloading anyway)
+ */
+ _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();
+ },
+
+ //--------------------------------------------------------------------------
+ // 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;
- }
+ this._getConfig = getConfig || this._getDefaultConfig;
+
+ weWidgets.computeFonts();
},
/**
- * 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);
- },
- /**
- * 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);
- $('.o_not_editable').attr("contentEditable", false);
+ $(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);
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,113 +316,25 @@ 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);
},
- save: function (context) {
- var self = this;
-
- this.__saved = {}; // list of allready saved views and data
-
- var defs = $('.o_dirty')
- .removeAttr('contentEditable')
- .removeClass('o_dirty oe_carlos_danger o_is_inline_editable')
- .map(function () {
- var $el = $(this);
-
- $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,
- });
- });
- });
- }).get();
-
- return $.when.apply(null, defs).then(function () {
- window.onbeforeunload = null;
- }, function (failed) {
- // If there were errors, re-enable edition
- self.stop();
- 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 = $("").text(failure.error.message).html();
- var data = msg.substring(3,msg.length-2).split(/', u'/);
- failure.error.message = '' + data[0] + '' + 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');
- });
- });
- },
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
/**
- * Get HTML cloned element with text nodes escaped for XML storage
+ * Stops the RTE.
*/
- 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) {
- this.nodeValue = $('').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', {
- model: 'ir.ui.view',
- method: 'save',
- args: [
- $el.data('oe-id'),
- markup,
- $el.data('oe-xpath') || null,
- _.omit(context || base.get_context(), 'lang')
- ],
- });
- },
-
- stop: function () {
+ cancel: function () {
if (this.$last) {
this.$last.destroy();
this.$last = null;
@@ -455,47 +342,299 @@ var RTE = Widget.extend({
$.fn.carousel = this.init_bootstrap_carousel;
- $(document).off(".rte");
+ 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");
+ $('.o_not_editable').removeAttr('contentEditable');
$(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
$(document).tooltip('destroy');
- $('body').removeClass("editor_enable");
+ $('body').removeClass('editor_enable');
this.trigger('rte:stop');
},
-
- cancel: function () {
- $('.o_editable').each(function () {
- var $node = $(this);
- $node.data('initInnerHTML', $node.html());
- });
- this.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');
},
-
- onClick: function (e) {
- e.preventDefault();
+ /**
+ * 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;
- // 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);
+ var $dirty = $('.o_dirty');
+ $dirty
+ .removeAttr('contentEditable')
+ .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');
+ }
});
+
+ // 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 = $('', {text: response.data.message}).html();
+ var data = msg.substring(3, msg.length -2).split(/', u'/);
+ response.data.message = '' + data[0] + '' + data[1];
+ }
+ $('.o_editable.' + id)
+ .removeClass(id)
+ .popover({
+ html: html,
+ trigger: 'hover',
+ content: response.data.message,
+ placement: 'auto top',
+ })
+ .popover('show');
+ });
+ });
+ });
+
+ return $.when.apply($, defs).then(function () {
+ window.onbeforeunload = null;
+ }, function (failed) {
+ // If there were errors, re-enable edition
+ self.cancel();
+ self.start();
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * When the users clicks on an editable element, this function allows to add
+ * external behaviors.
+ *
+ * @private
+ * @param {jQuery} $editable
+ */
+ _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) {
+ this.nodeValue = $('').text(this.nodeValue).html();
+ }
+ });
+ return escaped_el;
+ },
+ /**
+ * 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'),
+ this._getEscapedElement($el).prop('outerHTML'),
+ $el.data('oe-xpath') || null,
+ withLang ? context : _.omit(context, 'lang')
+ ],
+ });
+ },
- // activate editor
- onMousedown: function (event) {
- var $target = $(event.target);
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when any editable element is clicked -> Prevents default browser
+ * action for the element.
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onClick: function (e) {
+ e.preventDefault();
+ },
+ /**
+ * 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();
+ },
+ /**
+ * 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();
+ },
+ /**
+ * 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();
+ },
+ /**
+ * 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,351 +1,1112 @@
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;
-
- this._super.apply(this, arguments);
- if(!$('#oe_manipulators').length) {
- $("", {id: "oe_manipulators", 'class': "o_css_editor"}).appendTo('body');
- }
- this.$active_snipped_id = false;
- this.snippets = [];
-
- data.instance = this;
+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);
+ 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();
- });
-
- var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
- $.summernote.core.dom.isNotBreakable = function (node) {
- return _isNotBreakable(node) || $(node).is('div') || data.globalSelector.is($(node));
- };
-
- $(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);
- }
- });
-
- $(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');
- }
- });
- $(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();
- }
- });
- },
-
- _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)";
-
- 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);
- }
- };
+ // 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 {
- 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;
- });
+ this.dropped = false;
+ this.$el.draggable({
+ greedy: true,
+ appendTo: 'body',
+ cursor: 'move',
+ handle: '.oe_snippet_move',
+ cursorAt: {
+ left: 18,
+ top: 14
},
- 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));
+ 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;
},
- is: function ($from) {
- return $from.is(selector);
- }
- };
+ start: _.bind(self._onDragAndDropStart, self),
+ stop: _.bind(self._onDragAndDropStop, self)
+ });
+ }
+
+ return $.when.apply($, defs);
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ this._super.apply(this, arguments);
+ this.$target.removeData('snippet-editor');
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * 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();
}
},
-
- fetch_snippet_templates: function () {
- var self = this;
- var url = this._get_snippet_url();
- if (!url || !url.length) {
- this.$el.detach();
+ /**
+ * Notifies all the associated snippet options that the template which
+ * contains the snippet is about to be saved.
+ */
+ cleanForSave: function () {
+ if (this.isDestroyed()) {
return;
}
- 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);
+ _.each(this.styles, function (option) {
+ option.cleanForSave();
});
},
- compute_snippet_templates: function (html) {
- var self = this;
- var $html = $(html);
- var $scroll = $html.siblings("#o_scroll");
+ /**
+ * 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);
- if (!$scroll.length) {
- throw new Error("Wrong snippets xml definition");
+ // 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; });
+ }
+
+ 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;
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // 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');
+ }
+ if (this.$target.parent('.row').length) {
+ return _t("Column");
+ }
+ return _t("Block");
+ },
+ /**
+ * Instantiates the snippet's options.
+ *
+ * @private
+ */
+ _initializeOptions: function () {
+ var self = this;
+ var $styles = this.$el.find('.oe_options');
+ var $ul = $styles.find('ul:first');
+ this.styles = {};
+ this.selectorSiblings = [];
+ this.selectorChildren = [];
+
+ var i = 0;
+ $ul.append($('', {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($('', {class: 'divider'}));
+
+ 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($('', {class: 'dropdown-header o_parent_editor_header', text: parentEditor._getName()}).data('editor', parentEditor));
+ break;
+ }
+ }
+ }
+ });
+
+ if (!this.selectorSiblings.length && !this.selectorChildren.length) {
+ this.$el.find('.oe_snippet_move, .oe_snippet_clone').addClass('hidden');
}
- // 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
+ this.$el.find('[data-toggle="dropdown"]').dropdown();
- self.templateOptions = [];
- var selector = [];
- var $styles = $html.find("[data-js], [data-selector]");
+ 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();
+ }
+ },
+ });
+
+ var $parent = this.$target.parent();
+ this.$target.find('*').andSelf().tooltip('destroy');
+ this.$target.remove();
+ this.$el.remove();
+
+ 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);
+ }
+ }
+
+ 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();
+ });
+ }
+ }
+
+ // clean editor if they are image or table in deleted content
+ $('.note-control-selection').hide();
+ $('.o_table_handler').remove();
+
+ this.trigger_up('snippet_removed');
+ this.destroy();
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the 'clone' button is clicked.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onCloneClick: function (ev) {
+ ev.preventDefault();
+ var $clone = this.$target.clone(false);
+
+ 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)),
+ });
+ }
+ },
+ });
+ },
+ /**
+ * 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('');
+ self.$target.detach();
+ self.$el.addClass('hidden');
+
+ 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 $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,
+ });
+
+ $('body').addClass('move-important');
+
+ $('.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;
+
+ $('.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.$el.removeClass('hidden');
+ $('body').removeClass('move-important');
+ $clone.remove();
+
+ if (this.dropped) {
+ this.trigger_up('request_history_undo_record', {$target: this.$target});
+
+ if (prev) {
+ this.$target.insertAfter(prev);
+ } else if (next) {
+ this.$target.insertBefore(next);
+ } else {
+ $parent.prepend(this.$target);
+ }
+
+ for (var i in this.styles) {
+ this.styles[i].onMove();
+ }
+ }
+
+ self.trigger_up('drag_and_drop_stop', {
+ $snippet: self.$target,
+ });
+ },
+ /**
+ * 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);
+ }
+ });
+ }
+ // 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);
+ }
+ }
+ },
+ /**
+ * 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,
+ });
+ },
+ /**
+ * 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();
+ },
+});
+
+/**
+ * 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',
+ },
+
+ /**
+ * @constructor
+ */
+ init: function (parent, $editable) {
+ this._super.apply(this, arguments);
+
+ 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 = $('', {
+ 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 = $(srcElement);
+ if ($target.closest('.oe_overlay, .note-popover').length) {
+ return;
+ }
+ self._activateSnippet($target);
+ });
+
+ core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet);
+ core.bus.on('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
+
+ // Some summernote customization
+ var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
+ $.summernote.core.dom.isNotBreakable = function (node) {
+ return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node));
+ };
+
+ // 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');
+ }
+ });
+
+ // 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);
+ },
+
+ //--------------------------------------------------------------------------
+ // 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');
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * 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 = $('', {
+ class: 'oe_drop_zone oe_insert',
+ });
+
+ if ($selectorChildren) {
+ $selectorChildren.each(function () {
+ var $zone = $(this);
+ var css = window.getComputedStyle(this);
+ var float = css.float || css.cssFloat;
+ var $drop = zone_template.clone();
+
+ $zone.append($drop);
+ var node = $drop[0].previousSibling;
+ 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',
+ });
+ } 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'));
+ if (test) {
+ $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));
+ }
+ if (test) {
+ $drop.css({'float': 'none', 'display': 'inline-block'});
+ }
+ });
+
+ // add children near drop zone
+ $selectorSiblings = $(_.uniq(($selectorSiblings || $()).add($selectorChildren.children()).get()));
+ }
+
+ 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) {
+ $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));
+ }
+ $zone.before($drop);
+ }
+ 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));
+ }
+ $zone.after($drop);
+ }
+ });
+ }
+
+ var count;
+ var $zones;
+ do {
+ count = 0;
+ $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.
+ $zones = this.$editable.find('.oe_drop_zone:not(.oe_vertical)');
+ $zones.each(function () {
+ var zone = $(this);
+ var prev = zone.prev();
+ var next = zone.next();
+ // remove consecutive zone
+ if (prev.is('.oe_drop_zone') || next.is('.oe_drop_zone')) {
+ zone.remove();
+ return;
+ }
+ var float_prev = prev.css('float') || 'none';
+ 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')) {
+ zone.remove();
+ } else if (disp_prev !== null && disp_next !== null
+ && disp_prev !== 'block' && disp_next !== 'block') {
+ zone.remove();
+ }
+ });
+ },
+ /**
+ * 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();
+ }
+ }
+ if (this.$activeSnippet) {
+ if (this.$activeSnippet.data('snippet-editor')) {
+ this.$activeSnippet.data('snippet-editor').toggleFocus(false);
+ }
+ this.$activeSnippet = false;
+ }
+ 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 $.when();
+ },
+ /**
+ * 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();
+ }
+ },
+ /**
+ * 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;
+ 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);
+ },
+ /**
+ * 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;
+
+ // 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;
+ }
+
+ // (Re)join the subselectors
+ var selector =_.map(selectorList, function (s) {
+ return s + selectorConditions;
+ }).join(', ');
+
+ // 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;
+ },
+ /**
+ * 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');
+
+ $html.find('[data-oe-type="snippet"]').each(function () {
+ $(this).children()
+ .attr('data-oe-type', 'snippet')
+ .attr('data-oe-thumbnail', $(this).data('oe-thumbnail'));
+ });
+
+ this.templateOptions = [];
+ var selectors = [];
+ var $styles = $html.find('[data-selector]');
$styles.each(function () {
var $style = $(this);
- var no_check = $style.data('no-check');
+ 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': $style.data('selector'),
- 'selector': self._add_check_selector($style.data('selector'), no_check),
+ 'option': option_id,
+ 'base_selector': selector,
+ 'base_exclude': exclude,
+ 'selector': self._computeSelectorFunctions(selector, exclude, noCheck),
'$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()
+ '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);
- selector.push(option.selector);
+ selectors.push(option.selector);
});
+ $styles.addClass('hidden');
- $styles.addClass("hidden");
- data.globalSelector.closest = function ($from) {
+ globalSelector.closest = function ($from) {
var $temp;
var $target;
- var len = selector.length;
- for (var i = 0; i'+
- ''+
- ''+
- '');
- $div.find('span').text($snippet.attr("name"));
- $snippet.prepend($div);
+ var name = $snippet.attr('name');
+ var $sbody = $snippet.children(':not(.oe_snippet_thumbnail)').addClass('oe_snippet_body');
- // 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 + ')');
+ // 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('.');
}
- // end
+ $(snippetClasses).data('name', name);
+ $sbody.data('name', name);
+ }
- var moduleID = $snippet.data('moduleId');
- if (moduleID) {
- $snippet.addClass('o_snippet_install');
- var $installBtn = $('', {
- 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);
- }
+ // Create the thumbnail
+ if ($snippet.find('.oe_snippet_thumbnail').length) {
+ return; // Compatibility with elements which do not use 't-snippet'
}
- if (!$snippet.data("selector")) {
- $("> *:not(.oe_snippet_thumbnail)", this).addClass('oe_snippet_body');
+ var $thumbnail = $(_.str.sprintf(
+ '
'+
+ ''+
+ '%s'+
+ '
',
+ $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 = $('', {
+ 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);
}
- number++;
})
.not('[data-module-id]');
- // hide scroll if no snippets defined
- if (!number) {
+ // Hide scroll if no snippets defined
+ if (!this.$snippets.length) {
this.$el.detach();
}
- $("body").toggleClass("editor_has_snippets", !!number);
+ $('body').toggleClass('editor_has_snippets', this.$snippets.length > 0);
- // 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");
- });
+ // Register the text nodes that needs to be auto-selected on click
+ this._registerDefaultTexts();
- // clean t-oe
- $html.find('[data-oe-model], [data-oe-type]').each(function () {
- for (var k=0; k}
+ */
+ _createSnippetEditor: function ($snippet) {
+ var self = this;
+ var snippetEditor = $snippet.data('snippet-editor');
+ if (snippetEditor) {
+ return $.when(snippetEditor);
}
- $in.find("*").addBack()
- .contents()
- .filter(function () {
- return this.nodeType === 3 && this.textContent.match(/\S/);
- }).parent().addClass("o_default_snippet_text");
- },
-
- cover_target: function ($el, $target) {
- if ($el.data('not-cover_target')) {
- return;
+ var def;
+ var $parent = globalSelector.closest($snippet.parent());
+ if ($parent.length) {
+ def = this._createSnippetEditor($parent);
}
- 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),
- });
- $el.toggleClass('o_top_cover', pos.top <= (this.$('#o_scroll').position().top + 15));
+ 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;
+ });
},
-
- show_blocks: function () {
+ /**
+ * 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");
+ 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;
+ if (check || !($snippet_body.is(option.base_selector) && !$snippet_body.is(option.base_exclude))) return;
cache[k] = cache[k] || {
'drop-near': option['drop-near'] ? option['drop-near'].all().length : 0,
@@ -354,107 +1115,29 @@ data.Class = Widget.extend({
check = (cache[k]['drop-near'] || cache[k]['drop-in']);
});
- $snippet.toggleClass("disable", !check);
+ $snippet.toggleClass('o_disabled', !check);
});
},
- bind_snippet_click_editor: function () {
+ /**
+ * 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 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;
-
- setTimeout(function () {snipped_event_flag = false;}, 0);
- var $target = $(srcElement);
-
- if ($target.closest(".oe_overlay, .note-popover").length) {
- return;
- }
-
- if (!data.globalSelector.is($target)) {
- $target = data.globalSelector.closest($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();
- }
- }
- },
- snippet_focus: function ($snippet) {
- if ($snippet) {
- if ($snippet.data("snippet-editor")) {
- $snippet.data("snippet-editor").on_focus();
- }
- }
- },
- 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();
- });
- }
- }
- 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);
- }
- },
- 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));
- }
- },
-
- // 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 $tumb = $snippets.find('.oe_snippet_thumbnail_img:first');
var left = $tumb.outerWidth()/2;
var top = $tumb.outerHeight()/2;
var $toInsert, dropped, $snippet;
@@ -464,43 +1147,42 @@ data.Class = Widget.extend({
helper: 'clone',
zIndex: '1000',
appendTo: 'body',
- cursor: "move",
- handle: ".oe_snippet_thumbnail",
+ cursor: 'move',
+ handle: '.oe_snippet_thumbnail',
distance: 30,
cursorAt: {
- 'left': left,
- 'top': top
+ 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 $selectorSiblings = $();
+ var $selectorChildren = $();
var temp = self.templateOptions;
for (var k in temp) {
- if ($base_body.is(temp[k].base_selector)) {
+ if ($base_body.is(temp[k].base_selector) && !$base_body.is(temp[k].base_exclude)) {
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 (!$selectorSiblings) $selectorSiblings = temp[k]['drop-near'].all();
+ else $selectorSiblings = $selectorSiblings.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());
+ if (!$selectorChildren) $selectorChildren = temp[k]['drop-in'].all();
+ else $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
}
}
}
- $toInsert = $base_body.clone().data("name", $snippet.find(".oe_snippet_thumbnail_title").text());
+ $toInsert = $base_body.clone().data('name', $base_body.data('name'));
- 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");
+ 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;
}
- self.make_active(false);
- self.activate_insertion_zones($selector_siblings, $selector_children);
+ self._activateSnippet(false);
+ self._activateInsertionZones($selectorSiblings, $selectorChildren);
$('.oe_drop_zone').droppable({
over: function () {
@@ -509,7 +1191,7 @@ data.Class = Widget.extend({
},
out: function () {
var prev = $toInsert.prev();
- if(this === prev[0]) {
+ if (this === prev[0]) {
dropped = false;
$toInsert.detach();
$(this).removeClass('hidden');
@@ -531,624 +1213,148 @@ data.Class = Widget.extend({
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;
if (prev) {
$toInsert.detach();
- rte.historyRecordUndo($(prev));
+ self.trigger_up('request_history_undo_record', {$target: $(prev)});
$toInsert.insertAfter(prev);
} else if (next) {
$toInsert.detach();
- rte.historyRecordUndo($(next));
+ self.trigger_up('request_history_undo_record', {$target: $(next)});
$toInsert.insertBefore(next);
} else {
var $parent = $toInsert.parent();
$toInsert.detach();
- rte.historyRecordUndo($parent);
+ self.trigger_up('request_history_undo_record', {$target: $parent});
$parent.prepend($toInsert);
}
- $toInsert.closest(".o_editable").trigger("content_changed");
+ $toInsert.closest('.o_editable').trigger('content_changed');
- var $target = false;
- $target = $toInsert;
+ var $target = $toInsert;
- setTimeout(function () {
- self.$el.trigger('snippet-dropped', $target);
+ _.defer(function () {
+ self.trigger_up('snippet_dropped', {$target: $target});
+ self._disableUndroppableSnippets();
- animation.start(true, $target);
-
- self.call_for_all_snippets($target, function (editor, $snippet) {
- _.defer(function () { editor.drop_and_build_snippet(); });
+ self._callForEachChildSnippet($target, function (editor, $snippet) {
+ _.defer(function () {
+ editor.buildSnippet();
+ });
+ }).then(function () {
+ $target.closest('.o_editable').trigger('content_changed');
+ self._activateSnippet($target);
});
- self.create_snippet_editor($target);
- self.cover_target($target.data('overlay'), $target);
- $target.closest(".o_editable").trigger("content_changed");
-
- self.make_active($target);
- },0);
+ });
} else {
$toInsert.remove();
}
},
});
},
-
- // 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);
- }
- });
- },
-
- // 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();
- },
-
- // 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 = $("");
-
- if ($selector_children) {
- $selector_children.each(function () {
- var $zone = $(this);
- var css = window.getComputedStyle(this);
- var float = css.float || css.cssFloat;
- var $drop = zone_template.clone();
-
- $zone.append($drop);
- var node = $drop[0].previousSibling;
- 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'
- });
- } 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"));
- if (test) {
- $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));
- }
- if (test) {
- $drop.css({'float': 'none', 'display': 'inline-block'});
- }
- });
-
- // add children near drop zone
- $selector_siblings = $(_.uniq(($selector_siblings || $()).add($selector_children.children()).get()));
+ /**
+ * 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');
}
- if ($selector_siblings) {
- $selector_siblings.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) {
- $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));
- }
- $zone.before($drop);
- }
- 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));
- }
- $zone.after($drop);
- }
- });
- }
-
- var count;
- do {
- count = 0;
- $zones = self.$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)');
- $zones.each(function () {
- var zone = $(this);
- var prev = zone.prev();
- var next = zone.next();
- // remove consecutive zone
- if (prev.is('.oe_drop_zone') || next.is('.oe_drop_zone')) {
- zone.remove();
- return;
- }
- var float_prev = prev.css('float') || 'none';
- 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') ) {
- zone.remove();
- }else if( !( disp_prev === null
- || disp_next === null
- || disp_prev === 'block'
- || disp_next === 'block' )) {
- zone.remove();
- }
- });
+ $in.find('*').addBack()
+ .contents()
+ .filter(function () {
+ return this.nodeType === 3 && this.textContent.match(/\S/);
+ }).parent().addClass('o_default_snippet_text');
},
- // 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);
- }
- 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.$target.parent(".row").length) {
- return _t("Column");
- }
- return _t("Block");
- },
-
- // 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)
- });
- },
- _drag_and_drop_after_insert_dropzone: function () {},
- _drag_and_drop_active_drop_zone: function ($zones) {
- 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;
- },
- });
- },
- _drag_and_drop_start: function () {
- var self = this;
- self.buildingBlock.editor_busy = true;
- self.size = {
- width: self.$target.width(),
- height: self.$target.height()
- };
- self.$target.after("");
- 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());
- }
-
- self.buildingBlock.make_active(false);
- self.buildingBlock.activate_insertion_zones($selector_siblings, $selector_children);
-
- $("body").addClass('move-important');
-
- self._drag_and_drop_after_insert_dropzone();
- self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
- },
- _drag_and_drop_stop: function () {
- var self = this;
-
- $(".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();
-
- if (this.dropped) {
- this.buildingBlock.getParent().rte.historyRecordUndo(this.$target);
-
- if (prev) {
- this.$target.insertAfter(prev);
- } else if (next) {
- this.$target.insertBefore(next);
- } else {
- $parent.prepend(this.$target);
- }
-
- for (var i in this.styles) {
- this.styles[i].on_move();
- }
- }
-
- self.buildingBlock.editor_busy = false;
-
- self.init_parent_options();
- _.defer(function () {
- self.buildingBlock.cover_target(self.$target.data('overlay'), self.$target);
- });
- },
-
- 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 = [];
-
- var i = 0;
- $ul.append($("", {"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($("", {"class": "divider"}));
-
- 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($("", {"class": "dropdown-header o_parent_editor_header", text: parentEditor.getName()}).data("editor", parentEditor));
- break;
- }
- }
- }
- });
-
- if (!this.selector_siblings.length && !this.selector_children.length) {
- this.$overlay.find(".oe_snippet_move, .oe_snippet_clone").addClass('hidden');
- }
-
- this.$overlay.find('[data-toggle="dropdown"]').dropdown();
- },
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
/**
- * 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.
+ * Called when a child editor asks for insertion zones to be enabled.
+ *
+ * @private
+ * @param {OdooEvent} ev
*/
- init_parent_options: function () {
- 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);
- }
-
- $button.toggleClass("hidden", $parent.length === 0);
- $button.off("click").on('click', function (event) {
- event.preventDefault();
- _.defer(function () {
- self.buildingBlock.make_active($parent);
- });
- });
+ _onActivateInsertionZones: function (ev) {
+ this._activateInsertionZones(ev.data.$selectorSiblings, ev.data.$selectorChildren);
},
-
- on_clone: function (event) {
- event.preventDefault();
- var $clone = this.$target.clone(false);
-
- this.buildingBlock.getParent().rte.historyRecordUndo(this.$target);
-
- 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)),
- });
- }
- });
- return false;
+ /**
+ * 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);
},
-
- on_remove: function (event) {
- if (event !== undefined) {
- event.preventDefault();
- this.buildingBlock.getParent().rte.historyRecordUndo(this.$target);
- }
-
- this.on_blur();
-
- 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];
-
- var $parent = this.$target.parent();
- this.$target.find("*").andSelf().tooltip("destroy");
- this.$target.remove();
- this.$overlay.remove();
-
- 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);
- }
- }
-
- 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();
- });
- }
- }
-
- // clean editor if they are image or table in deleted content
- $(".note-control-selection").hide();
- $('.o_table_handler').remove();
-
- this.buildingBlock.$el.trigger("snippet-removed");
-
- return false;
+ /**
+ * Called when asked to clean the DOM for save. Should technically not be
+ * used but used by tests.
+ *
+ * @private
+ */
+ _onCleanForSaveDemand: function (ev) {
+ this.cleanForSave();
},
-
- /*
- * 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();
- }
+ /**
+ * Called when a child editor asks to deactivate the current snippet
+ * overlay.
+ *
+ * @private
+ */
+ _onDeactivateSnippet: function () {
+ this._activateSnippet(false);
},
-
- /* on_focus
- * This method is called when the user click inside the snippet in the dom
- */
- on_focus: function () {
- this._on_focus_blur(true);
+ /**
+ * 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);
},
-
- /* 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);
+ /**
+ * 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());
},
-
- _on_focus_blur: function (focus) {
- var do_action = (focus ? _do_action_focus : _do_action_blur);
-
- // 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; });
- }
-
- 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.$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;
- }
+ /**
+ * Called when a snippet is removed -> checks if there is draggable snippets
+ * to enable/disable as the DOM changed.
+ *
+ * @private
+ */
+ _onSnippetRemoved: function () {
+ this._disableUndroppableSnippets();
},
});
-/**
- * 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;
- },
- start: function () {
- this.buildingBlock.insertAfter(this.$el);
- this.rte.editable().find("*").off('mousedown mouseup click');
- return this._super.apply(this, arguments);
- },
- save: function () {
- this.buildingBlock.clean_for_save();
- return this._super.apply(this, arguments);
- },
-});
-
-/**
- * Add the ability the restart the animations
- */
-editor.Class.include({
- start: function () {
- return this._super.apply(this, arguments).then(function () {
- animation.start(true);
- });
- },
-});
-
-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';
+'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 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;
+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',
+ },
+ /**
+ * 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).
+ */
+ preventChildPropagation: false,
/**
- * 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.
+ * The option `$el` is supposed to be the associated 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
*/
- var SnippetOption = Class.extend({
- init: function (BuildingBlock, editor, $target, option_id) {
- this.buildingBlock = BuildingBlock;
- this.editor = editor;
- this.$target = $target;
+ 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.option = option_id;
- var option = this.buildingBlock.templateOptions[option_id];
- this.$el = option.$el.children("li").clone();
- this.data = option.$el.data();
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
- this.$overlay = this.$target.data('overlay') || $();
- },
+ /**
+ * 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 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(' ');
- // helper for this.$target.find
- $: function () {
- return this.$target.find.apply(this.$target, arguments);
- },
-
- _bind_li_menu: function () {
- this.$el.filter("li:hasData").find('a:first')
- .off('mouseenter click')
- .on('mouseenter click', _.bind(_mouse, this));
-
- this.$el
- .off('mouseenter click', "li:hasData a")
- .on('mouseenter click', "li:hasData a", _.bind(_mouse, this));
-
- this.$el.off('mouseleave')
- .on('mouseleave', _.bind(this.reset, this));
-
- this.$el.off('mouseleave', "ul")
- .on('mouseleave', "ul", _.bind(this.reset, this));
-
- this.methodsToReset = [];
-
- function _mouse(event) {
- var $next = $(event.currentTarget).parent();
-
- // 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];
- }
- }
- self.select("reset", $li);
- });
-
- 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 + "'");
- }
- }
- });
- },
-
- /**
- * 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]");
-
- 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]");
-
- var classes = $lis.map(function () {return $(this).data("select_class");}).get().join(" ");
-
- this.$target.removeClass(classes);
- if (value) {
- this.$target.addClass(value);
- }
- },
-
- /**
- * 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_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';
- }
-
- var resize = resize_values[compass];
- if (!resize) return;
-
-
- 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");
- }
- } 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.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;
- }
-
- 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');
- }
- };
-
- 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);
-
- $fixed_size.removeClass("active");
- $auto_size.addClass("active");
-
- 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
- });
- },
-
- _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();
+ 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 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(' ');
- registry["margin-y"] = registry.marginAndResize.extend({
- preventChildPropagation: true,
+ this.$target.removeClass(classes).addClass(activeClasses);
+ if (value && previewMode !== 'reset') {
+ this.$target.toggleClass(value);
+ }
+ },
- 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;
- },
- });
-
- registry.resize = registry.marginAndResize.extend({
- preventChildPropagation: true,
-
- getSize: function () {
- this.grid = this._super();
- this.grid.size = 8;
- return this.grid;
- },
- });
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
/**
- * 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 the helper method to search inside the $target element instead
+ * of the dropdown element.
+ *
+ * @override
*/
- registry.colorpicker = SnippetOption.extend({
- start: function () {
- var self = this;
- var res = this._super.apply(this, arguments);
+ $: 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]);
+ },
- if (!this.$el.find('.colorpicker').length) {
- var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
- var $clpicker = $(qweb.render('web_editor.colorpicker'));
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- // 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);
- }
+ /**
+ * 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;
- var $toggles = $pt.find('.o_colorpicker_section_menu');
- var $tabs = $pt.find('.o_colorpicker_section_tabs');
+ // 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});
+ }
- // Remove excluded palettes
- _.each(excluded, function (exc) {
- $clpicker.find('[data-name="' + exc + '"]').remove();
- });
+ // 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);
- var $sections = $clpicker.find('.o_colorpicker_section');
+ // Call the found method in the right order (parents -> child)
+ _.each(methods.reverse(), function (data) {
+ var $el = $(data[0]);
+ var methods = data[1];
- if ($sections.length > 1) { // Multi-palette layout
- $sections.each(function () {
- var $section = $(this);
- var id = 'o_palette_' + $section.data('name') + _.uniqueId();
-
- var $li = $('')
- .append($('', {href: '#' + id})
- .append($('', {'class': $section.data('iconClass') || '', html: $section.data('iconContent') || ''})));
- $toggles.append($li);
-
- $tabs.append($section.addClass('tab-pane').attr('id', id));
- });
-
- // 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.children('li').eq(pos).addClass('active');
- $palette_def.addClass('active');
- } else {
- $toggles.find('li').first().addClass('active');
- $tabs.find('div').first().addClass('active');
+ _.each(methods, function (value, methodName) {
+ if (self[methodName]) {
+ if (previewMode === true) {
+ self.__methodNames.push(methodName);
}
-
- $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);
+ self[methodName](previewMode, value, $el);
}
+ });
+ });
+ this.__methodNames = _.uniq(this.__methodNames);
- this.$el.find('li').append($pt);
+ 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
+ //--------------------------------------------------------------------------
+
+ /**
+ * 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();
+ },
+});
+
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+/**
+ * The registry object contains the list of available options.
+ */
+var registry = {};
+
+registry.marginAndResize = SnippetOption.extend({
+ preventChildPropagation: true,
+
+ /**
+ * @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';
}
- if (this.$el.data('area')) {
- this.$target = this.$target.find(this.$el.data('area'));
- this.$el.removeData('area').removeAttr('area');
+ 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';
}
- var classes = [];
- this.$el.find(".colorpicker button").each(function () {
- var $color = $(this);
- if (!$color.data("color")) {
+ var resize = resize_values[compass];
+ if (!resize) return;
+
+
+ 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 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._onResize(compass, null, dy);
+ self.trigger_up('cover_update');
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 className = 'bg-' + $color.data('color');
- $color.addClass(className);
- if (self.$target.hasClass(className)) {
- self.color = className;
- $color.addClass("selected");
+ 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;
}
- 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);
- });
+ if (change) {
+ self._onResize(compass, beginClass, current);
+ self.trigger_up('cover_update');
+ self._adaptMarginsPreviews();
+ $handle.addClass('oe_handle_change');
+ }
+ };
- this.$el.find('.note-color-reset').on('click', function () {
- self.$target.removeClass(self.classes);
- $colors.removeClass("selected");
+ 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') {
+ $fixed_size.addClass('active');
+ $auto_size.removeClass('active');
+ } else {
+ 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');
+
+ $fixed_size.removeClass('active');
+ $auto_size.addClass('active');
+
+ return false;
+ });
+ },
+ /**
+ * @override
+ */
+ onFocus : function () {
+ this._updateCursor();
+ this._adaptMarginsPreviews();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
/**
- * The background option is designed to change the background image of a snippet.
+ * Adapts the margin handles to fit the left, top and bottom margins.
+ *
+ * @private
*/
- registry.background = SnippetOption.extend({
- start: function () {
- var res = this._super.apply(this, arguments);
- this.bind_bg_events();
- return res;
- },
+ _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');
- 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");
+ 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);
+ },
+});
+
+/**
+ * Handles the edition of margin-top and margin-bottom.
+ */
+registry['margin-y'] = registry.marginAndResize.extend({
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @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({
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _getSize: function () {
+ this.grid = this._super();
+ this.grid.size = 8;
+ return this.grid;
+ },
+});
+
+/**
+ * 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',
+ }),
+
+ /**
+ * @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(',');
}
- },
- 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") {
+ // 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');
+
+ // Remove excluded palettes
+ _.each(excluded, function (exc) {
+ $clpicker.find('[data-name="' + exc + '"]').remove();
+ });
+
+ var $sections = $clpicker.find('.o_colorpicker_section');
+
+ if ($sections.length > 1) { // Multi-palette layout
+ $sections.each(function () {
+ var $section = $(this);
+ var id = 'o_palette_' + $section.data('name') + _.uniqueId();
+
+ var $li = $('')
+ .append($('', {href: '#' + id})
+ .append($('', {'class': $section.data('iconClass') || '', html: $section.data('iconContent') || ''})));
+ $toggles.append($li);
+
+ $tabs.append($section.addClass('tab-pane').attr('id', id));
+ });
+
+ // 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.children('li').eq(pos).addClass('active');
+ $palette_def.addClass('active');
+ } else {
+ $toggles.find('li').first().addClass('active');
+ $tabs.find('div').first().addClass('active');
+ }
+
+ $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);
+ }
+
+ 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;
}
- // Put fake image in the DOM, edit it and use it as background-image
- var $image = $("", {"class": "hidden", "src": value}).appendTo(this.$target);
-
- var _editor = new widget.MediaDialog(null, {}, 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(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);
-
- 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);
+ 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.$el.find('li[data-background]')
- .removeClass("active")
- .filter(function () {
- var bgOption = $(this).data("background");
- return (bgOption === "" && src === "" || bgOption !== "" && src.indexOf(bgOption) >= 0);
- })
- .addClass("active");
- }
- });
+ return res;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
/**
- * The background_position option is designed to change the background image position of a snippet.
+ * Called when a color button is clicked -> confirm the preview.
+ *
+ * @private
+ * @param {Event} ev
*/
- 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;
+ _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');
+ },
+});
- this.previous_state = [this.$target.attr('class'), this.$target.css('background-size'), this.$target.css('background-position')];
+/**
+ * Handles the edition of snippet's background image.
+ */
+registry.background = SnippetOption.extend({
+ /**
+ * @override
+ */
+ start: function () {
+ var res = this._super.apply(this, arguments);
+ this.bindBackgroundEvents();
+ return res;
+ },
- this.bg_pos = self.$target.css('background-position').split(' ');
- this.bg_siz = self.$target.css('background-size').split(' ');
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
- 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();
+ /**
+ * 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 = $('', {class: 'hidden', src: value}).appendTo(this.$target);
- 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);
+ var _editor = new widget.MediaDialog(this, {}, null, $image[0]).open();
+ _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
- 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');
- }
+ _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();
+ });
+ },
- 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('%', ''));
- }
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
- // Focus Point
- self.$focus = self.modal.$(".o_focus_point");
- self.update_pos_information();
+ /**
+ * 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();
+ },
- var img_url = /\(['"]?([^'"]+)['"]?\)/g.exec(self.$target.css('background-image'));
- img_url = (img_url && img_url[1]) || '';
- var $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"));
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- // 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;
+ /**
+ * @override
+ */
+ _setActive: function () {
+ this._super.apply(this, arguments);
- 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;
- });
+ 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);
+ }
- 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();
+ 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'],
+
+ /**
+ * @override
+ */
+ 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');
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * 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');
}
- function clip_value(value) {
+ 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('%', ''));
+ }
+
+ // Focus Point
+ self.$focus = self.modal.$('.o_focus_point');
+ self._updatePosInformation();
+
+ var img_url = /\(['"]?([^'"]+)['"]?\)/g.exec(self.$target.css('background-image'));
+ img_url = (img_url && img_url[1]) || '';
+ var $img = $('', {class: 'img img-responsive', src: img_url});
+ $img.on('load', function () {
+ self._bindImageEvents($img);
+ });
+ $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');
+ });
+ self.modal.$el.on('change', 'input, select', function (e) {
+ self._saveChanges();
+ });
+ self.modal.$('> label > input:checked').trigger('change');
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // 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 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,
- });
- },
- 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],
- });
- }
- },
- 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 */
-
- 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();
- });
-
- // 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 search input
- this.$search.find('input')
- .focus()
- .on('keyup', function (e) {
- self.find_existing($(this).val());
- });
-
- // 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]);
- }
-
- 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);
- });
- });
- } else {
- self.$target.html($(li).data("name"));
- }
-
- setTimeout(function () {
- self.buildingBlock.make_active(false);
- },0);
}
- });
+ },
+ /**
+ * 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],
+ });
+ }
+ },
+ /**
+ * 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();
- return {
- Class: SnippetOption,
- registry: registry,
- };
+ 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();
+ });
+ }
+ },0);
+
+ // bind search input
+ this.$search.find('input')
+ .focus()
+ .on('keyup', function (e) {
+ self._findExisting($(this).val());
+ });
+
+ // bind result
+ this.$ul.on('click', 'li:not(:first) a', function (e) {
+ self._selectRecord($(e.currentTarget));
+ });
+
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ onFocus: function () {
+ this.$target.attr('contentEditable', 'false');
+ this._clear();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * 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 {
+ domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]);
+ }
+ } else {
+ domain.push(['id', '=', name]);
+ }
+
+ 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'));
+ }
+
+ _.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 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 = $(" ")[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(" ");
- } 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; i0 && (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 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($("", {
- "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($('', {
+ 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($("", {
- "class": $img.data("class"),
- "style": $img.data("style")
+ $img.replaceWith($('', {
+ 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 = $('').appendTo("body");
+ var $c = $('').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({
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * 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')) {
- var translation_content = this.getEscapedElement($el).html();
-
- return ajax.jsonRpc('/web/dataset/call', 'call', {
+ 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 = $("", {"class": "form-group"}).appendTo(this.$el);
+ var $group = $('', {class: 'form-group'}).appendTo(this.$el);
_.each(this.translation, function (node, attr) {
var $node = $(node);
var $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 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;
},
- getTranlationObject: function (node) {
+ /**
+ * 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');
+ },
+ };
+ },
+ /**
+ * @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);
- },
- cancel: function () {
- var self = this;
- this.rte.cancel();
- this.$target.each(function () {
- $(this).html(self.getTranlationObject(this).value);
- });
- this.unarkTranslatableNode();
- this.trigger("cancel");
- this.$el.hide();
- window.onbeforeunload = null;
- },
- destroy: function () {
- this.cancel();
- this.$el.remove();
- this._super.apply(this, arguments);
- },
- 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");
- }
- };
- }
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the "cancel" button is clicked -> undo changes and leaves
+ * edition.
+ *
+ * @private
+ */
+ _onCancelClick: function () {
+ this._cancel();
+ },
+ /**
+ * 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 element which might have been inserted because of copy-paste
+ var $p = $(this);
+ $p.after($p.html()).remove();
+ });
+ var trans = this._getTranlationObject($node[0]);
+ $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
+ },
+ /**
+ * Called when the "save" button is clicked -> saves the translations.
+ *
+ * @private
+ */
+ _onSaveClick: function () {
+ this._save();
+ },
});
-
-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(/<span data-oe-model.+?>(.+?)<\/span>/, '\$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);
+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')) {
+ if (value !== fields_values[field_name]) {
+ rte.history.recordUndo($editable);
}
+ core.bus.trigger('deactivate_snippet');
+ }
- $editable.html(value);
+ $editable.html(value);
- if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
- $editable.trigger("content_changed");
- }
+ 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'+
- '\n'+
- '
\n'+
- '
\n'+
- '
\n'+
- '
Batnae municipium in Anthemusia
\n'+
- '
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.
\n'+
- '
Quam quidem partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.
\n'+
- '
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.
';
- 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'+
+ '\n'+
+ '
\n'+
+ '
\n'+
+ '
\n'+
+ '
Batnae municipium in Anthemusia
\n'+
+ '
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.
\n'+
+ '
Quam quidem partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.
\n'+
+ '
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.