odoo.define('website.snippets.options', function (require) { 'use strict'; var core = require('web.core'); var Dialog = require('web.Dialog'); var widgets = require('web_editor.widget'); var options = require('web_editor.snippets.options'); var _t = core._t; var qweb = core.qweb; options.registry.menu_data = options.Class.extend({ xmlDependencies: ['/website/static/src/xml/website.editor.xml'], /** * When the users selects a menu, a dialog is opened to ask him if he wants * to follow the link (and leave editor), edit the menu or do nothing. * * @override */ onFocus: function () { var self = this; (new Dialog(this, { title: _t("Confirmation"), $content: $(core.qweb.render('website.leaving_current_page_edition')), buttons: [ {text: _t("Go to Link"), classes: 'btn-primary', click: function () { self.trigger_up('request_save', { reload: false, onSuccess: function () { window.location.href = self.$target.attr('href'); }, }); }}, {text: _t("Edit the menu"), classes: 'btn-primary', click: function () { this.trigger_up('action_demand', { actionName: 'edit_menu', params: [ function () { var def = $.Deferred(); self.trigger_up('request_save', { onSuccess: def.resolve.bind(def), onFailure: def.reject.bind(def), }); return def; }, ], }); }}, {text: _t("Stay on this page"), close: true} ] })).open(); }, }); options.registry.company_data = options.Class.extend({ /** * Fetches data to determine the URL where the user can edit its company * data. Saves the info in the prototype to do this only once. * * @override */ start: function () { var proto = options.registry.company_data.prototype; var def; var self = this; if (proto.__link === undefined) { def = this._rpc({route: '/web/session/get_session_info'}).then(function (session) { return self._rpc({ model: 'res.users', method: 'read', args: [session.uid, ['company_id']], }); }).then(function (res) { proto.__link = '/web#action=base.action_res_company_form&view_type=form&id=' + (res && res[0] && res[0].company_id[0] || 1); }); } return $.when(this._super.apply(this, arguments), def); }, /** * When the users selects company data, opens a dialog to ask him if he * wants to be redirected to the company form view to edit it. * * @override */ onFocus: function () { var self = this; var proto = options.registry.company_data.prototype; Dialog.confirm(this, _t("Do you want to edit the company data ?"), { confirm_callback: function () { self.trigger_up('request_save', { reload: false, onSuccess: function () { window.location.href = proto.__link; }, }); }, }); }, }); /** * @todo should be refactored / reviewed */ options.registry.carousel = options.Class.extend({ /** * @override */ start: function () { var self = this; this.$target.carousel({interval: false}); this.id = this.$target.attr('id'); this.$inner = this.$target.find('.carousel-inner'); this.$indicators = this.$target.find('.carousel-indicators'); this.$target.carousel('pause'); this._rebindEvents(); var def = this._super.apply(this, arguments); // set background and prepare to clean for save this.$target.on('slid.bs.carousel', function () { self.$target.carousel('pause'); self.trigger_up('option_update', { optionNames: ['background', 'background_position', 'colorpicker'], name: 'target', data: self.$target.find('.item.active'), }); }); this.$target.trigger('slid.bs.carousel'); return def; }, /** * Associates unique ID on slider elements. * * @override */ onBuilt: function () { this.id = 'myCarousel' + new Date().getTime(); this.$target.attr('id', this.id); this.$target.find('[data-slide]').attr('data-cke-saved-href', '#' + this.id); this.$target.find('[data-target]').attr('data-target', '#' + this.id); this._rebindEvents(); }, /** * Associates unique ID on cloned slider elements. * * @override */ onClone: function () { var id = 'myCarousel' + new Date().getTime(); this.$target.attr('id', id); this.$target.find('[data-slide]').attr('href', '#' + id); this.$target.find('[data-slide-to]').attr('data-target', '#' + id); }, /** * @override */ cleanForSave: function () { this._super.apply(this, arguments); this.$target.find('.item').removeClass('next prev left right active') .first().addClass('active'); this.$target.find('.carousel-indicators').find('li').removeClass('active') .first().addClass('active'); this.$target.removeClass('oe_img_bg ' + this._class).css('background-image', ''); }, //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- /** * Adds a slide. * * @see this.selectClass for parameters */ addSlide: function (previewMode) { var self = this; var cycle = this.$inner.find('.item').length; var $active = this.$inner.find('.item.active, .item.prev, .item.next').first(); var index = $active.index(); this.$('.carousel-control, .carousel-indicators').removeClass('hidden'); this.$indicators.append('
'); var $clone = this.$('.item:first').clone(true); $clone.removeClass('active').insertAfter($active); _.defer(function () { self.$target.carousel().carousel(++index); self._rebindEvents(); }); }, /** * Removes the current slide. * * @see this.selectClass for parameters. */ removeSlide: function (previewMode) { if (this.remove_process) { return; } var self = this; var $items = this.$inner.find('.item'); var cycle = $items.length - 1; var $active = $items.filter('.active'); var index = $active.index(); if (cycle > 0) { this.remove_process = true; this.$target.on('slid.bs.carousel.slide_removal', function (event) { $active.remove(); self.$indicators.find('li:last').remove(); self.$target.off('slid.bs.carousel.slide_removal'); self._rebindEvents(); self.remove_process = false; if (cycle === 1) { self.$target.find('.carousel-control, .carousel-indicators').addClass('hidden'); } }); _.defer(function () { self.$target.carousel(index > 0 ? --index : cycle); }); } }, /** * Changes the interval for autoplay. * * @see this.selectClass for parameters */ interval: function (previewMode, value) { this.$target.attr('data-interval', value); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override */ _setActive: function () { this.$el.find('li[data-interval]').removeClass('active') .filter('li[data-interval=' + this.$target.attr('data-interval') + ']').addClass('active'); }, /** * Rebinds carousel events on indicators. * * @private */ _rebindEvents: function () { var self = this; this.$target.find('.carousel-control').off('click').on('click', function () { self.$target.carousel($(this).data('slide')); }); this.$target.find('.carousel-indicators [data-slide-to]').off('click').on('click', function () { self.$target.carousel(+$(this).data('slide-to')); }); /* Fix: backward compatibility saas-3 */ this.$target.find('.item.text_image, .item.image_text, .item.text_only').find('.container > .carousel-caption > div, .container > img.carousel-image').attr('contentEditable', 'true'); }, }); options.registry['margin-x'] = options.registry.marginAndResize.extend({ preventChildPropagation: true, /** * @override */ onClone: function (options) { this._super.apply(this, arguments); // Below condition is added to remove offset of target element only // and not its children to avoid design alteration of a container/block. if (options.isCurrent) { var _class = this.$target.attr('class').replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, ''); this.$target.attr('class', _class); } }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * @override */ _getSize: function () { this.grid = this._super(); var width = this.$target.parents('.row:first').first().outerWidth(); var grid = [1,2,3,4,5,6,7,8,9,10,11,12]; this.grid.e = [_.map(grid, function (v) {return 'col-md-'+v;}), _.map(grid, function (v) {return width/12*v;})]; grid = [-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11]; this.grid.w = [_.map(grid, function (v) {return 'col-md-offset-'+v;}), _.map(grid, function (v) {return width/12*v;}), 12]; return this.grid; }, /** * @override */ _onResize: function (compass, beginClass, current) { if (compass === 'w') { // don't change the right border position when we change the offset (replace col size) var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0); var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0); var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0); if (offset < 0) { offset = 0; } var colSize = beginCol - (offset - beginOffset); if (colSize <= 0) { colSize = 1; offset = beginOffset + beginCol - 1; } this.$target.attr('class',this.$target.attr('class').replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, '')); this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize)); if (offset > 0) { this.$target.addClass('col-md-offset-' + offset); } } this._super(compass, beginClass, current); }, }); options.registry.parallax = options.Class.extend({ /** * @override */ start: function () { this._refresh_callback = this._refresh.bind(this); this.$target.on('snippet-option-change snippet-option-preview', this._refresh_callback); return this._super.apply(this, arguments); }, /** * @override */ onFocus: function () { this.trigger_up('option_update', { optionNames: ['background', 'background_position'], name: 'target', data: this.$target.find('> .s_parallax_bg'), }); this._refresh(); }, /** * @override */ onMove: function () { this._refresh(); }, //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- /** * Changes the scrolling speed of the parallax effect. * * @see this.selectClass for parameters */ scroll: function (previewMode, value) { this.$target.attr('data-scroll-background-ratio', value); this._refresh(); }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Notifies the parallax animation that it needs to be reinitialized. * * @private */ _refresh: function () { var self = this; _.defer(function () { self.$target.data('snippet-view')._rebuild(); }); }, /** * @override */ _setActive: function () { this._super.apply(this, arguments); this.$el.find('[data-scroll]').removeClass('active') .filter('[data-scroll="' + (this.$target.attr('data-scroll-background-ratio') || 0) + '"]').addClass('active'); }, }); options.registry.ul = options.Class.extend({ /** * @override */ start: function () { var self = this; this.$target.on('mouseup', '.o_ul_toggle_self, .o_ul_toggle_next', function () { self.trigger_up('cover_update'); }); return this._super.apply(this, arguments); }, /** * @override */ cleanForSave: function () { this._super(); if (!this.$target.hasClass('o_ul_folded')) { this.$target.find('.o_close').removeClass('o_close'); } this.$target.find('li:not(:has(>ul))').css('list-style', ''); }, //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- /** * @override */ toggleClass: function () { this._super.apply(this, arguments); this.$target.data('snippet-view').destroy(); this.$target.find('.o_ul_toggle_self, .o_ul_toggle_next').remove(); this.$target.find('li:has(>ul,>ol)').map(function () { // get if the li contain a text label var texts = _.filter(_.toArray(this.childNodes), function (a) { return a.nodeType === 3;}); if (!texts.length || !texts.reduce(function (a,b) { return a.textContent + b.textContent;}).match(/\S/)) { return; } $(this).children('ul,ol').addClass('o_close'); return $(this).children(':not(ul,ol)')[0] || this; }) .prepend(''); var $li = this.$target.find('li:has(+li:not(>.o_ul_toggle_self)>ul, +li:not(>.o_ul_toggle_self)>ol)'); $li.map(function () { return $(this).children()[0] || this; }) .prepend(''); $li.removeClass('o_open').next().addClass('o_close'); this.$target.find('li').removeClass('o_open').css('list-style', ''); this.$target.find('li:has(.o_ul_toggle_self, .o_ul_toggle_next), li:has(>ul,>ol):not(:has(>li))').css('list-style', 'none'); this.$target.find('li:not(:has(>ul))').css('list-style', ''); this.trigger_up('animation_start_demand', { editableMode: true, $target: this.$target, }); }, }); options.registry.collapse = options.Class.extend({ /** * @override */ start: function () { var self = this; this.$target.on('shown.bs.collapse hidden.bs.collapse', '[role="tabpanel"]', function () { self.trigger_up('cover_update'); }); return this._super.apply(this, arguments); }, /** * @override */ onBuilt: function () { this._createIDs(); }, /** * @override */ onClone: function () { this.$target.find('[data-toggle="collapse"]').removeAttr('data-target').removeData('target'); this.$target.find('.panel-collapse').removeAttr('id'); this._createIDs(); }, /** * @override */ onMove: function () { this._createIDs(); var $panel = this.$target.find('.panel-collapse').removeData('bs.collapse'); if ($panel.attr('aria-expanded') === 'true') { $panel.closest('.panel-group').find('.panel-collapse[aria-expanded="true"]') .filter(function () {return this !== $panel[0];}) .collapse('hide') .one('hidden.bs.collapse', function () { $panel.trigger('shown.bs.collapse'); }); } }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Associates unique ids on collapse elements. * * @private */ _createIDs: function () { var time = new Date().getTime(); var $tab = this.$target.find('[data-toggle="collapse"]'); // link to the parent group var $tablist = this.$target.closest('.panel-group'); var tablist_id = $tablist.attr('id'); if (!tablist_id) { tablist_id = 'myCollapse' + time; $tablist.attr('id', tablist_id); } $tab.attr('data-parent', '#'+tablist_id); $tab.data('parent', '#'+tablist_id); // link to the collapse var $panel = this.$target.find('.panel-collapse'); var panel_id = $panel.attr('id'); if (!panel_id) { while ($('#'+(panel_id = 'myCollapseTab' + time)).length) { time++; } $panel.attr('id', panel_id); } $tab.attr('data-target', '#'+panel_id); $tab.data('target', '#'+panel_id); }, }); options.registry.gallery = options.Class.extend({ xmlDependencies: ['/website/static/src/xml/website.gallery.xml'], /** * @override */ start: function () { var self = this; // The snippet should not be editable this.$target.attr('contentEditable', false); // Make sure image previews are updated if images are changed this.$target.on('save', 'img', function (ev) { var $img = $(ev.currentTarget); var index = self.$target.find('.item.active').index(); self.$('.carousel:first li[data-target]:eq(' + index + ')') .css('background-image', 'url(' + $img.attr('src') + ')'); }); // When the snippet is empty, an edition button is the default content // TODO find a nicer way to do that to have editor style this.$target.on('click', '.o_add_images', function (e) { e.stopImmediatePropagation(); self.addImages(false); }); return this._super.apply(this, arguments); }, /** * @override */ onBuilt: function () { var uuid = new Date().getTime(); this.$target.find('.carousel').attr('id', 'slideshow_' + uuid); this.$target.find('[data-target]').attr('data-target', '#slideshow_' + uuid); }, /** * @override */ cleanForSave: function () { if (this.$target.hasClass('slideshow')) { this.$target.removeAttr('style'); } }, //-------------------------------------------------------------------------- // Options //-------------------------------------------------------------------------- /** * Allows to select images to add as part of the snippet. * * @see this.selectClass for parameters */ addImages: function (previewMode) { var self = this; var $container = this.$('.container:first'); var dialog = new widgets.MediaDialog(this, {select_images: true}, this.$target.closest('.o_editable'), null); var lastImage = _.last(this._getImages()); var index = lastImage ? this._getIndex(lastImage) : -1; dialog.on('save', this, function (attachments) { for (var i = 0 ; i < attachments.length; i++) { $('