diff options
Diffstat (limited to 'addons/survey/static/src/js/survey_session_leaderboard.js')
| -rw-r--r-- | addons/survey/static/src/js/survey_session_leaderboard.js | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/addons/survey/static/src/js/survey_session_leaderboard.js b/addons/survey/static/src/js/survey_session_leaderboard.js new file mode 100644 index 00000000..7d4ad6b8 --- /dev/null +++ b/addons/survey/static/src/js/survey_session_leaderboard.js @@ -0,0 +1,335 @@ +odoo.define('survey.session_leaderboard', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var SESSION_CHART_COLORS = require('survey.session_colors'); + +publicWidget.registry.SurveySessionLeaderboard = publicWidget.Widget.extend({ + init: function (parent, options) { + this._super.apply(this, arguments); + + this.surveyAccessToken = options.surveyAccessToken; + this.$sessionResults = options.sessionResults; + + this.BAR_MIN_WIDTH = '3rem'; + this.BAR_WIDTH = '24rem'; + this.BAR_HEIGHT = '3.8rem'; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Shows the question leaderboard on screen. + * It's based on the attendees score (descending). + * + * We fade out the $sessionResults to fade in our rendered template. + * + * The width of the progress bars is set after the rendering to enable a width css animation. + */ + showLeaderboard: function (fadeOut, isScoredQuestion) { + var self = this; + + var resolveFadeOut; + var fadeOutPromise; + if (fadeOut) { + fadeOutPromise = new Promise(function (resolve, reject) { resolveFadeOut = resolve; }); + self.$sessionResults.fadeOut(400, function () { + resolveFadeOut(); + }); + } else { + fadeOutPromise = Promise.resolve(); + self.$sessionResults.hide(); + self.$('.o_survey_session_leaderboard_container').empty(); + } + + var leaderboardPromise = this._rpc({ + route: _.str.sprintf('/survey/session/leaderboard/%s', this.surveyAccessToken) + }); + + Promise.all([fadeOutPromise, leaderboardPromise]).then(function (results) { + var leaderboardResults = results[1]; + var $renderedTemplate = $(leaderboardResults); + self.$('.o_survey_session_leaderboard_container').append($renderedTemplate); + + self.$('.o_survey_session_leaderboard_item').each(function (index) { + var rgb = SESSION_CHART_COLORS[index % 10]; + $(this) + .find('.o_survey_session_leaderboard_bar') + .css('background-color', `rgba(${rgb},1)`); + $(this) + .find('.o_survey_session_leaderboard_bar_question') + .css('background-color', `rgba(${rgb},${0.4})`); + }); + + self.$el.fadeIn(400, async function () { + if (isScoredQuestion) { + await self._prepareScores(); + await self._showQuestionScores(); + await self._sumScores(); + await self._reorderScores(); + } + }); + }); + }, + + /** + * Inverse the process, fading out our template to fade int the $sessionResults. + */ + hideLeaderboard: function () { + var self = this; + this.$el.fadeOut(400, function () { + self.$('.o_survey_session_leaderboard_container').empty(); + self.$sessionResults.fadeIn(400); + }); + }, + + /** + * This method animates the passed jQuery element from 0 points to {totalScore} points. + * It will create a nice "animated" effect of a counter increasing by {increment} until it + * reaches the actual score. + * + * @param {$.Element} $scoreEl the element to animate + * @param {Integer} currentScore the currently displayed score + * @param {Integer} totalScore to total score to animate to + * @param {Integer} increment the base increment of each animation iteration + * @param {Boolean} plusSign wether or not we add a "+" before the score + * @private + */ + _animateScoreCounter: function ($scoreEl, currentScore, totalScore, increment, plusSign) { + var self = this; + setTimeout(function () { + var nextScore = currentScore + increment; + if (nextScore > totalScore) { + nextScore = totalScore; + } + $scoreEl.text(`${plusSign ? '+ ' : ''}${Math.round(nextScore)} p`); + + if (nextScore < totalScore) { + self._animateScoreCounter($scoreEl, nextScore, totalScore, increment, plusSign); + } + }, 25); + }, + + /** + * Helper to move a score bar from its current position in the leaderboard + * to a new position. + * + * @param {$.Element} $score the score bar to move + * @param {Integer} position the new position in the leaderboard + * @param {Integer} offset an offset in 'rem' + * @param {Integer} timeout time to wait while moving before resolving the promise + */ + _animateMoveTo: function ($score, position, offset, timeout) { + var animationDone; + var animationPromise = new Promise(function (resolve) { + animationDone = resolve; + }); + $score.css('top', `calc(calc(${this.BAR_HEIGHT} * ${position}) + ${offset}rem)`); + setTimeout(animationDone, timeout); + return animationPromise; + }, + + /** + * Takes the leaderboard prior to the current question results + * and reduce all scores bars to a small width (3rem). + * We keep the small score bars on screen for 1s. + * + * This visually prepares the display of points for the current question. + * + * @private + */ + _prepareScores: function () { + var self = this; + var animationDone; + var animationPromise = new Promise(function (resolve) { + animationDone = resolve; + }); + setTimeout(function () { + this.$('.o_survey_session_leaderboard_bar').each(function () { + var currentScore = parseInt($(this) + .closest('.o_survey_session_leaderboard_item') + .data('currentScore')) + if (currentScore && currentScore !== 0) { + $(this).css('transition', `width 1s cubic-bezier(.4,0,.4,1)`); + $(this).css('width', self.BAR_MIN_WIDTH); + } + }); + setTimeout(animationDone, 1000); + }, 300); + + return animationPromise; + }, + + /** + * Now that we have summed the score for the current question to the total score + * of the user and re-weighted the bars accordingly, we need to re-order everything + * to match the new ranking. + * + * In addition to moving the bars to their new position, we create a "bounce" effect + * by moving the bar a little bit more to the top or bottom (depending on if it's moving up + * the ranking or down), the moving it the other way around, then moving it to its final + * position. + * + * (Feels complicated when explained but it's fairly simple once you see what it does). + * + * @private + */ + _reorderScores: function () { + var self = this; + var animationDone; + var animationPromise = new Promise(function (resolve) { + animationDone = resolve; + }); + setTimeout(function () { + self.$('.o_survey_session_leaderboard_item').each(async function () { + var $score = $(this); + var currentPosition = parseInt($(this).data('currentPosition')); + var newPosition = parseInt($(this).data('newPosition')); + if (currentPosition !== newPosition) { + var offset = newPosition > currentPosition ? 2 : -2; + await self._animateMoveTo($score, newPosition, offset, 300); + $score.css('transition', 'top ease-in-out .1s'); + await self._animateMoveTo($score, newPosition, offset * -0.3, 100); + await self._animateMoveTo($score, newPosition, 0, 0); + animationDone(); + } + }); + }, 1800); + + return animationPromise; + }, + + /** + * Will display the score for the current question. + * We simultaneously: + * - increase the width of "question bar" + * (faded out bar right next to the global score one) + * - animate the score for the question (ex: from + 0 p to + 40 p) + * + * (We keep a minimum width of 3rem to be able to display '+30 p' within the bar). + * + * @private + */ + _showQuestionScores: function () { + var self = this; + var animationDone; + var animationPromise = new Promise(function (resolve) { + animationDone = resolve; + }); + setTimeout(function () { + this.$('.o_survey_session_leaderboard_bar_question').each(function () { + var $barEl = $(this); + var width = `calc(calc(100% - ${self.BAR_WIDTH}) * ${$barEl.data('widthRatio')} + ${self.BAR_MIN_WIDTH})`; + $barEl.css('transition', 'width 1s ease-out'); + $barEl.css('width', width); + + var $scoreEl = $barEl + .find('.o_survey_session_leaderboard_bar_question_score') + .text('0 p'); + var questionScore = parseInt($barEl.data('questionScore')); + if (questionScore && questionScore > 0) { + var increment = parseInt($barEl.data('maxQuestionScore') / 40); + if (!increment || increment === 0){ + increment = 1; + } + $scoreEl.text('+ 0 p'); + console.log($barEl.data('maxQuestionScore')); + setTimeout(function () { + self._animateScoreCounter( + $scoreEl, + 0, + questionScore, + increment, + true); + }, 400); + } + setTimeout(animationDone, 1400); + }); + }, 300); + + return animationPromise; + }, + + /** + * After displaying the score for the current question, we sum the total score + * of the user so far with the score of the current question. + * + * Ex: + * We have ('#' for total score before question and '=' for current question score): + * 210 p ####=================================== +30 p John + * We want: + * 240 p ###################################==== +30 p John + * + * Of course, we also have to weight the bars based on the maximum score. + * So if John here has 50% of the points of the leader user, both the question score bar + * and the total score bar need to have their width divided by 2: + * 240 p ##################== +30 p John + * + * The width of both bars move at the same time to reach their new position, + * with an animation on the width property. + * The new width of the "question bar" should represent the ratio of won points + * when compared to the total points. + * (We keep a minimum width of 3rem to be able to display '+30 p' within the bar). + * + * The updated total score is animated towards the new value. + * we keep this on screen for 500ms before reordering the bars. + * + * @private + */ + _sumScores: function () { + var self = this; + var animationDone; + var animationPromise = new Promise(function (resolve) { + animationDone = resolve; + }); + // values that felt the best after a lot of testing + var growthAnimation = 'cubic-bezier(.5,0,.66,1.11)'; + setTimeout(function () { + this.$('.o_survey_session_leaderboard_item').each(function () { + var currentScore = parseInt($(this).data('currentScore')); + var updatedScore = parseInt($(this).data('updatedScore')); + var increment = parseInt($(this).data('maxQuestionScore') / 40); + if (!increment || increment === 0){ + increment = 1; + } + self._animateScoreCounter( + $(this).find('.o_survey_session_leaderboard_score'), + currentScore, + updatedScore, + increment, + false); + + var maxUpdatedScore = parseInt($(this).data('maxUpdatedScore')); + var baseRatio = updatedScore / maxUpdatedScore; + var questionScore = parseInt($(this).data('questionScore')); + var questionRatio = questionScore / + (updatedScore && updatedScore !== 0 ? updatedScore : 1); + // we keep a min fixed with of 3rem to be able to display "+ 5 p" + // even if the user already has 1.000.000 points + var questionWith = `calc(calc(calc(100% - ${self.BAR_WIDTH}) * ${questionRatio * baseRatio}) + ${self.BAR_MIN_WIDTH})`; + $(this) + .find('.o_survey_session_leaderboard_bar_question') + .css('transition', `width ease .5s ${growthAnimation}`) + .css('width', questionWith); + + var updatedScoreRatio = 1 - questionRatio; + var updatedScoreWidth = `calc(calc(100% - ${self.BAR_WIDTH}) * ${updatedScoreRatio * baseRatio})`; + $(this) + .find('.o_survey_session_leaderboard_bar') + .css('min-width', '0px') + .css('transition', `width ease .5s ${growthAnimation}`) + .css('width', updatedScoreWidth); + + setTimeout(animationDone, 500); + }); + }, 1400); + + return animationPromise; + } +}); + +return publicWidget.registry.SurveySessionLeaderboard; + +}); |
