diff options
Diffstat (limited to 'addons/website/static/src/js')
| -rw-r--r-- | addons/website/static/src/js/content/snippets.animation.js | 1668 | ||||
| -rw-r--r-- | addons/website/static/src/js/editor/snippets.options.js | 2528 |
2 files changed, 0 insertions, 4196 deletions
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js deleted file mode 100644 index f9883534bd5..00000000000 --- a/addons/website/static/src/js/content/snippets.animation.js +++ /dev/null @@ -1,1668 +0,0 @@ -odoo.define('website.content.snippets.animation', function (require) { -'use strict'; - -/** - * Provides a way to start JS code for snippets' initialization and animations. - */ - -var Class = require('web.Class'); -const ColorpickerDialog = require('web.ColorpickerDialog'); -var config = require('web.config'); -var core = require('web.core'); -var mixins = require('web.mixins'); -var publicWidget = require('web.public.widget'); -var utils = require('web.utils'); - -var qweb = core.qweb; -const _t = core._t; - -// Initialize fallbacks for the use of requestAnimationFrame, -// cancelAnimationFrame and performance.now() -window.requestAnimationFrame = window.requestAnimationFrame - || window.webkitRequestAnimationFrame - || window.mozRequestAnimationFrame - || window.msRequestAnimationFrame - || window.oRequestAnimationFrame; -window.cancelAnimationFrame = window.cancelAnimationFrame - || window.webkitCancelAnimationFrame - || window.mozCancelAnimationFrame - || window.msCancelAnimationFrame - || window.oCancelAnimationFrame; -if (!window.performance || !window.performance.now) { - window.performance = { - now: function () { - return Date.now(); - } - }; -} - -/** - * Add the notion of edit mode to public widgets. - */ -publicWidget.Widget.include({ - /** - * Indicates if the widget should not be instantiated in edit. The default - * is true, indeed most (all?) defined widgets only want to initialize - * events and states which should not be active in edit mode (this is - * especially true for non-website widgets). - * - * @type {boolean} - */ - disabledInEditableMode: true, - /** - * Acts as @see Widget.events except that the events are only binded if the - * Widget instance is instanciated in edit mode. The property is not - * considered if @see disabledInEditableMode is false. - */ - edit_events: null, - /** - * Acts as @see Widget.events except that the events are only binded if the - * Widget instance is instanciated in readonly mode. The property only - * makes sense if @see disabledInEditableMode is false, you should simply - * use @see Widget.events otherwise. - */ - read_events: null, - - /** - * Initializes the events that will need to be binded according to the - * given mode. - * - * @constructor - * @param {Object} parent - * @param {Object} [options] - * @param {boolean} [options.editableMode=false] - * true if the page is in edition mode - */ - init: function (parent, options) { - this._super.apply(this, arguments); - - this.editableMode = this.options.editableMode || false; - var extraEvents = this.editableMode ? this.edit_events : this.read_events; - if (extraEvents) { - this.events = _.extend({}, this.events || {}, extraEvents); - } - }, -}); - -/** - * In charge of handling one animation loop using the requestAnimationFrame - * feature. This is used by the `Animation` class below and should not be called - * directly by an end developer. - * - * This uses a simple API: it can be started, stopped, played and paused. - */ -var AnimationEffect = Class.extend(mixins.ParentedMixin, { - /** - * @constructor - * @param {Object} parent - * @param {function} updateCallback - the animation update callback - * @param {string} [startEvents=scroll] - * space separated list of events which starts the animation loop - * @param {jQuery|DOMElement} [$startTarget=window] - * the element(s) on which the startEvents are listened - * @param {Object} [options] - * @param {function} [options.getStateCallback] - * a function which returns a value which represents the state of the - * animation, i.e. for two same value, no refreshing of the animation - * is needed. Can be used for optimization. If the $startTarget is - * the window element, this defaults to returning the current - * scoll offset of the window or the size of the window for the - * scroll and resize events respectively. - * @param {string} [options.endEvents] - * space separated list of events which pause the animation loop. If - * not given, the animation is stopped after a while (if no - * startEvents is received again) - * @param {jQuery|DOMElement} [options.$endTarget=$startTarget] - * the element(s) on which the endEvents are listened - */ - init: function (parent, updateCallback, startEvents, $startTarget, options) { - mixins.ParentedMixin.init.call(this); - this.setParent(parent); - - options = options || {}; - this._minFrameTime = 1000 / (options.maxFPS || 100); - - // Initialize the animation startEvents, startTarget, endEvents, endTarget and callbacks - this._updateCallback = updateCallback; - this.startEvents = startEvents || 'scroll'; - this.$startTarget = $($startTarget || window); - if (options.getStateCallback) { - this._getStateCallback = options.getStateCallback; - } else if (this.startEvents === 'scroll' && this.$startTarget[0] === window) { - this._getStateCallback = function () { - return window.pageYOffset; - }; - } else if (this.startEvents === 'resize' && this.$startTarget[0] === window) { - this._getStateCallback = function () { - return { - width: window.innerWidth, - height: window.innerHeight, - }; - }; - } else { - this._getStateCallback = function () { - return undefined; - }; - } - this.endEvents = options.endEvents || false; - this.$endTarget = options.$endTarget ? $(options.$endTarget) : this.$startTarget; - - this._updateCallback = this._updateCallback.bind(parent); - this._getStateCallback = this._getStateCallback.bind(parent); - - // Add a namespace to events using the generated uid - this._uid = '_animationEffect' + _.uniqueId(); - this.startEvents = _processEvents(this.startEvents, this._uid); - if (this.endEvents) { - this.endEvents = _processEvents(this.endEvents, this._uid); - } - - function _processEvents(events, namespace) { - events = events.split(' '); - return _.each(events, function (e, index) { - events[index] += ('.' + namespace); - }).join(' '); - } - }, - /** - * @override - */ - destroy: function () { - mixins.ParentedMixin.destroy.call(this); - this.stop(); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Initializes when the animation must be played and paused and initializes - * the animation first frame. - */ - start: function () { - // Initialize the animation first frame - this._paused = false; - this._rafID = window.requestAnimationFrame((function (t) { - this._update(t); - this._paused = true; - }).bind(this)); - - // Initialize the animation play/pause events - if (this.endEvents) { - /** - * If there are endEvents, the animation should begin playing when - * the startEvents are triggered on the $startTarget and pause when - * the endEvents are triggered on the $endTarget. - */ - this.$startTarget.on(this.startEvents, (function (e) { - if (this._paused) { - _.defer(this.play.bind(this, e)); - } - }).bind(this)); - this.$endTarget.on(this.endEvents, (function () { - if (!this._paused) { - _.defer(this.pause.bind(this)); - } - }).bind(this)); - } else { - /** - * Else, if there is no endEvents, the animation should begin playing - * when the startEvents are *continuously* triggered on the - * $startTarget or fully played once. To achieve this, the animation - * begins playing and is scheduled to pause after 2 seconds. If the - * startEvents are triggered during that time, this is not paused - * for another 2 seconds. This allows to describe an "effect" - * animation (which lasts less than 2 seconds) or an animation which - * must be playing *during* an event (scroll, mousemove, resize, - * repeated clicks, ...). - */ - var pauseTimer = null; - this.$startTarget.on(this.startEvents, _.throttle((function (e) { - this.play(e); - - clearTimeout(pauseTimer); - pauseTimer = _.delay((function () { - this.pause(); - pauseTimer = null; - }).bind(this), 2000); - }).bind(this), 250, {trailing: false})); - } - }, - /** - * Pauses the animation and destroys the attached events which trigger the - * animation to be played or paused. - */ - stop: function () { - this.$startTarget.off(this.startEvents); - if (this.endEvents) { - this.$endTarget.off(this.endEvents); - } - this.pause(); - }, - /** - * Forces the requestAnimationFrame loop to start. - * - * @param {Event} e - the event which triggered the animation to play - */ - play: function (e) { - this._newEvent = e; - if (!this._paused) { - return; - } - this._paused = false; - this._rafID = window.requestAnimationFrame(this._update.bind(this)); - this._lastUpdateTimestamp = undefined; - }, - /** - * Forces the requestAnimationFrame loop to stop. - */ - pause: function () { - if (this._paused) { - return; - } - this._paused = true; - window.cancelAnimationFrame(this._rafID); - this._lastUpdateTimestamp = undefined; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Callback which is repeatedly called by the requestAnimationFrame loop. - * It controls the max fps at which the animation is running and initializes - * the values that the update callback needs to describe the animation - * (state, elapsedTime, triggered event). - * - * @private - * @param {DOMHighResTimeStamp} timestamp - */ - _update: function (timestamp) { - if (this._paused) { - return; - } - this._rafID = window.requestAnimationFrame(this._update.bind(this)); - - // Check the elapsed time since the last update callback call. - // Consider it 0 if there is no info of last timestamp and leave this - // _update call if it was called too soon (would overflow the set max FPS). - var elapsedTime = 0; - if (this._lastUpdateTimestamp) { - elapsedTime = timestamp - this._lastUpdateTimestamp; - if (elapsedTime < this._minFrameTime) { - return; - } - } - - // Check the new animation state thanks to the get state callback and - // store its new value. If the state is the same as the previous one, - // leave this _update call, except if there is an event which triggered - // the "play" method again. - var animationState = this._getStateCallback(elapsedTime, this._newEvent); - if (!this._newEvent - && animationState !== undefined - && _.isEqual(animationState, this._animationLastState)) { - return; - } - this._animationLastState = animationState; - - // Call the update callback with frame parameters - this._updateCallback(this._animationLastState, elapsedTime, this._newEvent); - this._lastUpdateTimestamp = timestamp; // Save the timestamp at which the update callback was really called - this._newEvent = undefined; // Forget the event which triggered the last "play" call - }, -}); - -/** - * Also register AnimationEffect automatically (@see effects, _prepareEffects). - */ -var Animation = publicWidget.Widget.extend({ - /** - * The max FPS at which all the automatic animation effects will be - * running by default. - */ - maxFPS: 100, - /** - * @see this._prepareEffects - * - * @type {Object[]} - * @type {string} startEvents - * The names of the events which trigger the effect to begin playing. - * @type {string} [startTarget] - * A selector to find the target where to listen for the start events - * (if no selector, the window target will be used). If the whole - * $target of the animation should be used, use the 'selector' string. - * @type {string} [endEvents] - * The name of the events which trigger the end of the effect (if none - * is defined, the animation will stop after a while - * @see AnimationEffect.start). - * @type {string} [endTarget] - * A selector to find the target where to listen for the end events - * (if no selector, the startTarget will be used). If the whole - * $target of the animation should be used, use the 'selector' string. - * @type {string} update - * A string which refers to a method which will be used as the update - * callback for the effect. It receives 3 arguments: the animation - * state, the elapsedTime since last update and the event which - * triggered the animation (undefined if just a new update call - * without trigger). - * @type {string} [getState] - * The animation state is undefined by default, the scroll offset for - * the particular {startEvents: 'scroll'} effect and an object with - * width and height for the particular {startEvents: 'resize'} effect. - * There is the possibility to define the getState callback of the - * animation effect with this key. This allows to improve performance - * even further in some cases. - */ - effects: [], - - /** - * Initializes the animation. The method should not be called directly as - * called automatically on animation instantiation and on restart. - * - * Also, prepares animation's effects and start them if any. - * - * @override - */ - start: function () { - this._prepareEffects(); - _.each(this._animationEffects, function (effect) { - effect.start(); - }); - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Registers `AnimationEffect` instances. - * - * This can be done by extending this method and calling the @see _addEffect - * method in it or, better, by filling the @see effects property. - * - * @private - */ - _prepareEffects: function () { - this._animationEffects = []; - - var self = this; - _.each(this.effects, function (desc) { - self._addEffect(self[desc.update], desc.startEvents, _findTarget(desc.startTarget), { - getStateCallback: desc.getState && self[desc.getState], - endEvents: desc.endEvents || undefined, - $endTarget: _findTarget(desc.endTarget), - maxFPS: self.maxFPS, - }); - - // Return the DOM element matching the selector in the form - // described above. - function _findTarget(selector) { - if (selector) { - if (selector === 'selector') { - return self.$target; - } - return self.$(selector); - } - return undefined; - } - }); - }, - /** - * Registers a new `AnimationEffect` according to given parameters. - * - * @private - * @see AnimationEffect.init - */ - _addEffect: function (updateCallback, startEvents, $startTarget, options) { - this._animationEffects.push( - new AnimationEffect(this, updateCallback, startEvents, $startTarget, options) - ); - }, -}); - -//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: - -var registry = publicWidget.registry; - -registry.slider = publicWidget.Widget.extend({ - selector: '.carousel', - disabledInEditableMode: false, - edit_events: { - 'content_changed': '_onContentChanged', - }, - - /** - * @override - */ - start: function () { - this.$('img').on('load.slider', () => this._computeHeights()); - this._computeHeights(); - // Initialize carousel and pause if in edit mode. - this.$target.carousel(this.editableMode ? 'pause' : undefined); - return this._super.apply(this, arguments); - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - this.$('img').off('.slider'); - this.$target.carousel('pause'); - this.$target.removeData('bs.carousel'); - _.each(this.$('.carousel-item'), function (el) { - $(el).css('min-height', ''); - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - */ - _computeHeights: function () { - var maxHeight = 0; - var $items = this.$('.carousel-item'); - $items.css('min-height', ''); - _.each($items, function (el) { - var $item = $(el); - var isActive = $item.hasClass('active'); - $item.addClass('active'); - var height = $item.outerHeight(); - if (height > maxHeight) { - maxHeight = height; - } - $item.toggleClass('active', isActive); - }); - $items.css('min-height', maxHeight); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onContentChanged: function (ev) { - this._computeHeights(); - }, -}); - -registry.parallax = Animation.extend({ - selector: '.parallax', - disabledInEditableMode: false, - effects: [{ - startEvents: 'scroll', - update: '_onWindowScroll', - }], - - /** - * @override - */ - start: function () { - this._rebuild(); - $(window).on('resize.animation_parallax', _.debounce(this._rebuild.bind(this), 500)); - return this._super.apply(this, arguments); - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - $(window).off('.animation_parallax'); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Prepares the background element which will scroll at a different speed - * according to the viewport dimensions and other snippet parameters. - * - * @private - */ - _rebuild: function () { - // Add/find bg DOM element to hold the parallax bg (support old v10.0 parallax) - if (!this.$bg || !this.$bg.length) { - this.$bg = this.$('> .s_parallax_bg'); - if (!this.$bg.length) { - this.$bg = $('<span/>', { - class: 's_parallax_bg' + (this.$target.hasClass('oe_custom_bg') ? ' oe_custom_bg' : ''), - }).prependTo(this.$target); - } - } - var urlTarget = this.$target.css('background-image'); - if (urlTarget !== 'none') { - this.$bg.css('background-image', urlTarget); - } - this.$target.css('background-image', 'none'); - - // Get parallax speed - this.speed = parseFloat(this.$target.attr('data-scroll-background-ratio') || 0); - - // Reset offset if parallax effect will not be performed and leave - this.$target.toggleClass('s_parallax_is_fixed', this.speed === 1); - var noParallaxSpeed = (this.speed === 0 || this.speed === 1); - this.$target.toggleClass('s_parallax_no_overflow_hidden', noParallaxSpeed); - if (noParallaxSpeed) { - this.$bg.css({ - transform: '', - top: '', - bottom: '', - }); - return; - } - - // Initialize parallax data according to snippet and viewport dimensions - this.viewport = document.body.clientHeight - $('#wrapwrap').position().top; - this.visibleArea = [this.$target.offset().top]; - this.visibleArea.push(this.visibleArea[0] + this.$target.innerHeight() + this.viewport); - this.ratio = this.speed * (this.viewport / 10); - - // Provide a "safe-area" to limit parallax - this.$bg.css({ - top: -this.ratio, - bottom: -this.ratio, - }); - }, - - //-------------------------------------------------------------------------- - // Effects - //-------------------------------------------------------------------------- - - /** - * Describes how to update the snippet when the window scrolls. - * - * @private - * @param {integer} scrollOffset - */ - _onWindowScroll: function (scrollOffset) { - // Speed == 0 is no effect and speed == 1 is handled by CSS only - if (this.speed === 0 || this.speed === 1) { - return; - } - - // Perform translation if the element is visible only - var vpEndOffset = scrollOffset + this.viewport; - if (vpEndOffset >= this.visibleArea[0] - && vpEndOffset <= this.visibleArea[1]) { - this.$bg.css('transform', 'translateY(' + _getNormalizedPosition.call(this, vpEndOffset) + 'px)'); - } - - function _getNormalizedPosition(pos) { - // Normalize scroll in a 1 to 0 range - var r = (pos - this.visibleArea[1]) / (this.visibleArea[0] - this.visibleArea[1]); - // Normalize accordingly to current options - return Math.round(this.ratio * (2 * r - 1)); - } - }, -}); - -registry.mediaVideo = publicWidget.Widget.extend({ - selector: '.media_iframe_video', - - /** - * @override - */ - start: function () { - // TODO: this code should be refactored to make more sense and be better - // integrated with Odoo (this refactoring should be done in master). - - var def = this._super.apply(this, arguments); - if (this.$target.children('iframe').length) { - // There already is an <iframe/>, do nothing - return def; - } - - // Bug fix / compatibility: empty the <div/> element as all information - // to rebuild the iframe should have been saved on the <div/> element - this.$target.empty(); - - // Add extra content for size / edition - this.$target.append( - '<div class="css_editable_mode_display"> </div>' + - '<div class="media_iframe_video_size"> </div>' - ); - - // Rebuild the iframe. Depending on version / compatibility / instance, - // the src is saved in the 'data-src' attribute or the - // 'data-oe-expression' one (the latter is used as a workaround in 10.0 - // system but should obviously be reviewed in master). - this.$target.append($('<iframe/>', { - src: _.escape(this.$target.data('oe-expression') || this.$target.data('src')), - frameborder: '0', - allowfullscreen: 'allowfullscreen', - sandbox: 'allow-scripts allow-same-origin', // https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/ - })); - - return def; - }, -}); - -registry.backgroundVideo = publicWidget.Widget.extend({ - selector: '.o_background_video', - xmlDependencies: ['/website/static/src/xml/website.background.video.xml'], - disabledInEditableMode: false, - - /** - * @override - */ - start: function () { - var proms = [this._super(...arguments)]; - - this.videoSrc = this.el.dataset.bgVideoSrc; - this.iframeID = _.uniqueId('o_bg_video_iframe_'); - - this.isYoutubeVideo = this.videoSrc.indexOf('youtube') >= 0; - this.isMobileEnv = config.device.size_class <= config.device.SIZES.LG && config.device.touch; - if (this.isYoutubeVideo && this.isMobileEnv) { - this.videoSrc = this.videoSrc + "&enablejsapi=1"; - - if (!window.YT) { - proms.push(new Promise(resolve => { - window.onYouTubeIframeAPIReady = () => resolve(); - })); - $('<script/>', { - src: 'https://www.youtube.com/iframe_api', - }).appendTo('head'); - } - } - - var throttledUpdate = _.throttle(() => this._adjustIframe(), 50); - - var $dropdownMenu = this.$el.closest('.dropdown-menu'); - if ($dropdownMenu.length) { - this.$dropdownParent = $dropdownMenu.parent(); - this.$dropdownParent.on('shown.bs.dropdown.backgroundVideo', throttledUpdate); - } - - $(window).on('resize.' + this.iframeID, throttledUpdate); - - return Promise.all(proms).then(() => this._appendBgVideo()); - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - - if (this.$dropdownParent) { - this.$dropdownParent.off('.backgroundVideo'); - } - - $(window).off('resize.' + this.iframeID); - - if (this.$bgVideoContainer) { - this.$bgVideoContainer.remove(); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Adjusts iframe sizes and position so that it fills the container and so - * that it is centered in it. - * - * @private - */ - _adjustIframe: function () { - this.$iframe.removeClass('show'); - - // Adjust the iframe - var wrapperWidth = this.$target.innerWidth(); - var wrapperHeight = this.$target.innerHeight(); - var relativeRatio = (wrapperWidth / wrapperHeight) / (16 / 9); - var style = {}; - if (relativeRatio >= 1.0) { - style['width'] = '100%'; - style['height'] = (relativeRatio * 100) + '%'; - style['left'] = '0'; - style['top'] = (-(relativeRatio - 1.0) / 2 * 100) + '%'; - } else { - style['width'] = ((1 / relativeRatio) * 100) + '%'; - style['height'] = '100%'; - style['left'] = (-((1 / relativeRatio) - 1.0) / 2 * 100) + '%'; - style['top'] = '0'; - } - this.$iframe.css(style); - - void this.$iframe[0].offsetWidth; // Force style addition - this.$iframe.addClass('show'); - }, - /** - * Append background video related elements to the target. - * - * @private - */ - _appendBgVideo: function () { - var $oldContainer = this.$bgVideoContainer || this.$('> .o_bg_video_container'); - this.$bgVideoContainer = $(qweb.render('website.background.video', { - videoSrc: this.videoSrc, - iframeID: this.iframeID, - })); - this.$iframe = this.$bgVideoContainer.find('.o_bg_video_iframe'); - this.$iframe.one('load', () => { - this.$bgVideoContainer.find('.o_bg_video_loading').remove(); - }); - this.$bgVideoContainer.prependTo(this.$target); - $oldContainer.remove(); - - this._adjustIframe(); - - // YouTube does not allow to auto-play video in mobile devices, so we - // have to play the video manually. - if (this.isMobileEnv && this.isYoutubeVideo) { - new window.YT.Player(this.iframeID, { - events: { - onReady: ev => ev.target.playVideo(), - } - }); - } - }, -}); - -registry.ul = publicWidget.Widget.extend({ - selector: 'ul.o_ul_folded, ol.o_ul_folded', - events: { - 'click .o_ul_toggle_next': '_onToggleNextClick', - 'click .o_ul_toggle_self': '_onToggleSelfClick', - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when a "toggle next" ul is clicked. - * - * @private - */ - _onToggleNextClick: function (ev) { - ev.preventDefault(); - var $target = $(ev.currentTarget); - $target.toggleClass('o_open'); - $target.closest('li').next().toggleClass('o_close'); - }, - /** - * Called when a "toggle self" ul is clicked. - * - * @private - */ - _onToggleSelfClick: function (ev) { - ev.preventDefault(); - var $target = $(ev.currentTarget); - $target.toggleClass('o_open'); - $target.closest('li').find('ul,ol').toggleClass('o_close'); - }, -}); - -registry.gallery = publicWidget.Widget.extend({ - selector: '.o_gallery:not(.o_slideshow)', - xmlDependencies: ['/website/static/src/xml/website.gallery.xml'], - events: { - 'click img': '_onClickImg', - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when an image is clicked. Opens a dialog to browse all the images - * with a bigger size. - * - * @private - * @param {Event} ev - */ - _onClickImg: function (ev) { - var self = this; - var $cur = $(ev.currentTarget); - - var urls = []; - var idx = undefined; - var milliseconds = undefined; - var params = undefined; - var $images = $cur.closest('.o_gallery').find('img'); - var size = 0.8; - var dimensions = { - min_width: Math.round(window.innerWidth * size * 0.9), - min_height: Math.round(window.innerHeight * size), - max_width: Math.round(window.innerWidth * size * 0.9), - max_height: Math.round(window.innerHeight * size), - width: Math.round(window.innerWidth * size * 0.9), - height: Math.round(window.innerHeight * size) - }; - - $images.each(function () { - urls.push($(this).attr('src')); - }); - var $img = ($cur.is('img') === true) ? $cur : $cur.closest('img'); - idx = urls.indexOf($img.attr('src')); - - milliseconds = $cur.closest('.o_gallery').data('interval') || false; - var $modal = $(qweb.render('website.gallery.slideshow.lightbox', { - srcs: urls, - index: idx, - dim: dimensions, - interval: milliseconds || 0, - id: _.uniqueId('slideshow_'), - })); - $modal.modal({ - keyboard: true, - backdrop: true, - }); - $modal.on('hidden.bs.modal', function () { - $(this).hide(); - $(this).siblings().filter('.modal-backdrop').remove(); // bootstrap leaves a modal-backdrop - $(this).remove(); - }); - $modal.find('.modal-content, .modal-body.o_slideshow').css('height', '100%'); - $modal.appendTo(document.body); - - $modal.one('shown.bs.modal', function () { - self.trigger_up('widgets_start_request', { - editableMode: false, - $target: $modal.find('.modal-body.o_slideshow'), - }); - }); - }, -}); - -registry.gallerySlider = publicWidget.Widget.extend({ - selector: '.o_slideshow', - xmlDependencies: ['/website/static/src/xml/website.gallery.xml'], - disabledInEditableMode: false, - - /** - * @override - */ - start: function () { - var self = this; - this.$carousel = this.$target.is('.carousel') ? this.$target : this.$target.find('.carousel'); - this.$indicator = this.$carousel.find('.carousel-indicators'); - this.$prev = this.$indicator.find('li.o_indicators_left').css('visibility', ''); // force visibility as some databases have it hidden - this.$next = this.$indicator.find('li.o_indicators_right').css('visibility', ''); - var $lis = this.$indicator.find('li[data-slide-to]'); - var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space - var realNbPerPage = nbPerPage || 1; - var nbPages = Math.ceil($lis.length / realNbPerPage); - - var index; - var page; - update(); - - function hide() { - $lis.each(function (i) { - $(this).toggleClass('d-none', i < page * nbPerPage || i >= (page + 1) * nbPerPage); - }); - if (self.editableMode) { // do not remove DOM in edit mode - return; - } - if (page <= 0) { - self.$prev.detach(); - } else { - self.$prev.prependTo(self.$indicator); - } - if (page >= nbPages - 1) { - self.$next.detach(); - } else { - self.$next.appendTo(self.$indicator); - } - } - - function update() { - index = $lis.index($lis.filter('.active')) || 0; - page = Math.floor(index / realNbPerPage); - hide(); - } - - this.$carousel.on('slide.bs.carousel.gallery_slider', function () { - setTimeout(function () { - var $item = self.$carousel.find('.carousel-inner .carousel-item-prev, .carousel-inner .carousel-item-next'); - var index = $item.index(); - $lis.removeClass('active') - .filter('[data-slide-to="' + index + '"]') - .addClass('active'); - }, 0); - }); - this.$indicator.on('click.gallery_slider', '> li:not([data-slide-to])', function () { - page += ($(this).hasClass('o_indicators_left') ? -1 : 1); - page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary - self.$carousel.carousel(page * realNbPerPage); - hide(); - }); - this.$carousel.on('slid.bs.carousel.gallery_slider', update); - - return this._super.apply(this, arguments); - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - - if (!this.$indicator) { - return; - } - - this.$prev.prependTo(this.$indicator); - this.$next.appendTo(this.$indicator); - this.$carousel.off('.gallery_slider'); - this.$indicator.off('.gallery_slider'); - }, -}); - -registry.socialShare = publicWidget.Widget.extend({ - selector: '.oe_social_share', - xmlDependencies: ['/website/static/src/xml/website.share.xml'], - events: { - 'mouseenter': '_onMouseEnter', - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - */ - _bindSocialEvent: function () { - this.$('.oe_social_facebook').click($.proxy(this._renderSocial, this, 'facebook')); - this.$('.oe_social_twitter').click($.proxy(this._renderSocial, this, 'twitter')); - this.$('.oe_social_linkedin').click($.proxy(this._renderSocial, this, 'linkedin')); - }, - /** - * @private - */ - _render: function () { - this.$el.popover({ - content: qweb.render('website.social_hover', {medias: this.socialList}), - placement: 'bottom', - container: this.$el, - html: true, - trigger: 'manual', - animation: false, - }).popover("show"); - - this.$el.off('mouseleave.socialShare').on('mouseleave.socialShare', function () { - var self = this; - setTimeout(function () { - if (!$(".popover:hover").length) { - $(self).popover('dispose'); - } - }, 200); - }); - }, - /** - * @private - */ - _renderSocial: function (social) { - var url = this.$el.data('urlshare') || document.URL.split(/[?#]/)[0]; - url = encodeURIComponent(url); - var title = document.title.split(" | ")[0]; // get the page title without the company name - var hashtags = ' #' + document.title.split(" | ")[1].replace(' ', '') + ' ' + this.hashtags; // company name without spaces (for hashtag) - var socialNetworks = { - 'facebook': 'https://www.facebook.com/sharer/sharer.php?u=' + url, - 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ') + url, - 'linkedin': 'https://www.linkedin.com/shareArticle?mini=true&url=' + url + '&title=' + encodeURIComponent(title), - }; - if (!_.contains(_.keys(socialNetworks), social)) { - return; - } - var wHeight = 500; - var wWidth = 500; - window.open(socialNetworks[social], '', 'menubar=no, toolbar=no, resizable=yes, scrollbar=yes, height=' + wHeight + ',width=' + wWidth); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the user hovers the animation element -> open the social - * links popover. - * - * @private - */ - _onMouseEnter: function () { - var social = this.$el.data('social'); - this.socialList = social ? social.split(',') : ['facebook', 'twitter', 'linkedin']; - this.hashtags = this.$el.data('hashtags') || ''; - - this._render(); - this._bindSocialEvent(); - }, -}); - -registry.facebookPage = publicWidget.Widget.extend({ - selector: '.o_facebook_page', - disabledInEditableMode: false, - - /** - * @override - */ - start: function () { - var def = this._super.apply(this, arguments); - - var params = _.pick(this.$el.data(), 'href', 'height', 'tabs', 'small_header', 'hide_cover', 'show_facepile'); - if (!params.href) { - return def; - } - params.width = utils.confine(Math.floor(this.$el.width()), 180, 500); - - var src = $.param.querystring('https://www.facebook.com/plugins/page.php', params); - this.$iframe = $('<iframe/>', { - src: src, - width: params.width, - height: params.height, - css: { - border: 'none', - overflow: 'hidden', - }, - scrolling: 'no', - frameborder: '0', - allowTransparency: 'true', - }); - this.$el.append(this.$iframe); - - return def; - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - - if (this.$iframe) { - this.$iframe.remove(); - } - }, -}); - -registry.anchorSlide = publicWidget.Widget.extend({ - selector: 'a[href^="/"][href*="#"], a[href^="#"]', - events: { - 'click': '_onAnimateClick', - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onAnimateClick: function (ev) { - if (this.$target[0].pathname !== window.location.pathname) { - return; - } - var hash = this.$target[0].hash; - if (!utils.isValidAnchor(hash)) { - return; - } - var $anchor = $(hash); - if (!$anchor.length || !$anchor.attr('data-anchor')) { - return; - } - ev.preventDefault(); - $('html, body').animate({ - scrollTop: $anchor.offset().top, - }, 500); - }, -}); - -registry.countdown = publicWidget.Widget.extend({ - selector: '.s_countdown', - xmlDependencies: ['/website/static/src/xml/website.s_countdown.xml'], - disabledInEditableMode: false, - - /** - * @override - */ - start: function () { - this.$wrapper = this.$('.s_countdown_canvas_wrapper'); - this.hereBeforeTimerEnds = false; - this.endAction = this.el.dataset.endAction; - this.endTime = parseInt(this.el.dataset.endTime); - this.size = parseInt(this.el.dataset.size); - this.display = this.el.dataset.display; - - this.layout = this.el.dataset.layout; - this.layoutBackground = this.el.dataset.layoutBackground; - this.progressBarStyle = this.el.dataset.progressBarStyle; - this.progressBarWeight = this.el.dataset.progressBarWeight; - - this.textColor = this._ensureCssColor(this.el.dataset.textColor); - this.layoutBackgroundColor = this._ensureCssColor(this.el.dataset.layoutBackgroundColor); - this.progressBarColor = this._ensureCssColor(this.el.dataset.progressBarColor); - - this.onlyOneUnit = this.display === 'd'; - this.width = parseInt(this.size); - if (this.layout === 'boxes') { - this.width /= 1.75; - } - this._initTimeDiff(); - - this._render(); - - this.setInterval = setInterval(this._render.bind(this), 1000); - return this._super(...arguments); - }, - /** - * @override - */ - destroy: function () { - this.$('.s_countdown_end_redirect_message').remove(); - this.$('canvas').remove(); - this.$('.s_countdown_end_message').addClass('d-none'); - this.$('.s_countdown_text_wrapper').remove(); - this.$('.s_countdown_canvas_wrapper').removeClass('d-none'); - - clearInterval(this.setInterval); - this._super(...arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Ensures the color is an actual css color. In case of a color variable, - * the color will be mapped to hexa. - * - * @private - * @param {string} color - * @returns {string} - */ - _ensureCssColor: function (color) { - if (ColorpickerDialog.isCSSColor(color)) { - return color; - } - const style = window.getComputedStyle(document.documentElement); - return style.getPropertyValue('--' + color).trim(); - }, - /** - * Gets the time difference in seconds between now and countdown due date. - * - * @private - */ - _getDelta: function () { - const currentTimestamp = Date.now() / 1000; - return this.endTime - currentTimestamp; - }, - /** - * Handles the action that should be executed once the countdown ends. - * - * @private - */ - _handleEndCountdownAction: function () { - if (this.endAction === 'redirect') { - const redirectUrl = this.el.dataset.redirectUrl; - if (this.hereBeforeTimerEnds) { - // Wait a bit, if the landing page has the same publish date - setTimeout(() => window.location = redirectUrl, 500); - } else { - // Show (non editable) msg when user lands on already finished countdown - if (!this.$('.s_countdown_end_redirect_message').length) { - this.$target.find('.container').append( - $(qweb.render('website.s_countdown.end_redirect_message', { - redirectUrl: redirectUrl, - })) - ); - } - } - } else if (this.endAction === 'message') { - this.$('.s_countdown_end_message').removeClass('d-none'); - } - }, - /** - * Initializes the `diff` object. It will contains every visible time unit - * which will each contain its related canvas, total step, label.. - * - * @private - */ - _initTimeDiff: function () { - const delta = this._getDelta(); - this.diff = []; - if (this._isUnitVisible('d') && !(this.onlyOneUnit && delta < 86400)) { - this.diff.push({ - canvas: $('<canvas/>').appendTo(this.$wrapper)[0], - // There is no logical number of unit (total) on which day units - // can be compared against, so we use an arbitrary number. - total: 15, - label: _t("Days"), - nbSeconds: 86400, - }); - } - if (this._isUnitVisible('h') || (this.onlyOneUnit && delta < 86400 && delta > 3600)) { - this.diff.push({ - canvas: $('<canvas/>').appendTo(this.$wrapper)[0], - total: 24, - label: _t("Hours"), - nbSeconds: 3600, - }); - } - if (this._isUnitVisible('m') || (this.onlyOneUnit && delta < 3600 && delta > 60)) { - this.diff.push({ - canvas: $('<canvas/>').appendTo(this.$wrapper)[0], - total: 60, - label: _t("Minutes"), - nbSeconds: 60, - }); - } - if (this._isUnitVisible('s') || (this.onlyOneUnit && delta < 60)) { - this.diff.push({ - canvas: $('<canvas/>').appendTo(this.$wrapper)[0], - total: 60, - label: _t("Seconds"), - nbSeconds: 1, - }); - } - }, - /** - * Returns weither or not the countdown should be displayed for the given - * unit (days, sec..). - * - * @private - * @param {string} unit - either 'd', 'm', 'h', or 's' - * @returns {boolean} - */ - _isUnitVisible: function (unit) { - return this.display.includes(unit); - }, - /** - * Draws the whole countdown, including one countdown for each time unit. - * - * @private - */ - _render: function () { - // If only one unit mode, restart widget on unit change to populate diff - if (this.onlyOneUnit && this._getDelta() < this.diff[0].nbSeconds) { - this.$('canvas').remove(); - this._initTimeDiff(); - } - this._updateTimeDiff(); - - const hideCountdown = this.isFinished && !this.editableMode && this.$el.hasClass('hide-countdown'); - if (this.layout === 'text') { - this.$('canvas').addClass('d-none'); - if (!this.$textWrapper) { - this.$textWrapper = $('<span/>').attr({ - class: 's_countdown_text_wrapper d-none', - }); - this.$textWrapper.text(_t("Countdown ends in")); - this.$textWrapper.append($('<span/>').attr({ - class: 's_countdown_text ml-1', - })); - this.$textWrapper.appendTo(this.$wrapper); - } - - this.$textWrapper.toggleClass('d-none', hideCountdown); - - const countdownText = this.diff.map(e => e.nb + ' ' + e.label).join(', '); - this.$('.s_countdown_text').text(countdownText.toLowerCase()); - } else { - for (const val of this.diff) { - const canvas = val.canvas; - const ctx = canvas.getContext("2d"); - ctx.canvas.width = this.width; - ctx.canvas.height = this.size; - this._clearCanvas(ctx); - - $(canvas).toggleClass('d-none', hideCountdown); - if (hideCountdown) { - return; - } - - // Draw canvas elements - if (this.layoutBackground !== 'none') { - this._drawBgShape(ctx, this.layoutBackground === 'plain'); - } - this._drawText(canvas, val.nb, val.label, this.layoutBackground === 'plain'); - if (this.progressBarStyle === 'surrounded') { - this._drawProgressBarBg(ctx, this.progressBarWeight === 'thin'); - } - if (this.progressBarStyle !== 'none') { - this._drawProgressBar(ctx, val.nb, val.total, this.progressBarWeight === 'thin'); - } - $(canvas).toggleClass('mx-2', this.layout === 'boxes'); - } - } - - if (this.isFinished) { - clearInterval(this.setInterval); - if (!this.editableMode) { - this._handleEndCountdownAction(); - } - } - }, - /** - * Updates the remaining units into the `diff` object. - * - * @private - */ - _updateTimeDiff: function () { - let delta = this._getDelta(); - this.isFinished = delta < 0; - if (this.isFinished) { - for (const unitData of this.diff) { - unitData.nb = 0; - } - return; - } - - this.hereBeforeTimerEnds = true; - for (const unitData of this.diff) { - unitData.nb = Math.floor(delta / unitData.nbSeconds); - delta -= unitData.nb * unitData.nbSeconds; - } - }, - - //-------------------------------------------------------------------------- - // Canvas drawing methods - //-------------------------------------------------------------------------- - - /** - * Erases the canvas. - * - * @private - * @param {RenderingContext} ctx - Context of the canvas - */ - _clearCanvas: function (ctx) { - ctx.clearRect(0, 0, this.size, this.size); - }, - /** - * Draws a text into the canvas. - * - * @private - * @param {HTMLCanvasElement} canvas - * @param {string} textNb - text to display in the center of the canvas, in big - * @param {string} textUnit - text to display bellow `textNb` in small - * @param {boolean} full - if true, the shape will be drawn up to the progressbar - */ - _drawText: function (canvas, textNb, textUnit, full = false) { - const ctx = canvas.getContext("2d"); - const nbSize = this.size / 4; - ctx.font = `${nbSize}px Arial`; - ctx.fillStyle = this.textColor; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(textNb, canvas.width / 2, canvas.height / 2); - - const unitSize = this.size / 12; - ctx.font = `${unitSize}px Arial`; - ctx.fillText(textUnit, canvas.width / 2, canvas.height / 2 + nbSize / 1.5, this.width); - - if (this.layout === 'boxes' && this.layoutBackground !== 'none' && this.progressBarStyle === 'none') { - let barWidth = this.size / (this.progressBarWeight === 'thin' ? 31 : 10); - if (full) { - barWidth = 0; - } - ctx.beginPath(); - ctx.moveTo(barWidth, this.size / 2); - ctx.lineTo(this.width - barWidth, this.size / 2); - ctx.stroke(); - } - }, - /** - * Draws a plain shape into the canvas. - * - * @private - * @param {RenderingContext} ctx - Context of the canvas - * @param {boolean} full - if true, the shape will be drawn up to the progressbar - */ - _drawBgShape: function (ctx, full = false) { - ctx.fillStyle = this.layoutBackgroundColor; - ctx.beginPath(); - if (this.layout === 'circle') { - let rayon = this.size / 2; - if (this.progressBarWeight === 'thin') { - rayon -= full ? this.size / 29 : this.size / 15; - } else { - rayon -= full ? 0 : this.size / 10; - } - ctx.arc(this.size / 2, this.size / 2, rayon, 0, Math.PI * 2); - ctx.fill(); - } else if (this.layout === 'boxes') { - let barWidth = this.size / (this.progressBarWeight === 'thin' ? 31 : 10); - if (full) { - barWidth = 0; - } - - ctx.fillStyle = this.layoutBackgroundColor; - ctx.rect(barWidth, barWidth, this.width - barWidth * 2, this.size - barWidth * 2); - ctx.fill(); - - const gradient = ctx.createLinearGradient(0, this.width, 0, 0); - gradient.addColorStop(0, '#ffffff24'); - gradient.addColorStop(1, this.layoutBackgroundColor); - ctx.fillStyle = gradient; - ctx.rect(barWidth, barWidth, this.width - barWidth * 2, this.size - barWidth * 2); - ctx.fill(); - $(ctx.canvas).css({'border-radius': '8px'}); - } - }, - /** - * Draws a progress bar around the countdown shape. - * - * @private - * @param {RenderingContext} ctx - Context of the canvas - * @param {string} nbUnit - how many unit should fill progress bar - * @param {string} totalUnit - number of unit to do a complete progress bar - * @param {boolean} thinLine - if true, the progress bar will be thiner - */ - _drawProgressBar: function (ctx, nbUnit, totalUnit, thinLine) { - ctx.strokeStyle = this.progressBarColor; - ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10; - if (this.layout === 'circle') { - ctx.beginPath(); - ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, Math.PI / -2, (Math.PI * 2) * (nbUnit / totalUnit) + (Math.PI / -2)); - ctx.stroke(); - } else if (this.layout === 'boxes') { - ctx.lineWidth *= 2; - let pc = nbUnit / totalUnit * 100; - - // Lines: Top(x1,y1,x2,y2) Right(x1,y1,x2,y2) Bottom(x1,y1,x2,y2) Left(x1,y1,x2,y2) - const linesCoordFuncs = [ - (linePc) => [0 + ctx.lineWidth / 2, 0, (this.width - ctx.lineWidth / 2) * linePc / 25 + ctx.lineWidth / 2, 0], - (linePc) => [this.width, 0 + ctx.lineWidth / 2, this.width, (this.size - ctx.lineWidth / 2) * linePc / 25 + ctx.lineWidth / 2], - (linePc) => [this.width - ((this.width - ctx.lineWidth / 2) * linePc / 25) - ctx.lineWidth / 2, this.size, this.width - ctx.lineWidth / 2, this.size], - (linePc) => [0, this.size - ((this.size - ctx.lineWidth / 2) * linePc / 25) - ctx.lineWidth / 2, 0, this.size - ctx.lineWidth / 2], - ]; - while (pc > 0 && linesCoordFuncs.length) { - const linePc = Math.min(pc, 25); - const lineCoord = (linesCoordFuncs.shift())(linePc); - ctx.beginPath(); - ctx.moveTo(lineCoord[0], lineCoord[1]); - ctx.lineTo(lineCoord[2], lineCoord[3]); - ctx.stroke(); - pc -= linePc; - } - } - }, - /** - * Draws a full lighter background progressbar around the shape. - * - * @private - * @param {RenderingContext} ctx - Context of the canvas - * @param {boolean} thinLine - if true, the progress bar will be thiner - */ - _drawProgressBarBg: function (ctx, thinLine) { - ctx.strokeStyle = this.progressBarColor; - ctx.globalAlpha = 0.2; - ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10; - if (this.layout === 'circle') { - ctx.beginPath(); - ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, 0, Math.PI * 2); - ctx.stroke(); - } else if (this.layout === 'boxes') { - ctx.lineWidth *= 2; - - // Lines: Top(x1,y1,x2,y2) Right(x1,y1,x2,y2) Bottom(x1,y1,x2,y2) Left(x1,y1,x2,y2) - const points = [ - [0 + ctx.lineWidth / 2, 0, this.width, 0], - [this.width, 0 + ctx.lineWidth / 2, this.width, this.size], - [0, this.size, this.width - ctx.lineWidth / 2, this.size], - [0, 0, 0, this.size - ctx.lineWidth / 2], - ]; - while (points.length) { - const point = points.shift(); - ctx.beginPath(); - ctx.moveTo(point[0], point[1]); - ctx.lineTo(point[2], point[3]); - ctx.stroke(); - } - } - ctx.globalAlpha = 1; - }, -}); - -registry.chart = publicWidget.Widget.extend({ - selector: '.s_chart', - disabledInEditableMode: false, - jsLibs: [ - '/web/static/lib/Chart/Chart.js', - ], - - /** - * @override - * @param {Object} parent - * @param {Object} options The default value of the chartbar. - */ - init: function (parent, options) { - this._super.apply(this, arguments); - this.style = window.getComputedStyle(document.documentElement); - }, - /** - * @override - */ - start: function () { - // Convert Theme colors to css color - const data = JSON.parse(this.el.dataset.data); - data.datasets.forEach(el => { - if (Array.isArray(el.backgroundColor)) { - el.backgroundColor = el.backgroundColor.map(el => this._convertToCssColor(el)); - el.borderColor = el.borderColor.map(el => this._convertToCssColor(el)); - } else { - el.backgroundColor = this._convertToCssColor(el.backgroundColor); - el.borderColor = this._convertToCssColor(el.borderColor); - } - el.borderWidth = this.el.dataset.borderWidth; - }); - - // Make chart data - const chartData = { - type: this.el.dataset.type, - data: data, - options: { - legend: { - display: this.el.dataset.legendPosition !== 'none', - position: this.el.dataset.legendPosition, - }, - tooltips: { - enabled: this.el.dataset.tooltipDisplay === 'true', - }, - title: { - display: !!this.el.dataset.title, - text: this.el.dataset.title, - }, - }, - }; - - // Add type specific options - if (this.el.dataset.type === 'radar') { - chartData.options.scale = { - ticks: { - beginAtZero: true, - } - }; - } else if (['pie', 'doughnut'].includes(this.el.dataset.type)) { - chartData.options.tooltips.callbacks = { - label: (tooltipItem, data) => { - const label = data.datasets[tooltipItem.datasetIndex].label; - const secondLabel = data.labels[tooltipItem.index]; - let final = label; - if (label) { - if (secondLabel) { - final = label + ' - ' + secondLabel; - } - } else if (secondLabel) { - final = secondLabel; - } - return final + ':' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; - }, - }; - } else { - chartData.options.scales = { - xAxes: [{ - stacked: this.el.dataset.stacked === 'true', - ticks: { - beginAtZero: true - }, - }], - yAxes: [{ - stacked: this.el.dataset.stacked === 'true', - ticks: { - beginAtZero: true - }, - }], - }; - } - - // Disable animation in edit mode - if (this.editableMode) { - chartData.options.animation = { - duration: 0, - }; - } - - const canvas = this.el.querySelector('canvas'); - this.chart = new window.Chart(canvas, chartData); - return this._super.apply(this, arguments); - }, - /** - * @override - * Discard all library changes to reset the state of the Html. - */ - destroy: function () { - if (this.chart) { // The widget can be destroyed before start has completed - this.chart.destroy(); - this.el.querySelectorAll('.chartjs-size-monitor').forEach(el => el.remove()); - } - this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - * @param {string} color A css color or theme color string - * @returns {string} Css color - */ - _convertToCssColor: function (color) { - if (!color) { - return 'transparent'; - } - return this.style.getPropertyValue(`--${color}`).trim() || color; - }, -}); - -return { - Widget: publicWidget.Widget, - Animation: Animation, - registry: registry, - - Class: Animation, // Deprecated -}; -}); diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js deleted file mode 100644 index 14005618d11..00000000000 --- a/addons/website/static/src/js/editor/snippets.options.js +++ /dev/null @@ -1,2528 +0,0 @@ -odoo.define('website.editor.snippets.options', function (require) { -'use strict'; - -var core = require('web.core'); -const ColorpickerDialog = require('web.ColorpickerDialog'); -var Dialog = require('web.Dialog'); -var weWidgets = require('wysiwyg.widgets'); -const wUtils = require('website.utils'); -var options = require('web_editor.snippets.options'); - -var _t = core._t; -var qweb = core.qweb; - -options.Class.include({ - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Refreshes all public widgets related to the given element. - * - * @private - * @param {jQuery} [$el=this.$target] - * @returns {Promise} - */ - _refreshPublicWidgets: function ($el) { - return new Promise((resolve, reject) => { - this.trigger_up('widgets_start_request', { - editableMode: true, - $target: $el || this.$target, - onSuccess: resolve, - onFailure: reject, - }); - }); - }, - /** - * @override - */ - _select: async function (previewMode, widget) { - await this._super(...arguments); - - if (!widget.$el.closest('[data-no-widget-refresh="true"]').length) { - // TODO the flag should be retrieved through widget params somehow - await this._refreshPublicWidgets(); - } - }, -}); - -options.registry.background.include({ - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _getDefaultTextContent: function () { - if (this._getMediaDialogOptions().noVideos) { - return this._super(...arguments); - } - return _t("Choose a picture or a video"); - }, - /** - * @override - */ - _getEditableMedia: function () { - if (!this._hasBgvideo()) { - return this._super(...arguments); - } - return this.$('.o_bg_video_iframe')[0]; - }, - /** - * @override - */ - _getMediaDialogOptions: function () { - return _.extend(this._super(...arguments), { - // For now, disable the possibility to have a parallax video bg - noVideos: this.$target.is('.parallax, .s_parallax_bg'), - isForBgVideo: true, - }); - }, - /** - * @override - */ - _computeWidgetState: function (methodName, params) { - if (methodName === 'chooseImage') { - return this._hasBgvideo() ? 'true' : ''; - } - return this._super(...arguments); - }, - /** - * Updates the background video used by the snippet. - * - * @private - * @see this.selectClass for parameters - * @returns {Promise} - */ - _setBgVideo: async function (previewMode, value) { - this.$('> .o_bg_video_container').toggleClass('d-none', previewMode === true); - - if (previewMode !== false) { - return; - } - - var target = this.$target[0]; - target.classList.toggle('o_background_video', !!(value && value.length)); - if (value && value.length) { - target.dataset.bgVideoSrc = value; - } else { - delete target.dataset.bgVideoSrc; - } - await this._refreshPublicWidgets(); - await this.updateUI(); - }, - /** - * Returns whether the current target has a background video or not. - * - * @private - * @returns {boolean} - */ - _hasBgvideo: function () { - return this.$target[0].classList.contains('o_background_video'); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @override - */ - _onBackgroundColorUpdate: function (ev, previewMode) { - var ret = this._super(...arguments); - if (ret) { - this._setBgVideo(previewMode); - } - return ret; - }, - /** - * @override - */ - _onSaveMediaDialog: async function (data) { - if (!data.bgVideoSrc) { - const _super = this._super.bind(this); - const args = arguments; - await this._setBgVideo(false); - return _super(...args); - } - // if the user chose a video, only add the video without removing the - // background - await this._setBgVideo(false, data.bgVideoSrc); - }, -}); - -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 prom = new Promise(function (resolve, reject) { - self.trigger_up('request_save', { - onSuccess: resolve, - onFailure: reject, - }); - }); - return prom; - }, - ], - }); - }}, - {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 prom; - var self = this; - if (proto.__link === undefined) { - prom = 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 Promise.all([this._super.apply(this, arguments), prom]); - }, - /** - * 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; - }, - }); - }, - }); - }, -}); - -options.registry.Carousel = options.Class.extend({ - /** - * @override - */ - start: function () { - this.$target.carousel('pause'); - this.$indicators = this.$target.find('.carousel-indicators'); - this.$controls = this.$target.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators'); - - // Prevent enabling the carousel overlay when clicking on the carousel - // controls (indeed we want it to change the carousel slide then enable - // the slide overlay) + See "CarouselItem" option. - this.$controls.addClass('o_we_no_overlay'); - - let _slideTimestamp; - this.$target.on('slide.bs.carousel.carousel_option', () => { - _slideTimestamp = window.performance.now(); - setTimeout(() => this.trigger_up('hide_overlay')); - }); - this.$target.on('slid.bs.carousel.carousel_option', () => { - // slid.bs.carousel is most of the time fired too soon by bootstrap - // since it emulates the transitionEnd with a setTimeout. We wait - // here an extra 20% of the time before retargeting edition, which - // should be enough... - const _slideDuration = (window.performance.now() - _slideTimestamp); - setTimeout(() => { - this.trigger_up('activate_snippet', { - $snippet: this.$target.find('.carousel-item.active'), - ifInactiveOptions: true, - }); - this.$target.trigger('active_slide_targeted'); - }, 0.2 * _slideDuration); - }); - - return this._super.apply(this, arguments); - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - this.$target.off('.carousel_option'); - }, - /** - * @override - */ - onBuilt: function () { - this._assignUniqueID(); - }, - /** - * @override - */ - onClone: function () { - this._assignUniqueID(); - }, - /** - * @override - */ - cleanForSave: function () { - const $items = this.$target.find('.carousel-item'); - $items.removeClass('next prev left right active').first().addClass('active'); - this.$indicators.find('li').removeClass('active').empty().first().addClass('active'); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Creates a unique ID for the carousel and reassign data-attributes that - * depend on it. - * - * @private - */ - _assignUniqueID: function () { - const id = 'myCarousel' + Date.now(); - this.$target.attr('id', id); - this.$target.find('[data-target]').attr('data-target', '#' + id); - this.$target.find('[data-slide]').attr('href', '#' + id); - }, -}); - -options.registry.CarouselItem = options.Class.extend({ - isTopOption: true, - - /** - * @override - */ - start: function () { - this.$carousel = this.$target.closest('.carousel'); - this.$indicators = this.$carousel.find('.carousel-indicators'); - this.$controls = this.$carousel.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators'); - - var leftPanelEl = this.$overlay.data('$optionsSection')[0]; - var titleTextEl = leftPanelEl.querySelector('we-title > span'); - this.counterEl = document.createElement('span'); - titleTextEl.appendChild(this.counterEl); - - leftPanelEl.querySelector('.oe_snippet_remove').classList.add('d-none'); // TODO improve the way to do that - - return this._super(...arguments); - }, - /** - * @override - */ - destroy: function () { - this._super(...arguments); - this.$carousel.off('.carousel_item_option'); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Updates the slide counter. - * - * @override - */ - updateUI: async function () { - await this._super(...arguments); - const $items = this.$carousel.find('.carousel-item'); - const $activeSlide = $items.filter('.active'); - const updatedText = ` (${$activeSlide.index() + 1}/${$items.length})`; - this.counterEl.textContent = updatedText; - }, - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Adds a slide. - * - * @see this.selectClass for parameters - */ - addSlide: function (previewMode) { - const $items = this.$carousel.find('.carousel-item'); - this.$controls.removeClass('d-none'); - this.$indicators.append($('<li>', { - 'data-target': '#' + this.$target.attr('id'), - 'data-slide-to': $items.length, - })); - // Need to remove editor data from the clone so it gets its own. - const $active = $items.filter('.active'); - $active.clone(false) - .removeClass('active') - .insertAfter($active); - this.$carousel.carousel('next'); - }, - /** - * Removes the current slide. - * - * @see this.selectClass for parameters. - */ - removeSlide: function (previewMode) { - const $items = this.$carousel.find('.carousel-item'); - const newLength = $items.length - 1; - if (!this.removing && newLength > 0) { - const $toDelete = $items.filter('.active'); - this.$carousel.one('active_slide_targeted.carousel_item_option', () => { - $toDelete.remove(); - this.$indicators.find('li:last').remove(); - this.$controls.toggleClass('d-none', newLength === 1); - this.$carousel.trigger('content_changed'); - this.removing = false; - }); - this.removing = true; - this.$carousel.carousel('prev'); - } - }, - /** - * Goes to next slide or previous slide. - * - * @see this.selectClass for parameters - */ - slide: function (previewMode, widgetValue, params) { - switch (widgetValue) { - case 'left': - this.$controls.filter('.carousel-control-prev')[0].click(); - break; - case 'right': - this.$controls.filter('.carousel-control-next')[0].click(); - break; - } - }, -}); - -options.registry.navTabs = options.Class.extend({ - /** - * @override - */ - start: function () { - this._findLinksAndPanes(); - return this._super.apply(this, arguments); - }, - /** - * @override - */ - onBuilt: function () { - this._generateUniqueIDs(); - }, - /** - * @override - */ - onClone: function () { - this._generateUniqueIDs(); - }, - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Creates a new tab and tab-pane. - * - * @see this.selectClass for parameters - */ - addTab: function (previewMode, widgetValue, params) { - var $activeItem = this.$navLinks.filter('.active').parent(); - var $activePane = this.$tabPanes.filter('.active'); - - var $navItem = $activeItem.clone(); - var $navLink = $navItem.find('.nav-link').removeClass('active show'); - var $tabPane = $activePane.clone().removeClass('active show'); - $navItem.insertAfter($activeItem); - $tabPane.insertAfter($activePane); - this._findLinksAndPanes(); - this._generateUniqueIDs(); - - $navLink.tab('show'); - }, - /** - * Removes the current active tab and its content. - * - * @see this.selectClass for parameters - */ - removeTab: function (previewMode, widgetValue, params) { - var self = this; - - var $activeLink = this.$navLinks.filter('.active'); - var $activePane = this.$tabPanes.filter('.active'); - - var $next = this.$navLinks.eq((this.$navLinks.index($activeLink) + 1) % this.$navLinks.length); - - return new Promise(resolve => { - $next.one('shown.bs.tab', function () { - $activeLink.parent().remove(); - $activePane.remove(); - self._findLinksAndPanes(); - resolve(); - }); - $next.tab('show'); - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _computeWidgetVisibility: async function (widgetName, params) { - if (widgetName === 'remove_tab_opt') { - return (this.$tabPanes.length > 2); - } - return this._super(...arguments); - }, - /** - * @private - */ - _findLinksAndPanes: function () { - this.$navLinks = this.$target.find('.nav-link'); - var $el = this.$target; - do { - $el = $el.parent(); - this.$tabPanes = $el.find('.tab-pane'); - } while (this.$tabPanes.length === 0 && !$el.is('body')); - }, - /** - * @private - */ - _generateUniqueIDs: function () { - for (var i = 0; i < this.$navLinks.length; i++) { - var id = _.now() + '_' + _.uniqueId(); - var idLink = 'nav_tabs_link_' + id; - var idContent = 'nav_tabs_content_' + id; - this.$navLinks.eq(i).attr({ - 'id': idLink, - 'href': '#' + idContent, - 'aria-controls': idContent, - }); - this.$tabPanes.eq(i).attr({ - 'id': idContent, - 'aria-labelledby': idLink, - }); - } - }, -}); - -options.registry.sizing_x = options.registry.sizing.extend({ - /** - * @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*(offset-xl-|offset-lg-)([0-9-]+)/g, ''); - this.$target.attr('class', _class); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _getSize: function () { - var width = this.$target.closest('.row').width(); - var gridE = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; - var gridW = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; - this.grid = { - e: [_.map(gridE, v => ('col-lg-' + v)), _.map(gridE, v => width / 12 * v), 'width'], - w: [_.map(gridW, v => ('offset-lg-' + v)), _.map(gridW, v => width / 12 * v), 'margin-left'], - }; - 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-lg-([0-9]+)|$/)[1] || 0); - var beginOffset = Number(beginClass.match(/offset-lg-([0-9-]+)|$/)[1] || beginClass.match(/offset-xl-([0-9-]+)|$/)[1] || 0); - var offset = Number(this.grid.w[0][current].match(/offset-lg-([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*(offset-xl-|offset-lg-|col-lg-)([0-9-]+)/g, '')); - - this.$target.addClass('col-lg-' + (colSize > 12 ? 12 : colSize)); - if (offset > 0) { - this.$target.addClass('offset-lg-' + offset); - } - } - this._super.apply(this, arguments); - }, -}); - -options.registry.layout_column = options.Class.extend({ - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Changes the number of columns. - * - * @see this.selectClass for parameters - */ - selectCount: function (previewMode, widgetValue, params) { - const nbColumns = parseInt(widgetValue); - this._updateColumnCount(nbColumns - this.$target.children().length); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _computeWidgetState: function (methodName, params) { - if (methodName === 'selectCount') { - return '' + this.$target.children().length; - } - return this._super(...arguments); - }, - /** - * Adds new columns which are clones of the last column or removes the - * last x columns. - * - * @private - * @param {integer} count - positif to add, negative to remove - */ - _updateColumnCount: function (count) { - if (!count) { - return; - } - - this.trigger_up('request_history_undo_record', {$target: this.$target}); - - if (count > 0) { - var $lastColumn = this.$target.children().last(); - for (var i = 0; i < count; i++) { - $lastColumn.clone().insertAfter($lastColumn); - } - } else { - var self = this; - _.each(this.$target.children().slice(count), function (el) { - self.trigger_up('remove_snippet', {$snippet: $(el)}); - }); - } - - this._resizeColumns(); - this.trigger_up('cover_update'); - }, - /** - * Resizes the columns so that they are kept on one row. - * - * @private - */ - _resizeColumns: function () { - var $columns = this.$target.children(); - var colsLength = $columns.length; - var colSize = Math.floor(12 / colsLength) || 1; - var colOffset = Math.floor((12 - colSize * colsLength) / 2); - var colClass = 'col-lg-' + colSize; - _.each($columns, function (column) { - var $column = $(column); - $column.attr('class', $column.attr('class').replace(/\b(col|offset)-lg(-\d+)?\b/g, '')); - $column.addClass(colClass); - }); - if (colOffset) { - $columns.first().addClass('offset-lg-' + colOffset); - } - }, -}); - -options.registry.parallax = options.Class.extend({ - /** - * @override - */ - onFocus: function () { - this.trigger_up('option_update', { - optionNames: ['background', 'BackgroundPosition'], - name: 'target', - data: this.$target.find('> .s_parallax_bg'), - }); - // Refresh the parallax animation on focus; at least useful because - // there may have been changes in the page that influenced the parallax - // rendering (new snippets, ...). - // TODO make this automatic. - this._refreshPublicWidgets(); - }, - /** - * @override - */ - onMove: function () { - this._refreshPublicWidgets(); - }, -}); - -var FacebookPageDialog = weWidgets.Dialog.extend({ - xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat( - ['/website/static/src/xml/website.facebook_page.xml'] - ), - template: 'website.facebook_page_dialog', - events: _.extend({}, weWidgets.Dialog.prototype.events || {}, { - 'change': '_onOptionChange', - }), - - /** - * @constructor - */ - init: function (parent, fbData, options) { - this._super(parent, _.extend({ - title: _t("Facebook Page"), - }, options || {})); - - this.fbData = $.extend(true, {}, fbData); - this.final_data = this.fbData; - }, - /** - * @override - */ - start: function () { - this.$previewPage = this.$('.o_facebook_page'); - this.opened().then(this._renderPreview.bind(this)); - return this._super.apply(this, arguments); - }, - - //------------------------------------------------------------------ - // Private - //------------------------------------------------------------------ - - /** - * Manages Facebook page preview. Also verifies if the page exists on - * Facebook or not. - * - * @private - */ - _renderPreview: function () { - var self = this; - var match = this.fbData.href.match(/^(?:https?:\/\/)?(?:www\.)?(?:fb|facebook)\.com\/(?:([\w.]+)|[^/?#]+-([0-9]{15,16}))(?:$|[/?# ])/); - if (match) { - // Check if the page exists on Facebook or not - $.ajax({ - url: 'https://graph.facebook.com/' + (match[2] || match[1]) + '/picture', - statusCode: { - 200: function () { - self._toggleWarning(true); - - // Managing height based on options - if (self.fbData.tabs) { - self.fbData.height = self.fbData.tabs === 'events' ? 300 : 500; - } else if (self.fbData.small_header) { - self.fbData.height = self.fbData.show_facepile ? 165 : 70; - } else if (!self.fbData.small_header) { - self.fbData.height = self.fbData.show_facepile ? 225 : 150; - } - options.registry.facebookPage.prototype.markFbElement(self.getParent(), self.$previewPage, self.fbData); - }, - 404: function () { - self._toggleWarning(false); - }, - }, - }); - } else { - this._toggleWarning(false); - } - }, - /** - * Toggles the warning message and save button and destroy iframe preview. - * - * @private - * @param {boolean} toggle - */ - _toggleWarning: function (toggle) { - this.trigger_up('widgets_stop_request', { - $target: this.$previewPage, - }); - this.$('.facebook_page_warning').toggleClass('d-none', toggle); - this.$footer.find('.btn-primary').prop('disabled', !toggle); - }, - - //------------------------------------------------------------------ - // Handlers - //------------------------------------------------------------------ - - /** - * Called when a facebook option is changed -> adapt the preview and saved - * data. - * - * @private - */ - _onOptionChange: function () { - var self = this; - // Update values in fbData - this.fbData.tabs = _.map(this.$('.o_facebook_tabs input:checked'), tab => tab.name).join(','); - this.fbData.href = this.$('.o_facebook_page_url').val(); - _.each(this.$('.o_facebook_options input'), function (el) { - self.fbData[el.name] = $(el).prop('checked'); - }); - this._renderPreview(); - }, -}); -options.registry.facebookPage = options.Class.extend({ - /** - * Initializes the required facebook page data to create the iframe. - * - * @override - */ - willStart: function () { - var defs = [this._super.apply(this, arguments)]; - - var defaults = { - href: false, - height: 215, - width: 350, - tabs: '', - small_header: false, - hide_cover: false, - show_facepile: false, - }; - this.fbData = _.defaults(_.pick(this.$target.data(), _.keys(defaults)), defaults); - - if (!this.fbData.href) { - // Fetches the default url for facebook page from website config - var self = this; - defs.push(this._rpc({ - model: 'website', - method: 'search_read', - args: [[], ['social_facebook']], - limit: 1, - }).then(function (res) { - if (res) { - self.fbData.href = res[0].social_facebook || 'https://www.facebook.com/Odoo'; - } - })); - } - - return Promise.all(defs); - }, - /** - * @override - */ - start: function () { - var self = this; - this.$target.on('click.facebook_page_option', '.o_add_facebook_page', function (ev) { - ev.preventDefault(); - self.fbPageOptions(); - }); - return this._super.apply(this, arguments); - }, - /** - * @override - */ - destroy: function () { - this._super.apply(this, arguments); - this.$target.off('.facebook_page_option'); - }, - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Opens a dialog to configure the facebook page options. - * - * @see this.selectClass for parameters - */ - fbPageOptions: function () { - var dialog = new FacebookPageDialog(this, this.fbData).open(); - dialog.on('save', this, function (fbData) { - this.$target.empty(); - this.fbData = fbData; - options.registry.facebookPage.prototype.markFbElement(this, this.$target, this.fbData); - }); - }, - - //-------------------------------------------------------------------------- - // Static - //-------------------------------------------------------------------------- - - /** - * @static - */ - markFbElement: function (self, $el, fbData) { - _.each(fbData, function (value, key) { - $el.attr('data-' + key, value); - $el.data(key, value); - }); - self._refreshPublicWidgets($el); - }, -}); - -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').css('list-style', ''); - } - }, - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * @override - */ - selectClass: async function () { - await this._super.apply(this, arguments); - - this.trigger_up('widgets_stop_request', { - $target: this.$target, - }); - - 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), a => (a.nodeType === 3)); - if (!texts.length || !texts.reduce((a, b) => (a.textContent + b.textContent)).match(/\S/)) { - return; - } - $(this).children('ul,ol').addClass('o_close'); - return $(this).children(':not(ul,ol)')[0] || this; - }) - .prepend('<a href="#" class="o_ul_toggle_self fa" />'); - var $li = this.$target.find('li:has(+li:not(>.o_ul_toggle_self)>ul, +li:not(>.o_ul_toggle_self)>ol)'); - $li.css('list-style', this.$target.hasClass('o_ul_folded') ? 'none' : ''); - $li.map((i, el) => ($(el).children()[0] || el)) - .prepend('<a href="#" class="o_ul_toggle_next fa" />'); - $li.removeClass('o_open').next().addClass('o_close'); - this.$target.find('li').removeClass('o_open'); - }, -}); - -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('.collapse').removeAttr('id'); - this._createIDs(); - }, - /** - * @override - */ - onMove: function () { - this._createIDs(); - var $panel = this.$target.find('.collapse').removeData('bs.collapse'); - if ($panel.attr('aria-expanded') === 'true') { - $panel.closest('.accordion').find('.collapse[aria-expanded="true"]') - .filter((i, el) => (el !== $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('.accordion'); - 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('.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.addClass('o_fake_not_editable').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('.carousel-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); - }); - - this.$target.on('dropped', 'img', function (ev) { - self.mode(null, self.getMode()); - if (!ev.target.height) { - $(ev.target).one('load', function () { - setTimeout(function () { - self.trigger_up('cover_update'); - }); - }); - } - }); - - if (this.$('.container:first > *:not(div)').length) { - self.mode(null, self.getMode()); - } - - 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 $container = this.$('.container:first'); - var dialog = new weWidgets.MediaDialog(this, {multiImages: true, onlyImages: true, mediaWidth: 1920}); - var lastImage = _.last(this._getImages()); - var index = lastImage ? this._getIndex(lastImage) : -1; - return new Promise(resolve => { - dialog.on('save', this, function (attachments) { - for (var i = 0; i < attachments.length; i++) { - $('<img/>', { - class: 'img img-fluid', - src: attachments[i].image_src, - 'data-index': ++index, - }).appendTo($container); - } - this.mode('reset', this.getMode()); - this.trigger_up('cover_update'); - resolve(); - }); - dialog.open(); - }); - }, - /** - * Allows to change the number of columns when displaying images with a - * grid-like layout. - * - * @see this.selectClass for parameters - */ - columns: function (previewMode, widgetValue, params) { - const nbColumns = parseInt(widgetValue || '1'); - this.$target.attr('data-columns', nbColumns); - - this.mode(previewMode, this.getMode(), {}); // TODO improve - }, - /** - * Get the image target's layout mode (slideshow, masonry, grid or nomode). - * - * @returns {String('slideshow'|'masonry'|'grid'|'nomode')} - */ - getMode: function () { - var mode = 'slideshow'; - if (this.$target.hasClass('o_masonry')) { - mode = 'masonry'; - } - if (this.$target.hasClass('o_grid')) { - mode = 'grid'; - } - if (this.$target.hasClass('o_nomode')) { - mode = 'nomode'; - } - return mode; - }, - /** - * Displays the images with the "grid" layout. - */ - grid: function () { - var imgs = this._getImages(); - var $row = $('<div/>', {class: 'row'}); - var columns = this._getColumns(); - var colClass = 'col-lg-' + (12 / columns); - var $container = this._replaceContent($row); - - _.each(imgs, function (img, index) { - var $img = $(img); - var $col = $('<div/>', {class: colClass}); - $col.append($img).appendTo($row); - if ((index + 1) % columns === 0) { - $row = $('<div/>', {class: 'row'}); - $row.appendTo($container); - } - }); - this.$target.css('height', ''); - }, - /** - * Allows to changes the interval of automatic slideshow (not active in - * edit mode). - */ - interval: function (previewMode, widgetValue) { - this.$target.find('.carousel:first').attr('data-interval', widgetValue || '0'); - }, - /** - * Displays the images with the "masonry" layout. - */ - masonry: function () { - var self = this; - var imgs = this._getImages(); - var columns = this._getColumns(); - var colClass = 'col-lg-' + (12 / columns); - var cols = []; - - var $row = $('<div/>', {class: 'row'}); - this._replaceContent($row); - - // Create columns - for (var c = 0; c < columns; c++) { - var $col = $('<div/>', {class: 'col o_snippet_not_selectable ' + colClass}); - $row.append($col); - cols.push($col[0]); - } - - // Dispatch images in columns by always putting the next one in the - // smallest-height column - while (imgs.length) { - var min = Infinity; - var $lowest; - _.each(cols, function (col) { - var $col = $(col); - var height = $col.is(':empty') ? 0 : $col.find('img').last().offset().top + $col.find('img').last().height() - self.$target.offset().top; - if (height < min) { - min = height; - $lowest = $col; - } - }); - $lowest.append(imgs.pop()); - } - }, - /** - * Allows to change the images layout. @see grid, masonry, nomode, slideshow - * - * @see this.selectClass for parameters - */ - mode: function (previewMode, widgetValue, params) { - widgetValue = widgetValue || 'slideshow'; // FIXME should not be needed - this.$target.css('height', ''); - this[widgetValue](); - this.$target - .removeClass('o_nomode o_masonry o_grid o_slideshow') - .addClass('o_' + widgetValue); - this.trigger_up('cover_update'); - }, - /** - * Displays the images with the standard layout: floating images. - */ - nomode: function () { - var $row = $('<div/>', {class: 'row'}); - var imgs = this._getImages(); - - this._replaceContent($row); - - _.each(imgs, function (img) { - var wrapClass = 'col-lg-3'; - if (img.width >= img.height * 2 || img.width > 600) { - wrapClass = 'col-lg-6'; - } - var $wrap = $('<div/>', {class: wrapClass}).append(img); - $row.append($wrap); - }); - }, - /** - * Allows to remove all images. Restores the snippet to the way it was when - * it was added in the page. - * - * @see this.selectClass for parameters - */ - removeAllImages: function (previewMode) { - var $addImg = $('<div>', { - class: 'alert alert-info css_editable_mode_display text-center', - }); - var $text = $('<span>', { - class: 'o_add_images', - style: 'cursor: pointer;', - text: _t(" Add Images"), - }); - var $icon = $('<i>', { - class: ' fa fa-plus-circle', - }); - this._replaceContent($addImg.append($icon).append($text)); - }, - /** - * Displays the images with a "slideshow" layout. - */ - slideshow: function () { - var imgStyle = this.$el.find('.active[data-styling]').data('styling') || ''; - var urls = _.map(this._getImages(), function (img) { - return $(img).attr('src'); - }); - var currentInterval = this.$target.find('.carousel:first').attr('data-interval'); - var params = { - srcs: urls, - index: 0, - title: "", - interval: currentInterval || this.$target.data('interval') || 0, - id: 'slideshow_' + new Date().getTime(), - userStyle: imgStyle, - }, - $slideshow = $(qweb.render('website.gallery.slideshow', params)); - this._replaceContent($slideshow); - _.each(this.$('img'), function (img, index) { - $(img).attr({contenteditable: true, 'data-index': index}); - }); - this.$target.css('height', Math.round(window.innerHeight * 0.7)); - - // Apply layout animation - this.$target.off('slide.bs.carousel').off('slid.bs.carousel'); - this.$('li.fa').off('click'); - }, - /** - * Allows to change the style of the individual images. - * - * @see this.selectClass for parameters - */ - styling: function (previewMode, value) { - var classes = _.map(this.$el.find('[data-styling]'), function (el) { - return $(el).data('styling'); - }).join(' '); - this.$('img').removeClass(classes).addClass(value); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Handles image removals and image index updates. - * - * @override - */ - notify: async function (name, data) { - await this._super(...arguments); - if (name === 'image_removed') { - data.$image.remove(); // Force the removal of the image before reset - this.mode('reset', this.getMode()); - } else if (name === 'image_index_request') { - var imgs = this._getImages(); - var position = _.indexOf(imgs, data.$image[0]); - imgs.splice(position, 1); - switch (data.position) { - case 'first': - imgs.unshift(data.$image[0]); - break; - case 'prev': - imgs.splice(position - 1, 0, data.$image[0]); - break; - case 'next': - imgs.splice(position + 1, 0, data.$image[0]); - break; - case 'last': - imgs.push(data.$image[0]); - break; - } - _.each(imgs, function (img, index) { - // Note: there might be more efficient ways to do that but it is - // more simple this way and allows compatibility with 10.0 where - // indexes were not the same as positions. - $(img).attr('data-index', index); - }); - this.mode('reset', this.getMode()); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _computeWidgetState: function (methodName, params) { - switch (methodName) { - case 'mode': { - let activeModeName = 'slideshow'; - for (const modeName of params.possibleValues) { - if (this.$target.hasClass(`o_${modeName}`)) { - activeModeName = modeName; - break; - } - } - this.activeMode = activeModeName; - return activeModeName; - } - case 'interval': { - const carousel = this.$target[0].querySelector('.carousel'); - return (carousel && carousel.dataset.interval || '0'); - } - case 'columns': { - return `${this._getColumns()}`; - } - case 'styling': { - const img = this.$target[0].querySelector('img'); - if (!img) { - return ''; - } - for (const className of params.possibleValues) { - if (className && img.classList.contains(className)) { - return className; - } - } - return ''; - } - } - return this._super(...arguments); - }, - /** - * Returns the images, sorted by index. - * - * @private - * @returns {DOMElement[]} - */ - _getImages: function () { - var imgs = this.$('img').get(); - var self = this; - imgs.sort(function (a, b) { - return self._getIndex(a) - self._getIndex(b); - }); - return imgs; - }, - /** - * Returns the index associated to a given image. - * - * @private - * @param {DOMElement} img - * @returns {integer} - */ - _getIndex: function (img) { - return img.dataset.index || 0; - }, - /** - * Returns the currently selected column option. - * - * @private - * @returns {integer} - */ - _getColumns: function () { - return parseInt(this.$target.attr('data-columns')) || 3; - }, - /** - * Empties the container, adds the given content and returns the container. - * - * @private - * @param {jQuery} $content - * @returns {jQuery} the main container of the snippet - */ - _replaceContent: function ($content) { - var $container = this.$('.container:first'); - $container.empty().append($content); - return $container; - }, -}); - -options.registry.countdown = options.Class.extend({ - events: _.extend({}, options.Class.prototype.events || {}, { - 'click .toggle-edit-message': '_onToggleEndMessageClick', - }), - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Changes the countdown action at zero. - * - * @see this.selectClass for parameters - */ - endAction: function (previewMode, widgetValue, params) { - this.$target[0].dataset.endAction = widgetValue; - if (widgetValue === 'message') { - if (!this.$target.find('.s_countdown_end_message').length) { - const message = this.endMessage || qweb.render('website.s_countdown.end_message'); - this.$target.find('.container').append(message); - } - } else { - const $message = this.$target.find('.s_countdown_end_message').detach(); - if ($message.length) { - this.endMessage = $message[0].outerHTML; - } - } - }, - /** - * Changes the countdown style. - * - * @see this.selectClass for parameters - */ - layout: function (previewMode, widgetValue, params) { - switch (widgetValue) { - case 'circle': - this.$target[0].dataset.progressBarStyle = 'disappear'; - this.$target[0].dataset.progressBarWeight = 'thin'; - this.$target[0].dataset.layoutBackground = 'none'; - break; - case 'boxes': - this.$target[0].dataset.progressBarStyle = 'none'; - this.$target[0].dataset.layoutBackground = 'plain'; - break; - case 'clean': - this.$target[0].dataset.progressBarStyle = 'none'; - this.$target[0].dataset.layoutBackground = 'none'; - break; - case 'text': - this.$target[0].dataset.progressBarStyle = 'none'; - this.$target[0].dataset.layoutBackground = 'none'; - break; - } - this.$target[0].dataset.layout = widgetValue; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - updateUIVisibility: async function () { - await this._super(...arguments); - const dataset = this.$target[0].dataset; - - // End Action UI - this.$el.find('.toggle-edit-message') - .toggleClass('d-none', dataset.endAction !== 'message'); - - // End Message UI - this.updateUIEndMessage(); - }, - /** - * @see this.updateUI - */ - updateUIEndMessage: function () { - this.$target.find('.s_countdown_canvas_wrapper') - .toggleClass("d-none", this.showEndMessage === true && this.$target.hasClass("hide-countdown")); - this.$target.find('.s_countdown_end_message') - .toggleClass("d-none", !this.showEndMessage); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _computeWidgetState: function (methodName, params) { - switch (methodName) { - case 'endAction': - case 'layout': - return this.$target[0].dataset[methodName]; - } - return this._super(...arguments); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onToggleEndMessageClick: function () { - this.showEndMessage = !this.showEndMessage; - this.$el.find(".toggle-edit-message") - .toggleClass('text-primary', this.showEndMessage); - this.updateUIEndMessage(); - this.trigger_up('cover_update'); - }, -}); - -options.registry.gallery_img = options.Class.extend({ - /** - * Rebuilds the whole gallery when one image is removed. - * - * @override - */ - onRemove: function () { - this.trigger_up('option_update', { - optionName: 'gallery', - name: 'image_removed', - data: { - $image: this.$target, - }, - }); - }, - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Allows to change the position of an image (its order in the image set). - * - * @see this.selectClass for parameters - */ - position: function (previewMode, widgetValue, params) { - this.trigger_up('deactivate_snippet'); - this.trigger_up('option_update', { - optionName: 'gallery', - name: 'image_index_request', - data: { - $image: this.$target, - position: widgetValue, - }, - }); - }, -}); - -options.registry.topMenuTransparency = options.Class.extend({ - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Handles the toggling between normal and overlay positions of the header. - * - * @see this.selectClass for params - */ - transparent: function (previewMode, widgetValue, params) { - var self = this; - this.trigger_up('action_demand', { - actionName: 'toggle_page_option', - params: [{name: 'header_overlay'}], - onSuccess: function () { - self.trigger_up('action_demand', { - actionName: 'toggle_page_option', - params: [{name: 'header_color', value: ''}], - }); - }, - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _computeWidgetState: function (methodName, params) { - if (methodName === 'transparent') { - return new Promise(resolve => { - this.trigger_up('action_demand', { - actionName: 'get_page_option', - params: ['header_overlay'], - onSuccess: v => resolve(v ? 'true' : ''), - }); - }); - } - return this._super(...arguments); - }, -}); - -options.registry.topMenuColor = options.Class.extend({ - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _computeVisibility: async function () { - const show = await this._super(...arguments); - if (!show) { - return false; - } - return new Promise(resolve => { - this.trigger_up('action_demand', { - actionName: 'get_page_option', - params: ['header_overlay'], - onSuccess: value => resolve(!!value), - }); - }); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @override - */ - _onColorButtonClick: function () { - this._super.apply(this, arguments); - var bgs = this.$target.attr('class').match(/bg-(\w|-)+/g); - var allowedBgs = this.classes.split(' '); - var color = _.intersection(bgs, allowedBgs).join(' '); - this.trigger_up('action_demand', { - actionName: 'toggle_page_option', - params: [{name: 'header_color', value: color}], - }); - }, -}); - -/** - * Handles the edition of snippet's anchor name. - */ -options.registry.anchor = options.Class.extend({ - xmlDependencies: ['/website/static/src/xml/website.editor.xml'], - isTopOption: true, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - start: function () { - // Generate anchor and copy it to clipboard on click, show the tooltip on success - this.$button = this.$el.find('we-button'); - const clipboard = new ClipboardJS(this.$button[0], {text: () => this._getAnchorLink()}); - clipboard.on('success', () => { - const anchor = decodeURIComponent(this._getAnchorLink()); - this.displayNotification({ - title: _t("Copied !"), - message: _.str.sprintf(_t("The anchor has been copied to your clipboard.<br>Link: %s"), anchor), - buttons: [{text: _t("edit"), click: () => this.openAnchorDialog()}], - }); - }); - - return this._super.apply(this, arguments); - }, - /** - * @override - */ - onClone: function () { - this.$target.removeAttr('id data-anchor'); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - /** - * @see this.selectClass for parameters - */ - openAnchorDialog: function (previewMode, widgetValue, params) { - var self = this; - var buttons = [{ - text: _t("Save & copy"), - classes: 'btn-primary', - click: function () { - var $input = this.$('.o_input_anchor_name'); - var anchorName = self._text2Anchor($input.val()); - if (self.$target[0].id === anchorName) { - // If the chosen anchor name is already the one used by the - // element, close the dialog and do nothing else - this.close(); - return; - } - - const alreadyExists = !!document.getElementById(anchorName); - this.$('.o_anchor_already_exists').toggleClass('d-none', !alreadyExists); - $input.toggleClass('is-invalid', alreadyExists); - if (!alreadyExists) { - self._setAnchorName(anchorName); - this.close(); - self.$button[0].click(); - } - }, - }, { - text: _t("Discard"), - close: true, - }]; - if (this.$target.attr('id')) { - buttons.push({ - text: _t("Remove"), - classes: 'btn-link ml-auto', - icon: 'fa-trash', - close: true, - click: function () { - self._setAnchorName(); - }, - }); - } - new Dialog(this, { - title: _t("Link Anchor"), - $content: $(qweb.render('website.dialog.anchorName', { - currentAnchor: decodeURIComponent(this.$target.attr('id')), - })), - buttons: buttons, - }).open(); - }, - /** - * @private - * @param {String} value - */ - _setAnchorName: function (value) { - if (value) { - this.$target.attr({ - 'id': value, - 'data-anchor': true, - }); - } else { - this.$target.removeAttr('id data-anchor'); - } - this.$target.trigger('content_changed'); - }, - /** - * Returns anchor text. - * - * @private - * @returns {string} - */ - _getAnchorLink: function () { - if (!this.$target[0].id) { - const $titles = this.$target.find('h1, h2, h3, h4, h5, h6'); - const title = $titles.length > 0 ? $titles[0].innerText : this.data.snippetName; - const anchorName = this._text2Anchor(title); - let n = ''; - while (document.getElementById(anchorName + n)) { - n = (n || 1) + 1; - } - this._setAnchorName(anchorName + n); - } - return `#${this.$target[0].id}`; - }, - /** - * Creates a safe id/anchor from text. - * - * @private - * @param {string} text - * @returns {string} - */ - _text2Anchor: function (text) { - return encodeURIComponent(text.trim().replace(/\s+/g, '-')); - }, -}); - -/** - * Allows edition of 'cover_properties' in website models which have such - * fields (blogs, posts, events, ...). - */ -options.registry.CoverProperties = options.Class.extend({ - /** - * @constructor - */ - init: function () { - this._super.apply(this, arguments); - - this.$image = this.$target.find('.o_record_cover_image'); - this.$filter = this.$target.find('.o_record_cover_filter'); - }, - /** - * @override - */ - start: function () { - this.$filterValueOpts = this.$el.find('[data-filter-value]'); - this.$filterColorOpts = this.$el.find('[data-filter-color]'); - this.filterColorClasses = this.$filterColorOpts.map(function () { - return $(this).data('filterColor'); - }).get().join(' '); - - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * @see this.selectClass for parameters - */ - clear: function (previewMode, widgetValue, params) { - this.$target.removeClass('o_record_has_cover'); - this.$image.css('background-image', ''); - }, - /** - * @see this.selectClass for parameters - */ - change: function (previewMode, widgetValue, params) { - var $image = $('<img/>'); - var background = this.$image.css('background-image'); - if (background && background !== 'none') { - $image.attr('src', background.match(/^url\(["']?(.+?)["']?\)$/)[1]); - } - - return new Promise(resolve => { - var editor = new weWidgets.MediaDialog(this, { - mediaWidth: 1920, - onlyImages: true, - firstFilters: ['background'] - }, $image[0]).open(); - editor.on('save', this, function (image) { - var src = image.src; - this.$image.css('background-image', src ? ('url(' + src + ')') : ''); - if (!this.$target.hasClass('o_record_has_cover')) { - this.$el.find('.o_record_cover_opt_size_default[data-select-class]').click(); - } - resolve(); - }); - }); - }, - /** - * @see this.selectClass for parameters - */ - filterValue: function (previewMode, widgetValue, params) { - this.$filter.css('opacity', widgetValue || 0); - }, - /** - * @see this.selectClass for parameters - */ - filterColor: function (previewMode, widgetValue, params) { - this.$filter.removeClass(this.filterColorClasses); - if (widgetValue) { - this.$filter.addClass(widgetValue); - } - - var $firstVisibleFilterOpt = this.$filterValueOpts.eq(1); - if (parseFloat(this.$filter.css('opacity')) < parseFloat($firstVisibleFilterOpt.data('filterValue'))) { - this.filterValue(previewMode, $firstVisibleFilterOpt.data('filterValue'), $firstVisibleFilterOpt); - } - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @private - */ - updateUI: async function () { - await this._super(...arguments); - - // Update saving dataset - this.$target[0].dataset.coverClass = this.$el.find('[data-cover-opt-name="size"] .active').data('selectClass') || ''; - this.$target[0].dataset.textSizeClass = this.$el.find('[data-cover-opt-name="text_size"] .active').data('selectClass') || ''; - this.$target[0].dataset.textAlignClass = this.$el.find('[data-cover-opt-name="text_align"] .active').data('selectClass') || ''; - this.$target[0].dataset.filterValue = this.$filterValueOpts.filter('.active').data('filterValue') || 0.0; - this.$target[0].dataset.filterColor = this.$filterColorOpts.filter('.active').data('filterColor') || ''; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _computeWidgetState: function (methodName, params) { - switch (methodName) { - case 'filterValue': { - return parseFloat(this.$filter.css('opacity')).toFixed(1); - } - case 'filterColor': { - const classes = this.filterColorClasses.split(' '); - for (const className of classes) { - if (this.$filter.hasClass(className)) { - return className; - } - } - return ''; - } - } - return this._super(...arguments); - }, - /** - * @override - */ - _computeWidgetVisibility: function (widgetName, params) { - const hasCover = this.$target.hasClass('o_record_has_cover'); - if (widgetName === 'remove_cover_opt') { - return hasCover; - } else if (params.coverOptName) { - var notAllowed = (this.$target.data(`use_${params.coverOptName}`) !== 'True'); - return (hasCover && !notAllowed); - } - return this._super(...arguments); - }, -}); - -/** - * Whether the section should be full-width (container-fluid) or use a classic container - */ -options.registry.SectionStretch = options.Class.extend({ - /** - * Only shows this option if it changes the visual. - * - * @override - */ - start: function () { - const $container = $('<div>', {class: 'container'}).insertAfter(this.$target); - const sizeDifference = this.$target.parent().width() / $container.outerWidth() - 1; - $container.remove(); - // The cutoff for the option is 5% difference in width - if (sizeDifference < 0.05) { - this.$el.addClass('d-none'); - } - - return this._super.apply(this, arguments); - }, -}); - -/** - * Allows snippets to be moved before the preceding element or after the following. - */ -options.registry.SnippetMove = options.Class.extend({ - /** - * @override - */ - start: function () { - var $buttons = this.$el.find('we-button'); - var $overlayArea = this.$overlay.find('.o_overlay_options'); - $overlayArea.prepend($buttons[0]); - $overlayArea.append($buttons[1]); - - return this._super(...arguments); - }, - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Moves the snippet around. - * - * @see this.selectClass for parameters - */ - moveSnippet: function (previewMode, widgetValue, params) { - switch (widgetValue) { - case 'prev': - this.$target.prev().before(this.$target); - break; - case 'next': - this.$target.next().after(this.$target); - break; - } - }, -}); - -options.registry.InnerChart = options.Class.extend({ - custom_events: _.extend({}, options.Class.prototype.custom_events, { - 'get_custom_colors': '_onGetCustomColors', - }), - events: _.extend({}, options.Class.prototype.events, { - 'click we-button.add_column': '_onAddColumnClick', - 'click we-button.add_row': '_onAddRowClick', - 'click we-button.o_we_matrix_remove_col': '_onRemoveColumnClick', - 'click we-button.o_we_matrix_remove_row': '_onRemoveRowClick', - 'blur we-matrix input': '_onMatrixInputFocusOut', - 'focus we-matrix input': '_onMatrixInputFocus', - }), - - /** - * @override - */ - init: function () { - this._super.apply(this, arguments); - this.themeArray = ['alpha', 'beta', 'gamma', 'delta', 'epsilon']; - this.style = window.getComputedStyle(document.documentElement); - }, - /** - * @override - */ - start: function () { - this.backSelectEl = this.el.querySelector('[data-name="chart_bg_color_opt"]'); - this.borderSelectEl = this.el.querySelector('[data-name="chart_border_color_opt"]'); - - // Build matrix content - this.tableEl = this.el.querySelector('we-matrix table'); - const data = JSON.parse(this.$target[0].dataset.data); - data.labels.forEach(el => { - this._addRow(el); - }); - data.datasets.forEach((el, i) => { - if (this._isPieChart()) { - // Add header colors in case the user changes the type of graph - const headerBackgroundColor = this.themeArray[i] || this._randomColor(); - const headerBorderColor = this.themeArray[i] || this._randomColor(); - this._addColumn(el.label, el.data, headerBackgroundColor, headerBorderColor, el.backgroundColor, el.borderColor); - } else { - this._addColumn(el.label, el.data, el.backgroundColor, el.borderColor); - } - }); - this._displayRemoveColButton(); - this._displayRemoveRowButton(); - this._setDefaultSelectedInput(); - return this._super(...arguments); - }, - /** - * @override - */ - updateUI: async function () { - // Selected input might not be in dom anymore if col/row removed - // Done before _super because _computeWidgetState of colorChange - if (!this.lastEditableSelectedInput.closest('table') || this.colorPaletteSelectedInput && !this.colorPaletteSelectedInput.closest('table')) { - this._setDefaultSelectedInput(); - } - - await this._super(...arguments); - - // prevent the columns from becoming too small. - this.tableEl.classList.toggle('o_we_matrix_five_col', this.tableEl.querySelectorAll('tr:first-child th').length > 5); - - this.backSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Background Color") : _t("Dataset Background Color"); - this.borderSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Border Color") : _t("Dataset Border Color"); - - // Dataset/Cell color - this.tableEl.querySelectorAll('input').forEach(el => el.style.border = ''); - const selector = this._isPieChart() ? 'td input' : 'tr:first-child input'; - this.tableEl.querySelectorAll(selector).forEach(el => { - const color = el.dataset.backgroundColor || el.dataset.borderColor; - if (color) { - el.style.border = '2px solid'; - el.style.borderColor = ColorpickerDialog.isCSSColor(color) ? color : this.style.getPropertyValue(`--${color}`).trim(); - } - }); - }, - - //-------------------------------------------------------------------------- - // Options - //-------------------------------------------------------------------------- - - /** - * Set the color on the selected input. - */ - colorChange: async function (previewMode, widgetValue, params) { - if (widgetValue) { - this.colorPaletteSelectedInput.dataset[params.attributeName] = widgetValue; - } else { - delete this.colorPaletteSelectedInput.dataset[params.attributeName]; - } - await this._reloadGraph(); - // To focus back the input that is edited we have to wait for the color - // picker to be fully reloaded. - await new Promise(resolve => setTimeout(() => { - this.lastEditableSelectedInput.focus(); - resolve(); - })); - }, - /** - * @override - */ - selectDataAttribute: async function (previewMode, widgetValue, params) { - await this._super(...arguments); - // Data might change if going from or to a pieChart. - if (params.attributeName === 'type') { - this._setDefaultSelectedInput(); - await this._reloadGraph(); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - */ - _computeWidgetState: function (methodName, params) { - if (methodName === 'colorChange') { - return this.colorPaletteSelectedInput && this.colorPaletteSelectedInput.dataset[params.attributeName] || ''; - } - return this._super(...arguments); - }, - /** - * @override - */ - _computeWidgetVisibility: function (widgetName, params) { - switch (widgetName) { - case 'stacked_chart_opt': { - return this._getColumnCount() > 1; - } - case 'chart_bg_color_opt': - case 'chart_border_color_opt': { - return !!this.colorPaletteSelectedInput; - } - } - return this._super(...arguments); - }, - /** - * Sets and reloads the data on the canvas if it has changed. - * Used in matrix related method. - * - * @private - */ - _reloadGraph: async function () { - const jsonValue = this._matrixToChartData(); - if (this.$target[0].dataset.data !== jsonValue) { - this.$target[0].dataset.data = jsonValue; - await this._refreshPublicWidgets(); - } - }, - /** - * Return a stringifyed chart.js data object from the matrix - * Pie charts have one color per data while other charts have one color per dataset. - * - * @private - */ - _matrixToChartData: function () { - const data = { - labels: [], - datasets: [], - }; - this.tableEl.querySelectorAll('tr:first-child input').forEach(el => { - data.datasets.push({ - label: el.value || '', - data: [], - backgroundColor: this._isPieChart() ? [] : el.dataset.backgroundColor || '', - borderColor: this._isPieChart() ? [] : el.dataset.borderColor || '', - }); - }); - this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el) => { - const title = el.querySelector('th input').value || ''; - data.labels.push(title); - el.querySelectorAll('td input').forEach((el, i) => { - data.datasets[i].data.push(el.value || 0); - if (this._isPieChart()) { - data.datasets[i].backgroundColor.push(el.dataset.backgroundColor || ''); - data.datasets[i].borderColor.push(el.dataset.borderColor || ''); - } - }); - }); - return JSON.stringify(data); - }, - /** - * Return a td containing a we-button with minus icon - * - * @param {...string} classes Classes to add to the we-button - * @returns {HTMLElement} - */ - _makeDeleteButton: function (...classes) { - const rmbuttonEl = options.buildElement('we-button', null, { - classes: ['fa', 'fa-fw', 'fa-minus', ...classes], - }); - const newEl = document.createElement('td'); - newEl.appendChild(rmbuttonEl); - return newEl; - }, - /** - * Add a column to the matrix - * The th (dataset label) of a column hold the colors for the entire dataset if the graph is not a pie chart - * If the graph is a pie chart the color of the td (data) are used. - * - * @private - * @param {String} title The title of the column - * @param {Array} values The values of the column input - * @param {String} heardeBackgroundColor The background color of the dataset - * @param {String} headerBorderColor The border color of the dataset - * @param {string[]} cellBackgroundColors The background colors of the datas inputs, random color if missing - * @param {string[]} cellBorderColors The border color of the datas inputs, no color if missing - */ - _addColumn: function (title, values, heardeBackgroundColor, headerBorderColor, cellBackgroundColors = [], cellBorderColors = []) { - const firstRow = this.tableEl.querySelector('tr:first-child'); - const headerInput = this._makeCell('th', title, heardeBackgroundColor, headerBorderColor); - firstRow.insertBefore(headerInput, firstRow.lastElementChild); - - this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el, i) => { - const newCell = this._makeCell('td', values ? values[i] : null, cellBackgroundColors[i] || this._randomColor(), cellBorderColors[i - 1]); - el.insertBefore(newCell, el.lastElementChild); - }); - - const lastRow = this.tableEl.querySelector('tr:last-child'); - const removeButton = this._makeDeleteButton('o_we_matrix_remove_col'); - lastRow.appendChild(removeButton); - }, - /** - * Add a row to the matrix - * The background color of the datas are random - * - * @private - * @param {String} tilte The title of the row - */ - _addRow: function (tilte) { - const trEl = document.createElement('tr'); - trEl.appendChild(this._makeCell('th', tilte)); - this.tableEl.querySelectorAll('tr:first-child input').forEach(() => { - trEl.appendChild(this._makeCell('td', null, this._randomColor())); - }); - trEl.appendChild(this._makeDeleteButton('o_we_matrix_remove_row')); - const tbody = this.tableEl.querySelector('tbody'); - tbody.insertBefore(trEl, tbody.lastElementChild); - }, - /** - * @private - * @param {string} tag tag of the HTML Element (td/th) - * @param {string} value The current value of the cell input - * @param {string} backgroundColor The background Color of the data on the graph - * @param {string} borderColor The border Color of the the data on the graph - * @returns {HTMLElement} - */ - _makeCell: function (tag, value, backgroundColor, borderColor) { - const newEl = document.createElement(tag); - const contentEl = document.createElement('input'); - contentEl.type = 'text'; - contentEl.value = value || ''; - if (backgroundColor) { - contentEl.dataset.backgroundColor = backgroundColor; - } - if (borderColor) { - contentEl.dataset.borderColor = borderColor; - } - newEl.appendChild(contentEl); - return newEl; - }, - /** - * Display the remove button coresponding to the colIndex - * - * @private - * @param {Int} colIndex Can be undefined, if so the last remove button of the column will be shown - */ - _displayRemoveColButton: function (colIndex) { - if (this._getColumnCount() > 1) { - this._displayRemoveButton(colIndex, 'o_we_matrix_remove_col'); - } - }, - /** - * Display the remove button coresponding to the rowIndex - * - * @private - * @param {Int} rowIndex Can be undefined, if so the last remove button of the row will be shown - */ - _displayRemoveRowButton: function (rowIndex) { - //Nbr of row minus header and button - const rowCount = this.tableEl.rows.length - 2; - if (rowCount > 1) { - this._displayRemoveButton(rowIndex, 'o_we_matrix_remove_row'); - } - }, - /** - * @private - * @param {Int} tdIndex Can be undefined, if so the last remove button will be shown - * @param {String} btnClass Either o_we_matrix_remove_col or o_we_matrix_remove_row - */ - _displayRemoveButton: function (tdIndex, btnClass) { - const removeBtn = this.tableEl.querySelectorAll(`td we-button.${btnClass}`); - removeBtn.forEach(el => el.style.display = ''); //hide all - const index = tdIndex < removeBtn.length ? tdIndex : removeBtn.length - 1; - removeBtn[index].style.display = 'inline-block'; - }, - /** - * @private - * @return {boolean} - */ - _isPieChart: function () { - return ['pie', 'doughnut'].includes(this.$target[0].dataset.type); - }, - /** - * Return the number of column minus header and button - * @private - * @return {integer} - */ - _getColumnCount: function () { - return this.tableEl.rows[0].cells.length - 2; - }, - /** - * Select the first data input - * - * @private - */ - _setDefaultSelectedInput: function () { - this.lastEditableSelectedInput = this.tableEl.querySelector('td input'); - if (this._isPieChart()) { - this.colorPaletteSelectedInput = this.lastEditableSelectedInput; - } else { - this.colorPaletteSelectedInput = this.tableEl.querySelector('th input'); - } - }, - /** - * Return a random hexadecimal color. - * - * @private - * @return {string} - */ - _randomColor: function () { - return '#' + ('00000' + (Math.random() * (1 << 24) | 0).toString(16)).slice(-6).toUpperCase(); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Used by colorPalette to retrieve the custom colors used on the chart - * Make an array with all the custom colors used on the chart - * and apply it to the onSuccess method provided by the trigger_up. - * - * @private - */ - _onGetCustomColors: function (ev) { - const data = JSON.parse(this.$target[0].dataset.data || ''); - let customColors = []; - data.datasets.forEach(el => { - if (this._isPieChart()) { - customColors = customColors.concat(el.backgroundColor).concat(el.borderColor); - } else { - customColors.push(el.backgroundColor); - customColors.push(el.borderColor); - } - }); - customColors = customColors.filter((el, i, array) => { - return !this.style.getPropertyValue(`--${el}`) && array.indexOf(el) === i && el !== ''; // unique non class not transparent - }); - ev.data.onSuccess(customColors); - }, - /** - * Add a row at the end of the matrix and display it's remove button - * Choose the color of the column from the theme array or a random color if they are already used - * - * @private - */ - _onAddColumnClick: function () { - const usedColor = Array.from(this.tableEl.querySelectorAll('tr:first-child input')).map(el => el.dataset.backgroundColor); - const color = this.themeArray.filter(el => !usedColor.includes(el))[0] || this._randomColor(); - this._addColumn(null, null, color, color); - this._reloadGraph().then(() => { - this._displayRemoveColButton(); - this.updateUI(); - }); - }, - /** - * Add a column at the end of the matrix and display it's remove button - * - * @private - */ - _onAddRowClick: function () { - this._addRow(); - this._reloadGraph().then(() => { - this._displayRemoveRowButton(); - this.updateUI(); - }); - }, - /** - * Remove the column and show the remove button of the next column or the last if no next. - * - * @private - * @param {Event} ev - */ - _onRemoveColumnClick: function (ev) { - const cell = ev.currentTarget.parentElement; - const cellIndex = cell.cellIndex; - this.tableEl.querySelectorAll('tr').forEach((el) => { - el.children[cellIndex].remove(); - }); - this._displayRemoveColButton(cellIndex - 1); - this._reloadGraph().then(() => { - this.updateUI(); - }); - }, - /** - * Remove the row and show the remove button of the next row or the last if no next. - * - * @private - * @param {Event} ev - */ - _onRemoveRowClick: function (ev) { - const row = ev.currentTarget.parentElement.parentElement; - const rowIndex = row.rowIndex; - row.remove(); - this._displayRemoveRowButton(rowIndex - 1); - this._reloadGraph().then(() => { - this.updateUI(); - }); - }, - /** - * @private - * @param {Event} ev - */ - _onMatrixInputFocusOut: function (ev) { - // Sometimes, an input is focusout for internal reason (like an undo - // recording) then focused again manually in the same JS stack - // execution. In that case, the blur should not trigger an option - // selection as the user did not leave the input. We thus defer the blur - // handling to then check that the target is indeed still blurred before - // executing the actual option selection. - setTimeout(() => { - if (ev.currentTarget === document.activeElement) { - return; - } - this._reloadGraph(); - }); - }, - /** - * Set the selected cell/header and display the related remove button - * - * @private - * @param {Event} ev - */ - _onMatrixInputFocus: function (ev) { - this.lastEditableSelectedInput = ev.target; - const col = ev.target.parentElement.cellIndex; - const row = ev.target.parentElement.parentElement.rowIndex; - if (this._isPieChart()) { - this.colorPaletteSelectedInput = ev.target.parentNode.tagName === 'TD' ? ev.target : null; - } else { - this.colorPaletteSelectedInput = this.tableEl.querySelector(`tr:first-child th:nth-of-type(${col + 1}) input`); - } - if (col > 0) { - this._displayRemoveColButton(col - 1); - } - if (row > 0) { - this._displayRemoveRowButton(row - 1); - } - this.updateUI(); - }, -}); - -const InputUserValueWidget = options.userValueWidgetsRegistry['we-input']; -const UrlPickerUserValueWidget = InputUserValueWidget.extend({ - custom_events: _.extend({}, InputUserValueWidget.prototype.custom_events || {}, { - website_url_chosen: '_onWebsiteURLChosen', - }), - - /** - * @override - */ - start: async function () { - await this._super(...arguments); - $(this.inputEl).addClass('text-left'); - wUtils.autocompleteWithPages(this, $(this.inputEl)); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the autocomplete change the input value. - * - * @private - */ - _onWebsiteURLChosen: function () { - $(this.inputEl).trigger('input'); - this._notifyValueChange(false); - }, -}); - -options.userValueWidgetsRegistry['we-urlpicker'] = UrlPickerUserValueWidget; -return { - UrlPickerUserValueWidget: UrlPickerUserValueWidget, -}; -}); |
