summaryrefslogtreecommitdiff
path: root/addons/survey/static/src/js/survey_session_leaderboard.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/survey/static/src/js/survey_session_leaderboard.js')
-rw-r--r--addons/survey/static/src/js/survey_session_leaderboard.js335
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;
+
+});