Rev 5 | AutorÃa | Ultima modificación | Ver Log |
// This file is part of Moodle - https://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Module to display and manage reactions and difficulty tracks on course page.* @copyright 2020 Quentin Fombaron, 2021 Astor Bizard* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/define(['jquery', 'core/ajax', 'core/notification'], function($, ajax, notification) {/*** Call a function each time a course section is loaded.* @param {Function} call Function to call.*/function callOnModulesListLoad(call) {call();// The following listener is needed for the Tiles course format, where sections are loaded on demand.$(document).ajaxComplete(function(event, xhr, settings) {if (typeof (settings.data) !== 'string') {return;}try {var data = JSON.parse(settings.data);if (data.length == 0 || typeof (data[0].methodname) === 'undefined') {return;}if (data[0].methodname == 'format_tiles_get_single_section_page_html' // Tile load.|| data[0].methodname == 'format_tiles_log_tile_click') { // Tile load, cached.call();}} catch (e) {// Something went wrong, it may not even be JSON. It is fine, this means it was not the call we were expecting.return;}});}/*** Set up difficulty tracks on course modules.* @param {Array} difficultyLevels Array of difficulty tracks, one entry for each course module.* @param {Array} trackColors Tracks colors, from block plugin configuration.* @param {Number|null} cmid The course module ID, if this page is a module view.*/function setUpDifficultyTracks(difficultyLevels, trackColors, cmid) {difficultyLevels.forEach(function(module) {var difficultyLevel = parseInt(module.difficultyLevel);var title = '';if (difficultyLevel > 0) {var track = ['greentrack', 'bluetrack', 'redtrack', 'blacktrack'][difficultyLevel - 1];title = M.util.get_string(track, 'block_point_view');}var $track = $('<div>', {'class': 'block_point_view track','title': title,'style': 'background-color: ' + trackColors[difficultyLevel] + ';'});// Decide where to put the track.var $container = $('#module-' + module.id + ' .activitytitle');if ($container.closest('.activity-grid').length) {// Moodle 4.3+.$container = $container.closest('.activity-grid');}if ($container.length === 0) {// This seems to be a label.$container = $('#module-' + module.id + ' .activity-item .description,' +'#module-' + module.id + ' .activity-item .activity-altcontent').first();}// Add the track.if ($container.find('.block_point_view.track').length === 0) {$container.prepend($track);}// If there is indentation, move the track after it.$container.find('.mod-indent').after($track);if (cmid === module.id) {// This is a module page, add the track to the title.$('.page-context-header').prepend($track);}});}/*** Get a jQuery object in reaction zone for given module ID.* @param {Number} moduleId Module ID.* @param {String} selector (optional) Sub-selector.* @return {jQuery} If selector was provided, the corresponding jQuery object within the reaction zone.* If not, the reaction zone jQuery object.*/function $get(moduleId, selector) {var $element = $('#module-' + moduleId + ' .block_point_view.reactions-container');if (typeof (selector) === 'undefined') {return $element;} else {return $element.find(selector);}}// Enumeration of the possible reactions.var Reactions = {none: 0,easy: 1,better: 2,hard: 3};// Array of Reaction of the user for the activity.var reactionVotedArray = {};/*** Set up difficulty tracks on course modules.* @param {Number} courseId Course ID.* @param {Array} modulesWithReactions Array of reactions state, one entry for each course module with reactions enabled.* @param {String} reactionsHtml HTML fragment for reactions.* @param {Array} pixSrc Array of pictures sources for group images.* @param {Number|null} cmid The course module ID, if this page is a module view.*/function setUpReactions(courseId, modulesWithReactions, reactionsHtml, pixSrc, cmid) {// For each selected module, create a reaction zone.modulesWithReactions.forEach(function(module) {var moduleId = parseInt(module.cmid);var uservote = parseInt(module.uservote);// Initialise reactionVotedArray.reactionVotedArray[moduleId] = uservote;if (module.cmid === cmid) {// Simulate an activity row on course page (so we treat it the same for what's next).$('<div id="module-' + moduleId + '" class="activity-wrapper mr-5" style="width: 165px;">').insertAfter($('.header-actions-container')).prepend('<div class="activity-instance">');}if ($('#module-' + moduleId).length === 1 && $get(moduleId).length === 0) {// Add the reaction zone to the module.var $module = $('#module-' + moduleId);if ($module.is('.modtype_label')) {// Label.$module.find('.description, .activity-grid').first().before(reactionsHtml);} else if ($module.find('.tiles-activity-container').length) {// Tiles format.$module.find('.tiles-activity-container').after(reactionsHtml);} else {$module.find('.activity-instance').after(reactionsHtml);}// Setup reaction change.var reactionsLock = false;$get(moduleId, '.reaction img').click(function() {// Use a mutex to avoid query / display inconsistencies.// This is not a perfect mutex, but is actually enough for our needs.if (reactionsLock === false) {reactionsLock = true;reactionChange(courseId, moduleId, $(this).data('reactionname')).always(function() {reactionsLock = false;updateGroupImgAndNb(moduleId, pixSrc);});}});// Initialize reactions state.$get(moduleId, '.reactionnb').each(function() {var reactionName = $(this).data('reactionname');var reactionNb = parseInt(module['total' + reactionName]);updateReactionNb(moduleId, reactionName, reactionNb, uservote === Reactions[reactionName]);});updateGroupImgAndNb(moduleId, pixSrc);// Setup animations.setupReactionsAnimation(moduleId, pixSrc);}});}/*** Manage a reaction change (user added, removed or updated their vote).* @param {Number} courseId Course ID.* @param {Number} moduleId Module ID.* @param {String} reactionName The reaction being clicked.* @returns {Promise} A promise, result of the change operations (ajax call and UI update).*/function reactionChange(courseId, moduleId, reactionName) {var reactionSelect = Reactions[reactionName];var previousReaction = reactionVotedArray[moduleId];// If the reaction being clicked is the current one, it is a vote remove.var newVote = (reactionSelect === previousReaction) ? Reactions.none : reactionSelect;return ajax.call([{methodname: 'block_point_view_update_db',args: {func: 'update',courseid: courseId,cmid: moduleId,vote: newVote}}])[0].done(function() {reactionVotedArray[moduleId] = newVote; // Set current reaction.if (previousReaction !== Reactions.none) {// User canceled their vote (or updated to another one).var previousReactionName = ['', 'easy', 'better', 'hard'][previousReaction];updateReactionNb(moduleId, previousReactionName, -1, false);}if (newVote !== Reactions.none) {// User added or updated their vote.updateReactionNb(moduleId, reactionName, +1, true); // Add new vote.}}).fail(notification.exception);}/*** Update the reactions group image and total number according to current votes.* @param {Number} moduleId Module ID.* @param {Array} pix Array of pictures sources for group images.*/function updateGroupImgAndNb(moduleId, pix) {// Build group image name.var groupImg = 'group_';var totalNb = 0;$get(moduleId, '.reactionnb').each(function() {var reactionNb = parseInt($(this).text());if (reactionNb > 0) {groupImg += $(this).data('reactionname').toUpperCase().charAt(0); // Add E, B or H.}totalNb += reactionNb;});// Modify the image source of the reaction group.$get(moduleId, '.group_img').attr('src', pix[groupImg]);// Update the total number of votes.var $groupNbWrapper = $get(moduleId, '.group_nb');var $groupNb = $groupNbWrapper.find('span');$groupNb.text(totalNb).attr('title', M.util.get_string('totalreactions', 'block_point_view', totalNb));$groupNbWrapper.toggleClass('invisible', totalNb === 0).toggleClass('voted', reactionVotedArray[moduleId] !== Reactions.none);// Adjust the size to fit within a fixed space (useful for the green dot).var digits = Math.min(('' + totalNb).length, 5);$groupNb.css({'right': Math.max(0.25 * (digits - 2), 0) + 'em','transform': 'scaleX(' + (1.0 + 0.03 * digits * digits - 0.35 * digits + 0.34) + ')'});}/*** Update a reaction number of votes.* @param {Number} moduleId Module ID.* @param {String} reactionName The reaction to update the number of.* @param {Number} diff Difference to apply (e.g. +1 for adding a vote, -1 for removing a vote).* @param {Boolean} isSelected Whether the reaction we are updating is the one now selected by user.*/function updateReactionNb(moduleId, reactionName, diff, isSelected) {var $reactionNb = $get(moduleId, '.reactionnb[data-reactionname="' + reactionName + '"]');var nbReaction = parseInt($reactionNb.text()) + diff;$reactionNb.text(nbReaction).toggleClass('nbselected', isSelected);$get(moduleId, '.reaction img[data-reactionname="' + reactionName + '"]').toggleClass('novote', nbReaction === 0);}/*** Set up animations to swap between reactions preview and vote interface.* @param {Number} moduleId Module ID.*/function setupReactionsAnimation(moduleId) {// Helpers to resize images for animations.var reactionImageSizeForRatio = function(ratio) {return {top: 15 - (10 * ratio),left: 10 - (10 * ratio),height: 20 * ratio};};var groupImageSizeForRatio = function(ratio) {return {left: -10 + (10 * ratio),height: 20 * ratio};};// Animation sequence to hide reactions preview and show vote interface.var showReactions = function(moduleId) {$get(moduleId, '.reactions').removeClass('invisible');$get(moduleId, '.group_img').css({'pointer-events': 'none'}).animate(groupImageSizeForRatio(0), 300).hide(0);$get(moduleId, '.group_nb').delay(200).hide(300);$('#module-' + moduleId + ' button[data-action="toggle-manual-completion"],' +'#module-' + moduleId + ' .activity-info .automatic-completion-conditions > span.badge:first-of-type,' +'#module-' + moduleId + ' .activity-information [data-region="completionrequirements"]').delay(200).queue(function(next) {// Use opacity transition for a smooth hiding.$(this).css({opacity: 0,transition: 'opacity 0.3s ease-in-out'});next();}).delay(300).queue(function(next) {// Actually make the element invisible to avoid accidental clicking on transparent element.$(this).addClass('invisible');next();});['easy', 'better', 'hard'].forEach(function(reaction, index) {var delay = 50 + 150 * index; // Easy: 50, better: 200, hard: 350.$get(moduleId, '.reaction img[data-reactionname="' + reaction + '"]').delay(delay).animate(reactionImageSizeForRatio(1), 300).css({'pointer-events': 'auto'});$get(moduleId, '.reactionnb[data-reactionname="' + reaction + '"]').delay(delay + 300).queue(function(next) {$(this).removeClass('invisible');next();});});};// Animation sequence to hide vote interface and show reaction preview.var hideReactions = function(moduleId) {['hard', 'better', 'easy'].forEach(function(reaction, index) {var delay = 50 + 250 * index; // Hard: 50, better: 300, easy: 550.$get(moduleId, '.reaction img[data-reactionname="' + reaction + '"]').css({'pointer-events': 'none'}).delay(delay).animate(reactionImageSizeForRatio(0), 500);$get(moduleId, '.reactionnb[data-reactionname="' + reaction + '"]').delay(delay).queue(function(next) {$(this).addClass('invisible');next();});});// Show the reaction group image with nice animation.$get(moduleId, '.group_img').delay(500).show(0).animate(groupImageSizeForRatio(1), 300).queue(function(next) {$get(moduleId, '.reactions').addClass('invisible');next();}).css({'pointer-events': 'auto'});$get(moduleId, '.group_nb').delay(600).show(0);$('#module-' + moduleId + ' button[data-action="toggle-manual-completion"],' +'#module-' + moduleId + ' .activity-info .automatic-completion-conditions > span.badge:first-of-type,' +'#module-' + moduleId + ' .activity-information [data-region="completionrequirements"]').delay(600).queue(function(next) {$(this).removeClass('invisible');// Use opacity transition for a smooth showing back.$(this).css({opacity: 1,transition: 'opacity 0.3s ease-in-out'});next();});};// Setup some timeouts and locks to trigger animations.var reactionsVisible = false;var groupTimeout = null;var reactionsTimeout = null;var triggerHideReactions = function() {reactionsTimeout = null;reactionsVisible = false;hideReactions(moduleId);};var triggerShowReactions = function() {groupTimeout = null;reactionsVisible = true;showReactions(moduleId);clearTimeout(reactionsTimeout);reactionsTimeout = setTimeout(triggerHideReactions, 2000); // Hide reactions after 2 seconds if mouse is already out.};// Reactions preview interactions.$get(moduleId, '.group_img').mouseover(function() {$(this).stop().animate(groupImageSizeForRatio(1.15), 100); // Widen image a little on hover.groupTimeout = setTimeout(triggerShowReactions, 300); // Show vote interface after 0.3s hover.}).mouseout(function() {if (!reactionsVisible) {// Cancel mouseover actions.clearTimeout(groupTimeout);$(this).stop().animate(groupImageSizeForRatio(1), 100);}}).click(triggerShowReactions); // Show vote interface instantly on click.// Reactions images interactions.$get(moduleId, '.reaction img').mouseover(function() {$(this).stop().animate(reactionImageSizeForRatio(2), 100); // Widen image a little on hover.}).mouseout(function() {$(this).stop().animate(reactionImageSizeForRatio(1), 100);});// Vote interface zone interactions$get(moduleId, '.reactions').mouseout(function() {clearTimeout(reactionsTimeout);reactionsTimeout = setTimeout(triggerHideReactions, 1000); // Hide vote interface after 1s out of it.}).mouseover(function() {clearTimeout(reactionsTimeout);});}return {init: function(courseId) {// Wait that the DOM is fully loaded.$(function() {var blockData = $('.block_point_view[data-blockdata]').data('blockdata');var cmid = null; // If this page is a course module view, retrieve the module ID.document.body.classList.forEach(function(bodyClass) {var matches = bodyClass.match(/cmid-(\d+)/);cmid = matches ? matches[1] : cmid;});callOnModulesListLoad(function() {setUpDifficultyTracks(blockData.difficultylevels, blockData.trackcolors, cmid);setUpReactions(courseId, blockData.moduleswithreactions, blockData.reactionstemplate, blockData.pix, cmid);});});}};});