diff options
| author | Christophe Matthieu <chm@odoo.com> | 2015-07-07 16:02:53 +0200 |
|---|---|---|
| committer | Christophe Matthieu <chm@odoo.com> | 2015-07-10 17:00:11 +0200 |
| commit | 8c559044a5e1be111dcb577ad7e5ab462c1002a0 (patch) | |
| tree | ba1602e15b2bca38babd6cca63a41da1d05b4321 /addons/web_editor/static/src/js | |
| parent | a616c57c609da0e5e56835dcb45e1c05590d25b8 (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.js | 322 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/base.js | 85 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor.js | 164 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/iframe.js | 78 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/inline.js | 77 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/rte.js | 597 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/rte.summernote.js | 781 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/snippets.animation.js | 82 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/snippets.editor.js | 1137 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/snippets.options.js | 534 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/summernote.js | 2192 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/tour.rte.js | 398 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/transcoder.js | 172 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/translator.js | 214 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/widgets.js | 1069 |
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(/"/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, "<"); + 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(/"/g, '"'); + this.title = ($(this.media).attr('title') || "").replace(/"/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, """) : null).attr('title', title ? title.replace(/"/g, """) : 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"> </div>'+ + '<div class="media_iframe_video_size" contentEditable="false"> </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(/</, "<").replace(/>/, ">").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 +}; + +}); |
