ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç moodle-gradereport_history-userselector.js000064400000063241152266473710015216 0ustar00YUI.add('moodle-gradereport_history-userselector', function (Y, NAME) { // This file is part of Moodle - http://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 . /** * The User Selector for the grade history report. * * @module moodle-gradereport_history-userselector * @package gradereport_history * @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @main moodle-gradereport_history-userselector */ /** * @module moodle-gradereport_history-userselector */ var COMPONENT = 'gradereport_history'; var USP = { AJAXURL: 'ajaxurl', BASE: 'base', CHECKBOX_NAME_PREFIX: 'usp-u', COURSEID: 'courseid', DIALOGUE_PREFIX: 'moodle-dialogue', NAME: 'gradereport_history_usp', PAGE: 'page', PARAMS: 'params', PERPAGE: 'perPage', SEARCH: 'search', SEARCHBTN: 'searchbtn', SELECTEDUSERS: 'selectedUsers', URL: 'url', USERCOUNT: 'userCount' }; var CSS = { ACCESSHIDE: 'accesshide', AJAXCONTENT: 'usp-ajax-content', CHECKBOX: 'usp-checkbox', CLOSE: 'close', CLOSEBTN: 'usp-finish', CONTENT: 'usp-content', DETAILS: 'details', EXTRAFIELDS: 'extrafields', FIRSTADDED: 'usp-first-added', FULLNAME: 'fullname', HEADER: 'usp-header', HIDDEN: 'hidden', LIGHTBOX: 'usp-loading-lightbox', LOADINGICON: 'loading-icon icon', MORERESULTS: 'usp-more-results', OPTIONS: 'options', PICTURE: 'usp-picture', RESULTSCOUNT: 'usp-results-count', SEARCH: 'usp-search', SEARCHBTN: 'usp-search-btn', SEARCHFIELD: 'usp-search-field', SEARCHRESULTS: 'usp-search-results', SELECTED: 'selected', USER: 'usp-user', USERS: 'usp-users', WRAP: 'usp-wrap' }; var SELECTORS = { AJAXCONTENT: '.' + CSS.AJAXCONTENT, FINISHBTN: '.' + CSS.CLOSEBTN + ' input', FIRSTADDED: '.' + CSS.FIRSTADDED, FULLNAME: '.' + CSS.FULLNAME + ' label', LIGHTBOX: '.' + CSS.LIGHTBOX, MORERESULTS: '.' + CSS.MORERESULTS, OPTIONS: '.' + CSS.OPTIONS, PICTURE: '.' + CSS.USER + ' .userpicture', RESULTSCOUNT: '.' + CSS.RESULTSCOUNT, RESULTSUSERS: '.' + CSS.SEARCHRESULTS + ' .' + CSS.USERS, SEARCHBTN: '.' + CSS.SEARCHBTN, SEARCHFIELD: '.' + CSS.SEARCHFIELD, SELECTEDNAMES: '.felement .selectednames', TRIGGER: '.gradereport_history_plugin input.selectortrigger', USER: '.' + CSS.USER, USERFULLNAMES: 'input[name="userfullnames"]', USERIDS: 'input[name="userids"]', USERSELECT: '.' + CSS.CHECKBOX + ' input[type=checkbox]' }; /** * User Selector. * * @namespace M.gradereport_history * @class UserSelector * @constructor */ var USERSELECTOR = function() { USERSELECTOR.superclass.constructor.apply(this, arguments); }; Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.core.dialogue, { /** * Whether or not this is the first time the user displays the dialogue within that request. * * @property _firstDisplay * @type Boolean * @private */ _firstDisplay: true, /** * The list of all the users selected while the dialogue is open. * * @type Object * @property _usersBufferList * @private */ _usersBufferList: null, /** * The Node on which the focus is set. * * @property _userTabFocus * @type Node * @private */ _userTabFocus: null, /** * Compiled template function for a user node. * * @property _userTemplate * @type Function * @private */ _userTemplate: null, initializer: function() { var bb = this.get('boundingBox'), content, params, tpl; tpl = Y.Handlebars.compile( '
' + '
' + '' + '
' + '
' + '
' + '
' + '
' + '{{get_string ' + '
' + '
' + '' + '
' + '
' + '
' + '
'); content = Y.Node.create( tpl({ COMPONENT: COMPONENT, CSS: CSS, loadingIcon: M.util.image_url('i/loading', 'moodle') }) ); // Set the title and content. this.getStdModNode(Y.WidgetStdMod.HEADER).prepend(Y.Node.create('

' + this.get('title') + '

')); this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); // Use standard dialogue class name. This removes the default styling of the footer. this.get('boundingBox').one('.moodle-dialogue-wrap').addClass('moodle-dialogue-content'); // Add the event on the button that opens the dialogue. Y.one(SELECTORS.TRIGGER).on('click', this.show, this); // The button to finalize the selection. bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this); // Delegate the keyboard navigation in the users list. bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this); // Delegate the action to select a user. Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this); Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this); params = this.get(USP.PARAMS); params.id = this.get(USP.COURSEID); this.set(USP.PARAMS, params); bb.one(SELECTORS.SEARCHBTN).on('click', this.search, this, false); }, /** * Display the dialogue. * * @method show */ show: function(e) { var bb; this._usersBufferList = Y.clone(this.get(USP.SELECTEDUSERS)); if (this._firstDisplay) { // Load the default list of users when the dialogue is loaded for the first time. this._firstDisplay = false; this.search(e, false); } else { // Leave the content as is, but reset the selection. bb = this.get('boundingBox'); // Remove all the selected users. bb.all(SELECTORS.USER).each(function(node) { this.markUserNode(node, false); }, this); // Select the users. Y.Object.each(this._usersBufferList, function(v, k) { var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]'); if (user) { this.markUserNode(user, true); } }, this); // Reset the tab focus. this.setUserTabFocus(bb.one(SELECTORS.USER)); } return Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this); }, /** * Search for users. * * @method search * @param {EventFacade} e The event. * @param {Boolean} append Whether we want to append the results to the current results or not. */ search: function(e, append) { if (e) { e.preventDefault(); } var params; if (append) { this.set(USP.PAGE, this.get(USP.PAGE) + 1); } else { this.set(USP.USERCOUNT, 0); this.set(USP.PAGE, 0); } params = this.get(USP.PARAMS); params.sesskey = M.cfg.sesskey; params.action = 'searchusers'; params.search = this.get('boundingBox').one(SELECTORS.SEARCHFIELD).get('value'); params.page = this.get(USP.PAGE); params.perpage = this.get(USP.PERPAGE); Y.io(M.cfg.wwwroot + this.get(USP.AJAXURL), { method: 'POST', data: window.build_querystring(params), on: { start: this.preSearch, complete: this.processSearchResults, end: this.postSearch }, context: this, "arguments": { // Quoted because this is a reserved keyword. append: append } }); }, /** * Pre search callback. * * @method preSearch * @param {String} transactionId The transaction ID. * @param {Object} args The arguments passed from YUI.io() */ preSearch: function(unused, args) { var bb = this.get('boundingBox'); // Display the lightbox. bb.one(SELECTORS.LIGHTBOX).removeClass(CSS.HIDDEN); // Set the number of results to 'loading...'. if (!args.append) { bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('loading', 'admin')); } }, /** * Post search callback. * * @method postSearch * @param {String} transactionId The transaction ID. * @param {Object} args The arguments passed from YUI.io() */ postSearch: function(transactionId, args) { var bb = this.get('boundingBox'), firstAdded = bb.one(SELECTORS.FIRSTADDED), firstUser; // Hide the lightbox. bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN); if (args.append && firstAdded) { // Sets the focus on the newly added user if we are appending results. this.setUserTabFocus(firstAdded); firstAdded.one(SELECTORS.USERSELECT).focus(); } else { // New search result, set the tab focus on the first user returned. firstUser = bb.one(SELECTORS.USER); if (firstUser) { this.setUserTabFocus(firstUser); } } }, /** * Process and display the search results. * * @method processSearchResults * @param {String} tid The transaction ID. * @param {Object} outcome The response object. * @param {Object} args The arguments passed from YUI.io(). */ processSearchResults: function(tid, outcome, args) { var result = false, error = false, bb = this.get('boundingBox'), users, userTemplate, count, selected, i, firstAdded = true, node, content, fetchmore, totalUsers; // Decodes the result. try { result = Y.JSON.parse(outcome.responseText); if (!result.success || result.error) { error = true; } } catch (e) { error = true; } // There was an error. if (error) { this.setContent(''); bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('errajaxsearch', COMPONENT)); return; } // Create the div containing the users when it is a fresh search. if (!args.append) { users = Y.Node.create('
'); } else { users = bb.one(SELECTORS.RESULTSUSERS); } // Compile the template for each user node. if (!this._userTemplate) { this._userTemplate = Y.Handlebars.compile( '
' + '
' + '' + '
' + '
{{{picture}}}
' + '
' + '
' + '' + '
' + '
{{{extrafields}}}
' + '
' + '
' ); } userTemplate = this._userTemplate; // Append the users one by one. count = this.get(USP.USERCOUNT); selected = ''; var user; for (i in result.response.users) { count++; user = result.response.users[i]; // If already selected. if (Y.Object.hasKey(this._usersBufferList, user.userid)) { selected = true; } else { selected = false; } node = Y.Node.create(userTemplate({ checkboxId: Y.guid(), COMPONENT: COMPONENT, count: count, CSS: CSS, extrafields: user.extrafields, extraFieldsId: Y.guid(), fullname: user.fullname, picture: user.picture, userId: user.userid, USP: USP })); this.markUserNode(node, selected); // Noting the first user that was when adding more results. if (args.append && firstAdded) { users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED); node.addClass(CSS.FIRSTADDED); firstAdded = false; } users.append(node); } this.set(USP.USERCOUNT, count); // Update the count of users, and add a button to load more if need be. totalUsers = parseInt(result.response.totalusers, 10); if (!args.append) { if (totalUsers === 0) { bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('noresults', 'moodle')); content = ''; } else { if (totalUsers === 1) { bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundoneuser', COMPONENT)); } else { bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundnusers', COMPONENT, totalUsers)); } content = Y.Node.create('
') .append(users); if (result.response.totalusers > (this.get(USP.PAGE) + 1) * this.get(USP.PERPAGE)) { fetchmore = Y.Node.create(''); fetchmore.one('a').on('click', this.search, this, true); fetchmore.one('a').on('key', this.search, 'space', this, true); content.append(fetchmore); } } this.setContent(content); } else { if (totalUsers <= (this.get(USP.PAGE) + 1) * this.get(USP.PERPAGE)) { bb.one(SELECTORS.MORERESULTS).remove(); } } }, /** * When the user has finished selecting users. * * @method finishSelectingUsers * @param {EventFacade} e The event. */ finishSelectingUsers: function(e) { e.preventDefault(); this.applySelection(); this.hide(); }, /** * Apply the selection made. * * @method applySelection * @param {EventFacade} e The event. */ applySelection: function() { var userIds = Y.Object.keys(this._usersBufferList); this.set(USP.SELECTEDUSERS, Y.clone(this._usersBufferList)) .setNameDisplay(); Y.one(SELECTORS.USERIDS).set('value', userIds.join()); }, /** * Select a user. * * @method SelectUser * @param {EventFacade} e The event. */ selectUser: function(e) { var user = e.currentTarget.ancestor(SELECTORS.USER), checkbox = user.one(SELECTORS.USERSELECT), fullname = user.one(SELECTORS.FULLNAME).get('innerHTML'), checked = checkbox.get('checked'), userId = user.getData('userid'); if (e.currentTarget !== checkbox) { // We triggered the selection from another node, so we need to change the checkbox value. checked = !checked; } if (checked) { // Selecting the user. this._usersBufferList[userId] = fullname; } else { // De-selecting the user. delete this._usersBufferList[userId]; delete this._usersBufferList[parseInt(userId, 10)]; // Also remove numbered keys. } this.markUserNode(user, checked); }, /** * Mark a user node as selected or not. * * This only takes care of the DOM side of things, not the internal mechanism * storing what users have been selected or not. * * @param {Node} node The user node. * @param {Boolean} selected True to mark as selected. * @chainable */ markUserNode: function(node, selected) { if (selected) { node.addClass(CSS.SELECTED) .set('aria-selected', true) .one(SELECTORS.USERSELECT) .set('checked', true); } else { node.removeClass(CSS.SELECTED) .set('aria-selected', false) .one(SELECTORS.USERSELECT) .set('checked', false); } return this; }, /** * Set the content of the dialogue. * * @method setContent * @param {String} content The content. * @chainable */ setContent: function(content) { this.get('boundingBox').one(SELECTORS.AJAXCONTENT).setHTML(content); return this; }, /** * Display the names of the selected users in the form. * * @method setNameDisplay */ setNameDisplay: function() { var namelist = Y.Object.values(this.get(USP.SELECTEDUSERS)); Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', ')); Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join()); }, /** * User keyboard navigation. * * @method userKeyboardNavigation */ userKeyboardNavigation: function(e) { var bb = this.get('boundingBox'), users = bb.all(SELECTORS.USER), direction = 1, user, current = e.target.ancestor(SELECTORS.USER, true); if (e.keyCode === 38) { direction = -1; } user = this.findFocusableUser(users, current, direction); if (user) { e.preventDefault(); user.one(SELECTORS.USERSELECT).focus(); this.setUserTabFocus(user); } }, /** * Find the next or previous focusable node. * * @param {NodeList} users The list of users. * @param {Node} user The user to start with. * @param {Number} direction The direction in which to go. * @return {Node|null} A user node, or null if not found. * @method findFocusableUser */ findFocusableUser: function(users, user, direction) { var index = users.indexOf(user); if (users.size() < 1) { return null; } if (index < 0) { return users.item(0); } index += direction; // Wrap the navigation when reaching the top of the bottom. if (index < 0) { index = users.size() - 1; } else if (index >= users.size()) { index = 0; } return users.item(index); }, /** * Set the user tab focus. * * @param {Node} user The user node. * @method setUserTabFocus */ setUserTabFocus: function(user) { if (this._userTabFocus) { this._userTabFocus.setAttribute('tabindex', '-1'); } if (!user) { // We were not passed a user, there is apparently none in the dialogue. Nothing to do here \\\o/. return; } this._userTabFocus = user.one(SELECTORS.USERSELECT); this._userTabFocus.setAttribute('tabindex', '0'); this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID()); } }, { NAME: USP.NAME, CSS_PREFIX: USP.CSS_PREFIX, ATTRS: { /** * The header. * * @attribute title * @default selectusers language string. * @type String */ title: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('selectusers', COMPONENT); } }, /** * The current page URL. * * @attribute url * @default null * @type String */ url: { validator: Y.Lang.isString, value: null }, /** * The URL to the Ajax file. * * @attribute ajaxurl * @default null * @type String */ ajaxurl: { validator: Y.Lang.isString, value: null }, /** * The names of the selected users. * * The keys are the user IDs, the values are their fullname. * * @attribute selectedUsers * @default null * @type Object */ selectedUsers: { validator: Y.Lang.isObject, value: null, getter: function(v) { if (v === null) { return {}; } return v; } }, /** * The course ID. * * @attribute courseid * @default null * @type Number */ courseid: { value: null }, /** * Array of parameters. * * @attribute params * @default [] * @type Array */ params: { validator: Y.Lang.isArray, value: [] }, /** * The page we are on. * * @attribute page * @default 0 * @type Number */ page: { validator: Y.Lang.isNumber, value: 0 }, /** * The number of users displayed. * * @attribute userCount * @default 0 * @type Number */ userCount: { value: 0, validator: Y.Lang.isNumber }, /** * The number of results per page. * * @attribute perPage * @default 25 * @type Number */ perPage: { value: 25, Validator: Y.Lang.isNumber } } }); Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), { /** * List of extra classes. * * @attribute extraClasses * @default ['gradereport_history_usp'] * @type Array */ extraClasses: { value: [ 'gradereport_history_usp' ] }, /** * Whether to focus on the target that caused the Widget to be shown. * * @attribute focusOnPreviousTargetAfterHide * @default true * @type Node */ focusOnPreviousTargetAfterHide: { value: true }, /** * * Width. * * @attribute width * @default '500px' * @type String|Number */ width: { value: '500px' }, /** * Boolean indicating whether or not the Widget is visible. * * @attribute visible * @default false * @type Boolean */ visible: { value: false }, /** * Whether the widget should be modal or not. * * @attribute modal * @type Boolean * @default true */ modal: { value: true }, /** * Whether the widget should be draggable or not. * * @attribute draggable * @type Boolean * @default true */ draggable: { value: true } }); Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) { return new USERSELECTOR(cfg); }; }, '@VERSION@', { "requires": [ "escape", "event-delegate", "event-key", "handlebars", "io-base", "json-parse", "moodle-core-notification-dialogue" ] }); moodle-gradereport_history-userselector-debug.js000064400000063472152266473710016310 0ustar00YUI.add('moodle-gradereport_history-userselector', function (Y, NAME) { // This file is part of Moodle - http://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 . /** * The User Selector for the grade history report. * * @module moodle-gradereport_history-userselector * @package gradereport_history * @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @main moodle-gradereport_history-userselector */ /** * @module moodle-gradereport_history-userselector */ var COMPONENT = 'gradereport_history'; var USP = { AJAXURL: 'ajaxurl', BASE: 'base', CHECKBOX_NAME_PREFIX: 'usp-u', COURSEID: 'courseid', DIALOGUE_PREFIX: 'moodle-dialogue', NAME: 'gradereport_history_usp', PAGE: 'page', PARAMS: 'params', PERPAGE: 'perPage', SEARCH: 'search', SEARCHBTN: 'searchbtn', SELECTEDUSERS: 'selectedUsers', URL: 'url', USERCOUNT: 'userCount' }; var CSS = { ACCESSHIDE: 'accesshide', AJAXCONTENT: 'usp-ajax-content', CHECKBOX: 'usp-checkbox', CLOSE: 'close', CLOSEBTN: 'usp-finish', CONTENT: 'usp-content', DETAILS: 'details', EXTRAFIELDS: 'extrafields', FIRSTADDED: 'usp-first-added', FULLNAME: 'fullname', HEADER: 'usp-header', HIDDEN: 'hidden', LIGHTBOX: 'usp-loading-lightbox', LOADINGICON: 'loading-icon icon', MORERESULTS: 'usp-more-results', OPTIONS: 'options', PICTURE: 'usp-picture', RESULTSCOUNT: 'usp-results-count', SEARCH: 'usp-search', SEARCHBTN: 'usp-search-btn', SEARCHFIELD: 'usp-search-field', SEARCHRESULTS: 'usp-search-results', SELECTED: 'selected', USER: 'usp-user', USERS: 'usp-users', WRAP: 'usp-wrap' }; var SELECTORS = { AJAXCONTENT: '.' + CSS.AJAXCONTENT, FINISHBTN: '.' + CSS.CLOSEBTN + ' input', FIRSTADDED: '.' + CSS.FIRSTADDED, FULLNAME: '.' + CSS.FULLNAME + ' label', LIGHTBOX: '.' + CSS.LIGHTBOX, MORERESULTS: '.' + CSS.MORERESULTS, OPTIONS: '.' + CSS.OPTIONS, PICTURE: '.' + CSS.USER + ' .userpicture', RESULTSCOUNT: '.' + CSS.RESULTSCOUNT, RESULTSUSERS: '.' + CSS.SEARCHRESULTS + ' .' + CSS.USERS, SEARCHBTN: '.' + CSS.SEARCHBTN, SEARCHFIELD: '.' + CSS.SEARCHFIELD, SELECTEDNAMES: '.felement .selectednames', TRIGGER: '.gradereport_history_plugin input.selectortrigger', USER: '.' + CSS.USER, USERFULLNAMES: 'input[name="userfullnames"]', USERIDS: 'input[name="userids"]', USERSELECT: '.' + CSS.CHECKBOX + ' input[type=checkbox]' }; /** * User Selector. * * @namespace M.gradereport_history * @class UserSelector * @constructor */ var USERSELECTOR = function() { USERSELECTOR.superclass.constructor.apply(this, arguments); }; Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.core.dialogue, { /** * Whether or not this is the first time the user displays the dialogue within that request. * * @property _firstDisplay * @type Boolean * @private */ _firstDisplay: true, /** * The list of all the users selected while the dialogue is open. * * @type Object * @property _usersBufferList * @private */ _usersBufferList: null, /** * The Node on which the focus is set. * * @property _userTabFocus * @type Node * @private */ _userTabFocus: null, /** * Compiled template function for a user node. * * @property _userTemplate * @type Function * @private */ _userTemplate: null, initializer: function() { var bb = this.get('boundingBox'), content, params, tpl; tpl = Y.Handlebars.compile( '
' + '
' + '' + '
' + '
' + '
' + '
' + '
' + '{{get_string ' + '
' + '
' + '' + '
' + '
' + '
' + '
'); content = Y.Node.create( tpl({ COMPONENT: COMPONENT, CSS: CSS, loadingIcon: M.util.image_url('i/loading', 'moodle') }) ); // Set the title and content. this.getStdModNode(Y.WidgetStdMod.HEADER).prepend(Y.Node.create('

' + this.get('title') + '

')); this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); // Use standard dialogue class name. This removes the default styling of the footer. this.get('boundingBox').one('.moodle-dialogue-wrap').addClass('moodle-dialogue-content'); // Add the event on the button that opens the dialogue. Y.one(SELECTORS.TRIGGER).on('click', this.show, this); // The button to finalize the selection. bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this); // Delegate the keyboard navigation in the users list. bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this); // Delegate the action to select a user. Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this); Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this); params = this.get(USP.PARAMS); params.id = this.get(USP.COURSEID); this.set(USP.PARAMS, params); bb.one(SELECTORS.SEARCHBTN).on('click', this.search, this, false); }, /** * Display the dialogue. * * @method show */ show: function(e) { var bb; this._usersBufferList = Y.clone(this.get(USP.SELECTEDUSERS)); if (this._firstDisplay) { // Load the default list of users when the dialogue is loaded for the first time. this._firstDisplay = false; this.search(e, false); } else { // Leave the content as is, but reset the selection. bb = this.get('boundingBox'); // Remove all the selected users. bb.all(SELECTORS.USER).each(function(node) { this.markUserNode(node, false); }, this); // Select the users. Y.Object.each(this._usersBufferList, function(v, k) { var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]'); if (user) { this.markUserNode(user, true); } }, this); // Reset the tab focus. this.setUserTabFocus(bb.one(SELECTORS.USER)); } return Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this); }, /** * Search for users. * * @method search * @param {EventFacade} e The event. * @param {Boolean} append Whether we want to append the results to the current results or not. */ search: function(e, append) { if (e) { e.preventDefault(); } var params; if (append) { this.set(USP.PAGE, this.get(USP.PAGE) + 1); } else { this.set(USP.USERCOUNT, 0); this.set(USP.PAGE, 0); } params = this.get(USP.PARAMS); params.sesskey = M.cfg.sesskey; params.action = 'searchusers'; params.search = this.get('boundingBox').one(SELECTORS.SEARCHFIELD).get('value'); params.page = this.get(USP.PAGE); params.perpage = this.get(USP.PERPAGE); Y.io(M.cfg.wwwroot + this.get(USP.AJAXURL), { method: 'POST', data: window.build_querystring(params), on: { start: this.preSearch, complete: this.processSearchResults, end: this.postSearch }, context: this, "arguments": { // Quoted because this is a reserved keyword. append: append } }); }, /** * Pre search callback. * * @method preSearch * @param {String} transactionId The transaction ID. * @param {Object} args The arguments passed from YUI.io() */ preSearch: function(unused, args) { var bb = this.get('boundingBox'); // Display the lightbox. bb.one(SELECTORS.LIGHTBOX).removeClass(CSS.HIDDEN); // Set the number of results to 'loading...'. if (!args.append) { bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('loading', 'admin')); } }, /** * Post search callback. * * @method postSearch * @param {String} transactionId The transaction ID. * @param {Object} args The arguments passed from YUI.io() */ postSearch: function(transactionId, args) { var bb = this.get('boundingBox'), firstAdded = bb.one(SELECTORS.FIRSTADDED), firstUser; // Hide the lightbox. bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN); if (args.append && firstAdded) { // Sets the focus on the newly added user if we are appending results. this.setUserTabFocus(firstAdded); firstAdded.one(SELECTORS.USERSELECT).focus(); } else { // New search result, set the tab focus on the first user returned. firstUser = bb.one(SELECTORS.USER); if (firstUser) { this.setUserTabFocus(firstUser); } } }, /** * Process and display the search results. * * @method processSearchResults * @param {String} tid The transaction ID. * @param {Object} outcome The response object. * @param {Object} args The arguments passed from YUI.io(). */ processSearchResults: function(tid, outcome, args) { var result = false, error = false, bb = this.get('boundingBox'), users, userTemplate, count, selected, i, firstAdded = true, node, content, fetchmore, totalUsers; // Decodes the result. try { result = Y.JSON.parse(outcome.responseText); if (!result.success || result.error) { error = true; } } catch (e) { error = true; } // There was an error. if (error) { this.setContent(''); bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('errajaxsearch', COMPONENT)); return; } // Create the div containing the users when it is a fresh search. if (!args.append) { users = Y.Node.create('
'); } else { users = bb.one(SELECTORS.RESULTSUSERS); } // Compile the template for each user node. if (!this._userTemplate) { this._userTemplate = Y.Handlebars.compile( '
' + '
' + '' + '
' + '
{{{picture}}}
' + '
' + '
' + '' + '
' + '
{{{extrafields}}}
' + '
' + '
' ); } userTemplate = this._userTemplate; // Append the users one by one. count = this.get(USP.USERCOUNT); selected = ''; var user; for (i in result.response.users) { count++; user = result.response.users[i]; // If already selected. if (Y.Object.hasKey(this._usersBufferList, user.userid)) { selected = true; } else { selected = false; } node = Y.Node.create(userTemplate({ checkboxId: Y.guid(), COMPONENT: COMPONENT, count: count, CSS: CSS, extrafields: user.extrafields, extraFieldsId: Y.guid(), fullname: user.fullname, picture: user.picture, userId: user.userid, USP: USP })); this.markUserNode(node, selected); // Noting the first user that was when adding more results. if (args.append && firstAdded) { users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED); node.addClass(CSS.FIRSTADDED); firstAdded = false; } users.append(node); } this.set(USP.USERCOUNT, count); // Update the count of users, and add a button to load more if need be. totalUsers = parseInt(result.response.totalusers, 10); if (!args.append) { if (totalUsers === 0) { bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('noresults', 'moodle')); content = ''; } else { if (totalUsers === 1) { bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundoneuser', COMPONENT)); } else { bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundnusers', COMPONENT, totalUsers)); } content = Y.Node.create('
') .append(users); if (result.response.totalusers > (this.get(USP.PAGE) + 1) * this.get(USP.PERPAGE)) { fetchmore = Y.Node.create(''); fetchmore.one('a').on('click', this.search, this, true); fetchmore.one('a').on('key', this.search, 'space', this, true); content.append(fetchmore); } } this.setContent(content); } else { if (totalUsers <= (this.get(USP.PAGE) + 1) * this.get(USP.PERPAGE)) { bb.one(SELECTORS.MORERESULTS).remove(); } } }, /** * When the user has finished selecting users. * * @method finishSelectingUsers * @param {EventFacade} e The event. */ finishSelectingUsers: function(e) { e.preventDefault(); this.applySelection(); this.hide(); }, /** * Apply the selection made. * * @method applySelection * @param {EventFacade} e The event. */ applySelection: function() { var userIds = Y.Object.keys(this._usersBufferList); this.set(USP.SELECTEDUSERS, Y.clone(this._usersBufferList)) .setNameDisplay(); Y.one(SELECTORS.USERIDS).set('value', userIds.join()); }, /** * Select a user. * * @method SelectUser * @param {EventFacade} e The event. */ selectUser: function(e) { var user = e.currentTarget.ancestor(SELECTORS.USER), checkbox = user.one(SELECTORS.USERSELECT), fullname = user.one(SELECTORS.FULLNAME).get('innerHTML'), checked = checkbox.get('checked'), userId = user.getData('userid'); if (e.currentTarget !== checkbox) { // We triggered the selection from another node, so we need to change the checkbox value. checked = !checked; } if (checked) { // Selecting the user. this._usersBufferList[userId] = fullname; } else { // De-selecting the user. delete this._usersBufferList[userId]; delete this._usersBufferList[parseInt(userId, 10)]; // Also remove numbered keys. } this.markUserNode(user, checked); }, /** * Mark a user node as selected or not. * * This only takes care of the DOM side of things, not the internal mechanism * storing what users have been selected or not. * * @param {Node} node The user node. * @param {Boolean} selected True to mark as selected. * @chainable */ markUserNode: function(node, selected) { if (selected) { node.addClass(CSS.SELECTED) .set('aria-selected', true) .one(SELECTORS.USERSELECT) .set('checked', true); } else { node.removeClass(CSS.SELECTED) .set('aria-selected', false) .one(SELECTORS.USERSELECT) .set('checked', false); } return this; }, /** * Set the content of the dialogue. * * @method setContent * @param {String} content The content. * @chainable */ setContent: function(content) { this.get('boundingBox').one(SELECTORS.AJAXCONTENT).setHTML(content); return this; }, /** * Display the names of the selected users in the form. * * @method setNameDisplay */ setNameDisplay: function() { var namelist = Y.Object.values(this.get(USP.SELECTEDUSERS)); Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', ')); Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join()); }, /** * User keyboard navigation. * * @method userKeyboardNavigation */ userKeyboardNavigation: function(e) { var bb = this.get('boundingBox'), users = bb.all(SELECTORS.USER), direction = 1, user, current = e.target.ancestor(SELECTORS.USER, true); if (e.keyCode === 38) { direction = -1; } user = this.findFocusableUser(users, current, direction); if (user) { e.preventDefault(); user.one(SELECTORS.USERSELECT).focus(); this.setUserTabFocus(user); } }, /** * Find the next or previous focusable node. * * @param {NodeList} users The list of users. * @param {Node} user The user to start with. * @param {Number} direction The direction in which to go. * @return {Node|null} A user node, or null if not found. * @method findFocusableUser */ findFocusableUser: function(users, user, direction) { var index = users.indexOf(user); if (users.size() < 1) { Y.log('The users list is empty', 'debug', COMPONENT); return null; } if (index < 0) { Y.log('Unable to find the user in the list of users', 'debug', COMPONENT); return users.item(0); } index += direction; // Wrap the navigation when reaching the top of the bottom. if (index < 0) { index = users.size() - 1; } else if (index >= users.size()) { index = 0; } return users.item(index); }, /** * Set the user tab focus. * * @param {Node} user The user node. * @method setUserTabFocus */ setUserTabFocus: function(user) { if (this._userTabFocus) { this._userTabFocus.setAttribute('tabindex', '-1'); } if (!user) { // We were not passed a user, there is apparently none in the dialogue. Nothing to do here \\\o/. return; } this._userTabFocus = user.one(SELECTORS.USERSELECT); this._userTabFocus.setAttribute('tabindex', '0'); this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID()); } }, { NAME: USP.NAME, CSS_PREFIX: USP.CSS_PREFIX, ATTRS: { /** * The header. * * @attribute title * @default selectusers language string. * @type String */ title: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('selectusers', COMPONENT); } }, /** * The current page URL. * * @attribute url * @default null * @type String */ url: { validator: Y.Lang.isString, value: null }, /** * The URL to the Ajax file. * * @attribute ajaxurl * @default null * @type String */ ajaxurl: { validator: Y.Lang.isString, value: null }, /** * The names of the selected users. * * The keys are the user IDs, the values are their fullname. * * @attribute selectedUsers * @default null * @type Object */ selectedUsers: { validator: Y.Lang.isObject, value: null, getter: function(v) { if (v === null) { return {}; } return v; } }, /** * The course ID. * * @attribute courseid * @default null * @type Number */ courseid: { value: null }, /** * Array of parameters. * * @attribute params * @default [] * @type Array */ params: { validator: Y.Lang.isArray, value: [] }, /** * The page we are on. * * @attribute page * @default 0 * @type Number */ page: { validator: Y.Lang.isNumber, value: 0 }, /** * The number of users displayed. * * @attribute userCount * @default 0 * @type Number */ userCount: { value: 0, validator: Y.Lang.isNumber }, /** * The number of results per page. * * @attribute perPage * @default 25 * @type Number */ perPage: { value: 25, Validator: Y.Lang.isNumber } } }); Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), { /** * List of extra classes. * * @attribute extraClasses * @default ['gradereport_history_usp'] * @type Array */ extraClasses: { value: [ 'gradereport_history_usp' ] }, /** * Whether to focus on the target that caused the Widget to be shown. * * @attribute focusOnPreviousTargetAfterHide * @default true * @type Node */ focusOnPreviousTargetAfterHide: { value: true }, /** * * Width. * * @attribute width * @default '500px' * @type String|Number */ width: { value: '500px' }, /** * Boolean indicating whether or not the Widget is visible. * * @attribute visible * @default false * @type Boolean */ visible: { value: false }, /** * Whether the widget should be modal or not. * * @attribute modal * @type Boolean * @default true */ modal: { value: true }, /** * Whether the widget should be draggable or not. * * @attribute draggable * @type Boolean * @default true */ draggable: { value: true } }); Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) { return new USERSELECTOR(cfg); }; }, '@VERSION@', { "requires": [ "escape", "event-delegate", "event-key", "handlebars", "io-base", "json-parse", "moodle-core-notification-dialogue" ] }); moodle-gradereport_history-userselector-min.js000064400000023206152266473710015774 0ustar00YUI.add("moodle-gradereport_history-userselector",function(T,e){var g="gradereport_history",p={AJAXURL:"ajaxurl",BASE:"base",CHECKBOX_NAME_PREFIX:"usp-u",COURSEID:"courseid",DIALOGUE_PREFIX:"moodle-dialogue",NAME:"gradereport_history_usp",PAGE:"page",PARAMS:"params",PERPAGE:"perPage",SEARCH:"search",SEARCHBTN:"searchbtn",SELECTEDUSERS:"selectedUsers",URL:"url",USERCOUNT:"userCount"},R={ACCESSHIDE:"accesshide",AJAXCONTENT:"usp-ajax-content",CHECKBOX:"usp-checkbox",CLOSE:"close",CLOSEBTN:"usp-finish",CONTENT:"usp-content",DETAILS:"details",EXTRAFIELDS:"extrafields",FIRSTADDED:"usp-first-added",FULLNAME:"fullname",HEADER:"usp-header",HIDDEN:"hidden",LIGHTBOX:"usp-loading-lightbox",LOADINGICON:"loading-icon icon",MORERESULTS:"usp-more-results",OPTIONS:"options",PICTURE:"usp-picture",RESULTSCOUNT:"usp-results-count",SEARCH:"usp-search",SEARCHBTN:"usp-search-btn",SEARCHFIELD:"usp-search-field",SEARCHRESULTS:"usp-search-results",SELECTED:"selected",USER:"usp-user",USERS:"usp-users",WRAP:"usp-wrap"},C={AJAXCONTENT:"."+R.AJAXCONTENT,FINISHBTN:"."+R.CLOSEBTN+" input",FIRSTADDED:"."+R.FIRSTADDED,FULLNAME:"."+R.FULLNAME+" label",LIGHTBOX:"."+R.LIGHTBOX,MORERESULTS:"."+R.MORERESULTS,OPTIONS:"."+R.OPTIONS,PICTURE:"."+R.USER+" .userpicture",RESULTSCOUNT:"."+R.RESULTSCOUNT,RESULTSUSERS:"."+R.SEARCHRESULTS+" ."+R.USERS,SEARCHBTN:"."+R.SEARCHBTN,SEARCHFIELD:"."+R.SEARCHFIELD,SELECTEDNAMES:".felement .selectednames",TRIGGER:".gradereport_history_plugin input.selectortrigger",USER:"."+R.USER,USERFULLNAMES:'input[name="userfullnames"]',USERIDS:'input[name="userids"]',USERSELECT:"."+R.CHECKBOX+" input[type=checkbox]"},s=function(){s.superclass.constructor.apply(this,arguments)};T.namespace("M.gradereport_history").UserSelector=T.extend(s,M.core.dialogue,{_firstDisplay:!0,_usersBufferList:null,_userTabFocus:null,_userTemplate:null,initializer:function(){var e=this.get("boundingBox"),s=T.Handlebars.compile('
{{get_string
'),s=T.Node.create(s({COMPONENT:g,CSS:R,loadingIcon:M.util.image_url("i/loading","moodle")}));this.getStdModNode(T.WidgetStdMod.HEADER).prepend(T.Node.create("

"+this.get("title")+"

")),this.setStdModContent(T.WidgetStdMod.BODY,s,T.WidgetStdMod.REPLACE),this.get("boundingBox").one(".moodle-dialogue-wrap").addClass("moodle-dialogue-content"),T.one(C.TRIGGER).on("click",this.show,this),e.one(C.FINISHBTN).on("click",this.finishSelectingUsers,this),e.delegate("key",this.userKeyboardNavigation,"down:38,40",C.AJAXCONTENT,this),T.delegate("click",this.selectUser,C.AJAXCONTENT,C.USERSELECT,this),T.delegate("click",this.selectUser,C.AJAXCONTENT,C.PICTURE,this),(s=this.get(p.PARAMS)).id=this.get(p.COURSEID),this.set(p.PARAMS,s),e.one(C.SEARCHBTN).on("click",this.search,this,!1)},show:function(e){var t;return this._usersBufferList=T.clone(this.get(p.SELECTEDUSERS)),this._firstDisplay?(this._firstDisplay=!1,this.search(e,!1)):((t=this.get("boundingBox")).all(C.USER).each(function(e){this.markUserNode(e,!1)},this),T.Object.each(this._usersBufferList,function(e,s){s=t.one(C.USER+'[data-userid="'+s+'"]');s&&this.markUserNode(s,!0)},this),this.setUserTabFocus(t.one(C.USER))),T.namespace("M.gradereport_history.UserSelector").superclass.show.call(this)},search:function(e,s){e&&e.preventDefault(),s?this.set(p.PAGE,this.get(p.PAGE)+1):(this.set(p.USERCOUNT,0),this.set(p.PAGE,0)),(e=this.get(p.PARAMS)).sesskey=M.cfg.sesskey,e.action="searchusers",e.search=this.get("boundingBox").one(C.SEARCHFIELD).get("value"),e.page=this.get(p.PAGE),e.perpage=this.get(p.PERPAGE),T.io(M.cfg.wwwroot+this.get(p.AJAXURL),{method:"POST",data:window.build_querystring(e),on:{start:this.preSearch,complete:this.processSearchResults,end:this.postSearch},context:this,arguments:{append:s}})},preSearch:function(e,s){var t=this.get("boundingBox");t.one(C.LIGHTBOX).removeClass(R.HIDDEN),s.append||t.one(C.RESULTSCOUNT).setHTML(M.util.get_string("loading","admin"))},postSearch:function(e,s){var t=this.get("boundingBox"),i=t.one(C.FIRSTADDED);t.one(C.LIGHTBOX).addClass(R.HIDDEN),s.append&&i?(this.setUserTabFocus(i),i.one(C.USERSELECT).focus()):(s=t.one(C.USER))&&this.setUserTabFocus(s)},processSearchResults:function(S,e,s){var t,d,i,r,E,a,n,o=!1,u=!1,l=this.get("boundingBox"),c=!0;try{(o=T.JSON.parse(e.responseText)).success&&!o.error||(u=!0)}catch(h){u=!0}if(u)return this.setContent(""),void l.one(C.RESULTSCOUNT).setHTML(M.util.get_string("errajaxsearch",g));for(E in t=s.append?l.one(C.RESULTSUSERS):T.Node.create('
'),this._userTemplate||(this._userTemplate=T.Handlebars.compile('
{{{picture}}}
{{{extrafields}}}
')),d=this._userTemplate,i=this.get(p.USERCOUNT),r="",o.response.users)i++, n=o.response.users[E],r=!!T.Object.hasKey(this._usersBufferList,n.userid),n=T.Node.create(d({checkboxId:T.guid(),COMPONENT:g,count:i,CSS:R,extrafields:n.extrafields,extraFieldsId:T.guid(),fullname:n.fullname,picture:n.picture,userId:n.userid,USP:p})),this.markUserNode(n,r),s.append&&c&&(t.all(C.FIRSTADDED).removeClass(R.FIRSTADDED),n.addClass(R.FIRSTADDED),c=!1),t.append(n);this.set(p.USERCOUNT,i),e=parseInt(o.response.totalusers,10),s.append?e<=(this.get(p.PAGE)+1)*this.get(p.PERPAGE)&&l.one(C.MORERESULTS).remove():(0===e?(l.one(C.RESULTSCOUNT).setHTML(M.util.get_string("noresults","moodle")),a=""):(1===e?l.one(C.RESULTSCOUNT).setHTML(M.util.get_string("foundoneuser",g)):l.one(C.RESULTSCOUNT).setHTML(M.util.get_string("foundnusers",g,e)),a=T.Node.create('
').append(t),o.response.totalusers>(this.get(p.PAGE)+1)*this.get(p.PERPAGE)&&((u=T.Node.create('")).one("a").on("click",this.search,this,!0),u.one("a").on("key",this.search,"space",this,!0),a.append(u))),this.setContent(a))},finishSelectingUsers:function(e){e.preventDefault(),this.applySelection(),this.hide()},applySelection:function(){var e=T.Object.keys(this._usersBufferList);this.set(p.SELECTEDUSERS,T.clone(this._usersBufferList)).setNameDisplay(),T.one(C.USERIDS).set("value",e.join())},selectUser:function(e){var s=e.currentTarget.ancestor(C.USER),t=s.one(C.USERSELECT),i=s.one(C.FULLNAME).get("innerHTML"),r=t.get("checked"),a=s.getData("userid");(r=e.currentTarget!==t?!r:r)?this._usersBufferList[a]=i:(delete this._usersBufferList[a],delete this._usersBufferList[parseInt(a,10)]),this.markUserNode(s,r)},markUserNode:function(e,s){return s?e.addClass(R.SELECTED).set("aria-selected",!0).one(C.USERSELECT).set("checked",!0):e.removeClass(R.SELECTED).set("aria-selected",!1).one(C.USERSELECT).set("checked",!1),this},setContent:function(e){return this.get("boundingBox").one(C.AJAXCONTENT).setHTML(e),this},setNameDisplay:function(){var e=T.Object.values(this.get(p.SELECTEDUSERS));T.one(C.SELECTEDNAMES).set("innerHTML",e.join(", ")),T.one(C.USERFULLNAMES).set("value",e.join())},userKeyboardNavigation:function(e){var s=this.get("boundingBox").all(C.USER),t=1,i=e.target.ancestor(C.USER,!0);38===e.keyCode&&(t=-1),(s=this.findFocusableUser(s,i,t))&&(e.preventDefault(),s.one(C.USERSELECT).focus(),this.setUserTabFocus(s))},findFocusableUser:function(e,s,t){s=e.indexOf(s);return e.size()<1?null:s<0?e.item(0):((s+=t)<0?s=e.size()-1:s>=e.size()&&(s=0),e.item(s))},setUserTabFocus:function(e){this._userTabFocus&&this._userTabFocus.setAttribute("tabindex","-1"),e&&(this._userTabFocus=e.one(C.USERSELECT),this._userTabFocus.setAttribute("tabindex","0"),this.get("boundingBox").one(C.RESULTSUSERS).setAttribute("aria-activedescendant",this._userTabFocus.generateID()))}},{NAME:p.NAME,CSS_PREFIX:p.CSS_PREFIX,ATTRS:{title:{validator:T.Lang.isString,valueFn:function(){return M.util.get_string("selectusers",g)}},url:{validator:T.Lang.isString,value:null},ajaxurl:{validator:T.Lang.isString,value:null},selectedUsers:{validator:T.Lang.isObject,value:null,getter:function(e){return null===e?{}:e}},courseid:{value:null},params:{validator:T.Lang.isArray,value:[]},page:{validator:T.Lang.isNumber,value:0},userCount:{value:0,validator:T.Lang.isNumber},perPage:{value:25,Validator:T.Lang.isNumber}}}),T.Base.modifyAttrs(T.namespace("M.gradereport_history.UserSelector"),{extraClasses:{value:["gradereport_history_usp"]},focusOnPreviousTargetAfterHide:{value:!0},width:{value:"500px"},visible:{value:!1},modal:{value:!0},draggable:{value:!0}}),T.namespace("M.gradereport_history.UserSelector").init=function(e){return new s(e)}},"@VERSION@",{requires:["escape","event-delegate","event-key","handlebars","io-base","json-parse","moodle-core-notification-dialogue"]});