ÿØÿà JFIF    ÿÛ „  ( %"1!%)+...383,7(-.+  -+++--++++---+-+-----+---------------+---+-++7-----ÿÀ  ß â" ÿÄ     ÿÄ H    !1AQaq"‘¡2B±ÁÑð#R“Ò Tbr‚²á3csƒ’ÂñDS¢³$CÿÄ   ÿÄ %  !1AQa"23‘ÿÚ   ? ôÿ ¨pŸªáÿ —åYõõ\?àÒü©ŠÄï¨pŸªáÿ —åYõõ\?àÓü©ŠÄá 0Ÿªáÿ Ÿå[úƒ ú®ði~TÁbqÐ8OÕpÿ ƒOò¤Oè`–RÂáœá™êi€ßÉ< FtŸI“öÌ8úDf´°å}“¾œ6  öFá°y¥jñÇh†ˆ¢ã/ÃÐ:ªcÈ "Y¡ðÑl>ÿ ”ÏËte:qž\oäŠe÷󲍷˜HT4&ÿ ÓÐü6ö®¿øþßèô Ÿ•7Ñi’•j|“ñì>b…þS?*Óôÿ ÓÐü*h¥£ír¶ü UãS炟[AÐaè[ûª•õ&õj?†Éö+EzP—WeÒírJFt ‘BŒ†Ï‡%#tE Øz ¥OÛ«!1›üä±Í™%ºÍãö]°î(–:@<‹ŒÊö×òÆt¦ãº+‡¦%ÌÁ²h´OƒJŒtMÜ>ÀÜÊw3Y´•牋4ǍýʏTì>œú=Íwhyë,¾Ôò×õ¿ßÊa»«þˆѪQ|%6ž™A õ%:øj<>É—ÿ Å_ˆCbõ¥š±ý¯Ýƒï…¶|RëócÍf溪“t.СøTÿ *Ä¿-{†çàczůŽ_–^XþŒ±miB[X±d 1,é”zEù»& î9gœf™9Ð'.;—™i}!ôšåîqêÛ٤ёý£½ÆA–àôe"A$˝Úsäÿ ÷Û #°xŸëí(l »ý3—¥5m! rt`†0~'j2(]S¦¦kv,ÚÇ l¦øJA£Šƒ J3E8ÙiŽ:cÉžúeZ°€¯\®kÖ(79«Ž:¯X”¾³Š&¡* ….‰Ž(ÜíŸ2¥ª‡×Hi²TF¤ò[¨íÈRëÉ䢍mgÑ.Ÿ<öäS0í„ǹÁU´f#Vß;Õ–…P@3ío<ä-±»Ž.L|kªÀê›fÂ6@»eu‚|ÓaÞÆŸ…¨ááå>åŠ?cKü6ùTÍÆ”†sĤÚ;H2RÚ†õ\Ö·Ÿn'¾ ñ#ºI¤Å´%çÁ­‚â7›‹qT3Iï¨ÖÚ5I7Ë!ÅOóŸ¶øÝñØôת¦$Tcö‘[«Ö³šÒ';Aþ ¸èíg A2Z"i¸vdÄ÷.iõ®§)¿]¤À†–‡É&ä{V¶iŽ”.Ó×Õÿ û?h¬Mt–íª[ÿ Ñÿ ÌV(í}=ibÔ¡›¥¢±b Lô¥‡piη_Z<‡z§èŒ)iÖwiÇ 2hÙ3·=’d÷8éŽ1¦¸c¤µ€7›7Ø ð\á)} ¹fËí›pAÃL%âc2 í§æQz¿;T8sæ°qø)QFMð‰XŒÂ±N¢aF¨…8¯!U  Z©RÊ ÖPVÄÀÍin™Ì-GˆªÅËŠ›•zË}º±ŽÍFò¹}Uw×#ä5B¤{î}Ð<ÙD é©¤&‡ïDbàÁôMÁ.// 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 . /** * A simple router for the message drawer that allows navigating between * the "pages" in the drawer. * * This module will maintain a linear history of the unique pages access * to allow navigating back. * * @module core_message/message_drawer_router * @copyright 2018 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define( [ 'jquery', 'core/pubsub', 'core/str', 'core_message/message_drawer_events', 'core/aria', ], function( $, PubSub, Str, MessageDrawerEvents, Aria ) { /* @var {object} routes Message drawer route elements and callbacks. */ var routes = {}; /* @var {object} history Store for route objects history. */ var history = {}; var SELECTORS = { CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]', ROUTES_BACK: '[data-route-back]' }; /** * Add a route. * * @param {String} namespace Unique identifier for the Routes * @param {string} route Route config name. * @param {array} parameters Route parameters. * @param {callback} onGo Route initialization function. * @param {callback} getDescription Route initialization function. */ var add = function(namespace, route, parameters, onGo, getDescription) { if (!routes[namespace]) { routes[namespace] = []; } routes[namespace][route] = { parameters: parameters, onGo: onGo, getDescription: getDescription }; }; /** * Go to a defined route and run the route callbacks. * * @param {String} namespace Unique identifier for the Routes * @param {string} newRoute Route config name. * @return {object} record Current route record with route config name and parameters. */ var changeRoute = function(namespace, newRoute) { var newConfig; // Check if the Route change call is made from an element in the app panel. var fromPanel = [].slice.call(arguments).some(function(arg) { return arg == 'frompanel'; }); // Get the rest of the arguments, if any. var args = [].slice.call(arguments, 2); var renderPromise = $.Deferred().resolve().promise(); Object.keys(routes[namespace]).forEach(function(route) { var config = routes[namespace][route]; var isMatch = route === newRoute; if (isMatch) { newConfig = config; } config.parameters.forEach(function(element) { // Some parameters may be null, or not an element. if (typeof element !== 'object' || element === null) { return; } element.removeClass('previous'); element.attr('data-from-panel', false); if (isMatch) { if (fromPanel) { // Set this attribute to let the conversation renderer know not to show a back button. element.attr('data-from-panel', true); } element.removeClass('hidden'); Aria.unhide(element.get()); } else { // For the message index page elements in the left panel should not be hidden. if (!element.attr('data-in-panel')) { element.addClass('hidden'); Aria.hide(element.get()); } else if (newRoute == 'view-search' || newRoute == 'view-overview') { element.addClass('hidden'); Aria.hide(element.get()); } } }); }); if (newConfig) { if (newConfig.onGo) { renderPromise = newConfig.onGo.apply(undefined, newConfig.parameters.concat(args)); var currentFocusElement = $(document.activeElement); var hasFocus = false; var firstFocusable = null; // No need to start at 0 as we know that is the namespace. for (var i = 1; i < newConfig.parameters.length; i++) { var element = newConfig.parameters[i]; // Some parameters may be null, or not an element. if (typeof element !== 'object' || element === null) { continue; } if (!firstFocusable) { firstFocusable = element; } if (element.has(currentFocusElement).length) { hasFocus = true; break; } } if (!hasFocus) { // This page doesn't have focus yet so focus the first focusable // element in the new view. firstFocusable.find(SELECTORS.CAN_RECEIVE_FOCUS).filter(':visible').first().focus(); } } } var record = { route: newRoute, params: args, renderPromise: renderPromise }; PubSub.publish(MessageDrawerEvents.ROUTE_CHANGED, record); return record; }; /** * Go to a defined route and store the route history. * * @param {String} namespace Unique identifier for the Routes * @return {object} record Current route record with route config name and parameters. */ var go = function(namespace) { var currentFocusElement = $(document.activeElement); var record = changeRoute.apply(namespace, arguments); var inHistory = false; if (!history[namespace]) { history[namespace] = []; } // History stores a unique list of routes. Check to see if the new route // is already in the history, if it is then forget all history after it. // This ensures there are no duplicate routes in history and that it represents // a linear path of routes (it never stores something like [foo, bar, foo])). history[namespace] = history[namespace].reduce(function(carry, previous) { if (previous.route === record.route) { inHistory = true; } if (!inHistory) { carry.push(previous); } return carry; }, []); var historylength = history[namespace].length; var previousRecord = historylength ? history[namespace][historylength - 1] : null; if (previousRecord) { var prevConfig = routes[namespace][previousRecord.route]; var elements = prevConfig.parameters; // The first one will be the namespace, skip it. for (var i = 1; i < elements.length; i++) { // Some parameters may be null, or not an element. if (typeof elements[i] !== 'object' || elements[i] === null) { continue; } elements[i].addClass('previous'); } previousRecord.focusElement = currentFocusElement; if (prevConfig.getDescription) { // If the route has a description then set it on the back button for // the new page we're displaying. prevConfig.getDescription.apply(null, prevConfig.parameters.concat(previousRecord.params)) .then(function(description) { return Str.get_string('backto', 'core_message', description); }) .then(function(label) { // Wait for the new page to finish rendering so that we know // that the back button is visible. return record.renderPromise.then(function() { // Find the elements for the new route we displayed. routes[namespace][record.route].parameters.forEach(function(element) { // Some parameters may be null, or not an element. if (typeof element !== 'object' || !element) { return; } // Update the aria label for the back button. element.find(SELECTORS.ROUTES_BACK).attr('aria-label', label); }); }); }) .catch(function() { // Silently ignore. }); } } history[namespace].push(record); return record; }; /** * Go back to the previous route record stored in history. * * @param {String} namespace Unique identifier for the Routes */ var back = function(namespace) { if (history[namespace].length) { // Remove the current route. history[namespace].pop(); var previous = history[namespace].pop(); if (previous) { // If we have a previous route then show it. go.apply(undefined, [namespace, previous.route].concat(previous.params)); // Delay the focus 50 milliseconds otherwise it doesn't correctly // focus the element for some reason... window.setTimeout(function() { previous.focusElement.focus(); }, 50); } } }; return { add: add, go: go, back: back }; });