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 editor = require('web_editor.editor'); var animation = require('web_editor.snippets.animation'); var options = require('web_editor.snippets.options'); var qweb = core.qweb; var _t = core._t; ajax.loadXML('/web_editor/static/src/xml/snippets.xml', qweb); var data = {}; /* ----- SNIPPET SELECTOR ---- */ $.extend($.expr[':'], { hasData: function (node) { return !!_.toArray(node.dataset).length; }, data: function (node, i, m) { return $(node).data(m[3]); } }); data.globalSelector = { closest: function () { return $(); }, all: function () { return $(); }, is: function () { return false; }, }; /** * Snippet editor main class. * Management of drag&drop and focus/blur of snippets. * Previously named "BuildingBlock". */ data.Class = Widget.extend({ id: "oe_snippets", activeSnippets: [], init: function (parent, $editable) { this.$editable = $editable; this._super.apply(this, arguments); if(!$('#oe_manipulators').length) { $("
", {id: "oe_manipulators", 'class': "o_css_editor"}).appendTo('body'); } this.$active_snipped_id = false; this.snippets = []; data.instance = this; }, start: function () { var self = this; $(window).resize(function () { _.defer(function () { $(document).click(); }); }); 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') || data.globalSelector.is($(node)); }; $(window).on('resize', function () { if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { self.cover_target(self.$active_snipped_id.data("snippet-editor").$overlay, self.$active_snipped_id); } }); $(document).on('mousemove', function () { if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { self.$active_snipped_id.data("snippet-editor").$overlay.removeClass('o_keypress'); } }); $(document).on('keydown', function (event) { if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { self.$active_snipped_id.data("snippet-editor").$overlay.addClass('o_keypress'); } if ((event.metaKey || (event.ctrlKey && !event.altKey)) && event.shiftKey && event.keyCode >= 48 && event.keyCode <= 57) { self.$('.scroll:first > ul li:eq('+(event.keyCode-49)+') a').trigger("click"); event.preventDefault(); } }); }, _get_snippet_url: function () { return '/web_editor/snippets'; }, _add_check_selector : function (selector, no_check, is_children) { var self = this; selector = selector.split(/\s*,/).join(":not(.o_snippet_not_selectable), ") + ":not(.o_snippet_not_selectable)"; if (no_check) { return { closest: function ($from, parentNode) { return $from.closest(selector, parentNode); }, all: function ($from) { return $from ? $from.find(selector) : $(selector); }, is: function ($from) { return $from.is(selector); } }; } else { selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable"; return { closest: function ($from, parentNode) { var parents = self.$editable.get(); return $from.closest(selector, parentNode).filter(function () { var node = this; while (node.parentNode) { if (parents.indexOf(node)!==-1) { return true; } node = node.parentNode; } return false; }); }, all: is_children ? function ($from) { return ($from || self.$editable).find(selector); } : 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.$el.detach(); return; } return ajax.jsonRpc(url, 'call', {}).then(function (html) { self.compute_snippet_templates(html); self.trigger("snippets:ready"); }, function () { console.warn('Snippets template not found:', url); }); }, compute_snippet_templates: function (html) { var self = this; var $html = $(html); 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"]').each(function () { $(this).children().attr('data-oe-type', "snippet").attr('data-oe-thumbnail', $(this).data('oe-thumbnail')); }); // 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, true), 'drop-in': $style.data('drop-in') && self._add_check_selector($style.data('drop-in'), no_check), 'data': $style.data() }; self.templateOptions.push(option); selector.push(option.selector); }); $styles.addClass("hidden"); data.globalSelector.closest = function ($from) { var $temp; var $target; var len = selector.length; for (var i = 0; i'+ '
'+ ''+ '
'); $div.find('span').text($snippet.attr("name")); $snippet.prepend($div); // 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 (!$snippet.data("selector")) { $("> *:not(.oe_snippet_thumbnail)", this).addClass('oe_snippet_body'); } number++; }); // hide scroll if no snippets defined if (!number) { this.$el.detach(); } $("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"); }); // clean t-oe $html.find('[data-oe-model], [data-oe-type]').each(function () { for (var k=0; k middle) { break; } last = $li[k]; } $(last).addClass("active"); }); self.make_snippet_draggable(self.$snippets); this.associate_snippet_names(this.$snippets); }, associate_snippet_names: function ($snippets) { _.each($snippets, function (snippet) { var $snippet = $(snippet); var $sbody = $snippet.find(".oe_snippet_body"); var snippet_classes = $sbody.attr("class").match(/s_[^ ]+/g); if (snippet_classes && snippet_classes.length) { snippet_classes = snippet_classes.join("."); } $("#wrapwrap ." + snippet_classes).data("name", $snippet.find(".oe_snippet_thumbnail_title").text()); }); }, cover_target: function ($el, $target) { if ($el.data('not-cover_target')) { return; } var pos = $target.offset(); var mt = parseInt($target.css("margin-top") || 0); $el.css({ width: $target.outerWidth(), left: pos.left, top: pos.top - mt, }); $el.find('.oe_handles').css({ height: $target.outerHeight(true), }); $el.toggleClass('o_top_cover', pos.top <= (this.$('#o_scroll').position().top + 15)); }, show_blocks: function () { var self = this; var cache = {}; this.$(".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); }); }, 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 (!data.globalSelector.is($target)) { $target = data.globalSelector.closest($target); } if (self.$active_snipped_id && self.$active_snipped_id.is($target)) { return; } self.make_active($target); }); }, snippet_blur: function ($snippet) { if ($snippet) { if ($snippet.data("snippet-editor")) { $snippet.data("snippet-editor").on_blur(); } } }, snippet_focus: function ($snippet) { if ($snippet) { if ($snippet.data("snippet-editor")) { $snippet.data("snippet-editor").on_focus(); } } }, clean_for_save: function () { var self = this; var opt = options.registry; var template = self.templateOptions; for (var k in template) { var Option = opt[template[k]['option']]; if (Option && Option.prototype.clean_for_save !== editor.dummy) { template[k].selector.all().filter(function () { var node = this; while (!/o_dirty|o_editable/.test(node.className) && node !== document) { node = node.parentNode; } return node.className.indexOf("o_dirty") !== -1; }).each(function () { new Option(self, null, $(this), k).clean_for_save(); }); } } this.$editable.find("*[contentEditable], *[attributeEditable]") .removeAttr('contentEditable') .removeProp('contentEditable') .removeAttr('attributeEditable') .removeProp('attributeEditable'); }, make_active: function ($snippet) { if ($snippet && this.$active_snipped_id && this.$active_snipped_id.get(0) === $snippet.get(0)) { return; } if (this.$active_snipped_id) { this.snippet_blur(this.$active_snipped_id); this.$active_snipped_id = false; } if ($snippet && $snippet.length) { if(_.indexOf(this.snippets, $snippet.get(0)) === -1) { this.snippets.push($snippet.get(0)); } this.$active_snipped_id = $snippet; this.create_snippet_editor(this.$active_snipped_id); this.cover_target($snippet.data('overlay'), $snippet); this.snippet_focus($snippet); } this.$el.trigger('snippet-activated', $snippet); if ($snippet) { $snippet.trigger('snippet-activated', $snippet); } }, create_snippet_editor: function ($snippet) { if (typeof $snippet.data("snippet-editor") === 'undefined') { if (!this.activate_overlay_zones($snippet).length) return; $snippet.data("snippet-editor", new data.Editor(this, $snippet)); } }, // activate drag and drop for the snippets in the snippet toolbar make_snippet_draggable: function ($snippets) { var self = this; var $tumb = $snippets.find(".oe_snippet_thumbnail_img:first"); var left = $tumb.outerWidth()/2; var top = $tumb.outerHeight()/2; var $toInsert, dropped, $snippet; $snippets.draggable({ greedy: true, helper: 'clone', zIndex: '1000', appendTo: 'body', cursor: "move", handle: ".oe_snippet_thumbnail", cursorAt: { 'left': left, 'top': top }, start: function () { dropped = false; // snippet_selectors => to get drop-near, drop-in $snippet = $(this); var $base_body = $snippet.find('.oe_snippet_body'); var $selector_siblings = $(); var $selector_children = $(); var temp = self.templateOptions; for (var k in temp) { if ($base_body.is(temp[k].base_selector)) { if (temp[k]['drop-near']) { if (!$selector_siblings) $selector_siblings = temp[k]['drop-near'].all(); else $selector_siblings = $selector_siblings.add(temp[k]['drop-near'].all()); } if (temp[k]['drop-in']) { if (!$selector_children) $selector_children = temp[k]['drop-in'].all(); else $selector_children = $selector_children.add(temp[k]['drop-in'].all()); } } } $toInsert = $base_body.clone().data("name", $snippet.find(".oe_snippet_thumbnail_title").text()); if (!$selector_siblings.length && !$selector_children.length) { console.warn($snippet.find(".oe_snippet_thumbnail_title").text() + " have not insert action: data-drop-near or data-drop-in"); return; } self.make_active(false); self.activate_insertion_zones($selector_siblings, $selector_children); $('.oe_drop_zone').droppable({ over: function () { dropped = true; $(this).first().after($toInsert).addClass('hidden'); }, out: function () { var prev = $toInsert.prev(); if(this === prev[0]) { dropped = false; $toInsert.detach(); $(this).removeClass('hidden'); } }, }); }, stop: function (ev, ui) { $toInsert.removeClass('oe_snippet_body'); if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3) { var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first(); if (el.length) { el.after($toInsert); dropped = true; } } self.$editable.find('.oe_drop_zone').droppable('destroy').remove(); if (dropped) { var prev = $toInsert.first()[0].previousSibling; var next = $toInsert.last()[0].nextSibling; var rte = self.getParent().rte; 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.$el.trigger('snippet-dropped', $target); animation.start(true, $target); self.call_for_all_snippets($target, function (editor, $snippet) { editor.drop_and_build_snippet(); }); self.create_snippet_editor($target); self.cover_target($target.data('overlay'), $target); $target.closest(".o_editable").trigger("content_changed"); self.make_active($target); },0); } else { $toInsert.remove(); } }, }); }, // call a method on a snippet and all his children call_for_all_snippets: function ($snippet, callback) { var self = this; $snippet.add(data.globalSelector.all($snippet)).each(function () { var $snippet = $(this); setTimeout(function () { self.create_snippet_editor($snippet); if ($snippet.data("snippet-editor")) { callback.call(self, $snippet.data("snippet-editor"), $snippet); } }); }); }, // return the original snippet in the editor bar from a snippet id (string) get_snippet_from_id: function (id) { return $('.oe_snippet').filter(function () { return $(this).data('option') === id; }).first(); }, // Create element insertion drop zones. two css selectors can be provided // selector.children -> will insert drop zones as direct child of the selected elements // in case the selected elements have children themselves, dropzones will be interleaved // with them. // selector.siblings -> will insert drop zones after and before selected elements activate_insertion_zones: function ($selector_siblings, $selector_children) { var self = this; var zone_template = $("
"); if ($selector_children) { $selector_children.each(function () { var $zone = $(this); var css = window.getComputedStyle(this); var float = css.float || css.cssFloat; var $drop = zone_template.clone(); $zone.append($drop); var node = $drop[0].previousSibling; var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); if (test) { $drop.addClass("oe_vertical").css({ 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), 'float': 'none', 'display': 'inline-block' }); } else if (float === "left" || float === "right") { $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30)); } $drop = $drop.clone(); $zone.prepend($drop); node = $drop[0].nextSibling; test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); if (test) { $drop.addClass("oe_vertical").css({ 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), 'float': 'none', 'display': 'inline-block' }); } else if (float === "left" || float === "right") { $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30)); } if (test) { $drop.css({'float': 'none', 'display': 'inline-block'}); } }); // add children near drop zone $selector_siblings = $(_.uniq(($selector_siblings || $()).add($selector_children.children()).get())); } 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) { clearTimeout(timer); timer = setTimeout(function () { if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) { self.cover_target($target.data('overlay'), $target); } }, 50); }); var resize = function () { if ($zone.parent().length) { self.cover_target($zone, $target); } else { $('body').off("resize", resize); } }; $('body').on("resize", resize); } self.cover_target($target.data('overlay'), $target); }); return $targets; } }); /** * Snippet editor Editor class. * Management of the overlay and option list for a snippet. */ data.Editor = Class.extend({ init: function (BuildingBlock, dom) { this.buildingBlock = BuildingBlock; this.$target = $(dom); this.$overlay = this.$target.data('overlay'); // Initialize parent button this.init_parent_options(); // Load overlay options content this.load_style_options(); // Initialize move/clone/remove buttons this.$overlay.on('click', '.oe_snippet_clone', _.bind(this.on_clone, this)); this.$overlay.on('click', '.oe_snippet_remove', _.bind(this.on_remove, this)); this._drag_and_drop(); }, getName: function () { if (this.$target.data("name") !== undefined) { return this.$target.data("name"); } if (this.$target.parent(".row").length) { return _t("Column"); } return _t("Block"); }, // activate drag and drop for the snippets in the snippet toolbar _drag_and_drop: function () { var self = this; this.dropped = false; this.$overlay.draggable({ greedy: true, appendTo: 'body', cursor: "move", handle: ".oe_snippet_move", cursorAt: { left: 18, top: 14 }, helper: function () { var $clone = $(this).clone().css({width: "24px", height: "24px", border: 0}); $clone.find(".oe_overlay_options >:not(:contains(.oe_snippet_move)), .oe_handle").remove(); $clone.find(":not(.glyphicon)").css({position: 'absolute', top: 0, left: 0}); $clone.appendTo("body").removeClass("hidden"); return $clone; }, start: _.bind(self._drag_and_drop_start, self), stop: _.bind(self._drag_and_drop_stop, self) }); }, _drag_and_drop_after_insert_dropzone: function () {}, _drag_and_drop_active_drop_zone: function ($zones) { var self = this; $zones.droppable({ over: function () { $(".oe_drop_zone.hide").removeClass("hide"); $(this).addClass("hide").first().after(self.$target); self.dropped = true; }, out: function () { $(this).removeClass("hide"); self.$target.detach(); self.dropped = false; }, }); }, _drag_and_drop_start: function () { var self = this; self.buildingBlock.editor_busy = true; self.size = { width: self.$target.width(), height: self.$target.height() }; self.$target.after("