odoo.define('web_editor.snippets.options', function (require) { 'use strict'; var ajax = require('web.ajax'); var Class = require('web.Class'); var core = require('web.core'); var Dialog = require('web.Dialog'); var base = require('web_editor.base'); var editor = require('web_editor.editor'); var widget = require('web_editor.widget'); var qweb = core.qweb; var _t = core._t; /** * The SnippetOption class handles one option for one snippet. The registry returned by * this module contains the names of the specialized SnippetOption which can be reference * thanks to the data-js key of the full options web_editor template. */ var SnippetOption = Class.extend({ init: function (BuildingBlock, editor, $target, option_id) { this.buildingBlock = BuildingBlock; this.editor = editor; this.$target = $target; this.option = option_id; var option = this.buildingBlock.templateOptions[option_id]; this.$el = option.$el.children("li").clone(); this.data = option.$el.data(); this.$overlay = this.$target.data('overlay') || $(); }, // helper for this.$target.find $: function () { return this.$target.find.apply(this.$target, arguments); }, _bind_li_menu: function () { this.$el.filter("li:hasData").find('a:first') .off('mouseenter click') .on('mouseenter click', _.bind(_mouse, this)); this.$el .off('mouseenter click', "li:hasData a") .on('mouseenter click', "li:hasData a", _.bind(_mouse, this)); this.$el.off('mouseleave') .on('mouseleave', _.bind(this.reset, this)); this.$el.off('mouseleave', "ul") .on('mouseleave', "ul", _.bind(this.reset, this)); this.methodsToReset = []; function _mouse(event) { var $next = $(event.currentTarget).parent(); // triggers preview or apply methods if a menu item has been clicked this.select(event.type === "click" ? "click" : "over", $next); if (event.type === 'click') { this.set_active(); this.$target.trigger("snippet-option-change", [this]); } else { this.$target.trigger("snippet-option-preview", [this]); } } }, /** * The set_active method tweaks the option dropdown to show the selected value * according to the state of the $target the option customizes. */ set_active: function () { var self = this; this.$el.find('[data-toggle_class], [data-select_class]') .addBack('[data-toggle_class], [data-select_class]') .removeClass("active") .filter(function () { var $elem = $(this); var className = $elem.data("toggle_class") || $elem.data("select_class"); return self.$target.hasClass(className); }) .addClass("active"); }, start: function () { this.set_active(); this.$target.on('snippet-option-reset', _.bind(this.set_active, this)); this._bind_li_menu(); }, on_focus : function () { this._bind_li_menu(); }, on_blur : function () {}, on_clone: function ($clone) {}, on_move: function () {}, on_remove: function () {}, drop_and_build_snippet: function () {}, reset: function () { var self = this; var lis = this.$el.find("li.active").addBack('.active').get(); _.each(lis.reverse(), function (li) { var $li = $(li); for (var k in self.methodsToReset) { var method = self.methodsToReset[k]; if ($li.closest('[data-' + method + ']').length) { delete self.methodsToReset[k]; } } self.select("reset", $li); }); for (var k in this.methodsToReset) { var method = this.methodsToReset[k]; if (method) { this[method]("reset", null); } } this.methodsToReset = []; this.$target.trigger("snippet-option-reset", [this]); }, /** * Handle action type on a particuler $li (sub)option of the option. * @param type action type to handle ("click", "over", "reset") * @param $li the suboption DOM element which is concerned by the action */ select: function (type, $li) { // (sub)options can say they respond to only one action type if ($li.data('only') && type !== $li.data('only')) { return; } // If the type is click, the user selected the option for good (so reset and record action) if (type === "click") { this.reset(); this.buildingBlock.getParent().rte.historyRecordUndo(this.$target); } // Search for methods (data-...) (i.e. data-toggle_class) on the selected (sub)option and its parents var el = $li[0]; var methods = []; do { var m = _.omit(el.dataset, ["oeId", "oeModel", "oeField", "oeXpath", "oeSourceId", "only"]); if (_.keys(m).length) { methods.push([el, m]); } el = el.parentNode; } while (this.$el.parent().has(el).length); // Call the found method in the right order (parents -> child) and save them for later reset var self = this; _.each(methods.reverse(), function (data) { var $el = $(data[0]); var methods = data[1]; for (var k in methods) { if (self[k]) { if (type !== "reset" && self.methodsToReset.indexOf(k) === -1) { self.methodsToReset.push(k); } self[k](type, methods[k], $el); } else { console.error("'" + self.option + "' snippet option have not method '" + k + "'"); } } }); }, /** * Default option methods toggle_class and selected_class handle the two behavior of having a list * of option classes to toggle or not and having a list of option classes where only one must be chosen. */ toggle_class: function (type, value, $li) { var $lis = this.$el.find("[data-toggle_class]").addBack("[data-toggle_class]"); function map($lis) { return $lis.map(function () {return $(this).data("toggle_class");}).get().join(" "); } var classes = map($lis); var active_classes = map($lis.filter('.active, :has(.active)')); this.$target.removeClass(classes).addClass(active_classes); if (type !== 'reset') { this.$target.toggleClass(value); } }, select_class: function (type, value, $li) { var $lis = this.$el.find("[data-select_class]").addBack("[data-select_class]"); var classes = $lis.map(function () {return $(this).data("select_class");}).get().join(" "); this.$target.removeClass(classes); if (value) { this.$target.addClass(value); } }, /** * Helper method to evaluate the value expression directly as a function */ eval: function (type, value, $li) { var fn = new Function("node", "type", "value", "$li", value); fn.call(this, this, type, value, $li); }, clean_for_save: editor.dummy }); var registry = {}; /* ----- default options ---- */ /** * The colorpicker option is designed to change the background color class of a snippet. This class change the * default background color and text color of the snippet content. */ registry.colorpicker = SnippetOption.extend({ start: function () { var self = this; var res = this._super.apply(this, arguments); if (!this.$el.find('.colorpicker').length) { var $pt = $(qweb.render('web_editor.snippet.option.colorpicker')); var $clpicker = $(qweb.render('web_editor.colorpicker')); var $toggles = $pt.find('.o_colorpicker_section_menu'); var $tabs = $pt.find('.o_colorpicker_section_tabs'); var $sections = $clpicker.find('.o_colorpicker_section'); if ($sections.length) { $sections.each(function () { var $section = $(this); var id = 'o_palette_' + $section.data('name') + _.uniqueId(); var $li = $('
  • ') .append($('', {href: '#' + id}) .append($('', {'class': $section.data('iconClass') || '', html: $section.data('iconContent') || ''}))); $toggles.append($li); $tabs.append($section.addClass('tab-pane').attr('id', id)); }); $toggles.find('li').first().addClass('active'); $tabs.find('div').first().addClass('active'); $toggles.on('click mouseover', '> li > a', function (e) { e.preventDefault(); e.stopPropagation(); $(this).tab('show'); }); } else { $toggles.parent().empty().append($clpicker); } this.$el.find('li').append($pt); } if (this.$el.data('area')) { this.$target = this.$target.find(this.$el.data('area')); this.$el.removeData('area').removeAttr('area'); } var classes = []; this.$el.find(".colorpicker button").map(function () { var $color = $(this); var className = 'bg-' + $color.data('color'); $color.addClass(className); if (self.$target.hasClass(className)) { self.color = className; $color.addClass("selected"); } classes.push(className); }); this.classes = classes.join(" "); this.bind_events(); return res; }, bind_events: function () { var self = this; var $colors = this.$el.find(".colorpicker button"); $colors .mouseenter(function (e) { self.$target.removeClass(self.classes).addClass('bg-' + $(this).data("color")); self.$target.trigger("background-color-event", e.type); }) .mouseleave(function (e) { self.$target.removeClass(self.classes); var $selected = $colors.filter(".selected"); if ($selected.length) { self.$target.addClass('bg-' + $selected.data("color")); } self.$target.trigger("background-color-event", e.type); }) .click(function (e) { $colors.removeClass("selected"); $(this).addClass("selected"); self.$target.closest(".o_editable").trigger("content_changed"); self.$target.trigger("background-color-event", e.type); }); this.$el.find('.note-color-reset').on('click', function () { self.$target.removeClass(self.classes); $colors.removeClass("selected"); }); } }); /** * The background option is designed to change the background image of a snippet. */ registry.background = SnippetOption.extend({ start: function () { var res = this._super.apply(this, arguments); this.$target.off(".background-option") .on("background-color-event.background-option", (function (e, type) { this.$el.find("li:first > a").trigger(type); }).bind(this)); return res; }, background: function (type, value, $li) { if (value && value.length) { this.$target.css("background-image", "url(" + value + ")"); this.$target.addClass("oe_img_bg"); } else { this.$target.css("background-image", ""); this.$target.removeClass("oe_img_bg oe_custom_bg"); } }, select_class: function (type, value, $li) { this.background(type, '', $li); this._super(type, value ? (value + " oe_img_bg") : value, $li); }, choose_image: function (type, value, $li) { if(type !== "click") { return; } // Put fake image in the DOM, edit it and use it as background-image var $image = $("", {"class": "hidden", "src": value}).appendTo(this.$target); var _editor = new widget.MediaDialog(null, {}, null, $image[0]).open(); _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); _editor.on('saved', this, function () { var value = $image.attr("src"); this.background(type, value, $li); this.$target.addClass("oe_custom_bg"); this.set_active(); this.$target.trigger("snippet-option-change", [this]); }); _editor.on('closed', this, function () { $image.remove(); }); }, set_active: function () { this._super.apply(this, arguments); var src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, ""); if (this.$target.hasClass('oe_custom_bg')) { this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src); } this.$el.find('li[data-background]') .removeClass("active") .filter(function () { var bgOption = $(this).data("background"); return (bgOption === "" && src === "" || bgOption !== "" && src.indexOf(bgOption) >= 0); }) .addClass("active"); } }); /** * The background_position option is designed to change the background image position of a snippet. */ registry.background_position = SnippetOption.extend({ start: function () { this._super.apply(this, arguments); this.on_focus(); var self = this; this.$target.on("snippet-option-change", function () { self.on_focus(); }); }, on_focus: function () { this._super.apply(this, arguments); this.$el.toggleClass('hidden', this.$target.css('background-image') === 'none'); }, background_position: function (type, value, $li) { if (type != 'click') { return; } var self = this; this.previous_state = [this.$target.attr('class'), this.$target.css('background-size'), this.$target.css('background-position')]; this.bg_pos = self.$target.css('background-position').split(' '); this.bg_siz = self.$target.css('background-size').split(' '); this.modal = new Dialog(null, { title: _t("Background Image Sizing"), $content: $(qweb.render('web_editor.dialog.background_position')), buttons: [ {text: _t("Ok"), classes: "btn-primary", close: true, click: _.bind(this.save, this)}, {text: _t("Discard"), close: true, click: _.bind(this.discard, this)}, ], }).open(); this.modal.opened().then(function () { // Fetch data form $target var value = ((self.$target.hasClass('o_bg_img_opt_contain'))? 'contain' : ((self.$target.hasClass('o_bg_img_opt_custom'))? 'custom' : 'cover')); self.modal.$("> label > input[value=" + value + "]").prop('checked', true); if(self.$target.hasClass("o_bg_img_opt_repeat")) { self.modal.$("#o_bg_img_opt_contain_repeat").prop('checked', true); self.modal.$("#o_bg_img_opt_custom_repeat").val('o_bg_img_opt_repeat'); } else if (self.$target.hasClass("o_bg_img_opt_repeat_x")) { self.modal.$("#o_bg_img_opt_custom_repeat").val('o_bg_img_opt_repeat_x'); } else if (self.$target.hasClass("o_bg_img_opt_repeat_y")) { self.modal.$("#o_bg_img_opt_custom_repeat").val('o_bg_img_opt_repeat_y'); } if(self.bg_pos.length > 1) { self.bg_pos = { x: self.bg_pos[0], y: self.bg_pos[1], }; self.modal.$("#o_bg_img_opt_custom_pos_x").val(self.bg_pos.x.replace('%', '')); self.modal.$("#o_bg_img_opt_custom_pos_y").val(self.bg_pos.y.replace('%', '')); } if(self.bg_siz.length > 1) { self.modal.$("#o_bg_img_opt_custom_size_x").val(self.bg_siz[0].replace('%', '')); self.modal.$("#o_bg_img_opt_custom_size_y").val(self.bg_siz[1].replace('%', '')); } // Focus Point self.$focus = self.modal.$(".o_focus_point"); self.update_pos_information(); var img_url = /\(['"]?([^'"]+)['"]?\)/g.exec(self.$target.css('background-image')); img_url = (img_url && img_url[1]) || ''; var $img = $('', {'class': 'img img-responsive', src: img_url}); $img.on('load', function () { self.bind_img_events($img); }); $img.prependTo(self.modal.$(".o_bg_img_opt_object")); // Bind events self.modal.$el.on('change', '> label > input', function (e) { self.modal.$('> .o_bg_img_opt').addClass('o_hidden') .filter("[data-value=" + e.target.value + "]") .removeClass('o_hidden'); }); self.modal.$el.on('change', 'input, select', function (e) { self.save(); }); self.modal.$("> label > input:checked").trigger('change'); }); }, bind_img_events: function ($img) { var self = this; var mousedown = false; $img.on('mousedown', function (e) { mousedown = true; }); $img.on('mousemove', function (e) { if (mousedown) { _update(e); } }); $img.on('mouseup', function (e) { self.$focus.addClass('o_with_transition'); _update(e); setTimeout(function () { self.$focus.removeClass('o_with_transition'); }, 200); mousedown = false; }); function _update(e) { var posX = e.pageX - $(e.target).offset().left; var posY = e.pageY - $(e.target).offset().top; self.bg_pos = { x: clip_value(posX/$img.width()*100).toFixed(2) + '%', y: clip_value(posY/$img.height()*100).toFixed(2) + '%', }; self.update_pos_information(); self.save(); } function clip_value(value) { return Math.max(0, Math.min(value, 100)); } }, update_pos_information: function () { this.modal.$(".o_bg_img_opt_ui_info .o_x").text(this.bg_pos.x); this.modal.$(".o_bg_img_opt_ui_info .o_y").text(this.bg_pos.y); this.$focus.css({ left: this.bg_pos.x, top: this.bg_pos.y, }); }, save: function () { this.clean(); var bg_img_size = this.modal.$('> :not(label):not(.o_hidden)').data('value') || 'cover'; switch (bg_img_size) { case "cover": this.$target.css('background-position', this.bg_pos.x + ' ' + this.bg_pos.y); break; case "contain": this.$target.addClass('o_bg_img_opt_contain'); this.$target.toggleClass('o_bg_img_opt_repeat', this.modal.$("#o_bg_img_opt_contain_repeat").prop("checked")); break; case "custom": this.$target.addClass('o_bg_img_opt_custom'); var sizeX = this.modal.$("#o_bg_img_opt_custom_size_x").val(); var sizeY = this.modal.$("#o_bg_img_opt_custom_size_y").val(); var posX = this.modal.$("#o_bg_img_opt_custom_pos_x").val(); var posY = this.modal.$("#o_bg_img_opt_custom_pos_y").val(); this.$target.addClass(this.modal.$("#o_bg_img_opt_custom_repeat").val()) .css({ 'background-size': ((sizeX)? sizeX + '%' : 'auto') + " " + ((sizeY)? sizeY + '%' : 'auto'), 'background-position': ((posX)? posX + '%' : 'auto') + " " + ((posY)? posY + '%' : 'auto'), }); break; } }, discard: function () { this.clean(); if (this.previous_state) { this.$target.addClass(this.previous_state[0]).css({ 'background-size': this.previous_state[1], 'background-position': this.previous_state[2], }); } }, clean: function () { this.$target.removeClass('o_bg_img_opt_contain o_bg_img_opt_custom o_bg_img_opt_repeat o_bg_img_opt_repeat_x o_bg_img_opt_repeat_y') .css({ 'background-size': '', 'background-position': '', }); }, }); /* t-field options */ registry.many2one = SnippetOption.extend({ start: function () { var self = this; this.Model = this.$target.data('oe-many2one-model'); this.ID = +this.$target.data('oe-many2one-id'); // create search button and bind search bar this.$btn = $(qweb.render("web_editor.many2one.button")) .insertAfter(this.$overlay.find('.oe_options')); this.$ul = this.$btn.find("ul"); this.$search = this.$ul.find('li:first'); this.$search.find('input').on('mousedown click mouseup keyup keydown', function (e) { e.stopPropagation(); }); // move menu item setTimeout(function () { if (self.$overlay.find('.oe_options').hasClass('hidden')) { self.$btn.css('height', '0').find('> a').addClass('hidden'); self.$ul.show().css({ 'top': '-24px', 'margin': '0', 'padding': '2px 0', 'position': 'relative' }); } else { self.$btn.find('a').on('click', function (e) { self.clear(); }); } },0); // bind search input this.$search.find('input') .focus() .on('keyup', function (e) { self.find_existing($(this).val()); }); // bind result this.$ul.on('click', "li:not(:first) a", function (e) { self.select_record(this); }); }, on_focus: function () { this.$target.attr('contentEditable', 'false'); this.clear(); this._super.apply(this, arguments); }, clear: function () { var self = this; this.$search.siblings().remove(); self.$search.find('input').val(""); setTimeout(function () { self.$search.find('input').focus(); },0); }, find_existing: function (name) { var self = this; var domain = []; if (!name || !name.length) { self.$search.siblings().remove(); return; } if (isNaN(+name)) { if (this.Model !== "res.partner") { domain.push(['name', 'ilike', name]); } else { domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]); } } else { domain.push(['id', '=', name]); } ajax.jsonRpc('/web/dataset/call_kw', 'call', { model: this.Model, method: 'search_read', args: [domain, this.Model === "res.partner" ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']], kwargs: { order: 'name DESC', limit: 5, context: base.get_context(), } }).then(function (result) { self.$search.siblings().remove(); self.$search.after(qweb.render("web_editor.many2one.search",{contacts:result})); }); }, get_contact_rendering: function (options) { return ajax.jsonRpc('/web/dataset/call_kw', 'call', { model: 'ir.qweb.field.contact', method: 'get_record_to_html', args: [[this.ID]], kwargs: { options: options, context: base.get_context(), } }); }, select_record: function (li) { var self = this; this.ID = +$(li).data("id"); this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID); this.buildingBlock.getParent().rte.historyRecordUndo(this.$target); this.$target.trigger('content_changed'); if (self.$target.data('oe-type') === "contact") { $('[data-oe-contact-options]') .filter('[data-oe-model="'+self.$target.data('oe-model')+'"]') .filter('[data-oe-id="'+self.$target.data('oe-id')+'"]') .filter('[data-oe-field="'+self.$target.data('oe-field')+'"]') .filter('[data-oe-contact-options!="'+self.$target.data('oe-contact-options')+'"]') .add(self.$target) .attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID) .each(function () { var $node = $(this); self.get_contact_rendering($node.data('oe-contact-options')) .then(function (html) { $node.html(html); }); }); } else { self.$target.html($(li).data("name")); } setTimeout(function () { self.buildingBlock.make_active(false); },0); } }); return { Class: SnippetOption, registry: registry, }; });