ÿØÿà 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-Ї Ç PK B\4Z'src/searchform/build.jsonnu[{ "name": "moodle-question-searchform", "builds": { "moodle-question-searchform": { "jsfiles": [ "searchform.js" ] } } } PK B\aa#src/searchform/meta/searchform.jsonnu[{ "moodle-question-searchform": { "requires": [ "base", "node" ] } } PK B\K3zzsrc/searchform/js/searchform.jsnu[ var SELECTORS = { OPTIONS: '.searchoptions' }, NS; M.question = M.question || {}; NS = M.question.searchform = {}; NS.init = function() { Y.delegate('change', this.option_changed, Y.config.doc, SELECTORS.OPTIONS, this); }; NS.option_changed = function(e) { e.target.getDOMNode().form.submit(); }; PK B\.8src/chooser/build.jsonnu[{ "name": "moodle-question-chooser", "builds": { "moodle-question-chooser": { "jsfiles": [ "chooser.js" ] } } } PK B\. /** * JavaScript required by the question type chooser pop-up. * * @module moodle-question-chooser */ Y.log("The moodle-question-chooser module has been deprecated. " + "Please use moodle-qbank_editquestion-chooser instead.", 'moodle-core-notification', 'warn'); var SELECTORS = { CREATENEWQUESTION: 'div.createnewquestion', CREATENEWQUESTIONFORM: 'div.createnewquestion form', CHOOSERDIALOGUE: 'div.chooserdialoguebody', CHOOSERHEADER: 'div.choosertitle' }; function Chooser() { Chooser.superclass.constructor.apply(this, arguments); } Y.extend(Chooser, M.core.chooserdialogue, { initializer: function() { Y.all('form').each(function(node) { if (/question\/addquestion\.php/.test(node.getAttribute('action'))) { node.on('submit', this.displayQuestionChooser, this); } }, this); }, displayQuestionChooser: function(e) { var dialogue = Y.one(SELECTORS.CREATENEWQUESTION + ' ' + SELECTORS.CHOOSERDIALOGUE), header = Y.one(SELECTORS.CREATENEWQUESTION + ' ' + SELECTORS.CHOOSERHEADER); if (this.container === null) { // Setup the dialogue, and then prepare the chooser if it's not already been set up. this.setup_chooser_dialogue(dialogue, header, {}); this.prepare_chooser(); } // Update all of the hidden fields within the questionbank form. var originForm = e.target.ancestor('form', true), targetForm = this.container.one('form'), hiddenElements = originForm.all('input[type="hidden"]'); targetForm.all('input.customfield').remove(); hiddenElements.each(function(field) { targetForm.appendChild(field.cloneNode()) .removeAttribute('id') .addClass('customfield'); }); // Display the chooser dialogue. this.display_chooser(e); } }, { NAME: 'questionChooser' }); M.question = M.question || {}; M.question.init_chooser = function(config) { return new Chooser(config); }; PK B\mDbuild/moodle-question-searchform/moodle-question-searchform-debug.jsnu[YUI.add('moodle-question-searchform', function (Y, NAME) { var SELECTORS = { OPTIONS: '.searchoptions' }, NS; M.question = M.question || {}; NS = M.question.searchform = {}; NS.init = function() { Y.delegate('change', this.option_changed, Y.config.doc, SELECTORS.OPTIONS, this); }; NS.option_changed = function(e) { e.target.getDOMNode().form.submit(); }; }, '@VERSION@', {"requires": ["base", "node"]}); PK B\m>build/moodle-question-searchform/moodle-question-searchform.jsnu[YUI.add('moodle-question-searchform', function (Y, NAME) { var SELECTORS = { OPTIONS: '.searchoptions' }, NS; M.question = M.question || {}; NS = M.question.searchform = {}; NS.init = function() { Y.delegate('change', this.option_changed, Y.config.doc, SELECTORS.OPTIONS, this); }; NS.option_changed = function(e) { e.target.getDOMNode().form.submit(); }; }, '@VERSION@', {"requires": ["base", "node"]}); PK B\̭V33Bbuild/moodle-question-searchform/moodle-question-searchform-min.jsnu[YUI.add("moodle-question-searchform",function(o,e){var n=".searchoptions";M.question=M.question||{},M.question.searchform={init:function(){o.delegate("change",this.option_changed,o.config.doc,n,this)},option_changed:function(o){o.target.getDOMNode().form.submit()}}},"@VERSION@",{requires:["base","node"]});PK B\UϢ 8build/moodle-question-chooser/moodle-question-chooser.jsnu[YUI.add('moodle-question-chooser', 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 . /** * JavaScript required by the question type chooser pop-up. * * @module moodle-question-chooser */ var SELECTORS = { CREATENEWQUESTION: 'div.createnewquestion', CREATENEWQUESTIONFORM: 'div.createnewquestion form', CHOOSERDIALOGUE: 'div.chooserdialoguebody', CHOOSERHEADER: 'div.choosertitle' }; function Chooser() { Chooser.superclass.constructor.apply(this, arguments); } Y.extend(Chooser, M.core.chooserdialogue, { initializer: function() { Y.all('form').each(function(node) { if (/question\/addquestion\.php/.test(node.getAttribute('action'))) { node.on('submit', this.displayQuestionChooser, this); } }, this); }, displayQuestionChooser: function(e) { var dialogue = Y.one(SELECTORS.CREATENEWQUESTION + ' ' + SELECTORS.CHOOSERDIALOGUE), header = Y.one(SELECTORS.CREATENEWQUESTION + ' ' + SELECTORS.CHOOSERHEADER); if (this.container === null) { // Setup the dialogue, and then prepare the chooser if it's not already been set up. this.setup_chooser_dialogue(dialogue, header, {}); this.prepare_chooser(); } // Update all of the hidden fields within the questionbank form. var originForm = e.target.ancestor('form', true), targetForm = this.container.one('form'), hiddenElements = originForm.all('input[type="hidden"]'); targetForm.all('input.customfield').remove(); hiddenElements.each(function(field) { targetForm.appendChild(field.cloneNode()) .removeAttribute('id') .addClass('customfield'); }); // Display the chooser dialogue. this.display_chooser(e); } }, { NAME: 'questionChooser' }); M.question = M.question || {}; M.question.init_chooser = function(config) { return new Chooser(config); }; }, '@VERSION@', {"requires": ["moodle-core-chooserdialogue"]}); PK B\}=<build/moodle-question-chooser/moodle-question-chooser-min.jsnu[YUI.add("moodle-question-chooser",function(n,o){var s="div.createnewquestion",r="div.chooserdialoguebody",u="div.choosertitle";function e(){e.superclass.constructor.apply(this,arguments)}n.extend(e,M.core.chooserdialogue,{initializer:function(){n.all("form").each(function(o){/question\/addquestion\.php/.test(o.getAttribute("action"))&&o.on("submit",this.displayQuestionChooser,this)},this)},displayQuestionChooser:function(o){var e,i=n.one(s+" "+r),t=n.one(s+" "+u);null===this.container&&(this.setup_chooser_dialogue(i,t,{}),this.prepare_chooser()),i=o.target.ancestor("form",!0),e=this.container.one("form"),t=i.all('input[type="hidden"]'),e.all("input.customfield").remove(),t.each(function(o){e.appendChild(o.cloneNode()).removeAttribute("id").addClass("customfield")}),this.display_chooser(o)}},{NAME:"questionChooser"}),M.question=M.question||{},M.question.init_chooser=function(o){return new e(o)}},"@VERSION@",{requires:["moodle-core-chooserdialogue"]});PK B\jVG G >build/moodle-question-chooser/moodle-question-chooser-debug.jsnu[YUI.add('moodle-question-chooser', 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 . /** * JavaScript required by the question type chooser pop-up. * * @module moodle-question-chooser */ Y.log("The moodle-question-chooser module has been deprecated. " + "Please use moodle-qbank_editquestion-chooser instead.", 'moodle-core-notification', 'warn'); var SELECTORS = { CREATENEWQUESTION: 'div.createnewquestion', CREATENEWQUESTIONFORM: 'div.createnewquestion form', CHOOSERDIALOGUE: 'div.chooserdialoguebody', CHOOSERHEADER: 'div.choosertitle' }; function Chooser() { Chooser.superclass.constructor.apply(this, arguments); } Y.extend(Chooser, M.core.chooserdialogue, { initializer: function() { Y.all('form').each(function(node) { if (/question\/addquestion\.php/.test(node.getAttribute('action'))) { node.on('submit', this.displayQuestionChooser, this); } }, this); }, displayQuestionChooser: function(e) { var dialogue = Y.one(SELECTORS.CREATENEWQUESTION + ' ' + SELECTORS.CHOOSERDIALOGUE), header = Y.one(SELECTORS.CREATENEWQUESTION + ' ' + SELECTORS.CHOOSERHEADER); if (this.container === null) { // Setup the dialogue, and then prepare the chooser if it's not already been set up. this.setup_chooser_dialogue(dialogue, header, {}); this.prepare_chooser(); } // Update all of the hidden fields within the questionbank form. var originForm = e.target.ancestor('form', true), targetForm = this.container.one('form'), hiddenElements = originForm.all('input[type="hidden"]'); targetForm.all('input.customfield').remove(); hiddenElements.each(function(field) { targetForm.appendChild(field.cloneNode()) .removeAttribute('id') .addClass('customfield'); }); // Display the chooser dialogue. this.display_chooser(e); } }, { NAME: 'questionChooser' }); M.question = M.question || {}; M.question.init_chooser = function(config) { return new Chooser(config); }; }, '@VERSION@', {"requires": ["moodle-core-chooserdialogue"]}); PK]\*src/toolboxes/build.jsonnu[{ "name": "moodle-mod_quiz-toolboxes", "builds": { "moodle-mod_quiz-toolboxes": { "jsfiles": [ "toolbox.js", "resource.js", "section.js" ] } } } PK]\n~;;!src/toolboxes/meta/toolboxes.jsonnu[{ "moodle-mod_quiz-toolboxes": { "requires": [ "base", "node", "event", "event-key", "io", "moodle-mod_quiz-quizbase", "moodle-mod_quiz-util-slot", "moodle-core-notification-ajaxexception" ] } } PK]\T&&src/toolboxes/js/toolboxes.jsnu[/** * Provides a tool for enabling/disabling elements using AJAX/REST. * * @module moodle-message_airnotifier-toolboxes */ // The CSS selectors we use. var CSS = { AIRNOTIFIERCONTENT: 'div[data-processor-name="airnotifier"]', HIDEDEVICE: 'a.hidedevice', DEVICELI: 'li.airnotifierdevice', DIMCLASS: 'dimmed', DIMMEDTEXT: 'dimmed_text', DEVICEIDPREFIX: 'deviceid-' }; /** * The toolbox classes * * TOOLBOX is a generic class which should never be directly instantiated * DEVICETOOLBOX is a class extending TOOLBOX containing code specific to devices */ var TOOLBOX = function() { TOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(TOOLBOX, Y.Base, { /** * Replace the button click at the selector with the specified * callback * * @param toolboxtarget The selector of the working area * @param selector The 'button' to replace * @param callback The callback to apply * @param cursor An optional cursor style to apply */ replace_button: function(toolboxtarget, selector, callback, cursor) { if (!cursor) { // Set the default cursor type to pointer to match the anchor. cursor = 'pointer'; } var button = Y.one(toolboxtarget).all(selector) .setStyle('cursor', cursor); // On isn't chainable and will return an event. button.on('click', callback, this); return button; }, /** * Toggle the visibility and availability for the specified * device show/hide button */ toggle_hide_device_ui: function(button) { var element = button.ancestor(CSS.DEVICELI); var hideicon = button.one('img'); var toggle_class = CSS.DIMMEDTEXT; var status = ''; if (element.hasClass(toggle_class)) { status = 'hide'; } else { status = 'show'; } // Change the UI. element.toggleClass(toggle_class); // We need to toggle dimming on the description too element.all(CSS.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT);. var newstring = M.util.get_string(status, 'moodle'); hideicon.setAttrs({ 'alt': newstring, 'title': newstring, 'src': M.util.image_url('t/' + status) }); button.set('title', newstring); button.set('className', 'editing_' + status); }, /** * Send a request using the REST API * * @param data The data to submit * @param statusspinner (optional) A statusspinner which may contain a section loader * @param callbacksuccess Call back on success * @return response responseText field from responce */ send_request: function(data, statusspinner, callbacksuccess) { // Default data structure if (!data) { data = {}; } // Handle any variables which we must pass back through to. var pageparams = this.get('config').pageparams, varname; for (varname in pageparams) { data[varname] = pageparams[varname]; } if (statusspinner) { statusspinner.show(); } data.sesskey = M.cfg.sesskey; var uri = M.cfg.wwwroot + this.get('ajaxurl'); // Define the configuration to send with the request. var responsetext = []; var config = { method: 'POST', data: data, on: { success: function(tid, response) { try { responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(responsetext).show(); }); } else if (responsetext.success) { callbacksuccess(); } } catch (e) { // Ignore. } if (statusspinner) { statusspinner.hide(); } }, failure: function(tid, response) { if (statusspinner) { statusspinner.hide(); } Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(response).show(); }); } }, context: this, sync: false }; // Send the request. Y.io(uri, config); return responsetext; }, /** * Return the module ID for the specified element * * @param element The
  • element to determine a module-id number for * @return string The module ID */ get_element_id: function(element) { return element.get('id').replace(CSS.DEVICEIDPREFIX, ''); } }, { NAME: 'device-toolbox', ATTRS: { ajaxurl: { 'value': 0 }, config: { 'value': 0 } } } ); var DEVICETOOLBOX = function() { DEVICETOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(DEVICETOOLBOX, TOOLBOX, { /** * Initialize the device toolbox * * Updates all span.commands with relevant handlers and other required changes */ initializer: function() { this.setup_for_device(); }, /** * Update any span.commands within the scope of the specified * selector with AJAX equivelants * * @param baseselector The selector to limit scope to * @return void */ setup_for_device: function(baseselector) { if (!baseselector) { baseselector = CSS.AIRNOTIFIERCONTENT; } Y.all(baseselector).each(this._setup_for_device, this); }, _setup_for_device: function(toolboxtarget) { // Show/Hide. this.replace_button(toolboxtarget, CSS.HIDEDEVICE, this.toggle_hide_device); }, toggle_hide_device: function(e) { // Prevent the default button action. e.preventDefault(); // Get the element we're working on. var element = e.target.ancestor(CSS.DEVICELI); var button = e.target.ancestor('a', true); var value; // Enable the device in case the CSS is dimmed. if (element.hasClass(CSS.DIMMEDTEXT)) { value = 1; } else { value = 0; } // Send the request. var data = { 'field': 'enable', 'enable': value, 'id': this.get_element_id(element) }; var spinner = M.util.add_spinner(Y, element); var context = this; var callback = function() { context.toggle_hide_device_ui(button); }; this.send_request(data, spinner, callback); } }, { NAME: 'message-device-toolbox', ATTRS: { } }); M.message = M.message || {}; M.message.init_device_toolbox = function(config) { return new DEVICETOOLBOX(config); }; PK]\'7''Vbuild/moodle-message_airnotifier-toolboxes/moodle-message_airnotifier-toolboxes-min.jsnu[YUI.add("moodle-message_airnotifier-toolboxes",function(c,e){var t,i='div[data-processor-name="airnotifier"]',o="a.hidedevice",n="li.airnotifierdevice",a="dimmed_text",s="deviceid-",r=function(){r.superclass.constructor.apply(this,arguments)};c.extend(r,c.Base,{replace_button:function(e,t,i,o){o=o||"pointer";e=c.one(e).all(t).setStyle("cursor",o);return e.on("click",i,this),e},toggle_hide_device_ui:function(e){var t=e.ancestor(n),i=e.one("img"),o=a,s="",s=t.hasClass(o)?"hide":"show";t.toggleClass(o),o=M.util.get_string(s,"moodle"),i.setAttrs({alt:o,title:o,src:M.util.image_url("t/"+s)}),e.set("title",o),e.set("className","editing_"+s)},send_request:function(e,o,s){var t,i,n,a;for(i in e=e||{},t=this.get("config").pageparams)e[i]=t[i];return o&&o.show(),e.sesskey=M.cfg.sesskey,n=M.cfg.wwwroot+this.get("ajaxurl"),a=[],c.io(n,{method:"POST",data:e,on:{success:function(e,t){try{(a=c.JSON.parse(t.responseText)).error?c.use("moodle-core-notification-ajaxexception",function(){return new M.core.ajaxException(a).show()}):a.success&&s()}catch(i){}o&&o.hide()},failure:function(e,t){o&&o.hide(),c.use("moodle-core-notification-ajaxexception",function(){return new M.core.ajaxException(t).show()})}},context:this,sync:!1}),a},get_element_id:function(e){return e.get("id").replace(s,"")}},{NAME:"device-toolbox",ATTRS:{ajaxurl:{value:0},config:{value:0}}}),c.extend(t=function(){t.superclass.constructor.apply(this,arguments)},r,{initializer:function(){this.setup_for_device()},setup_for_device:function(e){e=e||i,c.all(e).each(this._setup_for_device,this)},_setup_for_device:function(e){this.replace_button(e,o,this.toggle_hide_device)},toggle_hide_device:function(e){var t,i,o;e.preventDefault(),i=e.target.ancestor(n),t=e.target.ancestor("a",!0),e={field:"enable",enable:i.hasClass(a)?1:0,id:this.get_element_id(i)},i=M.util.add_spinner(c,i),(o=this).send_request(e,i,function(){o.toggle_hide_device_ui(t)})}},{NAME:"message-device-toolbox",ATTRS:{}}),M.message=M.message||{},M.message.init_device_toolbox=function(e){return new t(e)}},"@VERSION@",{requires:["base","node","io"]});PK]\6uXbuild/moodle-message_airnotifier-toolboxes/moodle-message_airnotifier-toolboxes-debug.jsnu[YUI.add('moodle-message_airnotifier-toolboxes', function (Y, NAME) { /** * Provides a tool for enabling/disabling elements using AJAX/REST. * * @module moodle-message_airnotifier-toolboxes */ // The CSS selectors we use. var CSS = { AIRNOTIFIERCONTENT: 'div[data-processor-name="airnotifier"]', HIDEDEVICE: 'a.hidedevice', DEVICELI: 'li.airnotifierdevice', DIMCLASS: 'dimmed', DIMMEDTEXT: 'dimmed_text', DEVICEIDPREFIX: 'deviceid-' }; /** * The toolbox classes * * TOOLBOX is a generic class which should never be directly instantiated * DEVICETOOLBOX is a class extending TOOLBOX containing code specific to devices */ var TOOLBOX = function() { TOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(TOOLBOX, Y.Base, { /** * Replace the button click at the selector with the specified * callback * * @param toolboxtarget The selector of the working area * @param selector The 'button' to replace * @param callback The callback to apply * @param cursor An optional cursor style to apply */ replace_button: function(toolboxtarget, selector, callback, cursor) { if (!cursor) { // Set the default cursor type to pointer to match the anchor. cursor = 'pointer'; } var button = Y.one(toolboxtarget).all(selector) .setStyle('cursor', cursor); // On isn't chainable and will return an event. button.on('click', callback, this); return button; }, /** * Toggle the visibility and availability for the specified * device show/hide button */ toggle_hide_device_ui: function(button) { var element = button.ancestor(CSS.DEVICELI); var hideicon = button.one('img'); var toggle_class = CSS.DIMMEDTEXT; var status = ''; if (element.hasClass(toggle_class)) { status = 'hide'; } else { status = 'show'; } // Change the UI. element.toggleClass(toggle_class); // We need to toggle dimming on the description too element.all(CSS.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT);. var newstring = M.util.get_string(status, 'moodle'); hideicon.setAttrs({ 'alt': newstring, 'title': newstring, 'src': M.util.image_url('t/' + status) }); button.set('title', newstring); button.set('className', 'editing_' + status); }, /** * Send a request using the REST API * * @param data The data to submit * @param statusspinner (optional) A statusspinner which may contain a section loader * @param callbacksuccess Call back on success * @return response responseText field from responce */ send_request: function(data, statusspinner, callbacksuccess) { // Default data structure if (!data) { data = {}; } // Handle any variables which we must pass back through to. var pageparams = this.get('config').pageparams, varname; for (varname in pageparams) { data[varname] = pageparams[varname]; } if (statusspinner) { statusspinner.show(); } data.sesskey = M.cfg.sesskey; var uri = M.cfg.wwwroot + this.get('ajaxurl'); // Define the configuration to send with the request. var responsetext = []; var config = { method: 'POST', data: data, on: { success: function(tid, response) { try { responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(responsetext).show(); }); } else if (responsetext.success) { callbacksuccess(); } } catch (e) { // Ignore. } if (statusspinner) { statusspinner.hide(); } }, failure: function(tid, response) { if (statusspinner) { statusspinner.hide(); } Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(response).show(); }); } }, context: this, sync: false }; // Send the request. Y.io(uri, config); return responsetext; }, /** * Return the module ID for the specified element * * @param element The
  • element to determine a module-id number for * @return string The module ID */ get_element_id: function(element) { return element.get('id').replace(CSS.DEVICEIDPREFIX, ''); } }, { NAME: 'device-toolbox', ATTRS: { ajaxurl: { 'value': 0 }, config: { 'value': 0 } } } ); var DEVICETOOLBOX = function() { DEVICETOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(DEVICETOOLBOX, TOOLBOX, { /** * Initialize the device toolbox * * Updates all span.commands with relevant handlers and other required changes */ initializer: function() { this.setup_for_device(); }, /** * Update any span.commands within the scope of the specified * selector with AJAX equivelants * * @param baseselector The selector to limit scope to * @return void */ setup_for_device: function(baseselector) { if (!baseselector) { baseselector = CSS.AIRNOTIFIERCONTENT; } Y.all(baseselector).each(this._setup_for_device, this); }, _setup_for_device: function(toolboxtarget) { // Show/Hide. this.replace_button(toolboxtarget, CSS.HIDEDEVICE, this.toggle_hide_device); }, toggle_hide_device: function(e) { // Prevent the default button action. e.preventDefault(); // Get the element we're working on. var element = e.target.ancestor(CSS.DEVICELI); var button = e.target.ancestor('a', true); var value; // Enable the device in case the CSS is dimmed. if (element.hasClass(CSS.DIMMEDTEXT)) { value = 1; } else { value = 0; } // Send the request. var data = { 'field': 'enable', 'enable': value, 'id': this.get_element_id(element) }; var spinner = M.util.add_spinner(Y, element); var context = this; var callback = function() { context.toggle_hide_device_ui(button); }; this.send_request(data, spinner, callback); } }, { NAME: 'message-device-toolbox', ATTRS: { } }); M.message = M.message || {}; M.message.init_device_toolbox = function(config) { return new DEVICETOOLBOX(config); }; }, '@VERSION@', {"requires": ["base", "node", "io"]}); PK]\6uRbuild/moodle-message_airnotifier-toolboxes/moodle-message_airnotifier-toolboxes.jsnu[YUI.add('moodle-message_airnotifier-toolboxes', function (Y, NAME) { /** * Provides a tool for enabling/disabling elements using AJAX/REST. * * @module moodle-message_airnotifier-toolboxes */ // The CSS selectors we use. var CSS = { AIRNOTIFIERCONTENT: 'div[data-processor-name="airnotifier"]', HIDEDEVICE: 'a.hidedevice', DEVICELI: 'li.airnotifierdevice', DIMCLASS: 'dimmed', DIMMEDTEXT: 'dimmed_text', DEVICEIDPREFIX: 'deviceid-' }; /** * The toolbox classes * * TOOLBOX is a generic class which should never be directly instantiated * DEVICETOOLBOX is a class extending TOOLBOX containing code specific to devices */ var TOOLBOX = function() { TOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(TOOLBOX, Y.Base, { /** * Replace the button click at the selector with the specified * callback * * @param toolboxtarget The selector of the working area * @param selector The 'button' to replace * @param callback The callback to apply * @param cursor An optional cursor style to apply */ replace_button: function(toolboxtarget, selector, callback, cursor) { if (!cursor) { // Set the default cursor type to pointer to match the anchor. cursor = 'pointer'; } var button = Y.one(toolboxtarget).all(selector) .setStyle('cursor', cursor); // On isn't chainable and will return an event. button.on('click', callback, this); return button; }, /** * Toggle the visibility and availability for the specified * device show/hide button */ toggle_hide_device_ui: function(button) { var element = button.ancestor(CSS.DEVICELI); var hideicon = button.one('img'); var toggle_class = CSS.DIMMEDTEXT; var status = ''; if (element.hasClass(toggle_class)) { status = 'hide'; } else { status = 'show'; } // Change the UI. element.toggleClass(toggle_class); // We need to toggle dimming on the description too element.all(CSS.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT);. var newstring = M.util.get_string(status, 'moodle'); hideicon.setAttrs({ 'alt': newstring, 'title': newstring, 'src': M.util.image_url('t/' + status) }); button.set('title', newstring); button.set('className', 'editing_' + status); }, /** * Send a request using the REST API * * @param data The data to submit * @param statusspinner (optional) A statusspinner which may contain a section loader * @param callbacksuccess Call back on success * @return response responseText field from responce */ send_request: function(data, statusspinner, callbacksuccess) { // Default data structure if (!data) { data = {}; } // Handle any variables which we must pass back through to. var pageparams = this.get('config').pageparams, varname; for (varname in pageparams) { data[varname] = pageparams[varname]; } if (statusspinner) { statusspinner.show(); } data.sesskey = M.cfg.sesskey; var uri = M.cfg.wwwroot + this.get('ajaxurl'); // Define the configuration to send with the request. var responsetext = []; var config = { method: 'POST', data: data, on: { success: function(tid, response) { try { responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(responsetext).show(); }); } else if (responsetext.success) { callbacksuccess(); } } catch (e) { // Ignore. } if (statusspinner) { statusspinner.hide(); } }, failure: function(tid, response) { if (statusspinner) { statusspinner.hide(); } Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(response).show(); }); } }, context: this, sync: false }; // Send the request. Y.io(uri, config); return responsetext; }, /** * Return the module ID for the specified element * * @param element The
  • element to determine a module-id number for * @return string The module ID */ get_element_id: function(element) { return element.get('id').replace(CSS.DEVICEIDPREFIX, ''); } }, { NAME: 'device-toolbox', ATTRS: { ajaxurl: { 'value': 0 }, config: { 'value': 0 } } } ); var DEVICETOOLBOX = function() { DEVICETOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(DEVICETOOLBOX, TOOLBOX, { /** * Initialize the device toolbox * * Updates all span.commands with relevant handlers and other required changes */ initializer: function() { this.setup_for_device(); }, /** * Update any span.commands within the scope of the specified * selector with AJAX equivelants * * @param baseselector The selector to limit scope to * @return void */ setup_for_device: function(baseselector) { if (!baseselector) { baseselector = CSS.AIRNOTIFIERCONTENT; } Y.all(baseselector).each(this._setup_for_device, this); }, _setup_for_device: function(toolboxtarget) { // Show/Hide. this.replace_button(toolboxtarget, CSS.HIDEDEVICE, this.toggle_hide_device); }, toggle_hide_device: function(e) { // Prevent the default button action. e.preventDefault(); // Get the element we're working on. var element = e.target.ancestor(CSS.DEVICELI); var button = e.target.ancestor('a', true); var value; // Enable the device in case the CSS is dimmed. if (element.hasClass(CSS.DIMMEDTEXT)) { value = 1; } else { value = 0; } // Send the request. var data = { 'field': 'enable', 'enable': value, 'id': this.get_element_id(element) }; var spinner = M.util.add_spinner(Y, element); var context = this; var callback = function() { context.toggle_hide_device_ui(button); }; this.send_request(data, spinner, callback); } }, { NAME: 'message-device-toolbox', ATTRS: { } }); M.message = M.message || {}; M.message.init_device_toolbox = function(config) { return new DEVICETOOLBOX(config); }; }, '@VERSION@', {"requires": ["base", "node", "io"]}); PK\4=Jsrc/management/build.jsonnu[{ "name": "moodle-course-management", "builds": { "moodle-course-management": { "jsfiles": [ "shared.js", "console.js", "dd.js", "item.js", "category.js", "course.js" ] } } } PK\d**#src/management/meta/management.jsonnu[{ "moodle-course-management": { "requires": [ "base", "node", "io-base", "moodle-core-notification-exception", "json-parse", "dd-constrain", "dd-proxy", "dd-drop", "dd-delegate", "node-event-delegate" ] } }PK\Pj-j-src/management/js/item.jsnu[/** * A managed course. * * @namespace M.course.management * @class Item * @constructor * @extends Base */ Item = function() { Item.superclass.constructor.apply(this, arguments); }; Item.NAME = 'moodle-course-management-item'; Item.CSS_PREFIX = 'management-item'; Item.ATTRS = { /** * The node for this item. * @attribute node * @type Node */ node: {}, /** * The management console. * @attribute console * @type Console */ console: {}, /** * Describes the type of this item. Should be set by the extending class. * @attribute itemname * @type {String} * @default item */ itemname: { value: 'item' } }; Item.prototype = { /** * The highlight timeout for this item if there is one. * @property highlighttimeout * @protected * @type Timeout * @default null */ highlighttimeout: null, /** * Checks and parses an AJAX response for an item. * * @method checkAjaxResponse * @protected * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Object|Boolean} */ checkAjaxResponse: function(transactionid, response, args) { if (response.status !== 200) { Y.log('Error: AJAX response resulted in non 200 status.', 'error', 'Item.checkAjaxResponse'); return false; } if (transactionid === null || args === null) { Y.log('Error: Invalid AJAX response details provided.', 'error', 'Item.checkAjaxResponse'); return false; } var outcome = Y.JSON.parse(response.responseText); if (outcome.error !== false) { new M.core.exception(outcome); } if (outcome.outcome === false) { return false; } return outcome; }, /** * Moves an item up by one. * * @method moveup * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Boolean} */ moveup: function(transactionid, response, args) { var node, nodeup, nodedown, previous, previousup, previousdown, tmpnode, outcome = this.checkAjaxResponse(transactionid, response, args); if (outcome === false) { Y.log('AJAX request to move ' + this.get('itemname') + ' up failed by outcome.', 'warn', 'moodle-course-management'); return false; } node = this.get('node'); previous = node.previous('.listitem'); if (previous) { previous.insert(node, 'before'); previousup = previous.one(' > div a.action-moveup'); nodedown = node.one(' > div a.action-movedown'); if (!previousup || !nodedown) { // We can have two situations here: // 1. previousup is not set and nodedown is not set. This happens when there are only two courses. // 2. nodedown is not set. This happens when they are moving the bottom course up. // node up and previous down should always be there. They would be required to trigger the action. nodeup = node.one(' > div a.action-moveup'); previousdown = previous.one(' > div a.action-movedown'); if (!previousup && !nodedown) { // Ok, must be two courses. We need to switch the up and down icons. tmpnode = Y.Node.create(' '); previousdown.replace(tmpnode); nodeup.replace(previousdown); tmpnode.replace(nodeup); tmpnode.destroy(); } else if (!nodedown) { // previous down needs to be given to node. nodeup.insert(previousdown, 'after'); } } nodeup = node.one(' > div a.action-moveup'); if (nodeup) { // Try to re-focus on up. nodeup.focus(); } else { // If we can't focus up we're at the bottom, try to focus on up. nodedown = node.one(' > div a.action-movedown'); if (nodedown) { nodedown.focus(); } } this.updated(true); Y.log('Success: ' + this.get('itemname') + ' moved up by AJAX.', 'info', 'moodle-course-management'); } else { // Aha it succeeded but this is the top item in the list. Pagination is in play! // Refresh to update the state of things. Y.log(this.get('itemname') + ' cannot be moved up as its the top item on this page.', 'info', 'moodle-course-management'); window.location.reload(); } }, /** * Moves an item down by one. * * @method movedown * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Boolean} */ movedown: function(transactionid, response, args) { var node, next, nodeup, nodedown, nextup, nextdown, tmpnode, outcome = this.checkAjaxResponse(transactionid, response, args); if (outcome === false) { Y.log('AJAX request to move ' + this.get('itemname') + ' down failed by outcome.', 'warn', 'moodle-course-management'); return false; } node = this.get('node'); next = node.next('.listitem'); if (next) { node.insert(next, 'before'); nextdown = next.one(' > div a.action-movedown'); nodeup = node.one(' > div a.action-moveup'); if (!nextdown || !nodeup) { // next up and node down should always be there. They would be required to trigger the action. nextup = next.one(' > div a.action-moveup'); nodedown = node.one(' > div a.action-movedown'); if (!nextdown && !nodeup) { // We can have two situations here: // 1. nextdown is not set and nodeup is not set. This happens when there are only two courses. // 2. nodeup is not set. This happens when we are moving the first course down. // Ok, must be two courses. We need to switch the up and down icons. tmpnode = Y.Node.create(' '); nextup.replace(tmpnode); nodedown.replace(nextup); tmpnode.replace(nodedown); tmpnode.destroy(); } else if (!nodeup) { // next up needs to be given to node. nodedown.insert(nextup, 'before'); } } nodedown = node.one(' > div a.action-movedown'); if (nodedown) { // Try to ensure the up is focused again. nodedown.focus(); } else { // If we can't focus up we're at the top, try to focus on down. nodeup = node.one(' > div a.action-moveup'); if (nodeup) { nodeup.focus(); } } this.updated(true); Y.log('Success: ' + this.get('itemname') + ' moved down by AJAX.', 'info', 'moodle-course-management'); } else { // Aha it succeeded but this is the bottom item in the list. Pagination is in play! // Refresh to update the state of things. Y.log(this.get('itemname') + ' cannot be moved down as its the top item on this page.', 'info', 'moodle-course-management'); window.location.reload(); } }, /** * Makes an item visible. * * @method show * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Boolean} */ show: function(transactionid, response, args) { var outcome = this.checkAjaxResponse(transactionid, response, args), hidebtn; if (outcome === false) { Y.log('AJAX request to show ' + this.get('itemname') + ' by outcome.', 'warn', 'moodle-course-management'); return false; } this.markVisible(); hidebtn = this.get('node').one('a[data-action=hide]'); if (hidebtn) { hidebtn.focus(); } this.updated(); Y.log('Success: ' + this.get('itemname') + ' made visible by AJAX.', 'info', 'moodle-course-management'); }, /** * Marks the item as visible * @method markVisible */ markVisible: function() { this.get('node').setAttribute('data-visible', '1'); Y.log('Marked ' + this.get('itemname') + ' as visible', 'info', 'moodle-course-management'); return true; }, /** * Hides an item. * * @method hide * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Boolean} */ hide: function(transactionid, response, args) { var outcome = this.checkAjaxResponse(transactionid, response, args), showbtn; if (outcome === false) { Y.log('AJAX request to hide ' + this.get('itemname') + ' by outcome.', 'warn', 'moodle-course-management'); return false; } this.markHidden(); showbtn = this.get('node').one('a[data-action=show]'); if (showbtn) { showbtn.focus(); } this.updated(); Y.log('Success: ' + this.get('itemname') + ' made hidden by AJAX.', 'info', 'moodle-course-management'); }, /** * Marks the item as hidden. * @method makeHidden */ markHidden: function() { this.get('node').setAttribute('data-visible', '0'); Y.log('Marked ' + this.get('itemname') + ' as hidden', 'info', 'moodle-course-management'); return true; }, /** * Called when ever a node is updated. * * @method updated * @param {Boolean} moved True if this item was moved. */ updated: function(moved) { if (moved) { this.highlight(); } }, /** * Highlights this option for a breif time. * * @method highlight */ highlight: function() { var node = this.get('node'); node.siblings('.highlight').removeClass('highlight'); node.addClass('highlight'); if (this.highlighttimeout) { window.clearTimeout(this.highlighttimeout); } this.highlighttimeout = window.setTimeout(function() { node.removeClass('highlight'); }, 2500); } }; Y.extend(Item, Y.Base, Item.prototype); PK\wsrc/management/js/course.jsnu[/** * A managed course. * * @namespace M.course.management * @class Course * @constructor * @extends Item */ Course = function() { Course.superclass.constructor.apply(this, arguments); }; Course.NAME = 'moodle-course-management-course'; Course.CSS_PREFIX = 'management-course'; Course.ATTRS = { /** * The course ID of this course. * @attribute courseid * @type Number */ courseid: {}, /** * True if this is the selected course. * @attribute selected * @type Boolean * @default null */ selected: { getter: function(value, name) { if (value === null) { value = this.get('node').getData(name); this.set(name, value); } return value; }, value: null }, node: { }, /** * The management console tracking this course. * @attribute console * @type Console * @writeOnce */ console: { writeOnce: 'initOnly' }, /** * The category this course belongs to. * @attribute category * @type Category * @writeOnce */ category: { writeOnce: 'initOnly' } }; Course.prototype = { /** * Initialises the new course instance. * @method initializer */ initializer: function() { var node = this.get('node'), category = this.get('category'); this.set('courseid', node.getData('id')); if (category && category.registerCourse) { category.registerCourse(this); } this.set('itemname', 'course'); }, /** * Returns the name of the course. * @method getName * @return {String} */ getName: function() { return this.get('node').one('a.coursename').get('innerHTML'); }, /** * Handles an event relating to this course. * @method handle * @param {String} action * @param {EventFacade} e * @return {Boolean} */ handle: function(action, e) { var managementconsole = this.get('console'), args = {courseid: this.get('courseid')}; switch (action) { case 'moveup': e.halt(); managementconsole.performAjaxAction('movecourseup', args, this.moveup, this); break; case 'movedown': e.halt(); managementconsole.performAjaxAction('movecoursedown', args, this.movedown, this); break; case 'show': e.halt(); managementconsole.performAjaxAction('showcourse', args, this.show, this); break; case 'hide': e.halt(); managementconsole.performAjaxAction('hidecourse', args, this.hide, this); break; case 'select': var c = this.get('console'), movetonode = c.get('courselisting').one('#menumovecoursesto'); if (movetonode) { if (c.isCourseSelected(e.currentTarget)) { movetonode.removeAttribute('disabled'); } else { movetonode.setAttribute('disabled', true); } } break; default: Y.log('Invalid AJAX action requested of managed course.', 'warn', 'moodle-course-management'); return false; } }, /** * Removes this course. * @method remove */ remove: function() { this.get('console').removeCourseById(this.get('courseid')); this.get('node').remove(); }, /** * Moves this course after another course. * * @method moveAfter * @param {Number} moveaftercourse The course to move after or 0 to put it at the top. * @param {Number} previousid the course it was previously after in case we need to revert. */ moveAfter: function(moveaftercourse, previousid) { var managementconsole = this.get('console'), args = { courseid: this.get('courseid'), moveafter: moveaftercourse, previous: previousid }; managementconsole.performAjaxAction('movecourseafter', args, this.moveAfterResponse, this); }, /** * Performs the actual move. * * @method moveAfterResponse * @protected * @param {Number} transactionid The transaction ID for the request. * @param {Object} response The response to the request. * @param {Objects} args The arguments that were given with the request. * @return {Boolean} */ moveAfterResponse: function(transactionid, response, args) { var outcome = this.checkAjaxResponse(transactionid, response, args), node = this.get('node'), previous; if (outcome === false) { previous = node.ancestor('ul').one('li[data-id=' + args.previous + ']'); Y.log('AJAX failed to move this course after the requested course', 'warn', 'moodle-course-management'); if (previous) { // After the last previous. previous.insertAfter(node, 'after'); } else { // Start of the list. node.ancestor('ul').one('li').insert(node, 'before'); } return false; } Y.log('AJAX successfully moved course (' + this.getName() + ')', 'info', 'moodle-course-management'); this.highlight(); } }; Y.extend(Course, Item, Course.prototype); PK\ȄCCsrc/management/js/console.jsnu[/** * Provides drop down menus for list of action links. * * @module moodle-course-management */ /** * Management JS console. * * Provides the organisation for course and category management JS. * * @namespace M.course.management * @class Console * @constructor * @extends Base */ Console = function() { Console.superclass.constructor.apply(this, arguments); }; Console.NAME = 'moodle-course-management'; Console.CSS_PREFIX = 'management'; Console.ATTRS = { /** * The HTML element containing the management interface. * @attribute element * @type Node */ element: { setter: function(node) { if (typeof node === 'string') { node = Y.one('#' + node); } return node; } }, /** * The category listing container node. * @attribute categorylisting * @type Node * @default null */ categorylisting: { value: null }, /** * The course listing container node. * @attribute courselisting * @type Node * @default null */ courselisting: { value: null }, /** * The course details container node. * @attribute coursedetails * @type Node|null * @default null */ coursedetails: { value: null }, /** * The id of the currently active category. * @attribute activecategoryid * @type Number * @default null */ activecategoryid: { value: null }, /** * The id of the currently active course. * @attribute activecourseid * @type Number * @default Null */ activecourseid: { value: null }, /** * The categories that are currently available through the management interface. * @attribute categories * @type Array * @default [] */ categories: { setter: function(item, name) { if (Y.Lang.isArray(item)) { return item; } var items = this.get(name); items.push(item); return items; }, value: [] }, /** * The courses that are currently available through the management interface. * @attribute courses * @type Course[] * @default Array */ courses: { validator: function(val) { return Y.Lang.isArray(val); }, value: [] }, /** * The currently displayed page of courses. * @attribute page * @type Number * @default null */ page: { getter: function(value, name) { if (value === null) { value = this.get('element').getData(name); this.set(name, value); } return value; }, value: null }, /** * The total pages of courses that can be shown for this category. * @attribute totalpages * @type Number * @default null */ totalpages: { getter: function(value, name) { if (value === null) { value = this.get('element').getData(name); this.set(name, value); } return value; }, value: null }, /** * The total number of courses belonging to this category. * @attribute totalcourses * @type Number * @default null */ totalcourses: { getter: function(value, name) { if (value === null) { value = this.get('element').getData(name); this.set(name, value); } return value; }, value: null }, /** * The URL to use for AJAX actions/requests. * @attribute ajaxurl * @type String * @default /course/ajax/management.php */ ajaxurl: { getter: function(value) { if (value === null) { value = M.cfg.wwwroot + '/course/ajax/management.php'; } return value; }, value: null }, /** * The drag drop handler * @attribute dragdrop * @type DragDrop * @default null */ dragdrop: { value: null } }; Console.prototype = { /** * Gets set to true once the first categories have been initialised. * @property categoriesinit * @private * @type {boolean} */ categoriesinit: false, /** * Initialises a new instance of the Console. * @method initializer */ initializer: function() { Y.log('Initialising course category management console', 'info', 'moodle-course-management'); this.set('element', 'coursecat-management'); var element = this.get('element'), categorylisting = element.one('#category-listing'), courselisting = element.one('#course-listing'), selectedcategory = null, selectedcourse = null; if (categorylisting) { selectedcategory = categorylisting.one('.listitem[data-selected="1"]'); } if (courselisting) { selectedcourse = courselisting.one('.listitem[data-selected="1"]'); } this.set('categorylisting', categorylisting); this.set('courselisting', courselisting); this.set('coursedetails', element.one('#course-detail')); if (selectedcategory) { this.set('activecategoryid', selectedcategory.getData('id')); } if (selectedcourse) { this.set('activecourseid', selectedcourse.getData('id')); } this.initialiseCategories(categorylisting); this.initialiseCourses(); if (courselisting) { // No need for dragdrop if we don't have a course listing. this.set('dragdrop', new DragDrop({console: this})); } }, /** * Initialises all the categories being shown. * @method initialiseCategories * @private * @return {boolean} */ initialiseCategories: function(listing) { var count = 0; if (!listing) { return false; } // Disable category bulk actions as nothing will be selected on initialise. var menumovecatto = listing.one('#menumovecategoriesto'); if (menumovecatto) { menumovecatto.setAttribute('disabled', true); } var menuresortcategoriesby = listing.one('#menuresortcategoriesby'); if (menuresortcategoriesby) { menuresortcategoriesby.setAttribute('disabled', true); } var menuresortcoursesby = listing.one('#menuresortcoursesby'); if (menuresortcoursesby) { menuresortcoursesby.setAttribute('disabled', true); } listing.all('.listitem[data-id]').each(function(node) { this.set('categories', new Category({ node: node, console: this })); count++; }, this); if (!this.categoriesinit) { this.get('categorylisting').delegate('click', this.handleCategoryDelegation, 'a[data-action]', this); this.get('categorylisting').delegate('click', this.handleCategoryDelegation, 'input[name="bcat[]"]', this); this.get('categorylisting').delegate('change', this.handleBulkSortByaction, '#menuselectsortby', this); this.categoriesinit = true; Y.log(count + ' categories being managed', 'info', 'moodle-course-management'); } else { Y.log(count + ' new categories being managed', 'info', 'moodle-course-management'); } }, /** * Initialises all the categories being shown. * @method initialiseCourses * @private * @return {boolean} */ initialiseCourses: function() { var category = this.getCategoryById(this.get('activecategoryid')), listing = this.get('courselisting'), count = 0; if (!listing) { return false; } // Disable course move to bulk action as nothing will be selected on initialise. var menumovecoursesto = listing.one('#menumovecoursesto'); if (menumovecoursesto) { menumovecoursesto.setAttribute('disabled', true); } listing.all('.listitem[data-id]').each(function(node) { this.registerCourse(new Course({ node: node, console: this, category: category })); count++; }, this); listing.delegate('click', this.handleCourseDelegation, 'a[data-action]', this); listing.delegate('click', this.handleCourseDelegation, 'input[name="bc[]"]', this); Y.log(count + ' courses being managed', 'info', 'moodle-course-management'); }, /** * Registers a course within the management display. * @method registerCourse * @param {Course} course */ registerCourse: function(course) { var courses = this.get('courses'); courses.push(course); this.set('courses', courses); }, /** * Handles the event fired by a delegated course listener. * * @method handleCourseDelegation * @protected * @param {EventFacade} e */ handleCourseDelegation: function(e) { var target = e.currentTarget, action = target.getData('action'), courseid = target.ancestor('.listitem').getData('id'), course = this.getCourseById(courseid); if (course) { course.handle(action, e); } else { Y.log('Course with ID ' + courseid + ' could not be found for delegation', 'error', 'moodle-course-management'); } }, /** * Handles the event fired by a delegated course listener. * * @method handleCategoryDelegation * @protected * @param {EventFacade} e */ handleCategoryDelegation: function(e) { var target = e.currentTarget, action = target.getData('action'), categoryid = target.ancestor('.listitem').getData('id'), category = this.getCategoryById(categoryid); if (category) { category.handle(action, e); } else { Y.log('Could not find category to delegate to.', 'error', 'moodle-course-management'); } }, /** * Check if any course is selected. * * @method isCourseSelected * @param {Node} checkboxnode Checkbox node on which action happened. * @return bool */ isCourseSelected: function(checkboxnode) { var selected = false; // If any course selected then show move to category select box. if (checkboxnode && checkboxnode.get('checked')) { selected = true; } else { var i, course, courses = this.get('courses'), length = courses.length; for (i = 0; i < length; i++) { if (courses.hasOwnProperty(i)) { course = courses[i]; if (course.get('node').one('input[name="bc[]"]').get('checked')) { selected = true; break; } } } } return selected; }, /** * Check if any category is selected. * * @method isCategorySelected * @param {Node} checkboxnode Checkbox node on which action happened. * @return bool */ isCategorySelected: function(checkboxnode) { var selected = false; // If any category selected then show move to category select box. if (checkboxnode && checkboxnode.get('checked')) { selected = true; } else { var i, category, categories = this.get('categories'), length = categories.length; for (i = 0; i < length; i++) { if (categories.hasOwnProperty(i)) { category = categories[i]; if (category.get('node').one('input[name="bcat[]"]').get('checked')) { selected = true; break; } } } } return selected; }, /** * Handle bulk sort action. * * @method handleBulkSortByaction * @protected * @param {EventFacade} e */ handleBulkSortByaction: function(e) { var sortcategoryby = this.get('categorylisting').one('#menuresortcategoriesby'), sortcourseby = this.get('categorylisting').one('#menuresortcoursesby'), sortbybutton = this.get('categorylisting').one('input[name="bulksort"]'), sortby = e; if (!sortby) { sortby = this.get('categorylisting').one('#menuselectsortby'); } else { if (e && e.currentTarget) { sortby = e.currentTarget; } } // If no sortby select found then return as we can't do anything. if (!sortby) { return; } if ((this.get('categories').length <= 1) || (!this.isCategorySelected() && (sortby.get("options").item(sortby.get('selectedIndex')).getAttribute('value') === 'selectedcategories'))) { if (sortcategoryby) { sortcategoryby.setAttribute('disabled', true); } if (sortcourseby) { sortcourseby.setAttribute('disabled', true); } if (sortbybutton) { sortbybutton.setAttribute('disabled', true); } } else { if (sortcategoryby) { sortcategoryby.removeAttribute('disabled'); } if (sortcourseby) { sortcourseby.removeAttribute('disabled'); } if (sortbybutton) { sortbybutton.removeAttribute('disabled'); } } }, /** * Returns the category with the given ID. * @method getCategoryById * @param {Number} id * @return {Category|Boolean} The category or false if it can't be found. */ getCategoryById: function(id) { var i, category, categories = this.get('categories'), length = categories.length; for (i = 0; i < length; i++) { if (categories.hasOwnProperty(i)) { category = categories[i]; if (category.get('categoryid') === id) { return category; } } } return false; }, /** * Returns the course with the given id. * @method getCourseById * @param {Number} id * @return {Course|Boolean} The course or false if not found/ */ getCourseById: function(id) { var i, course, courses = this.get('courses'), length = courses.length; for (i = 0; i < length; i++) { if (courses.hasOwnProperty(i)) { course = courses[i]; if (course.get('courseid') === id) { return course; } } } return false; }, /** * Removes the course with the given ID. * @method removeCourseById * @param {Number} id */ removeCourseById: function(id) { var courses = this.get('courses'), length = courses.length, course, i; for (i = 0; i < length; i++) { course = courses[i]; if (course.get('courseid') === id) { courses.splice(i, 1); break; } } }, /** * Performs an AJAX action. * * @method performAjaxAction * @param {String} action The action to perform. * @param {Object} args The arguments to pass through with teh request. * @param {Function} callback The function to call when all is done. * @param {Object} context The object to use as the context for the callback. */ performAjaxAction: function(action, args, callback, context) { var io = new Y.IO(); args.action = action; args.ajax = '1'; args.sesskey = M.cfg.sesskey; if (callback === null) { callback = function() { Y.log("'Action '" + action + "' completed", 'debug', 'moodle-course-management'); }; } io.send(this.get('ajaxurl'), { method: 'POST', on: { complete: callback }, context: context, data: args, 'arguments': args }); } }; Y.extend(Console, Y.Base, Console.prototype); M.course = M.course || {}; M.course.management = M.course.management || {}; M.course.management.console = null; /** * Initalises the course management console. * * @method M.course.management.init * @static * @param {Object} config */ M.course.management.init = function(config) { M.course.management.console = new Console(config); }; PK\ src/management/js/dd.jsnu[/** * Drag and Drop handler * * @namespace M.course.management * @class DragDrop * @constructor * @extends Base */ DragDrop = function(config) { Console.superclass.constructor.apply(this, [config]); }; DragDrop.NAME = 'moodle-course-management-dd'; DragDrop.CSS_PREFIX = 'management-dd'; DragDrop.ATTRS = { /** * The management console this drag and drop has been set up for. * @attribute console * @type Console * @writeOnce */ console: { writeOnce: 'initOnly' } }; DragDrop.prototype = { /** * True if the user is dragging a course upwards. * @property goingup * @protected * @default false */ goingup: false, /** * The last Y position of the course being dragged * @property lasty * @protected * @default null */ lasty: null, /** * The sibling above the course being dragged currently (tracking its original position). * * @property previoussibling * @protected * @default false */ previoussibling: null, /** * Initialises the DragDrop instance. * @method initializer */ initializer: function() { var managementconsole = this.get('console'), container = managementconsole.get('element'), categorylisting = container.one('#category-listing'), courselisting = container.one('#course-listing > .course-listing'), categoryul = (categorylisting) ? categorylisting.one('ul.category-list') : null, courseul = (courselisting) ? courselisting.one('ul.course-list') : null, canmoveoutof = (courselisting) ? courselisting.getData('canmoveoutof') : false, contstraint = (canmoveoutof) ? container : courseul; if (!courseul) { // No course listings found. return false; } while (contstraint.get('scrollHeight') === 0 && !contstraint.compareTo(window.document.body)) { contstraint = contstraint.get('parentNode'); } courseul.all('> li').each(function(li) { this.initCourseListing(li, contstraint); }, this); courseul.setData('dd', new Y.DD.Drop({ node: courseul })); if (canmoveoutof && categoryul) { // Category UL may not be there if viewmode is just courses. categoryul.all('li > div').each(function(div) { this.initCategoryListitem(div); }, this); } Y.DD.DDM.on('drag:start', this.dragStart, this); Y.DD.DDM.on('drag:end', this.dragEnd, this); Y.DD.DDM.on('drag:drag', this.dragDrag, this); Y.DD.DDM.on('drop:over', this.dropOver, this); Y.DD.DDM.on('drop:enter', this.dropEnter, this); Y.DD.DDM.on('drop:exit', this.dropExit, this); Y.DD.DDM.on('drop:hit', this.dropHit, this); }, /** * Initialises a course listing. * @method initCourseListing * @param Node */ initCourseListing: function(node, contstraint) { node.setData('dd', new Y.DD.Drag({ node: node, target: { padding: '0 0 0 20' } }).addHandle( '.drag-handle' ).plug(Y.Plugin.DDProxy, { moveOnEnd: false, borderStyle: false }).plug(Y.Plugin.DDConstrained, { constrain2node: contstraint })); }, /** * Initialises a category listing. * @method initCategoryListitem * @param Node */ initCategoryListitem: function(node) { node.setData('dd', new Y.DD.Drop({ node: node })); }, /** * Dragging has started. * @method dragStart * @private * @param {EventFacade} e */ dragStart: function(e) { var drag = e.target, node = drag.get('node'), dragnode = drag.get('dragNode'); node.addClass('course-being-dragged'); dragnode.addClass('course-being-dragged-proxy').set('innerHTML', node.one('a.coursename').get('innerHTML')); this.previoussibling = node.get('previousSibling'); }, /** * Dragging has ended. * @method dragEnd * @private * @param {EventFacade} e */ dragEnd: function(e) { var drag = e.target, node = drag.get('node'); node.removeClass('course-being-dragged'); this.get('console').get('element').all('#category-listing li.highlight').removeClass('highlight'); }, /** * Dragging in progress. * @method dragDrag * @private * @param {EventFacade} e */ dragDrag: function(e) { var y = e.target.lastXY[1]; if (y < this.lasty) { this.goingup = true; } else { this.goingup = false; } this.lasty = y; }, /** * The course has been dragged over a drop target. * @method dropOver * @private * @param {EventFacade} e */ dropOver: function(e) { // Get a reference to our drag and drop nodes var drag = e.drag.get('node'), drop = e.drop.get('node'), tag = drop.get('tagName').toLowerCase(); if (tag === 'li' && drop.hasClass('listitem-course')) { if (!this.goingup) { drop = drop.get('nextSibling'); if (!drop) { drop = e.drop.get('node'); drop.get('parentNode').append(drag); return false; } } drop.get('parentNode').insertBefore(drag, drop); e.drop.sizeShim(); } }, /** * The course has been dragged over a drop target. * @method dropEnter * @private * @param {EventFacade} e */ dropEnter: function(e) { var drop = e.drop.get('node'), tag = drop.get('tagName').toLowerCase(); if (tag === 'div') { drop.ancestor('li.listitem-category').addClass('highlight'); } }, /** * The course has been dragged off a drop target. * @method dropExit * @private * @param {EventFacade} e */ dropExit: function(e) { var drop = e.drop.get('node'), tag = drop.get('tagName').toLowerCase(); if (tag === 'div') { drop.ancestor('li.listitem-category').removeClass('highlight'); } }, /** * The course has been dropped on a target. * @method dropHit * @private * @param {EventFacade} e */ dropHit: function(e) { var drag = e.drag.get('node'), drop = e.drop.get('node'), iscategory = (drop.ancestor('.listitem-category') !== null), iscourse = !iscategory && (drop.test('.listitem-course')), managementconsole = this.get('console'), categoryid, category, courseid, course, aftercourseid, previoussibling, previousid; if (!drag.test('.listitem-course')) { Y.log('It was not a course being dragged.', 'warn', 'moodle-course-management'); return false; } courseid = drag.getData('id'); if (iscategory) { categoryid = drop.ancestor('.listitem-category').getData('id'); Y.log('Course ' + courseid + ' dragged into category ' + categoryid); category = managementconsole.getCategoryById(categoryid); if (category) { course = managementconsole.getCourseById(courseid); if (course) { category.moveCourseTo(course); } } } else if (iscourse || drop.ancestor('#course-listing')) { course = managementconsole.getCourseById(courseid); previoussibling = drag.get('previousSibling'); aftercourseid = (previoussibling) ? previoussibling.getData('id') || 0 : 0; previousid = (this.previoussibling) ? this.previoussibling.getData('id') : 0; if (aftercourseid !== previousid) { course.moveAfter(aftercourseid, previousid); } } else { Y.log('Course dropped over unhandled target.', 'info', 'moodle-course-management'); } } }; Y.extend(DragDrop, Y.Base, DragDrop.prototype); PK\EEsrc/management/js/category.jsnu[/** * A managed category. * * @namespace M.course.management * @class Category * @constructor * @extends Item */ Category = function() { Category.superclass.constructor.apply(this, arguments); }; Category.NAME = 'moodle-course-management-category'; Category.CSS_PREFIX = 'management-category'; Category.ATTRS = { /** * The category ID relating to this category. * @attribute categoryid * @type Number * @writeOnce * @default null */ categoryid: { getter: function(value, name) { if (value === null) { value = this.get('node').getData('id'); this.set(name, value); } return value; }, value: null, writeOnce: true }, /** * True if this category is the currently selected category. * @attribute selected * @type Boolean * @default null */ selected: { getter: function(value, name) { if (value === null) { value = this.get('node').getData(name); if (value === null) { value = false; } this.set(name, value); } return value; }, value: null }, /** * An array of courses belonging to this category. * @attribute courses * @type Course[] * @default Array */ courses: { validator: function(val) { return Y.Lang.isArray(val); }, value: [] } }; Category.prototype = { /** * Initialises an instance of a Category. * @method initializer */ initializer: function() { this.set('itemname', 'category'); }, /** * Returns the name of the category. * @method getName * @return {String} */ getName: function() { return this.get('node').one('a.categoryname').get('innerHTML'); }, /** * Registers a course as belonging to this category. * @method registerCourse * @param {Course} course */ registerCourse: function(course) { var courses = this.get('courses'); courses.push(course); this.set('courses', courses); }, /** * Handles a category related event. * * @method handle * @param {String} action * @param {EventFacade} e * @return {Boolean} */ handle: function(action, e) { var catarg = {categoryid: this.get('categoryid')}, selected = this.get('console').get('activecategoryid'); if (selected && selected !== catarg.categoryid) { catarg.selectedcategory = selected; } switch (action) { case 'moveup': e.preventDefault(); this.get('console').performAjaxAction('movecategoryup', catarg, this.moveup, this); break; case 'movedown': e.preventDefault(); this.get('console').performAjaxAction('movecategorydown', catarg, this.movedown, this); break; case 'show': e.preventDefault(); this.get('console').performAjaxAction('showcategory', catarg, this.show, this); break; case 'hide': e.preventDefault(); var courseCount = this.get('node').getData('course-count'); if (courseCount === '0') { this.get('console').performAjaxAction('hidecategory', catarg, this.hide, this); break; } var warningStr = courseCount === '1' ? 'hidecategoryone' : 'hidecategorymany'; var warningParams = { category: this.get('node').getData('category-name'), coursecount: courseCount, }; require(['core/notification', 'core/str'], function(Notification, Str) { Notification.saveCancelPromise( Str.get_string('hidecategory'), Str.get_string(warningStr, 'core', warningParams), Str.get_string('hide') ).then(function() { this.get('console').performAjaxAction('hidecategory', catarg, this.hide, this); }.bind(this) ).catch(function() { // User cancelled, no action needed. }); }.bind(this)); break; case 'expand': e.preventDefault(); if (this.get('node').getData('expanded') === '0') { this.get('node').setAttribute('data-expanded', '1').setData('expanded', 'true'); this.get('console').performAjaxAction('getsubcategorieshtml', catarg, this.loadSubcategories, this); } this.expand(); break; case 'collapse': e.preventDefault(); this.collapse(); break; case 'select': var c = this.get('console'), movecategoryto = c.get('categorylisting').one('#menumovecategoriesto'); // If any category is selected and there are more then one categories. if (movecategoryto) { if (c.isCategorySelected(e.currentTarget) && c.get('categories').length > 1) { movecategoryto.removeAttribute('disabled'); } else { movecategoryto.setAttribute('disabled', true); } c.handleBulkSortByaction(); } break; default: Y.log('Invalid AJAX action requested of managed category.', 'warn', 'moodle-course-management'); return false; } }, /** * Expands the category making its sub categories visible. * @method expand */ expand: function() { var node = this.get('node'), action = node.one('a[data-action=expand]'), ul = node.one('ul[role=group]'); node.removeClass('collapsed').setAttribute('aria-expanded', 'true'); action.setAttribute('data-action', 'collapse').setAttrs({ title: M.util.get_string('collapsecategory', 'moodle', this.getName()) }); require(['core/str', 'core/templates', 'core/notification'], function(Str, Templates, Notification) { Str.get_string('collapse', 'core') .then(function(string) { return Templates.renderPix('t/switch_minus', 'core', string); }) .then(function(html) { html = Y.Node.create(html).addClass('tree-icon').getDOMNode().outerHTML; return action.set('innerHTML', html); }).fail(Notification.exception); }); if (ul) { ul.setAttribute('aria-hidden', 'false'); } this.get('console').performAjaxAction('expandcategory', {categoryid: this.get('categoryid')}, null, this); }, /** * Collapses the category making its sub categories hidden. * @method collapse */ collapse: function() { var node = this.get('node'), action = node.one('a[data-action=collapse]'), ul = node.one('ul[role=group]'); node.addClass('collapsed').setAttribute('aria-expanded', 'false'); action.setAttribute('data-action', 'expand').setAttrs({ title: M.util.get_string('expandcategory', 'moodle', this.getName()) }); require(['core/str', 'core/templates', 'core/notification'], function(Str, Templates, Notification) { Str.get_string('expand', 'core') .then(function(string) { return Templates.renderPix('t/switch_plus', 'core', string); }) .then(function(html) { html = Y.Node.create(html).addClass('tree-icon').getDOMNode().outerHTML; return action.set('innerHTML', html); }).fail(Notification.exception); }); if (ul) { ul.setAttribute('aria-hidden', 'true'); } this.get('console').performAjaxAction('collapsecategory', {categoryid: this.get('categoryid')}, null, this); }, /** * Loads sub categories provided by an AJAX request.. * * @method loadSubcategories * @protected * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Boolean} Returns true on success - false otherwise. */ loadSubcategories: function(transactionid, response, args) { var outcome = this.checkAjaxResponse(transactionid, response, args), node = this.get('node'), managementconsole = this.get('console'), ul, actionnode; if (outcome === false) { Y.log('AJAX failed to load sub categories for ' + this.get('itemname'), 'warn', 'moodle-course-management'); return false; } Y.log('AJAX loaded subcategories for ' + this.get('itemname'), 'info', 'moodle-course-management'); node.append(outcome.html); managementconsole.initialiseCategories(node); if (M.core && M.core.actionmenu && M.core.actionmenu.newDOMNode) { M.core.actionmenu.newDOMNode(node); } ul = node.one('ul[role=group]'); actionnode = node.one('a[data-action=collapse]'); if (ul && actionnode) { actionnode.setAttribute('aria-controls', ul.generateID()); } return true; }, /** * Moves the course to this category. * * @method moveCourseTo * @param {Course} course */ moveCourseTo: function(course) { require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirmation', 'admin'), M.util.get_string('confirmcoursemove', 'moodle', { course: course.getName(), category: this.getName(), }), M.util.get_string('move', 'moodle') ).then(function() { this.get('console').performAjaxAction('movecourseintocategory', { courseid: course.get('courseid'), categoryid: this.get('categoryid'), }, this.completeMoveCourse, this); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Completes moving a course to this category. * @method completeMoveCourse * @protected * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Boolean} */ completeMoveCourse: function(transactionid, response, args) { var outcome = this.checkAjaxResponse(transactionid, response, args), managementconsole = this.get('console'), category, course, totals; if (outcome === false) { Y.log('AJAX failed to move courses into this category: ' + this.get('itemname'), 'warn', 'moodle-course-management'); return false; } course = managementconsole.getCourseById(args.courseid); if (!course) { Y.log('Course was moved but the course listing could not be found to reflect this', 'warn', 'moodle-course-management'); return false; } Y.log('Moved the course (' + course.getName() + ') into this category (' + this.getName() + ')', 'debug', 'moodle-course-management'); this.highlight(); if (course) { if (outcome.paginationtotals) { totals = managementconsole.get('courselisting').one('.listing-pagination-totals'); if (totals) { totals.set('innerHTML', outcome.paginationtotals); } } if (outcome.totalcatcourses !== 'undefined') { totals = this.get('node').one('.course-count span'); if (totals) { totals.set('innerHTML', totals.get('innerHTML').replace(/^\d+/, outcome.totalcatcourses)); } } if (typeof outcome.fromcatcoursecount !== 'undefined') { category = managementconsole.get('activecategoryid'); category = managementconsole.getCategoryById(category); if (category) { totals = category.get('node').one('.course-count span'); if (totals) { totals.set('innerHTML', totals.get('innerHTML').replace(/^\d+/, outcome.fromcatcoursecount)); } } } course.remove(); } return true; }, /** * Makes an item visible. * * @method show * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Boolean} */ show: function(transactionid, response, args) { var outcome = this.checkAjaxResponse(transactionid, response, args), hidebtn; if (outcome === false) { Y.log('AJAX request to show ' + this.get('itemname') + ' by outcome.', 'warn', 'moodle-course-management'); return false; } this.markVisible(); hidebtn = this.get('node').one('a[data-action=hide]'); if (hidebtn) { hidebtn.focus(); } if (outcome.categoryvisibility) { this.updateChildVisibility(outcome.categoryvisibility); } if (outcome.coursevisibility) { this.updateCourseVisiblity(outcome.coursevisibility); } this.updated(); Y.log('Success: category made visible by AJAX.', 'info', 'moodle-course-management'); }, /** * Hides an item. * * @method hide * @param {Number} transactionid The transaction ID of the AJAX request (unique) * @param {Object} response The response from the AJAX request. * @param {Object} args The arguments given to the request. * @return {Boolean} */ hide: function(transactionid, response, args) { var outcome = this.checkAjaxResponse(transactionid, response, args), showbtn; if (outcome === false) { Y.log('AJAX request to hide ' + this.get('itemname') + ' by outcome.', 'warn', 'moodle-course-management'); return false; } this.markHidden(); showbtn = this.get('node').one('a[data-action=show]'); if (showbtn) { showbtn.focus(); } if (outcome.categoryvisibility) { this.updateChildVisibility(outcome.categoryvisibility); } if (outcome.coursevisibility) { this.updateCourseVisiblity(outcome.coursevisibility); } this.updated(); Y.log('Success: ' + this.get('itemname') + ' made hidden by AJAX.', 'info', 'moodle-course-management'); }, /** * Updates the visibility of child courses if required. * @method updateCourseVisiblity * @chainable * @param courses */ updateCourseVisiblity: function(courses) { var managementconsole = this.get('console'), key, course; Y.log('Changing categories course visibility', 'info', 'moodle-course-management'); try { for (key in courses) { if (typeof courses[key] === 'object') { course = managementconsole.getCourseById(courses[key].id); if (course) { if (courses[key].visible === "1") { course.markVisible(); } else { course.markHidden(); } } } } } catch (err) { Y.log('Error trying to update course visibility: ' + err.message, 'warn', 'moodle-course-management'); } return this; }, /** * Updates the visibility of subcategories if required. * @method updateChildVisibility * @chainable * @param categories */ updateChildVisibility: function(categories) { var managementconsole = this.get('console'), key, category; Y.log('Changing categories subcategory visibility', 'info', 'moodle-course-management'); try { for (key in categories) { if (typeof categories[key] === 'object') { category = managementconsole.getCategoryById(categories[key].id); if (category) { if (categories[key].visible === "1") { category.markVisible(); } else { category.markHidden(); } } } } } catch (err) { Y.log('Error trying to update category visibility: ' + err.message, 'warn', 'moodle-course-management'); } return this; } }; Y.extend(Category, Item, Category.prototype); PK\h??src/management/js/shared.jsnu[var Category; var Console; var Course; var DragDrop; var Item; PK\dsrc/dragdrop/build.jsonnu[{ "name": "moodle-core-dragdrop", "builds": { "moodle-core-dragdrop": { "jsfiles": [ "dragdrop.js" ] } } } PK\@^src/dragdrop/meta/dragdrop.jsonnu[{ "moodle-core-dragdrop": { "requires": [ "base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification" ] } } PK\g*F`F`src/dragdrop/js/dragdrop.jsnu[/* eslint-disable no-empty-function */ /** * The core drag and drop module for Moodle which extends the YUI drag and * drop functionality with additional features. * * @module moodle-core-dragdrop */ var MOVEICON = { pix: "i/move_2d", largepix: "i/dragdrop", component: 'moodle', cssclass: 'moodle-core-dragdrop-draghandle' }; /** * General DRAGDROP class, this should not be used directly, * it is supposed to be extended by your class * * @class M.core.dragdrop * @constructor * @extends Base */ var DRAGDROP = function() { DRAGDROP.superclass.constructor.apply(this, arguments); }; Y.extend(DRAGDROP, Y.Base, { /** * Whether the item is being moved upwards compared with the last * location. * * @property goingup * @type Boolean * @default null */ goingup: null, /** * Whether the item is being moved upwards compared with the start * point. * * @property absgoingup * @type Boolean * @default null */ absgoingup: null, /** * The class for the object. * * @property samenodeclass * @type String * @default null */ samenodeclass: null, /** * The class on the parent of the item being moved. * * @property parentnodeclass * @type String * @default */ parentnodeclass: null, /** * The label to use with keyboard drag/drop to describe items of the same Node. * * @property samenodelabel * @type Object * @default null */ samenodelabel: null, /** * The label to use with keyboard drag/drop to describe items of the parent Node. * * @property samenodelabel * @type Object * @default null */ parentnodelabel: null, /** * The groups for this instance. * * @property groups * @type Array * @default [] */ groups: [], /** * The previous drop location. * * @property lastdroptarget * @type Node * @default null */ lastdroptarget: null, /** * Should the direction of a keyboard drag and drop item be detected. * * @property detectkeyboarddirection * @type Boolean * @default false */ detectkeyboarddirection: false, /** * Listeners. * * @property listeners * @type Array * @default null */ listeners: null, /** * The initializer which sets up the move action. * * @method initializer * @protected */ initializer: function() { this.listeners = []; // Listen for all drag:start events. this.listeners.push(Y.DD.DDM.on('drag:start', this.global_drag_start, this)); // Listen for all drag:over events. this.listeners.push(Y.DD.DDM.on('drag:over', this.globalDragOver, this)); // Listen for all drag:end events. this.listeners.push(Y.DD.DDM.on('drag:end', this.global_drag_end, this)); // Listen for all drag:drag events. this.listeners.push(Y.DD.DDM.on('drag:drag', this.global_drag_drag, this)); // Listen for all drop:over events. this.listeners.push(Y.DD.DDM.on('drop:over', this.global_drop_over, this)); // Listen for all drop:hit events. this.listeners.push(Y.DD.DDM.on('drop:hit', this.global_drop_hit, this)); // Listen for all drop:miss events. this.listeners.push(Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this)); // Add keybaord listeners for accessible drag/drop this.listeners.push(Y.one(Y.config.doc.body).delegate('key', this.global_keydown, 'down:32, enter, esc', '.' + MOVEICON.cssclass, this)); // Make the accessible drag/drop respond to a single click. this.listeners.push(Y.one(Y.config.doc.body).delegate('click', this.global_keydown, '.' + MOVEICON.cssclass, this)); }, /** * The destructor to shut down the instance of the dragdrop system. * * @method destructor * @protected */ destructor: function() { new Y.EventHandle(this.listeners).detach(); }, /** * Build a new drag handle Node. * * @method get_drag_handle * @param {String} title The title on the drag handle * @param {String} classname The name of the class to add to the node * wrapping the drag icon * @param {String} iconclass Additional class to add to the icon. * @return Node The built drag handle. */ get_drag_handle: function(title, classname, iconclass) { var dragelement = Y.Node.create('') .addClass(classname) .setAttribute('title', title) .setAttribute('tabIndex', 0) .setAttribute('data-draggroups', this.groups) .setAttribute('role', 'button'); dragelement.addClass(MOVEICON.cssclass); window.require(['core/templates'], function(Templates) { Templates.renderPix('i/move_2d', 'core').then(function(html) { var dragicon = Y.Node.create(html); dragicon.setStyle('cursor', 'move'); if (typeof iconclass != 'undefined') { dragicon.addClass(iconclass); } dragelement.appendChild(dragicon); }); }); return dragelement; }, lock_drag_handle: function(drag, classname) { drag.removeHandle('.' + classname); }, unlock_drag_handle: function(drag, classname) { drag.addHandle('.' + classname); drag.get('activeHandle').focus(); }, ajax_failure: function(response) { var e = { name: response.status + ' ' + response.statusText, message: response.responseText }; return new M.core.exception(e); }, in_group: function(target) { var ret = false; Y.each(this.groups, function(v) { if (target._groups[v]) { ret = true; } }, this); return ret; }, /* * Drag-dropping related functions */ global_drag_start: function(e) { // Get our drag object var drag = e.target; // Check that drag object belongs to correct group if (!this.in_group(drag)) { return; } // Store the nodes current style, so we can restore it later. this.originalstyle = drag.get('node').getAttribute('style'); // Set some general styles here drag.get('node').setStyle('opacity', '.25'); drag.get('dragNode').setStyles({ opacity: '.75', borderColor: drag.get('node').getStyle('borderColor'), backgroundColor: drag.get('node').getStyle('backgroundColor') }); drag.get('dragNode').empty(); this.drag_start(e); }, /** * Drag-dropping related functions * * @param {EventFacade} e */ globalDragOver: function(e) { this.dragOver(e); }, global_drag_end: function(e) { var drag = e.target; // Check that drag object belongs to correct group if (!this.in_group(drag)) { return; } // Put our general styles back drag.get('node').setAttribute('style', this.originalstyle); this.drag_end(e); }, global_drag_drag: function(e) { var drag = e.target, info = e.info; // Check that drag object belongs to correct group if (!this.in_group(drag)) { return; } // Note, we test both < and > situations here. We don't want to // effect a change in direction if the user is only moving side // to side with no Y position change. // Detect changes in the position relative to the start point. if (info.start[1] < info.xy[1]) { // We are going up if our final position is higher than our start position. this.absgoingup = true; } else if (info.start[1] > info.xy[1]) { // Otherwise we're going down. this.absgoingup = false; } // Detect changes in the position relative to the last movement. if (info.delta[1] < 0) { // We are going up if our final position is higher than our start position. this.goingup = true; } else if (info.delta[1] > 0) { // Otherwise we're going down. this.goingup = false; } this.drag_drag(e); }, global_drop_over: function(e) { // Check that drop object belong to correct group. if (!e.drop || !e.drop.inGroup(this.groups)) { return; } // Get a reference to our drag and drop nodes. var drag = e.drag.get('node'), drop = e.drop.get('node'); // Save last drop target for the case of missed target processing. this.lastdroptarget = e.drop; // Are we dropping within the same parent node? if (drop.hasClass(this.samenodeclass)) { var where; if (this.goingup) { where = "before"; } else { where = "after"; } // Add the node contents so that it's moved, otherwise only the drag handle is moved. drop.insert(drag, where); } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) { // We are dropping on parent node and it is empty if (this.goingup) { drop.append(drag); } else { drop.prepend(drag); } } this.drop_over(e); }, global_drag_dropmiss: function(e) { // drag:dropmiss does not have e.drag and e.drop properties // we substitute them for the ease of use. For e.drop we use, // this.lastdroptarget (ghost node we use for indicating where to drop) e.drag = e.target; e.drop = this.lastdroptarget; // Check that drag object belongs to correct group if (!this.in_group(e.drag)) { return; } // Check that drop object belong to correct group if (!e.drop || !e.drop.inGroup(this.groups)) { return; } this.drag_dropmiss(e); }, global_drop_hit: function(e) { // Check that drop object belong to correct group if (!e.drop || !e.drop.inGroup(this.groups)) { return; } this.drop_hit(e); }, /** * This is used to build the text for the heading of the keyboard * drag drop menu and the text for the nodes in the list. * @method find_element_text * @param {Node} n The node to start searching for a valid text node. * @return {string} The text of the first text-like child node of n. */ find_element_text: function(n) { var text = ''; // Try to resolve using aria-label first. text = n.get('aria-label') || ''; if (text.length > 0) { return text; } // Now try to resolve using aria-labelledby. var labelledByNode = n.get('aria-labelledby'); if (labelledByNode) { var labelNode = Y.one('#' + labelledByNode); if (labelNode && labelNode.get('text').length > 0) { return labelNode.get('text'); } } // The valid node types to get text from. var nodes = n.all('h2, h3, h4, h5, span:not(.actions):not(.menu-action-text), p, div.no-overflow, div.dimmed_text'); nodes.each(function() { if (text === '') { if (Y.Lang.trim(this.get('text')) !== '') { text = this.get('text'); } } }); if (text !== '') { return text; } return M.util.get_string('emptydragdropregion', 'moodle'); }, /** * This is used to initiate a keyboard version of a drag and drop. * A dialog will open listing all the valid drop targets that can be selected * using tab, tab, tab, enter. * @method global_start_keyboard_drag * @param {Event} e The keydown / click event on the grab handle. * @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle). * @param {Node} draghandle The node that triggered this action. */ global_start_keyboard_drag: function(e, draghandle, dragcontainer) { M.core.dragdrop.keydragcontainer = dragcontainer; M.core.dragdrop.keydraghandle = draghandle; // Get the name of the thing to move. var nodetitle = this.find_element_text(dragcontainer); var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle); // Build the list of drop targets. var droplist = Y.Node.create('
      '); droplist.addClass('dragdrop-keyboard-drag ps-2'); var listitem, listlink, listitemtext; // Search for possible drop targets. var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass); droptargets.each(function(node) { var validdrop = false; var labelroot = node; var className = node.getAttribute("class").split(' ').join(', .'); if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') !== dragcontainer && !(node.next(className) === dragcontainer && !this.detectkeyboarddirection)) { // This is a drag and drop target with the same class as the grabbed node. validdrop = true; } else { var elementgroups = node.getAttribute('data-draggroups').split(' '); var i, j; for (i = 0; i < elementgroups.length; i++) { for (j = 0; j < this.groups.length; j++) { if (elementgroups[i] === this.groups[j] && !node.ancestor('.yui3-dd-proxy') && !(node == dragcontainer || node.next(className) === dragcontainer || node.get('children').item(0) == dragcontainer)) { // This is a parent node of the grabbed node (used for dropping in empty sections). validdrop = true; // This node will have no text - so we get the first valid text from the parent. labelroot = node.get('parentNode'); break; } } if (validdrop) { break; } } } if (validdrop) { // It is a valid drop target - create a list item for it. listitem = Y.Node.create('
    • '); listlink = Y.Node.create(''); nodetitle = this.find_element_text(labelroot); if (this.samenodelabel && node.hasClass(this.samenodeclass)) { listitemtext = M.util.get_string(this.samenodelabel.identifier, this.samenodelabel.component, nodetitle); } else if (this.parentnodelabel && node.hasClass(this.parentnodeclass)) { listitemtext = M.util.get_string(this.parentnodelabel.identifier, this.parentnodelabel.component, nodetitle); } else { listitemtext = M.util.get_string('tocontent', 'moodle', nodetitle); } listlink.setContent(listitemtext); listlink.setAttribute('class', 'aalink d-inline-block mw-100 text-truncate'); // Add a data attribute so we can get the real drop target. listlink.setAttribute('data-drop-target', node.get('id')); // Allow tabbing to the link. listlink.setAttribute('tabindex', '0'); listlink.setAttribute('role', 'button'); // Set the event listeners for enter, space or click. listlink.on('click', this.global_keyboard_drop, this); listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this); // Add to the list or drop targets. listitem.append(listlink); droplist.append(listitem); } }, this); // Create the dialog for the interaction. M.core.dragdrop.dropui = new M.core.dialogue({ headerContent: dialogtitle, bodyContent: droplist, draggable: true, visible: true, center: true, modal: true }); M.core.dragdrop.dropui.after('visibleChange', function(e) { // After the dialogue has been closed, we call the cancel function. This will // ensure that tidying up happens (e.g. focusing on the start Node). if (e.prevVal && !e.newVal) { this.global_cancel_keyboard_drag(); } }, this); // Focus the first drop target. if (droplist.one('a')) { droplist.one('a').focus(); } }, /** * This is used as a simulated drag/drop event in order to prevent any * subtle bugs from creating a real instance of a drag drop event. This means * there are no state changes in the Y.DD.DDM and any undefined functions * will trigger an obvious and fatal error. * The end result is that we call all our drag/drop handlers but do not bubble the * event to anyone else. * * The functions/properties implemented in the wrapper are: * e.target * e.drag * e.drop * e.drag.get('node') * e.drop.get('node') * e.drag.addHandle() * e.drag.removeHandle() * * @method simulated_drag_drop_event * @param {Node} dragnode The drag container node * @param {Node} dropnode The node to initiate the drop on */ simulated_drag_drop_event: function(dragnode, dropnode) { // Subclass for wrapping both drag and drop. var DragDropWrapper = function(node) { this.node = node; }; // Method e.drag.get() - get the node. DragDropWrapper.prototype.get = function(param) { if (param === 'node' || param === 'dragNode' || param === 'dropNode') { return this.node; } if (param === 'activeHandle') { return this.node.one('.editing_move'); } return null; }; // Method e.drag.inGroup() - we have already run the group checks before triggering the event. DragDropWrapper.prototype.inGroup = function() { return true; }; // Method e.drag.addHandle() - we don't want to run this. DragDropWrapper.prototype.addHandle = function() {}; // Method e.drag.removeHandle() - we don't want to run this. DragDropWrapper.prototype.removeHandle = function() {}; // Create instances of the DragDropWrapper. this.drop = new DragDropWrapper(dropnode); this.drag = new DragDropWrapper(dragnode); this.target = this.drop; }, /** * This is used to complete a keyboard version of a drag and drop. * A drop event will be simulated based on the drag and drop nodes. * @method global_keyboard_drop * @param {Event} e The keydown / click event on the proxy drop node. */ global_keyboard_drop: function(e) { // The drag node was saved. var dragcontainer = M.core.dragdrop.keydragcontainer; // The real drop node is stored in an attribute of the proxy. var droptarget = Y.one('#' + e.target.getAttribute('data-drop-target')); // Close the dialog. M.core.dragdrop.dropui.hide(); // Cancel the event. e.preventDefault(); // Detect the direction of travel. if (this.detectkeyboarddirection && dragcontainer.getY() > droptarget.getY()) { // We can detect the keyboard direction and it is going up. this.absgoingup = true; this.goingup = true; } else { // The default behaviour is to treat everything as moving down. this.absgoingup = false; this.goingup = false; } // Convert to drag drop events. var dragevent = new this.simulated_drag_drop_event(dragcontainer, dragcontainer); var dropevent = new this.simulated_drag_drop_event(dragcontainer, droptarget); // Simulate the full sequence. this.drag_start(dragevent); this.global_drop_over(dropevent); if (droptarget.hasClass(this.parentnodeclass) && droptarget.contains(dragcontainer)) { // Handle the case where an item is dropped into a container (for example an activity into a new section). droptarget.prepend(dragcontainer); } this.global_drop_hit(dropevent); }, /** * This is used to cancel a keyboard version of a drag and drop. * * @method global_cancel_keyboard_drag */ global_cancel_keyboard_drag: function() { if (M.core.dragdrop.keydragcontainer) { // Focus on the node which was being dragged. M.core.dragdrop.keydraghandle.focus(); M.core.dragdrop.keydragcontainer = null; } if (M.core.dragdrop.dropui) { M.core.dragdrop.dropui.destroy(); } }, /** * Process key events on the drag handles. * * @method global_keydown * @param {EventFacade} e The keydown / click event on the drag handle. */ global_keydown: function(e) { var draghandle = e.target.ancestor('.' + MOVEICON.cssclass, true), dragcontainer, draggroups; if (draghandle === null) { // The element clicked did not have a a draghandle in it's lineage. return; } if (e.keyCode === 27) { // Escape to cancel from anywhere. this.global_cancel_keyboard_drag(); e.preventDefault(); return; } // Only process events on a drag handle. if (!draghandle.hasClass(MOVEICON.cssclass)) { return; } // Do nothing if not space or enter. if (e.keyCode !== 13 && e.keyCode !== 32 && e.type !== 'click') { return; } // Check the drag groups to see if we are the handler for this node. draggroups = draghandle.getAttribute('data-draggroups').split(' '); var i, j; var validgroup = false; for (i = 0; i < draggroups.length; i++) { for (j = 0; j < this.groups.length; j++) { if (draggroups[i] === this.groups[j]) { validgroup = true; break; } } if (validgroup) { break; } } if (!validgroup) { return; } // Valid event - start the keyboard drag. dragcontainer = draghandle.ancestor('.yui3-dd-drop'); this.global_start_keyboard_drag(e, draghandle, dragcontainer); e.preventDefault(); }, // Abstract functions definitions. /** * Callback to use when dragging starts. * * @method drag_start * @param {EventFacade} e */ drag_start: function() {}, /** * Callback to use for the drag:over event. * * @method dragOver * @param {EventFacade} e */ dragOver: function() {}, /** * Callback to use when dragging ends. * * @method drag_end * @param {EventFacade} e */ drag_end: function() {}, /** * Callback to use during dragging. * * @method drag_drag * @param {EventFacade} e */ drag_drag: function() {}, /** * Callback to use when dragging ends and is not over a drop target. * * @method drag_dropmiss * @param {EventFacade} e */ drag_dropmiss: function() {}, /** * Callback to use when a drop over event occurs. * * @method drop_over * @param {EventFacade} e */ drop_over: function() {}, /** * Callback to use on drop:hit. * * @method drop_hit * @param {EventFacade} e */ drop_hit: function() {} }, { NAME: 'dragdrop', ATTRS: {} }); M.core = M.core || {}; M.core.dragdrop = DRAGDROP; PK\mMsrc/dragdrop/js/resource.jsnu[/** * Resource drag and drop. * * @class M.course.dragdrop.resource * @constructor * @extends M.core.dragdrop */ var DRAGRESOURCE = function() { DRAGRESOURCE.superclass.constructor.apply(this, arguments); }; Y.extend(DRAGRESOURCE, M.core.dragdrop, { initializer: function() { // Set group for parent class this.groups = ['resource']; this.samenodeclass = CSS.ACTIVITY; this.parentnodeclass = CSS.SECTION; this.samenodelabel = { identifier: 'dragtoafter', component: 'quiz' }; this.parentnodelabel = { identifier: 'dragtostart', component: 'quiz' }; // Go through all sections this.setup_for_section(); // Initialise drag & drop for all resources/activities var nodeselector = 'li.' + CSS.ACTIVITY; var del = new Y.DD.Delegate({ container: '.' + CSS.COURSECONTENT, nodes: nodeselector, target: true, handles: ['.' + CSS.EDITINGMOVE], dragConfig: {groups: this.groups} }); del.dd.plug(Y.Plugin.DDProxy, { // Don't move the node at the end of the drag moveOnEnd: false, cloneNode: true }); del.dd.plug(Y.Plugin.DDConstrained, { // Keep it inside the .mod-quiz-edit-content constrain: '#' + CSS.SLOTS }); del.dd.plug(Y.Plugin.DDWinScroll); M.mod_quiz.quizbase.register_module(this); M.mod_quiz.dragres = this; }, /** * Apply dragdrop features to the specified selector or node that refers to section(s) * * @method setup_for_section * @param {String} baseselector The CSS selector or node to limit scope to */ setup_for_section: function() { Y.Node.all('.mod-quiz-edit-content ul.slots ul.section').each(function(resources) { resources.setAttribute('data-draggroups', this.groups.join(' ')); // Define empty ul as droptarget, so that item could be moved to empty list new Y.DD.Drop({ node: resources, groups: this.groups, padding: '20 0 20 0' }); // Initialise each resource/activity in this section this.setup_for_resource('li.activity'); }, this); }, /** * Apply dragdrop features to the specified selector or node that refers to resource(s) * * @method setup_for_resource * @param {String} baseselector The CSS selector or node to limit scope to */ setup_for_resource: function(baseselector) { Y.Node.all(baseselector).each(function(resourcesnode) { // Replace move icons var move = resourcesnode.one('a.' + CSS.EDITINGMOVE); if (move) { var resourcedraghandle = this.get_drag_handle(M.util.get_string('move', 'moodle'), CSS.EDITINGMOVE, CSS.ICONCLASS, true); move.replace(resourcedraghandle); } }, this); }, drag_start: function(e) { // Get our drag object var drag = e.target; drag.get('dragNode').setContent(drag.get('node').get('innerHTML')); drag.get('dragNode').all('.icon').setStyle('vertical-align', 'baseline'); }, drag_dropmiss: function(e) { // Missed the target, but we assume the user intended to drop it // on the last ghost node location, e.drag and e.drop should be // prepared by global_drag_dropmiss parent so simulate drop_hit(e). this.drop_hit(e); }, drop_hit: function(e) { var drag = e.drag; // Get a reference to our drag node var dragnode = drag.get('node'); // Add spinner if it not there var actionarea = dragnode.one(CSS.ACTIONAREA); var spinner = M.util.add_spinner(Y, actionarea); var params = {}; // Handle any variables which we must pass back through to var pageparams = this.get('config').pageparams; var varname; for (varname in pageparams) { params[varname] = pageparams[varname]; } // Prepare request parameters params.sesskey = M.cfg.sesskey; params.courseid = this.get('courseid'); params.quizid = this.get('quizid'); params['class'] = 'resource'; params.field = 'move'; params.id = Number(Y.Moodle.mod_quiz.util.slot.getId(dragnode)); params.sectionId = Y.Moodle.core_course.util.section.getId(dragnode.ancestor('li.section', true)); var previousslot = dragnode.previous(SELECTOR.SLOT); if (previousslot) { params.previousid = Number(Y.Moodle.mod_quiz.util.slot.getId(previousslot)); } var previouspage = dragnode.previous(SELECTOR.PAGE); if (previouspage) { params.page = Number(Y.Moodle.mod_quiz.util.page.getId(previouspage)); } // Do AJAX request var uri = M.cfg.wwwroot + this.get('ajaxurl'); Y.io(uri, { method: 'POST', data: params, on: { start: function() { this.lock_drag_handle(drag, CSS.EDITINGMOVE); spinner.show(); }, success: function(tid, response) { var responsetext = Y.JSON.parse(response.responseText); var params = {element: dragnode, visible: responsetext.visible}; M.mod_quiz.quizbase.invoke_function('set_visibility_resource_ui', params); this.unlock_drag_handle(drag, CSS.EDITINGMOVE); window.setTimeout(function() { spinner.hide(); }, 250); M.mod_quiz.resource_toolbox.reorganise_edit_page(); }, failure: function(tid, response) { this.ajax_failure(response); this.unlock_drag_handle(drag, CSS.SECTIONHANDLE); spinner.hide(); window.location.reload(true); } }, context: this }); }, global_drop_over: function(e) { // Overriding parent method so we can stop the slots being dragged before the first page node. // Check that drop object belong to correct group. if (!e.drop || !e.drop.inGroup(this.groups)) { return; } // Get a reference to our drag and drop nodes. var drag = e.drag.get('node'), drop = e.drop.get('node'); // Save last drop target for the case of missed target processing. this.lastdroptarget = e.drop; // Are we dropping within the same parent node? if (drop.hasClass(this.samenodeclass)) { var where; if (this.goingup) { where = "before"; } else { where = "after"; } drop.insert(drag, where); } else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) { // We are dropping on parent node and it is empty if (this.goingup) { drop.append(drag); } else { drop.prepend(drag); } } this.drop_over(e); } }, { NAME: 'mod_quiz-dragdrop-resource', ATTRS: { courseid: { value: null }, quizid: { value: null }, ajaxurl: { value: 0 }, config: { value: 0 } } }); M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.init_resource_dragdrop = function(params) { new DRAGRESOURCE(params); }; PK\z)l&l&src/dragdrop/js/section.jsnu[/** * Section drag and drop. * * @class M.mod_quiz.dragdrop.section * @constructor * @extends M.core.dragdrop */ var DRAGSECTION = function() { DRAGSECTION.superclass.constructor.apply(this, arguments); }; Y.extend(DRAGSECTION, M.core.dragdrop, { sectionlistselector: null, initializer: function() { // Set group for parent class this.groups = [CSS.SECTIONDRAGGABLE]; this.samenodeclass = 'section'; this.parentnodeclass = 'slots'; // Check if we are in single section mode if (Y.Node.one('.' + CSS.JUMPMENU)) { return false; } // Initialise sections dragging this.sectionlistselector = 'li.section'; if (this.sectionlistselector) { this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector; this.setup_for_section(this.sectionlistselector); // Make each li element in the lists of sections draggable var del = new Y.DD.Delegate({ container: '.' + CSS.COURSECONTENT, nodes: '.' + CSS.SECTIONDRAGGABLE, target: true, handles: ['.' + CSS.LEFT], dragConfig: {groups: this.groups} }); del.dd.plug(Y.Plugin.DDProxy, { // Don't move the node at the end of the drag moveOnEnd: false }); del.dd.plug(Y.Plugin.DDConstrained, { // Keep it inside the .mod-quiz-edit-content constrain: '#' + CSS.PAGECONTENT, stickY: true }); del.dd.plug(Y.Plugin.DDWinScroll); } }, /** * Apply dragdrop features to the specified selector or node that refers to section(s) * * @method setup_for_section * @param {String} baseselector The CSS selector or node to limit scope to */ setup_for_section: function(baseselector) { Y.Node.all(baseselector).each(function(sectionnode) { // Determine the section ID var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode); // We skip the top section as it is not draggable if (sectionid > 0) { // Remove move icons var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN); var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP); // Add dragger icon var title = M.util.get_string('movesection', 'moodle', sectionid); var cssleft = sectionnode.one('.' + CSS.LEFT); if ((movedown || moveup) && cssleft) { cssleft.setStyle('cursor', 'move'); cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true)); if (moveup) { moveup.remove(); } if (movedown) { movedown.remove(); } // This section can be moved - add the class to indicate this to Y.DD. sectionnode.addClass(CSS.SECTIONDRAGGABLE); } } }, this); }, /* * Drag-dropping related functions */ drag_start: function(e) { // Get our drag object var drag = e.target; // Creat a dummy structure of the outer elemnents for clean styles application var containernode = Y.Node.create('
        '); var sectionnode = Y.Node.create('
          '); sectionnode.setStyle('margin', 0); sectionnode.setContent(drag.get('node').get('innerHTML')); containernode.appendChild(sectionnode); drag.get('dragNode').setContent(containernode); drag.get('dragNode').addClass(CSS.COURSECONTENT); }, drag_dropmiss: function(e) { // Missed the target, but we assume the user intended to drop it // on the last last ghost node location, e.drag and e.drop should be // prepared by global_drag_dropmiss parent so simulate drop_hit(e). this.drop_hit(e); }, get_section_index: function(node) { var sectionlistselector = '.' + CSS.COURSECONTENT + ' li.section', sectionList = Y.all(sectionlistselector), nodeIndex = sectionList.indexOf(node), zeroIndex = sectionList.indexOf(Y.one('#section-0')); return (nodeIndex - zeroIndex); }, drop_hit: function(e) { var drag = e.drag; // Get references to our nodes and their IDs. var dragnode = drag.get('node'), dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode), loopstart = dragnodeid, dropnodeindex = this.get_section_index(dragnode), loopend = dropnodeindex; if (dragnodeid === dropnodeindex) { Y.log("Skipping move - same location moving " + dragnodeid + " to " + dropnodeindex, 'debug', 'moodle-mod_quiz-dragdrop'); return; } Y.log("Moving from position " + dragnodeid + " to position " + dropnodeindex, 'debug', 'moodle-mod_quiz-dragdrop'); if (loopstart > loopend) { // If we're going up, we need to swap the loop order // because loops can't go backwards. loopstart = dropnodeindex; loopend = dragnodeid; } // Get the list of nodes. drag.get('dragNode').removeClass(CSS.COURSECONTENT); var sectionlist = Y.Node.all(this.sectionlistselector); // Add a lightbox if it's not there. var lightbox = M.util.add_lightbox(Y, dragnode); // Handle any variables which we must pass via AJAX. var params = {}, pageparams = this.get('config').pageparams, varname; for (varname in pageparams) { if (!pageparams.hasOwnProperty(varname)) { continue; } params[varname] = pageparams[varname]; } // Prepare request parameters params.sesskey = M.cfg.sesskey; params.courseid = this.get('courseid'); params.quizid = this.get('quizid'); params['class'] = 'section'; params.field = 'move'; params.id = dragnodeid; params.value = dropnodeindex; // Perform the AJAX request. var uri = M.cfg.wwwroot + this.get('ajaxurl'); Y.io(uri, { method: 'POST', data: params, on: { start: function() { lightbox.show(); }, success: function(tid, response) { // Update section titles, we can't simply swap them as // they might have custom title try { var responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { new M.core.ajaxException(responsetext); } M.mod_quiz.edit.process_sections(Y, sectionlist, responsetext, loopstart, loopend); } catch (e) { // Ignore. } // Update all of the section IDs - first unset them, then set them // to avoid duplicates in the DOM. var index; // Classic bubble sort algorithm is applied to the section // nodes between original drag node location and the new one. var swapped = false; do { swapped = false; for (index = loopstart; index <= loopend; index++) { if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) > Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) { Y.log("Swapping " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) + " with " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index)), "debug", "moodle-mod_quiz-dragdrop"); // Swap section id. var sectionid = sectionlist.item(index - 1).get('id'); sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id')); sectionlist.item(index).set('id', sectionid); // See what format needs to swap. M.mod_quiz.edit.swap_sections(Y, index - 1, index); // Update flag. swapped = true; } } loopend = loopend - 1; } while (swapped); window.setTimeout(function() { lightbox.hide(); }, 250); }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); } }, context: this }); } }, { NAME: 'mod_quiz-dragdrop-section', ATTRS: { courseid: { value: null }, quizid: { value: null }, ajaxurl: { value: 0 }, config: { value: 0 } } }); M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.init_section_dragdrop = function(params) { new DRAGSECTION(params); }; PK\G:src/categoryexpander/build.jsonnu[{ "name": "moodle-course-categoryexpander", "builds": { "moodle-course-categoryexpander": { "jsfiles": [ "categoryexpander.js" ] } } } PK\W3/jj/src/categoryexpander/meta/categoryexpander.jsonnu[{ "moodle-course-categoryexpander": { "requires": [ "node", "event-key" ] } } PK\?@@+src/categoryexpander/js/categoryexpander.jsnu[/** * Adds toggling of subcategory with automatic loading using AJAX. * * This also includes application of an animation to improve user experience. * * @module moodle-course-categoryexpander */ /** * The course category expander. * * @constructor * @class Y.Moodle.course.categoryexpander */ var CSS = { CONTENTNODE: 'content', COLLAPSEALL: 'collapse-all', DISABLED: 'disabled', LOADED: 'loaded', NOTLOADED: 'notloaded', SECTIONCOLLAPSED: 'collapsed', HASCHILDREN: 'with_children' }, SELECTORS = { WITHCHILDRENTREES: '.with_children', LOADEDTREES: '.with_children.loaded', CONTENTNODE: '.content', CATEGORYLISTENLINK: '.category .info .categoryname', CATEGORYSPINNERLOCATION: '.categoryname', CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed', CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed', CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)', COLLAPSEEXPAND: '.collapseexpand', COURSEBOX: '.coursebox', COURSEBOXLISTENLINK: '.coursebox .moreinfo', COURSEBOXSPINNERLOCATION: '.info .moreinfo', COURSECATEGORYTREE: '.course_category_tree', PARENTWITHCHILDREN: '.category' }, NS = Y.namespace('Moodle.course.categoryexpander'), TYPE_CATEGORY = 0, TYPE_COURSE = 1, URL = M.cfg.wwwroot + '/course/category.ajax.php'; /** * Set up the category expander. * * No arguments are required. * * @method init */ NS.init = function() { var doc = Y.one(Y.config.doc); doc.delegate('click', this.toggle_category_expansion, SELECTORS.CATEGORYLISTENLINK, this); doc.delegate('click', this.toggle_coursebox_expansion, SELECTORS.COURSEBOXLISTENLINK, this); doc.delegate('click', this.collapse_expand_all, SELECTORS.COLLAPSEEXPAND, this); // Only set up they keybaord listeners when tab is first pressed - it // may never happen and modifying the DOM on a large number of nodes // can be very expensive. doc.once('key', this.setup_keyboard_listeners, 'tab', this); }; /** * Set up keyboard expansion for course content. * * This includes setting up the delegation but also adding the nodes to the * tabflow. * * @method setup_keyboard_listeners */ NS.setup_keyboard_listeners = function() { var doc = Y.one(Y.config.doc); Y.log('Setting the tabindex for all expandable course nodes', 'info', 'moodle-course-categoryexpander'); doc.all(SELECTORS.CATEGORYLISTENLINK, SELECTORS.COURSEBOXLISTENLINK, SELECTORS.COLLAPSEEXPAND).setAttribute('tabindex', '0'); Y.one(Y.config.doc).delegate('key', this.toggle_category_expansion, 'enter', SELECTORS.CATEGORYLISTENLINK, this); Y.one(Y.config.doc).delegate('key', this.toggle_coursebox_expansion, 'enter', SELECTORS.COURSEBOXLISTENLINK, this); Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this); }; /** * Expand all categories. * * @method expand_category * @private * @param {Node} categorynode The node to expand */ NS.expand_category = function(categorynode) { // Load the actual dependencies now that we've been called. Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { // Overload the expand_category with the _expand_category function to ensure that // this function isn't called in the future, and call it for the first time. NS.expand_category = NS._expand_category; NS.expand_category(categorynode); }); }; NS._expand_category = function(categorynode) { var categoryid, depth; if (!categorynode.hasClass(CSS.HASCHILDREN)) { // Nothing to do here - this category has no children. return; } if (categorynode.hasClass(CSS.LOADED)) { // We've already loaded this content so we just need to toggle the view of it. this.run_expansion(categorynode); return; } // We use Data attributes to store the category. categoryid = categorynode.getData('categoryid'); depth = categorynode.getData('depth'); if (typeof categoryid === "undefined" || typeof depth === "undefined") { return; } this._toggle_generic_expansion({ parentnode: categorynode, childnode: categorynode.one(SELECTORS.CONTENTNODE), spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, data: { categoryid: categoryid, depth: depth, showcourses: categorynode.getData('showcourses'), type: TYPE_CATEGORY } }); }; /** * Toggle the animation of the clicked category node. * * @method toggle_category_expansion * @private * @param {EventFacade} e */ NS.toggle_category_expansion = function(e) { // Load the actual dependencies now that we've been called. Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { // Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that // this function isn't called in the future, and call it for the first time. NS.toggle_category_expansion = NS._toggle_category_expansion; NS.toggle_category_expansion(e); }); }; /** * Toggle the animation of the clicked coursebox node. * * @method toggle_coursebox_expansion * @private * @param {EventFacade} e */ NS.toggle_coursebox_expansion = function(e) { // Load the actual dependencies now that we've been called. Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { // Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that // this function isn't called in the future, and call it for the first time. NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion; NS.toggle_coursebox_expansion(e); }); e.preventDefault(); }; NS._toggle_coursebox_expansion = function(e) { var courseboxnode; // Grab the parent category container - this is where the new content will be added. courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true); e.preventDefault(); if (courseboxnode.hasClass(CSS.LOADED)) { // We've already loaded this content so we just need to toggle the view of it. this.run_expansion(courseboxnode); return; } this._toggle_generic_expansion({ parentnode: courseboxnode, childnode: courseboxnode.one(SELECTORS.CONTENTNODE), spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION, data: { courseid: courseboxnode.getData('courseid'), type: TYPE_COURSE } }); }; NS._toggle_category_expansion = function(e) { var categorynode, categoryid, depth; if (e.target.test('a') || e.target.test('img')) { // Return early if either an anchor or an image were clicked. return; } // Grab the parent category container - this is where the new content will be added. categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true); if (!categorynode.hasClass(CSS.HASCHILDREN)) { // Nothing to do here - this category has no children. return; } if (categorynode.hasClass(CSS.LOADED)) { // We've already loaded this content so we just need to toggle the view of it. this.run_expansion(categorynode); return; } // We use Data attributes to store the category. categoryid = categorynode.getData('categoryid'); depth = categorynode.getData('depth'); if (typeof categoryid === "undefined" || typeof depth === "undefined") { return; } this._toggle_generic_expansion({ parentnode: categorynode, childnode: categorynode.one(SELECTORS.CONTENTNODE), spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, data: { categoryid: categoryid, depth: depth, showcourses: categorynode.getData('showcourses'), type: TYPE_CATEGORY } }); }; /** * Wrapper function to handle toggling of generic types. * * @method _toggle_generic_expansion * @private * @param {Object} config */ NS._toggle_generic_expansion = function(config) { var spinner; if (config.spinnerhandle) { // Add a spinner to give some feedback to the user. spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show(); } // Fetch the data. Y.io(URL, { method: 'POST', context: this, on: { complete: this.process_results }, data: config.data, "arguments": { parentnode: config.parentnode, childnode: config.childnode, spinner: spinner } }); }; /** * Apply the animation on the supplied node. * * @method run_expansion * @private * @param {Node} categorynode The node to apply the animation to */ NS.run_expansion = function(categorynode) { var categorychildren = categorynode.one(SELECTORS.CONTENTNODE), self = this, ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE); // Add our animation to the categorychildren. this.add_animation(categorychildren); // If we already have the class, remove it before showing otherwise we perform the // animation whilst the node is hidden. if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) { // To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class. categorychildren.setStyle('height', '0'); categorynode.removeClass(CSS.SECTIONCOLLAPSED); categorynode.setAttribute('aria-expanded', 'true'); categorychildren.fx.set('reverse', false); } else { categorychildren.fx.set('reverse', true); categorychildren.fx.once('end', function(e, categorynode) { categorynode.addClass(CSS.SECTIONCOLLAPSED); categorynode.setAttribute('aria-expanded', 'false'); }, this, categorynode); } categorychildren.fx.once('end', function(e, categorychildren) { // Remove the styles that the animation has set. categorychildren.setStyles({ height: '', opacity: '' }); // To avoid memory gobbling, remove the animation. It will be added back if called again. this.destroy(); self.update_collapsible_actions(ancestor); }, categorychildren.fx, categorychildren); // Now that everything has been set up, run the animation. categorychildren.fx.run(); }; /** * Toggle collapsing of all nodes. * * @method collapse_expand_all * @private * @param {EventFacade} e */ NS.collapse_expand_all = function(e) { // Load the actual dependencies now that we've been called. Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { // Overload the collapse_expand_all with the _collapse_expand_all function to ensure that // this function isn't called in the future, and call it for the first time. NS.collapse_expand_all = NS._collapse_expand_all; NS.collapse_expand_all(e); }); e.preventDefault(); }; NS._collapse_expand_all = function(e) { // The collapse/expand button has no actual target but we need to prevent it's default // action to ensure we don't make the page reload/jump. e.preventDefault(); if (e.currentTarget.hasClass(CSS.DISABLED)) { // The collapse/expand is currently disabled. return; } var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE); if (!ancestor) { return; } var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND); if (collapseall.hasClass(CSS.COLLAPSEALL)) { this.collapse_all(ancestor); } else { this.expand_all(ancestor); } this.update_collapsible_actions(ancestor); }; NS.expand_all = function(ancestor) { var finalexpansions = []; ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN) .each(function(c) { if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) { // Expand the hidden children first without animation. c.removeClass(CSS.SECTIONCOLLAPSED); c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED); } else { finalexpansions.push(c); } }, this); // Run the final expansion with animation on the visible items. Y.all(finalexpansions).each(function(c) { this.expand_category(c); }, this); }; NS.collapse_all = function(ancestor) { var finalcollapses = []; ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN) .each(function(c) { if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) { finalcollapses.push(c); } else { // Collapse the visible items first this.run_expansion(c); } }, this); // Run the final collapses now that the these are hidden hidden. Y.all(finalcollapses).each(function(c) { c.addClass(CSS.SECTIONCOLLAPSED); c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED); }, this); }; NS.update_collapsible_actions = function(ancestor) { var foundmaximisedchildren = false, // Grab the anchor for the collapseexpand all link. togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND); if (!togglelink) { // We should always have a togglelink but ensure. return; } // Search for any visibly expanded children. ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) { // If we can find any collapsed ancestors, skip. if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { return false; } foundmaximisedchildren = true; return true; }); if (foundmaximisedchildren) { // At least one maximised child found. Show the collapseall. togglelink.setHTML(M.util.get_string('collapseall', 'moodle')) .addClass(CSS.COLLAPSEALL) .removeClass(CSS.DISABLED); } else { // No maximised children found but there are collapsed children. Show the expandall. togglelink.setHTML(M.util.get_string('expandall', 'moodle')) .removeClass(CSS.COLLAPSEALL) .removeClass(CSS.DISABLED); } }; /** * Process the data returned by Y.io. * This includes appending it to the relevant part of the DOM, and applying our animations. * * @method process_results * @private * @param {String} tid The Transaction ID * @param {Object} response The Reponse returned by Y.IO * @param {Object} ioargs The additional arguments provided by Y.IO */ NS.process_results = function(tid, response, args) { var newnode, data; try { data = Y.JSON.parse(response.responseText); if (data.error) { return new M.core.ajaxException(data); } } catch (e) { return new M.core.exception(e); } // Insert the returned data into a new Node. newnode = Y.Node.create(data); // Append to the existing child location. args.childnode.appendChild(newnode); // Now that we have content, we can swap the classes on the toggled container. args.parentnode .addClass(CSS.LOADED) .removeClass(CSS.NOTLOADED); // Toggle the open/close status of the node now that it's content has been loaded. this.run_expansion(args.parentnode); // Remove the spinner now that we've started to show the content. if (args.spinner) { args.spinner.hide().destroy(); } }; /** * Add our animation to the Node. * * @method add_animation * @private * @param {Node} childnode */ NS.add_animation = function(childnode) { if (typeof childnode.fx !== "undefined") { // The animation has already been plugged to this node. return childnode; } childnode.plug(Y.Plugin.NodeFX, { from: { height: 0, opacity: 0 }, to: { // This sets a dynamic height in case the node content changes. height: function(node) { // Get expanded height (offsetHeight may be zero). return node.get('scrollHeight'); }, opacity: 1 }, duration: 0.2 }); return childnode; }; PK\BAAsrc/util/build.jsonnu[{ "name": "moodle-mod_quiz-util", "builds": { "moodle-mod_quiz-util-base": { "jsfiles": [ "base.js" ] }, "moodle-mod_quiz-util-slot": { "jsfiles": [ "slot.js" ] } , "moodle-mod_quiz-util-page": { "jsfiles": [ "page.js" ] } } } PK\kU%%src/util/meta/util.jsonnu[{ "moodle-mod_quiz-util": { "requires": [ "node", "moodle-core-actionmenu" ], "use": [ "moodle-mod_quiz-util-base" ], "submodules": { "moodle-mod_quiz-util-base": { }, "moodle-mod_quiz-util-slot": { "requires": [ "node", "moodle-mod_quiz-util-base" ] }, "moodle-mod_quiz-util-page": { "requires": [ "node", "moodle-mod_quiz-util-base" ] } } } } PK\A src/util/js/cm.jsnu[/** * A collection of utility classes for use with course modules. * * TODO: remove this module as part of MDL-83627. * * @module moodle-course-util * @submodule moodle-course-util-cm */ Y.namespace('Moodle.core_course.util.cm'); Y.log( 'YUI Moodle.core_course.util.cm is deprecated. Please, add support_components to your course format.', 'warn', 'moodle-course-coursebase' ); /** * A collection of utility classes for use with course modules. * * @class Moodle.core_course.util.cm * @static */ Y.Moodle.core_course.util.cm = { CONSTANTS: { MODULEIDPREFIX: 'module-' }, SELECTORS: { COURSEMODULE: '.activity', INSTANCENAME: '.instancename' }, /** * Retrieve the course module item from one of it's child Nodes. * * @method getCourseModuleNodeFromComponent * @param coursemodulecomponent {Node} The component Node. * @return {Node|null} The Course Module Node. */ getCourseModuleFromComponent: function(coursemodulecomponent) { return Y.one(coursemodulecomponent).ancestor(this.SELECTORS.COURSEMODULE, true); }, /** * Determines the section ID for the provided section. * * @method getId * @param coursemodule {Node} The course module to find an ID for. * @return {Number|false} The ID of the course module in question or false if no ID was found. */ getId: function(coursemodule) { // We perform a simple substitution operation to get the ID. var id = coursemodule.get('id').replace( this.CONSTANTS.MODULEIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Determines the section ID for the provided section. * * @method getName * @param coursemodule {Node} The course module to find an ID for. * @return {Number|false} The ID of the course module in question or false if no ID was found. */ getName: function(coursemodule) { var instance = coursemodule.one(this.SELECTORS.INSTANCENAME); if (instance) { return instance.get('firstChild').get('data'); } return null; } }; PK\לMsrc/util/js/base.jsnu[/** * The Moodle.mod_quiz.util classes provide quiz-related utility functions. * * @module moodle-mod_quiz-util * @main */ Y.namespace('Moodle.mod_quiz.util'); /** * A collection of general utility functions for use in quiz. * * @class Moodle.mod_quiz.util * @static */ PK\src/util/js/section.jsnu[/** * A collection of utility classes for use with course sections. * * TODO: remove this module as part of MDL-83627. * * @module moodle-course-util * @submodule moodle-course-util-section */ Y.namespace('Moodle.core_course.util.section'); Y.log( 'YUI Moodle.core_course.util.cm is deprecated. Please, add support_components to your course format.', 'warn', 'moodle-course-coursebase' ); /** * A collection of utility classes for use with course sections. * * @class Moodle.core_course.util.section * @static */ Y.Moodle.core_course.util.section = { CONSTANTS: { SECTIONIDPREFIX: 'section-' }, /** * Determines the section ID for the provided section. * * @method getId * @param section {Node} The section to find an ID for. * @return {Number|false} The ID of the section in question or false if no ID was found. */ getId: function(section) { // We perform a simple substitution operation to get the ID. var id = section.get('id').replace( this.CONSTANTS.SECTIONIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; } }; PK\src/coursebase/build.jsonnu[{ "name": "moodle-course-coursebase", "builds": { "moodle-course-coursebase": { "jsfiles": [ "coursebase.js" ] } } } PK\ZVBss"src/coursebase/meta/toolboxes.jsonnu[{ "moodle-course-coursebase": { "requires": [ "base", "node" ] } } PK\wsrc/coursebase/js/coursebase.jsnu[/** * The coursebase class to provide shared functionality to Modules within * Moodle. * * TODO: remove this module as part of MDL-83627. * * @module moodle-course-coursebase */ var COURSEBASENAME = 'course-coursebase'; var COURSEBASE = function() { COURSEBASE.superclass.constructor.apply(this, arguments); }; Y.log( 'YUI moodle-course-coursebase is deprecated. Please, add support_components to your course format.', 'warn', 'moodle-course-coursebase' ); /** * The coursebase class to provide shared functionality to Modules within * Moodle. * * @class M.course.coursebase * @constructor */ Y.extend(COURSEBASE, Y.Base, { // Registered Modules registermodules: [], /** * Register a new Javascript Module * * @method register_module * @param {Object} The instantiated module to call functions on * @chainable */ register_module: function(object) { this.registermodules.push(object); return this; }, /** * Invoke the specified function in all registered modules with the given arguments * * @method invoke_function * @param {String} functionname The name of the function to call * @param {mixed} args The argument supplied to the function * @chainable */ invoke_function: function(functionname, args) { var module; for (module in this.registermodules) { if (functionname in this.registermodules[module]) { this.registermodules[module][functionname](args); } } return this; } }, { NAME: COURSEBASENAME, ATTRS: {} }); // Ensure that M.course exists and that coursebase is initialised correctly M.course = M.course || {}; M.course.coursebase = M.course.coursebase || new COURSEBASE(); // Abstract functions that needs to be defined per format (course/format/somename/format.js) M.course.format = M.course.format || {}; /** * Swap section (should be defined in format.js if requred) * * @method M.course.format.swap_sections * @param {YUI} Y YUI3 instance * @param {string} node1 node to swap to * @param {string} node2 node to swap with * @return {NodeList} section list */ M.course.format.swap_sections = M.course.format.swap_sections || function() { return null; }; /** * Process sections after ajax response (should be defined in format.js) * If some response is expected, we pass it over to format, as it knows better * hot to process it. * * @method M.course.format.process_sections * @param {YUI} Y YUI3 instance * @param {NodeList} list of sections * @param {array} response ajax response * @param {string} sectionfrom first affected section * @param {string} sectionto last affected section */ M.course.format.process_sections = M.course.format.process_sections || function() { return null; }; /** * Get sections config for this format, for examples see function definition * in the formats. * * @method M.course.format.get_config * @return {object} section list configuration */ M.course.format.get_config = M.course.format.get_config || function() { return { container_node: null, // compulsory container_class: null, // compulsory section_wrapper_node: null, // optional section_wrapper_class: null, // optional section_node: null, // compulsory section_class: null // compulsory }; }; /** * Get section list for this format (usually items inside container_node.container_class selector) * * @method M.course.format.get_section_selector * @param {YUI} Y YUI3 instance * @return {string} section selector */ M.course.format.get_section_selector = M.course.format.get_section_selector || function() { var config = M.course.format.get_config(); if (config.section_node && config.section_class) { return config.section_node + '.' + config.section_class; } Y.log('section_node and section_class are not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase'); return null; }; /** * Get section wraper for this format (only used in case when each * container_node.container_class node is wrapped in some other element). * * @method M.course.format.get_section_wrapper * @param {YUI} Y YUI3 instance * @return {string} section wrapper selector or M.course.format.get_section_selector * if section_wrapper_node and section_wrapper_class are not defined in the format config. */ M.course.format.get_section_wrapper = M.course.format.get_section_wrapper || function(Y) { var config = M.course.format.get_config(); if (config.section_wrapper_node && config.section_wrapper_class) { return config.section_wrapper_node + '.' + config.section_wrapper_class; } return M.course.format.get_section_selector(Y); }; /** * Get the tag of container node * * @method M.course.format.get_containernode * @return {string} tag of container node. */ M.course.format.get_containernode = M.course.format.get_containernode || function() { var config = M.course.format.get_config(); if (config.container_node) { return config.container_node; } else { Y.log('container_node is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase'); } }; /** * Get the class of container node * * @method M.course.format.get_containerclass * @return {string} class of the container node. */ M.course.format.get_containerclass = M.course.format.get_containerclass || function() { var config = M.course.format.get_config(); if (config.container_class) { return config.container_class; } else { Y.log('container_class is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase'); } }; /** * Get the tag of draggable node (section wrapper if exists, otherwise section) * * @method M.course.format.get_sectionwrappernode * @return {string} tag of the draggable node. */ M.course.format.get_sectionwrappernode = M.course.format.get_sectionwrappernode || function() { var config = M.course.format.get_config(); if (config.section_wrapper_node) { return config.section_wrapper_node; } else { return config.section_node; } }; /** * Get the class of draggable node (section wrapper if exists, otherwise section) * * @method M.course.format.get_sectionwrapperclass * @return {string} class of the draggable node. */ M.course.format.get_sectionwrapperclass = M.course.format.get_sectionwrapperclass || function() { var config = M.course.format.get_config(); if (config.section_wrapper_class) { return config.section_wrapper_class; } else { return config.section_class; } }; /** * Get the tag of section node * * @method M.course.format.get_sectionnode * @return {string} tag of section node. */ M.course.format.get_sectionnode = M.course.format.get_sectionnode || function() { var config = M.course.format.get_config(); if (config.section_node) { return config.section_node; } else { Y.log('section_node is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase'); } }; /** * Get the class of section node * * @method M.course.format.get_sectionclass * @return {string} class of the section node. */ M.course.format.get_sectionclass = M.course.format.get_sectionclass || function() { var config = M.course.format.get_config(); if (config.section_class) { return config.section_class; } else { Y.log('section_class is not defined in M.course.format.get_config', 'warn', 'moodle-course-coursebase'); } }; PK\xrJJJbuild/moodle-course-categoryexpander/moodle-course-categoryexpander-min.jsnu[YUI.add("moodle-course-categoryexpander",function(s,e){var a="collapse-all",p="disabled",i="loaded",g="notloaded",t="collapsed",_="with_children",h=".with_children",u=".with_children.loaded",l=".content",n=".category .info .categoryname",x=".categoryname",f=".category.with_children.collapsed",y=".category.with_children.loaded.collapsed",r=".category.with_children.loaded:not(.collapsed)",c=".collapseexpand",m=".coursebox",o=".coursebox .moreinfo",v=".info .moreinfo",b=".course_category_tree",C=".category",d=s.namespace("Moodle.course.categoryexpander"),w=M.cfg.wwwroot+"/course/category.ajax.php";d.init=function(){var e=s.one(s.config.doc);e.delegate("click",this.toggle_category_expansion,n,this),e.delegate("click",this.toggle_coursebox_expansion,o,this),e.delegate("click",this.collapse_expand_all,c,this),e.once("key",this.setup_keyboard_listeners,"tab",this)},d.setup_keyboard_listeners=function(){s.one(s.config.doc).all(n,o,c).setAttribute("tabindex","0"),s.one(s.config.doc).delegate("key",this.toggle_category_expansion,"enter",n,this),s.one(s.config.doc).delegate("key",this.toggle_coursebox_expansion,"enter",o,this),s.one(s.config.doc).delegate("key",this.collapse_expand_all,"enter",c,this)},d.expand_category=function(e){s.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){d.expand_category=d._expand_category,d.expand_category(e)})},d._expand_category=function(e){var n,o;e.hasClass(_)&&(e.hasClass(i)?this.run_expansion(e):(n=e.getData("categoryid"),o=e.getData("depth"),void 0!==n&&void 0!==o&&this._toggle_generic_expansion({parentnode:e,childnode:e.one(l),spinnerhandle:x,data:{categoryid:n,depth:o,showcourses:e.getData("showcourses"),type:0}})))},d.toggle_category_expansion=function(e){s.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){d.toggle_category_expansion=d._toggle_category_expansion,d.toggle_category_expansion(e)})},d.toggle_coursebox_expansion=function(e){s.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){d.toggle_coursebox_expansion=d._toggle_coursebox_expansion,d.toggle_coursebox_expansion(e)}),e.preventDefault()},d._toggle_coursebox_expansion=function(e){var n=e.target.ancestor(m,!0);e.preventDefault(),n.hasClass(i)?this.run_expansion(n):this._toggle_generic_expansion({parentnode:n,childnode:n.one(l),spinnerhandle:v,data:{courseid:n.getData("courseid"),type:1}})},d._toggle_category_expansion=function(e){var n,o;e.target.test("a")||e.target.test("img")||(e=e.target.ancestor(C,!0)).hasClass(_)&&(e.hasClass(i)?this.run_expansion(e):(n=e.getData("categoryid"),o=e.getData("depth"),void 0!==n&&void 0!==o&&this._toggle_generic_expansion({parentnode:e,childnode:e.one(l),spinnerhandle:x,data:{categoryid:n,depth:o,showcourses:e.getData("showcourses"),type:0}})))},d._toggle_generic_expansion=function(e){var n;e.spinnerhandle&&(n=M.util.add_spinner(s,e.parentnode.one(e.spinnerhandle)).show()),s.io(w,{method:"POST",context:this,on:{complete:this.process_results},data:e.data,arguments:{parentnode:e.parentnode,childnode:e.childnode,spinner:n}})},d.run_expansion=function(e){var n=e.one(l),o=this,a=e.ancestor(b);this.add_animation(n),e.hasClass(t)?(n.setStyle("height","0"),e.removeClass(t),e.setAttribute("aria-expanded","true"),n.fx.set("reverse",!1)):(n.fx.set("reverse",!0),n.fx.once("end",function(e,n){n.addClass(t),n.setAttribute("aria-expanded","false")},this,e)),n.fx.once("end",function(e,n){n.setStyles({height:"",opacity:""}),this.destroy(),o.update_collapsible_actions(a)},n.fx,n),n.fx.run()},d.collapse_expand_all=function(e){s.use("io-base","json-parse","moodle-core-notification","anim-node-plugin",function(){d.collapse_expand_all=d._collapse_expand_all,d.collapse_expand_all(e)}),e.preventDefault()},d._collapse_expand_all=function(e){e.preventDefault(),e.currentTarget.hasClass(p)||(e=e.currentTarget.ancestor(b))&&(e.one(c).hasClass(a)?this.collapse_all(e):this.expand_all(e),this.update_collapsible_actions(e))},d.expand_all=function(e){var n=[];e.all(f).each(function(e){e.ancestor(f)?(e.removeClass(t),e.all(h).removeClass(t)):n.push(e)},this),s.all(n).each(function(e){this.expand_category(e)},this)},d.collapse_all=function(e){var n=[];e.all(r).each(function(e){e.ancestor(r)?n.push(e):this.run_expansion(e)},this),s.all(n).each(function(e){e.addClass(t),e.all(u).addClass(t)},this)},d.update_collapsible_actions=function(e){var n=!1,o=e.one(c);o&&(e.all(r).each(function(e){return!e.ancestor(y)&&(n=!0)}),(n?o.setHTML(M.util.get_string("collapseall","moodle")).addClass(a):o.setHTML(M.util.get_string("expandall","moodle")).removeClass(a)).removeClass(p))},d.process_results=function(e,n,o){var a;try{if((a=s.JSON.parse(n.responseText)).error)return new M.core.ajaxException(a)}catch(t){return new M.core.exception(t)}n=s.Node.create(a),o.childnode.appendChild(n),o.parentnode.addClass(i).removeClass(g),this.run_expansion(o.parentnode),o.spinner&&o.spinner.hide().destroy()},d.add_animation=function(e){return"undefined"!=typeof e.fx||e.plug(s.Plugin.NodeFX,{from:{height:0,opacity:0},to:{height:function(e){return e.get("scrollHeight")},opacity:1},duration:.2}),e}},"@VERSION@",{requires:["node","event-key"]});PK\ArArALbuild/moodle-course-categoryexpander/moodle-course-categoryexpander-debug.jsnu[YUI.add('moodle-course-categoryexpander', function (Y, NAME) { /** * Adds toggling of subcategory with automatic loading using AJAX. * * This also includes application of an animation to improve user experience. * * @module moodle-course-categoryexpander */ /** * The course category expander. * * @constructor * @class Y.Moodle.course.categoryexpander */ var CSS = { CONTENTNODE: 'content', COLLAPSEALL: 'collapse-all', DISABLED: 'disabled', LOADED: 'loaded', NOTLOADED: 'notloaded', SECTIONCOLLAPSED: 'collapsed', HASCHILDREN: 'with_children' }, SELECTORS = { WITHCHILDRENTREES: '.with_children', LOADEDTREES: '.with_children.loaded', CONTENTNODE: '.content', CATEGORYLISTENLINK: '.category .info .categoryname', CATEGORYSPINNERLOCATION: '.categoryname', CATEGORYWITHCOLLAPSEDCHILDREN: '.category.with_children.collapsed', CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed', CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)', COLLAPSEEXPAND: '.collapseexpand', COURSEBOX: '.coursebox', COURSEBOXLISTENLINK: '.coursebox .moreinfo', COURSEBOXSPINNERLOCATION: '.info .moreinfo', COURSECATEGORYTREE: '.course_category_tree', PARENTWITHCHILDREN: '.category' }, NS = Y.namespace('Moodle.course.categoryexpander'), TYPE_CATEGORY = 0, TYPE_COURSE = 1, URL = M.cfg.wwwroot + '/course/category.ajax.php'; /** * Set up the category expander. * * No arguments are required. * * @method init */ NS.init = function() { var doc = Y.one(Y.config.doc); doc.delegate('click', this.toggle_category_expansion, SELECTORS.CATEGORYLISTENLINK, this); doc.delegate('click', this.toggle_coursebox_expansion, SELECTORS.COURSEBOXLISTENLINK, this); doc.delegate('click', this.collapse_expand_all, SELECTORS.COLLAPSEEXPAND, this); // Only set up they keybaord listeners when tab is first pressed - it // may never happen and modifying the DOM on a large number of nodes // can be very expensive. doc.once('key', this.setup_keyboard_listeners, 'tab', this); }; /** * Set up keyboard expansion for course content. * * This includes setting up the delegation but also adding the nodes to the * tabflow. * * @method setup_keyboard_listeners */ NS.setup_keyboard_listeners = function() { var doc = Y.one(Y.config.doc); Y.log('Setting the tabindex for all expandable course nodes', 'info', 'moodle-course-categoryexpander'); doc.all(SELECTORS.CATEGORYLISTENLINK, SELECTORS.COURSEBOXLISTENLINK, SELECTORS.COLLAPSEEXPAND).setAttribute('tabindex', '0'); Y.one(Y.config.doc).delegate('key', this.toggle_category_expansion, 'enter', SELECTORS.CATEGORYLISTENLINK, this); Y.one(Y.config.doc).delegate('key', this.toggle_coursebox_expansion, 'enter', SELECTORS.COURSEBOXLISTENLINK, this); Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this); }; /** * Expand all categories. * * @method expand_category * @private * @param {Node} categorynode The node to expand */ NS.expand_category = function(categorynode) { // Load the actual dependencies now that we've been called. Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { // Overload the expand_category with the _expand_category function to ensure that // this function isn't called in the future, and call it for the first time. NS.expand_category = NS._expand_category; NS.expand_category(categorynode); }); }; NS._expand_category = function(categorynode) { var categoryid, depth; if (!categorynode.hasClass(CSS.HASCHILDREN)) { // Nothing to do here - this category has no children. return; } if (categorynode.hasClass(CSS.LOADED)) { // We've already loaded this content so we just need to toggle the view of it. this.run_expansion(categorynode); return; } // We use Data attributes to store the category. categoryid = categorynode.getData('categoryid'); depth = categorynode.getData('depth'); if (typeof categoryid === "undefined" || typeof depth === "undefined") { return; } this._toggle_generic_expansion({ parentnode: categorynode, childnode: categorynode.one(SELECTORS.CONTENTNODE), spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, data: { categoryid: categoryid, depth: depth, showcourses: categorynode.getData('showcourses'), type: TYPE_CATEGORY } }); }; /** * Toggle the animation of the clicked category node. * * @method toggle_category_expansion * @private * @param {EventFacade} e */ NS.toggle_category_expansion = function(e) { // Load the actual dependencies now that we've been called. Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { // Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that // this function isn't called in the future, and call it for the first time. NS.toggle_category_expansion = NS._toggle_category_expansion; NS.toggle_category_expansion(e); }); }; /** * Toggle the animation of the clicked coursebox node. * * @method toggle_coursebox_expansion * @private * @param {EventFacade} e */ NS.toggle_coursebox_expansion = function(e) { // Load the actual dependencies now that we've been called. Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { // Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that // this function isn't called in the future, and call it for the first time. NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion; NS.toggle_coursebox_expansion(e); }); e.preventDefault(); }; NS._toggle_coursebox_expansion = function(e) { var courseboxnode; // Grab the parent category container - this is where the new content will be added. courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true); e.preventDefault(); if (courseboxnode.hasClass(CSS.LOADED)) { // We've already loaded this content so we just need to toggle the view of it. this.run_expansion(courseboxnode); return; } this._toggle_generic_expansion({ parentnode: courseboxnode, childnode: courseboxnode.one(SELECTORS.CONTENTNODE), spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION, data: { courseid: courseboxnode.getData('courseid'), type: TYPE_COURSE } }); }; NS._toggle_category_expansion = function(e) { var categorynode, categoryid, depth; if (e.target.test('a') || e.target.test('img')) { // Return early if either an anchor or an image were clicked. return; } // Grab the parent category container - this is where the new content will be added. categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true); if (!categorynode.hasClass(CSS.HASCHILDREN)) { // Nothing to do here - this category has no children. return; } if (categorynode.hasClass(CSS.LOADED)) { // We've already loaded this content so we just need to toggle the view of it. this.run_expansion(categorynode); return; } // We use Data attributes to store the category. categoryid = categorynode.getData('categoryid'); depth = categorynode.getData('depth'); if (typeof categoryid === "undefined" || typeof depth === "undefined") { return; } this._toggle_generic_expansion({ parentnode: categorynode, childnode: categorynode.one(SELECTORS.CONTENTNODE), spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION, data: { categoryid: categoryid, depth: depth, showcourses: categorynode.getData('showcourses'), type: TYPE_CATEGORY } }); }; /** * Wrapper function to handle toggling of generic types. * * @method _toggle_generic_expansion * @private * @param {Object} config */ NS._toggle_generic_expansion = function(config) { var spinner; if (config.spinnerhandle) { // Add a spinner to give some feedback to the user. spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show(); } // Fetch the data. Y.io(URL, { method: 'POST', context: this, on: { complete: this.process_results }, data: config.data, "arguments": { parentnode: config.parentnode, childnode: config.childnode, spinner: spinner } }); }; /** * Apply the animation on the supplied node. * * @method run_expansion * @private * @param {Node} categorynode The node to apply the animation to */ NS.run_expansion = function(categorynode) { var categorychildren = categorynode.one(SELECTORS.CONTENTNODE), self = this, ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE); // Add our animation to the categorychildren. this.add_animation(categorychildren); // If we already have the class, remove it before showing otherwise we perform the // animation whilst the node is hidden. if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) { // To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class. categorychildren.setStyle('height', '0'); categorynode.removeClass(CSS.SECTIONCOLLAPSED); categorynode.setAttribute('aria-expanded', 'true'); categorychildren.fx.set('reverse', false); } else { categorychildren.fx.set('reverse', true); categorychildren.fx.once('end', function(e, categorynode) { categorynode.addClass(CSS.SECTIONCOLLAPSED); categorynode.setAttribute('aria-expanded', 'false'); }, this, categorynode); } categorychildren.fx.once('end', function(e, categorychildren) { // Remove the styles that the animation has set. categorychildren.setStyles({ height: '', opacity: '' }); // To avoid memory gobbling, remove the animation. It will be added back if called again. this.destroy(); self.update_collapsible_actions(ancestor); }, categorychildren.fx, categorychildren); // Now that everything has been set up, run the animation. categorychildren.fx.run(); }; /** * Toggle collapsing of all nodes. * * @method collapse_expand_all * @private * @param {EventFacade} e */ NS.collapse_expand_all = function(e) { // Load the actual dependencies now that we've been called. Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() { // Overload the collapse_expand_all with the _collapse_expand_all function to ensure that // this function isn't called in the future, and call it for the first time. NS.collapse_expand_all = NS._collapse_expand_all; NS.collapse_expand_all(e); }); e.preventDefault(); }; NS._collapse_expand_all = function(e) { // The collapse/expand button has no actual target but we need to prevent it's default // action to ensure we don't make the page reload/jump. e.preventDefault(); if (e.currentTarget.hasClass(CSS.DISABLED)) { // The collapse/expand is currently disabled. return; } var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE); if (!ancestor) { return; } var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND); if (collapseall.hasClass(CSS.COLLAPSEALL)) { this.collapse_all(ancestor); } else { this.expand_all(ancestor); } this.update_collapsible_actions(ancestor); }; NS.expand_all = function(ancestor) { var finalexpansions = []; ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN) .each(function(c) { if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDCHILDREN)) { // Expand the hidden children first without animation. c.removeClass(CSS.SECTIONCOLLAPSED); c.all(SELECTORS.WITHCHILDRENTREES).removeClass(CSS.SECTIONCOLLAPSED); } else { finalexpansions.push(c); } }, this); // Run the final expansion with animation on the visible items. Y.all(finalexpansions).each(function(c) { this.expand_category(c); }, this); }; NS.collapse_all = function(ancestor) { var finalcollapses = []; ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN) .each(function(c) { if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) { finalcollapses.push(c); } else { // Collapse the visible items first this.run_expansion(c); } }, this); // Run the final collapses now that the these are hidden hidden. Y.all(finalcollapses).each(function(c) { c.addClass(CSS.SECTIONCOLLAPSED); c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED); }, this); }; NS.update_collapsible_actions = function(ancestor) { var foundmaximisedchildren = false, // Grab the anchor for the collapseexpand all link. togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND); if (!togglelink) { // We should always have a togglelink but ensure. return; } // Search for any visibly expanded children. ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) { // If we can find any collapsed ancestors, skip. if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) { return false; } foundmaximisedchildren = true; return true; }); if (foundmaximisedchildren) { // At least one maximised child found. Show the collapseall. togglelink.setHTML(M.util.get_string('collapseall', 'moodle')) .addClass(CSS.COLLAPSEALL) .removeClass(CSS.DISABLED); } else { // No maximised children found but there are collapsed children. Show the expandall. togglelink.setHTML(M.util.get_string('expandall', 'moodle')) .removeClass(CSS.COLLAPSEALL) .removeClass(CSS.DISABLED); } }; /** * Process the data returned by Y.io. * This includes appending it to the relevant part of the DOM, and applying our animations. * * @method process_results * @private * @param {String} tid The Transaction ID * @param {Object} response The Reponse returned by Y.IO * @param {Object} ioargs The additional arguments provided by Y.IO */ NS.process_results = function(tid, response, args) { var newnode, data; try { data = Y.JSON.parse(response.responseText); if (data.error) { return new M.core.ajaxException(data); } } catch (e) { return new M.core.exception(e); } // Insert the returned data into a new Node. newnode = Y.Node.create(data); // Append to the existing child location. args.childnode.appendChild(newnode); // Now that we have content, we can swap the classes on the toggled container. args.parentnode .addClass(CSS.LOADED) .removeClass(CSS.NOTLOADED); // Toggle the open/close status of the node now that it's content has been loaded. this.run_expansion(args.parentnode); // Remove the spinner now that we've started to show the content. if (args.spinner) { args.spinner.hide().destroy(); } }; /** * Add our animation to the Node. * * @method add_animation * @private * @param {Node} childnode */ NS.add_animation = function(childnode) { if (typeof childnode.fx !== "undefined") { // The animation has already been plugged to this node. return childnode; } childnode.plug(Y.Plugin.NodeFX, { from: { height: 0, opacity: 0 }, to: { // This sets a dynamic height in case the node content changes. height: function(node) { // Get expanded height (offsetHeight may be zero). return node.get('scrollHeight'); }, opacity: 1 }, duration: 0.2 }); return childnode; }; }, '@VERSION@', {"requires": ["node", "event-key"]}); PK\l