summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js
diff options
context:
space:
mode:
authorChristophe Matthieu <chm@odoo.com>2015-07-07 16:02:53 +0200
committerChristophe Matthieu <chm@odoo.com>2015-07-10 17:00:11 +0200
commit8c559044a5e1be111dcb577ad7e5ab462c1002a0 (patch)
treeba1602e15b2bca38babd6cca63a41da1d05b4321 /addons/web_editor/static/src/js
parenta616c57c609da0e5e56835dcb45e1c05590d25b8 (diff)
[IMP] web_editor: split website into web_editor (to use editor in backend for html field) and website
Diffstat (limited to 'addons/web_editor/static/src/js')
-rw-r--r--addons/web_editor/static/src/js/backend.js322
-rw-r--r--addons/web_editor/static/src/js/base.js85
-rw-r--r--addons/web_editor/static/src/js/editor.js164
-rw-r--r--addons/web_editor/static/src/js/iframe.js78
-rw-r--r--addons/web_editor/static/src/js/inline.js77
-rw-r--r--addons/web_editor/static/src/js/rte.js597
-rw-r--r--addons/web_editor/static/src/js/rte.summernote.js781
-rw-r--r--addons/web_editor/static/src/js/snippets.animation.js82
-rw-r--r--addons/web_editor/static/src/js/snippets.editor.js1137
-rw-r--r--addons/web_editor/static/src/js/snippets.options.js534
-rw-r--r--addons/web_editor/static/src/js/summernote.js2192
-rw-r--r--addons/web_editor/static/src/js/tour.rte.js398
-rw-r--r--addons/web_editor/static/src/js/transcoder.js172
-rw-r--r--addons/web_editor/static/src/js/translator.js214
-rw-r--r--addons/web_editor/static/src/js/widgets.js1069
15 files changed, 7902 insertions, 0 deletions
diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js
new file mode 100644
index 00000000000..5725cfc39f6
--- /dev/null
+++ b/addons/web_editor/static/src/js/backend.js
@@ -0,0 +1,322 @@
+odoo.define('web_editor.backend', function (require) {
+'use strict';
+
+var core = require('web.core');
+var session = require('web.session');
+var Model = require('web.DataModel');
+var common = require('web.form_common');
+var base = require('web_editor.base');
+var editor = require('web_editor.editor');
+var summernote = require('web_editor.summernote');
+var transcoder = require('web_editor.transcoder');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+/**
+ * FieldTextHtml Widget
+ * Intended for FieldText widgets meant to display HTML content. This
+ * widget will instantiate an iframe with the editor summernote improved by odoo
+ */
+
+var widget = common.AbstractField.extend(common.ReinitializeFieldMixin);
+
+var FieldTextHtmlSimple = widget.extend({
+ template: 'web_editor.FieldTextHtmlSimple',
+ _config: function () {
+ var self = this;
+ return {
+ 'focus': false,
+ 'height': 180,
+ 'toolbar': [
+ ['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 (value) {
+ self.internal_set_value(value);
+ self.trigger('changed_value');
+ }
+ };
+ },
+ initialize_content: function() {
+ var self = this;
+ this.$textarea = this.$("textarea").val(this.get('value') || "<p><br/></p>");
+
+ if (this.get("effective_readonly")) {
+ this.$textarea.hide().after('<div class="note-editable"/>');
+ } else {
+ this.$textarea.summernote(this._config());
+
+ if (this.field.translate && this.view) {
+ $(QWeb.render('web_editor.FieldTextHtml.button.translate', this))
+ .appendTo(this.$('.note-toolbar'))
+ .on('click', this.on_translate);
+ }
+
+ var reset = _.bind(this.reset_history, this);
+ this.view.on('load_record', this, reset);
+ setTimeout(reset, 0);
+ }
+ this.$content = this.$('.note-editable:first');
+
+ $(".oe-view-manager-content").on("scroll", function () {
+ $('.o_table_handler').remove();
+ });
+ this._super();
+ },
+ reset_history: function () {
+ var history = this.$content.data('NoteHistory');
+ if (history) {
+ history.reset();
+ self.$('.note-toolbar').find('button[data-event="undo"]').attr('disabled', true);
+ }
+ },
+ text_to_html: function (text) {
+ var value = text || "";
+ if (value.match(/^\s*$/)) {
+ value = '<p><br/></p>';
+ } else {
+ value = "<p>"+value.split(/<br\/?>/).join("</p><p>")+"</p>";
+ value = value.replace('<p><p>', '<p>').replace('</p></p>', '</p>');
+ }
+ return value;
+ },
+ render_value: function() {
+ var value = this.get('value');
+ this.$textarea.val(value || '');
+ this.$content.html(this.text_to_html(value));
+ this.$content.focusInEnd();
+ var history = this.$content.data('NoteHistory');
+ if (history && history.recordUndo()) {
+ this.$('.note-toolbar').find('button[data-event="undo"]').attr('disabled', false);
+ }
+ },
+ is_false: function() {
+ return !this.get('value') || this.get('value') === "<p><br/></p>" || !this.get('value').match(/\S/);
+ },
+ before_save: function() {
+ if (this.options['style-inline']) {
+ transcoder.class_to_style(this.$content);
+ transcoder.font_to_img(this.$content);
+ this.internal_set_value(this.$content.html());
+ }
+ },
+ destroy_content: function () {
+ $(".oe-view-manager-content").off("scroll");
+ this.$textarea.destroy();
+ this._super();
+ }
+});
+
+var FieldTextHtml = widget.extend({
+ template: 'web_editor.FieldTextHtml',
+ willStart: function () {
+ var self = this;
+ return new Model('res.lang').call("search_read", [[['code', '!=', 'en_US']], ["name", "code"]]).then(function (res) {
+ self.languages = res;
+ });
+ },
+ start: function () {
+ var self = this;
+
+ this.callback = _.uniqueId('FieldTextHtml_');
+ window.odoo[this.callback+"_editor"] = function (EditorBar) {
+ setTimeout(function () {
+ self.on_editor_loaded(EditorBar);
+ },0);
+ };
+ window.odoo[this.callback+"_content"] = function (EditorBar) {
+ self.on_content_loaded();
+ };
+ window.odoo[this.callback+"_updown"] = null;
+ window.odoo[this.callback+"_downup"] = function (value) {
+ self.internal_set_value(value);
+ self.trigger('changed_value');
+ self.resize();
+ };
+
+ // init jqery objects
+ this.$iframe = this.$el.find('iframe');
+ this.document = null;
+ this.$body = $();
+ this.$content = $();
+
+ this.$iframe.css('min-height', 'calc(100vh - 360px)');
+
+ // init resize
+ this.resize = function resize() {
+ if (self.get('effective_readonly')) { return; }
+ if ($("body").hasClass("o_form_FieldTextHtml_fullscreen")) {
+ self.$iframe.css('height', (document.body.clientHeight - self.$iframe.offset().top) + 'px');
+ } else {
+ self.$iframe.css("height", (self.$body.find("#oe_snippets").length ? 500 : 300) + "px");
+ }
+ };
+ $(window).on('resize', self.resize);
+
+ return this._super();
+ },
+ get_url: function (_attr) {
+ var src = this.options.editor_url ? this.options.editor_url+"?" : "/web_editor/field/html?";
+ var datarecord = this.view.get_fields_values();
+
+ var attr = {
+ 'model': this.view.model,
+ 'field': this.name,
+ 'res_id': datarecord.id || '',
+ 'callback': this.callback
+ };
+ _attr = _attr || {};
+
+ if (this.options['style-inline']) {
+ attr.inline_mode = 1;
+ }
+ if (this.options.snippets) {
+ attr.snippets = this.options.snippets;
+ }
+ if (!this.get("effective_readonly")) {
+ attr.enable_editor = 1;
+ }
+ if (this.field.translate) {
+ attr.translatable = 1;
+ }
+ if (session.debug) {
+ attr.debug = 1;
+ }
+
+ attr.lang = attr.enable_editor ? 'en_US' : this.session.user_context.lang;
+
+ for (var k in _attr) {
+ attr[k] = _attr[k];
+ }
+
+ for (var k in attr) {
+ if (attr[k] !== null) {
+ src += "&"+k+"="+(_.isBoolean(attr[k]) ? +attr[k] : attr[k]);
+ }
+ }
+
+ delete datarecord[this.name];
+ src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord));
+
+ return src;
+ },
+ initialize_content: function() {
+ this.$el.closest('.modal-body').css('max-height', 'none');
+ this.$iframe = this.$el.find('iframe');
+ this.document = null;
+ this.$body = $();
+ this.$content = $();
+ this.editor = false;
+ window.odoo[this.callback+"_updown"] = null;
+ this.$iframe.attr("src", this.get_url());
+ },
+ on_content_loaded: function () {
+ var self = this;
+ this.document = this.$iframe.contents()[0];
+ this.$body = $("body", this.document);
+ this.$content = this.$body.find("#editable_area");
+ this.lang = this.$iframe.attr('src').match(/[?&]lang=([^&]+)/);
+ this.lang = this.lang ? this.lang[1] : this.view.dataset.context.lang;
+ this._dirty_flag = false;
+ this.render_value();
+ setTimeout(function () {
+ self.add_button();
+ setTimeout(self.resize,0);
+ }, 0);
+ },
+ on_editor_loaded: function (EditorBar) {
+ var self = this;
+ this.editor = EditorBar;
+ if (this.get('value') && window.odoo[self.callback+"_updown"] && !(this.$content.html()||"").length) {
+ this.render_value();
+ }
+ setTimeout(function () {
+ setTimeout(self.resize,0);
+ }, 0);
+ },
+ add_button: function () {
+ var self = this;
+ var $to = this.$body.find("#web_editor-top-edit, #wrapwrap").first();
+
+ $(QWeb.render('web_editor.FieldTextHtml.translate', {'widget': this}))
+ .appendTo($to)
+ .on('change', 'select', function () {
+ var lang = $(this).val();
+ var edit = !self.get("effective_readonly");
+ var trans = lang !== 'en_US';
+ self.$iframe.attr("src", self.get_url({
+ 'edit_translations': edit && trans,
+ 'enable_editor': edit && !trans,
+ 'lang': lang
+ }));
+ });
+
+ $(QWeb.render('web_editor.FieldTextHtml.fullscreen'))
+ .appendTo($to)
+ .on('click', '.o_fullscreen', function () {
+ $("body").toggleClass("o_form_FieldTextHtml_fullscreen");
+ var full = $("body").hasClass("o_form_FieldTextHtml_fullscreen");
+ self.$iframe.parents().toggleClass('o_form_fullscreen_ancestor', full);
+ self.resize();
+ });
+
+ this.$body.on('click', '[data-action="cancel"]', function (event) {
+ event.preventDefault();
+ self.initialize_content();
+ });
+ },
+ render_value: function() {
+ if (this.lang !== this.view.dataset.context.lang || this.$iframe.attr('src').match(/[?&]edit_translations=1/)) {
+ return;
+ }
+ var value = (this.get('value') || "").replace(/^<p[^>]*>(\s*|<br\/?>)<\/p>$/, '');
+ if (!this.$content) {
+ return;
+ }
+ if (!this.get("effective_readonly")) {
+ if(window.odoo[this.callback+"_updown"]) {
+ window.odoo[this.callback+"_updown"](value, this.view.get_fields_values(), this.name);
+ this.resize();
+ }
+ } else {
+ this.$content.html(value);
+ this.$iframe.css("height", (this.$body.height()+20) + "px");
+ }
+ },
+ is_false: function() {
+ return this.get('value') === false || !this.$content.html() || !this.$content.html().match(/\S/);
+ },
+ before_save: function () {
+ if (this.lang !== 'en_US' && this.$body.find('.o_dirty').length) {
+ this.internal_set_value( this.view.datarecord[this.name] );
+ this._dirty_flag = false;
+ return this.editor.save();
+ } else if (this._dirty_flag && this.editor && this.editor.buildingBlock) {
+ this.editor.buildingBlock.clean_for_save();
+ this.internal_set_value( this.$content.html() );
+ }
+ },
+ destroy: function () {
+ $(window).off('resize', self.resize);
+ delete window.odoo[this.callback+"_editor"];
+ delete window.odoo[this.callback+"_content"];
+ delete window.odoo[this.callback+"_updown"];
+ delete window.odoo[this.callback+"_downup"];
+ }
+});
+
+core.form_widget_registry
+ .add('html', FieldTextHtmlSimple)
+ .add('html_frame', FieldTextHtml);
+
+});
diff --git a/addons/web_editor/static/src/js/base.js b/addons/web_editor/static/src/js/base.js
new file mode 100644
index 00000000000..28b35722edd
--- /dev/null
+++ b/addons/web_editor/static/src/js/base.js
@@ -0,0 +1,85 @@
+odoo.define('web_editor.base', function (require) {
+"use strict";
+
+var core = require('web.core');
+var ajax = require('web.ajax');
+var qweb = core.qweb;
+var _t = core._t;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+var get_context = function (dict) {
+ var html = document.documentElement;
+ return _.extend({
+ 'lang': (html.getAttribute('lang') || '').replace('-', '_'),
+ }, dict);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* ----------------------------------------------------
+ Async Ready and Template loading
+ ---------------------------------------------------- */
+var dom_ready = $.Deferred();
+$(document).ready(function () {
+ dom_ready.resolve();
+ // fix for ie
+ if($.fn.placeholder) $('input, textarea').placeholder();
+});
+
+
+// todo: remove and load a bundle of translated templates
+var all_ready;
+var ready = function() {
+ if (!all_ready) {
+ all_ready = $.when(dom_ready, ajax.loadXML()).then(translations);
+ } else if(all_ready.state() === "resolved") { // can add async template
+ all_ready = $.when(dom_ready, ajax.loadXML());
+ }
+ return all_ready;
+};
+
+function translations() {
+ function translate_node(node){
+ if(node.nodeType === 3) { // TEXT_NODE
+ if(node.nodeValue.match(/\S/)){
+ var space = node.nodeValue.match(/^([\s]*)([\s\S]*?)([\s]*)$/);
+ node.nodeValue = space[1] + $.trim(_t(space[2])) + space[3];
+ }
+ }
+ else if(node.nodeType === 1 && node.hasChildNodes()) { // ELEMENT_NODE
+ _.each(node.childNodes, translate_node);
+ }
+ }
+ return ajax.jsonRpc(data.url_translations, 'call', {
+ 'mods': ['web_editor'],
+ 'lang': data.get_context().lang
+ })
+ .then(function(trans) {
+ _t.database.set_bundle(trans);
+ }).then(function () {
+ var keys = _.keys(qweb.templates);
+ for (var i = 0; i < keys.length; i++){
+ translate_node(qweb.templates[keys[i]]);
+ }
+ }).promise();
+}
+
+var 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();
+ }
+};
+
+var data = {
+ 'get_context': get_context,
+ 'url_translations': '/web/webclient/translations',
+ 'dom_ready': dom_ready,
+ 'ready': ready,
+ 'reload': reload
+};
+return data;
+
+});
diff --git a/addons/web_editor/static/src/js/editor.js b/addons/web_editor/static/src/js/editor.js
new file mode 100644
index 00000000000..b09a69e3818
--- /dev/null
+++ b/addons/web_editor/static/src/js/editor.js
@@ -0,0 +1,164 @@
+odoo.define('web_editor.editor', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var Widget = require('web.Widget');
+var core = require('web.core');
+var base = require('web_editor.base');
+var rte = require('web_editor.rte');
+
+var qweb = core.qweb;
+var _t = core._t;
+var editor = {};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+editor.dummy = function () {return true;}; // used for snippets, options...
+
+editor.editable = !!$('html').data('editable');
+
+ajax.loadXML('/web_editor/static/src/xml/editor.xml', qweb);
+
+base.dom_ready.done(function () {
+ $(document).on('click', '.note-editable', function (ev) {
+ ev.preventDefault();
+ });
+
+ $(document).on('submit', '.note-editable form .btn', function (ev) {
+ // Disable form submition in editable mode
+ ev.preventDefault();
+ });
+
+ $(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();
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/* ----- TOP EDITOR BAR FOR ADMIN ---- */
+
+base.ready().then(function () {
+ if (editor.editable && location.search.indexOf("enable_editor") >= 0 && !editor.no_editor) {
+ editor.editor_bar = new editor.Class();
+ editor.editor_bar.prependTo(document.body);
+ }
+});
+
+editor.Class = Widget.extend({
+ template: 'web_editor.editorbar',
+ events: {
+ 'click button[data-action=save]': 'save',
+ 'click a[data-action=cancel]': 'cancel',
+ },
+ 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');
+ });
+ return res;
+ },
+ start: function() {
+ var self = this;
+
+ this.$('button[data-action=save]').prop('disabled', true);
+ $('.dropdown-toggle').dropdown();
+
+ this.display_placeholder();
+
+ this.rte.on('change', this, this.proxy('rte_changed'));
+ this.rte.start();
+
+ var flag = false;
+ window.onbeforeunload = function(event) {
+ if ($('.o_editable.o_dirty').length && !flag) {
+ flag = true;
+ setTimeout(function () {flag=false;},0);
+ return _t('This document is not saved!');
+ }
+ };
+ return this._super();
+ },
+ 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]")
+ .addClass("oe_empty")
+ .attr("data-oe-placeholder", _t("Press The Top-Left Edit Button"));
+
+ 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 () {
+ // todo: use !rte.history.getEditableHasUndo().length (but phantomjs unknow error)
+ this.$('button[data-action=save]').prop('disabled', !$('.o_dirty').length);
+ },
+ save: function () {
+ return this.rte.save().then(function () {
+ editor.reload();
+ });
+ },
+ /**
+ * Saves an RTE content, which always corresponds to a view section (?).
+ */
+ save_without_reload: function () {
+ return this.rte.save();
+ },
+ cancel: function () {
+ new $.Deferred(function (d) {
+ if (!rte.history.getEditableHasUndo().length) {
+ return d.resolve();
+ }
+ var $dialog = $(qweb.render('web_editor.discard')).appendTo(document.body);
+ $dialog.on('click', '.btn-danger', function () {
+ d.resolve();
+ }).on('hidden.bs.modal', function () {
+ d.reject();
+ });
+ d.always(function () {
+ $dialog.remove();
+ });
+ $dialog.modal('show');
+ }).then(function () {
+ window.onbeforeunload = null;
+ editor.reload();
+ });
+ },
+});
+
+return editor;
+
+});
+
diff --git a/addons/web_editor/static/src/js/iframe.js b/addons/web_editor/static/src/js/iframe.js
new file mode 100644
index 00000000000..8b287064b7c
--- /dev/null
+++ b/addons/web_editor/static/src/js/iframe.js
@@ -0,0 +1,78 @@
+odoo.define('web_editor.iframe', function (require) {
+'use strict';
+
+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');
+
+window.top.odoo[callback+"_updown"] = function (value, fields_values, field_name) {
+ var $editable = $("#editable_area");
+ if(value !== $editable.prop("innerHTML")) {
+ if ($('body').hasClass('editor_enable')) {
+ if (value !== fields_values[field_name]) {
+ rte.history.recordUndo($editable);
+ }
+ snippet_editor.instance.make_active(false);
+ }
+
+ $editable.html(value);
+
+ if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
+ $editable.trigger("content_changed");
+ }
+ }
+};
+
+editor.Class.include({
+ start: function () {
+ this.on('rte:start', this, function () {
+ this.$('form').hide();
+
+ if (window.top.odoo[callback+"_editor"]) {
+ window.top.odoo[callback+"_editor"](this);
+ }
+
+ var $editable = $("#editable_area");
+ setTimeout(function () {
+ $($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'));
+ }
+ });
+ });
+
+ this.on("snippets:ready", this, function () {
+ $(window.top).trigger("resize");
+ });
+ return this._super();
+ }
+});
+
+snippet_editor.Class.include({
+ _get_snippet_url: function () {
+ return snippets_url;
+ }
+});
+
+translator.Class.include({
+ start: function () {
+ var self = this;
+ return this._super().then(function () {
+ $('button[data-action=save]').hide();
+ if (window.top.odoo[callback+"_editor"]) {
+ window.top.odoo[callback+"_editor"](self);
+ }
+ });
+ }
+});
+
+});
+
+$(function () {
+ odoo.init();
+});
diff --git a/addons/web_editor/static/src/js/inline.js b/addons/web_editor/static/src/js/inline.js
new file mode 100644
index 00000000000..c5fd2cfd924
--- /dev/null
+++ b/addons/web_editor/static/src/js/inline.js
@@ -0,0 +1,77 @@
+odoo.define('web_editor.inline', function (require) {
+'use strict';
+
+var editor = require('web_editor.editor');
+var rte = require('web_editor.rte');
+var widget = require('web_editor.widget');
+var transcoder = require('web_editor.transcoder');
+var snippet_editor = require('web_editor.snippet.editor');
+
+widget.MediaDialog.include({
+ start: function () {
+ this._super();
+ this.$('[href="#editor-media-video"]').addClass('hidden');
+ }
+});
+
+editor.Class.include({
+ start: function () {
+ var self = this;
+ if (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")) {
+ setTimeout(function () {
+ var node = $("#editable_area *")
+ .filter(function () { return this.textContent.match(/\S|\u00A0/); })
+ .add($("#editable_area"))
+ .last()[0];
+ $.summernote.core.range.create(node, $.summernote.core.dom.nodeLength(node)).select();
+ },0);
+ }
+ });
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+});
+
+snippet_editor.Class.include({
+ start: function () {
+ var self = this;
+ this._super();
+ setTimeout(function () {
+ var $editable = $("#editable_area");
+ transcoder.img_to_font($editable);
+ transcoder.style_to_class($editable);
+ });
+ },
+ clean_for_save: function () {
+ this._super();
+ var $editable = $("#editable_area");
+ transcoder.class_to_style($editable);
+ transcoder.font_to_img($editable);
+ },
+
+});
+
+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);
+ }
+
+ $editable.html(value);
+
+ transcoder.img_to_font($editable);
+ transcoder.style_to_class($editable);
+ } else {
+ $editable.trigger("content_changed");
+ }
+};
+
+}); \ No newline at end of file
diff --git a/addons/web_editor/static/src/js/rte.js b/addons/web_editor/static/src/js/rte.js
new file mode 100644
index 00000000000..c8d5655cbd6
--- /dev/null
+++ b/addons/web_editor/static/src/js/rte.js
@@ -0,0 +1,597 @@
+odoo.define('web_editor.rte', function (require) {
+'use strict';
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var utils = require('web.utils');
+var Widget = require('web.Widget');
+var summernote = require('web_editor.summernote');
+var base = require('web_editor.base');
+
+var _t = core._t;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* 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) {
+ var aUndo = [];
+ var pos = 0;
+
+ this.makeSnap = function (event, rng) {
+ rng = rng || range.create();
+ var elEditable = $(rng.sc).closest('.o_editable')[0];
+ if (!elEditable) {
+ return false;
+ }
+ return {
+ event: event,
+ editable: elEditable,
+ contents: elEditable.innerHTML,
+ bookmark: rng && rng.bookmark(elEditable),
+ scrollTop: $(elEditable).scrollTop()
+ };
+ };
+
+ this.applySnap = function (oSnap) {
+ var $editable = $(oSnap.editable);
+
+ if (!!document.documentMode) {
+ $editable.removeAttr("contentEditable").removeProp("contentEditable");
+ }
+
+ $editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
+ $(".oe_overlay").remove();
+ $(".note-control-selection").hide();
+
+ $editable.trigger("content_changed");
+
+ if (!oSnap.bookmark || oSnap.event === "blur") {
+ return;
+ }
+
+ try {
+ var r = oSnap.editable.innerHTML === "" ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
+ r.select();
+ } catch(e) {
+ console.error(e);
+ return;
+ }
+
+ $(document).trigger("click");
+ $(".o_editable *").filter(function () {
+ var $el = $(this);
+ if($el.data('snippet-editor')) {
+ $el.removeData();
+ }
+ });
+
+
+ setTimeout(function () {
+ var target = dom.isBR(r.sc) ? r.sc.parentNode : dom.node(r.sc);
+ if (!target) {
+ return;
+ }
+
+ $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);
+ target.dispatchEvent(evt);
+
+ $editable.trigger("keyup");
+ },0);
+ };
+
+ this.undo = function () {
+ if (!pos) { return; }
+ var _toSnap = toSnap;
+ if (_toSnap) {
+ this.saveSnap();
+ }
+ if (!aUndo[pos] && (!aUndo[pos] || aUndo[pos].event !== 'undo')) {
+ var temp = this.makeSnap('undo');
+ if (temp && (!pos || temp.contents !== aUndo[pos-1].contents)) {
+ aUndo[pos] = temp;
+ } else {
+ pos--;
+ }
+ } else if (_toSnap) {
+ pos--;
+ }
+ this.applySnap(aUndo[Math.max(--pos,0)]);
+ while (pos && (aUndo[pos].event === "blur" || aUndo[pos].event === "activate")) {
+ this.applySnap(aUndo[--pos]);
+ }
+ };
+ this.hasUndo = function () {
+ 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";
+ });
+ };
+
+ this.getEditableHasUndo = function () {
+ var editable = [];
+ 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") {
+ editable.push(undo.editable);
+ }
+ });
+ return _.uniq(editable);
+ };
+
+ this.redo = function () {
+ if (!aUndo[pos+1]) { return; }
+ this.applySnap(aUndo[++pos]);
+ while (aUndo[pos+1] && (aUndo[pos].event === "blur" || aUndo[pos].event === "activate" || aUndo[pos].event === "undo")) {
+ this.applySnap(aUndo[Math.max(++pos,aUndo.length-1)]);
+ }
+ };
+ this.hasRedo = function () {
+ return aUndo.length > pos+1;
+ };
+
+ var toSnap, split;
+ this.recordUndo = function ($editable, event, internal_history) {
+ var self = this;
+ if (!$editable) {
+ $editable = $(range.create().sc).closest(".o_editable");
+ }
+
+ 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
+ setTimeout(function () {
+ $editable.trigger("content_changed");
+ },0);
+ }
+ }
+
+ if (aUndo[pos]) {
+ pos = Math.min(pos, aUndo.length);
+ aUndo.splice(Math.max(pos,1), aUndo.length);
+ }
+
+ // => make a snap when the user change editable zone (because: don't make snap for each keydown)
+ if (toSnap && (toSnap.split || !event || toSnap.event !== event || toSnap.editable !== $editable[0])) {
+ this.saveSnap();
+ }
+
+ if (pos && aUndo[pos-1].editable !== $editable[0]) {
+ var snap = this.makeSnap('blur', range.create(aUndo[pos-1].editable, 0));
+ pos++;
+ aUndo.push(snap);
+ }
+
+ if (range.create()) {
+ toSnap = self.makeSnap(event);
+ } else {
+ toSnap = false;
+ }
+ };
+
+ this.splitNext = function () {
+ if (toSnap) {
+ toSnap.split = true;
+ }
+ };
+
+ this.saveSnap = function () {
+ if (toSnap) {
+ if (!aUndo[pos]) {
+ pos++;
+ }
+ aUndo.push(toSnap);
+ delete toSnap.split;
+ toSnap = null;
+ }
+ };
+};
+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){
+ while (node) {
+ if (node.className) {
+ if (node.className.indexOf('o_not_editable')!==-1 ) {
+ return false;
+ }
+ if (node.className.indexOf('o_editable')!==-1 ) {
+ return true;
+ }
+ }
+ node = node.parentNode;
+ }
+ return false;
+ }
+});
+
+$.fn.extend({
+ focusIn: function () {
+ if (this.length) {
+ range.create(dom.firstChild(this[0]), 0).select();
+ }
+ return this;
+ },
+ focusInEnd: function () {
+ if (this.length) {
+ var last = dom.lastChild(this[0]);
+ range.create(last, dom.nodeLength(last)).select();
+ }
+ return this;
+ },
+ selectContent: function () {
+ if (this.length) {
+ var next = dom.lastChild(this[0]);
+ 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) {
+ var self = this;
+ this.EditorBar = EditorBar;
+ data.rte = this;
+ this._super.apply(this, arguments);
+
+ this.init_bootstrap_carousel = $.fn.carousel;
+ this.edit_bootstrap_carousel = function () {
+ var res = self.init_bootstrap_carousel.apply(this, arguments);
+ // off bootstrap keydown event to remove event.preventDefault()
+ // and allow to change cursor position
+ $(this).off('keydown.bs.carousel');
+ return res;
+ };
+
+ if (config) {
+ this.config = config;
+ }
+ },
+ /**
+ * Add a record undo to history
+ * @param {DOM} target where the dom is changed is editable zone
+ */
+ historyRecordUndo: function ($target, event, internal_history) {
+ var rng = range.create();
+ var $editable = $(rng && rng.sc).closest(".o_editable");
+ if (!rng || !$editable.length) {
+ $editable = $($target).closest(".o_editable");
+ rng = range.create($target.closest("*")[0],0);
+ } else {
+ rng = $editable.data('range') || rng;
+ }
+ rng.select();
+ history.recordUndo($editable, event, internal_history);
+ },
+ /**
+ * 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
+ */
+ start: function () {
+ var self = this;
+
+ this.saving_mutex = new utils.Mutex();
+
+ $.fn.carousel = this.edit_bootstrap_carousel;
+
+ $(document).on('keydown', this, this.onKeydown);
+ $(document).on('mousedown activate', this, this.onMousedown);
+ $(document).on('mouseup', this, this.onMouseup);
+
+ $('.o_not_editable').attr("contentEditable", false);
+
+ var $editable = this.editable();
+
+ $editable.addClass('o_editable').data('rte', self);
+
+ $editable.each(function () {
+ var $node = $(this);
+
+ // add class to display inline-block for empty t-field
+ if(window.getComputedStyle(this).display === "inline" && $node.data('oe-type') !== "image") {
+ $node.addClass('o_is_inline_editable');
+ }
+
+ // start element observation
+ $node.one('content_changed', function () {
+ $(this).addClass('o_dirty');
+ });
+
+ $node.data('initInnerHTML', $node.html());
+ });
+
+ $(document).on('content_changed', '.o_editable', function () {
+ self.trigger('change', this);
+ });
+
+ $('#wrapwrap, .o_editable').on('click', '*', this, this.onClick);
+
+ $('body').addClass("editor_enable");
+
+ $(document)
+ .tooltip({
+ selector: '[data-oe-readonly]',
+ container: 'body',
+ trigger: 'hover',
+ delay: { "show": 1000, "hide": 100 },
+ placement: 'bottom',
+ title: _t("Readonly field")
+ })
+ .on('click', function () {
+ $(this).tooltip('hide');
+ });
+
+ $(document).trigger('mousedown');
+ self.trigger('rte:start');
+ },
+
+ save: function (context) {
+ var self = this;
+
+ this.__saved = {}; // list of allready saved views and data
+
+ var editables = history.getEditableHasUndo();
+
+ 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 = $("<div/>").text(failure.error.message).html();
+ var data = msg.substring(3,msg.length-2).split(/', u'/);
+ failure.error.message = '<b>' + data[0] + '</b>' + data[1];
+ }
+ $('.o_editable.' + failure.id)
+ .removeClass(failure.id)
+ .popover({
+ html: html,
+ trigger: 'hover',
+ content: failure.error.message,
+ placement: 'auto top',
+ })
+ // Force-show popovers so users will notice them.
+ .popover('show');
+ });
+ });
+ },
+
+ 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 true;
+ this.__saved[key] = true;
+ }
+
+ return ajax.jsonRpc('/web/dataset/call', 'call', {
+ model: 'ir.ui.view',
+ method: 'save',
+ args: [
+ $el.data('oe-id'),
+ $el.prop('outerHTML'),
+ $el.data('oe-xpath') || null,
+ context || base.get_context()
+ ],
+ });
+ },
+
+ stop: function () {
+ if (this.$last) {
+ this.$last.destroy();
+ this.$last = null;
+ }
+
+ $.fn.carousel = this.init_bootstrap_carousel;
+
+ $(document).off('keydown', this.onKeydown);
+ $(document).off('mousedown applySnap', this.onMousedown);
+ $(document).off('mouseup', this.onMouseup);
+ $('.o_not_editable').removeAttr("contentEditable");
+ $(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
+ $('#wrapwrap, .o_editable').off('click', this.onClick);
+ $(document).tooltip('destroy');
+ $('body').removeClass("editor_enable");
+ this.trigger('rte:stop');
+ },
+
+ cancel: function () {
+ $('.o_editable').each(function () {
+ var $node = $(this);
+ $node.data('initInnerHTML', $node.html());
+ });
+ this.stop();
+ },
+
+ onClick: function (event) {
+ event.preventDefault();
+ },
+
+ // handler for cancel editor
+ onKeydown: function (event) {
+ if (event.keyCode === 27 && !$('.modal-content:visible').length) {
+ setTimeout(function () {
+ $('#editor-top-navbar [data-action="cancel"]').click();
+ var $modal = $('.modal-content > .modal-body').parents(".modal:first");
+ $modal.off('keyup.dismiss.bs.modal');
+ setTimeout(function () {
+ $modal.on('keyup.dismiss.bs.modal', function () {
+ $(this).modal('hide');
+ });
+ },500);
+ },0);
+ }
+ },
+
+ // activate editor
+ onMousedown: function (event) {
+ var $target = $(event.target);
+ var $editable = $target.closest('.o_editable');
+
+ if (!$editable.size()) {
+ return;
+ }
+
+ var rte = $editable.data('rte') || event.data;
+
+ if (rte && rte.$last && (!$editable.size() || rte.$last[0] != $editable[0])) {
+ var $destroy = rte.$last;
+ history.splitNext();
+
+ setTimeout(function () {
+ var id = $destroy.data('note-id');
+ $destroy.destroy().removeData('note-id').removeAttr('data-note-id');
+ $('#note-popover-'+id+', #note-handle-'+id+', #note-dialog-'+id+'').remove();
+ },150); // setTimeout to remove flickering when change to editable zone (re-create an editor)
+ rte.$last = null;
+ }
+ if ($editable.size() && (!rte.$last || rte.$last[0] != $editable[0]) &&
+ ($target.closest('[contenteditable]').attr('contenteditable') || "").toLowerCase() !== 'false') {
+
+ $editable.summernote(rte.config($editable));
+
+ $editable.data('NoteHistory', history);
+ rte.$last = $editable;
+
+ // firefox & IE fix
+ try {
+ document.execCommand('enableObjectResizing', false, false);
+ document.execCommand('enableInlineTableEditing', false, false);
+ document.execCommand( '2D-position', false, false);
+ } 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;});
+
+ if (!range.create()) {
+ $editable.focusIn();
+ }
+
+ if (dom.isImg($target[0])) {
+ $target.trigger('mousedown'); // for activate selection on picture
+ }
+ }
+ },
+
+ onMouseup: function (event) {
+ var $target = $(event.target);
+ var $editable = $target.closest('.o_editable');
+
+ if (!$editable.size()) {
+ return;
+ }
+
+ var rte = event.data;
+ setTimeout(function () {
+ rte.historyRecordUndo($target, 'activate', true);
+ },0);
+ },
+
+ 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 = {
+ 'history': history,
+ 'Class': RTE
+};
+return data;
+
+});
diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js
new file mode 100644
index 00000000000..1086f4b369c
--- /dev/null
+++ b/addons/web_editor/static/src/js/rte.summernote.js
@@ -0,0 +1,781 @@
+odoo.define('web_editor.rte.summernote', function (require) {
+'use strict';
+
+var core = require('web.core');
+var ajax = require('web.ajax');
+var base = require('web_editor.base');
+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()]
+}).done(function (data) {
+ QWeb.add_template(data);
+});
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* 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;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* 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);
+
+ if (!QWeb.has_template('web_editor.colorpicker')) {
+ return;
+ }
+
+ var $color = $container.find('.note-color');
+ var html = QWeb.render('web_editor.colorpicker');
+ $color.find('.note-color-palette').prepend(html);
+ var $bg = $color.find('.colorpicker:first button');
+ var $fore = $color.find('.colorpicker:last button');
+
+ $bg.each(function () { $(this).attr('data-event', 'backColor').attr('data-value', $(this).attr('class')); });
+ $fore.each(function () { $(this).attr('data-event', 'foreColor').attr('data-value', $(this).attr('class').replace(/bg-/, 'text-')); });
+};
+
+var fn_tplPopovers = renderer.tplPopovers;
+renderer.tplPopovers = function (lang, options) {
+ var $popover = $(fn_tplPopovers.call(this, lang, options));
+
+ var $imagePopover = $popover.find('.note-image-popover');
+ var $linkPopover = $popover.find('.note-link-popover');
+ var $airPopover = $popover.find('.note-air-popover');
+
+ if (window === window.top) {
+ $popover.children().addClass("hidden-xs");
+ }
+
+ //////////////// image popover
+
+ // add center button for images
+ var $centerbutton = $(tplIconButton('fa fa-align-center', {
+ title: _t('Center'),
+ event: 'floatMe',
+ value: 'center'
+ })).insertAfter($imagePopover.find('[data-event="floatMe"][data-value="left"]'));
+ $imagePopover.find('button[data-event="removeMedia"]').parent().remove();
+ $imagePopover.find('button[data-event="floatMe"][data-value="none"]').remove();
+
+ var $alt = $('<div class="btn-group"/>');
+ $alt.prependTo($imagePopover.find('.popover-content'));
+ $alt.append('<button class="btn btn-default btn-sm btn-small" data-event="alt"><strong>Alt: </strong><span class="o_image_alt"></span></button>');
+
+ // padding button
+ var $padding = $('<div class="btn-group"/>');
+ $padding.insertBefore($imagePopover.find('.btn-group:first'));
+ var $button = $(tplIconButton('fa fa-plus-square-o', {
+ title: _t('Padding'),
+ dropdown: true
+ })).appendTo($padding);
+ var $ul = $('<ul class="dropdown-menu"/>').insertAfter($button);
+ $ul.append('<li><a data-event="padding" href="#" data-value="">'+_t('None')+'</a></li>');
+ $ul.append('<li><a data-event="padding" href="#" data-value="small">'+_t('Small')+'</a></li>');
+ $ul.append('<li><a data-event="padding" href="#" data-value="medium">'+_t('Medium')+'</a></li>');
+ $ul.append('<li><a data-event="padding" href="#" data-value="large">'+_t('Large')+'</a></li>');
+ $ul.append('<li><a data-event="padding" href="#" data-value="xl">'+_t('Xl')+'</a></li>');
+
+ // circle, boxed... options became toggled
+ $imagePopover.find('[data-event="imageShape"]:not([data-value])').remove();
+ var $button = $(tplIconButton('fa fa-sun-o', {
+ title: _t('Shadow'),
+ event: 'imageShape',
+ value: 'shadow'
+ })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="img-circle"]'));
+
+ // add spin for fa
+ var $spin = $('<div class="btn-group hidden only_fa"/>').insertAfter($button.parent());
+ $(tplIconButton('fa fa-refresh', {
+ title: _t('Spin'),
+ event: 'imageShape',
+ value: 'fa-spin'
+ })).appendTo($spin);
+
+ // resize for fa
+ var $resizefa = $('<div class="btn-group hidden only_fa"/>')
+ .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])'));
+ for (var size=1; size<=5; size++) {
+ $(tplButton('<span class="note-fontsize-10">'+size+'x</span>', {
+ title: size+"x",
+ event: 'resizefa',
+ value: size+''
+ })).appendTo($resizefa);
+ }
+ var $colorfa = $airPopover.find('.note-color').clone();
+ $colorfa.find(".btn-group:first").remove();
+ $colorfa.find("ul.dropdown-menu").css('min-width', '172px');
+ $colorfa.find('button[data-event="color"]').attr('data-value', '{"foreColor": "#f00"}')
+ .find("i").css({'background': '', 'color': '#f00'});
+ $resizefa.after($colorfa);
+
+ // show dialog box and delete
+ var $imageprop = $('<div class="btn-group"/>');
+ $imageprop.appendTo($imagePopover.find('.popover-content'));
+ $(tplIconButton('fa fa-picture-o', {
+ title: _t('Edit'),
+ event: 'showImageDialog'
+ })).appendTo($imageprop);
+ $(tplIconButton('fa fa-trash-o', {
+ title: _t('Remove'),
+ event: 'delete'
+ })).appendTo($imageprop);
+
+ $imagePopover.find('.popover-content').append($airPopover.find(".note-history").clone());
+
+ $imagePopover.find('[data-event="showImageDialog"]').before($airPopover.find('[data-event="showLinkDialog"]').clone());
+
+ //////////////// link popover
+
+ $linkPopover.find('.popover-content').append($airPopover.find(".note-history").clone());
+
+ $linkPopover.find('button[data-event="showLinkDialog"] i').attr("class", "fa fa-link");
+ $linkPopover.find('button[data-event="unlink"]').before($airPopover.find('button[data-event="showImageDialog"]').clone());
+
+ //////////////// text/air popover
+
+ //// highlight the text format
+ $airPopover.find('.note-style').on('mousedown', function () {
+ var $format = $airPopover.find('[data-event="formatBlock"]');
+ var node = range.create().sc;
+ var formats = $format.map(function () { return $(this).data("value"); }).get();
+ while (node && (!node.tagName || (!node.tagName || formats.indexOf(node.tagName.toLowerCase()) === -1))) {
+ node = node.parentNode;
+ }
+ $format.parent().removeClass('active');
+ $format.filter('[data-value="'+(node ? node.tagName.toLowerCase() : "p")+'"]')
+ .parent().addClass("active");
+ });
+
+ //////////////// tooltip
+
+ $airPopover.add($linkPopover).add($imagePopover).find("button")
+ .tooltip('destroy')
+ .tooltip({
+ container: 'body',
+ trigger: 'hover',
+ placement: 'bottom'
+ }).on('click', function () {$(this).tooltip('hide');});
+
+ return $popover;
+};
+
+var fn_boutton_update = eventHandler.popover.button.update;
+eventHandler.popover.button.update = function ($container, oStyle) {
+ // stop animation when edit content
+ var previous = $(".note-control-selection").data('target');
+ if (previous) {
+ $(previous).css({"-webkit-animation-play-state": "", "animation-play-state": "", "-webkit-transition": "", "transition": "", "-webkit-animation": "", "animation": ""});
+ }
+ if (oStyle.image) {
+ $(oStyle.image).css({"-webkit-animation": "none", "animation": "none"});
+ }
+ // end
+
+ fn_boutton_update.call(this, $container, oStyle);
+
+ if (oStyle.image) {
+ $container.find('[data-event]').parent().removeClass("active");
+
+ $container.find('a[data-event="padding"][data-value="small"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-small"));
+ $container.find('a[data-event="padding"][data-value="medium"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-medium"));
+ $container.find('a[data-event="padding"][data-value="large"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-large"));
+ $container.find('a[data-event="padding"][data-value="xl"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-xl"));
+ $container.find('a[data-event="padding"][data-value=""]').parent().toggleClass("active", !$container.find('.active a[data-event="padding"]').length);
+
+ if (dom.isImgFont(oStyle.image)) {
+
+ $container.find('.btn-group:not(.only_fa):has(button[data-event="resize"],button[data-event="imageShape"])').addClass("hidden");
+ $container.find('.only_fa').removeClass("hidden");
+ $container.find('button[data-event="resizefa"][data-value="2"]').toggleClass("active", $(oStyle.image).hasClass("fa-2x"));
+ $container.find('button[data-event="resizefa"][data-value="3"]').toggleClass("active", $(oStyle.image).hasClass("fa-3x"));
+ $container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x"));
+ $container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x"));
+ $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length);
+
+ $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin"));
+
+ $container.find('.note-color').removeClass("hidden");
+
+ } else {
+
+ $container.find('.hidden:not(.only_fa)').removeClass("hidden");
+ $container.find('.only_fa').addClass("hidden");
+ var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/);
+ if (width) {
+ width = width[2];
+ }
+ $container.find('button[data-event="resize"][data-value="1"]').toggleClass("active", width === "100%");
+ $container.find('button[data-event="resize"][data-value="0.5"]').toggleClass("active", width === "50%");
+ $container.find('button[data-event="resize"][data-value="0.25"]').toggleClass("active", width === "25%");
+
+ $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));
+
+ if (!$(oStyle.image).is("img")) {
+ $container.find('.btn-group:has(button[data-event="imageShape"])').addClass("hidden");
+ }
+
+ $container.find('.note-color').addClass("hidden");
+
+ }
+
+ $container.find('button[data-event="floatMe"][data-value="left"]').toggleClass("active", $(oStyle.image).hasClass("pull-left"));
+ $container.find('button[data-event="floatMe"][data-value="center"]').toggleClass("active", $(oStyle.image).hasClass("center-block"));
+ $container.find('button[data-event="floatMe"][data-value="right"]').toggleClass("active", $(oStyle.image).hasClass("pull-right"));
+
+ $(oStyle.image).trigger('attributes_change');
+ }
+};
+
+var fn_popover_update = eventHandler.popover.update;
+eventHandler.popover.update = function ($popover, oStyle, isAirMode) {
+ var $imagePopover = $popover.find('.note-image-popover');
+ var $linkPopover = $popover.find('.note-link-popover');
+ var $airPopover = $popover.find('.note-air-popover');
+
+ fn_popover_update.call(this, $popover, oStyle, isAirMode);
+
+ if (!isAirMode || $(oStyle.range.sc).closest('[data-oe-model]:not([data-oe-model="ir.ui.view"]):not([data-oe-type="html"])').length) {
+ $imagePopover.hide();
+ $linkPopover.hide();
+ $airPopover.hide();
+ return;
+ }
+
+ if (oStyle.image) {
+ if (oStyle.image.parentNode.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
+ oStyle.image = oStyle.image.parentNode;
+ }
+ var alt = $(oStyle.image).attr("alt");
+
+ $imagePopover.find('.o_image_alt').text( (alt || "").replace(/&quot;/g, '"') ).parent().toggle(oStyle.image.tagName === "IMG");
+ $imagePopover.show();
+
+ range.createFromNode(dom.firstChild(oStyle.image)).select();
+ } else {
+ $(".note-control-selection").hide();
+ }
+
+ if (oStyle.image || (!oStyle.range.isCollapsed() || (oStyle.range.sc.tagName && !dom.isAnchor(oStyle.range.sc)) || (oStyle.image && !$(oStyle.image).closest('a').length))) {
+ $linkPopover.hide();
+ oStyle.anchor = false;
+ }
+
+ if (oStyle.image || oStyle.anchor || !$(oStyle.range.sc).closest('.note-editable').length) {
+ $airPopover.hide();
+ } else {
+ $airPopover.show();
+ }
+};
+
+eventHandler.handle.update = function ($handle, oStyle, isAirMode) {
+ $handle.toggle(!!oStyle.image);
+ if (oStyle.image) {
+ var $selection = $handle.find('.note-control-selection');
+ var $image = $(oStyle.image);
+ var szImage = {
+ w: parseInt($image.outerWidth(true), 10),
+ h: parseInt($image.outerHeight(true), 10)
+ };
+ $selection.data('target', oStyle.image); // save current image element.
+ var sSizing = szImage.w + 'x' + szImage.h;
+ $selection.find('.note-control-selection-info').text(szImage.h > 50 ? sSizing : "");
+
+ $selection.find('.note-control-sizing').toggleClass('note-control-sizing note-control-holder').css({
+ 'border-top': 0,
+ 'border-left': 0
+ });
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* hack for image and link editor */
+
+function getImgTarget () {
+ return $(".note-control-selection").data('target');
+}
+eventHandler.editor.padding = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ var paddings = "small medium large xl".split(/\s+/);
+ $editable.data('NoteHistory').recordUndo();
+ if (sValue.length) {
+ paddings.splice(paddings.indexOf(sValue),1);
+ $target.toggleClass('padding-'+sValue);
+ }
+ $target.removeClass("padding-" + paddings.join(" padding-"));
+};
+eventHandler.editor.resize = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ $editable.data('NoteHistory').recordUndo();
+ var width = ($target.attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+)%/);
+ if (width) {
+ width = width[2]/100;
+ }
+ $(getImgTarget()).css('width', width != sValue ? (sValue * 100) + '%' : '');
+};
+eventHandler.editor.resizefa = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ $editable.data('NoteHistory').recordUndo();
+ $target.attr('class', $target.attr('class').replace(/\s*fa-[0-9]+x/g, ''));
+ if (+sValue > 1) {
+ $target.addClass('fa-'+sValue+'x');
+ }
+};
+eventHandler.editor.floatMe = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ $editable.data('NoteHistory').recordUndo();
+ switch (sValue) {
+ case 'center': $target.toggleClass('center-block').removeClass('pull-right pull-left'); break;
+ case 'left': $target.toggleClass('pull-left').removeClass('pull-right center-block'); break;
+ case 'right': $target.toggleClass('pull-right').removeClass('pull-left center-block'); break;
+ }
+};
+eventHandler.editor.imageShape = function ($editable, sValue) {
+ var $target = $(getImgTarget());
+ $editable.data('NoteHistory').recordUndo();
+ $target.toggleClass(sValue);
+};
+
+eventHandler.dialog.showLinkDialog = function ($editable, $dialog, linkInfo) {
+ $editable.data('range').select();
+ $editable.data('NoteHistory').recordUndo();
+
+ var editor = new widgets.LinkDialog($editable, linkInfo);
+ editor.appendTo(document.body);
+
+ 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();
+ });
+ return def;
+};
+eventHandler.dialog.showImageDialog = function ($editable) {
+ var r = $editable.data('range');
+ if (r.sc.tagName && r.sc.childNodes.length) {
+ r.sc = r.sc.childNodes[r.so];
+ }
+ var editor = new widgets.MediaDialog($editable, dom.isImg(r.sc) ? r.sc : null);
+ editor.appendTo(document.body);
+ 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($editable, media).appendTo(document.body);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+dom.isVoid = function (node) {
+ return node && /^BR|^IMG|^HR/.test(node.nodeName.toUpperCase()) || dom.isImg(node);
+};
+dom.isImg = function (node) {
+ return dom.isImgFont(node) || (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i)) ));
+};
+dom.isForbiddenNode = function (node) {
+ return $(node).is(".media_iframe_video, .fa, img");
+};
+
+dom.isImgFont = function (node) {
+ var nodeName = node && node.nodeName.toUpperCase();
+ var className = (node && node.className || "");
+ if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) {
+ var classNames = className.split(/\s+/);
+ for (var k=0; k<widgets.fontIcons.length; k++) {
+ if (_.intersection(widgets.fontIcons[k].icons, classNames).length) {
+ return true;
+ }
+ }
+ }
+ return false;
+};
+// re-overwrite font to include theme icons
+var isFont = dom.isFont;
+dom.isFont = function (node) {
+ return dom.isImgFont(node) || isFont(node);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+var fn_visible = $.summernote.pluginEvents.visible;
+$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
+ var res = fn_visible.call(this, event, editor, layoutInfo);
+ var $node = $(dom.node(range.create().sc));
+ if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) && $node.hasClass("o_editable") && !$node[0].children.length) {
+ var p = $('<p><br/></p>')[0];
+ $node.append( p );
+ range.createFromNode(p.firstChild).select();
+ }
+ return res;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* fix ie and re-range to don't break snippet*/
+
+var initial_data = {};
+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();
+ if (r) {
+ var rng = r.reRange(event.keyCode <= 38);
+ if (r !== rng) {
+ rng.select();
+ }
+ }
+ }
+}
+function reRangeSelect (event, dx, dy) {
+ var r = range.create();
+ if (!r || r.isCollapsed()) return;
+
+ // check if the user move the caret on up or down
+ var data = r.reRange(dy < 0 || (dy === 0 && dx < 0));
+
+ if (data.sc !== r.sc || data.so !== r.so || data.ec !== r.ec || data.eo !== r.eo) {
+ setTimeout(function () {
+ data.select();
+ $(data.sc.parentNode).closest('.note-popover');
+ },0);
+ }
+
+ $(data.sc).closest('.o_editable').data('range', r);
+ return r;
+}
+function summernote_mouseup (event) {
+ if ($(event.target).closest("#web_editor-top-navbar, .note-popover").length) {
+ return;
+ }
+ // don't rerange if simple click
+ if (initial_data.event) {
+ var dx = event.clientX - (event.shiftKey ? initial_data.rect.left : initial_data.event.clientX);
+ var dy = event.clientY - (event.shiftKey ? initial_data.rect.top : initial_data.event.clientY);
+ if (10 < Math.pow(dx, 2)+Math.pow(dy, 2)) {
+ reRangeSelect(event, dx, dy);
+ }
+ }
+
+ if (!$(event.target).closest(".o_editable").length) {
+ return;
+ }
+ if (!initial_data.range || !event.shiftKey) {
+ setTimeout(function () {
+ initial_data.range = range.create();
+ },0);
+ }
+}
+var remember_selection;
+function summernote_mousedown (event) {
+ rte.history.splitNext();
+
+ var $editable = $(event.target).closest(".o_editable, .note-editor");
+
+ if (!!document.documentMode) {
+ summernote_ie_fix(event, function (node) { return node.tagName === "DIV" || node.tagName === "IMG" || (node.dataset && node.dataset.oeModel); });
+ } else if (last_div && event.target !== last_div) {
+ if (last_div.tagName === "A") {
+ summernote_ie_fix(event, function (node) { return node.dataset && node.dataset.oeModel; });
+ } else if ($editable.length) {
+ if (summernote_ie_fix(event, function (node) { return node.tagName === "A"; })) {
+ r = range.create();
+ r.select();
+ }
+ }
+ }
+
+ // remember_selection when click on non editable area
+ var r = range.create();
+ if ($(r ? dom.node(r.sc) : event.srcElement || event.target).closest('#web_editor-top-navbar, #oe_main_menu_navbar, .note-popover, .note-toolbar, .modal').length) {
+ if (!$(event.target).is('input, select, label, button, a')) {
+ if (!remember_selection && $editable[0]) {
+ remember_selection = range.create(dom.firstChild($editable[0]), 0);
+ }
+ if (!remember_selection && $editable[0]) {
+ try {
+ remember_selection.select();
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+ }
+ } else if (r && $(dom.node(r.sc)).closest('.o_editable, .note-editable').length) {
+ remember_selection = r;
+ }
+
+ initial_data.event = event;
+
+ // keep selection when click with shift
+ if (event.shiftKey && $editable.length) {
+ if (initial_data.range) {
+ initial_data.range.select();
+ }
+ var rect = r && r.getClientRects();
+ initial_data.rect = rect && rect.length ? rect[0] : { top: 0, left: 0 };
+ }
+}
+
+var last_div;
+var last_div_change;
+var last_editable;
+function summernote_ie_fix (event, pred) {
+ var editable;
+ var div;
+ var node = event.target;
+ 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))) {
+ break;
+ }
+ if (node.className && node.className.indexOf('o_editable') !== -1) {
+ if (!div) {
+ div = node;
+ }
+ editable = node;
+ break;
+ }
+ node = node.parentNode;
+ }
+
+ if (!editable) {
+ $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable");
+ $(last_editable).attr("contentEditable", "true").prop("contentEditable", "true");
+ last_div_change = null;
+ last_editable = null;
+ return;
+ }
+
+ if (div === last_div) {
+ return;
+ }
+
+ last_div = div;
+
+ $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable");
+
+ if (last_editable !== editable) {
+ if ($(editable).is("[contentEditable='true']")) {
+ $(editable).removeAttr("contentEditable").removeProp("contentEditable");
+ last_editable = editable;
+ } else {
+ last_editable = null;
+ }
+ }
+ if (!$(div).attr("contentEditable") && !$(div).is("[data-oe-type='many2one'], [data-oe-type='contact']")) {
+ $(div).attr("contentEditable", "true").prop("contentEditable", "true");
+ last_div_change = div;
+ } else {
+ last_div_change = null;
+ }
+ return editable !== div ? div : null;
+}
+
+var fn_attach = eventHandler.attach;
+eventHandler.attach = function (oLayoutInfo, options) {
+ fn_attach.call(this, oLayoutInfo, options);
+ oLayoutInfo.editor.on('dragstart', 'img', function (e) { e.preventDefault(); });
+ $(document).on('mousedown', summernote_mousedown);
+ $(document).on('mouseup', summernote_mouseup);
+ oLayoutInfo.editor.off('click').on('click', function (e) {e.preventDefault();}); // if the content editable is a link
+ oLayoutInfo.editor.on('dblclick', 'img, .media_iframe_video, span.fa, i.fa, span.fa', function (event) {
+ if (!$(event.target).closest(".note-toolbar").length) { // prevent icon edition of top bar for default summernote
+ new widgets.MediaDialog(oLayoutInfo.editor, event.target).appendTo(document.body);
+ }
+ });
+ $(document).on("keyup", reRangeSelectKey);
+
+ var clone_data = false;
+ var $node = oLayoutInfo.editor;
+ if ($node.data('oe-model')) {
+ $node.on('content_changed', function () {
+
+ var $nodes = $('[data-oe-model]')
+ .filter(function () { return this != $node[0];})
+ .filter('[data-oe-model="'+$node.data('oe-model')+'"]')
+ .filter('[data-oe-id="'+$node.data('oe-id')+'"]')
+ .filter('[data-oe-field="'+$node.data('oe-field')+'"]');
+ if ($node.data('oe-type')) $nodes = $nodes.filter('[data-oe-type="'+$node.data('oe-type')+'"]');
+ if ($node.data('oe-expression')) $nodes = $nodes.filter('[data-oe-expression="'+$node.data('oe-expression')+'"]');
+ if ($node.data('oe-xpath')) $nodes = $nodes.filter('[data-oe-xpath="'+$node.data('oe-xpath')+'"]');
+ if ($node.data('oe-contact-options')) $nodes = $nodes.filter('[data-oe-contact-options="'+$node.data('oe-contact-options')+'"]');
+
+ var nodes = $node.get();
+
+ if ($node.data('oe-type') === "many2one") {
+ $nodes = $nodes.add($('[data-oe-model]')
+ .filter(function () { return this != $node[0] && nodes.indexOf(this) === -1; })
+ .filter('[data-oe-many2one-model="'+$node.data('oe-many2one-model')+'"]')
+ .filter('[data-oe-many2one-id="'+$node.data('oe-many2one-id')+'"]')
+ .filter('[data-oe-type="many2one"]'));
+
+ $nodes = $nodes.add($('[data-oe-model]')
+ .filter(function () { return this != $node[0] && nodes.indexOf(this) === -1; })
+ .filter('[data-oe-model="'+$node.data('oe-many2one-model')+'"]')
+ .filter('[data-oe-id="'+$node.data('oe-many2one-id')+'"]')
+ .filter('[data-oe-field="name"]'));
+ }
+
+ if (!clone_data) {
+ clone_data = true;
+ $nodes.html(this.innerHTML);
+ clone_data = false;
+ }
+ });
+ }
+
+ var $toolbar = $(oLayoutInfo.popover).add(oLayoutInfo.toolbar);
+ $('button[data-event="undo"], button[data-event="redo"]', $toolbar).attr('disabled', true);
+
+ $(oLayoutInfo.editor)
+ .add(oLayoutInfo.handle)
+ .add(oLayoutInfo.popover)
+ .add(oLayoutInfo.toolbar)
+ .on('click keyup', function () {
+ $('button[data-event="undo"]', $toolbar).attr('disabled', !oLayoutInfo.editable.data('NoteHistory').hasUndo());
+ $('button[data-event="redo"]', $toolbar).attr('disabled', !oLayoutInfo.editable.data('NoteHistory').hasRedo());
+ });
+};
+var fn_detach = eventHandler.detach;
+eventHandler.dettach = function (oLayoutInfo, options) {
+ fn_detach.call(this, oLayoutInfo, options);
+ oLayoutInfo.editor.off("dragstart");
+ $(document).off('mousedown', summernote_mousedown);
+ $(document).off('mouseup', summernote_mouseup);
+ oLayoutInfo.editor.off("dblclick");
+ $(document).off("keyup", reRangeSelectKey);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* Translation for odoo */
+
+$.summernote.lang.odoo = {
+ font: {
+ bold: _t('Bold'),
+ italic: _t('Italic'),
+ underline: _t('Underline'),
+ strikethrough: _t('Strikethrough'),
+ subscript: _t('Subscript'),
+ superscript: _t('Superscript'),
+ clear: _t('Remove Font Style'),
+ height: _t('Line Height'),
+ name: _t('Font Family'),
+ size: _t('Font Size')
+ },
+ image: {
+ image: _t('Picture'),
+ insert: _t('Insert Image'),
+ resizeFull: _t('Resize Full'),
+ resizeHalf: _t('Resize Half'),
+ resizeQuarter: _t('Resize Quarter'),
+ floatLeft: _t('Float Left'),
+ floatRight: _t('Float Right'),
+ floatNone: _t('Float None'),
+ dragImageHere: _t('Drag an image here'),
+ selectFromFiles: _t('Select from files'),
+ url: _t('Image URL'),
+ remove: _t('Remove Image')
+ },
+ link: {
+ link: _t('Link'),
+ insert: _t('Insert Link'),
+ unlink: _t('Unlink'),
+ edit: _t('Edit'),
+ textToDisplay: _t('Text to display'),
+ url: _t('To what URL should this link go?'),
+ openInNewWindow: _t('Open in new window')
+ },
+ video: {
+ video: _t('Video'),
+ videoLink: _t('Video Link'),
+ insert: _t('Insert Video'),
+ url: _t('Video URL?'),
+ providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)')
+ },
+ table: {
+ table: _t('Table')
+ },
+ hr: {
+ insert: _t('Insert Horizontal Rule')
+ },
+ style: {
+ style: _t('Style'),
+ normal: _t('Normal'),
+ blockquote: _t('Quote'),
+ pre: _t('Code'),
+ h1: _t('Header 1'),
+ h2: _t('Header 2'),
+ h3: _t('Header 3'),
+ h4: _t('Header 4'),
+ h5: _t('Header 5'),
+ h6: _t('Header 6')
+ },
+ lists: {
+ unordered: _t('Unordered list'),
+ ordered: _t('Ordered list')
+ },
+ options: {
+ help: _t('Help'),
+ fullscreen: _t('Full Screen'),
+ codeview: _t('Code View')
+ },
+ paragraph: {
+ paragraph: _t('Paragraph'),
+ outdent: _t('Outdent'),
+ indent: _t('Indent'),
+ left: _t('Align left'),
+ center: _t('Align center'),
+ right: _t('Align right'),
+ justify: _t('Justify full')
+ },
+ color: {
+ recent: _t('Recent Color'),
+ more: _t('More Color'),
+ background: _t('Background Color'),
+ foreground: _t('Foreground Color'),
+ transparent: _t('Transparent'),
+ setTransparent: _t('Set transparent'),
+ reset: _t('Reset'),
+ resetToDefault: _t('Reset to default')
+ },
+ shortcut: {
+ shortcuts: _t('Keyboard shortcuts'),
+ close: _t('Close'),
+ textFormatting: _t('Text formatting'),
+ action: _t('Action'),
+ paragraphFormatting: _t('Paragraph formatting'),
+ documentStyle: _t('Document Style')
+ },
+ history: {
+ undo: _t('Undo'),
+ redo: _t('Redo')
+ }
+};
+
+});
diff --git a/addons/web_editor/static/src/js/snippets.animation.js b/addons/web_editor/static/src/js/snippets.animation.js
new file mode 100644
index 00000000000..b3fccc5584a
--- /dev/null
+++ b/addons/web_editor/static/src/js/snippets.animation.js
@@ -0,0 +1,82 @@
+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 = [];
+
+var Animation = Class.extend({
+ selector: false,
+ $: function () {
+ return this.$el.find.apply(this.$el, arguments);
+ },
+ init: function (dom, editable_mode) {
+ this.$el = this.$target = $(dom);
+ this.start(editable_mode);
+ },
+ /*
+ * start
+ * This method is called after init
+ */
+ start: function (editable_mode) {
+ },
+ /*
+ * stop
+ * This method is called to stop the animation (e.g.: when rte is launch)
+ */
+ stop: function () {
+ },
+});
+
+var start = function (editable_mode, $target) {
+ for (var k in registry) {
+ var Animation = registry[k];
+ var selector = "";
+ if (Animation.prototype.selector) {
+ if (selector != "") selector += ", "
+ selector += Animation.prototype.selector;
+ }
+ if ($target) {
+ if ($target.is(selector)) selector = $target;
+ else continue;
+ }
+
+ $(selector).each(function() {
+ var $snipped_id = $(this);
+ if ( !$snipped_id.parents("#oe_snippets").length &&
+ !$snipped_id.parent("body").length &&
+ !$snipped_id.data("snippet-view")) {
+ ready.push($snipped_id);
+ $snipped_id.data("snippet-view", new Animation($snipped_id, editable_mode));
+ } else if ($snipped_id.data("snippet-view")) {
+ $snipped_id.data("snippet-view").start(editable_mode);
+ }
+ });
+ }
+};
+
+var stop = function () {
+ $(ready).each(function() {
+ var $snipped_id = $(this);
+ if ($snipped_id.data("snippet-view")) {
+ $snipped_id.data("snippet-view").stop();
+ }
+ });
+};
+
+base.ready(function () {
+ setTimeout(function () {
+ start();
+ },0);
+});
+
+return {
+ 'Class': Class,
+ 'registry': registry,
+ 'start': start,
+ 'stop': stop
+};
+
+}); \ No newline at end of file
diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js
new file mode 100644
index 00000000000..9d37a9f84c8
--- /dev/null
+++ b/addons/web_editor/static/src/js/snippets.editor.js
@@ -0,0 +1,1137 @@
+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 Widget = require('web.Widget');
+var base = require('web_editor.base');
+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 dummy = editor.dummy;
+
+/* ----- SNIPPET SELECTOR ---- */
+
+ajax.loadXML('/web_editor/static/src/xml/snippets.xml', qweb);
+
+editor.Class.include({
+ init: function() {
+ var self = this;
+ var res = this._super.apply(this, arguments);
+ var $editable = this.rte.editable();
+ this.buildingBlock = new BuildingBlock(this, $editable);
+ this.buildingBlock.on("snippets:ready", this, function () {
+ self.trigger("snippets:ready");
+ });
+ return res;
+ },
+ start: function () {
+ var self = this;
+ animation.stop();
+ this.buildingBlock.insertAfter(this.$el);
+ animation.start(true);
+
+ this.rte.editable().find("*").off('mousedown mouseup click');
+ return this._super();
+ },
+ save: function () {
+ this.buildingBlock.clean_for_save();
+ this._super();
+ },
+});
+
+/* ----- SNIPPET SELECTOR ---- */
+
+$.extend($.expr[':'],{
+ hasData: function(node,i,m){
+ return !!_.toArray(node.dataset).length;
+ },
+ data: function(node,i,m){
+ return $(node).data(m[3]);
+ }
+});
+
+var globalSelector = {
+ closest: function () { return $(); },
+ all: function () { return $(); },
+ is: function () { return false; },
+};
+
+/* ----- Jquery activate block ---- */
+
+$.fn.extend({
+ activateBlock: function () {
+ var target = globalSelector.closest($(this))[0] || (dom.isBR(this) ? this.parentNode : dom.node(this));
+ 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);
+ return this;
+ }
+});
+
+/* ----- BuildingBlock (managment of drag&drop and focus/blur snippets) ---- */
+
+var BuildingBlock = Widget.extend({
+ template: 'web_editor.snippets',
+ activeSnippets: [],
+ init: function (parent, $editable) {
+ this.parent = parent;
+ this.$editable = $editable;
+
+ this._super.apply(this, arguments);
+ if(!$('#oe_manipulators').length){
+ $("<div id='oe_manipulators' class='o_css_editor'></div>").appendTo('body');
+ }
+ this.$active_snipped_id = false;
+ this.snippets = [];
+
+ data.instance = this;
+ },
+ start: function() {
+ var self = this;
+ this.$snippet = $("#oe_snippets");
+
+ this.$el
+ .on("mouseenter", function () { self.show(); })
+ .on("mouseleave", function (event) { if (event.clientX>0 && event.clientY>0) self.hide(); });
+
+ $(window).resize(function () {
+ setTimeout('$(document).click()',0);
+ });
+
+ this.fetch_snippet_templates();
+ this.bind_snippet_click_editor();
+
+ $(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') || 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.$snippet.find('.scroll:first > ul li:eq('+(event.keyCode-49)+') a').trigger("click");
+ self.show();
+ event.preventDefault();
+ }
+ });
+ },
+
+ _get_snippet_url: function () {
+ return '/web_editor/snippets';
+ },
+ _add_check_selector : function (selector, no_check) {
+ var self = this;
+ var 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 ? $from.find(selector) : $(selector);
+ },
+ is: function ($from) {
+ return $from.is(selector);
+ }
+ };
+ } else {
+ var selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable";
+ return {
+ closest: function ($from, parentNode) {
+ var parents = self.$editable.get();
+ return $from.closest(selector, parentNode).filter(function () {
+ var node = this;
+ while (node.parentNode) {
+ if (parents.indexOf(node)!==-1) {
+ return true;
+ }
+ node = node.parentNode;
+ }
+ return false;
+ });
+ },
+ all: function ($from) {
+ return $from ? $from.find(selector) : self.$editable.filter(selector).add(self.$editable.find(selector));
+ },
+ is: function ($from) {
+ return $from.is(selector);
+ }
+ };
+ }
+ },
+
+ fetch_snippet_templates: function () {
+ var self = this;
+ var url = this._get_snippet_url();
+ if (!url || !url.length) {
+ this.$snippet.detach();
+ return;
+ }
+ this.$el.find("#o_left_bar").addClass("hidden");
+ return ajax.jsonRpc(url, 'call', {}).then(function (html) {
+ self.compute_snippet_templates(html);
+ self.trigger("snippets:ready");
+ });
+ },
+ compute_snippet_templates: function (html) {
+ var self = this;
+ var $html = $(html);
+ var $left_bar = this.$el.find("#o_left_bar");
+ var $ul = $html.siblings("ul");
+ var $scroll = $html.siblings("#o_scroll");
+
+ if (!$scroll.length) {
+ throw new Error("Wrong snippets xml definition");
+ }
+
+ $ul.children().tooltip({
+ delay: { "show": 500, "hide": 100 },
+ container: 'body',
+ title: function () {
+ return (navigator.appVersion.indexOf('Mac') > -1 ? 'CMD' : 'CTRL')+'+SHIFT+'+($(this).index()+1);
+ },
+ trigger: 'hover',
+ placement: 'top'
+ }).on('click', function () {$(this).tooltip('hide');});
+
+ // t-snippet
+ $html.find('[data-oe-type="snippet"][data-oe-name]').each(function () {
+ var $div = $('<div/>').insertAfter(this).append(this).attr('name', $(this).data('oe-name'));
+ });
+ // end
+
+ self.templateOptions = [];
+ var selector = [];
+ var $styles = $html.find("[data-js], [data-selector]");
+ $styles.each(function () {
+ var $style = $(this);
+ var no_check = $style.data('no-check');
+ var option_id = $style.data('js');
+ var option = {
+ 'option' : option_id,
+ 'base_selector': $style.data('selector'),
+ 'selector': self._add_check_selector($style.data('selector'), no_check),
+ '$el': $style,
+ 'drop-near': $style.data('drop-near') && self._add_check_selector($style.data('drop-near'), no_check),
+ 'drop-in': $style.data('drop-in') && self._add_check_selector($style.data('drop-in'), no_check),
+ 'data': $style.data()
+ };
+ self.templateOptions.push(option);
+ selector.push(option.selector);
+ });
+
+ $styles.addClass("hidden");
+ globalSelector = {
+ closest: function ($from) {
+ var $temp;
+ var $target;
+ var len = selector.length;
+ for (var i = 0; i<len; i++) {
+ $temp = selector[i].closest($from, $target && $target[0]);
+ if (!$target || $temp.length) {
+ $target = $temp;
+ }
+ }
+ return $target;
+ },
+ all: function ($from) {
+ var $target;
+ var len = selector.length;
+ for (var i = 0; i<len; i++) {
+ if (!$target) $target = selector[i].all($from);
+ else $target = $target.add(selector[i].all($from));
+ }
+ return $target;
+ },
+ is: function ($from) {
+ var len = selector.length;
+ for (var i = 0; i<len; i++) {
+ if (selector[i].is($from)) {
+ return true;
+ }
+ }
+ return false;
+ },
+ };
+
+ var number = 0;
+
+ // oe_snippet_body
+ self.$snippets = $scroll.find(".o_panel_body").children()
+ .addClass("oe_snippet")
+ .each(function () {
+ if (!$('.oe_snippet_thumbnail', this).size()) {
+ var $div = $(
+ '<div class="oe_snippet_thumbnail">'+
+ '<div class="oe_snippet_thumbnail_img"/>'+
+ '<span class="oe_snippet_thumbnail_title"></span>'+
+ '</div>');
+ $div.find('span').text($(this).attr("name"));
+ $(this).prepend($div);
+
+ // from t-snippet
+ var thumbnail = $("[data-oe-thumbnail]", this).data("oe-thumbnail");
+ if (thumbnail) {
+ $div.find('.oe_snippet_thumbnail_img').css('background-image', 'url(' + thumbnail + ')');
+ }
+ // end
+ }
+ if (!$(this).data("selector")) {
+ $("> *:not(.oe_snippet_thumbnail)", this).addClass('oe_snippet_body');
+ }
+ number++;
+ });
+
+ // hide scroll if no snippets defined
+ if (!number) {
+ this.$snippet.detach();
+ } else {
+ this.$el.find("#o_left_bar").removeClass("hidden");
+ }
+ $("body").toggleClass("editor_has_snippets", !!number);
+
+ // select all default text to edit (if snippet default text)
+ /*self.$snippets.find('.oe_snippet_body, .oe_snippet_body *')
+ .contents()
+ .filter(function() {
+ return this.nodeType === 3 && this.textContent.match(/\S/);
+ }).parent().addClass("o_default_snippet_text");*/
+ $(document).on("mouseup", ".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");
+ });
+ // end
+
+ // clean t-oe
+ $html.find('[data-oe-model], [data-oe-type]').each(function () {
+ for (var k=0; k<this.attributes.length; k++) {
+ if (this.attributes[k].name.indexOf('data-oe-') === 0) {
+ $(this).removeAttr(this.attributes[k].name);
+ k--;
+ }
+ }
+ });
+ // end
+
+ $html.find('.o_not_editable').attr("contentEditable", false);
+
+ $left_bar.append($html);
+
+ // animate for list of snippet blocks
+ $left_bar.on('click', '.scroll-link', function (event) {
+ event.preventDefault();
+ var targetOffset = $($(this).attr("href")).position().top - $ul.outerHeight() + $scroll[0].scrollTop;
+ $scroll.animate({'scrollTop': targetOffset}, 750);
+ });
+ $scroll.on('scroll', function () {
+ var middle = $scroll.height()/4;
+ var $li = $ul.find("a").parent().removeClass('active');
+ var last;
+ for (var k=0; k<$li.length; k++) {
+ var li = $($li[k]);
+ if (!li.data('target')) {
+ li.data('target', $($("a", li).attr("href")));
+ }
+ if (li.data('target').position().top > middle) {
+ break;
+ }
+ last = $li[k];
+ }
+ $(last).addClass("active");
+ });
+ // end
+
+ // display scrollbar
+ $(window).on('resize', function () {
+ $scroll.css("overflow", "");
+ var height = $left_bar.height() - $ul.outerHeight();
+ $scroll.css("height", height);
+ var $last = $scroll.children(":visible").last().children(".o_panel_body");
+ $last.css({'min-height': (height-$last.prev().outerHeight())+'px'});
+ if ($scroll[0].scrollHeight + $ul[0].scrollHeight > document.body.clientHeight) {
+ $scroll.css("overflow", "auto").css("width", "226px");
+ } else {
+ $scroll.css("width", "");
+ }
+ }).trigger('resize');
+ // end
+
+ self.make_snippet_draggable(self.$snippets);
+ },
+
+ cover_target: function ($el, $target){
+ if($el.data('not-cover_target')) return;
+ var pos = $target.offset();
+ var mt = parseInt($target.css("margin-top") || 0);
+ var mb = parseInt($target.css("margin-bottom") || 0);
+ var width = $target.outerWidth();
+ var bigger = pos.left+width > $("body").outerWidth()-8;
+ $el.css({
+ 'width': width,
+ 'top': pos.top - mt - 5,
+ 'left': pos.left-1
+ });
+ $el.find(".oe_handle.e,.oe_handle.w").css({'height': $target.outerHeight() + mt + mb+1});
+ if (bigger) {
+ $el.find(".oe_handle.e").css({right: 0, margin: 0})
+ .find("div").css({right: 0, left: 'auto'});
+ } else {
+ $el.find(".oe_handle.e").css({right: "", margin: ""})
+ .find("div").css({right: "", left: ""});
+ }
+ $el.find(".oe_handle.s").css({'top': $target.outerHeight() + mt + mb});
+ $el.find(".oe_handle.size").css({'top': $target.outerHeight() + mt});
+ $el.find(".oe_handle.s,.oe_handle.n").css({'width': width-2});
+ },
+
+ show_blocks: function () {
+ var self = this;
+ var cache = {};
+ this.$snippet.find(".o_panel").each(function () {
+ var catcheck = false;
+ var $category = $(this);
+ $category.find(".oe_snippet_body").each(function () {
+ var $snippet = $(this);
+
+ var check = false;
+
+ for (var k in self.templateOptions) {
+ var option = self.templateOptions[k];
+ if ($snippet.is(option.base_selector)) {
+
+ cache[k] = cache[k] || {
+ 'drop-near': option['drop-near'] ? option['drop-near'].all() : [],
+ 'drop-in': option['drop-in'] ? option['drop-in'].all() : []
+ };
+
+ if (cache[k]['drop-near'].length || cache[k]['drop-in'].length) {
+ catcheck = true;
+ check = true;
+ break;
+ }
+ }
+ }
+
+ if (check) {
+ $snippet.closest(".oe_snippet").removeClass("disable");
+ } else {
+ $snippet.closest(".oe_snippet").addClass("disable");
+ }
+ });
+
+ $('#oe_snippets .scroll a[data-toggle="tab"][href="#' + $category.attr("id") + '"]').toggle(catcheck);
+ });
+ },
+ show: function () {
+ var self = this;
+ this.make_active(false);
+ this.$el.addClass("o_open");
+ this.show_blocks();
+ },
+ hide: function () {
+ this.$el.removeClass("o_open");
+ },
+ bind_snippet_click_editor: function () {
+ var self = this;
+ var snipped_event_flag;
+ $(document).on('click', '*', function (event) {
+ var srcElement = event.srcElement || (event.originalEvent && (event.originalEvent.originalTarget || event.originalEvent.target) || event.target);
+ if (self.editor_busy || snipped_event_flag===srcElement || !srcElement) {
+ return;
+ }
+ snipped_event_flag = srcElement;
+
+ setTimeout(function () {snipped_event_flag = false;}, 0);
+ var $target = $(srcElement);
+
+ if ($target.closest(".oe_overlay, .note-popover").length) {
+ return;
+ }
+
+ if (!globalSelector.is($target)) {
+ $target = 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 !== dummy) {
+ template[k].selector.all().filter(".o_dirty").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_overlay(this.$active_snipped_id);
+ this.snippet_focus($snippet);
+ }
+ this.$snippet.trigger('snippet-activated', $snippet);
+ if ($snippet) {
+ $snippet.trigger('snippet-activated', $snippet);
+ }
+ },
+ create_overlay: function ($snippet) {
+ if (typeof $snippet.data("snippet-editor") === 'undefined') {
+ var $targets = this.activate_overlay_zones($snippet);
+ if (!$targets.length) return;
+ $snippet.data("snippet-editor", new Editor(this, $snippet));
+ }
+ this.cover_target($snippet.data('overlay'), $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 left = $tumb.outerWidth()/2;
+ var top = $tumb.outerHeight()/2;
+ var $toInsert, dropped, $snippet, action, snipped_id;
+
+ $snippets.draggable({
+ greedy: true,
+ helper: 'clone',
+ zIndex: '1000',
+ appendTo: 'body',
+ cursor: "move",
+ handle: ".oe_snippet_thumbnail",
+ cursorAt: {
+ 'left': left,
+ 'top': top
+ },
+ start: function(){
+ self.hide();
+ 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 vertical = false;
+ var temp = self.templateOptions;
+ for (var k in temp) {
+ if ($base_body.is(temp[k].base_selector)) {
+ if (temp[k]['drop-near']) {
+ if (!$selector_siblings) $selector_siblings = temp[k]['drop-near'].all();
+ else $selector_siblings = $selector_siblings.add(temp[k]['drop-near'].all());
+ }
+ if (temp[k]['drop-in']) {
+ if (!$selector_children) $selector_children = temp[k]['drop-in'].all();
+ else $selector_children = $selector_children.add(temp[k]['drop-in'].all());
+ }
+ }
+ }
+
+ $toInsert = $base_body.clone();
+
+ if (!$selector_siblings.length && !$selector_children.length) {
+ console.debug($snippet.find(".oe_snippet_thumbnail_title").text() + " have not insert action: data-drop-near or data-drop-in");
+ return;
+ }
+
+ self.activate_insertion_zones($selector_siblings, $selector_children);
+
+ $('.oe_drop_zone').droppable({
+ over: function(){
+ dropped = true;
+ $(this).first().after($toInsert);
+ },
+ out: function(){
+ var prev = $toInsert.prev();
+ if(this === prev[0]){
+ dropped = false;
+ $toInsert.detach();
+ }
+ }
+ });
+ },
+ stop: function(ev, ui){
+ $toInsert.removeClass('oe_snippet_body');
+
+ if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3) {
+ var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first();
+ if (el.length) {
+ el.after($toInsert);
+ dropped = true;
+ }
+ }
+
+ self.$editable.find('.oe_drop_zone').droppable('destroy').remove();
+
+ if (dropped) {
+
+ var prev = $toInsert.first()[0].previousSibling;
+ var next = $toInsert.last()[0].nextSibling;
+ var rte = self.parent.rte;
+
+ if (prev) {
+ $toInsert.detach();
+ rte.historyRecordUndo($(prev));
+ $toInsert.insertAfter(prev);
+ } else if (next) {
+ $toInsert.detach();
+ rte.historyRecordUndo($(next));
+ $toInsert.insertBefore(next);
+ } else {
+ var $parent = $toInsert.parent();
+ $toInsert.detach();
+ rte.historyRecordUndo($parent);
+ $parent.prepend($toInsert);
+ }
+
+ $toInsert.closest(".o_editable").trigger("content_changed");
+
+ var $target = false;
+ $target = $toInsert;
+
+ setTimeout(function () {
+ self.$snippet.trigger('snippet-dropped', $target);
+
+ animation.start(true, $target);
+
+ // drop_and_build_snippet
+ self.create_overlay($target);
+ if ($target.data("snippet-editor")) {
+ $target.data("snippet-editor").drop_and_build_snippet();
+ }
+ for (var k in self.templateOptions) {
+ self.templateOptions[k].selector.all($target).each(function () {
+ var $snippet = $(this);
+ self.create_overlay($snippet);
+ if ($snippet.data("snippet-editor")) {
+ $snippet.data("snippet-editor").drop_and_build_snippet();
+ }
+ });
+ }
+ $target.closest(".o_editable").trigger("content_changed");
+ // end
+
+ self.make_active($target);
+ },0);
+ } else {
+ $toInsert.remove();
+ }
+ },
+ });
+ },
+
+ // 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 = $("<div class='oe_drop_zone oe_insert'></div>");
+
+ 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 oe_vertical_text").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);
+ var node = $drop[0].nextSibling;
+ var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR"));
+ if (test) {
+ $drop.addClass("oe_vertical oe_vertical_text").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()));
+ }
+
+ 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();
+ }
+ });
+ },
+
+ // 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;
+
+ function is_visible($el){
+ return $el.css('display') != 'none'
+ && $el.css('opacity') != '0'
+ && $el.css('visibility') != 'hidden';
+ }
+
+ // filter out invisible elements
+ $targets = $targets.filter(function(){ return is_visible($(this)); });
+
+ // filter out elements with invisible parents
+ $targets = $targets.filter(function(){
+ var parents = $(this).parents().filter(function(){ return !is_visible($(this)); });
+ return parents.length === 0;
+ });
+
+ $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) {
+ if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) {
+ clearTimeout(timer);
+ timer = setTimeout(function () {
+ if ($target.data('overlay')) {
+ 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;
+ }
+});
+
+/* ----- Editor (object who contain overlay and the option list) ---- */
+
+var Editor = Class.extend({
+ init: function (BuildingBlock, dom) {
+ this.buildingBlock = BuildingBlock;
+ this.$target = $(dom);
+ this.$overlay = this.$target.data('overlay');
+ this.load_style_options();
+ this.get_parent_block();
+ this.start();
+ },
+
+ // 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.hide();
+ self.buildingBlock.editor_busy = true;
+ self.size = {
+ width: self.$target.width(),
+ height: self.$target.height()
+ };
+ self.$target.after("<div class='oe_drop_clone' style='display: none;'/>");
+ self.$target.detach();
+ self.$overlay.addClass("hidden");
+
+ var $selector_siblings;
+ for (var i=0; i<self.selector_siblings.length; i++) {
+ if (!$selector_siblings) $selector_siblings = self.selector_siblings[i].all();
+ else $selector_siblings = $selector_siblings.add(self.selector_siblings[i].all());
+ }
+ var $selector_children;
+ for (var 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.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;
+ var $dropzone = this.$target.prev();
+ var prev = $dropzone.length && $dropzone[0].previousSibling;
+ var next = this.$target.last()[0].nextSibling;
+ var $parent = this.$target.parent();
+
+ $(".oe_drop_clone").after(this.$target);
+
+ this.$overlay.removeClass("hidden");
+ $("body").removeClass('move-important');
+ $('.oe_drop_zone').droppable('destroy').remove();
+ $(".oe_drop_clone, .oe_drop_to_remove").remove();
+
+ if (this.dropped) {
+ this.buildingBlock.parent.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.get_parent_block();
+ setTimeout(function () {self.buildingBlock.create_overlay(self.$target);},0);
+ },
+
+ 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 = [];
+ _.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'];
+ var Editor = options.registry[option] || options.Class;
+ var editor = self.styles[option] = new Editor(self.buildingBlock, self, self.$target, option_id);
+ $ul.append(editor.$el.addClass("snippet-option-" + option));
+ editor.start();
+ });
+
+ if (!this.selector_siblings.length && !this.selector_children.length) {
+ this.$overlay.find(".oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove").addClass('hidden');
+ }
+
+ if ($ul.find("li").length) {
+ $styles.removeClass("hidden");
+ }
+ this.$overlay.find('[data-toggle="dropdown"]').dropdown();
+ },
+
+ get_parent_block: function () {
+ var self = this;
+ var $button = this.$overlay.find('.oe_snippet_parent');
+ var $parent = globalSelector.closest(this.$target.parent());
+ if ($parent.length) {
+ $button.removeClass("hidden");
+ $button.off("click").on('click', function (event) {
+ event.preventDefault();
+ setTimeout(function () {
+ self.buildingBlock.make_active($parent);
+ }, 0);
+ });
+ } else {
+ $button.addClass("hidden");
+ }
+ },
+
+ /*
+ * start
+ * This method is called after init and _readXMLData
+ */
+ start: function () {
+ var self = this;
+ 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();
+ }
+ },
+
+ on_clone: function (event) {
+ event.preventDefault();
+ var $clone = this.$target.clone(false);
+
+ this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
+
+ this.$target.after($clone);
+ for (var i in this.styles){
+ this.styles[i].on_clone($clone);
+ }
+ this.buildingBlock.create_overlay(this.$target);
+ return false;
+ },
+
+ on_remove: function (event) {
+ event.preventDefault();
+ this.on_blur();
+
+ this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
+
+ var index = _.indexOf(this.buildingBlock.snippets, this.$target.get(0));
+ for (var i in this.styles){
+ this.styles[i].on_remove();
+ }
+ delete this.buildingBlock.snippets[index];
+
+ var $editable = this.$target.closest(".o_editable");
+
+ // remove node and his empty
+ var node = this.$target.parent()[0];
+
+ this.$target.remove();
+ this.$overlay.remove();
+
+ if (node && node.firstChild) {
+ $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
+ if (!node.firstChild.tagName && node.firstChild.textContent === " ") {
+ node.firstChild.parentNode.removeChild(node.firstChild);
+ }
+ }
+
+ // clean editor if they are image or table in deleted content
+ $(".note-control-selection").hide();
+ $('.o_table_handler').remove();
+
+ return false;
+ },
+
+ /*
+ * 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();
+ }
+ },
+
+ /* on_focus
+ * This method is called when the user click inside the snippet in the dom
+ */
+ on_focus : function () {
+ this.$overlay.addClass('oe_active');
+ for (var i in this.styles){
+ this.styles[i].on_focus();
+ }
+ },
+
+ /* on_focus
+ * This method is called when the user click outside the snippet in the dom, after a focus
+ */
+ on_blur : function () {
+ for (var i in this.styles){
+ this.styles[i].on_blur();
+ }
+ this.$overlay.removeClass('oe_active');
+ },
+});
+
+var data = {
+ 'Class': BuildingBlock,
+ 'globalSelector': globalSelector,
+};
+return data;
+
+});
diff --git a/addons/web_editor/static/src/js/snippets.options.js b/addons/web_editor/static/src/js/snippets.options.js
new file mode 100644
index 00000000000..a46c00baa25
--- /dev/null
+++ b/addons/web_editor/static/src/js/snippets.options.js
@@ -0,0 +1,534 @@
+odoo.define('web_editor.snippets.options', function (require) {
+'use strict';
+
+var Class = require('web.Class');
+var ajax = require('web.ajax');
+var core = require('web.core');
+var base = require('web_editor.base');
+var editor = require('web_editor.editor');
+var widget = require('web_editor.widget');
+var animation = require('web_editor.snippets.animation');
+
+var qweb = core.qweb;
+
+/* ----- Editor option (object link the the xml with data-js) ---- */
+
+var Option = Class.extend({
+ // initialisation (don't overwrite)
+ init: function (BuildingBlock, editor, $target, option_id) {
+ this.buildingBlock = BuildingBlock;
+ this.editor = editor;
+ this.$target = $target;
+ var option = this.buildingBlock.templateOptions[option_id];
+ var styles = this.$target.data("snippet-option-ids") || {};
+ styles[option_id] = this;
+ this.$target.data("snippet-option-ids", styles);
+ this.$overlay = this.$target.data('overlay') || $('<div>');
+ this.option= option_id;
+ this.$el = option.$el.find(">li").clone();
+ this.data = option.$el.data();
+ },
+
+ // helper for this.$target.find
+ $: function (selector) {
+ return this.$target(selector);
+ },
+
+ _bind_li_menu: function () {
+ this.$el.filter("li:hasData").find('a:first')
+ .off('mouseenter click')
+ .on('mouseenter click', _.bind(this._mouse, this));
+
+ this.$el
+ .off('mouseenter click', "li:hasData a")
+ .on('mouseenter click', "li:hasData a", _.bind(this._mouse, this));
+
+ this.$el.closest("ul").add(this.$el)
+ .off('mouseleave')
+ .on('mouseleave', _.bind(this.reset, this));
+
+ this.$el
+ .off('mouseleave', "ul")
+ .on('mouseleave', "ul", _.bind(this.reset, this));
+
+ this.reset_methods = [];
+ },
+
+ /**
+ * this method handles mouse:over and mouse:leave on the snippet editor menu
+ */
+ _time_mouseleave: null,
+ _mouse: function (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]);
+ }
+ },
+ /*
+ * select and set item active or not (add highlight item and his parents)
+ * called before start
+ */
+ set_active: function () {
+ var classes = _.uniq((this.$target.attr("class") || '').split(/\s+/));
+ this.$el.find('[data-toggle_class], [data-select_class]')
+ .add(this.$el)
+ .filter('[data-toggle_class], [data-select_class]')
+ .removeClass("active")
+ .filter('[data-toggle_class="' + classes.join('"], [data-toggle_class="') + '"] ,'+
+ '[data-select_class="' + classes.join('"], [data-select_class="') + '"]')
+ .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 = self.$el.add(self.$el.find('li')).filter('.active').get();
+ lis.reverse();
+ _.each(lis, function (li) {
+ var $li = $(li);
+ for (var k in self.reset_methods) {
+ var method = self.reset_methods[k];
+ if ($li.is('[data-'+method+']') || $li.closest('[data-'+method+']').size()) {
+ delete self.reset_methods[k];
+ }
+ }
+ self.select("reset", $li);
+ });
+
+ for (var k in self.reset_methods) {
+ var method = self.reset_methods[k];
+ if (method) {
+ self[method]("reset", null);
+ }
+ }
+ self.reset_methods = [];
+ self.$target.trigger("snippet-option-reset", [this]);
+ },
+
+ // call data-method args as method (data-only can be used)
+ select: function (type, $li) {
+ var self = this,
+ $methods = [],
+ el = $li[0];
+
+ if ($li.data('only') && type !== $li.data('only')) {
+ return;
+ }
+
+ if (type==="click") {
+ this.reset();
+ this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
+ }
+
+ function filter (k) { return k !== 'oeId' && k !== 'oeModel' && k !== 'oeField' && k !== 'oeXpath' && k !== 'oeSourceId' && k !== 'only';}
+ function hasData(el) {
+ for (var k in el.dataset) {
+ if (filter (k)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ function method(el) {
+ var data = {};
+ for (var k in el.dataset) {
+ if (filter (k)) {
+ data[k] = el.dataset[k];
+ }
+ }
+ return data;
+ }
+
+ while (el && this.$el.is(el) || _.some(this.$el.map(function () {return $.contains(this, el);}).get()) ) {
+ if (hasData(el)) {
+ $methods.push(el);
+ }
+ el = el.parentNode;
+ }
+
+ $methods.reverse();
+
+ _.each($methods, function (el) {
+ var $el = $(el);
+ var methods = method(el);
+
+ for (var k in methods) {
+ if (self[k]) {
+ if (type !== "reset" && self.reset_methods.indexOf(k) === -1) {
+ self.reset_methods.push(k);
+ }
+ self[k](type, methods[k], $el);
+ } else {
+ console.error("'"+self.option+"' snippet have not method '"+k+"'");
+ }
+ }
+ });
+ },
+
+ // default method for snippet
+ toggle_class: function (type, value, $li) {
+ var $lis = this.$el.find('[data-toggle_class]').add(this.$el).filter('[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);
+ this.$target.addClass(active_classes);
+
+ if (type !== 'reset') {
+ this.$target.toggleClass(value);
+ }
+ },
+ select_class: function (type, value, $li) {
+ var $lis = this.$el.find('[data-select_class]').add(this.$el).filter('[data-select_class]');
+
+ var classes = $lis.map(function () {return $(this).data('select_class');}).get();
+
+ this.$target.removeClass(classes.join(" "));
+ if(value) this.$target.addClass(value);
+ },
+ 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
+});
+
+/* ----- default options ---- */
+
+var media = Option.extend({
+ start: function () {
+ var self =this;
+ this._super();
+ animation.start(true, this.$target);
+ this.$target.closest(".o_editable").on("content_changed", function () {
+ if (!self.$target.parent().length) {
+ self.$target.remove();
+ self.$overlay.remove();
+ }
+ });
+ },
+ edition: function (type, value) {
+ if(type !== "click") return;
+ var self = this;
+ var _editor = new widget.MediaDialog(this.$target.closest('.o_editable'), this.$target[0]);
+ _editor.appendTo(document.body);
+ _editor.on('saved', this, function (item, old) {
+ self.editor.on_blur();
+ self.buildingBlock.make_active(false);
+ if (self.$target.parent().data("oe-field") !== "image") {
+ setTimeout(function () {
+ self.buildingBlock.make_active($(item));
+ },0);
+ }
+ });
+ },
+ on_focus : function () {
+ var self = this;
+ var $parent = this.$target.parent();
+
+ if ($parent.data("oe-field") === "image" && $parent.hasClass('o_editable')) {
+ this.$overlay.addClass("hidden");
+ self.edition('click', null);
+ self.buildingBlock.make_active(false);
+ }
+ }
+});
+
+var colorpicker = Option.extend({
+ start: function () {
+ var self = this;
+ var res = this._super();
+
+ if (!this.$el.find('.colorpicker').length) {
+ this.$el.find('li').append( qweb.render('web_editor.colorpicker') );
+ }
+ 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").map(function () {
+ var $color = $(this);
+ var color = $color.attr("class");
+ if (self.$target.hasClass(color)) {
+ self.color = color;
+ $color.parent().addClass("selected");
+ }
+ classes.push(color);
+ });
+ this.classes = classes.join(" ");
+
+ this.bind_events();
+ return res;
+ },
+ bind_events: function () {
+ var self = this;
+ var $td = this.$el.find(".colorpicker td");
+ var $colors = this.$el.find(".colorpicker button");
+ $colors
+ .mouseenter(function () {
+ self.$target.removeClass(self.classes).addClass($(this).attr("class"));
+ })
+ .mouseleave(function () {
+ self.$target.removeClass(self.classes)
+ .addClass($td.filter(".selected").children().attr("class"));
+ })
+ .click(function () {
+ $td.removeClass("selected");
+ $(this).parent().addClass("selected");
+ });
+ }
+});
+
+var background = Option.extend({
+ start: function ($change_target) {
+ this.$bg = $change_target || this.$target;
+ this._super();
+ var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
+ if (this.$bg.hasClass('oe_custom_bg')) {
+ this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src);
+ }
+ },
+ background: function(type, value, $li) {
+ if (value && value.length) {
+ this.$bg.attr("style", 'background-image: url(' + value + ')' + (this.$bg.attr("style") || '').replace(/background-image:[^;]+/, '') );
+ this.$bg.addClass("oe_img_bg");
+ } else {
+ this.$bg.css("background-image", "");
+ this.$bg.removeClass("oe_img_bg").removeClass("oe_custom_bg");
+ }
+ },
+ choose_image: function(type, value, $li) {
+ if(type !== "click") return;
+
+ var self = this;
+ var $image = $('<img class="hidden"/>');
+ $image.attr("src", value);
+ $image.appendTo(self.$bg);
+
+ var _editor = new widget.MediaDialog(null, $image[0]);
+ _editor.appendTo(document.body);
+ _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+
+ _editor.on('saved', self, function () {
+ var value = $image.attr("src");
+ $image.remove();
+ self.$el.find('li[data-choose_image]').data("background", value).attr("data-background", value);
+ self.background(type, value,$li);
+ self.$bg.addClass('oe_custom_bg');
+ self.$bg.trigger("snippet-option-change", [self]);
+ self.set_active();
+ });
+ _editor.on('cancel', self, function () {
+ $image.remove();
+ });
+ },
+ set_active: function () {
+ var self = this;
+ var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
+ this._super();
+
+ this.$el.find('li[data-background]:not([data-background=""])')
+ .removeClass("active")
+ .each(function () {
+ var background = $(this).data("background") || $(this).attr("data-background");
+ if ((src.length && background.length && src.indexOf(background) !== -1) || (!src.length && !background.length)) {
+ $(this).addClass("active");
+ }
+ });
+
+ if (!this.$el.find('li[data-background].active').size()) {
+ this.$el.find('li[data-background=""]:not([data-choose_image])').addClass("active");
+ } else {
+ this.$el.find('li[data-background=""]:not([data-choose_image])').removeClass("active");
+ }
+ }
+});
+
+/* t-field options */
+
+var many2one = Option.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();
+ },
+
+ 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.parent.rte.historyRecordUndo(this.$target);
+
+ 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);
+ }
+});
+
+/* end*/
+
+return {
+ 'registry': {
+ 'media': media,
+ 'colorpicker': colorpicker,
+ 'background': background,
+ 'many2one': many2one,
+ },
+ 'Class': Option
+};
+
+});
diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js
new file mode 100644
index 00000000000..56dd3249a6c
--- /dev/null
+++ b/addons/web_editor/static/src/js/summernote.js
@@ -0,0 +1,2192 @@
+odoo.define('web_editor.summernote', function (require) {
+'use strict';
+
+var core = require('web.core');
+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;
+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.editor;
+var renderer = $.summernote.renderer;
+var options = $.summernote.options;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* Add method to Summernote*/
+
+dom.hasContentAfter = function (node) {
+ var next;
+ while (node.nextSibling) {
+ next = node.nextSibling;
+ if (next.tagName || next.textContent.match(/\S|\u00A0/) || dom.isBR(next)) return next;
+ node = next;
+ }
+};
+dom.hasContentBefore = function (node) {
+ var prev;
+ while (node.previousSibling) {
+ prev = node.previousSibling;
+ if (prev.tagName || prev.textContent.match(/\S|\u00A0/) || dom.isBR(prev)) return prev;
+ node = prev;
+ }
+};
+dom.ancestorHaveNextSibling = function (node, pred) {
+ pred = pred || dom.hasContentAfter;
+ while (!node.nextSibling || !pred(node)) { node = node.parentNode; }
+ return node;
+};
+dom.ancestorHavePreviousSibling = function (node, pred) {
+ pred = pred || dom.hasContentBefore;
+ while (!node.previousSibling || !pred(node)) { node = node.parentNode; }
+ return node;
+};
+dom.nextElementSibling = function (node) {
+ while (node) {
+ node = node.nextSibling;
+ if (node && node.tagName) {
+ break;
+ }
+ }
+ return node;
+};
+dom.previousElementSibling = function (node) {
+ while (node) {
+ node = node.previousSibling;
+ if (node && node.tagName) {
+ break;
+ }
+ }
+ return node;
+};
+dom.lastChild = function (node) {
+ while (node.lastChild) { node = node.lastChild; }
+ return node;
+};
+dom.firstChild = function (node) {
+ while (node.firstChild) { node = node.firstChild; }
+ return node;
+};
+dom.lastElementChild = function (node, deep) {
+ node = deep ? dom.lastChild(node) : node.lastChild;
+ return !node || node.tagName ? node : dom.previousElementSibling(node);
+};
+dom.firstElementChild = function (node, deep) {
+ node = deep ? dom.firstChild(node) : node.firstChild;
+ return !node || node.tagName ? node : dom.nextElementSibling(node);
+};
+dom.isEqual = function (prev, cur) {
+ if (prev.tagName !== cur.tagName) {
+ return false;
+ }
+ if ((prev.attributes ? prev.attributes.length : 0) !== (cur.attributes ? cur.attributes.length : 0)) {
+ return false;
+ }
+
+ function strip(text) {
+ return text && text.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
+ }
+ var att, att2;
+ loop_prev:
+ for(var a in prev.attributes) {
+ att = prev.attributes[a];
+ for(var b in cur.attributes) {
+ att2 = cur.attributes[b];
+ if (att.name === att2.name) {
+ if (strip(att.value) != strip(att2.value)) return false;
+ continue loop_prev;
+ }
+ }
+ return false;
+ }
+ 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];
+ if (attr.attributeName !== 'style') {
+ return false;
+ }
+ }
+ return true;
+};
+dom.hasProgrammaticStyle = function (node) {
+ var styles = ["float", "display", "position", "top", "left", "right", "bottom"];
+ for (var i = 0; i < node.style.length; i++) {
+ var style = node.style[i];
+ if (styles.indexOf(style) !== -1) {
+ return true;
+ }
+ }
+ return false;
+};
+dom.mergeFilter = function (prev, cur, parent) {
+ // merge text nodes
+ if (prev && (dom.isText(prev) || (dom.pasteTextApply.indexOf(prev.tagName) !== -1 && prev !== cur.parentNode)) && dom.isText(cur)) {
+ return true;
+ }
+ if (prev && prev.tagName === "P" && dom.isText(cur)) {
+ return true;
+ }
+ if (prev && dom.isText(cur) && !cur.textContent.match(/\S|\u00A0/) && (dom.isText(prev) || prev.textContent.match(/\S|\u00A0/))) {
+ return true;
+ }
+ if (prev && !dom.isBR(prev) && dom.isEqual(prev, cur) &&
+ ((prev.tagName && window.getComputedStyle(prev).display === "inline" &&
+ cur.tagName && window.getComputedStyle(cur).display === "inline"))) {
+ return true;
+ }
+ if (dom.isEqual(parent, cur) &&
+ ((parent.tagName && window.getComputedStyle(parent).display === "inline" &&
+ cur.tagName && window.getComputedStyle(cur).display === "inline"))) {
+ return true;
+ }
+ if (parent && cur.tagName === "FONT" && (!cur.firstChild || (!cur.attributes.getNamedItem('style') && !cur.className.length))) {
+ return true;
+ }
+ // On backspace, webkit browsers create a <span> with a bunch of
+ // inline styles "remembering" where they come from.
+ // chances are we had e.g.
+ // <p>foo</p>
+ // <p>bar</p>
+ // merged the lines getting this in webkit
+ // <p>foo<span>bar</span></p>
+ if (parent && cur.tagName === "SPAN" && dom.hasOnlyStyle(cur) && !dom.hasProgrammaticStyle(cur)) {
+ return true;
+ }
+};
+dom.doMerge = function (prev, cur) {
+ if (prev.tagName) {
+ if (prev.childNodes.length && !prev.textContent.match(/\S/) && dom.firstElementChild(prev) && dom.isBR(dom.firstElementChild(prev))) {
+ prev.removeChild(dom.firstElementChild(prev));
+ }
+ if (cur.tagName) {
+ while (cur.firstChild) {
+ prev.appendChild(cur.firstChild);
+ }
+ cur.parentNode.removeChild(cur);
+ } else {
+ prev.appendChild(cur);
+ }
+ } else {
+ if (cur.tagName) {
+ var deep = cur;
+ while (deep.tagName && deep.firstChild) {deep = deep.firstChild;}
+ prev.appendData(deep.textContent);
+ cur.parentNode.removeChild(cur);
+ } else {
+ prev.appendData(cur.textContent);
+ cur.parentNode.removeChild(cur);
+ }
+ }
+};
+dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
+ mergeFilter = mergeFilter || dom.mergeFilter;
+ var _merged = false;
+ var add = all || false;
+
+ if (!begin) {
+ begin = node;
+ while(begin.firstChild) {begin = begin.firstChild;}
+ so = 0;
+ } else if (begin.tagName && begin.childNodes[so]) {
+ begin = begin.childNodes[so];
+ so = 0;
+ }
+ if (!end) {
+ end = node;
+ 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;
+ }
+
+ begin = dom.firstChild(begin);
+ if (dom.isText(begin) && so > begin.textContent.length) {
+ so = 0;
+ }
+ end = dom.firstChild(end);
+ if (dom.isText(end) && eo > end.textContent.length) {
+ eo = 0;
+ }
+
+ function __merge (node) {
+ var merged = false;
+ var prev;
+ for (var k=0; k<node.childNodes.length; k++) {
+ var cur = node.childNodes[k];
+
+ if (cur === begin) {
+ if (!all) add = true;
+ }
+
+ __merge(cur);
+ dom.orderClass(dom.node(cur));
+
+ if (!add || !cur) continue;
+ if (cur === end) {
+ if (!all) add = false;
+ }
+
+ // create the first prev value
+ if (!prev) {
+ if (mergeFilter.call(dom, prev, cur, node)) {
+ prev = prev || cur.previousSibling;
+ dom.moveTo(cur, cur.parentNode, cur);
+ k--;
+ } else {
+ prev = cur;
+ }
+ continue;
+ } else if (mergeFilter.call(dom, null, cur, node)) { // merge with parent
+ prev = prev || cur.previousSibling;
+ dom.moveTo(cur, cur.parentNode, cur);
+ k--;
+ continue;
+ }
+
+ // merge nodes
+ if (mergeFilter.call(dom, prev, cur, node)) {
+ var p = prev;
+ var c = cur;
+ // compute prev/end and offset
+ if (prev.tagName) {
+ if (cur.tagName) {
+ if (cur === begin) begin = prev;
+ if (cur === end) end = prev;
+ }
+ } else {
+ if (cur.tagName) {
+ var deep = cur;
+ while (deep.tagName && deep.lastChild) {deep = deep.lastChild;}
+ if (deep === begin) {
+ so += prev.textContent.length;
+ begin = prev;
+ }
+ if (deep === end) {
+ eo += prev.textContent.length;
+ end = prev;
+ }
+ } else {
+ // merge text nodes
+ if (cur === begin) {
+ so += prev.textContent.length;
+ begin = prev;
+ }
+ if (cur === end) {
+ eo += prev.textContent.length;
+ end = prev;
+ }
+ }
+ }
+
+ dom.doMerge(p, c);
+
+ merged = true;
+ k--;
+ continue;
+ }
+
+ prev = cur;
+ }
+
+ // an other loop to merge the new shibbing nodes
+ if (merged) {
+ _merged = true;
+ __merge(node);
+ }
+ }
+ if (node) {
+ __merge(node);
+ }
+
+ return {
+ merged: _merged,
+ sc: begin,
+ ec: end,
+ so: so,
+ eo: eo
+ };
+};
+dom.autoMerge = function (target, previous) {
+ var node = dom.lastChild(target);
+ var nodes = [];
+ var temp;
+
+ while (node) {
+ nodes.push(node);
+ if (temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node))) {
+ if (!dom.isText(node) && !dom.isMergable(node) && temp.tagName !== node.tagName) {
+ nodes = [];
+ }
+ break;
+ }
+ node = node.parentNode;
+ }
+
+ while (nodes.length) {
+ node = nodes.pop();
+ if (node && (temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node))) &&
+ temp.tagName === node.tagName &&
+ !dom.isText(node) &&
+ dom.isMergable(node) &&
+ !dom.isNotBreakable(node) && !dom.isNotBreakable(previous ? dom.previousElementSibling(node) : dom.nextElementSibling(node))) {
+
+ if (previous) {
+ dom.doMerge(temp, node);
+ } else {
+ dom.doMerge(node, temp);
+ }
+ }
+ }
+};
+dom.removeSpace = function (node, begin, so, end, eo) {
+ var removed = false;
+ var add = node === begin;
+
+ if (node === begin && begin === end && dom.isBR(node)) {
+ return {
+ removed: removed,
+ sc: begin,
+ ec: end,
+ so: so,
+ eo: eo
+ };
+ }
+
+ (function __remove_space (node) {
+ if (!node) return;
+ for (var k=0; k<node.childNodes.length; k++) {
+ var cur = node.childNodes[k];
+
+ if (cur === begin) add = true;
+
+ if (cur.tagName && cur.tagName !== "SCRIPT" && cur.tagName !== "STYLE" && window.getComputedStyle(cur).whiteSpace !== "pre") {
+ __remove_space(cur);
+ }
+
+ if (!add) continue;
+ if (cur === end) add = false;
+
+ // remove begin empty text node
+ if (node.childNodes.length > 1 && dom.isText(cur) && !cur.textContent.match(/\S|\u00A0/)) {
+ removed = true;
+ if (cur === begin) {
+ so = 0;
+ begin = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(cur)));
+ }
+ if (cur === end) {
+ eo = 1;
+ end = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(cur)));
+ if (dom.isText(end)) {
+ eo = end.textContent.length;
+ }
+ }
+ cur.parentNode.removeChild(cur);
+ begin = dom.lastChild(begin);
+ end = dom.lastChild(end);
+ k--;
+ continue;
+ }
+
+ // convert HTML space
+ if (dom.isText(cur)) {
+ var text;
+ var exp1 = /[\t\n\r ]+/g;
+ var exp2 = /(?!([ ]|\u00A0)|^)\u00A0(?!([ ]|\u00A0)|$)/g;
+ if (cur === begin) {
+ var temp = cur.textContent.substr(0, so);
+ var _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
+ so -= temp.length - _temp.length;
+ }
+ if (cur === end) {
+ var temp = cur.textContent.substr(0, eo);
+ var _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
+ eo -= temp.length - _temp.length;
+ }
+ var text = cur.textContent.replace(exp1, ' ').replace(exp2, ' ');
+ removed = removed || cur.textContent.length !== text.length;
+ cur.textContent = text;
+ }
+ }
+ })(node);
+
+ return {
+ removed: removed,
+ sc: begin,
+ ec: end,
+ so: !dom.isBR(begin) && so > 0 ? so : 0,
+ eo: eo
+ };
+};
+dom.node = function (node) {
+ return node.tagName ? node : node.parentNode;
+};
+dom.pasteTextApply = "h1 h2 h3 h4 h5 h6 li p".split(" ");
+dom.pasteTextClose = "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li pre".split(" ");
+dom.pasteText = function (textNode, offset, text, isOnlyText) {
+ // clean the node
+ var node = dom.node(textNode);
+ var data = dom.merge(node.parentNode, textNode, offset, textNode, offset, null, true);
+ data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
+ // Break the text node up
+ if (data.sc.tagName) {
+ if (node === data.sc.parentNode) {
+ data.sc = node.insertBefore(document.createTextNode(" "), data.sc);
+ } else if (node.firstChild && !dom.isBR(dom.firstChild(node))) {
+ data.sc = node.insertBefore(document.createTextNode(" "), dom.firstChild(node));
+ } else {
+ data.sc = node.appendChild(document.createTextNode(" "));
+ }
+ data.so = 0;
+ }
+ data.sc.splitText(data.so);
+ var first = data.sc;
+ var last = data.sc.nextSibling;
+
+ isOnlyText = isOnlyText || !text.match('\n');
+
+ if (!isOnlyText) {
+ // tag to close and open
+ var tag = node.tagName.toLowerCase();
+ if(dom.pasteTextApply.indexOf(tag) === -1) {
+ text = text.split('\n').join("<br/>");
+ } else {
+ text = "<"+tag+">"+text.split('\n').join("</"+tag+"><"+tag+">")+"</"+tag+">";
+ }
+
+ var $text = $(text);
+
+ // split parent node and insert text
+ if(dom.pasteTextClose.indexOf(tag) !== -1) {
+ var $next = $(node).clone().empty();
+ $next.append( last );
+ $(node).after( $next );
+ $(node).after( $text );
+ } else {
+ $(data.sc).after( $text );
+ }
+ } else {
+ first.appendData( text );
+ }
+
+ // clean the dom content
+ data = dom.merge(node.parentNode.parentNode, last, 0, last, 0, null, true);
+ data = dom.removeSpace(node.parentNode.parentNode, data.sc, data.so, data.ec, data.eo);
+
+ // move caret
+ range.create(data.sc, data.so, data.ec, data.eo).select();
+};
+dom.removeBetween = function (sc, so, ec, eo, towrite) {
+ if (ec.tagName) {
+ if (ec.childNodes[eo]) {
+ ec = ec.childNodes[eo];
+ eo = 0;
+ } else {
+ ec = dom.lastChild(ec);
+ eo = dom.nodeLength(ec);
+ }
+ }
+ if (sc.tagName) {
+ sc = sc.childNodes[so] || dom.firstChild(ec);
+ so = 0;
+ if (!dom.hasContentBefore(sc)) {
+ sc.parentNode.insertBefore(document.createTextNode('\u00A0'), sc);
+ }
+ }
+ if (!eo && sc !== ec) {
+ ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(ec)));
+ eo = ec.textContent.length;
+ }
+
+ var ancestor = dom.commonAncestor(sc.tagName ? sc.parentNode : sc, ec.tagName ? ec.parentNode : ec) || dom.ancestor(sc, dom.isEditable);
+
+ if (!dom.isContentEditable(ancestor)) {
+ return {
+ sc: sc,
+ so: so,
+ ec: sc,
+ eo: eo
+ };
+ }
+
+ if (ancestor.tagName) {
+ var ancestor_sc = sc;
+ var ancestor_ec = ec;
+ while (ancestor !== ancestor_sc && ancestor !== ancestor_sc.parentNode) { ancestor_sc = ancestor_sc.parentNode; }
+ while (ancestor !== ancestor_ec && ancestor !== ancestor_ec.parentNode) { ancestor_ec = ancestor_ec.parentNode; }
+
+
+ var node = dom.node(sc);
+ if (!dom.isNotBreakable(node)) {
+ sc = dom.splitTree(ancestor_sc, {'node': sc, 'offset': so});
+ }
+ var before = dom.hasContentBefore(dom.ancestorHavePreviousSibling(sc));
+
+ var after;
+ if (ec.textContent.slice(eo, Infinity).match(/\S|\u00A0/)) {
+ after = dom.splitTree(ancestor_ec, {'node': ec, 'offset': eo});
+ } else {
+ after = dom.hasContentAfter(dom.ancestorHaveNextSibling(ec));
+ }
+
+ var nodes = dom.listBetween(sc, ec);
+
+ var ancestor_first_last = function (node) {
+ return node === before || node === after;
+ };
+
+ for (var i=0; i<nodes.length; i++) {
+ if (!dom.ancestor(nodes[i], ancestor_first_last) && !$.contains(nodes[i], before) && !$.contains(nodes[i], after)) {
+ nodes[i].parentNode.removeChild(nodes[i]);
+ }
+ }
+
+ sc = before ? dom.lastChild(before) : dom.firstChild(after);
+ so = sc.textContent.length;
+
+ if (before) {
+ var text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
+ so -= sc.textContent.length - text.length;
+ sc.textContent = text;
+ }
+ if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) {
+ var br = $("<br/>")[0];
+ node.appendChild(br);
+ sc = br;
+ so = 0;
+ }
+ dom.autoMerge(sc, false);
+
+ } else {
+
+ var text = ancestor.textContent;
+ ancestor.textContent = text.slice(0, so) + text.slice(eo, Infinity).replace(/^[ \t\n\r]+/, '\u00A0');
+
+ }
+
+ eo = so;
+ if(!dom.isBR(sc) && !sc.textContent.match(/\S|\u00A0/) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
+ ancestor = dom.node(sc);
+ var text = document.createTextNode('\u00A0');
+ $(sc).before(text);
+ sc = text;
+ so = 0;
+ eo = 1;
+ }
+ return {
+ sc: sc,
+ so: so,
+ ec: sc,
+ eo: eo
+ };
+};
+dom.indent = function (node) {
+ var margin = parseFloat(node.style.marginLeft || 0)+1.5;
+ node.style.marginLeft = margin + "em";
+ return margin;
+};
+dom.outdent = function (node) {
+ var margin = parseFloat(node.style.marginLeft || 0)-1.5;
+ node.style.marginLeft = margin > 0 ? margin + "em" : "";
+ return margin;
+};
+dom.scrollIntoViewIfNeeded = function (node) {
+ var node = dom.node(node);
+
+ var $span;
+ if (dom.isBR(node)) {
+ $span = $('<span/>').text('\u00A0');
+ $(node).after($span);
+ node = $span[0];
+ }
+
+ if (node.scrollIntoViewIfNeeded) {
+ node.scrollIntoViewIfNeeded(false);
+ } else {
+ var offsetParent = node.offsetParent;
+ while (offsetParent) {
+ var elY = 0;
+ var elH = node.offsetHeight;
+ var parent = node;
+
+ while (offsetParent && parent) {
+ elY += node.offsetTop;
+
+ // get if a parent have a scrollbar
+ parent = node.parentNode;
+ while (parent != offsetParent &&
+ (parent.tagName === "BODY" || ["auto", "scroll"].indexOf(window.getComputedStyle(parent).overflowY) === -1)) {
+ parent = parent.parentNode;
+ }
+ node = parent;
+
+ if (parent !== offsetParent) {
+ elY -= parent.offsetTop;
+ parent = null;
+ }
+
+ offsetParent = node.offsetParent;
+ }
+
+ if ((node.tagName === "BODY" || ["auto", "scroll"].indexOf(window.getComputedStyle(node).overflowY) !== -1) &&
+ (node.scrollTop + node.clientHeight) < (elY + elH)) {
+ node.scrollTop = (elY + elH) - node.clientHeight;
+ }
+ }
+ }
+
+ if ($span) {
+ $span.remove();
+ }
+
+ return;
+};
+dom.moveTo = function (node, target, before) {
+ var nodes = [];
+ while (node.firstChild) {
+ nodes.push(node.firstChild);
+ if (before) {
+ target.insertBefore(node.firstChild, before);
+ } else {
+ target.appendChild(node.firstChild);
+ }
+ }
+ node.parentNode.removeChild(node);
+ return nodes;
+};
+dom.isMergable = function (node) {
+ return node.tagName && "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a ul ol font".indexOf(node.tagName.toLowerCase()) !== -1;
+};
+dom.isSplitable = function (node) {
+ return node.tagName && "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a font".indexOf(node.tagName.toLowerCase()) !== -1;
+};
+dom.isRemovableEmptyNode = function (node) {
+ return "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a ul ol font span br".indexOf(node.tagName.toLowerCase()) !== -1;
+};
+dom.isForbiddenNode = function (node) {
+ return node.tagName === "BR" || $(node).is(".fa, img");
+};
+dom.listBetween = function (sc, ec) {
+ var nodes = [];
+ var ancestor = dom.commonAncestor(sc, ec);
+ dom.walkPoint({'node': sc, 'offset': 0}, {'node': ec, 'offset': 0}, function (point) {
+ if (ancestor !== point.node || ancestor === sc || ancestor === ec) {
+ nodes.push(point.node);
+ }
+ });
+ return list.unique(nodes);
+};
+dom.isNotBreakable = function (node) {
+ // avoid triple click => crappy dom
+ return !dom.isText(node) && !dom.isBR(dom.firstChild(node)) && dom.isVoid(dom.firstChild(node));
+};
+
+dom.isContentEditable = function (node) {
+ return $(node).closest('[contenteditable]').is('[contenteditable="true"]');
+};
+
+dom.isFont = function (node) {
+ var nodeName = node && node.nodeName.toUpperCase();
+ return node && (nodeName === "FONT" ||
+ (nodeName === "SPAN" && (
+ node.className.match(/(^|\s)fa(\s|$)/i) ||
+ node.className.match(/(^|\s)(text|bg)-/i) ||
+ (node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color):/i)))) );
+};
+dom.isVisibleText = function (textNode) {
+ return !!textNode.textContent.match(/\S|\u00A0/);
+};
+/**
+ * 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;
+ style = style.replace(/[\s\n\r]+/, ' ').replace(/^ ?;? ?| ?;? ?$/g, '').replace(/ ?; ?/g, ';');
+ if (!style.length) {
+ node.removeAttribute("style");
+ return null;
+ }
+ style = style.split(";");
+ style.sort();
+ style = style.join("; ")+";";
+ 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('class');
+ if (!className) return null;
+ className = className.replace(/[\s\n\r]+/, ' ').replace(/^ | $/g, '').replace(/ +/g, ' ');
+ if (!className.length) {
+ node.removeAttribute("class");
+ return null;
+ }
+ className = className.split(" ");
+ className.sort();
+ className = className.join(" ");
+ 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;
+ }
+ if (from.parentNode === to) {
+ while (from.lastChild) {
+ dom.insertAfter(from.lastChild, from);
+ }
+ } else {
+ while (from.firstChild && from.firstChild != to) {
+ to.appendChild(from.firstChild);
+ }
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) {
+ var sc = this.sc;
+ var so = this.so;
+ var ec = this.ec;
+ var eo = this.eo;
+ isNotBreakable = isNotBreakable || dom.isNotBreakable;
+
+ // search the first snippet editable node
+ var start = keep_end ? ec : sc;
+ while (start) {
+ if (isNotBreakable(start, sc, so, ec, eo)) {
+ break;
+ }
+ start = start.parentNode;
+ }
+
+ // check if the end caret have the same node
+ var lastFilterEnd;
+ var end = keep_end ? sc : ec;
+ while (end) {
+ if (start === end) {
+ break;
+ }
+ if (isNotBreakable(end, sc, so, ec, eo)) {
+ lastFilterEnd = end;
+ }
+ end = end.parentNode;
+ }
+ if (lastFilterEnd) {
+ end = lastFilterEnd;
+ }
+ if (!end) {
+ end = document.getElementsByTagName('body')[0];
+ }
+
+ // if same node, keep range
+ if (start === end || !start) {
+ return this;
+ }
+
+ // reduce or extend the range to don't break a isNotBreakable area
+ if ($.contains(start, end)) {
+
+ if (keep_end) {
+ sc = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)));
+ so = sc.textContent.length;
+ } else if (!eo) {
+ ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)));
+ eo = ec.textContent.length;
+ } else {
+ ec = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(end)));
+ eo = 0;
+ }
+ } else {
+
+ if (keep_end) {
+ sc = dom.firstChild(start);
+ so = 0;
+ } else {
+ ec = dom.lastChild(start);
+ eo = ec.textContent.length;
+ }
+ }
+
+ return new range.WrappedRange(sc, so, ec, eo);
+};
+range.WrappedRange.prototype.deleteContents = function (towrite) {
+ var prevBP = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
+
+ $(dom.node(prevBP.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
+ return new range.WrappedRange(
+ prevBP.sc,
+ prevBP.so,
+ prevBP.ec,
+ prevBP.eo
+ );
+};
+range.WrappedRange.prototype.clean = function (mergeFilter, all) {
+ var node = dom.node(this.sc === this.ec ? this.sc : this.commonAncestor());
+ if (node.childNodes.length <=1) {
+ return this;
+ }
+
+ var merge = dom.merge(node, this.sc, this.so, this.ec, this.eo, mergeFilter, all);
+ var rem = dom.removeSpace(node.parentNode, merge.sc, merge.so, merge.ec, merge.eo);
+
+ if (merge.merged || rem.removed) {
+ return range.create(rem.sc, rem.so, rem.ec, rem.eo);
+ }
+ return this;
+};
+range.WrappedRange.prototype.remove = function (mergeFilter) {
+};
+range.WrappedRange.prototype.isOnCellFirst = function () {
+ var node = dom.ancestor(this.sc, function (node) {return ["LI", "DIV", "TD","TH"].indexOf(node.tagName) !== -1;});
+ return node && ["TD","TH"].indexOf(node.tagName) !== -1;
+};
+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) {
+ return memo + '<li><a data-event="fontSize" href="#" data-value="' + v + '">' +
+ '<i class="fa fa-check"></i> ' + v +
+ '</a></li>';
+ }, '');
+
+ var sLabel = '<span class="note-current-fontsize">11</span>';
+ return renderer.getTemplate().button(sLabel, {
+ title: lang.font.size,
+ dropdown: '<ul class="dropdown-menu">' + items + '</ul>'
+ });
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* add some text commands */
+
+key.nameFromCode[46] = 'DELETE';
+key.nameFromCode[27] = 'ESCAPE';
+
+options.keyMap.pc['BACKSPACE'] = 'backspace';
+options.keyMap.pc['DELETE'] = 'delete';
+options.keyMap.pc['ENTER'] = 'enter';
+options.keyMap.pc['ESCAPE'] = 'cancel';
+options.keyMap.mac['SHIFT+TAB'] = 'untab';
+
+options.keyMap.mac['BACKSPACE'] = 'backspace';
+options.keyMap.mac['CMD+BACKSPACE'] = 'delete';
+options.keyMap.mac['SHIFT+BACKSPACE'] = 'delete';
+options.keyMap.mac['ENTER'] = 'enter';
+options.keyMap.mac['ESCAPE'] = 'cancel';
+
+$.summernote.pluginEvents.insertTable = function (event, editor, layoutInfo, sDim) {
+ var $editable = layoutInfo.editable();
+ var dimension = sDim.split('x');
+ var rng = range.create();
+ rng = rng.deleteContents();
+
+ var isBodyContainer = dom.isBodyContainer;
+ dom.isBodyContainer = dom.isNotBreakable;
+ rng.insertNode(editor.table.createTable(dimension[0], dimension[1]));
+ dom.isBodyContainer = isBodyContainer;
+
+ editor.afterCommand($editable);
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
+ var $editable = layoutInfo.editable();
+ var r = range.create();
+ var outdent = outdent || false;
+
+ if (r.isCollapsed()) {
+ if (r.isOnCell() && r.isOnCellFirst()) {
+ var td = dom.ancestor(r.sc, dom.isCell);
+ if (!outdent && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode)) {
+ range.create(td.lastChild, td.lastChild.textContent.length, td.lastChild, td.lastChild.textContent.length).select();
+ $.summernote.pluginEvents.enter(event, editor, layoutInfo);
+ } else if (outdent && !dom.previousElementSibling(td) && !$(td.parentNode).text().match(/\S/)) {
+ $.summernote.pluginEvents.backspace(event, editor, layoutInfo);
+ } else {
+ $editable.data('NoteHistory').splitNext();
+ editor.table.tab(r, outdent);
+ }
+ event.preventDefault();
+ return false;
+ }
+
+ if (!r.sc.textContent.slice(0,r.so).match(/\S/) && r.isOnList()) {
+ if (outdent) {
+ $.summernote.pluginEvents.outdent(event, editor, layoutInfo);
+ } else {
+ $.summernote.pluginEvents.indent(event, editor, layoutInfo);
+ }
+ } else {
+ if (!outdent){
+ var next = r.sc.splitText(r.so);
+ editor.typing.insertTab($editable, r, options.tabsize);
+ r = range.create(next, 0, next, 0);
+ r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
+ range.create(r.sc, r.so, r.ec, r.eo).select();
+ } 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);
+ 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();
+ }
+ event.preventDefault();
+ return false;
+ }
+ }
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.untab = function (event, editor, layoutInfo) {
+ return $.summernote.pluginEvents.tab(event, editor, layoutInfo, true);
+};
+$.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, 'enter');
+
+ var r = range.create();
+ if (!r.isContentEditable()) {
+ event.preventDefault();
+ return false;
+ }
+ if (!r.isCollapsed()) {
+ r = r.deleteContents();
+ r.select();
+ }
+
+ var br = $("<br/>")[0];
+
+ // table: add a tr
+ var td = dom.ancestor(r.sc, dom.isCell);
+ if (td && (r.sc === td || r.sc === td.lastChild || (dom.isBR(td.lastChild) && r.sc === td.lastChild.previousSibling)) && r.so === r.sc.textContent.length && r.isOnCell() && !dom.nextElementSibling(td)) {
+ var $node = $(td.parentNode);
+ var $clone = $node.clone();
+ $clone.children().html(dom.blank);
+ $node.after($clone);
+ var node = dom.firstElementChild($clone[0]) || $clone[0];
+ range.create(node, 0, node, 0).select();
+ dom.scrollIntoViewIfNeeded(br);
+ event.preventDefault();
+ return false;
+ }
+
+ var contentBefore = r.sc.textContent.slice(0,r.so).match(/\S|\u00A0/);
+ if (!contentBefore && dom.isText(r.sc)) {
+ var node = r.sc.previousSibling;
+ while (!contentBefore && node && dom.isText(node)) {
+ contentBefore = dom.isVisibleText(node);
+ node = node.previousSibling;
+ }
+ }
+
+ var node = dom.node(r.sc);
+ var last = node;
+ while (node && dom.isSplitable(node) && !dom.isList(node)) {
+ last = node;
+ node = node.parentNode;
+ }
+
+ if (last === node && !dom.isBR(node)) {
+ node = r.insertNode(br, true);
+ if (isFormatNode(last.firstChild) && $(last).closest(options.styleTags.join(',')).length) {
+ dom.moveContent(last.firstChild, last);
+ last.removeChild(last.firstChild);
+ }
+ do {
+ node = dom.hasContentAfter(node);
+ } while (node && dom.isBR(node));
+
+ // create an other br because the user can't see the new line with only br in a block
+ if (!node) {
+ $(br).before($("<br/>")[0]);
+ }
+ node = br.nextSibling || br;
+ } else if (last === node && dom.isBR(node)) {
+ $(node).after(br);
+ node = br;
+ } else if (!r.so && r.isOnList() && !r.sc.textContent.length && !dom.ancestor(r.sc, dom.isLi).nextElementSibling) {
+ // double enter on the end of a list = new line out of the list
+ node = $('<p></p>').append(br).insertAfter(dom.ancestor(r.sc, dom.isList))[0];
+ node = br;
+ } else if (last === r.sc) {
+ if (dom.isBR(last)) {
+ last = last.parentNode;
+ }
+ var $node = $(last);
+ var $clone = $node.clone().text("");
+ $node.after($clone);
+ node = dom.node(dom.firstElementChild($clone[0]) || $clone[0]);
+ $(node).html(br);
+ node = br;
+ } else {
+ node = dom.splitTree(last, {'node': r.sc, 'offset': r.so});
+ if (!contentBefore) {
+ var cur = dom.node(dom.lastChild(node.previousSibling));
+ if (!dom.isBR(cur)) {
+ $(cur).html(br);
+ }
+ }
+ if (!node.textContent.match(/\S|\u00A0/)) {
+ node = dom.firstChild(node);
+ $(dom.node( dom.isBR(node) ? node.parentNode : node )).html(br);
+ node = br;
+ }
+ }
+
+ if (node) {
+ node = dom.firstChild(node);
+ if (dom.isBR(node)) {
+ range.createFromNode(node).select();
+ } else {
+ range.create(node,0).select();
+ }
+ dom.scrollIntoViewIfNeeded(node);
+ }
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, "visible");
+
+ var r = range.create();
+ if (r && !r.isCollapsed()) {
+ r = r.deleteContents(true);
+ r.select();
+ }
+
+ // don't write in forbidden tag (like span for font awsome)
+ var node = dom.firstChild(r.sc.tagName && r.so ? r.sc.childNodes[r.so] : r.sc);
+ while (node.parentNode) {
+ if (dom.isForbiddenNode(node)) {
+ var text = node.previousSibling;
+ if (text && dom.isText(text) && text.textContent.match(/\S|\u00A0/)) {
+ range.create(text, text.textContent.length, text, text.textContent.length).select();
+ } else {
+ text = node.parentNode.insertBefore(document.createTextNode( "." ), node);
+ range.create(text, 1, text, 1).select();
+ setTimeout(function () {
+ var text = range.create().sc;
+ text.textContent = text.textContent.replace(/^./, '');
+ range.create(text, text.textContent.length, text, text.textContent.length).select();
+ },0);
+ }
+ break;
+ }
+ node = node.parentNode;
+ }
+
+ 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(sc, ec) {
+ var nodes = dom.listBetween(sc, ec);
+ nodes.push(dom.node(sc), dom.node(ec));
+ for (var i in nodes) {
+ if (dom.isCell(nodes[i])) {
+ $(nodes[i]).html("<br/>");
+ }
+ }
+ event.preventDefault();
+ return false;
+}
+
+$.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, "delete");
+
+ var r = range.create();
+ if (!r.isContentEditable()) {
+ return false;
+ }
+ if (!r.isCollapsed()) {
+ if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) {
+ return remove_table_content(r.sc, r.ec);
+ }
+ r = r.deleteContents();
+ r.select();
+ event.preventDefault();
+ return false;
+ }
+
+ var target = r.ec;
+ var offset = r.eo;
+ if (target.tagName && target.childNodes[offset]) {
+ target = target.childNodes[offset];
+ offset = 0;
+ }
+
+ var node = dom.node(target);
+ var data = dom.merge(node, target, offset, target, offset, null, true);
+ data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
+ r = range.create(data.sc, data.so);
+ r.select();
+ target = r.sc;
+ offset = r.so;
+
+ while (!dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;}
+
+ var contentAfter = target.textContent.slice(offset,Infinity).match(/\S|\u00A0/);
+ var content = target.textContent.replace(/[ \t\r\n]+$/, '');
+ var temp;
+ var temp2;
+
+ // media
+ if (dom.isImg(node) || (!contentAfter && dom.isImg(dom.hasContentAfter(node)))) {
+ var parent;
+ var index;
+ if (!dom.isImg(node)) {
+ node = dom.hasContentAfter(node);
+ }
+ while (dom.isImg(node)) {
+ parent = node.parentNode;
+ index = dom.makeOffsetPath(parent, node)[0];
+ if (index>0) {
+ var next = node.previousSibling;
+ range.create(next,next.textContent.length).select();
+ }
+ if (!dom.hasContentAfter(node) && !dom.hasContentBefore(node)) {
+ parent.appendChild($('<br/>')[0]);
+ }
+ parent.removeChild(node);
+ node = parent;
+ }
+ }
+ // empty tag
+ else if (!content.length && target.tagName && dom.isRemovableEmptyNode(dom.isBR(target) ? target.parentNode : target)) {
+ if (node === $editable[0] || $.contains(node, $editable[0])) {
+ event.preventDefault();
+ return false;
+ }
+ var before = false;
+ var next = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
+ if (!dom.isContentEditable(next)) {
+ before = true;
+ next = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
+ }
+ dom.removeSpace(next.parentNode, next, 0, next, 0); // clean before jump for not select invisible space between 2 tag
+ next = dom.firstChild(next);
+ node.parentNode.removeChild(node);
+ range.create(next, before ? next.textContent.length : 0).select();
+ }
+ // normal feature if same tag and not the end
+ else if (contentAfter) {
+ return true;
+ }
+ // merge with the next text node
+ else if (dom.isText(target) && (temp = dom.hasContentAfter(target)) && dom.isText(temp)) {
+ return true;
+ }
+ //merge with the next block
+ else if ((temp = dom.ancestorHaveNextSibling(target)) &&
+ dom.isMergable(temp) &&
+ dom.isMergable(temp2 = dom.hasContentAfter(temp)) &&
+ temp.tagName === temp2.tagName &&
+ (temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
+ !dom.isNotBreakable(temp) &&
+ !dom.isNotBreakable(temp2)) {
+ dom.autoMerge(target, false);
+ var next = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(target)));
+ if (dom.isBR(next)) {
+ range.create(next.previousSibling, next.previousSibling.textContent.length).select();
+ next.parentNode.removeChild(next);
+ } else {
+ range.create(next, 0).select();
+ }
+ }
+ // jump to next node for delete
+ else if ((temp = dom.ancestorHaveNextSibling(target)) && (temp2 = dom.hasContentAfter(temp)) && dom.isContentEditable(temp2)) {
+
+ dom.removeSpace(temp2.parentNode, temp2, 0, temp, 0); // clean before jump for not select invisible space between 2 tag
+ temp2 = dom.firstChild(temp2);
+
+ r = range.create(temp2, 0);
+ r.select();
+
+ if ((dom.isText(temp) || window.getComputedStyle(temp).display === "inline") && (dom.isText(temp2) || window.getComputedStyle(temp2).display === "inline")) {
+ if (dom.isText(temp2)) {
+ temp2.textContent = temp2.textContent.replace(/^\s*\S/, '');
+ } else {
+ $.summernote.pluginEvents.delete(event, editor, layoutInfo);
+ }
+ }
+ }
+
+ $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, "backspace");
+
+ var r = range.create();
+ if (!r.isContentEditable()) {
+ event.preventDefault();
+ return false;
+ }
+ if (!r.isCollapsed()) {
+ if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) {
+ return remove_table_content(r.sc, r.ec);
+ }
+ r = r.deleteContents();
+ r.select();
+ event.preventDefault();
+ return false;
+ }
+
+ var target = r.sc;
+ var offset = r.so;
+ if (target.tagName && target.childNodes[offset]) {
+ target = target.childNodes[offset];
+ offset = 0;
+ }
+
+ var node = dom.node(target);
+ var data = dom.merge(node, target, offset, target, offset, null, true);
+ data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
+ r = range.create(data.sc, data.so);
+ r.select();
+ target = r.sc;
+ offset = r.so;
+
+ while (node.parentNode && !dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;}
+
+ var contentBefore = target.textContent.slice(0,offset).match(/\S|\u00A0/);
+ var content = target.textContent.replace(/[ \t\r\n]+$/, '');
+ var temp;
+ var temp2;
+
+ // delete media
+ if (dom.isImg(node) || (!contentBefore && dom.isImg(dom.hasContentBefore(node)))) {
+ if (!dom.isImg(node)) {
+ node = dom.hasContentBefore(node);
+ }
+ range.createFromNode(node).select();
+ $.summernote.pluginEvents.delete(event, editor, layoutInfo);
+ }
+ // table tr td
+ else if (r.isOnCell() && !offset && (target === (temp = dom.ancestor(target, dom.isCell)) || target === temp.firstChild)) {
+ if (dom.previousElementSibling(temp)) {
+ var td = dom.previousElementSibling(temp);
+ node = td.lastChild || td;
+ } else {
+ var tr = temp.parentNode;
+ var prevTr = dom.previousElementSibling(tr);
+ if (!$(temp.parentNode).text().match(/\S|\u00A0/)) {
+ if (prevTr) {
+ tr.parentNode.removeChild(tr);
+ node = (dom.lastElementChild(prevTr).lastChild && dom.lastElementChild(prevTr).lastChild.tagName ? dom.lastElementChild(prevTr).lastChild.previousSibling : dom.lastElementChild(prevTr).lastChild) || dom.lastElementChild(prevTr);
+ range.create(node, node.textContent.length, node, node.textContent.length).select();
+ }
+ } else {
+ node = dom.lastElementChild(prevTr).lastChild || dom.lastElementChild(prevTr);
+ }
+ }
+ if (dom.isBR(node)) {
+ range.createFromNode(node).select();
+ } else {
+ range.create(node, dom.nodeLength(node)).select();
+ }
+ }
+ // empty tag
+ else if (!content.length && target.tagName && dom.isRemovableEmptyNode(target)) {
+ if (node === $editable[0] || $.contains(node, $editable[0])) {
+ event.preventDefault();
+ return false;
+ }
+ var before = true;
+ var prev = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
+ if (!dom.isContentEditable(prev)) {
+ before = false;
+ prev = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
+ }
+ dom.removeSpace(prev.parentNode, prev, 0, prev, 0); // clean before jump for not select invisible space between 2 tag
+ prev = dom.lastChild(prev);
+ node.parentNode.removeChild(node);
+ range.createFromNode(prev).select();
+ range.create(prev, before ? prev.textContent.length : 0).select();
+ }
+ // normal feature if same tag and not the begin
+ else if (contentBefore) {
+ return true;
+ }
+ // merge with the previous text node
+ else if (dom.isText(target) && (temp = dom.hasContentBefore(target)) && (dom.isText(temp) || dom.isBR(temp))) {
+ return true;
+ }
+ //merge with the previous block
+ else if ((temp = dom.ancestorHavePreviousSibling(target)) &&
+ dom.isMergable(temp) &&
+ dom.isMergable(temp2 = dom.hasContentBefore(temp)) &&
+ temp.tagName === temp2.tagName &&
+ (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);
+ dom.autoMerge(target, true);
+ range.create(prev, 0).select();
+ }
+ // jump to previous node for delete
+ else if ((temp = dom.ancestorHavePreviousSibling(target)) && (temp2 = dom.hasContentBefore(temp)) && dom.isContentEditable(temp2)) {
+
+ dom.removeSpace(temp2.parentNode, temp2, 0, temp, 0); // clean before jump for not select invisible space between 2 tag
+ temp2 = dom.lastChild(temp2);
+
+ r = range.create(temp2, temp2.textContent.length, temp2, temp2.textContent.length);
+ r.select();
+
+ if ((dom.isText(temp) || window.getComputedStyle(temp).display === "inline") && (dom.isText(temp2) || window.getComputedStyle(temp2).display === "inline")) {
+ if (dom.isText(temp2)) {
+ temp2.textContent = temp2.textContent.replace(/\S\s*$/, '');
+ } else {
+ $.summernote.pluginEvents.backspace(event, editor, layoutInfo);
+ }
+ }
+ }
+
+ var rng = range.create();
+ if (rng) {
+ $(dom.node(rng.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
+ dom.scrollIntoViewIfNeeded(rng.sc.parentNode.previousElementSibling || rng.sc);
+ }
+
+ event.preventDefault();
+ return false;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* add list command (create a uggly dom for chrome) */
+
+function isFormatNode(node) {
+ return node.tagName && options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
+}
+
+$.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutInfo, sorted) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable);
+
+ var parent;
+ var rng = range.create();
+ var node = rng.sc;
+ while (node && node !== $editable[0]) {
+
+ parent = node.parentNode;
+ if (node.tagName === (sorted ? "UL" : "OL")) {
+
+ var ul = document.createElement(sorted ? "ol" : "ul");
+ ul.className = node.className;
+ parent.insertBefore(ul, node);
+ while (node.firstChild) {
+ ul.appendChild(node.firstChild);
+ }
+ parent.removeChild(node);
+ rng.select();
+ return;
+
+ } else if (node.tagName === (sorted ? "OL" : "UL")) {
+
+ var lis = [];
+ for (var i=0; i<node.children.length; i++) {
+ lis.push(node.children[i]);
+ }
+
+ if (parent.tagName === "LI") {
+ node = parent;
+ parent = node.parentNode;
+ _.each(lis, function (li) {
+ parent.insertBefore(li, node);
+ });
+ } else {
+ _.each(lis, function (li) {
+ while (li.firstChild) {
+ parent.insertBefore(li.firstChild, node);
+ }
+ });
+ }
+
+ parent.removeChild(node);
+ rng.select();
+ return;
+
+ }
+ node = parent;
+ }
+
+ var p0 = rng.sc;
+ while (p0 && p0.parentNode && p0.parentNode !== $editable[0] && !isFormatNode(p0)) {
+ p0 = p0.parentNode;
+ }
+ if (!p0) return;
+ var p1 = rng.ec;
+ while (p1 && p1.parentNode && p1.parentNode !== $editable[0] && !isFormatNode(p1)) {
+ p1 = p1.parentNode;
+ }
+ if (!p0.parentNode || p0.parentNode !== p1.parentNode) {
+ return;
+ }
+
+ var parent = p0.parentNode;
+ var ul = document.createElement(sorted ? "ol" : "ul");
+ parent.insertBefore(ul, p0);
+ var childNodes = parent.childNodes;
+ var brs = [];
+ var begin = false;
+ for (var i=0; i<childNodes.length; i++) {
+ if (begin && dom.isBR(childNodes[i])) {
+ parent.removeChild(childNodes[i]);
+ i--;
+ }
+ if ((!dom.isText(childNodes[i]) && !isFormatNode(childNodes[i])) || (!ul.firstChild && childNodes[i] !== p0) ||
+ $.contains(ul, childNodes[i]) || (dom.isText(childNodes[i]) && !childNodes[i].textContent.match(/\S|u00A0/))) {
+ continue;
+ }
+ begin = true;
+ var li = document.createElement('li');
+ ul.appendChild(li);
+ li.appendChild(childNodes[i]);
+ if (li.firstChild === p1) {
+ break;
+ }
+ i--;
+ }
+ if (dom.isBR(childNodes[i])) {
+ parent.removeChild(childNodes[i]);
+ }
+
+ for (var i=0; i<brs.length; i++) {
+ parent.removeChild(brs[i]);
+ }
+ rng.clean().select();
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.insertOrderedList = function (event, editor, layoutInfo) {
+ return $.summernote.pluginEvents.insertUnorderedList(event, editor, layoutInfo, true);
+};
+$.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable);
+ var r = range.create();
+
+ var flag = false;
+ function indentUL (UL, start, end) {
+ var next;
+ var tagName = UL.tagName;
+ var node = UL.firstChild;
+ var parent = UL.parentNode;
+ var ul = document.createElement(tagName);
+ var li = document.createElement("li");
+ li.style.listStyle = "none";
+ li.appendChild(ul);
+
+ if (flag) {
+ flag = 1;
+ }
+
+ // create and fill ul into a li
+ while (node) {
+ if (flag === 1 || node === start || $.contains(node, start)) {
+ flag = true;
+ node.parentNode.insertBefore(li, node);
+ }
+ next = dom.nextElementSibling(node);
+ if (flag) {
+ ul.appendChild(node);
+ }
+ if (node === end || $.contains(node, end)) {
+ flag = false;
+ break;
+ }
+ node = next;
+ }
+
+ var temp;
+ var prev = dom.previousElementSibling(li);
+ if (prev && prev.tagName === "LI" && (temp = dom.firstElementChild(prev)) && temp.tagName === tagName && ((dom.firstElementChild(prev) || prev.firstChild) !== ul)) {
+ dom.doMerge(dom.firstElementChild(prev) || prev.firstChild, ul);
+ li = prev;
+ li.parentNode.removeChild(dom.nextElementSibling(li));
+ }
+ var next = dom.nextElementSibling(li);
+ if (next && next.tagName === "LI" && (temp = dom.firstElementChild(next)) && temp.tagName === tagName && (dom.firstElementChild(li) !== dom.firstElementChild(next))) {
+ dom.doMerge(dom.firstElementChild(li), dom.firstElementChild(next));
+ li.parentNode.removeChild(dom.nextElementSibling(li));
+ }
+ }
+ function outdenttUL (UL, start, end) {
+ var next;
+ var tagName = UL.tagName;
+ var node = UL.firstChild;
+ var parent = UL.parentNode;
+ var li = UL.parentNode.tagName === "LI" ? UL.parentNode : UL;
+ var ul = UL.parentNode.tagName === "LI" ? UL.parentNode.parentNode : UL.parentNode;
+ start = dom.ancestor(start, dom.isLi);
+ end = dom.ancestor(end, dom.isLi);
+
+ if (ul.tagName !== "UL" && ul.tagName !== "OL") return;
+
+ // create and fill ul into a li
+ while (node) {
+ if (node === start || $.contains(node, start)) {
+ flag = true;
+ if (dom.previousElementSibling(node) && li.tagName === "LI") {
+ li = dom.splitTree(li, dom.prevPoint({'node': node, 'offset': 0}));
+ }
+ }
+ next = dom.nextElementSibling(node);
+ if (flag) {
+ ul = node.parentNode;
+ li.parentNode.insertBefore(node, li);
+ if (!ul.children.length) {
+ if (ul.parentNode.tagName === "LI") {
+ ul = ul.parentNode;
+ }
+ ul.parentNode.removeChild(ul);
+ }
+ }
+
+ if (node === end || $.contains(node, end)) {
+ flag = false;
+ break;
+ }
+ node = next;
+ }
+
+ dom.merge(parent, start, 0, end, 1, null, true);
+ }
+ function indentOther (p, start, end) {
+ if (p === start || $.contains(p, start)) {
+ flag = true;
+ }
+ if (flag) {
+ if (outdent) {
+ dom.outdent(p);
+ } else {
+ dom.indent(p);
+ }
+ }
+ if (p === end || $.contains(p, end)) {
+ flag = false;
+ }
+ }
+
+ var ancestor = r.commonAncestor();
+ var $dom = $(ancestor);
+
+ if (!dom.isList(ancestor)) {
+ $dom = $(ancestor).children();
+ }
+ if (!$dom.length) {
+ $dom = $(dom.ancestor(r.sc, dom.isList));
+ if (!$dom.length) {
+ $dom = $(r.sc).closest(options.styleTags.join(','));
+ }
+ }
+
+ $dom.each(function () {
+ if (flag || $.contains(this, r.sc)) {
+ if (dom.isList(this)) {
+ if (outdent) {
+ outdenttUL(this, r.sc, r.ec);
+ } else {
+ indentUL(this, r.sc, r.ec);
+ }
+ } else if (isFormatNode(this)) {
+ indentOther(this, r.sc, r.ec);
+ }
+ }
+ });
+
+ if ($dom.length) {
+ var $parent = $dom.parent();
+
+ // remove text nodes between lists
+ var $ul = $parent.find('ul, ol');
+ if (!$ul.length) {
+ $ul = $(dom.ancestor(r.sc, dom.isList));
+ }
+ $ul.each(function () {
+ if (this.previousSibling &&
+ this.previousSibling !== dom.previousElementSibling(this) &&
+ !this.previousSibling.textContent.match(/\S/)) {
+ this.parentNode.removeChild(this.previousSibling);
+ }
+ if (this.nextSibling &&
+ this.nextSibling !== dom.nextElementSibling(this) &&
+ !this.nextSibling.textContent.match(/\S/)) {
+ this.parentNode.removeChild(this.nextSibling);
+ }
+ });
+
+ // merge same ul or ol
+ r = dom.merge($parent[0], r.sc, r.so, r.ec, r.eo, function (prev, cur) {
+ if (prev && dom.isList(prev) && dom.isEqual(prev, cur)) {
+ return true;
+ }
+ }, true);
+ range.create(r.sc, r.so, r.ec, r.eo).select();
+ }
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.outdent = function (event, editor, layoutInfo) {
+ return $.summernote.pluginEvents.indent(event, editor, layoutInfo, true);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+$.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTagName) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable);
+ event.preventDefault();
+
+ var r = range.create();
+ if (!r) {
+ return;
+ }
+ r.reRange().select();
+
+ if (sTagName === "blockquote" || sTagName === "pre") {
+ sTagName = $.summernote.core.agent.isMSIE ? '<' + sTagName + '>' : sTagName;
+ document.execCommand('FormatBlock', false, sTagName);
+ return;
+ }
+
+ // fix by odoo because if you select a style in a li with no p tag all the ul is wrapped by the style tag
+ var nodes = dom.listBetween(r.sc, r.ec);
+ for (var i=0; i<nodes.length; i++) {
+ if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && nodes[i].textContent.match(/\S|\u00A0/)) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) {
+ var ancestor = dom.ancestor(nodes[i], isFormatNode);
+ if (!ancestor) {
+ dom.wrap(nodes[i], sTagName);
+ } else if (ancestor.tagName.toLowerCase() !== sTagName) {
+ var tag = document.createElement(sTagName);
+ ancestor.parentNode.insertBefore(tag, ancestor);
+ dom.moveContent(ancestor, tag);
+ if (ancestor.className) {
+ tag.className = ancestor.className;
+ }
+ ancestor.parentNode.removeChild(ancestor);
+ }
+ }
+ }
+ r.select();
+};
+$.summernote.pluginEvents.removeFormat = function (event, editor, layoutInfo, value) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable);
+ var node = range.create().sc.parentNode;
+ document.execCommand('removeFormat');
+ document.execCommand('removeFormat');
+ var r = range.create();
+ r = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
+ range.create(r.sc, r.so, r.ec, r.eo).select();
+ event.preventDefault();
+ return false;
+};
+var fn_boutton_updateRecentColor = eventHandler.toolbar.button.updateRecentColor;
+eventHandler.toolbar.button.updateRecentColor = function (elBtn, sEvent, sValue) {
+ fn_boutton_updateRecentColor.call(this, elBtn, sEvent, sValue);
+ var font = $(elBtn).closest('.note-color').find('.note-recent-color i')[0];
+
+ if (sEvent === "foreColor") {
+ if (sValue.indexOf('text-') !== -1) {
+ font.className += ' ' + sValue;
+ font.style.color = '';
+ } else {
+ font.className = font.className.replace(/(^|\s+)text-\S+/);
+ font.style.color = sValue !== 'inherit' ? sValue : "";
+ }
+ } else {
+ if (sValue.indexOf('bg-') !== -1) {
+ font.className += ' ' + sValue;
+ font.style.backgroundColor = "";
+ } else {
+ font.className = font.className.replace(/(^|\s+)bg-\S+/);
+ font.style.backgroundColor = sValue !== 'inherit' ? sValue : "";
+ }
+ }
+ event.preventDefault();
+ return false;
+};
+
+options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62];
+$.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) {
+ var rng = range.create();
+ var startPoint = rng.getStartPoint();
+ var endPoint = rng.getEndPoint();
+
+ if (rng.isCollapsed()) {
+ return {
+ sc: startPoint.node,
+ so: startPoint.offset,
+ ec: endPoint.node,
+ offset: endPoint.offset
+ };
+ }
+
+ if (startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset]) {
+ startPoint.node = startPoint.node.childNodes[startPoint.offset];
+ startPoint.offset = 0;
+ }
+ if (endPoint.node.tagName && endPoint.node.childNodes[endPoint.offset]) {
+ endPoint.node = endPoint.node.childNodes[endPoint.offset];
+ endPoint.offset = 0;
+ }
+
+ // get first and last point
+ if (endPoint.offset && endPoint.offset != dom.nodeLength(endPoint.node)) {
+ var ancestor = dom.ancestor(endPoint.node, dom.isFont) || endPoint.node;
+ dom.splitTree(ancestor, endPoint);
+ }
+ if (startPoint.offset && startPoint.offset != dom.nodeLength(startPoint.node)) {
+ var ancestor = dom.ancestor(startPoint.node, dom.isFont) || startPoint.node;
+ var node = dom.splitTree(ancestor, startPoint);
+ if (endPoint.node === startPoint.node) {
+ endPoint.node = node;
+ endPoint.offset = dom.nodeLength(node);
+ }
+ startPoint.node = node;
+ startPoint.offset = 0;
+ }
+
+ // get list of nodes to change
+ var nodes = [];
+ dom.walkPoint(startPoint, endPoint, function (point) {
+ var node = point.node;
+ if (((dom.isText(node) && dom.isVisibleText(node)) ||
+ (dom.isFont(node) && !dom.isVisibleText(node))) &&
+ (node != endPoint.node || endPoint.offset)) {
+
+ nodes.push(point.node);
+
+ }
+ });
+ nodes = list.unique(nodes);
+
+ // apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
+ var node, font, $font, fonts = [], className;
+ if (color || bgcolor || size) {
+ for (var i=0; i<nodes.length; i++) {
+ node = nodes[i];
+
+ font = dom.ancestor(node, dom.isFont);
+ if (!font) {
+ if (node.textContent.match(/^[ ]|[ ]$/)) {
+ node.textContent = node.textContent.replace(/^[ ]|[ ]$/g, '\u00A0');
+ }
+
+ font = dom.create("font");
+ node.parentNode.insertBefore(font, node);
+ font.appendChild(node);
+ }
+
+ fonts.push(font);
+
+ className = font.className.split(/\s+/);
+
+ if (color) {
+ for (var k=0; k<className.length; k++) {
+ if (!className[k].length && className[k].slice(0,5) === "text-") {
+ className.splice(k,1);
+ k--;
+ }
+ }
+
+ if (color.indexOf('text-') !== -1) {
+ font.className = className.join(" ") + " " + color;
+ font.style.color = "inherit";
+ } else {
+ font.className = className.join(" ");
+ font.style.color = color;
+ }
+ }
+ if (bgcolor) {
+ for (var k=0; k<className.length; k++) {
+ if (className[k].length && className[k].slice(0,3) === "bg-") {
+ className.splice(k,1);
+ k--;
+ }
+ }
+
+ if (bgcolor.indexOf('bg-') !== -1) {
+ font.className = className.join(" ") + " " + bgcolor;
+ font.style.backgroundColor = "inherit";
+ } else {
+ font.className = className.join(" ");
+ font.style.backgroundColor = bgcolor;
+ }
+ }
+ if (size) {
+ font.style.fontSize = "inherit";
+ if (!isNaN(size) && Math.abs(parseInt(window.getComputedStyle(font).fontSize, 10)-size)/size > 0.05) {
+ font.style.fontSize = size + "px";
+ }
+ }
+ }
+ }
+
+ // remove empty values
+ // we must remove the value in 2 steps (applay inherit then remove) because some
+ // browser like chrome have some time an error for the rendering and/or keep inherit
+ for (var i=0; i<fonts.length; i++) {
+ font = fonts[i];
+ if (font.style.backgroundColor === "inherit") {
+ font.style.backgroundColor = "";
+ }
+ if (font.style.color === "inherit") {
+ font.style.color = "";
+ }
+ if (font.style.fontSize === "inherit") {
+ font.style.fontSize = "";
+ }
+
+ $font = $(font);
+
+ if (!$font.css("color") && !$font.css("background-color") && !$font.css("font-size")) {
+ $font.removeAttr("style");
+ }
+ if (!font.className.length) {
+ $font.removeAttr("class");
+ }
+ }
+
+ // select nodes to clean (to remove empty font and merge same nodes)
+ var nodes = [];
+ dom.walkPoint(startPoint, endPoint, function (point) {
+ nodes.push(point.node);
+ });
+ nodes = list.unique(nodes);
+
+ function remove (node, to) {
+ if (node === endPoint.node) {
+ endPoint = dom.prevPoint(endPoint);
+ }
+ if (to) {
+ dom.moveContent(node, to);
+ }
+ dom.remove(node);
+ }
+
+ // remove node without attributes (move content), and merge the same nodes
+ var className2, style, style2, $prev;
+ for (var i=0; i<nodes.length; i++) {
+ node = nodes[i];
+
+ if ((dom.isText(node) || dom.isBR(node)) && !dom.isVisibleText(node)) {
+ remove(node);
+ nodes.splice(i,1);
+ i--;
+ continue;
+ }
+
+ font = dom.ancestor(node, dom.isFont);
+ node = font || dom.ancestor(node, dom.isSpan);
+
+ if (!node) {
+ continue;
+ }
+
+ $font = $(node);
+ className = dom.orderClass(node);
+ style = dom.orderStyle(node);
+
+ if (!className && !style) {
+ remove(node, node.parentNode);
+ nodes.splice(i,1);
+ i--;
+ continue;
+ }
+
+ if (i>0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) {
+ className2 = font.getAttribute('class');
+ style2 = font.getAttribute('style');
+ if (node !== font && className == className2 && style == style2) {
+ remove(node, font);
+ nodes.splice(i,1);
+ i--;
+ continue;
+ }
+ }
+ }
+
+ range.create(startPoint.node, startPoint.offset, endPoint.node, endPoint.offset).select();
+};
+$.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value) {
+ var $editable = layoutInfo.editable();
+ event.preventDefault();
+ $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, 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;
+
+ if (foreColor) { $.summernote.pluginEvents.foreColor(event, editor, layoutInfo, foreColor); }
+ if (backColor) { $.summernote.pluginEvents.backColor(event, editor, layoutInfo, backColor); }
+};
+$.summernote.pluginEvents.foreColor = function (event, editor, layoutInfo, foreColor) {
+ var $editable = layoutInfo.editable();
+ $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, foreColor, null, null);
+ editor.afterCommand($editable);
+};
+$.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backColor) {
+ var $editable = layoutInfo.editable();
+ var r = range.create();
+ if (r.isCollapsed() && r.isOnCell()) {
+ var cell = dom.ancestor(r.sc, dom.isCell);
+ cell.className = cell.className.replace(new RegExp('(^|\\s+)bg-[^\\s]+(\\s+|$)', 'gi'), '');
+ cell.style.backgroundColor = "";
+ if (backColor.indexOf('bg-') !== -1) {
+ cell.className += ' ' + backColor;
+ } else if (backColor !== 'inherit') {
+ cell.style.backgroundColor = backColor;
+ }
+ return;
+ }
+ $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, backColor, null);
+ editor.afterCommand($editable);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+options.onCreateLink = function (sLinkUrl) {
+ if (sLinkUrl.indexOf('mailto:') === 0) {
+ // pass
+ } else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
+ sLinkUrl = 'mailto:' + sLinkUrl;
+ } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl.indexOf('/') !== 0 && sLinkUrl.indexOf('#') !== 0) {
+ sLinkUrl = 'http://' + sLinkUrl;
+ }
+ return sLinkUrl;
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+/* table */
+
+function summernote_table_scroll (event) {
+ if (range.create().isOnCell()) {
+ $('.o_table_handler').remove();
+ }
+}
+function summernote_table_update (oStyle) {
+ var r = range.create();
+ if (!r || !r.isOnCell() || !r.isOnCellFirst()) {
+ $('.o_table_handler').remove();
+ return;
+ }
+ var table = dom.ancestor(oStyle.range.sc, dom.isTable);
+ var $editable = $(table).closest('.o_editable');
+
+ $('.o_table_handler').remove();
+
+ var $dels = $();
+ var $adds = $();
+ var $tds = $('tr:first', table).children();
+ $tds.each(function () {
+ var $td = $(this);
+ var pos = $td.offset();
+
+ var $del = $('<span class="o_table_handler fa fa-minus-square"/>').appendTo('body');
+ $del.data('td', this);
+ $dels = $dels.add($del);
+ $del.css({
+ left: ((pos.left + $td.outerWidth()/2)-6) + "px",
+ top: (pos.top-6) + "px"
+ });
+
+ var $add = $('<span class="o_table_handler fa fa-plus-square"/>').appendTo('body');
+ $add.data('td', this);
+ $adds = $adds.add($add);
+ $add.css({
+ left: (pos.left-6) + "px",
+ top: (pos.top-6) + "px"
+ });
+ });
+
+ var $last = $tds.last();
+ var pos = $last.offset();
+ var $add = $('<span class="o_table_handler fa fa-plus-square"/>').appendTo('body');
+ $adds = $adds.add($add);
+ $add.css({
+ left: (pos.left+$last.outerWidth()-6) + "px",
+ top: (pos.top-6) + "px"
+ });
+
+ var $table = $(table);
+ $dels.data('table', table).on('mousedown', function (event) {
+ var td = $(this).data('td');
+ $editable.data('NoteHistory').recordUndo($editable);
+
+ var newTd;
+ if ($(td).siblings().length) {
+ var eq = $(td).index();
+ $table.find('tr').each(function () {
+ $('> td:eq('+eq+')', this).remove();
+ });
+ newTd = $table.find('tr:first > td:eq('+eq+'), tr:first > td:last').first();
+ } else {
+ var prev = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling($table[0])));
+ $table.remove();
+ $('.o_table_handler').remove();
+ r = range.create(prev, prev.textContent.length);
+ r.select();
+ $(r.sc).trigger('mouseup');
+ return;
+ }
+
+ $('.o_table_handler').remove();
+ range.create(newTd[0], 0, newTd[0], 0).select();
+ newTd.trigger('mouseup');
+ });
+ $adds.data('table', table).on('mousedown', function (event) {
+ var td = $(this).data('td');
+ $editable.data('NoteHistory').recordUndo($editable);
+
+ var newTd;
+ if (td) {
+ var eq = $(td).index();
+ $table.find('tr').each(function () {
+ $('td:eq('+eq+')', this).before('<td>'+dom.blank+'</td>');
+ });
+ newTd = $table.find('tr:first td:eq('+eq+')');
+ } else {
+ $table.find('tr').each(function () {
+ $(this).append('<td>'+dom.blank+'</td>');
+ });
+ newTd = $table.find('tr:first td:last');
+ }
+
+ $('.o_table_handler').remove();
+ range.create(newTd[0], 0, newTd[0], 0).select();
+ newTd.trigger('mouseup');
+ });
+
+ $dels.css({
+ 'position': 'absolute',
+ 'cursor': 'pointer',
+ 'background-color': '#fff',
+ 'color': '#ff0000'
+ });
+ $adds.css({
+ 'position': 'absolute',
+ 'cursor': 'pointer',
+ 'background-color': '#fff',
+ 'color': '#00ff00'
+ });
+}
+var fn_popover_update = eventHandler.popover.update;
+eventHandler.popover.update = function ($popover, oStyle, isAirMode) {
+ fn_popover_update.call(this, $popover, oStyle, isAirMode);
+ summernote_table_update(oStyle);
+};
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+function summernote_paste (event) {
+ // keep norma feature if copy a picture
+ var clipboardData = event.originalEvent.clipboardData;
+ if (clipboardData.items) {
+ var item = list.last(clipboardData.items);
+ var isClipboardImage = item.kind === 'file' && item.type.indexOf('image/') !== -1;
+ if (isClipboardImage) {
+ return true;
+ }
+ }
+
+ var $editable = $(event.currentTarget);
+ $editable = $editable.is('[contenteditable]') ? $editable : $editable.find('[contenteditable]');
+ $editable.data('NoteHistory').recordUndo($editable);
+
+ if (["INPUT", "TEXTAREA"].indexOf(event.target.tagName) === -1) {
+ var r = range.create();
+ if (!r.isCollapsed()) {
+ r = r.deleteContents();
+ r.select();
+ }
+
+ var text = clipboardData.getData("text/plain").replace(/</g, "&lt;");
+ dom.pasteText(r.sc, r.so, text);
+ event.preventDefault();
+ return false;
+ }
+}
+
+var fn_attach = eventHandler.attach;
+eventHandler.attach = function (oLayoutInfo, options) {
+ var $editable = oLayoutInfo.editor.hasClass('note-editable') ? oLayoutInfo.editor : oLayoutInfo.editor.find('.note-editable');
+ fn_attach.call(this, oLayoutInfo, options);
+ oLayoutInfo.editor.on("paste", summernote_paste);
+ $editable.on("scroll", summernote_table_scroll);
+};
+var fn_dettach = eventHandler.dettach;
+eventHandler.dettach = function (oLayoutInfo, options) {
+ var $editable = oLayoutInfo.editor.hasClass('note-editable') ? oLayoutInfo.editor : oLayoutInfo.editor.find('.note-editable');
+ fn_dettach.call(this, oLayoutInfo, options);
+ oLayoutInfo.editor.off("paste", summernote_paste);
+ $editable.off("scroll", summernote_table_scroll);
+};
+
+return $.summernote;
+
+});
diff --git a/addons/web_editor/static/src/js/tour.rte.js b/addons/web_editor/static/src/js/tour.rte.js
new file mode 100644
index 00000000000..c257176fd6f
--- /dev/null
+++ b/addons/web_editor/static/src/js/tour.rte.js
@@ -0,0 +1,398 @@
+odoo.define('web_editor.tour', function (require) {
+'use strict';
+
+var core = require('web.core');
+var Tour = require('web.Tour');
+var base = require('web_editor.base');
+var snippet_editor = require('web_editor.snippet.editor');
+
+var _t = core._t;
+
+base.ready().done(function () {
+
+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);
+};
+
+Tour.register({
+ id: 'rte',
+ name: "Test RTE",
+ mode: 'test',
+ path: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
+ steps: [
+ {
+ title: "Change html for this test",
+ onload: function () {
+ var $el = $(this.element);
+ var html = '\n'+
+ '<section>\n'+
+ ' <div class="container">\n'+
+ ' <div class="row">\n'+
+ ' <div class="col-md-6 mt16">\n'+
+ '<h1 id="text_title_id">Batnae municipium in Anthemusia</h1> \n'+
+ ' <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>\n'+
+ ' <p> Quam <img style="width: 25%" src="/web_editor/static/src/img/drag_here.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+
+ ' <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+
+ '<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+
+ ' </div>\n'+
+ ' <div class="col-md-6 mt16">\n'+
+ ' <img class="img img-responsive shadow mb16" src="/web_editor/static/src/img/drag_here.png" alt="Odoo text and image block">\n'+
+ ' </div>\n'+
+ ' </div>\n'+
+ '</section>\n';
+ $("#editable_area").html(html);
+ }
+ },
+ {
+ element: '#editable_area > section .row > div:first',
+ title: "simulate triple click and change text bg-color",
+ onload: function () {
+ var $el = $(this.element);
+ var $h1 = $('h1', $el);
+ $.summernote.core.range.create($h1[0].firstChild, 0, $('p', $el)[0], 0).select();
+ click_event($h1[0], 'mouseup');
+ }
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first',
+ element: '.note-popover button[data-event="color"]:visible',
+ title: "change text bg-color after triple click",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1 font',
+ element: '.note-color button.dropdown-toggle:visible',
+ title: "change selection to change text color",
+ onload: function () {
+ var $el = $('#editable_area > section .row > div:first:not(:has(p font)) h1 font');
+ $.summernote.core.range.create($el[0].firstChild, 5, $el[0].firstChild, 10).select();
+ click_event($el[0], 'mouseup');
+ }
+ },
+ {
+ element: 'div[data-target-event="foreColor"]:visible .note-color-row:eq(1) button[data-event="foreColor"]:first',
+ title: "change text color",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first h1 font:eq(2)',
+ element: '.note-color button.dropdown-toggle:visible',
+ title: "change selection to change text bg-color again",
+ onload: function () {
+ var $el = $('#editable_area > section .row > div:first h1 font:eq(2)');
+ $.summernote.core.range.create($el.prev()[0].firstChild, 3, $el[0].firstChild, 10).select();
+ click_event($el.prev()[0], 'mouseup');
+ }
+ },
+ {
+ element: 'div[data-target-event="backColor"] .colorpicker button[data-event="backColor"]:first',
+ title: "change text backColor again",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first h1 font:eq(4)',
+ element: '.note-color button.dropdown-toggle:visible',
+ title: "change selection (h1 and p) to change text color with class",
+ onload: function () {
+ var $el = $('#editable_area > section .row > div:first h1 font:eq(4)');
+ $.summernote.core.range.create($el.prev()[0].firstChild, 3, $el.parent("h1").next("p")[0].firstChild, 30).select();
+ click_event($el.prev()[0], 'mouseup');
+ }
+ },
+ {
+ element: 'div[data-target-event="foreColor"]:visible button[data-event="foreColor"][data-value^="text-"]:first',
+ title: "change text foreColor again",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first p font',
+ element: '.o_editable.note-editable.o_dirty',
+ title: "delete selection",
+ keydown: 46 // delete
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1',
+ element: '.o_editable.note-editable.o_dirty',
+ title: "clean and delete (backspace) an other selection",
+ onload: function () {
+ var $el = $(this.waitFor);
+ $.summernote.core.range.createFromNode($el.next("p")[0]).clean();
+ $.summernote.core.range.create($el.find('font:containsExact(ici)')[0].firstChild, 1, $el.next().next()[0].firstChild, 5).select();
+ click_event($el.find('font:last')[0], 'mouseup');
+ },
+ keydown: 8 // backspace
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
+ element: '.o_editable.note-editable.o_dirty',
+ title: "delete an other selection",
+ onload: function () {
+ var $el = $(this.waitFor);
+ $.summernote.core.range.create($el.find('font:first')[0].firstChild, 3, $el.next("p")[0].childNodes[2], 8).select();
+ click_event($el.find('font:first')[0], 'mouseup');
+ },
+ keydown: 46
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )',
+ element: '.note-air-popover button[data-event="undo"]',
+ title: "undo",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )',
+ element: '.note-air-popover button[data-event="undo"]',
+ title: "undo adain",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1',
+ element: '.o_editable.note-editable.o_dirty',
+ title: "delete (backspace) after undo",
+ keydown: 8 // backspace
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
+ element: '#editable_area > section .row > div:first img[style*="25%"]',
+ title: "click on image",
+ },
+ {
+ element: '.note-image-popover:visible button[data-event="resize"][data-value="0.5"]',
+ title: "Click on resize half",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first img[style*="50%"]',
+ element: '.note-image-popover:visible button[data-event="showImageDialog"]',
+ title: "Click on edit picture",
+ },
+ {
+ waitFor: '.modal #editor-media-image',
+ element: '.modal a[data-toggle="tab"]:contains(Pictogram)',
+ title: "Click on pictogram tab",
+ },
+ {
+ element: '#editor-media-icon.active span.fa:first',
+ title: "select a pictogram",
+ },
+ {
+ waitFor: '#editor-media-icon.active span.font-icons-selected',
+ element: '.modal button.save',
+ title: "save pictogram",
+ },
+ {
+ element: '.note-image-popover button[data-event="resizefa"][data-value="3"]',
+ title: "select a size for the pictogram",
+ },
+ {
+ waitFor: '#wrapwrap span.fa-3x',
+ element: '.note-image-popover:visible button[data-event="floatMe"][data-value="right"]',
+ title: "click on float right",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first span.fa.pull-right',
+ element: '.note-image-popover:visible button[data-event="showLinkDialog"]',
+ title: "click on create link",
+ },
+ {
+ waitFor: '.modal a#link-preview:containsRegex(/^<span [^>]+><\\/span>$/) > span.fa.fa-3x.pull-right',
+ element: '.modal .dropdown:has(.link-style) a[data-toggle="dropdown"]',
+ title: "click on color style",
+ },
+ {
+ element: '.modal .dropdown ul label.btn-success',
+ title: "choose success style",
+ },
+ // {
+ // waitFor: '.modal a#link-preview.btn:containsRegex(/^<span [^>]+><\\/span>$/)',
+ // element: '.modal .select2-container.url-source a.select2-choice',
+ // title: "click to choose an internal page",
+ // },
+ // {
+ // element: '.select2-drop:visible .select2-search input',
+ // title: "search 'contact'",
+ // sampleText: "contact",
+ // },
+ // {
+ // element: '.select2-drop:visible .select2-results .select2-result div:contains(/page/)',
+ // title: "select /page/contactus",
+ // },
+ {
+ // waitNot: '.select2-drop:visible',
+ element: '#link-text',
+ title: "change text label",
+ sampleText: "ABC[IMG] DEF",
+ },
+ {
+ waitFor: '.modal a#link-preview.btn',
+ element: '#link-external',
+ sampleText: "http://www.odoo.com",
+ title: "insert a link url",
+ },
+ {
+ waitFor: '.modal a#link-preview.btn:containsRegex(/^ABC<span [^>]+><\\/span> DEF$/)',
+ element: '.modal button.save',
+ title: "save link",
+ },
+ {
+ waitNot: '#link-preview',
+ waitFor: 'a.btn[href^="http://"]:has(span.fa.fa-3x.pull-right)',
+ element: '#editable_area > section .row > div:last img',
+ title: "click on other picture",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first span.fa.pull-right',
+ element: '.note-image-popover:visible button[data-event="showLinkDialog"]',
+ title: "click on create link again",
+ },
+ {
+ waitFor: '.modal a#link-preview:containsRegex(/^<img [^>]+>$/)',
+ element: '.modal .dropdown:has(.link-style) a[data-toggle="dropdown"]',
+ title: "click on color style again",
+ },
+ {
+ element: '.modal .dropdown ul label.btn-success',
+ title: "choose success style",
+ },
+ {
+ waitFor: '.modal a#link-preview.btn',
+ element: '#link-external',
+ sampleText: "test@test.test",
+ title: "insert an email",
+ },
+ {
+ waitFor: '.modal a#link-preview.btn[href="mailto:test@test.test"]',
+ element: '.modal button.save',
+ title: "save link",
+ },
+ {
+ waitNot: '#link-preview',
+ waitFor: '#editable_area > section .row > div:eq(1) > a > img',
+ element: '#editable_area > section .row > div:first p:eq(2)',
+ title: "triple enter then double backspace",
+ keydown: [66, 13, 66, 13, 13, 8, 8], // B enter B enter enter backspace backspace
+ onload: function () {
+ var p = $(this.element)[0].firstChild;
+ $.summernote.core.range.create(p, p.textContent.length, p, p.textContent.length).select();
+ click_event(p, 'mouseup');
+ },
+ },
+ {
+ waitNot: '#editable_area > section .row > div:first p:eq(4), #editable_area > section .row > div:eq(3)',
+ waitFor: '#editable_area > section .row > div:first p:eq(3)',
+ title: "add ul content",
+ onload: function () {
+ var $el = $('#editable_area > section .row > div:first');
+ var html = ' <ul> '+
+ '\n <li> <p>Batnae municipium. </p></li>'+
+ '\n <li> Seleucia praeter.</li>'+
+ '\n <li><p>Et hanc quidem.</p></li>'+
+ '\n </ul>';
+ $el.append(html);
+ var node = $el.find('ul li p:last')[0].firstChild;
+ $.summernote.core.range.create(node, 6).select();
+ click_event(node, 'mouseup');
+ }
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first ul li p:first',
+ element: '.note-air-popover .note-style button.dropdown-toggle',
+ title: "click on style dropdown",
+ },
+ {
+ element: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
+ title: "select h3",
+ onload: function () {
+ var node = $('#editable_area > section .row > div:first ul li p:last')[0].firstChild;
+ $.summernote.core.range.create(node, 0).select();
+ click_event(node, 'mouseup');
+ }
+ },
+ {
+ element: '#editable_area > section .row > div:first > ul > li > h3',
+ title: "double tabulation",
+ keydown: [9, 9] // tabulation
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
+ element: '.note-air-popover button[data-event="insertOrderedList"]',
+ title: "click on order list",
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first ul > li > ul > li > ol > li > h3',
+ element: '#editable_area > section .row > div:first ul li > p:last',
+ title: "enter in ul",
+ keydown: [66, 13], // enter
+ onload: function () {
+ $(this.element)[0].firstChild.textContent += "";
+ $.summernote.core.range.create($(this.element)[0].firstChild, 7).select();
+ click_event($(this.element)[0], 'mouseup');
+ }
+ },
+ {
+ element: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
+ title: "backspace in list",
+ keydown: 8
+ },
+ {
+ waitFor: '#editable_area > section .row > div:first ul li p:eq(1)',
+ title: "end",
+ },
+ ]
+});
+
+
+Tour.register({
+ id: 'rte_inline',
+ name: "Test RTE Inline",
+ mode: 'test',
+ path: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
+ steps: [
+ {
+ title: "Change html for this test",
+ onload: function () {
+ var $el = $(this.element);
+ var html = '\n'+
+ '<div>\n'+
+ ' <table cellspacing="0" cellpadding="0" width="100%">\n'+
+ ' <tbody>\n'+
+ ' <tr>\n'+
+ ' <td valign="center" width="270">\n'+
+ ' <img src="/logo.png" alt="Your Logo" class="img-circle img-thumbnail">\n'+
+ ' </td>\n'+
+ ' <td valign="center" width="270">\n'+
+ ' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+
+ ' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square pull-right"></span>\n'+
+ ' </td>\n'+
+ ' </tr>\n'+
+ ' </tbody>\n'+
+ ' </table>\n'+
+ '</div>';
+ $("#editable_area").html(html);
+ }
+ },
+ {
+ title: "call clean for save",
+ element: '#wrapwrap table',
+ onload: function () {
+ snippet_editor.instance.clean_for_save();
+ }
+ },
+ {
+ waitFor: '#wrapwrap img:first[style*="border-bottom-left-radius:50%"][style*="border-top-width"][style*="padding-top"]',
+ title: "check the image style",
+ },
+ {
+ waitFor: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(66,139,202)/27"]',
+ title: "check the font image src",
+ },
+ {
+ waitFor: '#wrapwrap img:eq(1)[style*="height:2em"]',
+ title: "check the font class to css",
+ },
+ {
+ waitFor: '#wrapwrap img:eq(2)[style*="float:right"]',
+ title: "check the second font class to css",
+ },
+ ]
+});
+
+
+});
+
+});
diff --git a/addons/web_editor/static/src/js/transcoder.js b/addons/web_editor/static/src/js/transcoder.js
new file mode 100644
index 00000000000..c7bcd14107f
--- /dev/null
+++ b/addons/web_editor/static/src/js/transcoder.js
@@ -0,0 +1,172 @@
+odoo.define('web_editor.transcoder', function (require) {
+'use strict';
+
+var widget = require('web_editor.widget');
+
+var cache = {};
+var rulesCache = [];
+var getMatchedCSSRules = function (a) {
+ if(cache[a.tagName + "." +a.className]) {
+ return cache[a.tagName + "." +a.className];
+ }
+ if (!rulesCache.length) {
+ var sheets = document.styleSheets;
+ for(var i = sheets.length-1; i >= 0 ; i--) {
+ var rules = sheets[i].rules || sheets[i].cssRules;
+ if (rules) {
+ for(var r = rules.length-1; r >= 0; r--) {
+ var selectorText = rules[r].selectorText;
+ if (selectorText &&
+ rules[r].cssText &&
+ selectorText.indexOf(".") !== -1 &&
+ selectorText.indexOf(".note-") === -1 &&
+
+ selectorText.indexOf(":hover") === -1 &&
+ selectorText.indexOf(":before") === -1 &&
+ selectorText.indexOf(":after") === -1 &&
+ selectorText.indexOf(":active") === -1 &&
+ selectorText.indexOf(":link") === -1 &&
+ selectorText.indexOf("::") === -1) {
+ var st = selectorText.split(/\s*,\s*/);
+ for (var k=0; k<st.length; k++) {
+ rulesCache.push({ 'selector': st[k], 'style': rules[r].style });
+ }
+ }
+ }
+ }
+ }
+ rulesCache.reverse();
+ }
+
+ var css = [];
+ a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector;
+ for(var r = 0; r < rulesCache.length; r++) {
+ if (a.matches(rulesCache[r].selector)) {
+ var style = rulesCache[r].style;
+ if (style.parentRule) {
+ var style_obj = {};
+ for (var k=0, len=style.length; k<len; k++) {
+ style_obj[style[k]] = style[style[k]];
+ }
+ rulesCache[r].style = style = style_obj;
+ }
+ css.push([rulesCache[r].selector, style]);
+ }
+ }
+
+ function specificity (selector) {
+ // http://www.w3.org/TR/css3-selectors/#specificity
+ var a = 0;
+ selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ""; });
+ var b = 0;
+ selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ""; });
+ var c = 0;
+ selector.replace(/(\s+|:+)[a-z0-9_-]+/gi, function (a) { if(a.indexOf(':not(')===-1) c++; return ""; });
+ return a*100 + b*10 + c;
+ }
+ css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); });
+
+ var style = {};
+ _.each(css, function (v,k) {
+ _.each(v[1], function (v,k) {
+ if (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1) {
+ style[k] = v;
+ }
+ });
+ });
+ return cache[a.tagName + "." +a.className] = style;
+};
+
+// convert font awsome into image
+var font_to_img = function ($editable) {
+ $(".fa", $editable).each(function () {
+ var $font = $(this);
+ var content;
+ _.find(widget.fontIcons, function (font) {
+ return _.find(widget.getCssSelectors(font.parser), function (css) {
+ if ($font.is(css[0].replace(/::?before$/, ''))) {
+ content = css[1].match(/content:\s*['"]?(.)['"]?/)[1];
+ return true;
+ }
+ });
+ });
+ if (content) {
+ var size = parseInt(parseFloat($font.css("font-size"))/parseFloat($font.parent().css("font-size")),10);
+ var color = $font.css("color").replace(/\s/g, '');
+ var src = _.str.sprintf('/web_editor/font_to_img/%s/%s/'+$font.width(), window.encodeURI(content), window.encodeURI(color));
+ var style = $font.attr("style");
+ style = (style ? style.replace(/\s/g, '').replace(/(^|;)height:[^;]*/, '$1').replace(/(^|;)font-size:[^;]*/, '$1') : "") + "height:"+size+"em;";
+ var $img = $("<img/>").attr("src", src).attr("data-class", $font.attr("class")).attr("style", style);
+ $font.replaceWith($img);
+ } else {
+ $font.remove();
+ }
+ });
+};
+// convert image into font awsome
+var img_to_font = function ($editable) {
+ $("img[src*='/web_editor/font_to_img/']", $editable).each(function () {
+ var $img = $(this);
+ var $font = $("<span/>").attr("class", $img.data("class")).attr("style", $img.attr("style")).css("height", "");
+ $img.replaceWith($font);
+ });
+};
+
+// convert class into inline style to send by mail
+var class_to_style = function ($editable) {
+ if (!rulesCache.length) {
+ getMatchedCSSRules($editable[0]);
+ }
+ var selector = _.map(rulesCache, function (a) { return a.selector;}).join(",");
+ $editable.find(selector).each(function () {
+ var $target = $(this);
+ var css = getMatchedCSSRules(this);
+ var style = $target.attr("style") || "";
+ _.each(css, function (v,k) {
+ if (style.indexOf(k) === -1) {
+ style = k+":"+v+";"+style;
+ }
+ });
+ $target.attr("style", style);
+ });
+};
+// convert style into inline class from mail
+var style_to_class = function ($editable) {
+ getMatchedCSSRules($editable[0]);
+ var classes = [];
+ $editable.find("[class]").each(function () {
+ classes = classes.concat(this.className.split(/\s+/));
+ });
+
+ var maybe_selector = _.filter(rulesCache, function (a) { return !!_.find(classes, function (b) { return a.selector.indexOf("."+b) !== -1; } ); });
+ var selector = _.map(maybe_selector, function (a) { return a.selector;}).join(",");
+
+ var $c = $('<span/>').appendTo("body");
+
+ $editable.find(selector).each(function () {
+ var $target = $(this);
+ var css = getMatchedCSSRules(this);
+ var style = "";
+ _.each(css, function (v,k) {
+ if (style.indexOf(k) === -1) {
+ style = k+":"+v+";"+style;
+ }
+ });
+ css = $c.attr("style", style).attr("style").split(/\s*;\s*/);
+ style = $target.attr("style") || "";
+ _.each(css, function (v) {
+ style = style.replace(v, '');
+ });
+ $target.attr("style", style.replace(/;+(\s;)*/g, ';').replace(/^;/g, ''));
+ });
+ $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,
+};
+
+}); \ No newline at end of file
diff --git a/addons/web_editor/static/src/js/translator.js b/addons/web_editor/static/src/js/translator.js
new file mode 100644
index 00000000000..190706a111c
--- /dev/null
+++ b/addons/web_editor/static/src/js/translator.js
@@ -0,0 +1,214 @@
+odoo.define('web_editor.translate', function (require) {
+'use strict';
+
+var core = require('web.core');
+var Model = require('web.Model');
+var ajax = require('web.ajax');
+var Class = require('web.Class');
+var Widget = require('web.Widget');
+var base = require('web_editor.base');
+var rte = require('web_editor.rte');
+
+var qweb = core.qweb;
+
+ajax.loadXML('/web_editor/static/src/xml/translator.xml', qweb);
+
+
+var translatable = !!$('html').data('translatable');
+var edit_translations = !!$('html').data('edit_translations');
+
+function unbind_click(event) {
+ if (event.ctrlKey || !$(event.target).is(':o_editable')) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+}
+
+
+var RTE_Translate = rte.Class.extend({
+ saveElement: function ($el, context) {
+ // remove multi edition
+ if ($el.data('oe-translation-id')) {
+ var key = 'translation:'+$el.data('oe-translation-id');
+ if (this.__saved[key]) return true;
+ this.__saved[key] = true;
+
+ return ajax.jsonRpc('/web/dataset/call', 'call', {
+ model: 'ir.translation',
+ method: 'write',
+ args: [
+ [+$el.data('oe-translation-id')],
+ {value: $el.html(), state: 'translated'},
+ context || base.get_context()
+ ],
+ });
+ }
+
+ return this._super.apply(this, arguments);
+ },
+});
+
+var Translate = Widget.extend({
+ events: {
+ 'click [data-action="save"]': 'save_and_reload',
+ 'click [data-action="cancel"]': 'cancel',
+ },
+ template: 'web_editor.translator',
+ init: function (parent, $target, lang) {
+ this.parent = parent;
+ this.ir_translation = new Model('ir.translation');
+ this.lang = lang || base.get_context().lang;
+ this.setTarget($target);
+ this._super();
+
+ this.rte = new RTE_Translate(this, this.config);
+ this.rte.on('change', this, this.onChange);
+ },
+ start: function () {
+ this._super();
+ return this.edit();
+ },
+ setTarget: function ($target) {
+ this.$target = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
+ },
+ find: function (selector) {
+ return selector ? this.$target.find(selector).addBack().filter(selector) : this.$target;
+ },
+ edit: function () {
+ var flag = false;
+ window.onbeforeunload = function(event) {
+ if ($('.o_editable.o_dirty').length && !flag) {
+ flag = true;
+ setTimeout(function () {flag=false;},0);
+ return _t('This document is not saved!');
+ }
+ };
+ this.$target.addClass("o_editable");
+ this.rte.start();
+ this.translations = [];
+ this.markTranslatableNodes();
+ this.onTranslateReady();
+ },
+ onTranslateReady: function () {
+ this.$el.show();
+ this.trigger("edit");
+ },
+ onChange: function (node) {
+ var $node = $(node);
+ var trans = this.getTranlationObject($node[0]);
+ $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
+ },
+ getTranlationObject: function (node) {
+ var $node = $(node);
+ var id = +$node.data('oe-translation-id');
+ if (!id) {
+ id = $node.data('oe-model')+','+$node.data('oe-id')+','+$node.data('oe-field');
+ }
+ var trans = _.find(this.translations, function (trans) {
+ return trans.id === id;
+ });
+ if (!trans) {
+ this.translations.push(trans = {'id': id});
+ }
+ return trans;
+ },
+ markTranslatableNodes: function (node) {
+ var self = this;
+ this.$target.each(function () {
+ var $node = $(this);
+ var trans = self.getTranlationObject(this);
+ trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
+ });
+ this.$target.parent().on('click', unbind_click);
+ console.info('Click on CTRL when you click in an translatable area to have the default behavior');
+ },
+ unarkTranslatableNode: function () {
+ this.$target.removeClass('o_editable').removeAttr('contentEditable');
+ this.$target.parent().off('click', this.__unbind_click);
+ this.$target_attr.off('mousedown click mouseup', this.__translate_attribute);
+ },
+ 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();
+ },
+ destroy: function () {
+ this.cancel();
+ this.$el.remove();
+ this._super();
+ },
+
+ config: function ($editable) {
+ if ($editable.data('oe-model')) {
+ return {
+ 'airMode' : true,
+ 'focus': false,
+ 'airPopover': [
+ ['history', ['undo', 'redo']],
+ ],
+ 'styleWithSpan': false,
+ 'inlinemedia' : ['p'],
+ 'lang': "odoo",
+ 'onChange': function (html, $editable) {
+ $editable.trigger("content_changed");
+ }
+ };
+ }
+ return {
+ 'airMode' : true,
+ 'focus': false,
+ 'airPopover': [
+ ['font', ['bold', 'italic', 'underline', 'clear']],
+ ['fontsize', ['fontsize']],
+ ['color', ['color']],
+ ['history', ['undo', 'redo']],
+ ],
+ 'styleWithSpan': false,
+ 'inlinemedia' : ['p'],
+ 'lang': "odoo",
+ 'onChange': function (html, $editable) {
+ $editable.trigger("content_changed");
+ }
+ };
+ }
+});
+
+
+if (edit_translations) {
+ base.ready().then(function () {
+ data.instance = new Translate(this, $('#wrapwrap'));
+ data.instance.prependTo(document.body);
+
+ $('a[href*=edit_translations]').each(function () {
+ this.href = this.href.replace(/[$?]edit_translations[^&?]+/, '');
+ });
+ $('form[action*=edit_translations]').each(function () {
+ this.action = this.action.replace(/[$?]edit_translations[^&?]+/, '');
+ });
+ });
+}
+
+var data = {
+ 'translatable': translatable,
+ 'edit_translations': edit_translations,
+ 'Class': Translate,
+};
+return data;
+
+});
diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js
new file mode 100644
index 00000000000..6fab80ad802
--- /dev/null
+++ b/addons/web_editor/static/src/js/widgets.js
@@ -0,0 +1,1069 @@
+odoo.define('web_editor.widget', function (require) {
+'use strict';
+
+var core = require('web.core');
+var ajax = require('web.ajax');
+var Widget = require('web.Widget');
+var base = require('web_editor.base');
+var rte = require('web_editor.rte');
+
+var QWeb = core.qweb;
+var range = $.summernote.core.range;
+var dom = $.summernote.core.dom;
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+var Dialog = Widget.extend({
+ events: {
+ 'hidden.bs.modal': 'destroy',
+ 'click button.save': 'save',
+ 'click button[data-dismiss="modal"]': 'cancel',
+ },
+ init: function () {
+ this._super();
+ },
+ start: function () {
+ var sup = this._super();
+ this.$el.modal({backdrop: 'static'});
+ this.$('input:first').focus();
+ return sup;
+ },
+ save: function () {
+ this.close();
+ this.trigger("saved");
+ },
+ cancel: function () {
+ this.trigger("cancel");
+ },
+ close: function () {
+ this.$el.modal('hide');
+ },
+});
+
+/**
+ * alt widget. Lets users change a alt & title on a media
+ */
+var alt = Dialog.extend({
+ template: 'web_editor.dialog.alt',
+ init: function ($editable, media) {
+ this.$editable = $editable;
+ this.media = media;
+ this.alt = ($(this.media).attr('alt') || "").replace(/&quot;/g, '"');
+ this.title = ($(this.media).attr('title') || "").replace(/&quot;/g, '"');
+ return this._super();
+ },
+ save: function () {
+ var self = this;
+ range.createFromNode(self.media).select();
+ this.$editable.data('NoteHistory').recordUndo();
+ var alt = this.$('#alt').val();
+ var title = this.$('#title').val();
+ $(this.media).attr('alt', alt ? alt.replace(/"/g, "&quot;") : null).attr('title', title ? title.replace(/"/g, "&quot;") : null);
+ setTimeout(function () {
+ click_event(self.media, "mouseup");
+ },0);
+ return this._super();
+ },
+});
+
+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);
+};
+
+/**
+ * MediaDialog widget. Lets users change a media, including uploading a
+ * new image, font awsome or video and can change a media into an other
+ * media
+ *
+ * options: select_images: allow the selection of more of one image
+ */
+var MediaDialog = Dialog.extend({
+ template: 'web_editor.dialog.media',
+ events : _.extend({}, Dialog.prototype.events, {
+ 'input input#icon-search': 'search',
+ }),
+ init: function ($editable, media, options) {
+ this._super();
+ if ($editable) {
+ this.$editable = $editable;
+ this.rte = this.$editable.rte || this.$editable.data('rte');
+ }
+ this.options = options || {};
+ this.old_media = media;
+ this.media = media;
+ this.isNewMedia = !media;
+ this.range = range.create();
+ },
+ start: function () {
+ var self = this;
+
+ this.only_images = this.options.only_images || this.options.select_images || (this.media && $(this.media).parent().data("oe-field") === "image");
+ if (this.only_images) {
+ this.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+ }
+
+ if (this.media) {
+ if (this.media.nodeName === "IMG") {
+ this.$('[href="#editor-media-image"]').tab('show');
+ } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) {
+ this.$('[href="#editor-media-video"]').tab('show');
+ } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) {
+ this.media = this.media.parentNode;
+ this.$('[href="#editor-media-video"]').tab('show');
+ } else if (this.media.className.match(/(^|\s)fa($|\s)/)) {
+ this.$('[href="#editor-media-icon"]').tab('show');
+ }
+ }
+
+ this.imageDialog = new ImageDialog(this, this.media, this.options);
+ this.imageDialog.appendTo(this.$("#editor-media-image"));
+ this.iconDialog = new fontIconsDialog(this, this.media, this.options);
+ this.iconDialog.appendTo(this.$("#editor-media-icon"));
+ this.videoDialog = new VideoDialog(this, this.media, this.options);
+ this.videoDialog.appendTo(this.$("#editor-media-video"));
+
+ this.active = this.imageDialog;
+
+ $('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
+ if ($(event.target).is('[href="#editor-media-image"]')) {
+ self.active = self.imageDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-icon"]')) {
+ self.active = self.iconDialog;
+ self.$('li.search, li.previous, li.next').removeClass("hidden");
+ self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
+ } else if ($(event.target).is('[href="#editor-media-video"]')) {
+ self.active = self.videoDialog;
+ self.$('.nav-tabs li.search').addClass("hidden");
+ }
+ });
+
+ return this._super();
+ },
+ save: function () {
+ if (this.options.select_images) {
+ this.trigger("saved", this.active.save());
+ this.close();
+ return;
+ }
+ if(this.rte) {
+ this.range.select();
+ this.rte.historyRecordUndo(this.media);
+ }
+
+ var self = this;
+ if (self.media) {
+ this.media.innerHTML = "";
+ if (this.active !== this.imageDialog) {
+ this.imageDialog.clear();
+ }
+ if (this.active !== this.iconDialog) {
+ this.iconDialog.clear();
+ }
+ if (this.active !== this.videoDialog) {
+ this.videoDialog.clear();
+ }
+ } else {
+ this.media = document.createElement("img");
+ this.range.insertNode(this.media, true);
+ this.active.media = this.media;
+ }
+ this.active.save();
+
+ if (this.active.add_class) {
+ $(this.media).addClass(this.active.add_class);
+ }
+
+ $(document.body).trigger("media-saved", [self.active.media, self.old_media]);
+ self.trigger("saved", [self.active.media, self.old_media]);
+ setTimeout(function () {
+ if (!self.active.media.parentNode) {
+ return;
+ }
+ range.createFromNode(self.active.media).select();
+ click_event(self.active.media, "mousedown");
+ if (!this.only_images) {
+ setTimeout(function () {
+ if($(self.active.media).parent().data("oe-field") !== "image") {
+ click_event(self.active.media, "click");
+ }
+ click_event(self.active.media, "mouseup");
+ },0);
+ }
+ },0);
+
+ this.close();
+ },
+ searchTimer: null,
+ search: function () {
+ var self = this;
+ var needle = this.$("input#icon-search").val();
+ clearTimeout(this.searchTimer);
+ this.searchTimer = setTimeout(function () {
+ self.active.search(needle || "");
+ },250);
+ }
+});
+
+/**
+ * ImageDialog widget. Lets users change an image, including uploading a
+ * new image in OpenERP or selecting the image style (if supported by
+ * the caller).
+ */
+var IMAGES_PER_ROW = 6;
+var IMAGES_ROWS = 2;
+var ImageDialog = Widget.extend({
+ template: 'web_editor.dialog.image',
+ events: _.extend({}, Dialog.prototype.events, {
+ 'change .url-source': function (e) {
+ this.changed($(e.target));
+ },
+ 'click button.filepicker': function () {
+ var filepicker = this.$('input[type=file]');
+ if (!_.isEmpty(filepicker)){
+ filepicker[0].click();
+ }
+ },
+ 'click .js_disable_optimization': function () {
+ this.$('input[name="disable_optimization"]').val('1');
+ var filepicker = this.$('button.filepicker');
+ if (!_.isEmpty(filepicker)){
+ filepicker[0].click();
+ }
+ },
+ 'change input[type=file]': 'file_selection',
+ 'submit form': 'form_submit',
+ 'change input.url': "change_input",
+ 'keyup input.url': "change_input",
+ //'change select.image-style': 'preview_image',
+ 'click .existing-attachments img': 'select_existing',
+ 'click .existing-attachment-remove': 'try_remove',
+ }),
+ init: function (parent, media, options) {
+ this._super();
+ this.options = options || {};
+ this.parent = parent;
+ this.media = media;
+ this.images = [];
+ this.page = 0;
+ },
+ start: function () {
+ this.$preview = this.$('.preview-container').detach();
+ var self = this;
+ var res = this._super();
+ var o = { url: null, alt: null };
+ // avoid typos, prevent addition of new properties to the object
+ Object.preventExtensions(o);
+
+ if ($(this.media).is("img")) {
+ o.url = this.media.getAttribute('src');
+ } else {
+ this.add_class = "img-responsive pull-left";
+ }
+ this.parent.$(".pager > li").click(function (e) {
+ e.preventDefault();
+ var $target = $(e.currentTarget);
+ if ($target.hasClass('disabled')) {
+ return;
+ }
+ self.page += $target.hasClass('previous') ? -1 : 1;
+ self.display_attachments();
+ });
+ this.set_image(o.url, o.alt);
+ this.fetch_existing();
+ return res;
+ },
+ push: function (url, alt, id) {
+ if (this.options.select_images) {
+ var img = _.select(this.images, function (v) { return v.url == url;});
+ if (img.length) {
+ this.images.splice(this.images.indexOf(img[0]),1);
+ return;
+ }
+ } else {
+ this.images = [];
+ }
+ this.images.push({'url': url, 'alt': alt, 'id': id});
+ },
+ save: function () {
+ if (this.options.select_images) {
+ this.parent.trigger("save", this.images);
+ return this.images;
+ }
+ this.parent.trigger("save", this.media);
+
+ var img = this.images[0] || {
+ 'url': this.$(".existing-attachments img:first").attr('src'),
+ 'alt': this.$(".existing-attachments img:first").attr('alt')
+ };
+
+ if (this.media.tagName !== "IMG") {
+ var media = document.createElement('img');
+ $(this.media).replaceWith(media);
+ this.media = media;
+ }
+
+ $(this.media).attr('src', img.url).attr('alt', img.alt);
+
+ var style = this.style;
+ this.media.setAttribute('src', img.url);
+ if (style) { this.media.addClass(style); }
+
+ return this.media;
+ },
+ clear: function () {
+ this.media.className = this.media.className.replace(/(^|\s)(img(\s|$)|img-[^\s]*)/g, ' ');
+ },
+ cancel: function () {
+ this.trigger('cancel');
+ },
+ change_input: function (e) {
+ var $input = $(e.target);
+ var $button = $input.parent().find("button");
+ if ($input.val() === "") {
+ $button.addClass("btn-default").removeClass("btn-primary");
+ } else {
+ $button.removeClass("btn-default").addClass("btn-primary");
+ }
+ },
+ search: function (needle) {
+ var self = this;
+ this.fetch_existing(needle).then(function () {
+ self.selected_existing();
+ });
+ },
+ set_image: function (url, alt, error) {
+ var self = this;
+ if (url) {
+ this.push(url, alt);
+ }
+ this.$('input.url').val('');
+ this.fetch_existing().then(function () {
+ self.selected_existing();
+ });
+ },
+ form_submit: function (event) {
+ var self = this;
+ var $form = this.$('form[action="/web_editor/attachment/add"]');
+ if (!$form.find('input[name="upload"]').val().length) {
+ var url = $form.find('input[name="url"]').val();
+ if (this.selected_existing().size()) {
+ event.preventDefault();
+ return false;
+ }
+ }
+ $form.find('.well > div').hide().last().after('<span class="fa fa-spin fa-3x fa-refresh"/>');
+
+ var callback = _.uniqueId('func_');
+ this.$('input[name=func]').val(callback);
+ window[callback] = function (attachments, error) {
+ delete window[callback];
+ $form.find('.well > span').remove();
+ $form.find('.well > div').show();
+ if (error || !attachments.length) {
+ self.file_selected(null, error || !attachments.length);
+ }
+ for (var i=0; i<attachments.length; i++) {
+ self.file_selected(attachments[i]['local_url'] || '/web_editor/image/ir.attachment/'+attachments[i].id+'_'+(new Date().getTime())+'/datas', error);
+ }
+ };
+ },
+ file_selection: function () {
+ this.$el.addClass('nosave');
+ this.$('form').removeClass('has-error').find('.help-block').empty();
+ this.$('button.filepicker').removeClass('btn-danger btn-success');
+ this.$('form').submit();
+ },
+ file_selected: function(url, error) {
+ var $button = this.$('button.filepicker');
+ if (!error) {
+ $button.addClass('btn-success');
+ } else {
+ url = null;
+ this.$('form').addClass('has-error')
+ .find('.help-block').text(error);
+ $button.addClass('btn-danger');
+ }
+ this.set_image(url, null, error);
+
+ if (!this.options.select_images) {
+ // auto save and close popup
+ this.parent.save();
+ }
+ },
+ fetch_existing: function (needle) {
+ var domain = [['res_model', '=', 'ir.ui.view'], '|',
+ ['mimetype', '=', false], ['mimetype', '=like', 'image/%']];
+ if (needle && needle.length) {
+ domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
+ }
+ return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'ir.attachment',
+ method: 'search_read',
+ args: [],
+ kwargs: {
+ domain: domain,
+ fields: ['name', 'local_url'],
+ order: 'id desc',
+ context: base.get_context()
+ }
+ }).then(this.proxy('fetched_existing'));
+ },
+ fetched_existing: function (records) {
+ this.records = records;
+ this.display_attachments();
+ },
+ display_attachments: function () {
+ this.$('.help-block').empty();
+ var per_screen = IMAGES_PER_ROW * IMAGES_ROWS;
+
+ var from = this.page * per_screen;
+ var records = this.records;
+
+ // Create rows of 3 records
+ var rows = _(records).chain()
+ .slice(from, from + per_screen)
+ .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); })
+ .values()
+ .value();
+ this.$('.existing-attachments').replaceWith(
+ QWeb.render(
+ 'web_editor.dialog.image.existing.content', {rows: rows}));
+ this.parent.$('.pager')
+ .find('li.previous').toggleClass('disabled', (from === 0)).end()
+ .find('li.next').toggleClass('disabled', (from + per_screen >= records.length));
+
+ this.selected_existing();
+ },
+ select_existing: function (e) {
+ var $img = $(e.currentTarget);
+ this.push($img.attr('src'), $img.attr('alt'), $img.data('id'));
+ this.selected_existing();
+ },
+ selected_existing: function () {
+ var self = this;
+ this.$('.existing-attachment-cell.media_selected').removeClass("media_selected");
+ var $select = this.$('.existing-attachment-cell img').filter(function () {
+ var $img = $(this);
+ var url = $img.attr("src");
+ return !!_.select(self.images, function (v) {
+ if (v.url === url) {
+ if (!v.id) {
+ v.id = $img.data('id');
+ v.alt = $img.attr('alt');
+ }
+ return true;
+ }
+ }).length;
+ });
+ $select.parent().addClass("media_selected");
+ return $select;
+ },
+ try_remove: function (e) {
+ var $help_block = this.$('.help-block').empty();
+ var self = this;
+ var $a = $(e.target);
+ var id = parseInt($a.data('id'), 10);
+ var attachment = _.findWhere(this.records, {id: id});
+ var $both = $a.parent().children();
+
+ $both.css({borderWidth: "5px", borderColor: "#f00"});
+
+ return ajax.jsonRpc('/web_editor/attachment/remove', 'call', {'ids': [id]}).then(function (prevented) {
+ if (_.isEmpty(prevented)) {
+ self.records = _.without(self.records, attachment);
+ self.display_attachments();
+ return;
+ }
+ $both.css({borderWidth: "", borderColor: ""});
+ $help_block.replaceWith(QWeb.render(
+ 'web_editor.dialog.image.existing.error', {
+ views: prevented[id]
+ }
+ ));
+ });
+ },
+});
+
+
+var cacheCssSelectors = {};
+var getCssSelectors = function(filter) {
+ var css = [];
+ if (cacheCssSelectors[filter]) {
+ return cacheCssSelectors[filter];
+ }
+ var sheets = document.styleSheets;
+ for(var i = 0; i < sheets.length; i++) {
+ var rules = sheets[i].rules || sheets[i].cssRules;
+ if (rules) {
+ for(var r = 0; r < rules.length; r++) {
+ var selectorText = rules[r].selectorText;
+ if (selectorText) {
+ var match = selectorText.match(filter);
+ if (match) {
+ css.push([match[1], rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, '')]);
+ }
+ }
+ }
+ }
+ }
+ return cacheCssSelectors[filter] = css;
+};
+function computeFonts() {
+ _.each(fontIcons, function (data) {
+ data.icons = _.map(getCssSelectors(data.parser), function (css) {
+ return css[0].slice(1, css[0].length).replace(/::?before$/, '');
+ });
+ });
+}
+
+rte.Class.include({
+ init: function (EditorBar) {
+ this._super.apply(this, arguments);
+ computeFonts();
+ }
+});
+
+/* list of font icons to load by editor. The icons are displayed in the media editor and
+ * identified like font and image (can be colored, spinned, resized with fa classes).
+ * To add font, push a new object {base, parser}
+ * - base: class who appear on all fonts (eg: fa fa-refresh)
+ * - parser: regular expression used to select all font in css style sheets
+ */
+var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}];
+
+
+/**
+ * FontIconsDialog widget. Lets users change a font awsome, suport all
+ * font awsome loaded in the css files.
+ */
+var fontIconsDialog = Widget.extend({
+ template: 'web_editor.dialog.font-icons',
+ events : _.extend({}, Dialog.prototype.events, {
+ 'click .font-icons-icon': function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.$('#fa-icon').val(e.target.getAttribute('data-id'));
+ $(".font-icons-icon").removeClass("font-icons-selected");
+ $(event.target).addClass("font-icons-selected");
+ },
+ }),
+
+ // extract list of font (like awsome) from the cheatsheet.
+ renderElement: function() {
+ this.iconsParser = fontIcons;
+ this.icons = _.flatten(_.map(fontIcons, function (data) {
+ return data.icons;
+ }));
+ this._super();
+ },
+
+ init: function (parent, media) {
+ this._super();
+ this.parent = parent;
+ this.media = media;
+ if (_.find(fontIcons, function (data) {return !data.icons;})) {
+ computeFonts();
+ }
+ },
+ start: function () {
+ return this._super().then(this.proxy('load_data'));
+ },
+ search: function (needle) {
+ var iconsParser = this.iconsParser;
+ if (needle) {
+ var parser = [];
+ var fontIcon;
+
+ for (var k=0; k<fontIcons.length; k++) {
+ fontIcon = fontIcons[k];
+ var icons = _(fontIcon.icons).filter(function (icon) {
+ return icon.indexOf(needle) !== -1;
+ });
+ if (icons.length) {
+ parser.push({
+ base: fontIcon.base,
+ icons: icons
+ });
+ }
+ }
+ iconsParser = parser;
+ }
+ this.$('div.font-icons-icons').html(
+ QWeb.render('web_editor.dialog.font-icons.icons', {'iconsParser': iconsParser}));
+ },
+ /**
+ * Removes existing FontAwesome classes on the bound element, and sets
+ * all the new ones if necessary.
+ */
+ save: function () {
+ var self = this;
+ this.parent.trigger("save", this.media);
+ var icons = this.icons;
+ var style = this.media.attributes.style ? this.media.attributes.style.value : '';
+ var classes = (this.media.className||"").split(/\s+/);
+ var non_fa_classes = _.reject(classes, function (cls) {
+ return self.getFont(cls);
+ });
+ var final_classes = non_fa_classes.concat(this.get_fa_classes());
+ if (this.media.tagName !== "SPAN") {
+ var media = document.createElement('span');
+ $(this.media).replaceWith(media);
+ this.media = media;
+ style = style.replace(/\s*width:[^;]+/, '');
+ }
+ $(this.media).attr("class", _.compact(final_classes).join(' ')).attr("style", style);
+ },
+ /**
+ * return the data font object (with base, parser and icons) or null
+ */
+ getFont: function (classNames) {
+ if (!(classNames instanceof Array)) {
+ classNames = (classNames||"").split(/\s+/);
+ }
+ var fontIcon, className;
+ for (var i=0; i<classNames.length; i++) {
+ className = classNames[i];
+ for (var k=0; k<fontIcons.length; k++) {
+ fontIcon = fontIcons[k];
+ if (className === fontIcon.base || fontIcon.icons.indexOf(className) !== -1) {
+ return {
+ 'base': fontIcon.base,
+ 'parser': fontIcon.parser,
+ 'icons': fontIcon.icons,
+ 'font': className
+ };
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Looks up the various FontAwesome classes on the bound element and
+ * sets the corresponding template/form elements to the right state.
+ * If multiple classes of the same category are present on an element
+ * (e.g. fa-lg and fa-3x) the last one occurring will be selected,
+ * which may not match the visual look of the element.
+ */
+ load_data: function () {
+ var classes = (this.media&&this.media.className||"").split(/\s+/);
+ for (var i = 0; i < classes.length; i++) {
+ var cls = classes[i];
+ switch(cls) {
+ case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x':
+ // size classes
+ this.$('#fa-size').val(cls);
+ continue;
+ case 'fa-spin':
+ case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270':
+ case 'fa-flip-horizontal':case 'fa-rotate-vertical':
+ this.$('#fa-rotation').val(cls);
+ continue;
+ case 'fa-fw':
+ continue;
+ case 'fa-border':
+ this.$('#fa-border').prop('checked', true);
+ continue;
+ case '': continue;
+ default:
+ $(".font-icons-icon").removeClass("font-icons-selected").filter("."+cls).addClass("font-icons-selected");
+ for (var k=0; k<this.icons.length; k++) {
+ if (this.icons.indexOf(cls) !== -1) {
+ this.$('#fa-icon').val(cls);
+ break;
+ }
+ }
+ }
+ }
+ },
+ /**
+ * Serializes the dialog to an array of FontAwesome classes. Includes
+ * the base ``fa``.
+ */
+ get_fa_classes: function () {
+ var font = this.getFont(this.$('#fa-icon').val());
+ return [
+ font ? font.base : 'fa',
+ font ? font.font : "",
+ this.$('#fa-size').val(),
+ this.$('#fa-rotation').val(),
+ this.$('#fa-border').prop('checked') ? 'fa-border' : ''
+ ];
+ },
+ clear: function () {
+ this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' ');
+ },
+});
+
+
+function createVideoNode(url) {
+ // video url patterns(youtube, instagram, vimeo, dailymotion, youku)
+ var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
+ var ytMatch = url.match(ytRegExp);
+
+ var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/;
+ var igMatch = url.match(igRegExp);
+
+ var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/;
+ var vMatch = url.match(vRegExp);
+
+ var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
+ var vimMatch = url.match(vimRegExp);
+
+ var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/;
+ var dmMatch = url.match(dmRegExp);
+
+ var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/;
+ var youkuMatch = url.match(youkuRegExp);
+
+ var $video = $('<iframe>');
+ if (ytMatch && ytMatch[1].length === 11) {
+ var youtubeId = ytMatch[1];
+ $video = $('<iframe>')
+ .attr('src', '//www.youtube.com/embed/' + youtubeId)
+ .attr('width', '640').attr('height', '360');
+ } else if (igMatch && igMatch[0].length) {
+ $video = $('<iframe>')
+ .attr('src', igMatch[0] + '/embed/')
+ .attr('width', '612').attr('height', '710')
+ .attr('scrolling', 'no')
+ .attr('allowtransparency', 'true');
+ } else if (vMatch && vMatch[0].length) {
+ $video = $('<iframe>')
+ .attr('src', vMatch[0] + '/embed/simple')
+ .attr('width', '600').attr('height', '600')
+ .attr('class', 'vine-embed');
+ } else if (vimMatch && vimMatch[3].length) {
+ $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
+ .attr('src', '//player.vimeo.com/video/' + vimMatch[3])
+ .attr('width', '640').attr('height', '360');
+ } else if (dmMatch && dmMatch[2].length) {
+ $video = $('<iframe>')
+ .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2])
+ .attr('width', '640').attr('height', '360');
+ } else if (youkuMatch && youkuMatch[1].length) {
+ $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
+ .attr('height', '498')
+ .attr('width', '510')
+ .attr('src', '//player.youku.com/embed/' + youkuMatch[1]);
+ } else {
+ // this is not a known video link. Now what, Cat? Now what?
+ }
+
+ $video.attr('frameborder', 0);
+
+ return $video;
+}
+
+/**
+ * VideoDialog widget. Lets users change a video, support all summernote
+ * video, and embled iframe
+ */
+var VideoDialog = Widget.extend({
+ template: 'web_editor.dialog.video',
+ events : _.extend({}, Dialog.prototype.events, {
+ 'click input#urlvideo ~ button': 'get_video',
+ 'click input#embedvideo ~ button': 'get_embed_video',
+ 'change input#urlvideo': 'change_input',
+ 'keyup input#urlvideo': 'change_input',
+ 'change input#embedvideo': 'change_input',
+ 'keyup input#embedvideo': 'change_input'
+ }),
+ init: function (parent, media) {
+ this._super();
+ this.parent = parent;
+ this.media = media;
+ },
+ start: function () {
+ this.$preview = this.$('.preview-container').detach();
+ this.$iframe = this.$("iframe");
+ var $media = $(this.media);
+ if ($media.hasClass("media_iframe_video")) {
+ var src = $media.data('src');
+ this.$("input#urlvideo").val(src);
+ this.$("#autoplay").attr("checked", src.indexOf('autoplay=1') != -1);
+ this.get_video();
+ } else {
+ this.add_class = "pull-left";
+ }
+ return this._super();
+ },
+ change_input: function (e) {
+ var $input = $(e.target);
+ var $button = $input.parent().find("button");
+ if ($input.val() === "") {
+ $button.addClass("btn-default").removeClass("btn-primary");
+ } else {
+ $button.removeClass("btn-default").addClass("btn-primary");
+ }
+ },
+ get_embed_video: function (event) {
+ event.preventDefault();
+ var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/);
+ if (embedvideo) {
+ this.$("input#urlvideo").val(embedvideo[1]);
+ this.get_video(event);
+ }
+ return false;
+ },
+ get_video: function (event) {
+ if (event) event.preventDefault();
+ var $video = createVideoNode(this.$("input#urlvideo").val());
+ this.$iframe.replaceWith($video);
+ this.$iframe = $video;
+ return false;
+ },
+ save: function () {
+ this.parent.trigger("save", this.media);
+ var video_id = this.$("#video_id").val();
+ if (!video_id) {
+ this.$("button.btn-primary").click();
+ video_id = this.$("#video_id").val();
+ }
+ var video_type = this.$("#video_type").val();
+ var $iframe = $(
+ '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+
+ '<div class="css_editable_mode_display">&nbsp;</div>'+
+ '<div class="media_iframe_video_size" contentEditable="false">&nbsp;</div>'+
+ '<iframe src="'+this.$iframe.attr("src")+'" frameborder="0" allowfullscreen="allowfullscreen" contentEditable="false"></iframe>'+
+ '</div>');
+ $(this.media).replaceWith($iframe);
+ this.media = $iframe[0];
+ },
+ clear: function () {
+ if (this.media.dataset.src) {
+ try {
+ delete this.media.dataset.src;
+ } catch(e) {
+ this.media.dataset.src = undefined;
+ }
+ }
+ this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' ');
+ },
+});
+
+/* ----- EDITOR: LINK & MEDIA ---- */
+var LinkDialog = Dialog.extend({
+ template: 'web_editor.dialog.link',
+ events: _.extend({}, Dialog.prototype.events, {
+ 'change :input.url-source': 'changed',
+ 'keyup :input.url': 'onkeyup',
+ 'keyup :input': 'preview',
+ 'click button.remove': 'remove_link',
+ 'change input#link-text': function (e) {
+ this.text = $(e.target).val();
+ },
+ 'change .link-style': function (e) {
+ this.preview();
+ },
+ }),
+ init: function (editable, linkInfo) {
+ this._super(editable, linkInfo);
+ this.editable = editable;
+ this.data = linkInfo || {};
+
+ this.data.className = "";
+ if (this.data.range) {
+ this.data.iniClassName = $(this.data.range.sc).filter("a").attr("class") || "";
+ this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' ');
+
+ var is_link = this.data.range.isOnAnchor();
+ var r = this.data.range;
+
+ var sc = r.sc;
+ var so = r.so;
+ var ec = r.ec;
+ var eo = r.eo;
+
+ var nodes;
+ if (!is_link) {
+ if (sc.tagName) {
+ sc = dom.firstChild(so ? sc.childNodes[so] : sc);
+ so = 0;
+ } else if (so !== sc.textContent.length) {
+ if (sc === ec) {
+ ec = sc = sc.splitText(so);
+ eo -= so;
+ } else {
+ sc = sc.splitText(so);
+ }
+ so = 0;
+ }
+ if (ec.tagName) {
+ ec = dom.lastChild(eo ? ec.childNodes[eo-1] : ec);
+ eo = ec.textContent.length;
+ } else if (eo !== ec.textContent.length) {
+ ec.splitText(eo);
+ }
+
+ nodes = dom.listBetween(sc, ec);
+
+ // browsers can't target a picture or void node
+ if (dom.isVoid(sc) || dom.isImg(sc)) {
+ so = dom.listPrev(sc).length-1;
+ sc = sc.parentNode;
+ }
+ if (dom.isBR(ec)) {
+ eo = dom.listPrev(ec).length-1;
+ ec = ec.parentNode;
+ } else if (dom.isVoid(ec) || dom.isImg(sc)) {
+ eo = dom.listPrev(ec).length;
+ ec = ec.parentNode;
+ }
+
+ this.data.range = range.create(sc, so, ec, eo);
+ this.data.range.select();
+ } else {
+ nodes = dom.ancestor(sc, dom.isAnchor).childNodes;
+ }
+
+ if (dom.isImg(sc) && nodes.indexOf(sc) === -1) {
+ nodes.push(sc);
+ }
+ if (nodes.length > 1 || dom.ancestor(nodes[0], dom.isImg)) {
+ var text = "";
+ this.data.images = [];
+ for (var i=0; i<nodes.length; i++) {
+ if (dom.ancestor(nodes[i], dom.isImg)) {
+ this.data.images.push(dom.ancestor(nodes[i], dom.isImg));
+ text += '[IMG]';
+ } else if (!is_link && i===0) {
+ text += nodes[i].textContent.slice(so, Infinity);
+ } else if (!is_link && i===nodes.length-1) {
+ text += nodes[i].textContent.slice(0, eo);
+ } else {
+ text += nodes[i].textContent;
+ }
+ }
+ this.data.text = text;
+ }
+ }
+
+ this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' ');
+ },
+ start: function () {
+ this.bind_data();
+ this.$('input.url-source:eq(1)').closest('.list-group-item').addClass('active');
+ return this._super();
+ },
+ get_data: function (test) {
+ var self = this;
+ var def = new $.Deferred();
+ var $e = this.$('.active input.url-source');
+ if (!$e.length) {
+ $e = this.$('input.url-source:first');
+ }
+ var val = $e.val();
+ var label = this.$('#link-text').val() || val;
+
+ if (label && this.data.images) {
+ for(var i=0; i<this.data.images.length; i++) {
+ label = label.replace(/</, "&lt;").replace(/>/, "&gt;").replace(/\[IMG\]/, this.data.images[i].outerHTML);
+ }
+ }
+
+ if (!test && (!val || !$e[0].checkValidity())) {
+ // FIXME: error message
+ $e.closest('.form-group').addClass('has-error');
+ $e.focus();
+ def.reject();
+ }
+
+ var style = this.$("input[name='link-style-type']:checked").val() || '';
+ var size = this.$("input[name='link-style-size']:checked").val() || '';
+ var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size;
+ var isNewWindow = this.$('input.window-new').prop('checked');
+
+ if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) {
+ self.get_data_buy_mail(def, $e, isNewWindow, label, classes);
+ } else {
+ self.get_data_buy_url(def, $e, isNewWindow, label, classes);
+ }
+ return def;
+ },
+ get_data_buy_mail: function (def, $e, isNewWindow, label, classes) {
+ var val = $e.val();
+ def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes);
+ },
+ get_data_buy_url: function (def, $e, isNewWindow, label, classes) {
+ def.resolve($e.val(), isNewWindow, label, classes);
+ },
+ save: function () {
+ var self = this;
+ var _super = this._super.bind(this);
+ return this.get_data()
+ .then(function (url, new_window, label, classes) {
+ self.data.url = url;
+ self.data.newWindow = new_window;
+ self.data.text = label;
+ self.data.className = classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, '');
+
+ self.trigger("save", self.data);
+ }).then(_super);
+ },
+ bind_data: function () {
+ var href = this.data.url;
+ var new_window = this.data.isNewWindow;
+ var text = this.data.text;
+ var classes = this.data.iniClassName;
+
+ this.$('input#link-text').val(text);
+ this.$('input.window-new').prop('checked', new_window);
+
+ if (classes) {
+ this.$('input[value!=""]').each(function () {
+ var $option = $(this);
+ if (classes.indexOf($option.val()) !== -1) {
+ $option.attr("checked", "checked");
+ }
+ });
+ }
+
+ if (href) {
+ var match;
+ if(match = /mailto:(.+)/.exec(href)) {
+ this.$('input.email-address').val(match = /mailto:(.+)/.exec(href) ? match[1] : '');
+ } else {
+ this.$('input.url').val(href);
+ }
+ }
+ this.preview();
+ },
+ changed: function (e) {
+ $(e.target).closest('.list-group-item')
+ .addClass('active')
+ .siblings().removeClass('active')
+ .addBack().removeClass('has-error');
+ this.preview();
+ },
+ onkeyup: function (e) {
+ var $e = $(e.target);
+ var is_link = ($e.val()||'').length && $e.val().indexOf("@") === -1;
+ this.$('input.window-new').closest("div").toggle(is_link);
+ this.preview();
+ },
+ preview: function () {
+ var $preview = this.$("#link-preview");
+ this.get_data(true).then(function (url, new_window, label, classes) {
+ $preview.attr("target", new_window ? '_blank' : "")
+ .attr("href", url && url.length ? url : "#")
+ .html((label && label.length ? label : url))
+ .attr("class", classes.replace(/pull-\w+/, ''));
+ });
+ }
+});
+
+return {
+ 'getCssSelectors': getCssSelectors,
+ 'Dialog': Dialog,
+ 'alt': alt,
+ 'MediaDialog': MediaDialog,
+ 'fontIcons': fontIcons,
+ 'LinkDialog': LinkDialog
+};
+
+});