summaryrefslogtreecommitdiff
path: root/addons/website/static/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/website/static/src/js')
-rw-r--r--addons/website/static/src/js/content/snippets.animation.js1668
-rw-r--r--addons/website/static/src/js/editor/snippets.options.js2528
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">&nbsp;</div>' +
- '<div class="media_iframe_video_size">&nbsp;</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,
-};
-});