ÿØÿà 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-Ї Ç config/yui2.js000064400000001562152265472450007257 0ustar00// 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 . if (/-skin|reset|fonts|grids|base/.test(me.name)){ me.type = 'css'; me.path = me.path.replace(/\.js/, '.css'); me.path = me.path.replace(/\/yui2-skin/, '/assets/skins/sam/yui2-skin'); } config/moodle.js000064400000003061152265472450007642 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . // A module name should be composed of: // moodle--[-][-skin] var parts = me.name.replace(/^moodle-/,'').split('-'), component = parts.shift(), module = parts[0], min = '-min'; if (/-(skin|core)$/.test(me.name)) { // For these types, we need to remove the final word and set the type. parts.pop(); me.type = 'css'; // CSS is not minified - clear the min option. min = ''; } if (module) { // Determine the filename based on the remaining parts. var filename = parts.join('-'); // Build the first part of the filename. me.path = component + '/' + module + '/' + filename + min + '.' + me.type; } else { // This is a hangup from the old ways of writing Modules. // We will start to warn about this once we have removed all core components of this form. me.path = component + '/' + component + '.' + me.type; } common/yuidoc.json000064400000000603152265472450010236 0ustar00{ "name": "Moodle", "description": "YUI documentation for Moodle", "url": "https://moodle.org", "options": { "external": { "data": "http://yuilibrary.com/yui/docs/api/data.json" }, "linkNatives": true, "attributesEmit": "true", "selleck": "true", "outdir": "../yuidocs", "paths": "**/yui/src/*" } } src/handlebars/build.json000064400000000265152265472450011447 0ustar00{ "name": "moodle-core-handlebars", "builds": { "moodle-core-handlebars": { "jsfiles": [ "handlebars.js" ] } } } src/handlebars/meta/handlebars.json000064400000000214152265472450013373 0ustar00{ "moodle-core-handlebars": { "condition": { "trigger": "handlebars", "when": "after" } } } src/handlebars/js/handlebars.js000064400000001463152265472450012533 0ustar00// Handlebars only exists in the global namespace and we do not want to // Clone the entire thing. // It should be reasonably safe within Moodle to add some core // functionality to Handlebars at this level. Y.Handlebars.registerHelper('get_string', function() { var args = new Y.Array(arguments); // Pop off the Handlebars object at the end. args.pop(); // Add the Handlebars hashtable to support get_string arguments. args.push(arguments[arguments.length - 1].hash); // Perform the get_string. return M.util.get_string.apply(this, args); }); Y.Handlebars.registerHelper('image_url', function() { var args = new Y.Array(arguments); // Pop off the Handlebars object at the end. args.pop(); // Perform the get_string. return M.util.image_url.apply(this, args); }); src/notification/build.json000064400000001410152265472450012023 0ustar00{ "name": "moodle-core-notification", "builds": { "moodle-core-notification": { "jsfiles": [ "notification.js" ] }, "moodle-core-notification-dialogue": { "jsfiles": [ "shared.js", "dialogue.js", "info.js" ] }, "moodle-core-notification-alert": { "jsfiles": [ "shared.js", "alert.js" ] }, "moodle-core-notification-confirm": { "jsfiles": [ "shared.js", "confirm.js" ] }, "moodle-core-notification-exception": { "jsfiles": [ "shared.js", "exception.js" ] }, "moodle-core-notification-ajaxexception": { "jsfiles": [ "shared.js", "ajaxexception.js" ] } } } src/notification/meta/notification.json000064400000001756152265472450014355 0ustar00{ "moodle-core-notification": { "requires": [ "moodle-core-notification-dialogue", "moodle-core-notification-alert", "moodle-core-notification-confirm", "moodle-core-notification-exception", "moodle-core-notification-ajaxexception" ] }, "moodle-core-notification-dialogue": { "requires": [ "base", "node", "panel", "escape", "event-key", "dd-plugin", "moodle-core-widget-focusafterclose", "moodle-core-lockscroll" ] }, "moodle-core-notification-alert": { "requires": [ "moodle-core-notification-dialogue" ] }, "moodle-core-notification-confirm": { "requires": [ "moodle-core-notification-dialogue" ] }, "moodle-core-notification-exception": { "requires": [ "moodle-core-notification-dialogue" ] }, "moodle-core-notification-ajaxexception": { "requires": [ "moodle-core-notification-dialogue" ] } } src/notification/js/notification.js000064400000001436152265472450013501 0ustar00/** * The notification module provides a standard set of dialogues for use * within Moodle. * * @module moodle-core-notification * @main */ /** * To avoid bringing moodle-core-notification into modules in it's * entirety, we now recommend using on of the subclasses of * moodle-core-notification. These include: *
*
moodle-core-notification-dialogue
*
moodle-core-notification-alert
*
moodle-core-notification-confirm
*
moodle-core-notification-exception
*
moodle-core-notification-ajaxexception
*
* * @class M.core.notification * @deprecated */ Y.log("The moodle-core-notification parent module has been deprecated. " + "Please use one of its subclasses instead.", 'moodle-core-notification', 'warn'); src/notification/js/alert.js000064400000006626152265472450012130 0ustar00/** * A dialogue type designed to display an alert to the user. * * @module moodle-core-notification * @submodule moodle-core-notification-alert */ var ALERT_NAME = 'Moodle alert', ALERT; /** * Extends core Dialogue to show the alert dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.alert * @extends M.core.dialogue */ ALERT = function(config) { config.closeButton = false; ALERT.superclass.constructor.apply(this, [config]); }; Y.extend(ALERT, M.core.notification.info, { /** * The list of events to detach when destroying this dialogue. * * @property _closeEvents * @type EventHandle[] * @private */ _closeEvents: null, initializer: function() { this._closeEvents = []; this.publish('complete'); var yes = Y.Node.create(''), content = Y.Node.create('
') .append(Y.Node.create('
' + this.get('message') + '
')) .append(Y.Node.create('
') .append(yes)); this.get(BASE).addClass('moodle-dialogue-confirm'); setTimeout((function() { this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); }).bind(this), 100); this.setStdModContent(Y.WidgetStdMod.HEADER, '
' + this.get(TITLE) + '
', Y.WidgetStdMod.REPLACE); this._closeEvents.push( Y.on('key', this.submit, window, 'down:13', this), yes.on('click', this.submit, this) ); var closeButton = this.get('boundingBox').one('.closebutton'); if (closeButton) { // The close button should act exactly like the 'No' button. this._closeEvents.push( closeButton.on('click', this.submit, this) ); } }, submit: function() { new Y.EventHandle(this._closeEvents).detach(); this.fire('complete'); this.hide(); this.destroy(); } }, { NAME: ALERT_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The title of the alert. * * @attribute title * @type String * @default 'Alert' */ title: { validator: Y.Lang.isString, value: 'Alert' }, /** * The message of the alert. * * @attribute message * @type String * @default 'Confirm' */ message: { validator: Y.Lang.isString, value: 'Confirm' }, /** * The button text to use to accept the alert. * * @attribute yesLabel * @type String * @default 'OK' */ yesLabel: { validator: Y.Lang.isString, setter: function(txt) { if (!txt) { txt = 'OK'; } return txt; }, value: 'OK' } } }); M.core.alert = ALERT; src/notification/js/dialogue.js000064400000073330152265472450012606 0ustar00/** * The generic dialogue class for use in Moodle. * * @module moodle-core-notification * @submodule moodle-core-notification-dialogue */ var DIALOGUE_NAME = 'Moodle dialogue', DIALOGUE, DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen', DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden', DIALOGUE_SELECTOR = ' [role=dialog]', MENUBAR_SELECTOR = '[role=menubar]', DOT = '.', HAS_ZINDEX = 'moodle-has-zindex', CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]):not([disabled]):not([tabindex^="-"]),' + 'a[href]:not([disabled]):not([tabindex^="-"]),' + 'button:not([disabled]):not([tabindex^="-"]),' + 'textarea:not([disabled]):not([tabindex^="-"]),' + 'select:not([disabled]):not([tabindex^="-"]),' + '[tabindex]:not([disabled]):not([tabindex^="-"])', FORM_SELECTOR = 'form'; /** * A re-usable dialogue box with Moodle classes applied. * * @param {Object} c Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.dialogue * @extends Panel */ DIALOGUE = function(config) { // The code below is a hack to add the custom content node to the DOM, on the fly, per-instantiation and to assign the value // of 'srcNode' to this newly created node. Normally (see docs: https://yuilibrary.com/yui/docs/widget/widget-extend.html), // this node would be pre-existing in the DOM, and an id string would simply be passed in as a property of the config object // during widget instantiation, however, because we're creating it on the fly (and 'config.srcNode' isn't set yet), care must // be taken to add it to the DOM and to properly set the value of 'config.srcNode' before calling the parent constructor. // Note: additional classes can be added to this content node by setting the 'additionalBaseClass' config property (a string). var id = 'moodle-dialogue-' + Y.stamp(this) + '-wrap'; // Can't use this.get('id') as it's not set at this stage. config.notificationBase = Y.Node.create('
') .append(Y.Node.create( '' ) .append(Y.Node.create('
')) .append(Y.Node.create('
')) .append(Y.Node.create('
'))); config.attachmentPoint = config.attachmentPoint || document.body; Y.one(config.attachmentPoint).append(config.notificationBase); config.srcNode = '#' + id; delete config.buttons; // Don't let anyone pass in buttons as we want to control these during init. addButton can be used later. DIALOGUE.superclass.constructor.apply(this, [config]); }; Y.extend(DIALOGUE, Y.Panel, { // Window resize event listener. _resizeevent: null, // Orientation change event listener. _orientationevent: null, _calculatedzindex: false, // Current maskNode id _currentMaskNodeId: null, /** * The original position of the dialogue before it was reposition to * avoid browser jumping. * * @property _originalPosition * @protected * @type Array */ _originalPosition: null, /** * The list of elements that have been aria hidden when displaying * this dialogue. * * @property _hiddenSiblings * @protected * @type Array */ _hiddenSiblings: null, /** * Hide the modal only if it doesn't contain a form. * * @method hideIfNotForm */ hideIfNotForm: function() { var bb = this.get('boundingBox'), formElement = bb.one(FORM_SELECTOR); if (formElement === null) { this.hide(); } }, /** * Initialise the dialogue. * * @method initializer */ initializer: function() { var bb; if (this.get('closeButton') !== false) { var title = this.get('closeButtonTitle'); // The buttons constructor does not allow custom attributes. this.get('buttons').header[0].setAttribute('title', title); this.get('buttons').header[0].setAttribute('aria-label', title); } this.setStdModContent(Y.WidgetStdMod.HEADER, '
' + this.get('headerContent') + '
', Y.WidgetStdMod.REPLACE); // Initialise the element cache. this._hiddenSiblings = []; if (this.get('render')) { this.render(); } this.after('visibleChange', this.visibilityChanged, this); if (this.get('center')) { this.centerDialogue(); } if (this.get('modal')) { // If we're a modal then make sure our container is ARIA // hidden by default. ARIA visibility is managed for modal dialogues. this.get(BASE).set('aria-hidden', 'true'); this.plug(Y.M.core.LockScroll); } // Remove the `focusoutside` listener. // It conflicts with the ARIA focuslock manager which supports both YUI and non-YUI dialogues. this.set('focusOn', Y.Array(this.get('focusOn')).filter(function(node) { return node.eventName !== 'focusoutside'; })); Y.one('document').on('orientationchange', function() { // This will detect a change in orientation and re-trigger centering. this.centerDialogOnVisible(); }, this); Y.one('window').on('resize', function() { // Detect window resize (most browsers). this.centerDialogOnVisible(); }, this); // Observe dialog on size change. this.centerDialogOnDialogSizeChange(this); // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507 // and allow setting of z-index in theme. bb = this.get('boundingBox'); bb.addClass(HAS_ZINDEX); // Add any additional classes that were specified. Y.Array.each(this.get('extraClasses'), bb.addClass, bb); if (this.get('visible')) { this.applyZIndex(); this.applyAndTrapFocus(); // Only do accessibility hiding for modals because the ARIA spec // says that all ARIA dialogues should be modal. if (this.get('modal')) { // Make this dialogue visible to screen readers. this.setAccessibilityVisible(); } } // Recalculate the zIndex every time the modal is altered. this.on('maskShow', this.applyZIndex); this.on('maskShow', function() { // When the mask shows, position the boundingBox at the top-left of the window such that when it is // focused, the position does not change. var w = Y.one(Y.config.win), bb = this.get('boundingBox'); if (!this.get('center')) { this._originalPosition = bb.getXY(); } // Check if maskNode already init click event. var maskNode = this.get('maskNode'); if (this._currentMaskNodeId !== maskNode.get('_yuid')) { this._currentMaskNodeId = maskNode.get('_yuid'); maskNode.on('click', this.hideIfNotForm, this); } if (bb.getStyle('position') !== 'fixed') { // If the boundingBox has been positioned in a fixed manner, then it will not position correctly to scrollTop. bb.setStyles({ top: w.get('scrollTop'), left: w.get('scrollLeft') }); } }, this); // Add any additional classes to the content node if required. var nBase = this.get('notificationBase'); var additionalClasses = this.get('additionalBaseClass'); if (additionalClasses !== '') { nBase.addClass(additionalClasses); } // Remove the dialogue from the DOM when it is destroyed. this.after('destroyedChange', function() { this.get(BASE).remove(true); }, this); }, /** * Either set the zindex to the supplied value, or set it to one more than the highest existing * dialog in the page. * * @method applyZIndex */ applyZIndex: function() { var highestzindex = 1040, zindexvalue = 1, bb = this.get('boundingBox'), ol = this.get('maskNode'), zindex = this.get('zIndex'); if (zindex !== 0 && !this._calculatedzindex) { // The zindex was specified so we should use that. bb.setStyle('zIndex', zindex); } else { // Determine the correct zindex by looking at all existing dialogs and menubars in the page. Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) { var zindex = this.findZIndex(node); if (zindex > highestzindex) { highestzindex = zindex; } }, this); // Only set the zindex if we found a wrapper. zindexvalue = (highestzindex + 1).toString(); bb.setStyle('zIndex', zindexvalue); this.set('zIndex', zindexvalue); if (this.get('modal')) { ol.setStyle('zIndex', zindexvalue); // In IE8, the z-indexes do not take effect properly unless you toggle // the lightbox from 'fixed' to 'static' and back. This code does so // using the minimum setTimeouts that still actually work. if (Y.UA.ie && Y.UA.compareVersions(Y.UA.ie, 9) < 0) { setTimeout(function() { ol.setStyle('position', 'static'); setTimeout(function() { ol.setStyle('position', 'fixed'); }, 0); }, 0); } } this._calculatedzindex = true; } }, /** * Finds the zIndex of the given node or its parent. * * @method findZIndex * @param {Node} node The Node to apply the zIndex to. * @return {Number} Either the zIndex, or 0 if one was not found. */ findZIndex: function(node) { // In most cases the zindex is set on the parent of the dialog. var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex'); if (zindex) { return parseInt(zindex, 10); } return 0; }, /** * Event listener for the visibility changed event. * * @method visibilityChanged * @param {EventFacade} e */ visibilityChanged: function(e) { var titlebar, bb; if (e.attrName === 'visible') { this.get('maskNode').addClass(CSS_CLASSES.LIGHTBOX); // Going from visible to hidden. if (e.prevVal && !e.newVal) { bb = this.get('boundingBox'); if (this._resizeevent) { this._resizeevent.detach(); this._resizeevent = null; } if (this._orientationevent) { this._orientationevent.detach(); this._orientationevent = null; } require(['core/local/aria/focuslock'], function(FocusLockManager) { // Untrap focus when the dialogue is hidden. FocusLockManager.untrapFocus(); }); if (this.get('modal')) { // Hide this dialogue from screen readers. this.setAccessibilityHidden(); } } // Going from hidden to visible. if (!e.prevVal && e.newVal) { // This needs to be done each time the dialog is shown as new dialogs may have been opened. this.applyZIndex(); // This needs to be done each time the dialog is shown as the window may have been resized. this.makeResponsive(); if (!this.shouldResizeFullscreen()) { if (this.get('draggable')) { titlebar = '#' + this.get('id') + ' .' + CSS_CLASSES.HEADER; this.plug(Y.Plugin.Drag, {handles: [titlebar]}); Y.one(titlebar).setStyle('cursor', 'move'); } } // Only do accessibility hiding for modals because the ARIA spec // says that all ARIA dialogues should be modal. if (this.get('modal')) { // Make this dialogue visible to screen readers. this.setAccessibilityVisible(); } } if (this.get('center') && !e.prevVal && e.newVal) { this.centerDialogue(); } } }, /** * If the responsive attribute is set on the dialog, and the window size is * smaller than the responsive width - make the dialog fullscreen. * * @method makeResponsive */ makeResponsive: function() { var bb = this.get('boundingBox'); if (this.shouldResizeFullscreen()) { // Make this dialogue fullscreen on a small screen. // Disable the page scrollbars. // Size and position the fullscreen dialog. bb.addClass(DIALOGUE_FULLSCREEN_CLASS); bb.setStyles({'left': null, 'top': null, 'width': null, 'height': null, 'right': null, 'bottom': null}); } else { if (this.get('responsive')) { // We must reset any of the fullscreen changes. bb.removeClass(DIALOGUE_FULLSCREEN_CLASS) .setStyles({'width': this.get('width'), 'height': this.get('height')}); } } // Update Lock scroll if the plugin is present. if (this.lockScroll) { this.lockScroll.updateScrollLock(this.shouldResizeFullscreen()); } }, /** * Center the dialog on the screen. * * @method centerDialogue */ centerDialogue: function() { var bb = this.get('boundingBox'), hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS), x, y; // Don't adjust the position if we are in full screen mode. if (this.shouldResizeFullscreen()) { return; } if (hidden) { bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS); } x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth')) / 2), 15); y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight')) / 2), 15) + Y.one(window).get('scrollTop'); bb.setStyles({'left': x, 'top': y}); if (hidden) { bb.addClass(DIALOGUE_HIDDEN_CLASS); } this.makeResponsive(); }, /** * Automatic re-center dialog when dialog size is changed. * * @method centerDialogOnDialogSizeChange * @param {M.core.dialogue} dialog object to apply centering. */ centerDialogOnDialogSizeChange: function(dialog) { // ResizeObserver doesn't get recognized in JSHint. // So we need to suppress the false warning. var observer = new ResizeObserver(function() { // jshint ignore:line dialog.centerDialogOnVisible(); }); var bb = dialog.get('boundingBox'); observer.observe(bb._node, {attributes: true, attributeFilter: ['class']}); }, /** * Centering dialog when dialog is visible. * * @method centerDialogOnVisible */ centerDialogOnVisible: function() { if (!this.get('visible')) { return; // Only centre visible dialogue. } if (this.name !== DIALOGUE_NAME) { return; // Only centre Moodle dialogues. } if (this.shouldResizeFullscreen()) { this.makeResponsive(); } this.centerDialogue(); }, /** * Return whether this dialogue should be fullscreen or not. * * Responsive attribute must be true and we should not be in an iframe and the screen width should * be less than the responsive width. * * @method shouldResizeFullscreen * @return {Boolean} */ shouldResizeFullscreen: function() { return (window === window.parent) && this.get('responsive') && Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth'); }, _focus: function() { this.focus(); }, show: function() { var result = DIALOGUE.superclass.show.call(this); if (!this.get('center') && this._originalPosition) { // Restore the dialogue position to it's location before it was moved at show time. this.get('boundingBox').setXY(this._originalPosition); } this.applyAndTrapFocus(); return result; }, hide: function(e) { if (e) { // If the event was closed by an escape key event, then we need to check that this // dialogue is currently focused to prevent closing all dialogues in the stack. if (e.type === 'key' && e.keyCode === 27 && !this.get('focused')) { return; } } // Unlock scroll if the plugin is present. if (this.lockScroll) { this.lockScroll.disableScrollLock(); } return DIALOGUE.superclass.hide.call(this, arguments); }, /** * Setup key delegation to keep tabbing within the open dialogue. * * @method keyDelegation */ keyDelegation: function() { Y.log('The keyDelegation function has been deprecated in favour of the AMD core/local/aria/focuslock module'); var bb = this.get('boundingBox'); bb.delegate('key', function(e) { var target = e.target; var direction = 'forward'; if (e.shiftKey) { direction = 'backward'; } if (this.trapFocus(target, direction)) { e.preventDefault(); } }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this); }, /** * Trap the tab focus within the open modal. * * @method trapFocus * @param {string} target the element target * @param {string} direction tab key for forward and tab+shift for backward * @return {Boolean} The result of the focus action. */ trapFocus: function(target, direction) { var bb = this.get('boundingBox'), firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR), lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop(); if (target === lastitem && direction === 'forward') { // Tab key. return firstitem.focus(); } else if (target === firstitem && direction === 'backward') { // Tab+shift key. return lastitem.focus(); } }, /** * Sets the appropriate aria attributes on this dialogue and the other * elements in the DOM to ensure that screen readers are able to navigate * the dialogue popup correctly. * * @method setAccessibilityVisible */ setAccessibilityVisible: function() { // Get the element that contains this dialogue because we need it // to filter out from the document.body child elements. var container = this.get(BASE); // We need to get a list containing each sibling element and the shallowest // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging // the fact that this dialogue is always appended to the document body therefore // it's siblings are the shallowest non-ancestral nodes. If that changes then // this code should also be updated. Y.one(document.body).get('children').each(function(node) { // Skip the element that contains us. if (node !== container) { var hidden = node.get('aria-hidden'); // If they are already hidden we can ignore them. if (hidden !== 'true') { // Save their current state. node.setData('previous-aria-hidden', hidden); this._hiddenSiblings.push(node); // Hide this node from screen readers. node.set('aria-hidden', 'true'); } } }, this); // Make us visible to screen readers. container.set('aria-hidden', 'false'); }, /** * Restores the aria visibility on the DOM elements changed when displaying * the dialogue popup and makes the dialogue aria hidden to allow screen * readers to navigate the main page correctly when the dialogue is closed. * * @method setAccessibilityHidden */ setAccessibilityHidden: function() { var container = this.get(BASE); container.set('aria-hidden', 'true'); // Restore the sibling nodes back to their original values. Y.Array.each(this._hiddenSiblings, function(node) { var previousValue = node.getData('previous-aria-hidden'); // If the element didn't previously have an aria-hidden attribute // then we can just remove the one we set. if (previousValue === null) { node.removeAttribute('aria-hidden'); } else { // Otherwise set it back to the old value (which will be false). node.set('aria-hidden', previousValue); } }); // Clear the cache. No longer need to store these. this._hiddenSiblings = []; }, /** * Focuses on the node specified by focusOnShowSelector, or the first focusable node if nothing is specified. * It also traps the focus to the current bounding box. * * @method applyAndTrapFocus */ applyAndTrapFocus: function() { var content = this.bodyNode; var focusSelector = this.get('focusOnShowSelector'); var focusNode = null; // Try and find a node to focus on using the focusOnShowSelector attribute. if (focusSelector !== null) { focusNode = this.get('boundingBox').one(focusSelector); } if (!focusNode) { // Fall back to the first focusable element in the body of the dialogue if no focus node was found yet. if (content && content !== '') { focusNode = content.one(CAN_RECEIVE_FOCUS_SELECTOR); } } require(['core/local/aria/focuslock'], function(FocusLockManager) { // Trap focus to the current bounding box. FocusLockManager.trapFocus(this.get('boundingBox').getDOMNode()); if (focusNode) { focusNode.focus(); } }.bind(this)); }, }, { NAME: DIALOGUE_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * Any additional classes to add to the base Node. * * @attribute additionalBaseClass * @type String * @default '' */ additionalBaseClass: { value: '' }, /** * The Notification base Node. * * @attribute notificationBase * @type Node */ notificationBase: { }, /** * Whether to display the dialogue modally and with a * lightbox style. * * @attribute lightbox * @type Boolean * @default true * @deprecated Since Moodle 2.7. Please use modal instead. */ lightbox: { lazyAdd: false, setter: function(value) { Y.log("The lightbox attribute of M.core.dialogue has been deprecated since Moodle 2.7, " + "please use the modal attribute instead", 'warn', 'moodle-core-notification-dialogue'); this.set('modal', value); } }, /** * Whether to display a close button on the dialogue. * * Note, we do not recommend hiding the close button as this has * potential accessibility concerns. * * @attribute closeButton * @type Boolean * @default true */ closeButton: { validator: Y.Lang.isBoolean, value: true }, /** * The title for the close button if one is to be shown. * * @attribute closeButtonTitle * @type String * @default 'Close' */ closeButtonTitle: { validator: Y.Lang.isString, value: M.util.get_string('closebuttontitle', 'moodle') }, /** * Whether to display the dialogue centrally on the screen. * * @attribute center * @type Boolean * @default true */ center: { validator: Y.Lang.isBoolean, value: true }, /** * Whether to make the dialogue movable around the page. * * @attribute draggable * @type Boolean * @default false */ draggable: { validator: Y.Lang.isBoolean, value: false }, /** * Used to generate a unique id for the dialogue. * * @attribute COUNT * @type String * @default null * @writeonce */ COUNT: { writeOnce: true, valueFn: function() { return Y.stamp(this); } }, /** * Used to disable the fullscreen resizing behaviour if required. * * @attribute responsive * @type Boolean * @default true */ responsive: { validator: Y.Lang.isBoolean, value: true }, /** * The width that this dialogue should be resized to fullscreen. * * @attribute responsiveWidth * @type Number * @default 768 */ responsiveWidth: { value: 768 }, /** * Selector to a node that should recieve focus when this dialogue is shown. * * The default behaviour is to focus on the header. * * @attribute focusOnShowSelector * @default null * @type String */ focusOnShowSelector: { value: null } } }); Y.Base.modifyAttrs(DIALOGUE, { /** * String with units, or number, representing the width of the Widget. * If a number is provided, the default unit, defined by the Widgets * DEF_UNIT, property is used. * * If a value of 'auto' is used, then an empty String is instead * returned. * * @attribute width * @default '400px' * @type {String|Number} */ width: { value: '400px', setter: function(value) { if (value === 'auto') { return ''; } return value; } }, /** * Boolean indicating whether or not the Widget is visible. * * We override this from the default Widget attribute value. * * @attribute visible * @default false * @type Boolean */ visible: { value: false }, /** * A convenience Attribute, which can be used as a shortcut for the * `align` Attribute. * * Note: We override this in Moodle such that it sets a value for the * `center` attribute if set. The `centered` will always return false. * * @attribute centered * @type Boolean|Node * @default false */ centered: { setter: function(value) { if (value) { this.set('center', true); } return false; } }, /** * Boolean determining whether to render the widget during initialisation. * * We override this to change the default from false to true for the dialogue. * We then proceed to early render the dialogue during our initialisation rather than waiting * for YUI to render it after that. * * @attribute render * @type Boolean * @default true */ render: { value: true, writeOnce: true }, /** * Any additional classes to add to the boundingBox. * * @attribute extraClasses * @type Array * @default [] */ extraClasses: { value: [] }, /** * Identifier for the widget. * * @attribute id * @type String * @default a product of guid(). * @writeOnce */ id: { writeOnce: true, valueFn: function() { var id = 'moodle-dialogue-' + Y.stamp(this); return id; } }, /** * Collection containing the widget's buttons. * * @attribute buttons * @type Object * @default {} */ buttons: { getter: Y.WidgetButtons.prototype._getButtons, setter: Y.WidgetButtons.prototype._setButtons, valueFn: function() { if (this.get('closeButton') === false) { return null; } else { return [ { section: Y.WidgetStdMod.HEADER, classNames: 'closebutton', action: function() { this.hide(); } } ]; } } } }); Y.Base.mix(DIALOGUE, [Y.M.core.WidgetFocusAfterHide]); M.core.dialogue = DIALOGUE; src/notification/js/confirm.js000064400000013264152265472450012452 0ustar00/** * A dialogue type designed to display a confirmation to the user. * * @module moodle-core-notification * @submodule moodle-core-notification-confirm */ var CONFIRM_NAME = 'Moodle confirmation dialogue', CONFIRM; /** * Extends core Dialogue to show the confirmation dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.confirm * @extends M.core.dialogue */ CONFIRM = function(config) { CONFIRM.superclass.constructor.apply(this, [config]); }; Y.extend(CONFIRM, M.core.notification.info, { /** * The list of events to detach when destroying this dialogue. * * @property _closeEvents * @type EventHandle[] * @private */ _closeEvents: null, /** * A reference to the yes button. * * @property _yesButton * @type Node * @private */ _yesButton: null, /** * A reference to the No button. * * @property _noButton * @type Node * @private */ _noButton: null, /** * A reference to the Question. * * @property _question * @type Node * @private */ _question: null, initializer: function() { window.console.error( 'The module "moodle-core-notification-confirm" module has been deprecated and will be removed in Moodle 4.4.' ); this._closeEvents = []; this.publish('complete'); this.publish('complete-yes'); this.publish('complete-no'); this._yesButton = Y.Node.create(''); this._noButton = Y.Node.create(''); this._question = Y.Node.create('
' + this.get(QUESTION) + '
'); var content = Y.Node.create('
') .append(this._question) .append(Y.Node.create( '
' ) .append(this._yesButton) .append(this._noButton)); this.get(BASE).addClass('moodle-dialogue-confirm'); this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); this.setStdModContent(Y.WidgetStdMod.HEADER, '
' + this.get(TITLE) + '
', Y.WidgetStdMod.REPLACE); this._closeEvents.push( Y.on('key', this.submit, window, 'down:27', this, false), this._yesButton.on('click', this.submit, this, true), this._noButton.on('click', this.submit, this, false) ); var closeButton = this.get('boundingBox').one('.closebutton'); if (closeButton) { // The close button should act exactly like the 'No' button. this._closeEvents.push( closeButton.on('click', this.submit, this) ); } }, submit: function(e, outcome) { new Y.EventHandle(this._closeEvents).detach(); this.fire('complete', outcome); if (outcome) { this.fire('complete-yes'); } else { this.fire('complete-no'); } this.hide(); this.destroy(); } }, { NAME: CONFIRM_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The button text to use to accept the confirmation. * * @attribute yesLabel * @type String * @default 'Yes' */ yesLabel: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('yes', 'moodle'); }, setter: function(value) { if (this._yesButton) { this._yesButton.set('value', value); } return value; } }, /** * The button text to use to reject the confirmation. * * @attribute noLabel * @type String * @default 'No' */ noLabel: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('no', 'moodle'); }, setter: function(value) { if (this._noButton) { this._noButton.set('value', value); } return value; } }, /** * The title of the dialogue. * * @attribute title * @type String * @default 'Confirm' */ title: { validator: Y.Lang.isString, value: M.util.get_string('confirm', 'moodle') }, /** * The question posed by the dialogue. * * @attribute question * @type String * @default 'Are you sure?' */ question: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('areyousure', 'moodle'); }, setter: function(value) { if (this._question) { this._question.set('value', value); } return value; } } } }); Y.augment(CONFIRM, Y.EventTarget); M.core.confirm = CONFIRM; src/notification/js/ajaxexception.js000064400000011721152265472450013653 0ustar00/** * A dialogue type designed to display an appropriate error when an error * thrown in the Moodle codebase was reported during an AJAX request. * * @module moodle-core-notification * @submodule moodle-core-notification-ajaxexception */ var AJAXEXCEPTION_NAME = 'Moodle AJAX exception', AJAXEXCEPTION; /** * Extends core Dialogue to show the exception dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.ajaxException * @extends M.core.dialogue */ AJAXEXCEPTION = function(config) { config.name = config.name || M.util.get_string('error', 'moodle'); config.closeButton = true; AJAXEXCEPTION.superclass.constructor.apply(this, [config]); }; Y.extend(AJAXEXCEPTION, M.core.notification.info, { _keypress: null, initializer: function(config) { var content, self = this, delay = this.get('hideTimeoutDelay'), labelsep = M.util.get_string('labelsep', 'langconfig'); this.get(BASE).addClass('moodle-dialogue-exception'); this.setStdModContent(Y.WidgetStdMod.HEADER, '
' + Y.Escape.html(config.name) + '
', Y.WidgetStdMod.REPLACE); content = Y.Node.create('
') .append(Y.Node.create('
' + Y.Escape.html(this.get('error')) + '
')) .append(Y.Node.create('')) .append(Y.Node.create('')) .append(Y.Node.create('')); if (M.cfg.developerdebug) { content.all('.moodle-exception-param').removeClass('hidden'); } this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); if (delay) { this._hideTimeout = setTimeout(function() { self.hide(); }, delay); } this.after('visibleChange', this.errorVisibilityChanged, this); this._keypress = Y.on('key', this.hide, window, 'down:13, 27', this); this.centerDialogue(); }, errorVisibilityChanged: function(e) { if (e.attrName === 'visible' && e.prevVal && !e.newVal) { var self = this; this._keypress.detach(); setTimeout(function() { self.destroy(); }, 1000); } } }, { NAME: AJAXEXCEPTION_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The error message given in the exception. * * @attribute error * @type String * @default 'Unknown error' * @optional */ error: { validator: Y.Lang.isString, value: M.util.get_string('unknownerror', 'moodle') }, /** * Any additional debug information given in the exception. * * @attribute stacktrace * @type String|null * @default null * @optional */ debuginfo: { value: null }, /** * The complete stack trace provided in the exception. * * @attribute stacktrace * @type String|null * @default null * @optional */ stacktrace: { value: null }, /** * A link which may be used by support staff to replicate the issue. * * @attribute reproductionlink * @type String * @default null * @optional */ reproductionlink: { setter: function(link) { if (link !== null) { link = Y.Escape.html(link); link = '' + link.replace(M.cfg.wwwroot, '') + ''; } return link; }, value: null }, /** * If set, the dialogue is hidden after the specified timeout period. * * @attribute hideTimeoutDelay * @type Number * @default null * @optional */ hideTimeoutDelay: { validator: Y.Lang.isNumber, value: null } } }); M.core.ajaxException = AJAXEXCEPTION; src/notification/js/info.js000064400000002123152265472450011740 0ustar00/** * A dialogue type designed to display informative messages to users. * * @module moodle-core-notification */ /** * Extends core Dialogue to provide a type of dialogue which can be used * for informative message which are modal, and centered. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.notification.info * @extends M.core.dialogue */ var INFO = function() { INFO.superclass.constructor.apply(this, arguments); }; Y.extend(INFO, M.core.dialogue, { initializer: function() { this.show(); } }, { NAME: 'Moodle information dialogue', CSS_PREFIX: DIALOGUE_PREFIX }); Y.Base.modifyAttrs(INFO, { /** * Whether the widget should be modal or not. * * We override this to change the default from false to true for a subset of dialogues. * * @attribute modal * @type Boolean * @default true */ modal: { validator: Y.Lang.isBoolean, value: true } }); M.core.notification = M.core.notification || {}; M.core.notification.info = INFO; src/notification/js/exception.js000064400000013102152265472450013002 0ustar00/** * A dialogue type designed to display an appropriate error when a generic * javascript error was thrown and caught. * * @module moodle-core-notification * @submodule moodle-core-notification-exception */ var EXCEPTION_NAME = 'Moodle exception', EXCEPTION; /** * Extends core Dialogue to show the exception dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.exception * @extends M.core.dialogue */ EXCEPTION = function(c) { var config = Y.mix({}, c); config.width = config.width || (M.cfg.developerdebug) ? Math.floor(Y.one(document.body).get('winWidth') / 3) + 'px' : null; config.closeButton = true; // We need to allow some properties which are part of the exception // prototype, otherwise AttributeCore filters them during value normalisation. var allowlist = [ 'message', 'name', 'fileName', 'lineNumber', 'stack' ]; Y.Array.each(allowlist, function(k) { config[k] = c[k]; }); EXCEPTION.superclass.constructor.apply(this, [config]); }; Y.extend(EXCEPTION, M.core.notification.info, { _hideTimeout: null, _keypress: null, initializer: function(config) { var content, self = this, delay = this.get('hideTimeoutDelay'), labelsep = M.util.get_string('labelsep', 'langconfig'); this.get(BASE).addClass('moodle-dialogue-exception'); this.setStdModContent(Y.WidgetStdMod.HEADER, '
' + Y.Escape.html(config.name) + '
', Y.WidgetStdMod.REPLACE); content = Y.Node.create('
') .append(Y.Node.create('
' + Y.Escape.html(this.get('message')) + '
')) .append(Y.Node.create('')) .append(Y.Node.create('')) .append(Y.Node.create('')); if (M.cfg.developerdebug) { content.all('.moodle-exception-param').removeClass('hidden'); } this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); if (delay) { this._hideTimeout = setTimeout(function() { self.hide(); }, delay); } this.after('visibleChange', this.errorVisibilityChanged, this); this._keypress = Y.on('key', this.hide, window, 'down:13,27', this); this.centerDialogue(); }, errorVisibilityChanged: function(e) { if (e.attrName === 'visible' && e.prevVal && !e.newVal) { if (this._keypress) { this._keypress.detach(); } var self = this; setTimeout(function() { self.destroy(); }, 1000); } } }, { NAME: EXCEPTION_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The message of the alert. * * @attribute message * @type String * @default '' */ message: { value: '' }, /** * The name of the alert. * * @attribute title * @type String * @default '' */ name: { value: '' }, /** * The name of the file where the error was thrown. * * @attribute fileName * @type String * @default '' */ fileName: { value: '' }, /** * The line number where the error was thrown. * * @attribute lineNumber * @type String * @default '' */ lineNumber: { value: '' }, /** * The backtrace from the error * * @attribute lineNumber * @type String * @default '' */ stack: { setter: function(str) { var lines = Y.Escape.html(str).split("\n"), pattern = new RegExp('^(.+)@(' + M.cfg.wwwroot + ')?(.{0,75}).*:(\\d+)$'), i; for (i in lines) { lines[i] = lines[i].replace(pattern, "
ln: $4
" + "
$3
" + "
$1
"); } return lines.join("\n"); }, value: '' }, /** * If set, the dialogue is hidden after the specified timeout period. * * @attribute hideTimeoutDelay * @type Number * @default null * @optional */ hideTimeoutDelay: { validator: Y.Lang.isNumber, value: null } } }); M.core.exception = EXCEPTION; src/notification/js/shared.js000064400000001224152265472450012254 0ustar00/* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; src/actionmenu/build.json000064400000000223152265472450011500 0ustar00{ "name": "moodle-core-actionmenu", "builds": { "moodle-core-actionmenu": { "jsfiles": [ "actionmenu.js" ] } } } src/actionmenu/meta/actionmenu.json000064400000000175152265472450013477 0ustar00{ "moodle-core-actionmenu": { "requires": [ "base", "event", "node-event-simulate" ] } } src/actionmenu/js/actionmenu.js000064400000045053152265472450012634 0ustar00/** * Provides drop down menus for list of action links. * * @module moodle-core-actionmenu */ var BODY = Y.one(Y.config.doc.body), CSS = { MENUSHOWN: 'action-menu-shown' }, SELECTOR = { CAN_RECEIVE_FOCUS_SELECTOR: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]', MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]', MENUBAR: '[role="menubar"]', MENUITEM: '[role="menuitem"]', MENUCONTENT: '.menu[data-rel=menu-content]', MENUCONTENTCHILD: 'li a', MENUCHILD: '.menu li a', TOGGLE: '.toggle-display', KEEPOPEN: '[data-keepopen="1"]', MENUBARITEMS: [ '[role="menubar"] > [role="menuitem"]', '[role="menubar"] > [role="presentation"] > [role="menuitem"]' ], MENUITEMS: [ '> [role="menuitem"]', '> [role="presentation"] > [role="menuitem"]' ] }, ACTIONMENU, ALIGN = { TL: 'tl', TR: 'tr', BL: 'bl', BR: 'br' }; /** * Action menu support. * This converts a generic list of links into a drop down menu opened by hovering or clicking * on a menu icon. * * @namespace M.core.actionmenu * @class ActionMenu * @constructor * @extends Base */ ACTIONMENU = function() { ACTIONMENU.superclass.constructor.apply(this, arguments); }; ACTIONMENU.prototype = { /** * The dialogue used for all action menu displays. * @property type * @type M.core.dialogue * @protected */ dialogue: null, /** * An array of events attached during the display of the dialogue. * @property events * @type Object * @protected */ events: [], /** * The node that owns the currently displayed menu. * * @property owner * @type Node * @default null */ owner: null, /** * The menu button that toggles this open. * * @property menulink * @type Node * @protected */ menulink: null, /** * The set of menu nodes. * * @property menuChildren * @type NodeList * @protected */ menuChildren: null, /** * The first menu item. * * @property firstMenuChild * @type Node * @protected */ firstMenuChild: null, /** * The last menu item. * * @property lastMenuChild * @type Node * @protected */ lastMenuChild: null, /** * Called during the initialisation process of the object. * * @method initializer */ initializer: function() { Y.log('Initialising the action menu manager', 'debug', ACTIONMENU.NAME); Y.all(SELECTOR.MENU).each(this.enhance, this); BODY.delegate('key', this.moveMenuItem, 'down:37,39', SELECTOR.MENUBARITEMS.join(','), this); BODY.delegate('click', this.toggleMenu, SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); BODY.delegate('key', this.showIfHidden, 'down:enter,38,40', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); // Ensure that we toggle on menuitems when the spacebar is pressed. BODY.delegate('key', function(e) { e.currentTarget.simulate('click'); e.preventDefault(); }, 'down:32', SELECTOR.MENUBARITEMS.join(',')); }, /** * Enhances a menu adding aria attributes and flagging it as functional. * * @method enhance * @param {Node} menu * @return boolean */ enhance: function(menu) { var menucontent = menu.one(SELECTOR.MENUCONTENT), align; if (!menucontent) { return false; } align = menucontent.getData('align') || this.get('align').join('-'); menu.one(SELECTOR.TOGGLE).set('aria-haspopup', true); menucontent.set('aria-hidden', true); if (!menucontent.hasClass('align-' + align)) { menucontent.addClass('align-' + align); } if (menucontent.getDOMNode().childElementCount) { menu.setAttribute('data-enhanced', '1'); } }, /** * Handle movement between menu items in a menubar. * * @method moveMenuItem * @param {EventFacade} e The event generating the move request * @chainable */ moveMenuItem: function(e) { var nextFocus, menuitem = e.target.ancestor(SELECTOR.MENUITEM, true); if (e.keyCode === 37) { nextFocus = this.getMenuItem(menuitem, true); } else if (e.keyCode === 39) { nextFocus = this.getMenuItem(menuitem); } if (nextFocus) { nextFocus.focus(); } return this; }, /** * Get the next menuitem in a menubar. * * @method getMenuItem * @param {Node} currentItem The currently focused item in the menubar * @param {Boolean} [previous=false] Move backwards in the menubar instead of forwards * @return {Node|null} The next item, or null if none was found */ getMenuItem: function(currentItem, previous) { var menubar = currentItem.ancestor(SELECTOR.MENUBAR), menuitems, next; if (!menubar) { return null; } menuitems = menubar.all(SELECTOR.MENUITEMS.join(',')); if (!menuitems) { return null; } var childCount = menuitems.size(); if (childCount === 1) { // Only one item, exit now because we should already be on it. return null; } // Determine the next child. var index = 0, direction = 1, checkCount = 0; // Work out the index of the currently selected item. for (index = 0; index < childCount; index++) { if (menuitems.item(index) === currentItem) { break; } } // Check that the menu item was found - otherwise return null. if (menuitems.item(index) !== currentItem) { return null; } // Reverse the direction if we want the previous item. if (previous) { direction = -1; } do { // Update the index in the direction of travel. index += direction; next = menuitems.item(index); // Check that we don't loop multiple times. checkCount++; } while (next && next.hasAttribute('hidden')); return next; }, /** * Hides the menu if it is visible. * @param {EventFacade} e * @method hideMenu */ hideMenu: function(e) { if (this.dialogue) { Y.log('Hiding an action menu', 'debug', ACTIONMENU.NAME); this.dialogue.removeClass('show'); this.dialogue.one(SELECTOR.MENUCONTENT).set('aria-hidden', true); this.dialogue = null; } for (var i in this.events) { if (this.events[i].detach) { this.events[i].detach(); } } this.events = []; if (this.owner) { this.owner.removeClass(CSS.MENUSHOWN); this.owner = null; } if (this.menulink) { if (e.type != 'click') { this.menulink.focus(); } this.menulink = null; } }, showIfHidden: function(e) { var menu = e.target.ancestor(SELECTOR.MENU), menuvisible = (menu.hasClass('show')); if (!menuvisible) { e.preventDefault(); this.showMenu(e, menu); } return this; }, /** * Toggles the display of the menu. * @method toggleMenu * @param {EventFacade} e */ toggleMenu: function(e) { var menu = e.target.ancestor(SELECTOR.MENU), menuvisible = (menu.hasClass('show')); // Prevent event propagation as it will trigger the hideIfOutside event handler in certain situations. e.halt(true); this.hideMenu(e); if (menuvisible) { // The menu was visible and the user has clicked to toggle it again. return; } this.showMenu(e, menu); }, /** * Handle keyboard events when the menu is open. We respond to: * * escape (exit) * * tab (move to next menu item) * * up/down (move to previous/next menu item) * * @method handleKeyboardEvent * @param {EventFacade} e The key event */ handleKeyboardEvent: function(e) { var next; var markEventHandled = function(e) { e.preventDefault(); e.stopPropagation(); }; // Handle when the menu is still selected. if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) { if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) { this.firstMenuChild.focus(); markEventHandled(e); } else if (e.keyCode === 38 && this.lastMenuChild) { this.lastMenuChild.focus(); markEventHandled(e); } else if (e.keyCode === 9 && e.shiftKey) { this.hideMenu(e); markEventHandled(e); } return this; } if (e.keyCode === 27) { // The escape key was pressed so close the menu. this.hideMenu(e); markEventHandled(e); } else if (e.keyCode === 32) { // The space bar was pressed. Trigger a click. markEventHandled(e); e.currentTarget.simulate('click'); } else if (e.keyCode === 9) { // The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options. // We only override the Shift + Tab on the first option, and Tab on the last option to change where the // focus is moved to. if (e.target === this.firstMenuChild && e.shiftKey) { this.hideMenu(e); markEventHandled(e); } else if (e.target === this.lastMenuChild && !e.shiftKey) { if (this.hideMenu(e)) { // Determine the next selector and focus on it. next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR); if (next) { next.focus(); markEventHandled(e); } } } } else if (e.keyCode === 38 || e.keyCode === 40) { // The up (38) or down (40) key was pushed. // On cursor moves we loops through the menu rather than exiting it as in the tab behaviour. var found = false, index = 0, direction = 1, checkCount = 0; // Determine which menu item is currently selected. while (!found && index < this.menuChildren.size()) { if (this.menuChildren.item(index) === e.currentTarget) { found = true; } else { index++; } } if (!found) { Y.log("Unable to find this menu item in the list of menu children", 'debug', 'moodle-core-actionmenu'); return; } if (e.keyCode === 38) { // Moving up so reverse the direction. direction = -1; } // Try to find the next do { index += direction; if (index < 0) { index = this.menuChildren.size() - 1; } else if (index >= this.menuChildren.size()) { // Handle wrapping. index = 0; } next = this.menuChildren.item(index); // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item. checkCount++; } while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden')); if (next) { next.focus(); markEventHandled(e); } } }, /** * Hides the menu if the event happened outside the menu. * * @protected * @method hideIfOutside * @param {EventFacade} e */ hideIfOutside: function(e) { if (!e.target.ancestor(SELECTOR.MENUCONTENT, true)) { this.hideMenu(e); } }, /** * Displays the menu with the given content and alignment. * * @method showMenu * @param {EventFacade} e * @param {Node} menu * @return M.core.dialogue */ showMenu: function(e, menu) { Y.log('Displaying an action menu', 'debug', ACTIONMENU.NAME); var ownerselector = menu.getData('owner'), menucontent = menu.one(SELECTOR.MENUCONTENT); this.owner = (ownerselector) ? menu.ancestor(ownerselector) : null; this.dialogue = menu; menu.addClass('show'); if (this.owner) { this.owner.addClass(CSS.MENUSHOWN); this.menulink = this.owner.one(SELECTOR.TOGGLE); } else { this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true); } this.constrain(menucontent.set('aria-hidden', false)); this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD); if (this.menuChildren.size() > 0) { this.firstMenuChild = this.menuChildren.item(0); this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1); this.firstMenuChild.focus(); } // Close the menu if the user presses escape. this.events.push(BODY.on('key', this.hideMenu, 'esc', this)); // Close the menu if the user clicks outside the menu. this.events.push(BODY.on('click', this.hideIfOutside, this)); // Close the menu if the user focuses outside the menu. this.events.push(BODY.delegate('focus', this.hideIfOutside, '*', this)); // Check keyboard changes. this.events.push( menu.delegate('key', this.handleKeyboardEvent, 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this) ); // Close the menu after a button was pushed. this.events.push(menu.delegate('click', function(e) { if (e.currentTarget.test(SELECTOR.KEEPOPEN)) { return; } this.hideMenu(e); }, SELECTOR.MENUCHILD, this)); return true; }, /** * Constrains the node to its the page width. * * @method constrain * @param {Node} node */ constrain: function(node) { var selector = node.getData('constraint'), nx = node.getX(), ny = node.getY(), nwidth = node.get('offsetWidth'), nheight = node.get('offsetHeight'), cx = 0, cy = 0, cwidth, cheight, coverflow = 'auto', newwidth = null, newheight = null, newleft = null, newtop = null, boxshadow = null; if (selector) { selector = node.ancestor(selector); } if (selector) { cwidth = selector.get('offsetWidth'); cheight = selector.get('offsetHeight'); cx = selector.getX(); cy = selector.getY(); coverflow = selector.getStyle('overflow') || 'auto'; } else { cwidth = node.get('docWidth'); cheight = node.get('docHeight'); } // Constrain X. // First up if the width is more than the constrain its easily full width + full height. if (nwidth > cwidth) { // The width of the constraint. newwidth = nwidth = cwidth; // The constraints xpoint. newleft = nx = cx; } else { if (nx < cx) { // If nx is less than cx we need to move it right. newleft = nx = cx; } else if (nx + nwidth >= cx + cwidth) { // The top right of the node is outside of the constraint, move it in. newleft = cx + cwidth - nwidth; } } // Constrain Y. if (nheight > cheight && coverflow.toLowerCase() === 'hidden') { // The node extends over the constrained area and would be clipped. // Reduce the height of the node and force its overflow to scroll. newheight = nheight = cheight; node.setStyle('overflow', 'auto'); } // If the node is below the top of the constraint AND // the node is longer than the constraint allows. if (ny >= cy && ny + nheight > cy + cheight) { // Move it up. newtop = cy + cheight - nheight; try { boxshadow = node.getStyle('boxShadow').replace(/.*? (\d+)px \d+px$/, '$1'); if (new RegExp(/^\d+$/).test(boxshadow) && newtop - cy > boxshadow) { newtop -= boxshadow; } } catch (ex) { Y.log('Failed to determine box-shadow margin.', 'warn', ACTIONMENU.NAME); } } if (newleft !== null) { node.setX(newleft); } if (newtop !== null) { node.setY(newtop); } if (newwidth !== null) { node.setStyle('width', newwidth.toString() + 'px'); } if (newheight !== null) { node.setStyle('height', newheight.toString() + 'px'); } } }; Y.extend(ACTIONMENU, Y.Base, ACTIONMENU.prototype, { NAME: 'moodle-core-actionmenu', ATTRS: { align: { value: [ ALIGN.TR, // The dialogue. ALIGN.BR // The button ] } } }); M.core = M.core || {}; M.core.actionmenu = M.core.actionmenu || {}; /** * * @static * @property M.core.actionmenu.instance * @type {ACTIONMENU} */ M.core.actionmenu.instance = null; /** * Init function - will only ever create one instance of the actionmenu class. * * @method M.core.actionmenu.init * @static * @param {Object} params */ M.core.actionmenu.init = M.core.actionmenu.init || function(params) { M.core.actionmenu.instance = M.core.actionmenu.instance || new ACTIONMENU(params); }; /** * Registers a new DOM node with the action menu causing it to be enhanced if required. * * @method M.core.actionmenu.newDOMNode * @param node * @return {boolean} */ M.core.actionmenu.newDOMNode = function(node) { if (M.core.actionmenu.instance === null) { return true; } node.all(SELECTOR.MENU).each(M.core.actionmenu.instance.enhance, M.core.actionmenu.instance); }; src/blocks/build.json000064400000000314152265472450010614 0ustar00{ "name": "moodle-core-blocks", "builds": { "moodle-core-blocks": { "jsfiles": [ "shared.js", "blocks.js", "manager.js", "blockregion.js" ] } } } src/blocks/meta/blocks.json000064400000000335152265472450011723 0ustar00{ "moodle-core-blocks": { "requires": [ "base", "node", "io", "dom", "dd", "dd-scroll", "moodle-core-dragdrop", "moodle-core-notification" ] } } src/blocks/js/blockregion.js000064400000016122152265472450012076 0ustar00/** * This file contains the Block Region class used by the drag and drop manager. * * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ /** * Constructs a new block region object. * * @namespace M.core.blockdraganddrop * @class BlockRegion * @constructor * @extends Base */ BLOCKREGION = function() { BLOCKREGION.superclass.constructor.apply(this, arguments); }; BLOCKREGION.prototype = { /** * Called during the initialisation process of the object. * @method initializer */ initializer: function() { var node = this.get('node'); Y.log('Block region `' + this.get('region') + '` initialising', 'info'); if (!node) { Y.log('block region known about but no HTML structure found for it. Guessing structure.', 'warn'); node = this.create_and_add_node(); } var body = Y.one('body'), hasblocks = node.all('.' + CSS.BLOCK).size() > 0, hasregionclass = this.get_has_region_class(); this.set('hasblocks', hasblocks); if (!body.hasClass(hasregionclass)) { body.addClass(hasregionclass); } body.addClass((hasblocks) ? this.get_used_region_class() : this.get_empty_region_class()); body.removeClass((hasblocks) ? this.get_empty_region_class() : this.get_used_region_class()); }, /** * Creates a generic block region node and adds it to the DOM at the best guess location. * Any calling of this method is an unfortunate circumstance. * @method create_and_add_node * @return Node The newly created Node */ create_and_add_node: function() { var c = Y.Node.create, region = this.get('region'), node = c('
') .addClass(CSS.BLOCKREGION) .setData('blockregion', region), regions = this.get('manager').get('regions'), i, haspre = false, haspost = false, added = false, pre, post; for (i in regions) { if (regions[i].match(/(pre|left)/)) { haspre = regions[i]; } else if (regions[i].match(/(post|right)/)) { haspost = regions[i]; } } if (haspre !== false && haspost !== false) { if (region === haspre) { post = Y.one('#block-region-' + haspost); if (post) { post.insert(node, 'before'); added = true; } } else { pre = Y.one('#block-region-' + haspre); if (pre) { pre.insert(node, 'after'); added = true; } } } if (added === false) { Y.one('body').append(node); } this.set('node', node); return node; }, /** * Change the move icons to enhanced drag handles and changes the cursor to a move icon when over the header. * @param M.core.dragdrop the block manager * @method change_block_move_icons */ change_block_move_icons: function(manager) { var handle; this.get('node').all('.' + CSS.BLOCK + ' a.' + CSS.EDITINGMOVE).each(function(moveicon) { moveicon.setStyle('cursor', 'move'); handle = manager.get_drag_handle(moveicon.getAttribute('title'), '', 'icon', true); // Dragdrop module assigns this with a button role by default. // However, the block move icon is part of a menubar, so it should have a menuitem role. handle.setAttribute('role', 'menuitem'); moveicon.replace(handle); }); }, /** * Returns the class name on the body that signifies the document knows about this region. * @method get_has_region_class * @return String */ get_has_region_class: function() { return 'has-region-' + this.get('region'); }, /** * Returns the class name to use on the body if the region contains no blocks. * @method get_empty_region_class * @return String */ get_empty_region_class: function() { return 'empty-region-' + this.get('region'); }, /** * Returns the class name to use on the body if the region contains blocks. * @method get_used_region_class * @return String */ get_used_region_class: function() { return 'used-region-' + this.get('region'); }, /** * Returns the node to use as the drop target for this region. * @method get_droptarget * @return Node */ get_droptarget: function() { var node = this.get('node'); if (node.test('[data-droptarget="1"]')) { return node; } return node.one('[data-droptarget="1"]'); }, /** * Enables the block region so that we can be sure the user can see it. * This is done even if it is empty. * @method enable */ enable: function() { Y.one('body').addClass(this.get_used_region_class()).removeClass(this.get_empty_region_class()); }, /** * Disables the region if it contains no blocks, essentially hiding it from the user. * @method disable_if_required */ disable_if_required: function() { if (this.get('node').all('.' + CSS.BLOCK).size() === 0) { Y.one('body').addClass(this.get_empty_region_class()).removeClass(this.get_used_region_class()); } } }; Y.extend(BLOCKREGION, Y.Base, BLOCKREGION.prototype, { NAME: 'core-blocks-dragdrop-blockregion', ATTRS: { /** * The drag and drop manager that created this block region instance. * @attribute manager * @type M.core.blockdraganddrop.Manager * @writeOnce */ manager: { // Can only be set during initialisation and must be set then. writeOnce: 'initOnly', validator: function(value) { return Y.Lang.isObject(value) && value instanceof MANAGER; } }, /** * The name of the block region this object represents. * @attribute region * @type String * @writeOnce */ region: { // Can only be set during initialisation and must be set then. writeOnce: 'initOnly', validator: function(value) { return Y.Lang.isString(value); } }, /** * The node the block region HTML starts at.s * @attribute region * @type Y.Node */ node: { validator: function(value) { return Y.Lang.isObject(value) || Y.Lang.isNull(value); } }, /** * True if the block region currently contains blocks. * @attribute hasblocks * @type Boolean * @default false */ hasblocks: { value: false, validator: function(value) { return Y.Lang.isBoolean(value); } } } }); src/blocks/js/manager.js000064400000030765152265472450011223 0ustar00/** * This file contains the drag and drop manager class. * * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ /** * Constructs a new Block drag and drop manager. * * @namespace M.core.blockdraganddrop * @class Manager * @constructor * @extends M.core.dragdrop */ MANAGER = function() { MANAGER.superclass.constructor.apply(this, arguments); }; MANAGER.prototype = { /** * The skip block link from above the block being dragged while a drag is in progress. * Required by the M.core.dragdrop from whom this class extends. * @private * @property skipnodetop * @type Node * @default null */ skipnodetop: null, /** * The skip block link from below the block being dragged while a drag is in progress. * Required by the M.core.dragdrop from whom this class extends. * @private * @property skipnodebottom * @type Node * @default null */ skipnodebottom: null, /** * An associative object of regions and the * @property regionobjects * @type {Object} Primitive object mocking an associative array. * @type {BLOCKREGION} [regionname]* Each item uses the region name as the key with the value being * an instance of the BLOCKREGION class. */ regionobjects: {}, /** * Called during the initialisation process of the object. * @method initializer */ initializer: function() { Y.log('Initialising drag and drop for blocks.', 'info'); var regionnames = this.get('regions'); var i = 0; var dragContainer; var dragdelegation; var region; var regionContainer; var regionname; // Evil required by M.core.dragdrop. this.groups = ['block']; this.samenodeclass = CSS.BLOCK; this.parentnodeclass = CSS.BLOCKREGION; // Detect the direction of travel. this.detectkeyboarddirection = true; // Add relevant classes and ID to 'content' block region on Dashboard page. var myhomecontent = Y.Node.all('body#' + CSS.MYINDEX + ' #' + CSS.REGIONMAIN + ' > .' + CSS.REGIONCONTENT); if (myhomecontent.size() > 0) { var contentregion = myhomecontent.item(0); contentregion.addClass(CSS.BLOCKREGION); contentregion.set('id', CSS.REGIONCONTENT); contentregion.one('div').addClass(CSS.REGIONCONTENT); } for (i in regionnames) { regionname = regionnames[i]; regionContainer = Y.one('#block-region-' + regionname); region = new BLOCKREGION({ manager: this, region: regionname, node: regionContainer, }); this.regionobjects[regionname] = region; // Setting blockregion as droptarget (the case when it is empty) // The region-post (the right one) // is very narrow, so add extra padding on the left to drop block on it. new Y.DD.Drop({ node: region.get_droptarget(), groups: this.groups, padding: '40 240 40 240' }); // Make each div element in the list of blocks draggable dragdelegation = new Y.DD.Delegate({ container: region.get_droptarget(), nodes: '.' + CSS.BLOCK, target: true, handles: [SELECTOR.DRAGHANDLE], invalid: '.block-hider-hide, .block-hider-show, .moveto, .block_fake', dragConfig: {groups: this.groups} }); dragdelegation.dd.plug(Y.Plugin.DDProxy, { // Don't move the node at the end of the drag moveOnEnd: false }); if (regionContainer === null) { dragdelegation.dd.plug(Y.Plugin.DDWinScroll); } else { dragContainer = regionContainer.ancestor('.drag-container', true); if (dragContainer) { dragdelegation.dd.plug(Y.Plugin.DDNodeScroll, { node: dragContainer, }); } else { dragdelegation.dd.plug(Y.Plugin.DDWinScroll); } } // On the DD Manager start operation, we enable all block regions so that they can be drop targets. This // must be done *before* drag:start but after dragging has been initialised. Y.DD.DDM.on('ddm:start', this.enable_all_regions, this); region.change_block_move_icons(this); } Y.log('Initialisation of drag and drop for blocks complete.', 'info'); }, /** * Returns the ID of the block the given node represents. * @method get_block_id * @param {Node} node * @return {int} The blocks ID in the database. */ get_block_id: function(node) { return Number(node.get('id').replace(/inst/i, '')); }, /** * Returns the block region that the node is part of or belonging to. * @method get_block_region * @param {Y.Node} node * @return {string} The region name. */ get_block_region: function(node) { if (!node.test('[data-blockregion]')) { node = node.ancestor('[data-blockregion]'); } return node.getData('blockregion'); }, /** * Returns the BLOCKREGION instance that represents the block region the given node is part of. * @method get_region_object * @param {Y.Node} node * @return {BLOCKREGION} */ get_region_object: function(node) { return this.regionobjects[this.get_block_region(node)]; }, /** * Enables all fo the regions so that they are all visible while dragging is occuring. * * @method enable_all_regions */ enable_all_regions: function() { var groups = Y.DD.DDM.activeDrag.get('groups'); // As we're called by Y.DD.DDM, we can't be certain that the call // relates specifically to a block drag/drop operation. Test // whether the relevant group applies here. if (!groups || Y.Array.indexOf(groups, 'block') === -1) { return; } var i; for (i in this.regionobjects) { if (!this.regionobjects.hasOwnProperty(i)) { continue; } this.regionobjects[i].enable(); } }, /** * Disables enabled regions if they contain no blocks. * @method disable_regions_if_required */ disable_regions_if_required: function() { var i = 0; for (i in this.regionobjects) { this.regionobjects[i].disable_if_required(); } }, /** * Called by M.core.dragdrop.global_drag_start when dragging starts. * @method drag_start * @param {Event} e */ drag_start: function(e) { // Get our drag object var drag = e.target; // Store the parent node of original drag node (block) // we will need it later for show/hide empty regions // Determine skipnodes and store them if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) { this.skipnodetop = drag.get('node').previous(); } if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) { this.skipnodebottom = drag.get('node').next(); } }, dragOver: function(e) { var nearestRegion = e.drop.get('node').ancestor('.drag-container', true); if (nearestRegion) { if (e.drag[Y.Plugin.DDNodeScroll]) { if (e.drag[Y.Plugin.DDNodeScroll].get('node') === nearestRegion) { // Do not bother resetting the region - it has not changed. return; } else { e.drag.unplug(Y.Plugin.DDNodeScroll); } } e.drag.plug(Y.Plugin.DDNodeScroll, { node: nearestRegion, }); } }, /** * Called by M.core.dragdrop.global_drop_over when something is dragged over a drop target. * @method drop_over * @param {Event} e */ drop_over: function(e) { // Get a reference to our drag and drop nodes var drag = e.drag.get('node'); var drop = e.drop.get('node'); // We need to fix the case when parent drop over event has determined // 'goingup' and appended the drag node after admin-block. if (drop.hasClass(CSS.REGIONCONTENT) && drop.one('.' + CSS.BLOCKADMINBLOCK) && drop.one('.' + CSS.BLOCKADMINBLOCK).next('.' + CSS.BLOCK)) { drop.prepend(drag); } }, /** * Called by M.core.dragdrop.global_drop_end when a drop has been completed. * @method drop_end */ drop_end: function() { // Clear variables. this.skipnodetop = null; this.skipnodebottom = null; this.disable_regions_if_required(); }, /** * Called by M.core.dragdrop.global_drag_dropmiss when something has been dropped on a node that isn't contained by * a drop target. * * @method drag_dropmiss * @param {Event} e */ 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); }, /** * Called by M.core.dragdrop.global_drag_hit when something has been dropped on a drop target. * @method drop_hit * @param {Event} e */ drop_hit: function(e) { // Get a reference to our drag node var dragnode = e.drag.get('node'); var dropnode = e.drop.get('node'); // Amend existing skipnodes if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) { // the one that belongs to block below move below dragnode.insert(dragnode.previous(), 'after'); } // Move original skipnodes if (this.skipnodetop) { dragnode.insert(this.skipnodetop, 'before'); } if (this.skipnodebottom) { dragnode.insert(this.skipnodebottom, 'after'); } // Add lightbox if it not there var lightbox = M.util.add_lightbox(Y, dragnode); // Prepare request parameters var params = { sesskey: M.cfg.sesskey, pagehash: this.get('pagehash'), action: 'move', bui_moveid: this.get_block_id(dragnode), bui_newregion: this.get_block_region(dropnode) }; if (this.get('cmid')) { params.cmid = this.get('cmid'); } if (dragnode.next('.' + CSS.BLOCK) && !dragnode.next('.' + CSS.BLOCK).hasClass(CSS.BLOCKADMINBLOCK)) { params.bui_beforeid = this.get_block_id(dragnode.next('.' + CSS.BLOCK)); } // Do AJAX request Y.io(M.cfg.wwwroot + AJAXURL, { method: 'POST', data: params, on: { start: function() { lightbox.show(); }, success: function(tid, response) { window.setTimeout(function() { lightbox.hide(); }, 250); try { var responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); }, complete: function() { this.disable_regions_if_required(); } }, context: this }); } }; Y.extend(MANAGER, M.core.dragdrop, MANAGER.prototype, { NAME: 'core-blocks-dragdrop-manager', ATTRS: { /** * The page identifier. * @attribute pagehash * @type string|null * @default null */ pagehash: { value: null }, /** * An array of block regions that are present on the page. * @attribute regions * @type array|null * @default Array[] */ regions: { value: [] } } }); src/blocks/js/blocks.js000064400000032200152265472450011050 0ustar00/** * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ var AJAXURL = '/lib/ajax/blocks.php', CSS = { BLOCK: 'block', BLOCKREGION: 'block-region', BLOCKADMINBLOCK: 'block_adminblock', EDITINGMOVE: 'editing_move', HEADER: 'header', LIGHTBOX: 'lightbox', REGIONCONTENT: 'region-content', SKIPBLOCK: 'skip-block', SKIPBLOCKTO: 'skip-block-to', MYINDEX: 'page-my-index', REGIONMAIN: 'region-main', BLOCKSMOVING: 'blocks-moving' }; var SELECTOR = { DRAGHANDLE: '.' + CSS.HEADER + ' .commands .moodle-core-dragdrop-draghandle' }; /** * Legacy drag and drop manager. * This drag and drop manager is specifically designed for themes using side-pre and side-post * that do not make use of the block output methods introduced by MDL-39824. * * @namespace M.core.blockdraganddrop * @class LegacyManager * @constructor * @extends M.core.dragdrop */ DRAGBLOCK = function() { DRAGBLOCK.superclass.constructor.apply(this, arguments); }; Y.extend(DRAGBLOCK, M.core.dragdrop, { skipnodetop: null, skipnodebottom: null, dragsourceregion: null, initializer: function() { // Set group for parent class this.groups = ['block']; this.samenodeclass = CSS.BLOCK; this.parentnodeclass = CSS.REGIONCONTENT; // Add relevant classes and ID to 'content' block region on Dashboard page. var myhomecontent = Y.Node.all('body#' + CSS.MYINDEX + ' #' + CSS.REGIONMAIN + ' > .' + CSS.REGIONCONTENT); if (myhomecontent.size() > 0) { var contentregion = myhomecontent.item(0); contentregion.addClass(CSS.BLOCKREGION); contentregion.set('id', CSS.REGIONCONTENT); contentregion.one('div').addClass(CSS.REGIONCONTENT); } // Initialise blocks dragging // Find all block regions on the page var blockregionlist = Y.Node.all('div.' + CSS.BLOCKREGION); if (blockregionlist.size() === 0) { return false; } // See if we are missing either of block regions, // if yes we need to add an empty one to use as target if (blockregionlist.size() !== this.get('regions').length) { var blockregion = Y.Node.create('
') .addClass(CSS.BLOCKREGION); var regioncontent = Y.Node.create('
') .addClass(CSS.REGIONCONTENT); blockregion.appendChild(regioncontent); var pre = blockregionlist.filter('#region-pre'); var post = blockregionlist.filter('#region-post'); if (pre.size() === 0 && post.size() === 1) { // pre block is missing, instert it before post blockregion.setAttrs({id: 'region-pre'}); post.item(0).insert(blockregion, 'before'); blockregionlist.unshift(blockregion); } else if (post.size() === 0 && pre.size() === 1) { // post block is missing, instert it after pre blockregion.setAttrs({id: 'region-post'}); pre.item(0).insert(blockregion, 'after'); blockregionlist.push(blockregion); } } blockregionlist.each(function(blockregionnode) { // Setting blockregion as droptarget (the case when it is empty) // The region-post (the right one) // is very narrow, so add extra padding on the left to drop block on it. new Y.DD.Drop({ node: blockregionnode.one('div.' + CSS.REGIONCONTENT), groups: this.groups, padding: '40 240 40 240' }); // Make each div element in the list of blocks draggable var del = new Y.DD.Delegate({ container: blockregionnode, nodes: '.' + CSS.BLOCK, target: true, handles: [SELECTOR.DRAGHANDLE], invalid: '.block-hider-hide, .block-hider-show, .moveto', 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.DDWinScroll); var blocklist = blockregionnode.all('.' + CSS.BLOCK); blocklist.each(function(blocknode) { var move = blocknode.one('a.' + CSS.EDITINGMOVE); if (move) { move.replace(this.get_drag_handle(move.getAttribute('title'), '', 'iconsmall', true)); blocknode.one(SELECTOR.DRAGHANDLE).setStyle('cursor', 'move'); } }, this); }, this); }, get_block_id: function(node) { return Number(node.get('id').replace(/inst/i, '')); }, get_block_region: function(node) { var region = node.ancestor('div.' + CSS.BLOCKREGION).get('id').replace(/region-/i, ''); if (Y.Array.indexOf(this.get('regions'), region) === -1) { // Must be standard side-X if (window.right_to_left()) { if (region === 'post') { region = 'pre'; } else if (region === 'pre') { region = 'post'; } } return 'side-' + region; } // Perhaps custom region return region; }, get_region_id: function(node) { return node.get('id').replace(/region-/i, ''); }, drag_start: function(e) { // Get our drag object var drag = e.target; // Store the parent node of original drag node (block) // we will need it later for show/hide empty regions this.dragsourceregion = drag.get('node').ancestor('div.' + CSS.BLOCKREGION); // Determine skipnodes and store them if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) { this.skipnodetop = drag.get('node').previous(); } if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) { this.skipnodebottom = drag.get('node').next(); } // Add the blocks-moving class so that the theme can respond if need be. Y.one('body').addClass(CSS.BLOCKSMOVING); }, drop_over: function(e) { // Get a reference to our drag and drop nodes var drag = e.drag.get('node'); var drop = e.drop.get('node'); // We need to fix the case when parent drop over event has determined // 'goingup' and appended the drag node after admin-block. if (drop.hasClass(this.parentnodeclass) && drop.one('.' + CSS.BLOCKADMINBLOCK) && drop.one('.' + CSS.BLOCKADMINBLOCK).next('.' + CSS.BLOCK)) { drop.prepend(drag); } // Block is moved within the same region // stop here, no need to modify anything. if (this.dragsourceregion.contains(drop)) { return false; } // TODO: Hiding-displaying block region only works for base theme blocks // (region-pre, region-post) at the moment. It should be improved // to work with custom block regions as well. // TODO: Fix this for the case when user drag block towards empty section, // then the section appears, then user chnages his mind and moving back to // original section. The opposite section remains opened and empty. var documentbody = Y.one('body'); // Moving block towards hidden region-content, display it var regionname = this.get_region_id(this.dragsourceregion); if (documentbody.hasClass('side-' + regionname + '-only')) { documentbody.removeClass('side-' + regionname + '-only'); } // Moving from empty region-content towards the opposite one, // hide empty one (only for region-pre, region-post areas at the moment). regionname = this.get_region_id(drop.ancestor('div.' + CSS.BLOCKREGION)); if (this.dragsourceregion.all('.' + CSS.BLOCK).size() === 0 && this.dragsourceregion.get('id').match(/(region-pre|region-post)/i)) { if (!documentbody.hasClass('side-' + regionname + '-only')) { documentbody.addClass('side-' + regionname + '-only'); } } }, drag_end: function() { // clear variables this.skipnodetop = null; this.skipnodebottom = null; this.dragsourceregion = null; // Remove the blocks moving class once the drag-drop is over. Y.one('body').removeClass(CSS.BLOCKSMOVING); }, 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); }, drop_hit: function(e) { var drag = e.drag; // Get a reference to our drag node var dragnode = drag.get('node'); var dropnode = e.drop.get('node'); // Amend existing skipnodes if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) { // the one that belongs to block below move below dragnode.insert(dragnode.previous(), 'after'); } // Move original skipnodes if (this.skipnodetop) { dragnode.insert(this.skipnodetop, 'before'); } if (this.skipnodebottom) { dragnode.insert(this.skipnodebottom, 'after'); } // Add lightbox if it not there var lightbox = M.util.add_lightbox(Y, dragnode); // Prepare request parameters var params = { sesskey: M.cfg.sesskey, pagehash: this.get('pagehash'), action: 'move', bui_moveid: this.get_block_id(dragnode), bui_newregion: this.get_block_region(dropnode) }; if (this.get('cmid')) { params.cmid = this.get('cmid'); } if (dragnode.next('.' + this.samenodeclass) && !dragnode.next('.' + this.samenodeclass).hasClass(CSS.BLOCKADMINBLOCK)) { params.bui_beforeid = this.get_block_id(dragnode.next('.' + this.samenodeclass)); } // Do AJAX request Y.io(M.cfg.wwwroot + AJAXURL, { method: 'POST', data: params, on: { start: function() { lightbox.show(); }, success: function(tid, response) { window.setTimeout(function() { lightbox.hide(); }, 250); try { var responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); } }, context: this }); } }, { NAME: 'core-blocks-dragdrop', ATTRS: { pagehash: { value: null }, regions: { value: null } } }); M.core = M.core || {}; M.core.blockdraganddrop = M.core.blockdraganddrop || {}; /** * True if the page is using the new blocks methods. * @private * @static * @property M.core.blockdraganddrop._isusingnewblocksmethod * @type Boolean * @default null */ M.core.blockdraganddrop._isusingnewblocksmethod = null; /** * Returns true if the page is using the new blocks methods. * @static * @method M.core.blockdraganddrop.is_using_blocks_render_method * @return Boolean */ M.core.blockdraganddrop.is_using_blocks_render_method = function() { if (this._isusingnewblocksmethod === null) { var goodregions = Y.all('.block-region[data-blockregion]').size(); var allregions = Y.all('.block-region').size(); this._isusingnewblocksmethod = (allregions === goodregions); if (goodregions > 0 && allregions > 0 && goodregions !== allregions) { Y.log('Both core_renderer::blocks and core_renderer::blocks_for_region have been used.', 'warn', 'moodle-core_blocks'); } } return this._isusingnewblocksmethod; }; /** * Initialises a drag and drop manager. * This should only ever be called once for a page. * @static * @method M.core.blockdraganddrop.init * @param {Object} params * @return Manager */ M.core.blockdraganddrop.init = function(params) { if (this.is_using_blocks_render_method()) { Y.log('Block drag and drop initialised for the blocks method.', 'info', 'moodle-core_blocks'); new MANAGER(params); } else { Y.log('Block drag and drop initialised with the legacy manager (blocks_for_region used).', 'info', 'moodle-core_blocks'); new DRAGBLOCK(params); } }; /* * Legacy code to keep things working. */ M.core_blocks = M.core_blocks || {}; M.core_blocks.init_dragdrop = function(params) { M.core.blockdraganddrop.init(params); }; src/blocks/js/shared.js000064400000000055152265472450011044 0ustar00var MANAGER; var BLOCKREGION; var DRAGBLOCK; src/widget-focusafterclose/build.json000064400000000315152265472450014010 0ustar00{ "name": "moodle-core-widget-focusafterclose", "builds": { "moodle-core-widget-focusafterclose": { "jsfiles": [ "focusafter.js" ] } } } src/widget-focusafterclose/meta/notification.json000064400000000205152265472450016323 0ustar00{ "moodle-core-widget-focusafterclose": { "requires": [ "base-build", "widget" ] } } src/widget-focusafterclose/js/focusafter.js000064400000012402152265472450015131 0ustar00/** * Provides support for focusing on different nodes after the Widget is * hidden. * * If the focusOnPreviousTargetAfterHide attribute is true, then the module hooks * into the show function for that Widget to try and determine which Node * caused the Widget to be shown. * * Alternatively, the focusAfterHide attribute can be passed a Node. * * @module moodle-core-widget-focusafterhide */ var CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), ' + 'a[href], button, textarea, select, ' + '[tabindex], [contenteditable="true"]'; /** * Provides support for focusing on different nodes after the Widget is * hidden. * * @class M.core.WidgetFocusAfterHide */ function WidgetFocusAfterHide() { Y.after(this._bindUIFocusAfterHide, this, 'bindUI'); if (this.get('rendered')) { this._bindUIFocusAfterHide(); } } WidgetFocusAfterHide.ATTRS = { /** * Whether to focus on the target that caused the Widget to be shown. * * If this is true, and a valid Node is found, any Node specified to focusAfterHide * will be ignored. * * @attribute focusOnPreviousTargetAfterHide * @default false * @type boolean */ focusOnPreviousTargetAfterHide: { value: false }, /** * The Node to focus on after hiding the Widget. * * Note: If focusOnPreviousTargetAfterHide is true, and a valid Node is found, then this * value will be ignored. If it is true and not found, then this value will be used as * a fallback. * * @attribute focusAfterHide * @default null * @type Node */ focusAfterHide: { value: null, type: Y.Node } }; WidgetFocusAfterHide.prototype = { /** * The list of Event Handles which we should cancel when the dialogue is destroyed. * * @property uiHandleFocusAfterHide * @type array * @protected */ _uiHandlesFocusAfterHide: [], /** * A reference to the real show method which is being overwritten. * * @property _showFocusAfterHide * @type function * @default null * @protected */ _showFocusAfterHide: null, /** * A reference to the detected previous target. * * @property _previousTargetFocusAfterHide * @type function * @default null * @protected */ _previousTargetFocusAfterHide: null, initializer: function() { if (this.get('focusOnPreviousTargetAfterHide') && this.show) { // Overwrite the parent method so that we can get the focused // target. this._showFocusAfterHide = this.show; this.show = function(e) { this._showFocusAfterHide.apply(this, arguments); // We use a property rather than overriding the focusAfterHide parameter in // case the target cannot be found at hide time. this._previousTargetFocusAfterHide = null; if (e && e.currentTarget) { Y.log("Determined a Node which caused the Widget to be shown", 'debug', 'moodle-core-widget-focusafterhide'); this._previousTargetFocusAfterHide = e.currentTarget; } }; } }, destructor: function() { new Y.EventHandle(this.uiHandleFocusAfterHide).detach(); }, /** * Set up the event handling required for this module to work. * * @method _bindUIFocusAfterHide * @private */ _bindUIFocusAfterHide: function() { // Detach the old handles first. new Y.EventHandle(this.uiHandleFocusAfterHide).detach(); this.uiHandleFocusAfterHide = [ this.after('visibleChange', this._afterHostVisibleChangeFocusAfterHide) ]; }, /** * Handle the change in UI visibility. * * This method changes the focus after the hide has taken place. * * @method _afterHostVisibleChangeFocusAfterHide * @private */ _afterHostVisibleChangeFocusAfterHide: function() { if (!this.get('visible')) { if (this._attemptFocus(this._previousTargetFocusAfterHide)) { Y.log("Focusing on the target automatically determined when the Widget was opened", 'debug', 'moodle-core-widget-focusafterhide'); } else if (this._attemptFocus(this.get('focusAfterHide'))) { // Fall back to the focusAfterHide value if one was specified. Y.log("Focusing on the target provided to focusAfterHide", 'debug', 'moodle-core-widget-focusafterhide'); } else { Y.log("No valid focus target found - not returning focus.", 'debug', 'moodle-core-widget-focusafterhide'); } } }, _attemptFocus: function(node) { var focusTarget = Y.one(node); if (focusTarget) { focusTarget = focusTarget.ancestor(CAN_RECEIVE_FOCUS_SELECTOR, true); if (focusTarget) { focusTarget.focus(); return true; } } return false; } }; var NS = Y.namespace('M.core'); NS.WidgetFocusAfterHide = WidgetFocusAfterHide; src/dragdrop/build.json000064400000000257152265472450011147 0ustar00{ "name": "moodle-core-dragdrop", "builds": { "moodle-core-dragdrop": { "jsfiles": [ "dragdrop.js" ] } } } src/dragdrop/meta/dragdrop.json000064400000000402152265472450012570 0ustar00{ "moodle-core-dragdrop": { "requires": [ "base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification" ] } } src/dragdrop/js/dragdrop.js000064400000060106152265472450011730 0ustar00/* 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; src/maintenancemodetimer/build.json000064400000000261152265472450013530 0ustar00{ "name": "moodle-core-maintenancemodetimer", "builds": { "moodle-core-maintenancemodetimer": { "jsfiles": [ "maintenancemodetimer.js" ] } } } src/maintenancemodetimer/meta/maintenancemodetimer.json000064400000000147152265472450017552 0ustar00{ "moodle-core-maintenancemodetimer": { "requires": [ "base", "node" ] } } src/maintenancemodetimer/js/maintenancemodetimer.js000064400000004166152265472450016710 0ustar00/** * Maintenance mode timer display. * * @module moodle-core-maintenancemodetimer */ var MAINTENANCEMODETIMER = function() { MAINTENANCEMODETIMER.superclass.constructor.apply(this, arguments); }; Y.extend(MAINTENANCEMODETIMER, Y.Base, { timeleftinsec: 0, maintenancenode: Y.one('.box.maintenancewarning'), /** * Initialise timer if maintenancemode set. * * @method initializer * @param config {Array} array with timeleftinsec set. */ initializer: function(config) { if (this.maintenancenode) { this.timeleftinsec = config.timeleftinsec; this.maintenancenode.setAttribute('aria-live', 'polite'); Y.later(1000, this, 'updatetimer', null, true); } }, /** * Decrement time left and update display text. * * @method updatetimer */ updatetimer: function() { this.timeleftinsec -= 1; if (this.timeleftinsec <= 0) { this.maintenancenode.set('text', M.util.get_string('sitemaintenance', 'admin')); } else { var a = {}; a.sec = this.timeleftinsec % 60; a.min = Math.floor(this.timeleftinsec / 60) % 60; a.hour = Math.floor(this.timeleftinsec / 3600); if (a.hour > 0) { this.maintenancenode.set('text', M.util.get_string('maintenancemodeisscheduledlong', 'admin', a)); } else { this.maintenancenode.set('text', M.util.get_string('maintenancemodeisscheduled', 'admin', a)); } } // Set error class to highlight the importance. if (this.timeleftinsec < 30) { this.maintenancenode.addClass('alert-error') .addClass('alert-danger') .removeClass('alert-warning'); } else { this.maintenancenode.addClass('alert-warning') .removeClass('alert-error') .removeClass('alert-danger'); } } }); M.core = M.core || {}; M.core.maintenancemodetimer = M.core.maintenancemodetimer || function(config) { return new MAINTENANCEMODETIMER(config); }; src/event/build.json000064400000000246152265472450010464 0ustar00{ "name": "moodle-core-event", "builds": { "moodle-core-event": { "jsfiles": [ "event.js" ] } } } src/event/meta/event.json000064400000000140152265472450011425 0ustar00{ "moodle-core-event": { "requires": [ "event-custom" ] } } src/event/js/event.js000064400000012543152265472450010570 0ustar00// 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 . /** * @module moodle-core-event */ var LOGNAME = 'moodle-core-event'; /** * List of published global JS events in Moodle. This is a collection * of global events that can be subscribed to, or fired from any plugin. * * @namespace M.core * @class event */ M.core = M.core || {}; var eventsConfigured = !!M.core.event; M.core.event = M.core.event || { /** * This event is triggered when a page has added dynamic nodes to a page * that should be processed by the filter system. An example is loading * user text that could have equations in it. MathJax can typeset the equations * but only if it is notified that there are new nodes in the page that need processing. * To trigger this event use M.core.Event.fire(M.core.Event.FILTER_CONTENT_UPDATED, {nodes: list}); * * @event "filter-content-updated" * @param nodes {Y.NodeList} List of nodes added to the DOM. */ FILTER_CONTENT_UPDATED: "filter-content-updated", /** * This event is triggered when an editor has recovered some draft text. * It can be used to determine let other sections know that they should reset their * form comparison for changes. * * @event "editor-content-restored" */ EDITOR_CONTENT_RESTORED: "editor-content-restored", /** * This event is triggered when an mform is about to be submitted via ajax. * * @event "form-submit-ajax" */ FORM_SUBMIT_AJAX: "form-submit-ajax" }; M.core.globalEvents = M.core.globalEvents || { /** * This event is triggered when form has an error * * @event "form_error" * @param formid {string} Id of form with error. * @param elementid {string} Id of element with error. */ FORM_ERROR: "form_error", /** * This event is triggered when the content of a block has changed * * @event "block_content_updated" * @param instanceid ID of the block instance that was updated */ BLOCK_CONTENT_UPDATED: "block_content_updated" }; if (!eventsConfigured) { var eventDefaultConfig = { emitFacade: true, defaultFn: function(e) { Y.log('Event fired: ' + e.type, 'debug', LOGNAME); }, preventedFn: function(e) { Y.log('Event prevented: ' + e.type, 'debug', LOGNAME); }, stoppedFn: function(e) { Y.log('Event stopped: ' + e.type, 'debug', LOGNAME); } }; // Publish events with a custom config here. // Publish all the events with a standard config. var key; for (key in M.core.event) { if (M.core.event.hasOwnProperty(key) && Y.getEvent(M.core.event[key]) === null) { Y.publish(M.core.event[key], eventDefaultConfig); } } // Publish global events. for (key in M.core.globalEvents) { // Make sure the key exists and that the event has not yet been published. Otherwise, skip publishing. if (M.core.globalEvents.hasOwnProperty(key) && Y.Global.getEvent(M.core.globalEvents[key]) === null) { Y.Global.publish(M.core.globalEvents[key], Y.merge(eventDefaultConfig, {broadcast: true})); Y.log('Global event published: ' + key, 'debug', LOGNAME); } } /** * Apply a callback when the DOM is modified and matchecs the supplied targetSelector. * * @method listen * @param {String} targetSelector The selector to apply * @param {Function} applyCallback The function to call on the found node */ var listenForMutation = function(targetSelector, applyCallback) { // Add a MutationObserver to check for new children to the tree. var newNodeObserver = new MutationObserver(function(mutationList) { mutationList.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node instanceof Element && node.matches(targetSelector)) { applyCallback(node); } }); }); }); newNodeObserver.observe(document, {childList: true, subtree: true}); }; // YUI Custom Events do not bubble up the DOM, only up the inheritance path of custom classes. // We have to add a Node Observer to watch for new forms that may need to be watched in future. require(['core_form/events'], function(formEvents) { listenForMutation('form', function(form) { Y.one(form).on(M.core.event.FORM_SUBMIT_AJAX, function(e) { // Prevent cyclical calls. if (e && e.fallbackHandled) { return; } formEvents.notifyFormSubmittedByJavascript(form, window.skipValidation, true); }); }); }); } src/chooserdialogue/build.json000064400000000242152265472450012513 0ustar00{ "name": "moodle-core-chooserdialogue", "builds": { "moodle-core-chooserdialogue": { "jsfiles": [ "chooserdialogue.js" ] } } } src/chooserdialogue/meta/chooserdialogue.json000064400000000207152265472450015517 0ustar00{ "moodle-core-chooserdialogue": { "requires": [ "base", "panel", "moodle-core-notification" ] } } src/chooserdialogue/js/chooserdialogue.js000064400000031650152265472450014656 0ustar00/** * A type of dialogue used as for choosing options. * * @module moodle-core-chooserdialogue */ /** * A type of dialogue used as for choosing options. * * @constructor * @class M.core.chooserdialogue */ var CHOOSERDIALOGUE = function() { CHOOSERDIALOGUE.superclass.constructor.apply(this, arguments); }; Y.extend(CHOOSERDIALOGUE, Y.Base, { // The panel widget panel: null, // The submit button - we disable this until an element is set submitbutton: null, // The chooserdialogue container container: null, // Any event listeners we may need to cancel later listenevents: [], bodycontent: null, headercontent: null, instanceconfig: null, // The hidden field storing the disabled element values for submission. hiddenRadioValue: null, setup_chooser_dialogue: function(bodycontent, headercontent, config) { this.bodycontent = bodycontent; this.headercontent = headercontent; this.instanceconfig = config; }, prepare_chooser: function() { if (this.panel) { return; } // Ensure that we're showing the JS version of the chooser. Y.one(Y.config.doc.body).addClass('jschooser'); // Set Default options var paramkey, params = { bodyContent: this.bodycontent.get('innerHTML'), headerContent: this.headercontent.get('innerHTML'), width: '540px', draggable: true, visible: false, // Hide by default zindex: 100, // Display in front of other items modal: true, // This dialogue should be modal. shim: true, closeButtonTitle: this.get('closeButtonTitle'), focusOnPreviousTargetAfterHide: true, render: false, extraClasses: this._getClassNames() }; // Override with additional options for (paramkey in this.instanceconfig) { params[paramkey] = this.instanceconfig[paramkey]; } // Create the panel this.panel = new M.core.dialogue(params); // Remove the template for the chooser this.bodycontent.remove(); this.headercontent.remove(); // Hide and then render the panel this.panel.hide(); this.panel.render(); // Set useful links. this.container = this.panel.get('boundingBox').one('.choosercontainer'); this.options = this.container.all('.option input[type=radio]'); // The hidden form element we use when submitting. this.hiddenRadioValue = Y.Node.create(''); this.container.one('form').appendChild(this.hiddenRadioValue); // Add the chooserdialogue class to the container for styling this.panel.get('boundingBox').addClass('chooserdialogue'); }, /** * Display the module chooser * * @method display_chooser * @param {EventFacade} e Triggering Event */ display_chooser: function(e) { var bb, dialogue, thisevent; this.prepare_chooser(); // Stop the default event actions before we proceed e.preventDefault(); bb = this.panel.get('boundingBox'); dialogue = this.container.one('.alloptions'); // This will detect a change in orientation and retrigger centering thisevent = Y.one('document').on('orientationchange', function() { this.center_dialogue(dialogue); }, this); this.listenevents.push(thisevent); // Detect window resizes (most browsers) thisevent = Y.one('window').on('resize', function() { this.center_dialogue(dialogue); }, this); this.listenevents.push(thisevent); // These will trigger a check_options call to display the correct help thisevent = this.container.on('click', this.check_options, this); this.listenevents.push(thisevent); thisevent = this.container.on('key_up', this.check_options, this); this.listenevents.push(thisevent); thisevent = this.container.on('dblclick', function(e) { if (e.target.ancestor('div.option')) { this.check_options(); // Prevent duplicate submissions this.submitbutton.setAttribute('disabled', 'disabled'); this.options.setAttribute('disabled', 'disabled'); this.cancel_listenevents(); this.container.one('form').submit(); } }, this); this.listenevents.push(thisevent); this.container.one('form').on('submit', function() { // Prevent duplicate submissions on submit this.submitbutton.setAttribute('disabled', 'disabled'); this.options.setAttribute('disabled', 'disabled'); this.cancel_listenevents(); }, this); // Hook onto the cancel button to hide the form thisevent = this.container.one('.addcancel').on('click', this.cancel_popup, this); this.listenevents.push(thisevent); // Hide will be managed by cancel_popup after restoring the body overflow thisevent = bb.one('button.closebutton').on('click', this.cancel_popup, this); this.listenevents.push(thisevent); // Grab global keyup events and handle them thisevent = Y.one('document').on('keydown', this.handle_key_press, this); this.listenevents.push(thisevent); // Add references to various elements we adjust this.submitbutton = this.container.one('.submitbutton'); // Disable the submit element until the user makes a selection this.submitbutton.set('disabled', 'true'); // Ensure that the options are shown this.options.removeAttribute('disabled'); // Display the panel this.panel.show(e); // Re-centre the dialogue after we've shown it. this.center_dialogue(dialogue); // Finally, focus the first radio element - this enables form selection via the keyboard this.container.one('.option input[type=radio]').focus(); // Trigger check_options to set the initial jumpurl this.check_options(); }, /** * Cancel any listen events in the listenevents queue * * Several locations add event handlers which should only be called before the form is submitted. This provides * a way of cancelling those events. * * @method cancel_listenevents */ cancel_listenevents: function() { // Detach all listen events to prevent duplicate triggers var thisevent; while (this.listenevents.length) { thisevent = this.listenevents.shift(); thisevent.detach(); } }, /** * Calculate the optimum height of the chooser dialogue * * This tries to set a sensible maximum and minimum to ensure that some options are always shown, and preferably * all, whilst fitting the box within the current viewport. * * @method center_dialogue * @param Node {dialogue} Y.Node The dialogue */ center_dialogue: function(dialogue) { var bb = this.panel.get('boundingBox'), winheight = bb.get('winHeight'), newheight, totalheight; if (this.panel.shouldResizeFullscreen()) { dialogue.setStyle('maxHeight', '100%'); dialogue.setStyle('height', 'auto'); this.panel.makeResponsive(); return; } // Try and set a sensible max-height -- this must be done before setting the top // Set a default height of 640px newheight = this.get('maxheight'); if (winheight <= newheight) { // Deal with smaller window sizes if (winheight <= this.get('minheight')) { newheight = this.get('minheight'); } else { newheight = winheight; } } // If the dialogue is larger than a reasonable minimum height, we // disable the page scrollbars. if (newheight > this.get('minheight')) { // Disable the page scrollbars. if (this.panel.lockScroll && !this.panel.lockScroll.isActive()) { this.panel.lockScroll.enableScrollLock(true); } } else { // Re-enable the page scrollbars. if (this.panel.lockScroll && this.panel.lockScroll.isActive()) { this.panel.lockScroll.disableScrollLock(); } } // Take off 15px top and bottom for borders, plus 69px for the title and 57px for the // button area before setting the new max-height. totalheight = newheight; newheight = newheight - (69 + 57 + 15 + 15); dialogue.setStyle('maxHeight', newheight + 'px'); var dialogueheight = bb.getStyle('height'); if (dialogueheight.match(/.*px$/)) { dialogueheight = dialogueheight.replace(/px$/, ''); } else { dialogueheight = totalheight; } if (dialogueheight < this.get('baseheight')) { dialogueheight = this.get('baseheight'); dialogue.setStyle('height', dialogueheight + 'px'); } else { dialogue.setStyle('height', 'auto'); } this.panel.centerDialogue(); }, handle_key_press: function(e) { if (e.keyCode === 27) { this.cancel_popup(e); } }, cancel_popup: function(e) { // Prevent normal form submission before hiding e.preventDefault(); this.hide(); }, hide: function() { // Cancel all listen events this.cancel_listenevents(); this.container.detachAll(); this.panel.hide(); }, check_options: function() { // Check which options are set, and change the parent class // to show/hide help as required this.options.each(function(thisoption) { var optiondiv = thisoption.get('parentNode').get('parentNode'); if (thisoption.get('checked')) { optiondiv.addClass('selected'); // Trigger any events for this option this.option_selected(thisoption); // Ensure that the form may be submitted this.submitbutton.removeAttribute('disabled'); // Ensure that the radio remains focus so that keyboard navigation is still possible thisoption.focus(); } else { optiondiv.removeClass('selected'); } }, this); }, option_selected: function(e) { // Set a hidden input field with the value and name of the radio button. When we submit the form, we // disable the radios to prevent duplicate submission. This has the result however that the value is never // submitted so we set this value to a hidden field instead this.hiddenRadioValue.setAttrs({ value: e.get('value'), name: e.get('name') }); }, /** * Return an array of class names prefixed with 'chooserdialogue-' and * the name of the type of dialogue. * * Note: Class name are converted to lower-case. * * If an array of arguments is supplied, each of these is prefixed and * lower-cased also. * * If no arguments are supplied, then the prefix is returned on it's * own. * * @method _getClassNames * @param {Array} [args] Any additional names to prefix and lower-case. * @return {Array} * @private */ _getClassNames: function(args) { var prefix = 'chooserdialogue-' + this.name, results = []; results.push(prefix.toLowerCase()); if (args) { var arg; for (arg in args) { results.push((prefix + '-' + arg).toLowerCase()); } } return results; } }, { NAME: 'moodle-core-chooserdialogue', ATTRS: { /** * The minimum height (in pixels) before resizing is prevented and scroll * locking disabled. * * @attribute minheight * @type Number * @default 300 */ minheight: { value: 300 }, /** * The base height?? * * @attribute baseheight * @type Number * @default 400 */ baseheight: { value: 400 }, /** * The maximum height (in pixels) at which we stop resizing. * * @attribute maxheight * @type Number * @default 300 */ maxheight: { value: 660 }, /** * The title of the close button. * * @attribute closeButtonTitle * @type String * @default 'Close' */ closeButtonTitle: { validator: Y.Lang.isString, value: 'Close' } } }); M.core = M.core || {}; M.core.chooserdialogue = CHOOSERDIALOGUE; src/lockscroll/build.json000064400000000223152265472450011505 0ustar00{ "name": "moodle-core-lockscroll", "builds": { "moodle-core-lockscroll": { "jsfiles": [ "lockscroll.js" ] } } } src/lockscroll/meta/lockscroll.json000064400000000145152265472450013506 0ustar00{ "moodle-core-lockscroll": { "requires": [ "plugin", "base-build" ] } } src/lockscroll/js/lockscroll.js000064400000013744152265472450012650 0ustar00/** * Provides the ability to lock the scroll for a page, allowing nested * locking. * * @module moodle-core-lockscroll */ /** * Provides the ability to lock the scroll for a page. * * This is achieved by applying the class 'lockscroll' to the body Node. * * Nested widgets are also supported and the scroll lock is only removed * when the final plugin instance is disabled. * * @class M.core.LockScroll * @extends Plugin.Base */ Y.namespace('M.core').LockScroll = Y.Base.create('lockScroll', Y.Plugin.Base, [], { /** * Whether the LockScroll has been activated. * * @property _enabled * @type Boolean * @protected */ _enabled: false, /** * Handle destruction of the lockScroll instance, including disabling * of the current instance. * * @method destructor */ destructor: function() { this.disableScrollLock(); }, /** * Start locking the page scroll. * * This is achieved by applying the lockscroll class to the body Node. * * A count of the total number of active, and enabled, lockscroll instances is also kept on * the body to ensure that premature disabling does not occur. * * @method enableScrollLock * @param {Boolean} forceOnSmallWindow Whether to enable the scroll lock, even for small window sizes. * @chainable */ enableScrollLock: function(forceOnSmallWindow) { if (this.isActive()) { Y.log('LockScroll already active. Ignoring enable request', 'warn', 'moodle-core-lockscroll'); return; } if (!this.shouldLockScroll(forceOnSmallWindow)) { Y.log('Dialogue height greater than window height. Ignoring enable request.', 'warn', 'moodle-core-lockscroll'); return; } Y.log('Enabling LockScroll.', 'debug', 'moodle-core-lockscroll'); this._enabled = true; var body = Y.one(Y.config.doc.body); // Get width of body before turning on lockscroll. var widthBefore = body.getComputedStyle('width'); // We use a CSS class on the body to handle the actual locking. body.addClass('lockscroll'); // Increase the count of active instances - this is used to ensure that we do not // remove the locking when parent windows are still open. // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them. var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 0, newCount = currentCount + 1; body.setAttribute('data-activeScrollLocks', newCount); Y.log("Setting the activeScrollLocks count from " + currentCount + " to " + newCount, 'debug', 'moodle-core-lockscroll'); // When initially enabled, set the body max-width to its current width. This // avoids centered elements jumping because the width changes when scrollbars // disappear. if (currentCount === 0) { body.setStyle('maxWidth', widthBefore); } return this; }, /** * Recalculate whether lock scrolling should be on or off. * * @method shouldLockScroll * @param {Boolean} forceOnSmallWindow Whether to enable the scroll lock, even for small window sizes. * @return boolean */ shouldLockScroll: function(forceOnSmallWindow) { var dialogueHeight = this.get('host').get('boundingBox').get('region').height, // Most modern browsers use win.innerHeight, but some older versions of IE use documentElement.clientHeight. // We fall back to 0 if neither can be found which has the effect of disabling scroll locking. windowHeight = Y.config.win.innerHeight || Y.config.doc.documentElement.clientHeight || 0; if (!forceOnSmallWindow && dialogueHeight > (windowHeight - 10)) { return false; } else { return true; } }, /** * Recalculate whether lock scrolling should be on or off because the size of the dialogue changed. * * @method updateScrollLock * @param {Boolean} forceOnSmallWindow Whether to enable the scroll lock, even for small window sizes. * @chainable */ updateScrollLock: function(forceOnSmallWindow) { // Both these functions already check if scroll lock is active and do the right thing. if (this.shouldLockScroll(forceOnSmallWindow)) { this.enableScrollLock(forceOnSmallWindow); } else { this.disableScrollLock(true); } return this; }, /** * Stop locking the page scroll. * * The instance may be disabled but the scroll lock not removed if other instances of the * plugin are also active. * * @method disableScrollLock * @chainable */ disableScrollLock: function(force) { if (this.isActive()) { Y.log('Disabling LockScroll.', 'debug', 'moodle-core-lockscroll'); this._enabled = false; var body = Y.one(Y.config.doc.body); // Decrease the count of active instances. // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them. var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 1, newCount = currentCount - 1; if (force || currentCount === 1) { body.removeClass('lockscroll'); body.setStyle('maxWidth', null); } body.setAttribute('data-activeScrollLocks', currentCount - 1); Y.log("Setting the activeScrollLocks count from " + currentCount + " to " + newCount, 'debug', 'moodle-core-lockscroll'); } return this; }, /** * Return whether scroll locking is active. * * @method isActive * @return Boolean */ isActive: function() { return this._enabled; } }, { NS: 'lockScroll', ATTRS: { } }); build/moodle-core-notification-exception/moodle-core-notification-exception-min.js000064400000005104152265472450024550 0ustar00YUI.add("moodle-core-notification-exception",function(o,e){var a;M.core=M.core||{},o.extend(a=function(i){var t=o.mix({},i);t.width=t.width||M.cfg.developerdebug?Math.floor(o.one(document.body).get("winWidth")/3)+"px":null,t.closeButton=!0,o.Array.each(["message","name","fileName","lineNumber","stack"],function(e){t[e]=i[e]}),a.superclass.constructor.apply(this,[t])},M.core.notification.info,{_hideTimeout:null,_keypress:null,initializer:function(e){var i=this,t=this.get("hideTimeoutDelay"),a=M.util.get_string("labelsep","langconfig");this.get("notificationBase").addClass("moodle-dialogue-exception"),this.setStdModContent(o.WidgetStdMod.HEADER,'
    '+o.Escape.html(e.name)+"
    ",o.WidgetStdMod.REPLACE),e=o.Node.create('
    ').append(o.Node.create('
    '+o.Escape.html(this.get("message"))+"
    ")).append(o.Node.create('")).append(o.Node.create('")).append(o.Node.create('")),M.cfg.developerdebug&&e.all(".moodle-exception-param").removeClass("hidden"),this.setStdModContent(o.WidgetStdMod.BODY,e,o.WidgetStdMod.REPLACE),t&&(this._hideTimeout=setTimeout(function(){i.hide()},t)),this.after("visibleChange",this.errorVisibilityChanged,this),this._keypress=o.on("key",this.hide,window,"down:13,27",this),this.centerDialogue()},errorVisibilityChanged:function(e){var i;"visible"===e.attrName&&e.prevVal&&!e.newVal&&(this._keypress&&this._keypress.detach(),i=this,setTimeout(function(){i.destroy()},1e3))}},{NAME:"Moodle exception",CSS_PREFIX:"moodle-dialogue",ATTRS:{message:{value:""},name:{value:""},fileName:{value:""},lineNumber:{value:""},stack:{setter:function(e){var i,t=o.Escape.html(e).split("\n"),a=new RegExp("^(.+)@("+M.cfg.wwwroot+")?(.{0,75}).*:(\\d+)$");for(i in t)t[i]=t[i].replace(a,"
    ln: $4
    $3
    $1
    ");return t.join("\n")},value:""},hideTimeoutDelay:{validator:o.Lang.isNumber,value:null}}}),M.core.exception=a},"@VERSION@",{requires:["moodle-core-notification-dialogue"]});build/moodle-core-notification-exception/moodle-core-notification-exception.js000064400000014542152265472450023775 0ustar00YUI.add('moodle-core-notification-exception', function (Y, NAME) { /* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; /** * A dialogue type designed to display an appropriate error when a generic * javascript error was thrown and caught. * * @module moodle-core-notification * @submodule moodle-core-notification-exception */ var EXCEPTION_NAME = 'Moodle exception', EXCEPTION; /** * Extends core Dialogue to show the exception dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.exception * @extends M.core.dialogue */ EXCEPTION = function(c) { var config = Y.mix({}, c); config.width = config.width || (M.cfg.developerdebug) ? Math.floor(Y.one(document.body).get('winWidth') / 3) + 'px' : null; config.closeButton = true; // We need to allow some properties which are part of the exception // prototype, otherwise AttributeCore filters them during value normalisation. var allowlist = [ 'message', 'name', 'fileName', 'lineNumber', 'stack' ]; Y.Array.each(allowlist, function(k) { config[k] = c[k]; }); EXCEPTION.superclass.constructor.apply(this, [config]); }; Y.extend(EXCEPTION, M.core.notification.info, { _hideTimeout: null, _keypress: null, initializer: function(config) { var content, self = this, delay = this.get('hideTimeoutDelay'), labelsep = M.util.get_string('labelsep', 'langconfig'); this.get(BASE).addClass('moodle-dialogue-exception'); this.setStdModContent(Y.WidgetStdMod.HEADER, '
    ' + Y.Escape.html(config.name) + '
    ', Y.WidgetStdMod.REPLACE); content = Y.Node.create('
    ') .append(Y.Node.create('
    ' + Y.Escape.html(this.get('message')) + '
    ')) .append(Y.Node.create('')) .append(Y.Node.create('')) .append(Y.Node.create('')); if (M.cfg.developerdebug) { content.all('.moodle-exception-param').removeClass('hidden'); } this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); if (delay) { this._hideTimeout = setTimeout(function() { self.hide(); }, delay); } this.after('visibleChange', this.errorVisibilityChanged, this); this._keypress = Y.on('key', this.hide, window, 'down:13,27', this); this.centerDialogue(); }, errorVisibilityChanged: function(e) { if (e.attrName === 'visible' && e.prevVal && !e.newVal) { if (this._keypress) { this._keypress.detach(); } var self = this; setTimeout(function() { self.destroy(); }, 1000); } } }, { NAME: EXCEPTION_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The message of the alert. * * @attribute message * @type String * @default '' */ message: { value: '' }, /** * The name of the alert. * * @attribute title * @type String * @default '' */ name: { value: '' }, /** * The name of the file where the error was thrown. * * @attribute fileName * @type String * @default '' */ fileName: { value: '' }, /** * The line number where the error was thrown. * * @attribute lineNumber * @type String * @default '' */ lineNumber: { value: '' }, /** * The backtrace from the error * * @attribute lineNumber * @type String * @default '' */ stack: { setter: function(str) { var lines = Y.Escape.html(str).split("\n"), pattern = new RegExp('^(.+)@(' + M.cfg.wwwroot + ')?(.{0,75}).*:(\\d+)$'), i; for (i in lines) { lines[i] = lines[i].replace(pattern, "
    ln: $4
    " + "
    $3
    " + "
    $1
    "); } return lines.join("\n"); }, value: '' }, /** * If set, the dialogue is hidden after the specified timeout period. * * @attribute hideTimeoutDelay * @type Number * @default null * @optional */ hideTimeoutDelay: { validator: Y.Lang.isNumber, value: null } } }); M.core.exception = EXCEPTION; }, '@VERSION@', {"requires": ["moodle-core-notification-dialogue"]}); build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js000064400000014542152265472450025061 0ustar00YUI.add('moodle-core-notification-exception', function (Y, NAME) { /* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; /** * A dialogue type designed to display an appropriate error when a generic * javascript error was thrown and caught. * * @module moodle-core-notification * @submodule moodle-core-notification-exception */ var EXCEPTION_NAME = 'Moodle exception', EXCEPTION; /** * Extends core Dialogue to show the exception dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.exception * @extends M.core.dialogue */ EXCEPTION = function(c) { var config = Y.mix({}, c); config.width = config.width || (M.cfg.developerdebug) ? Math.floor(Y.one(document.body).get('winWidth') / 3) + 'px' : null; config.closeButton = true; // We need to allow some properties which are part of the exception // prototype, otherwise AttributeCore filters them during value normalisation. var allowlist = [ 'message', 'name', 'fileName', 'lineNumber', 'stack' ]; Y.Array.each(allowlist, function(k) { config[k] = c[k]; }); EXCEPTION.superclass.constructor.apply(this, [config]); }; Y.extend(EXCEPTION, M.core.notification.info, { _hideTimeout: null, _keypress: null, initializer: function(config) { var content, self = this, delay = this.get('hideTimeoutDelay'), labelsep = M.util.get_string('labelsep', 'langconfig'); this.get(BASE).addClass('moodle-dialogue-exception'); this.setStdModContent(Y.WidgetStdMod.HEADER, '
    ' + Y.Escape.html(config.name) + '
    ', Y.WidgetStdMod.REPLACE); content = Y.Node.create('
    ') .append(Y.Node.create('
    ' + Y.Escape.html(this.get('message')) + '
    ')) .append(Y.Node.create('')) .append(Y.Node.create('')) .append(Y.Node.create('')); if (M.cfg.developerdebug) { content.all('.moodle-exception-param').removeClass('hidden'); } this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); if (delay) { this._hideTimeout = setTimeout(function() { self.hide(); }, delay); } this.after('visibleChange', this.errorVisibilityChanged, this); this._keypress = Y.on('key', this.hide, window, 'down:13,27', this); this.centerDialogue(); }, errorVisibilityChanged: function(e) { if (e.attrName === 'visible' && e.prevVal && !e.newVal) { if (this._keypress) { this._keypress.detach(); } var self = this; setTimeout(function() { self.destroy(); }, 1000); } } }, { NAME: EXCEPTION_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The message of the alert. * * @attribute message * @type String * @default '' */ message: { value: '' }, /** * The name of the alert. * * @attribute title * @type String * @default '' */ name: { value: '' }, /** * The name of the file where the error was thrown. * * @attribute fileName * @type String * @default '' */ fileName: { value: '' }, /** * The line number where the error was thrown. * * @attribute lineNumber * @type String * @default '' */ lineNumber: { value: '' }, /** * The backtrace from the error * * @attribute lineNumber * @type String * @default '' */ stack: { setter: function(str) { var lines = Y.Escape.html(str).split("\n"), pattern = new RegExp('^(.+)@(' + M.cfg.wwwroot + ')?(.{0,75}).*:(\\d+)$'), i; for (i in lines) { lines[i] = lines[i].replace(pattern, "
    ln: $4
    " + "
    $3
    " + "
    $1
    "); } return lines.join("\n"); }, value: '' }, /** * If set, the dialogue is hidden after the specified timeout period. * * @attribute hideTimeoutDelay * @type Number * @default null * @optional */ hideTimeoutDelay: { validator: Y.Lang.isNumber, value: null } } }); M.core.exception = EXCEPTION; }, '@VERSION@', {"requires": ["moodle-core-notification-dialogue"]}); build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-debug.js000064400000004353152265472450024274 0ustar00YUI.add('moodle-core-maintenancemodetimer', function (Y, NAME) { /** * Maintenance mode timer display. * * @module moodle-core-maintenancemodetimer */ var MAINTENANCEMODETIMER = function() { MAINTENANCEMODETIMER.superclass.constructor.apply(this, arguments); }; Y.extend(MAINTENANCEMODETIMER, Y.Base, { timeleftinsec: 0, maintenancenode: Y.one('.box.maintenancewarning'), /** * Initialise timer if maintenancemode set. * * @method initializer * @param config {Array} array with timeleftinsec set. */ initializer: function(config) { if (this.maintenancenode) { this.timeleftinsec = config.timeleftinsec; this.maintenancenode.setAttribute('aria-live', 'polite'); Y.later(1000, this, 'updatetimer', null, true); } }, /** * Decrement time left and update display text. * * @method updatetimer */ updatetimer: function() { this.timeleftinsec -= 1; if (this.timeleftinsec <= 0) { this.maintenancenode.set('text', M.util.get_string('sitemaintenance', 'admin')); } else { var a = {}; a.sec = this.timeleftinsec % 60; a.min = Math.floor(this.timeleftinsec / 60) % 60; a.hour = Math.floor(this.timeleftinsec / 3600); if (a.hour > 0) { this.maintenancenode.set('text', M.util.get_string('maintenancemodeisscheduledlong', 'admin', a)); } else { this.maintenancenode.set('text', M.util.get_string('maintenancemodeisscheduled', 'admin', a)); } } // Set error class to highlight the importance. if (this.timeleftinsec < 30) { this.maintenancenode.addClass('alert-error') .addClass('alert-danger') .removeClass('alert-warning'); } else { this.maintenancenode.addClass('alert-warning') .removeClass('alert-error') .removeClass('alert-danger'); } } }); M.core = M.core || {}; M.core.maintenancemodetimer = M.core.maintenancemodetimer || function(config) { return new MAINTENANCEMODETIMER(config); }; }, '@VERSION@', {"requires": ["base", "node"]}); build/moodle-core-maintenancemodetimer/moodle-core-maintenancemodetimer-min.js000064400000002307152265472450023766 0ustar00YUI.add("moodle-core-maintenancemodetimer",function(t,e){var n=function(){n.superclass.constructor.apply(this,arguments)};t.extend(n,t.Base,{timeleftinsec:0,maintenancenode:t.one(".box.maintenancewarning"),initializer:function(e){this.maintenancenode&&(this.timeleftinsec=e.timeleftinsec,this.maintenancenode.setAttribute("aria-live","polite"),t.later(1e3,this,"updatetimer",null,!0))},updatetimer:function(){var e;--this.timeleftinsec,this.timeleftinsec<=0?this.maintenancenode.set("text",M.util.get_string("sitemaintenance","admin")):((e={}).sec=this.timeleftinsec%60,e.min=Math.floor(this.timeleftinsec/60)%60,e.hour=Math.floor(this.timeleftinsec/3600),0 0) { this.maintenancenode.set('text', M.util.get_string('maintenancemodeisscheduledlong', 'admin', a)); } else { this.maintenancenode.set('text', M.util.get_string('maintenancemodeisscheduled', 'admin', a)); } } // Set error class to highlight the importance. if (this.timeleftinsec < 30) { this.maintenancenode.addClass('alert-error') .addClass('alert-danger') .removeClass('alert-warning'); } else { this.maintenancenode.addClass('alert-warning') .removeClass('alert-error') .removeClass('alert-danger'); } } }); M.core = M.core || {}; M.core.maintenancemodetimer = M.core.maintenancemodetimer || function(config) { return new MAINTENANCEMODETIMER(config); }; }, '@VERSION@', {"requires": ["base", "node"]}); build/moodle-core-widget-focusafterclose/moodle-core-widget-focusafterclose.js000064400000011423152265472450023744 0ustar00YUI.add('moodle-core-widget-focusafterclose', function (Y, NAME) { /** * Provides support for focusing on different nodes after the Widget is * hidden. * * If the focusOnPreviousTargetAfterHide attribute is true, then the module hooks * into the show function for that Widget to try and determine which Node * caused the Widget to be shown. * * Alternatively, the focusAfterHide attribute can be passed a Node. * * @module moodle-core-widget-focusafterhide */ var CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), ' + 'a[href], button, textarea, select, ' + '[tabindex], [contenteditable="true"]'; /** * Provides support for focusing on different nodes after the Widget is * hidden. * * @class M.core.WidgetFocusAfterHide */ function WidgetFocusAfterHide() { Y.after(this._bindUIFocusAfterHide, this, 'bindUI'); if (this.get('rendered')) { this._bindUIFocusAfterHide(); } } WidgetFocusAfterHide.ATTRS = { /** * Whether to focus on the target that caused the Widget to be shown. * * If this is true, and a valid Node is found, any Node specified to focusAfterHide * will be ignored. * * @attribute focusOnPreviousTargetAfterHide * @default false * @type boolean */ focusOnPreviousTargetAfterHide: { value: false }, /** * The Node to focus on after hiding the Widget. * * Note: If focusOnPreviousTargetAfterHide is true, and a valid Node is found, then this * value will be ignored. If it is true and not found, then this value will be used as * a fallback. * * @attribute focusAfterHide * @default null * @type Node */ focusAfterHide: { value: null, type: Y.Node } }; WidgetFocusAfterHide.prototype = { /** * The list of Event Handles which we should cancel when the dialogue is destroyed. * * @property uiHandleFocusAfterHide * @type array * @protected */ _uiHandlesFocusAfterHide: [], /** * A reference to the real show method which is being overwritten. * * @property _showFocusAfterHide * @type function * @default null * @protected */ _showFocusAfterHide: null, /** * A reference to the detected previous target. * * @property _previousTargetFocusAfterHide * @type function * @default null * @protected */ _previousTargetFocusAfterHide: null, initializer: function() { if (this.get('focusOnPreviousTargetAfterHide') && this.show) { // Overwrite the parent method so that we can get the focused // target. this._showFocusAfterHide = this.show; this.show = function(e) { this._showFocusAfterHide.apply(this, arguments); // We use a property rather than overriding the focusAfterHide parameter in // case the target cannot be found at hide time. this._previousTargetFocusAfterHide = null; if (e && e.currentTarget) { this._previousTargetFocusAfterHide = e.currentTarget; } }; } }, destructor: function() { new Y.EventHandle(this.uiHandleFocusAfterHide).detach(); }, /** * Set up the event handling required for this module to work. * * @method _bindUIFocusAfterHide * @private */ _bindUIFocusAfterHide: function() { // Detach the old handles first. new Y.EventHandle(this.uiHandleFocusAfterHide).detach(); this.uiHandleFocusAfterHide = [ this.after('visibleChange', this._afterHostVisibleChangeFocusAfterHide) ]; }, /** * Handle the change in UI visibility. * * This method changes the focus after the hide has taken place. * * @method _afterHostVisibleChangeFocusAfterHide * @private */ _afterHostVisibleChangeFocusAfterHide: function() { if (!this.get('visible')) { if (this._attemptFocus(this._previousTargetFocusAfterHide)) { } else if (this._attemptFocus(this.get('focusAfterHide'))) { // Fall back to the focusAfterHide value if one was specified. } else { } } }, _attemptFocus: function(node) { var focusTarget = Y.one(node); if (focusTarget) { focusTarget = focusTarget.ancestor(CAN_RECEIVE_FOCUS_SELECTOR, true); if (focusTarget) { focusTarget.focus(); return true; } } return false; } }; var NS = Y.namespace('M.core'); NS.WidgetFocusAfterHide = WidgetFocusAfterHide; }, '@VERSION@', {"requires": ["base-build", "widget"]}); build/moodle-core-widget-focusafterclose/moodle-core-widget-focusafterclose-debug.js000064400000012601152265472450025027 0ustar00YUI.add('moodle-core-widget-focusafterclose', function (Y, NAME) { /** * Provides support for focusing on different nodes after the Widget is * hidden. * * If the focusOnPreviousTargetAfterHide attribute is true, then the module hooks * into the show function for that Widget to try and determine which Node * caused the Widget to be shown. * * Alternatively, the focusAfterHide attribute can be passed a Node. * * @module moodle-core-widget-focusafterhide */ var CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), ' + 'a[href], button, textarea, select, ' + '[tabindex], [contenteditable="true"]'; /** * Provides support for focusing on different nodes after the Widget is * hidden. * * @class M.core.WidgetFocusAfterHide */ function WidgetFocusAfterHide() { Y.after(this._bindUIFocusAfterHide, this, 'bindUI'); if (this.get('rendered')) { this._bindUIFocusAfterHide(); } } WidgetFocusAfterHide.ATTRS = { /** * Whether to focus on the target that caused the Widget to be shown. * * If this is true, and a valid Node is found, any Node specified to focusAfterHide * will be ignored. * * @attribute focusOnPreviousTargetAfterHide * @default false * @type boolean */ focusOnPreviousTargetAfterHide: { value: false }, /** * The Node to focus on after hiding the Widget. * * Note: If focusOnPreviousTargetAfterHide is true, and a valid Node is found, then this * value will be ignored. If it is true and not found, then this value will be used as * a fallback. * * @attribute focusAfterHide * @default null * @type Node */ focusAfterHide: { value: null, type: Y.Node } }; WidgetFocusAfterHide.prototype = { /** * The list of Event Handles which we should cancel when the dialogue is destroyed. * * @property uiHandleFocusAfterHide * @type array * @protected */ _uiHandlesFocusAfterHide: [], /** * A reference to the real show method which is being overwritten. * * @property _showFocusAfterHide * @type function * @default null * @protected */ _showFocusAfterHide: null, /** * A reference to the detected previous target. * * @property _previousTargetFocusAfterHide * @type function * @default null * @protected */ _previousTargetFocusAfterHide: null, initializer: function() { if (this.get('focusOnPreviousTargetAfterHide') && this.show) { // Overwrite the parent method so that we can get the focused // target. this._showFocusAfterHide = this.show; this.show = function(e) { this._showFocusAfterHide.apply(this, arguments); // We use a property rather than overriding the focusAfterHide parameter in // case the target cannot be found at hide time. this._previousTargetFocusAfterHide = null; if (e && e.currentTarget) { Y.log("Determined a Node which caused the Widget to be shown", 'debug', 'moodle-core-widget-focusafterhide'); this._previousTargetFocusAfterHide = e.currentTarget; } }; } }, destructor: function() { new Y.EventHandle(this.uiHandleFocusAfterHide).detach(); }, /** * Set up the event handling required for this module to work. * * @method _bindUIFocusAfterHide * @private */ _bindUIFocusAfterHide: function() { // Detach the old handles first. new Y.EventHandle(this.uiHandleFocusAfterHide).detach(); this.uiHandleFocusAfterHide = [ this.after('visibleChange', this._afterHostVisibleChangeFocusAfterHide) ]; }, /** * Handle the change in UI visibility. * * This method changes the focus after the hide has taken place. * * @method _afterHostVisibleChangeFocusAfterHide * @private */ _afterHostVisibleChangeFocusAfterHide: function() { if (!this.get('visible')) { if (this._attemptFocus(this._previousTargetFocusAfterHide)) { Y.log("Focusing on the target automatically determined when the Widget was opened", 'debug', 'moodle-core-widget-focusafterhide'); } else if (this._attemptFocus(this.get('focusAfterHide'))) { // Fall back to the focusAfterHide value if one was specified. Y.log("Focusing on the target provided to focusAfterHide", 'debug', 'moodle-core-widget-focusafterhide'); } else { Y.log("No valid focus target found - not returning focus.", 'debug', 'moodle-core-widget-focusafterhide'); } } }, _attemptFocus: function(node) { var focusTarget = Y.one(node); if (focusTarget) { focusTarget = focusTarget.ancestor(CAN_RECEIVE_FOCUS_SELECTOR, true); if (focusTarget) { focusTarget.focus(); return true; } } return false; } }; var NS = Y.namespace('M.core'); NS.WidgetFocusAfterHide = WidgetFocusAfterHide; }, '@VERSION@', {"requires": ["base-build", "widget"]}); build/moodle-core-widget-focusafterclose/moodle-core-widget-focusafterclose-min.js000064400000002556152265472450024534 0ustar00YUI.add("moodle-core-widget-focusafterclose",function(t,e){function i(){t.after(this._bindUIFocusAfterHide,this,"bindUI"),this.get("rendered")&&this._bindUIFocusAfterHide()}i.ATTRS={focusOnPreviousTargetAfterHide:{value:!1},focusAfterHide:{value:null,type:t.Node}},i.prototype={_uiHandlesFocusAfterHide:[],_showFocusAfterHide:null,_previousTargetFocusAfterHide:null,initializer:function(){this.get("focusOnPreviousTargetAfterHide")&&this.show&&(this._showFocusAfterHide=this.show,this.show=function(e){this._showFocusAfterHide.apply(this,arguments),this._previousTargetFocusAfterHide=null,e&&e.currentTarget&&(this._previousTargetFocusAfterHide=e.currentTarget)})},destructor:function(){new t.EventHandle(this.uiHandleFocusAfterHide).detach()},_bindUIFocusAfterHide:function(){new t.EventHandle(this.uiHandleFocusAfterHide).detach(),this.uiHandleFocusAfterHide=[this.after("visibleChange",this._afterHostVisibleChangeFocusAfterHide)]},_afterHostVisibleChangeFocusAfterHide:function(){this.get("visible")||this._attemptFocus(this._previousTargetFocusAfterHide)||this._attemptFocus(this.get("focusAfterHide"))},_attemptFocus:function(e){e=t.one(e);return!!(e=e&&e.ancestor('input:not([type="hidden"]), a[href], button, textarea, select, [tabindex], [contenteditable="true"]',!0))&&(e.focus(),!0)}},t.namespace("M.core").WidgetFocusAfterHide=i},"@VERSION@",{requires:["base-build","widget"]});build/moodle-core-event/moodle-core-event-min.js000064400000002275152265472450015654 0ustar00YUI.add("moodle-core-event",function(e,o){var n,t,r,c;if(M.core=M.core||{},n=!!M.core.event,M.core.event=M.core.event||{FILTER_CONTENT_UPDATED:"filter-content-updated",EDITOR_CONTENT_RESTORED:"editor-content-restored",FORM_SUBMIT_AJAX:"form-submit-ajax"},M.core.globalEvents=M.core.globalEvents||{FORM_ERROR:"form_error",BLOCK_CONTENT_UPDATED:"block_content_updated"},!n){for(r in t={emitFacade:!0,defaultFn:function(e){},preventedFn:function(e){},stoppedFn:function(e){}},M.core.event)M.core.event.hasOwnProperty(r)&&null===e.getEvent(M.core.event[r])&&e.publish(M.core.event[r],t);for(r in M.core.globalEvents)M.core.globalEvents.hasOwnProperty(r)&&null===e.Global.getEvent(M.core.globalEvents[r])&&e.Global.publish(M.core.globalEvents[r],e.merge(t,{broadcast:!0}));c=function(o,n){new MutationObserver(function(e){e.forEach(function(e){e.addedNodes.forEach(function(e){e instanceof Element&&e.matches(o)&&n(e)})})}).observe(document,{childList:!0,subtree:!0})},require(["core_form/events"],function(n){c("form",function(o){e.one(o).on(M.core.event.FORM_SUBMIT_AJAX,function(e){e&&e.fallbackHandled||n.notifyFormSubmittedByJavascript(o,window.skipValidation,!0)})})})}},"@VERSION@",{requires:["event-custom"]});build/moodle-core-event/moodle-core-event.js000064400000012277152265472450015076 0ustar00YUI.add('moodle-core-event', 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 . /** * @module moodle-core-event */ var LOGNAME = 'moodle-core-event'; /** * List of published global JS events in Moodle. This is a collection * of global events that can be subscribed to, or fired from any plugin. * * @namespace M.core * @class event */ M.core = M.core || {}; var eventsConfigured = !!M.core.event; M.core.event = M.core.event || { /** * This event is triggered when a page has added dynamic nodes to a page * that should be processed by the filter system. An example is loading * user text that could have equations in it. MathJax can typeset the equations * but only if it is notified that there are new nodes in the page that need processing. * To trigger this event use M.core.Event.fire(M.core.Event.FILTER_CONTENT_UPDATED, {nodes: list}); * * @event "filter-content-updated" * @param nodes {Y.NodeList} List of nodes added to the DOM. */ FILTER_CONTENT_UPDATED: "filter-content-updated", /** * This event is triggered when an editor has recovered some draft text. * It can be used to determine let other sections know that they should reset their * form comparison for changes. * * @event "editor-content-restored" */ EDITOR_CONTENT_RESTORED: "editor-content-restored", /** * This event is triggered when an mform is about to be submitted via ajax. * * @event "form-submit-ajax" */ FORM_SUBMIT_AJAX: "form-submit-ajax" }; M.core.globalEvents = M.core.globalEvents || { /** * This event is triggered when form has an error * * @event "form_error" * @param formid {string} Id of form with error. * @param elementid {string} Id of element with error. */ FORM_ERROR: "form_error", /** * This event is triggered when the content of a block has changed * * @event "block_content_updated" * @param instanceid ID of the block instance that was updated */ BLOCK_CONTENT_UPDATED: "block_content_updated" }; if (!eventsConfigured) { var eventDefaultConfig = { emitFacade: true, defaultFn: function(e) { }, preventedFn: function(e) { }, stoppedFn: function(e) { } }; // Publish events with a custom config here. // Publish all the events with a standard config. var key; for (key in M.core.event) { if (M.core.event.hasOwnProperty(key) && Y.getEvent(M.core.event[key]) === null) { Y.publish(M.core.event[key], eventDefaultConfig); } } // Publish global events. for (key in M.core.globalEvents) { // Make sure the key exists and that the event has not yet been published. Otherwise, skip publishing. if (M.core.globalEvents.hasOwnProperty(key) && Y.Global.getEvent(M.core.globalEvents[key]) === null) { Y.Global.publish(M.core.globalEvents[key], Y.merge(eventDefaultConfig, {broadcast: true})); } } /** * Apply a callback when the DOM is modified and matchecs the supplied targetSelector. * * @method listen * @param {String} targetSelector The selector to apply * @param {Function} applyCallback The function to call on the found node */ var listenForMutation = function(targetSelector, applyCallback) { // Add a MutationObserver to check for new children to the tree. var newNodeObserver = new MutationObserver(function(mutationList) { mutationList.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node instanceof Element && node.matches(targetSelector)) { applyCallback(node); } }); }); }); newNodeObserver.observe(document, {childList: true, subtree: true}); }; // YUI Custom Events do not bubble up the DOM, only up the inheritance path of custom classes. // We have to add a Node Observer to watch for new forms that may need to be watched in future. require(['core_form/events'], function(formEvents) { listenForMutation('form', function(form) { Y.one(form).on(M.core.event.FORM_SUBMIT_AJAX, function(e) { // Prevent cyclical calls. if (e && e.fallbackHandled) { return; } formEvents.notifyFormSubmittedByJavascript(form, window.skipValidation, true); }); }); }); } }, '@VERSION@', {"requires": ["event-custom"]}); build/moodle-core-event/moodle-core-event-debug.js000064400000012711152265472450016153 0ustar00YUI.add('moodle-core-event', 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 . /** * @module moodle-core-event */ var LOGNAME = 'moodle-core-event'; /** * List of published global JS events in Moodle. This is a collection * of global events that can be subscribed to, or fired from any plugin. * * @namespace M.core * @class event */ M.core = M.core || {}; var eventsConfigured = !!M.core.event; M.core.event = M.core.event || { /** * This event is triggered when a page has added dynamic nodes to a page * that should be processed by the filter system. An example is loading * user text that could have equations in it. MathJax can typeset the equations * but only if it is notified that there are new nodes in the page that need processing. * To trigger this event use M.core.Event.fire(M.core.Event.FILTER_CONTENT_UPDATED, {nodes: list}); * * @event "filter-content-updated" * @param nodes {Y.NodeList} List of nodes added to the DOM. */ FILTER_CONTENT_UPDATED: "filter-content-updated", /** * This event is triggered when an editor has recovered some draft text. * It can be used to determine let other sections know that they should reset their * form comparison for changes. * * @event "editor-content-restored" */ EDITOR_CONTENT_RESTORED: "editor-content-restored", /** * This event is triggered when an mform is about to be submitted via ajax. * * @event "form-submit-ajax" */ FORM_SUBMIT_AJAX: "form-submit-ajax" }; M.core.globalEvents = M.core.globalEvents || { /** * This event is triggered when form has an error * * @event "form_error" * @param formid {string} Id of form with error. * @param elementid {string} Id of element with error. */ FORM_ERROR: "form_error", /** * This event is triggered when the content of a block has changed * * @event "block_content_updated" * @param instanceid ID of the block instance that was updated */ BLOCK_CONTENT_UPDATED: "block_content_updated" }; if (!eventsConfigured) { var eventDefaultConfig = { emitFacade: true, defaultFn: function(e) { Y.log('Event fired: ' + e.type, 'debug', LOGNAME); }, preventedFn: function(e) { Y.log('Event prevented: ' + e.type, 'debug', LOGNAME); }, stoppedFn: function(e) { Y.log('Event stopped: ' + e.type, 'debug', LOGNAME); } }; // Publish events with a custom config here. // Publish all the events with a standard config. var key; for (key in M.core.event) { if (M.core.event.hasOwnProperty(key) && Y.getEvent(M.core.event[key]) === null) { Y.publish(M.core.event[key], eventDefaultConfig); } } // Publish global events. for (key in M.core.globalEvents) { // Make sure the key exists and that the event has not yet been published. Otherwise, skip publishing. if (M.core.globalEvents.hasOwnProperty(key) && Y.Global.getEvent(M.core.globalEvents[key]) === null) { Y.Global.publish(M.core.globalEvents[key], Y.merge(eventDefaultConfig, {broadcast: true})); Y.log('Global event published: ' + key, 'debug', LOGNAME); } } /** * Apply a callback when the DOM is modified and matchecs the supplied targetSelector. * * @method listen * @param {String} targetSelector The selector to apply * @param {Function} applyCallback The function to call on the found node */ var listenForMutation = function(targetSelector, applyCallback) { // Add a MutationObserver to check for new children to the tree. var newNodeObserver = new MutationObserver(function(mutationList) { mutationList.forEach(function(mutation) { mutation.addedNodes.forEach(function(node) { if (node instanceof Element && node.matches(targetSelector)) { applyCallback(node); } }); }); }); newNodeObserver.observe(document, {childList: true, subtree: true}); }; // YUI Custom Events do not bubble up the DOM, only up the inheritance path of custom classes. // We have to add a Node Observer to watch for new forms that may need to be watched in future. require(['core_form/events'], function(formEvents) { listenForMutation('form', function(form) { Y.one(form).on(M.core.event.FORM_SUBMIT_AJAX, function(e) { // Prevent cyclical calls. if (e && e.fallbackHandled) { return; } formEvents.notifyFormSubmittedByJavascript(form, window.skipValidation, true); }); }); }); } }, '@VERSION@', {"requires": ["event-custom"]}); build/moodle-core-lockscroll/moodle-core-lockscroll-debug.js000064400000014127152265472450020232 0ustar00YUI.add('moodle-core-lockscroll', function (Y, NAME) { /** * Provides the ability to lock the scroll for a page, allowing nested * locking. * * @module moodle-core-lockscroll */ /** * Provides the ability to lock the scroll for a page. * * This is achieved by applying the class 'lockscroll' to the body Node. * * Nested widgets are also supported and the scroll lock is only removed * when the final plugin instance is disabled. * * @class M.core.LockScroll * @extends Plugin.Base */ Y.namespace('M.core').LockScroll = Y.Base.create('lockScroll', Y.Plugin.Base, [], { /** * Whether the LockScroll has been activated. * * @property _enabled * @type Boolean * @protected */ _enabled: false, /** * Handle destruction of the lockScroll instance, including disabling * of the current instance. * * @method destructor */ destructor: function() { this.disableScrollLock(); }, /** * Start locking the page scroll. * * This is achieved by applying the lockscroll class to the body Node. * * A count of the total number of active, and enabled, lockscroll instances is also kept on * the body to ensure that premature disabling does not occur. * * @method enableScrollLock * @param {Boolean} forceOnSmallWindow Whether to enable the scroll lock, even for small window sizes. * @chainable */ enableScrollLock: function(forceOnSmallWindow) { if (this.isActive()) { Y.log('LockScroll already active. Ignoring enable request', 'warn', 'moodle-core-lockscroll'); return; } if (!this.shouldLockScroll(forceOnSmallWindow)) { Y.log('Dialogue height greater than window height. Ignoring enable request.', 'warn', 'moodle-core-lockscroll'); return; } Y.log('Enabling LockScroll.', 'debug', 'moodle-core-lockscroll'); this._enabled = true; var body = Y.one(Y.config.doc.body); // Get width of body before turning on lockscroll. var widthBefore = body.getComputedStyle('width'); // We use a CSS class on the body to handle the actual locking. body.addClass('lockscroll'); // Increase the count of active instances - this is used to ensure that we do not // remove the locking when parent windows are still open. // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them. var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 0, newCount = currentCount + 1; body.setAttribute('data-activeScrollLocks', newCount); Y.log("Setting the activeScrollLocks count from " + currentCount + " to " + newCount, 'debug', 'moodle-core-lockscroll'); // When initially enabled, set the body max-width to its current width. This // avoids centered elements jumping because the width changes when scrollbars // disappear. if (currentCount === 0) { body.setStyle('maxWidth', widthBefore); } return this; }, /** * Recalculate whether lock scrolling should be on or off. * * @method shouldLockScroll * @param {Boolean} forceOnSmallWindow Whether to enable the scroll lock, even for small window sizes. * @return boolean */ shouldLockScroll: function(forceOnSmallWindow) { var dialogueHeight = this.get('host').get('boundingBox').get('region').height, // Most modern browsers use win.innerHeight, but some older versions of IE use documentElement.clientHeight. // We fall back to 0 if neither can be found which has the effect of disabling scroll locking. windowHeight = Y.config.win.innerHeight || Y.config.doc.documentElement.clientHeight || 0; if (!forceOnSmallWindow && dialogueHeight > (windowHeight - 10)) { return false; } else { return true; } }, /** * Recalculate whether lock scrolling should be on or off because the size of the dialogue changed. * * @method updateScrollLock * @param {Boolean} forceOnSmallWindow Whether to enable the scroll lock, even for small window sizes. * @chainable */ updateScrollLock: function(forceOnSmallWindow) { // Both these functions already check if scroll lock is active and do the right thing. if (this.shouldLockScroll(forceOnSmallWindow)) { this.enableScrollLock(forceOnSmallWindow); } else { this.disableScrollLock(true); } return this; }, /** * Stop locking the page scroll. * * The instance may be disabled but the scroll lock not removed if other instances of the * plugin are also active. * * @method disableScrollLock * @chainable */ disableScrollLock: function(force) { if (this.isActive()) { Y.log('Disabling LockScroll.', 'debug', 'moodle-core-lockscroll'); this._enabled = false; var body = Y.one(Y.config.doc.body); // Decrease the count of active instances. // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them. var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 1, newCount = currentCount - 1; if (force || currentCount === 1) { body.removeClass('lockscroll'); body.setStyle('maxWidth', null); } body.setAttribute('data-activeScrollLocks', currentCount - 1); Y.log("Setting the activeScrollLocks count from " + currentCount + " to " + newCount, 'debug', 'moodle-core-lockscroll'); } return this; }, /** * Return whether scroll locking is active. * * @method isActive * @return Boolean */ isActive: function() { return this._enabled; } }, { NS: 'lockScroll', ATTRS: { } }); }, '@VERSION@', {"requires": ["plugin", "base-build"]}); build/moodle-core-lockscroll/moodle-core-lockscroll-min.js000064400000002314152265472450017722 0ustar00YUI.add("moodle-core-lockscroll",function(l,t){l.namespace("M.core").LockScroll=l.Base.create("lockScroll",l.Plugin.Base,[],{_enabled:!1,destructor:function(){this.disableScrollLock()},enableScrollLock:function(t){var e,o;if(!this.isActive()&&this.shouldLockScroll(t))return this._enabled=!0,e=(t=l.one(l.config.doc.body)).getComputedStyle("width"),t.addClass("lockscroll"),o=parseInt(t.getAttribute("data-activeScrollLocks"),10)||0,t.setAttribute("data-activeScrollLocks",o+1),0===o&&t.setStyle("maxWidth",e),this},shouldLockScroll:function(t){var e=this.get("host").get("boundingBox").get("region").height,o=l.config.win.innerHeight||l.config.doc.documentElement.clientHeight||0;return!(!t&&o-10 (windowHeight - 10)) { return false; } else { return true; } }, /** * Recalculate whether lock scrolling should be on or off because the size of the dialogue changed. * * @method updateScrollLock * @param {Boolean} forceOnSmallWindow Whether to enable the scroll lock, even for small window sizes. * @chainable */ updateScrollLock: function(forceOnSmallWindow) { // Both these functions already check if scroll lock is active and do the right thing. if (this.shouldLockScroll(forceOnSmallWindow)) { this.enableScrollLock(forceOnSmallWindow); } else { this.disableScrollLock(true); } return this; }, /** * Stop locking the page scroll. * * The instance may be disabled but the scroll lock not removed if other instances of the * plugin are also active. * * @method disableScrollLock * @chainable */ disableScrollLock: function(force) { if (this.isActive()) { this._enabled = false; var body = Y.one(Y.config.doc.body); // Decrease the count of active instances. // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them. var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 1, newCount = currentCount - 1; if (force || currentCount === 1) { body.removeClass('lockscroll'); body.setStyle('maxWidth', null); } body.setAttribute('data-activeScrollLocks', currentCount - 1); } return this; }, /** * Return whether scroll locking is active. * * @method isActive * @return Boolean */ isActive: function() { return this._enabled; } }, { NS: 'lockScroll', ATTRS: { } }); }, '@VERSION@', {"requires": ["plugin", "base-build"]}); build/moodle-core-notification/moodle-core-notification-min.js000064400000000403152265472450020555 0ustar00YUI.add("moodle-core-notification",function(o,i){},"@VERSION@",{requires:["moodle-core-notification-dialogue","moodle-core-notification-alert","moodle-core-notification-confirm","moodle-core-notification-exception","moodle-core-notification-ajaxexception"]});build/moodle-core-notification/moodle-core-notification.js000064400000001705152265472450020002 0ustar00YUI.add('moodle-core-notification', function (Y, NAME) { /** * The notification module provides a standard set of dialogues for use * within Moodle. * * @module moodle-core-notification * @main */ /** * To avoid bringing moodle-core-notification into modules in it's * entirety, we now recommend using on of the subclasses of * moodle-core-notification. These include: *
    *
    moodle-core-notification-dialogue
    *
    moodle-core-notification-alert
    *
    moodle-core-notification-confirm
    *
    moodle-core-notification-exception
    *
    moodle-core-notification-ajaxexception
    *
    * * @class M.core.notification * @deprecated */ }, '@VERSION@', { "requires": [ "moodle-core-notification-dialogue", "moodle-core-notification-alert", "moodle-core-notification-confirm", "moodle-core-notification-exception", "moodle-core-notification-ajaxexception" ] }); build/moodle-core-notification/moodle-core-notification-debug.js000064400000002152152265472450021063 0ustar00YUI.add('moodle-core-notification', function (Y, NAME) { /** * The notification module provides a standard set of dialogues for use * within Moodle. * * @module moodle-core-notification * @main */ /** * To avoid bringing moodle-core-notification into modules in it's * entirety, we now recommend using on of the subclasses of * moodle-core-notification. These include: *
    *
    moodle-core-notification-dialogue
    *
    moodle-core-notification-alert
    *
    moodle-core-notification-confirm
    *
    moodle-core-notification-exception
    *
    moodle-core-notification-ajaxexception
    *
    * * @class M.core.notification * @deprecated */ Y.log("The moodle-core-notification parent module has been deprecated. " + "Please use one of its subclasses instead.", 'moodle-core-notification', 'warn'); }, '@VERSION@', { "requires": [ "moodle-core-notification-dialogue", "moodle-core-notification-alert", "moodle-core-notification-confirm", "moodle-core-notification-exception", "moodle-core-notification-ajaxexception" ] }); build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js000064400000077356152265472450024463 0ustar00YUI.add('moodle-core-notification-dialogue', function (Y, NAME) { /* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; /** * The generic dialogue class for use in Moodle. * * @module moodle-core-notification * @submodule moodle-core-notification-dialogue */ var DIALOGUE_NAME = 'Moodle dialogue', DIALOGUE, DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen', DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden', DIALOGUE_SELECTOR = ' [role=dialog]', MENUBAR_SELECTOR = '[role=menubar]', DOT = '.', HAS_ZINDEX = 'moodle-has-zindex', CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]):not([disabled]):not([tabindex^="-"]),' + 'a[href]:not([disabled]):not([tabindex^="-"]),' + 'button:not([disabled]):not([tabindex^="-"]),' + 'textarea:not([disabled]):not([tabindex^="-"]),' + 'select:not([disabled]):not([tabindex^="-"]),' + '[tabindex]:not([disabled]):not([tabindex^="-"])', FORM_SELECTOR = 'form'; /** * A re-usable dialogue box with Moodle classes applied. * * @param {Object} c Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.dialogue * @extends Panel */ DIALOGUE = function(config) { // The code below is a hack to add the custom content node to the DOM, on the fly, per-instantiation and to assign the value // of 'srcNode' to this newly created node. Normally (see docs: https://yuilibrary.com/yui/docs/widget/widget-extend.html), // this node would be pre-existing in the DOM, and an id string would simply be passed in as a property of the config object // during widget instantiation, however, because we're creating it on the fly (and 'config.srcNode' isn't set yet), care must // be taken to add it to the DOM and to properly set the value of 'config.srcNode' before calling the parent constructor. // Note: additional classes can be added to this content node by setting the 'additionalBaseClass' config property (a string). var id = 'moodle-dialogue-' + Y.stamp(this) + '-wrap'; // Can't use this.get('id') as it's not set at this stage. config.notificationBase = Y.Node.create('
    ') .append(Y.Node.create( '' ) .append(Y.Node.create('
    ')) .append(Y.Node.create('
    ')) .append(Y.Node.create('
    '))); config.attachmentPoint = config.attachmentPoint || document.body; Y.one(config.attachmentPoint).append(config.notificationBase); config.srcNode = '#' + id; delete config.buttons; // Don't let anyone pass in buttons as we want to control these during init. addButton can be used later. DIALOGUE.superclass.constructor.apply(this, [config]); }; Y.extend(DIALOGUE, Y.Panel, { // Window resize event listener. _resizeevent: null, // Orientation change event listener. _orientationevent: null, _calculatedzindex: false, // Current maskNode id _currentMaskNodeId: null, /** * The original position of the dialogue before it was reposition to * avoid browser jumping. * * @property _originalPosition * @protected * @type Array */ _originalPosition: null, /** * The list of elements that have been aria hidden when displaying * this dialogue. * * @property _hiddenSiblings * @protected * @type Array */ _hiddenSiblings: null, /** * Hide the modal only if it doesn't contain a form. * * @method hideIfNotForm */ hideIfNotForm: function() { var bb = this.get('boundingBox'), formElement = bb.one(FORM_SELECTOR); if (formElement === null) { this.hide(); } }, /** * Initialise the dialogue. * * @method initializer */ initializer: function() { var bb; if (this.get('closeButton') !== false) { var title = this.get('closeButtonTitle'); // The buttons constructor does not allow custom attributes. this.get('buttons').header[0].setAttribute('title', title); this.get('buttons').header[0].setAttribute('aria-label', title); } this.setStdModContent(Y.WidgetStdMod.HEADER, '
    ' + this.get('headerContent') + '
    ', Y.WidgetStdMod.REPLACE); // Initialise the element cache. this._hiddenSiblings = []; if (this.get('render')) { this.render(); } this.after('visibleChange', this.visibilityChanged, this); if (this.get('center')) { this.centerDialogue(); } if (this.get('modal')) { // If we're a modal then make sure our container is ARIA // hidden by default. ARIA visibility is managed for modal dialogues. this.get(BASE).set('aria-hidden', 'true'); this.plug(Y.M.core.LockScroll); } // Remove the `focusoutside` listener. // It conflicts with the ARIA focuslock manager which supports both YUI and non-YUI dialogues. this.set('focusOn', Y.Array(this.get('focusOn')).filter(function(node) { return node.eventName !== 'focusoutside'; })); Y.one('document').on('orientationchange', function() { // This will detect a change in orientation and re-trigger centering. this.centerDialogOnVisible(); }, this); Y.one('window').on('resize', function() { // Detect window resize (most browsers). this.centerDialogOnVisible(); }, this); // Observe dialog on size change. this.centerDialogOnDialogSizeChange(this); // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507 // and allow setting of z-index in theme. bb = this.get('boundingBox'); bb.addClass(HAS_ZINDEX); // Add any additional classes that were specified. Y.Array.each(this.get('extraClasses'), bb.addClass, bb); if (this.get('visible')) { this.applyZIndex(); this.applyAndTrapFocus(); // Only do accessibility hiding for modals because the ARIA spec // says that all ARIA dialogues should be modal. if (this.get('modal')) { // Make this dialogue visible to screen readers. this.setAccessibilityVisible(); } } // Recalculate the zIndex every time the modal is altered. this.on('maskShow', this.applyZIndex); this.on('maskShow', function() { // When the mask shows, position the boundingBox at the top-left of the window such that when it is // focused, the position does not change. var w = Y.one(Y.config.win), bb = this.get('boundingBox'); if (!this.get('center')) { this._originalPosition = bb.getXY(); } // Check if maskNode already init click event. var maskNode = this.get('maskNode'); if (this._currentMaskNodeId !== maskNode.get('_yuid')) { this._currentMaskNodeId = maskNode.get('_yuid'); maskNode.on('click', this.hideIfNotForm, this); } if (bb.getStyle('position') !== 'fixed') { // If the boundingBox has been positioned in a fixed manner, then it will not position correctly to scrollTop. bb.setStyles({ top: w.get('scrollTop'), left: w.get('scrollLeft') }); } }, this); // Add any additional classes to the content node if required. var nBase = this.get('notificationBase'); var additionalClasses = this.get('additionalBaseClass'); if (additionalClasses !== '') { nBase.addClass(additionalClasses); } // Remove the dialogue from the DOM when it is destroyed. this.after('destroyedChange', function() { this.get(BASE).remove(true); }, this); }, /** * Either set the zindex to the supplied value, or set it to one more than the highest existing * dialog in the page. * * @method applyZIndex */ applyZIndex: function() { var highestzindex = 1040, zindexvalue = 1, bb = this.get('boundingBox'), ol = this.get('maskNode'), zindex = this.get('zIndex'); if (zindex !== 0 && !this._calculatedzindex) { // The zindex was specified so we should use that. bb.setStyle('zIndex', zindex); } else { // Determine the correct zindex by looking at all existing dialogs and menubars in the page. Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) { var zindex = this.findZIndex(node); if (zindex > highestzindex) { highestzindex = zindex; } }, this); // Only set the zindex if we found a wrapper. zindexvalue = (highestzindex + 1).toString(); bb.setStyle('zIndex', zindexvalue); this.set('zIndex', zindexvalue); if (this.get('modal')) { ol.setStyle('zIndex', zindexvalue); // In IE8, the z-indexes do not take effect properly unless you toggle // the lightbox from 'fixed' to 'static' and back. This code does so // using the minimum setTimeouts that still actually work. if (Y.UA.ie && Y.UA.compareVersions(Y.UA.ie, 9) < 0) { setTimeout(function() { ol.setStyle('position', 'static'); setTimeout(function() { ol.setStyle('position', 'fixed'); }, 0); }, 0); } } this._calculatedzindex = true; } }, /** * Finds the zIndex of the given node or its parent. * * @method findZIndex * @param {Node} node The Node to apply the zIndex to. * @return {Number} Either the zIndex, or 0 if one was not found. */ findZIndex: function(node) { // In most cases the zindex is set on the parent of the dialog. var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex'); if (zindex) { return parseInt(zindex, 10); } return 0; }, /** * Event listener for the visibility changed event. * * @method visibilityChanged * @param {EventFacade} e */ visibilityChanged: function(e) { var titlebar, bb; if (e.attrName === 'visible') { this.get('maskNode').addClass(CSS_CLASSES.LIGHTBOX); // Going from visible to hidden. if (e.prevVal && !e.newVal) { bb = this.get('boundingBox'); if (this._resizeevent) { this._resizeevent.detach(); this._resizeevent = null; } if (this._orientationevent) { this._orientationevent.detach(); this._orientationevent = null; } require(['core/local/aria/focuslock'], function(FocusLockManager) { // Untrap focus when the dialogue is hidden. FocusLockManager.untrapFocus(); }); if (this.get('modal')) { // Hide this dialogue from screen readers. this.setAccessibilityHidden(); } } // Going from hidden to visible. if (!e.prevVal && e.newVal) { // This needs to be done each time the dialog is shown as new dialogs may have been opened. this.applyZIndex(); // This needs to be done each time the dialog is shown as the window may have been resized. this.makeResponsive(); if (!this.shouldResizeFullscreen()) { if (this.get('draggable')) { titlebar = '#' + this.get('id') + ' .' + CSS_CLASSES.HEADER; this.plug(Y.Plugin.Drag, {handles: [titlebar]}); Y.one(titlebar).setStyle('cursor', 'move'); } } // Only do accessibility hiding for modals because the ARIA spec // says that all ARIA dialogues should be modal. if (this.get('modal')) { // Make this dialogue visible to screen readers. this.setAccessibilityVisible(); } } if (this.get('center') && !e.prevVal && e.newVal) { this.centerDialogue(); } } }, /** * If the responsive attribute is set on the dialog, and the window size is * smaller than the responsive width - make the dialog fullscreen. * * @method makeResponsive */ makeResponsive: function() { var bb = this.get('boundingBox'); if (this.shouldResizeFullscreen()) { // Make this dialogue fullscreen on a small screen. // Disable the page scrollbars. // Size and position the fullscreen dialog. bb.addClass(DIALOGUE_FULLSCREEN_CLASS); bb.setStyles({'left': null, 'top': null, 'width': null, 'height': null, 'right': null, 'bottom': null}); } else { if (this.get('responsive')) { // We must reset any of the fullscreen changes. bb.removeClass(DIALOGUE_FULLSCREEN_CLASS) .setStyles({'width': this.get('width'), 'height': this.get('height')}); } } // Update Lock scroll if the plugin is present. if (this.lockScroll) { this.lockScroll.updateScrollLock(this.shouldResizeFullscreen()); } }, /** * Center the dialog on the screen. * * @method centerDialogue */ centerDialogue: function() { var bb = this.get('boundingBox'), hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS), x, y; // Don't adjust the position if we are in full screen mode. if (this.shouldResizeFullscreen()) { return; } if (hidden) { bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS); } x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth')) / 2), 15); y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight')) / 2), 15) + Y.one(window).get('scrollTop'); bb.setStyles({'left': x, 'top': y}); if (hidden) { bb.addClass(DIALOGUE_HIDDEN_CLASS); } this.makeResponsive(); }, /** * Automatic re-center dialog when dialog size is changed. * * @method centerDialogOnDialogSizeChange * @param {M.core.dialogue} dialog object to apply centering. */ centerDialogOnDialogSizeChange: function(dialog) { // ResizeObserver doesn't get recognized in JSHint. // So we need to suppress the false warning. var observer = new ResizeObserver(function() { // jshint ignore:line dialog.centerDialogOnVisible(); }); var bb = dialog.get('boundingBox'); observer.observe(bb._node, {attributes: true, attributeFilter: ['class']}); }, /** * Centering dialog when dialog is visible. * * @method centerDialogOnVisible */ centerDialogOnVisible: function() { if (!this.get('visible')) { return; // Only centre visible dialogue. } if (this.name !== DIALOGUE_NAME) { return; // Only centre Moodle dialogues. } if (this.shouldResizeFullscreen()) { this.makeResponsive(); } this.centerDialogue(); }, /** * Return whether this dialogue should be fullscreen or not. * * Responsive attribute must be true and we should not be in an iframe and the screen width should * be less than the responsive width. * * @method shouldResizeFullscreen * @return {Boolean} */ shouldResizeFullscreen: function() { return (window === window.parent) && this.get('responsive') && Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth'); }, _focus: function() { this.focus(); }, show: function() { var result = DIALOGUE.superclass.show.call(this); if (!this.get('center') && this._originalPosition) { // Restore the dialogue position to it's location before it was moved at show time. this.get('boundingBox').setXY(this._originalPosition); } this.applyAndTrapFocus(); return result; }, hide: function(e) { if (e) { // If the event was closed by an escape key event, then we need to check that this // dialogue is currently focused to prevent closing all dialogues in the stack. if (e.type === 'key' && e.keyCode === 27 && !this.get('focused')) { return; } } // Unlock scroll if the plugin is present. if (this.lockScroll) { this.lockScroll.disableScrollLock(); } return DIALOGUE.superclass.hide.call(this, arguments); }, /** * Setup key delegation to keep tabbing within the open dialogue. * * @method keyDelegation */ keyDelegation: function() { Y.log('The keyDelegation function has been deprecated in favour of the AMD core/local/aria/focuslock module'); var bb = this.get('boundingBox'); bb.delegate('key', function(e) { var target = e.target; var direction = 'forward'; if (e.shiftKey) { direction = 'backward'; } if (this.trapFocus(target, direction)) { e.preventDefault(); } }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this); }, /** * Trap the tab focus within the open modal. * * @method trapFocus * @param {string} target the element target * @param {string} direction tab key for forward and tab+shift for backward * @return {Boolean} The result of the focus action. */ trapFocus: function(target, direction) { var bb = this.get('boundingBox'), firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR), lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop(); if (target === lastitem && direction === 'forward') { // Tab key. return firstitem.focus(); } else if (target === firstitem && direction === 'backward') { // Tab+shift key. return lastitem.focus(); } }, /** * Sets the appropriate aria attributes on this dialogue and the other * elements in the DOM to ensure that screen readers are able to navigate * the dialogue popup correctly. * * @method setAccessibilityVisible */ setAccessibilityVisible: function() { // Get the element that contains this dialogue because we need it // to filter out from the document.body child elements. var container = this.get(BASE); // We need to get a list containing each sibling element and the shallowest // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging // the fact that this dialogue is always appended to the document body therefore // it's siblings are the shallowest non-ancestral nodes. If that changes then // this code should also be updated. Y.one(document.body).get('children').each(function(node) { // Skip the element that contains us. if (node !== container) { var hidden = node.get('aria-hidden'); // If they are already hidden we can ignore them. if (hidden !== 'true') { // Save their current state. node.setData('previous-aria-hidden', hidden); this._hiddenSiblings.push(node); // Hide this node from screen readers. node.set('aria-hidden', 'true'); } } }, this); // Make us visible to screen readers. container.set('aria-hidden', 'false'); }, /** * Restores the aria visibility on the DOM elements changed when displaying * the dialogue popup and makes the dialogue aria hidden to allow screen * readers to navigate the main page correctly when the dialogue is closed. * * @method setAccessibilityHidden */ setAccessibilityHidden: function() { var container = this.get(BASE); container.set('aria-hidden', 'true'); // Restore the sibling nodes back to their original values. Y.Array.each(this._hiddenSiblings, function(node) { var previousValue = node.getData('previous-aria-hidden'); // If the element didn't previously have an aria-hidden attribute // then we can just remove the one we set. if (previousValue === null) { node.removeAttribute('aria-hidden'); } else { // Otherwise set it back to the old value (which will be false). node.set('aria-hidden', previousValue); } }); // Clear the cache. No longer need to store these. this._hiddenSiblings = []; }, /** * Focuses on the node specified by focusOnShowSelector, or the first focusable node if nothing is specified. * It also traps the focus to the current bounding box. * * @method applyAndTrapFocus */ applyAndTrapFocus: function() { var content = this.bodyNode; var focusSelector = this.get('focusOnShowSelector'); var focusNode = null; // Try and find a node to focus on using the focusOnShowSelector attribute. if (focusSelector !== null) { focusNode = this.get('boundingBox').one(focusSelector); } if (!focusNode) { // Fall back to the first focusable element in the body of the dialogue if no focus node was found yet. if (content && content !== '') { focusNode = content.one(CAN_RECEIVE_FOCUS_SELECTOR); } } require(['core/local/aria/focuslock'], function(FocusLockManager) { // Trap focus to the current bounding box. FocusLockManager.trapFocus(this.get('boundingBox').getDOMNode()); if (focusNode) { focusNode.focus(); } }.bind(this)); }, }, { NAME: DIALOGUE_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * Any additional classes to add to the base Node. * * @attribute additionalBaseClass * @type String * @default '' */ additionalBaseClass: { value: '' }, /** * The Notification base Node. * * @attribute notificationBase * @type Node */ notificationBase: { }, /** * Whether to display the dialogue modally and with a * lightbox style. * * @attribute lightbox * @type Boolean * @default true * @deprecated Since Moodle 2.7. Please use modal instead. */ lightbox: { lazyAdd: false, setter: function(value) { Y.log("The lightbox attribute of M.core.dialogue has been deprecated since Moodle 2.7, " + "please use the modal attribute instead", 'warn', 'moodle-core-notification-dialogue'); this.set('modal', value); } }, /** * Whether to display a close button on the dialogue. * * Note, we do not recommend hiding the close button as this has * potential accessibility concerns. * * @attribute closeButton * @type Boolean * @default true */ closeButton: { validator: Y.Lang.isBoolean, value: true }, /** * The title for the close button if one is to be shown. * * @attribute closeButtonTitle * @type String * @default 'Close' */ closeButtonTitle: { validator: Y.Lang.isString, value: M.util.get_string('closebuttontitle', 'moodle') }, /** * Whether to display the dialogue centrally on the screen. * * @attribute center * @type Boolean * @default true */ center: { validator: Y.Lang.isBoolean, value: true }, /** * Whether to make the dialogue movable around the page. * * @attribute draggable * @type Boolean * @default false */ draggable: { validator: Y.Lang.isBoolean, value: false }, /** * Used to generate a unique id for the dialogue. * * @attribute COUNT * @type String * @default null * @writeonce */ COUNT: { writeOnce: true, valueFn: function() { return Y.stamp(this); } }, /** * Used to disable the fullscreen resizing behaviour if required. * * @attribute responsive * @type Boolean * @default true */ responsive: { validator: Y.Lang.isBoolean, value: true }, /** * The width that this dialogue should be resized to fullscreen. * * @attribute responsiveWidth * @type Number * @default 768 */ responsiveWidth: { value: 768 }, /** * Selector to a node that should recieve focus when this dialogue is shown. * * The default behaviour is to focus on the header. * * @attribute focusOnShowSelector * @default null * @type String */ focusOnShowSelector: { value: null } } }); Y.Base.modifyAttrs(DIALOGUE, { /** * String with units, or number, representing the width of the Widget. * If a number is provided, the default unit, defined by the Widgets * DEF_UNIT, property is used. * * If a value of 'auto' is used, then an empty String is instead * returned. * * @attribute width * @default '400px' * @type {String|Number} */ width: { value: '400px', setter: function(value) { if (value === 'auto') { return ''; } return value; } }, /** * Boolean indicating whether or not the Widget is visible. * * We override this from the default Widget attribute value. * * @attribute visible * @default false * @type Boolean */ visible: { value: false }, /** * A convenience Attribute, which can be used as a shortcut for the * `align` Attribute. * * Note: We override this in Moodle such that it sets a value for the * `center` attribute if set. The `centered` will always return false. * * @attribute centered * @type Boolean|Node * @default false */ centered: { setter: function(value) { if (value) { this.set('center', true); } return false; } }, /** * Boolean determining whether to render the widget during initialisation. * * We override this to change the default from false to true for the dialogue. * We then proceed to early render the dialogue during our initialisation rather than waiting * for YUI to render it after that. * * @attribute render * @type Boolean * @default true */ render: { value: true, writeOnce: true }, /** * Any additional classes to add to the boundingBox. * * @attribute extraClasses * @type Array * @default [] */ extraClasses: { value: [] }, /** * Identifier for the widget. * * @attribute id * @type String * @default a product of guid(). * @writeOnce */ id: { writeOnce: true, valueFn: function() { var id = 'moodle-dialogue-' + Y.stamp(this); return id; } }, /** * Collection containing the widget's buttons. * * @attribute buttons * @type Object * @default {} */ buttons: { getter: Y.WidgetButtons.prototype._getButtons, setter: Y.WidgetButtons.prototype._setButtons, valueFn: function() { if (this.get('closeButton') === false) { return null; } else { return [ { section: Y.WidgetStdMod.HEADER, classNames: 'closebutton', action: function() { this.hide(); } } ]; } } } }); Y.Base.mix(DIALOGUE, [Y.M.core.WidgetFocusAfterHide]); M.core.dialogue = DIALOGUE; /** * A dialogue type designed to display informative messages to users. * * @module moodle-core-notification */ /** * Extends core Dialogue to provide a type of dialogue which can be used * for informative message which are modal, and centered. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.notification.info * @extends M.core.dialogue */ var INFO = function() { INFO.superclass.constructor.apply(this, arguments); }; Y.extend(INFO, M.core.dialogue, { initializer: function() { this.show(); } }, { NAME: 'Moodle information dialogue', CSS_PREFIX: DIALOGUE_PREFIX }); Y.Base.modifyAttrs(INFO, { /** * Whether the widget should be modal or not. * * We override this to change the default from false to true for a subset of dialogues. * * @attribute modal * @type Boolean * @default true */ modal: { validator: Y.Lang.isBoolean, value: true } }); M.core.notification = M.core.notification || {}; M.core.notification.info = INFO; }, '@VERSION@', { "requires": [ "base", "node", "panel", "escape", "event-key", "dd-plugin", "moodle-core-widget-focusafterclose", "moodle-core-lockscroll" ] }); build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js000064400000022041152265472450024135 0ustar00YUI.add("moodle-core-notification-dialogue",function(s,h){var u,i,t,n,a,l,e,o="moodle-dialogue",d="notificationBase",r={BASE:"moodle-dialogue-base",WRAP:"moodle-dialogue-wrap",HEADER:"moodle-dialogue-hd",BODY:"moodle-dialogue-bd",CONTENT:"moodle-dialogue-content",FOOTER:"moodle-dialogue-ft",HIDDEN:"hidden",LIGHTBOX:"moodle-dialogue-lightbox"};M.core=M.core||{},t=o+"-fullscreen",n=o+"-hidden",a="moodle-has-zindex",s.extend(i=function(e){var t="moodle-dialogue-"+s.stamp(this)+"-wrap";e.notificationBase=s.Node.create('
    ').append(s.Node.create('').append(s.Node.create('
    ')).append(s.Node.create('
    ')).append(s.Node.create('
    '))),e.attachmentPoint=e.attachmentPoint||document.body,s.one(e.attachmentPoint).append(e.notificationBase),e.srcNode="#"+t,delete e.buttons,i.superclass.constructor.apply(this,[e])},s.Panel,{_resizeevent:null,_orientationevent:null,_calculatedzindex:!"form",_currentMaskNodeId:null,_originalPosition:null,_hiddenSiblings:null,hideIfNotForm:function(){null===this.get("boundingBox").one("form")&&this.hide()},initializer:function(){var e,t;!1!==this.get("closeButton")&&(e=this.get("closeButtonTitle"),this.get("buttons").header[0].setAttribute("title",e),this.get("buttons").header[0].setAttribute("aria-label",e)),this.setStdModContent(s.WidgetStdMod.HEADER,'
    '+this.get("headerContent")+"
    ",s.WidgetStdMod.REPLACE),this._hiddenSiblings=[],this.get("render")&&this.render(),this.after("visibleChange",this.visibilityChanged,this),this.get("center")&&this.centerDialogue(),this.get("modal")&&(this.get(d).set("aria-hidden","true"),this.plug(s.M.core.LockScroll)),this.set("focusOn",s.Array(this.get("focusOn")).filter(function(e){return"focusoutside"!==e.eventName})),s.one("document").on("orientationchange",function(){this.centerDialogOnVisible()},this),s.one("window").on("resize",function(){this.centerDialogOnVisible()},this),this.centerDialogOnDialogSizeChange(this),(e=this.get("boundingBox")).addClass(a),s.Array.each(this.get("extraClasses"),e.addClass,e),this.get("visible")&&(this.applyZIndex(),this.applyAndTrapFocus(),this.get("modal")&&this.setAccessibilityVisible()),this.on("maskShow",this.applyZIndex),this.on("maskShow",function(){var e,t=s.one(s.config.win),i=this.get("boundingBox");this.get("center")||(this._originalPosition=i.getXY()),e=this.get("maskNode"),this._currentMaskNodeId!==e.get("_yuid")&&(this._currentMaskNodeId=e.get("_yuid"),e.on("click",this.hideIfNotForm,this)),"fixed"!==i.getStyle("position")&&i.setStyles({top:t.get("scrollTop"),left:t.get("scrollLeft")})},this),e=this.get("notificationBase"),""!==(t=this.get("additionalBaseClass"))&&e.addClass(t),this.after("destroyedChange",function(){this.get(d).remove(!0)},this)},applyZIndex:function(){var e,t=1040,i=this.get("boundingBox"),o=this.get("maskNode"),n=this.get("zIndex");0===n||this._calculatedzindex?(s.all(" [role=dialog], [role=menubar], ."+a).each(function(e){e=this.findZIndex(e);t') .append(Y.Node.create( '' ) .append(Y.Node.create('
    ')) .append(Y.Node.create('
    ')) .append(Y.Node.create('
    '))); config.attachmentPoint = config.attachmentPoint || document.body; Y.one(config.attachmentPoint).append(config.notificationBase); config.srcNode = '#' + id; delete config.buttons; // Don't let anyone pass in buttons as we want to control these during init. addButton can be used later. DIALOGUE.superclass.constructor.apply(this, [config]); }; Y.extend(DIALOGUE, Y.Panel, { // Window resize event listener. _resizeevent: null, // Orientation change event listener. _orientationevent: null, _calculatedzindex: false, // Current maskNode id _currentMaskNodeId: null, /** * The original position of the dialogue before it was reposition to * avoid browser jumping. * * @property _originalPosition * @protected * @type Array */ _originalPosition: null, /** * The list of elements that have been aria hidden when displaying * this dialogue. * * @property _hiddenSiblings * @protected * @type Array */ _hiddenSiblings: null, /** * Hide the modal only if it doesn't contain a form. * * @method hideIfNotForm */ hideIfNotForm: function() { var bb = this.get('boundingBox'), formElement = bb.one(FORM_SELECTOR); if (formElement === null) { this.hide(); } }, /** * Initialise the dialogue. * * @method initializer */ initializer: function() { var bb; if (this.get('closeButton') !== false) { var title = this.get('closeButtonTitle'); // The buttons constructor does not allow custom attributes. this.get('buttons').header[0].setAttribute('title', title); this.get('buttons').header[0].setAttribute('aria-label', title); } this.setStdModContent(Y.WidgetStdMod.HEADER, '
    ' + this.get('headerContent') + '
    ', Y.WidgetStdMod.REPLACE); // Initialise the element cache. this._hiddenSiblings = []; if (this.get('render')) { this.render(); } this.after('visibleChange', this.visibilityChanged, this); if (this.get('center')) { this.centerDialogue(); } if (this.get('modal')) { // If we're a modal then make sure our container is ARIA // hidden by default. ARIA visibility is managed for modal dialogues. this.get(BASE).set('aria-hidden', 'true'); this.plug(Y.M.core.LockScroll); } // Remove the `focusoutside` listener. // It conflicts with the ARIA focuslock manager which supports both YUI and non-YUI dialogues. this.set('focusOn', Y.Array(this.get('focusOn')).filter(function(node) { return node.eventName !== 'focusoutside'; })); Y.one('document').on('orientationchange', function() { // This will detect a change in orientation and re-trigger centering. this.centerDialogOnVisible(); }, this); Y.one('window').on('resize', function() { // Detect window resize (most browsers). this.centerDialogOnVisible(); }, this); // Observe dialog on size change. this.centerDialogOnDialogSizeChange(this); // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507 // and allow setting of z-index in theme. bb = this.get('boundingBox'); bb.addClass(HAS_ZINDEX); // Add any additional classes that were specified. Y.Array.each(this.get('extraClasses'), bb.addClass, bb); if (this.get('visible')) { this.applyZIndex(); this.applyAndTrapFocus(); // Only do accessibility hiding for modals because the ARIA spec // says that all ARIA dialogues should be modal. if (this.get('modal')) { // Make this dialogue visible to screen readers. this.setAccessibilityVisible(); } } // Recalculate the zIndex every time the modal is altered. this.on('maskShow', this.applyZIndex); this.on('maskShow', function() { // When the mask shows, position the boundingBox at the top-left of the window such that when it is // focused, the position does not change. var w = Y.one(Y.config.win), bb = this.get('boundingBox'); if (!this.get('center')) { this._originalPosition = bb.getXY(); } // Check if maskNode already init click event. var maskNode = this.get('maskNode'); if (this._currentMaskNodeId !== maskNode.get('_yuid')) { this._currentMaskNodeId = maskNode.get('_yuid'); maskNode.on('click', this.hideIfNotForm, this); } if (bb.getStyle('position') !== 'fixed') { // If the boundingBox has been positioned in a fixed manner, then it will not position correctly to scrollTop. bb.setStyles({ top: w.get('scrollTop'), left: w.get('scrollLeft') }); } }, this); // Add any additional classes to the content node if required. var nBase = this.get('notificationBase'); var additionalClasses = this.get('additionalBaseClass'); if (additionalClasses !== '') { nBase.addClass(additionalClasses); } // Remove the dialogue from the DOM when it is destroyed. this.after('destroyedChange', function() { this.get(BASE).remove(true); }, this); }, /** * Either set the zindex to the supplied value, or set it to one more than the highest existing * dialog in the page. * * @method applyZIndex */ applyZIndex: function() { var highestzindex = 1040, zindexvalue = 1, bb = this.get('boundingBox'), ol = this.get('maskNode'), zindex = this.get('zIndex'); if (zindex !== 0 && !this._calculatedzindex) { // The zindex was specified so we should use that. bb.setStyle('zIndex', zindex); } else { // Determine the correct zindex by looking at all existing dialogs and menubars in the page. Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) { var zindex = this.findZIndex(node); if (zindex > highestzindex) { highestzindex = zindex; } }, this); // Only set the zindex if we found a wrapper. zindexvalue = (highestzindex + 1).toString(); bb.setStyle('zIndex', zindexvalue); this.set('zIndex', zindexvalue); if (this.get('modal')) { ol.setStyle('zIndex', zindexvalue); // In IE8, the z-indexes do not take effect properly unless you toggle // the lightbox from 'fixed' to 'static' and back. This code does so // using the minimum setTimeouts that still actually work. if (Y.UA.ie && Y.UA.compareVersions(Y.UA.ie, 9) < 0) { setTimeout(function() { ol.setStyle('position', 'static'); setTimeout(function() { ol.setStyle('position', 'fixed'); }, 0); }, 0); } } this._calculatedzindex = true; } }, /** * Finds the zIndex of the given node or its parent. * * @method findZIndex * @param {Node} node The Node to apply the zIndex to. * @return {Number} Either the zIndex, or 0 if one was not found. */ findZIndex: function(node) { // In most cases the zindex is set on the parent of the dialog. var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex'); if (zindex) { return parseInt(zindex, 10); } return 0; }, /** * Event listener for the visibility changed event. * * @method visibilityChanged * @param {EventFacade} e */ visibilityChanged: function(e) { var titlebar, bb; if (e.attrName === 'visible') { this.get('maskNode').addClass(CSS_CLASSES.LIGHTBOX); // Going from visible to hidden. if (e.prevVal && !e.newVal) { bb = this.get('boundingBox'); if (this._resizeevent) { this._resizeevent.detach(); this._resizeevent = null; } if (this._orientationevent) { this._orientationevent.detach(); this._orientationevent = null; } require(['core/local/aria/focuslock'], function(FocusLockManager) { // Untrap focus when the dialogue is hidden. FocusLockManager.untrapFocus(); }); if (this.get('modal')) { // Hide this dialogue from screen readers. this.setAccessibilityHidden(); } } // Going from hidden to visible. if (!e.prevVal && e.newVal) { // This needs to be done each time the dialog is shown as new dialogs may have been opened. this.applyZIndex(); // This needs to be done each time the dialog is shown as the window may have been resized. this.makeResponsive(); if (!this.shouldResizeFullscreen()) { if (this.get('draggable')) { titlebar = '#' + this.get('id') + ' .' + CSS_CLASSES.HEADER; this.plug(Y.Plugin.Drag, {handles: [titlebar]}); Y.one(titlebar).setStyle('cursor', 'move'); } } // Only do accessibility hiding for modals because the ARIA spec // says that all ARIA dialogues should be modal. if (this.get('modal')) { // Make this dialogue visible to screen readers. this.setAccessibilityVisible(); } } if (this.get('center') && !e.prevVal && e.newVal) { this.centerDialogue(); } } }, /** * If the responsive attribute is set on the dialog, and the window size is * smaller than the responsive width - make the dialog fullscreen. * * @method makeResponsive */ makeResponsive: function() { var bb = this.get('boundingBox'); if (this.shouldResizeFullscreen()) { // Make this dialogue fullscreen on a small screen. // Disable the page scrollbars. // Size and position the fullscreen dialog. bb.addClass(DIALOGUE_FULLSCREEN_CLASS); bb.setStyles({'left': null, 'top': null, 'width': null, 'height': null, 'right': null, 'bottom': null}); } else { if (this.get('responsive')) { // We must reset any of the fullscreen changes. bb.removeClass(DIALOGUE_FULLSCREEN_CLASS) .setStyles({'width': this.get('width'), 'height': this.get('height')}); } } // Update Lock scroll if the plugin is present. if (this.lockScroll) { this.lockScroll.updateScrollLock(this.shouldResizeFullscreen()); } }, /** * Center the dialog on the screen. * * @method centerDialogue */ centerDialogue: function() { var bb = this.get('boundingBox'), hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS), x, y; // Don't adjust the position if we are in full screen mode. if (this.shouldResizeFullscreen()) { return; } if (hidden) { bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS); } x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth')) / 2), 15); y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight')) / 2), 15) + Y.one(window).get('scrollTop'); bb.setStyles({'left': x, 'top': y}); if (hidden) { bb.addClass(DIALOGUE_HIDDEN_CLASS); } this.makeResponsive(); }, /** * Automatic re-center dialog when dialog size is changed. * * @method centerDialogOnDialogSizeChange * @param {M.core.dialogue} dialog object to apply centering. */ centerDialogOnDialogSizeChange: function(dialog) { // ResizeObserver doesn't get recognized in JSHint. // So we need to suppress the false warning. var observer = new ResizeObserver(function() { // jshint ignore:line dialog.centerDialogOnVisible(); }); var bb = dialog.get('boundingBox'); observer.observe(bb._node, {attributes: true, attributeFilter: ['class']}); }, /** * Centering dialog when dialog is visible. * * @method centerDialogOnVisible */ centerDialogOnVisible: function() { if (!this.get('visible')) { return; // Only centre visible dialogue. } if (this.name !== DIALOGUE_NAME) { return; // Only centre Moodle dialogues. } if (this.shouldResizeFullscreen()) { this.makeResponsive(); } this.centerDialogue(); }, /** * Return whether this dialogue should be fullscreen or not. * * Responsive attribute must be true and we should not be in an iframe and the screen width should * be less than the responsive width. * * @method shouldResizeFullscreen * @return {Boolean} */ shouldResizeFullscreen: function() { return (window === window.parent) && this.get('responsive') && Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth'); }, _focus: function() { this.focus(); }, show: function() { var result = DIALOGUE.superclass.show.call(this); if (!this.get('center') && this._originalPosition) { // Restore the dialogue position to it's location before it was moved at show time. this.get('boundingBox').setXY(this._originalPosition); } this.applyAndTrapFocus(); return result; }, hide: function(e) { if (e) { // If the event was closed by an escape key event, then we need to check that this // dialogue is currently focused to prevent closing all dialogues in the stack. if (e.type === 'key' && e.keyCode === 27 && !this.get('focused')) { return; } } // Unlock scroll if the plugin is present. if (this.lockScroll) { this.lockScroll.disableScrollLock(); } return DIALOGUE.superclass.hide.call(this, arguments); }, /** * Setup key delegation to keep tabbing within the open dialogue. * * @method keyDelegation */ keyDelegation: function() { var bb = this.get('boundingBox'); bb.delegate('key', function(e) { var target = e.target; var direction = 'forward'; if (e.shiftKey) { direction = 'backward'; } if (this.trapFocus(target, direction)) { e.preventDefault(); } }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this); }, /** * Trap the tab focus within the open modal. * * @method trapFocus * @param {string} target the element target * @param {string} direction tab key for forward and tab+shift for backward * @return {Boolean} The result of the focus action. */ trapFocus: function(target, direction) { var bb = this.get('boundingBox'), firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR), lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop(); if (target === lastitem && direction === 'forward') { // Tab key. return firstitem.focus(); } else if (target === firstitem && direction === 'backward') { // Tab+shift key. return lastitem.focus(); } }, /** * Sets the appropriate aria attributes on this dialogue and the other * elements in the DOM to ensure that screen readers are able to navigate * the dialogue popup correctly. * * @method setAccessibilityVisible */ setAccessibilityVisible: function() { // Get the element that contains this dialogue because we need it // to filter out from the document.body child elements. var container = this.get(BASE); // We need to get a list containing each sibling element and the shallowest // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging // the fact that this dialogue is always appended to the document body therefore // it's siblings are the shallowest non-ancestral nodes. If that changes then // this code should also be updated. Y.one(document.body).get('children').each(function(node) { // Skip the element that contains us. if (node !== container) { var hidden = node.get('aria-hidden'); // If they are already hidden we can ignore them. if (hidden !== 'true') { // Save their current state. node.setData('previous-aria-hidden', hidden); this._hiddenSiblings.push(node); // Hide this node from screen readers. node.set('aria-hidden', 'true'); } } }, this); // Make us visible to screen readers. container.set('aria-hidden', 'false'); }, /** * Restores the aria visibility on the DOM elements changed when displaying * the dialogue popup and makes the dialogue aria hidden to allow screen * readers to navigate the main page correctly when the dialogue is closed. * * @method setAccessibilityHidden */ setAccessibilityHidden: function() { var container = this.get(BASE); container.set('aria-hidden', 'true'); // Restore the sibling nodes back to their original values. Y.Array.each(this._hiddenSiblings, function(node) { var previousValue = node.getData('previous-aria-hidden'); // If the element didn't previously have an aria-hidden attribute // then we can just remove the one we set. if (previousValue === null) { node.removeAttribute('aria-hidden'); } else { // Otherwise set it back to the old value (which will be false). node.set('aria-hidden', previousValue); } }); // Clear the cache. No longer need to store these. this._hiddenSiblings = []; }, /** * Focuses on the node specified by focusOnShowSelector, or the first focusable node if nothing is specified. * It also traps the focus to the current bounding box. * * @method applyAndTrapFocus */ applyAndTrapFocus: function() { var content = this.bodyNode; var focusSelector = this.get('focusOnShowSelector'); var focusNode = null; // Try and find a node to focus on using the focusOnShowSelector attribute. if (focusSelector !== null) { focusNode = this.get('boundingBox').one(focusSelector); } if (!focusNode) { // Fall back to the first focusable element in the body of the dialogue if no focus node was found yet. if (content && content !== '') { focusNode = content.one(CAN_RECEIVE_FOCUS_SELECTOR); } } require(['core/local/aria/focuslock'], function(FocusLockManager) { // Trap focus to the current bounding box. FocusLockManager.trapFocus(this.get('boundingBox').getDOMNode()); if (focusNode) { focusNode.focus(); } }.bind(this)); }, }, { NAME: DIALOGUE_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * Any additional classes to add to the base Node. * * @attribute additionalBaseClass * @type String * @default '' */ additionalBaseClass: { value: '' }, /** * The Notification base Node. * * @attribute notificationBase * @type Node */ notificationBase: { }, /** * Whether to display the dialogue modally and with a * lightbox style. * * @attribute lightbox * @type Boolean * @default true * @deprecated Since Moodle 2.7. Please use modal instead. */ lightbox: { lazyAdd: false, setter: function(value) { this.set('modal', value); } }, /** * Whether to display a close button on the dialogue. * * Note, we do not recommend hiding the close button as this has * potential accessibility concerns. * * @attribute closeButton * @type Boolean * @default true */ closeButton: { validator: Y.Lang.isBoolean, value: true }, /** * The title for the close button if one is to be shown. * * @attribute closeButtonTitle * @type String * @default 'Close' */ closeButtonTitle: { validator: Y.Lang.isString, value: M.util.get_string('closebuttontitle', 'moodle') }, /** * Whether to display the dialogue centrally on the screen. * * @attribute center * @type Boolean * @default true */ center: { validator: Y.Lang.isBoolean, value: true }, /** * Whether to make the dialogue movable around the page. * * @attribute draggable * @type Boolean * @default false */ draggable: { validator: Y.Lang.isBoolean, value: false }, /** * Used to generate a unique id for the dialogue. * * @attribute COUNT * @type String * @default null * @writeonce */ COUNT: { writeOnce: true, valueFn: function() { return Y.stamp(this); } }, /** * Used to disable the fullscreen resizing behaviour if required. * * @attribute responsive * @type Boolean * @default true */ responsive: { validator: Y.Lang.isBoolean, value: true }, /** * The width that this dialogue should be resized to fullscreen. * * @attribute responsiveWidth * @type Number * @default 768 */ responsiveWidth: { value: 768 }, /** * Selector to a node that should recieve focus when this dialogue is shown. * * The default behaviour is to focus on the header. * * @attribute focusOnShowSelector * @default null * @type String */ focusOnShowSelector: { value: null } } }); Y.Base.modifyAttrs(DIALOGUE, { /** * String with units, or number, representing the width of the Widget. * If a number is provided, the default unit, defined by the Widgets * DEF_UNIT, property is used. * * If a value of 'auto' is used, then an empty String is instead * returned. * * @attribute width * @default '400px' * @type {String|Number} */ width: { value: '400px', setter: function(value) { if (value === 'auto') { return ''; } return value; } }, /** * Boolean indicating whether or not the Widget is visible. * * We override this from the default Widget attribute value. * * @attribute visible * @default false * @type Boolean */ visible: { value: false }, /** * A convenience Attribute, which can be used as a shortcut for the * `align` Attribute. * * Note: We override this in Moodle such that it sets a value for the * `center` attribute if set. The `centered` will always return false. * * @attribute centered * @type Boolean|Node * @default false */ centered: { setter: function(value) { if (value) { this.set('center', true); } return false; } }, /** * Boolean determining whether to render the widget during initialisation. * * We override this to change the default from false to true for the dialogue. * We then proceed to early render the dialogue during our initialisation rather than waiting * for YUI to render it after that. * * @attribute render * @type Boolean * @default true */ render: { value: true, writeOnce: true }, /** * Any additional classes to add to the boundingBox. * * @attribute extraClasses * @type Array * @default [] */ extraClasses: { value: [] }, /** * Identifier for the widget. * * @attribute id * @type String * @default a product of guid(). * @writeOnce */ id: { writeOnce: true, valueFn: function() { var id = 'moodle-dialogue-' + Y.stamp(this); return id; } }, /** * Collection containing the widget's buttons. * * @attribute buttons * @type Object * @default {} */ buttons: { getter: Y.WidgetButtons.prototype._getButtons, setter: Y.WidgetButtons.prototype._setButtons, valueFn: function() { if (this.get('closeButton') === false) { return null; } else { return [ { section: Y.WidgetStdMod.HEADER, classNames: 'closebutton', action: function() { this.hide(); } } ]; } } } }); Y.Base.mix(DIALOGUE, [Y.M.core.WidgetFocusAfterHide]); M.core.dialogue = DIALOGUE; /** * A dialogue type designed to display informative messages to users. * * @module moodle-core-notification */ /** * Extends core Dialogue to provide a type of dialogue which can be used * for informative message which are modal, and centered. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.notification.info * @extends M.core.dialogue */ var INFO = function() { INFO.superclass.constructor.apply(this, arguments); }; Y.extend(INFO, M.core.dialogue, { initializer: function() { this.show(); } }, { NAME: 'Moodle information dialogue', CSS_PREFIX: DIALOGUE_PREFIX }); Y.Base.modifyAttrs(INFO, { /** * Whether the widget should be modal or not. * * We override this to change the default from false to true for a subset of dialogues. * * @attribute modal * @type Boolean * @default true */ modal: { validator: Y.Lang.isBoolean, value: true } }); M.core.notification = M.core.notification || {}; M.core.notification.info = INFO; }, '@VERSION@', { "requires": [ "base", "node", "panel", "escape", "event-key", "dd-plugin", "moodle-core-widget-focusafterclose", "moodle-core-lockscroll" ] }); build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js000064400000014722152265472450024157 0ustar00YUI.add('moodle-core-notification-confirm', function (Y, NAME) { /* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; /** * A dialogue type designed to display a confirmation to the user. * * @module moodle-core-notification * @submodule moodle-core-notification-confirm */ var CONFIRM_NAME = 'Moodle confirmation dialogue', CONFIRM; /** * Extends core Dialogue to show the confirmation dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.confirm * @extends M.core.dialogue */ CONFIRM = function(config) { CONFIRM.superclass.constructor.apply(this, [config]); }; Y.extend(CONFIRM, M.core.notification.info, { /** * The list of events to detach when destroying this dialogue. * * @property _closeEvents * @type EventHandle[] * @private */ _closeEvents: null, /** * A reference to the yes button. * * @property _yesButton * @type Node * @private */ _yesButton: null, /** * A reference to the No button. * * @property _noButton * @type Node * @private */ _noButton: null, /** * A reference to the Question. * * @property _question * @type Node * @private */ _question: null, initializer: function() { window.console.error( 'The module "moodle-core-notification-confirm" module has been deprecated and will be removed in Moodle 4.4.' ); this._closeEvents = []; this.publish('complete'); this.publish('complete-yes'); this.publish('complete-no'); this._yesButton = Y.Node.create(''); this._noButton = Y.Node.create(''); this._question = Y.Node.create('
    ' + this.get(QUESTION) + '
    '); var content = Y.Node.create('
    ') .append(this._question) .append(Y.Node.create( '
    ' ) .append(this._yesButton) .append(this._noButton)); this.get(BASE).addClass('moodle-dialogue-confirm'); this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); this.setStdModContent(Y.WidgetStdMod.HEADER, '
    ' + this.get(TITLE) + '
    ', Y.WidgetStdMod.REPLACE); this._closeEvents.push( Y.on('key', this.submit, window, 'down:27', this, false), this._yesButton.on('click', this.submit, this, true), this._noButton.on('click', this.submit, this, false) ); var closeButton = this.get('boundingBox').one('.closebutton'); if (closeButton) { // The close button should act exactly like the 'No' button. this._closeEvents.push( closeButton.on('click', this.submit, this) ); } }, submit: function(e, outcome) { new Y.EventHandle(this._closeEvents).detach(); this.fire('complete', outcome); if (outcome) { this.fire('complete-yes'); } else { this.fire('complete-no'); } this.hide(); this.destroy(); } }, { NAME: CONFIRM_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The button text to use to accept the confirmation. * * @attribute yesLabel * @type String * @default 'Yes' */ yesLabel: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('yes', 'moodle'); }, setter: function(value) { if (this._yesButton) { this._yesButton.set('value', value); } return value; } }, /** * The button text to use to reject the confirmation. * * @attribute noLabel * @type String * @default 'No' */ noLabel: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('no', 'moodle'); }, setter: function(value) { if (this._noButton) { this._noButton.set('value', value); } return value; } }, /** * The title of the dialogue. * * @attribute title * @type String * @default 'Confirm' */ title: { validator: Y.Lang.isString, value: M.util.get_string('confirm', 'moodle') }, /** * The question posed by the dialogue. * * @attribute question * @type String * @default 'Are you sure?' */ question: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('areyousure', 'moodle'); }, setter: function(value) { if (this._question) { this._question.set('value', value); } return value; } } } }); Y.augment(CONFIRM, Y.EventTarget); M.core.confirm = CONFIRM; }, '@VERSION@', {"requires": ["moodle-core-notification-dialogue"]}); build/moodle-core-notification-confirm/moodle-core-notification-confirm.js000064400000014722152265472450023073 0ustar00YUI.add('moodle-core-notification-confirm', function (Y, NAME) { /* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; /** * A dialogue type designed to display a confirmation to the user. * * @module moodle-core-notification * @submodule moodle-core-notification-confirm */ var CONFIRM_NAME = 'Moodle confirmation dialogue', CONFIRM; /** * Extends core Dialogue to show the confirmation dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.confirm * @extends M.core.dialogue */ CONFIRM = function(config) { CONFIRM.superclass.constructor.apply(this, [config]); }; Y.extend(CONFIRM, M.core.notification.info, { /** * The list of events to detach when destroying this dialogue. * * @property _closeEvents * @type EventHandle[] * @private */ _closeEvents: null, /** * A reference to the yes button. * * @property _yesButton * @type Node * @private */ _yesButton: null, /** * A reference to the No button. * * @property _noButton * @type Node * @private */ _noButton: null, /** * A reference to the Question. * * @property _question * @type Node * @private */ _question: null, initializer: function() { window.console.error( 'The module "moodle-core-notification-confirm" module has been deprecated and will be removed in Moodle 4.4.' ); this._closeEvents = []; this.publish('complete'); this.publish('complete-yes'); this.publish('complete-no'); this._yesButton = Y.Node.create(''); this._noButton = Y.Node.create(''); this._question = Y.Node.create('
    ' + this.get(QUESTION) + '
    '); var content = Y.Node.create('
    ') .append(this._question) .append(Y.Node.create( '
    ' ) .append(this._yesButton) .append(this._noButton)); this.get(BASE).addClass('moodle-dialogue-confirm'); this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); this.setStdModContent(Y.WidgetStdMod.HEADER, '
    ' + this.get(TITLE) + '
    ', Y.WidgetStdMod.REPLACE); this._closeEvents.push( Y.on('key', this.submit, window, 'down:27', this, false), this._yesButton.on('click', this.submit, this, true), this._noButton.on('click', this.submit, this, false) ); var closeButton = this.get('boundingBox').one('.closebutton'); if (closeButton) { // The close button should act exactly like the 'No' button. this._closeEvents.push( closeButton.on('click', this.submit, this) ); } }, submit: function(e, outcome) { new Y.EventHandle(this._closeEvents).detach(); this.fire('complete', outcome); if (outcome) { this.fire('complete-yes'); } else { this.fire('complete-no'); } this.hide(); this.destroy(); } }, { NAME: CONFIRM_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The button text to use to accept the confirmation. * * @attribute yesLabel * @type String * @default 'Yes' */ yesLabel: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('yes', 'moodle'); }, setter: function(value) { if (this._yesButton) { this._yesButton.set('value', value); } return value; } }, /** * The button text to use to reject the confirmation. * * @attribute noLabel * @type String * @default 'No' */ noLabel: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('no', 'moodle'); }, setter: function(value) { if (this._noButton) { this._noButton.set('value', value); } return value; } }, /** * The title of the dialogue. * * @attribute title * @type String * @default 'Confirm' */ title: { validator: Y.Lang.isString, value: M.util.get_string('confirm', 'moodle') }, /** * The question posed by the dialogue. * * @attribute question * @type String * @default 'Are you sure?' */ question: { validator: Y.Lang.isString, valueFn: function() { return M.util.get_string('areyousure', 'moodle'); }, setter: function(value) { if (this._question) { this._question.set('value', value); } return value; } } } }); Y.augment(CONFIRM, Y.EventTarget); M.core.confirm = CONFIRM; }, '@VERSION@', {"requires": ["moodle-core-notification-dialogue"]}); build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js000064400000005207152265472450023652 0ustar00YUI.add("moodle-core-notification-confirm",function(i,t){var e;M.core=M.core||{},i.extend(e=function(t){e.superclass.constructor.apply(this,[t])},M.core.notification.info,{_closeEvents:null,_yesButton:null,_noButton:null,_question:null,initializer:function(){var t;window.console.error('The module "moodle-core-notification-confirm" module has been deprecated and will be removed in Moodle 4.4.'),this._closeEvents=[],this.publish("complete"),this.publish("complete-yes"),this.publish("complete-no"),this._yesButton=i.Node.create(''),this._noButton=i.Node.create(''),this._question=i.Node.create('
    '+this.get("question")+"
    "),t=i.Node.create('
    ').append(this._question).append(i.Node.create('
    ').append(this._yesButton).append(this._noButton)),this.get("notificationBase").addClass("moodle-dialogue-confirm"),this.setStdModContent(i.WidgetStdMod.BODY,t,i.WidgetStdMod.REPLACE),this.setStdModContent(i.WidgetStdMod.HEADER,'
    '+this.get("title")+"
    ",i.WidgetStdMod.REPLACE),this._closeEvents.push(i.on("key",this.submit,window,"down:27",this,!1),this._yesButton.on("click",this.submit,this,!0),this._noButton.on("click",this.submit,this,!1)),(t=this.get("boundingBox").one(".closebutton"))&&this._closeEvents.push(t.on("click",this.submit,this))},submit:function(t,e){new i.EventHandle(this._closeEvents).detach(),this.fire("complete",e),e?this.fire("complete-yes"):this.fire("complete-no"),this.hide(),this.destroy()}},{NAME:"Moodle confirmation dialogue",CSS_PREFIX:"moodle-dialogue",ATTRS:{yesLabel:{validator:i.Lang.isString,valueFn:function(){return M.util.get_string("yes","moodle")},setter:function(t){return this._yesButton&&this._yesButton.set("value",t),t}},noLabel:{validator:i.Lang.isString,valueFn:function(){return M.util.get_string("no","moodle")},setter:function(t){return this._noButton&&this._noButton.set("value",t),t}},title:{validator:i.Lang.isString,value:M.util.get_string("confirm","moodle")},question:{validator:i.Lang.isString,valueFn:function(){return M.util.get_string("areyousure","moodle")},setter:function(t){return this._question&&this._question.set("value",t),t}}}}),i.augment(e,i.EventTarget),M.core.confirm=e},"@VERSION@",{requires:["moodle-core-notification-dialogue"]});build/moodle-core-dragdrop/moodle-core-dragdrop-min.js000064400000016502152265472450017014 0ustar00YUI.add("moodle-core-dragdrop",function(h,t){var d="moodle-core-dragdrop-draghandle",e=function(){e.superclass.constructor.apply(this,arguments)};h.extend(e,h.Base,{goingup:null,absgoingup:null,samenodeclass:null,parentnodeclass:null,samenodelabel:null,parentnodelabel:null,groups:[],lastdroptarget:null,detectkeyboarddirection:!1,listeners:null,initializer:function(){this.listeners=[],this.listeners.push(h.DD.DDM.on("drag:start",this.global_drag_start,this)),this.listeners.push(h.DD.DDM.on("drag:over",this.globalDragOver,this)),this.listeners.push(h.DD.DDM.on("drag:end",this.global_drag_end,this)),this.listeners.push(h.DD.DDM.on("drag:drag",this.global_drag_drag,this)),this.listeners.push(h.DD.DDM.on("drop:over",this.global_drop_over,this)),this.listeners.push(h.DD.DDM.on("drop:hit",this.global_drop_hit,this)),this.listeners.push(h.DD.DDM.on("drag:dropmiss",this.global_drag_dropmiss,this)),this.listeners.push(h.one(h.config.doc.body).delegate("key",this.global_keydown,"down:32, enter, esc","."+d,this)),this.listeners.push(h.one(h.config.doc.body).delegate("click",this.global_keydown,"."+d,this))},destructor:function(){new h.EventHandle(this.listeners).detach()},get_drag_handle:function(t,e,o){var r=h.Node.create("").addClass(e).setAttribute("title",t).setAttribute("tabIndex",0).setAttribute("data-draggroups",this.groups).setAttribute("role","button");return r.addClass(d),window.require(["core/templates"],function(t){t.renderPix("i/move_2d","core").then(function(t){t=h.Node.create(t);t.setStyle("cursor","move"),void 0!==o&&t.addClass(o),r.appendChild(t)})}),r},lock_drag_handle:function(t,e){t.removeHandle("."+e)},unlock_drag_handle:function(t,e){t.addHandle("."+e),t.get("activeHandle").focus()},ajax_failure:function(t){t={name:t.status+" "+t.statusText,message:t.responseText};return new M.core.exception(t)},in_group:function(e){var o=!1;return h.each(this.groups,function(t){e._groups[t]&&(o=!0)},this),o},global_drag_start:function(t){var e=t.target;this.in_group(e)&&(this.originalstyle=e.get("node").getAttribute("style"),e.get("node").setStyle("opacity",".25"),e.get("dragNode").setStyles({opacity:".75",borderColor:e.get("node").getStyle("borderColor"),backgroundColor:e.get("node").getStyle("backgroundColor")}),e.get("dragNode").empty(),this.drag_start(t))},globalDragOver:function(t){this.dragOver(t)},global_drag_end:function(t){var e=t.target;this.in_group(e)&&(e.get("node").setAttribute("style",this.originalstyle),this.drag_end(t))},global_drag_drag:function(t){var e=t.target,o=t.info;this.in_group(e)&&(o.start[1]o.xy[1]&&(this.absgoingup=!1),o.delta[1]<0?this.goingup=!0:0")).addClass("dragdrop-keyboard-drag ps-2"),h.all("."+this.samenodeclass+", ."+this.parentnodeclass).each(function(t){var e,o,r,a=!1,n=t,i=t.getAttribute("class").split(" ").join(", .");if(t.drop&&t.drop.inGroup(this.groups)&&t.drop.get("node")!==d&&(t.next(i)!==d||this.detectkeyboarddirection))a=!0;else for(e=t.getAttribute("data-draggroups").split(" "),o=0;o"),p=h.Node.create(""),s=this.find_element_text(n),u=this.samenodelabel&&t.hasClass(this.samenodeclass)?M.util.get_string(this.samenodelabel.identifier,this.samenodelabel.component,s):this.parentnodelabel&&t.hasClass(this.parentnodeclass)?M.util.get_string(this.parentnodelabel.identifier,this.parentnodelabel.component,s):M.util.get_string("tocontent","moodle",s),p.setContent(u),p.setAttribute("class","aalink d-inline-block mw-100 text-truncate"),p.setAttribute("data-drop-target",t.get("id")),p.setAttribute("tabindex","0"),p.setAttribute("role","button"),p.on("click",this.global_keyboard_drop,this),p.on("key",this.global_keyboard_drop,"down:enter,32",this),g.append(p),l.append(g))},this),M.core.dragdrop.dropui=new M.core.dialogue({headerContent:e,bodyContent:l,draggable:!0,visible:!0,center:!0,modal:!0}),M.core.dragdrop.dropui.after("visibleChange",function(t){t.prevVal&&!t.newVal&&this.global_cancel_keyboard_drag()},this),l.one("a")&&l.one("a").focus()},simulated_drag_drop_event:function(t,e){var o=function(t){this.node=t};o.prototype.get=function(t){return"node"===t||"dragNode"===t||"dropNode"===t?this.node:"activeHandle"===t?this.node.one(".editing_move"):null},o.prototype.inGroup=function(){return!0},o.prototype.addHandle=function(){},o.prototype.removeHandle=function(){},this.drop=new o(e),this.drag=new o(t),this.target=this.drop},global_keyboard_drop:function(t){var e, o=M.core.dragdrop.keydragcontainer,r=h.one("#"+t.target.getAttribute("data-drop-target"));M.core.dragdrop.dropui.hide(),t.preventDefault(),this.detectkeyboarddirection&&o.getY()>r.getY()?(this.absgoingup=!0,this.goingup=!0):(this.absgoingup=!1,this.goingup=!1),t=new this.simulated_drag_drop_event(o,o),e=new this.simulated_drag_drop_event(o,r),this.drag_start(t),this.global_drop_over(e),r.hasClass(this.parentnodeclass)&&r.contains(o)&&r.prepend(o),this.global_drop_hit(e)},global_cancel_keyboard_drag:function(){M.core.dragdrop.keydragcontainer&&(M.core.dragdrop.keydraghandle.focus(),M.core.dragdrop.keydragcontainer=null),M.core.dragdrop.dropui&&M.core.dragdrop.dropui.destroy()},global_keydown:function(t){var e,o,r,a,n,i=t.target.ancestor("."+d,!0);if(null!==i){if(27===t.keyCode)return this.global_cancel_keyboard_drag(),void t.preventDefault();if(i.hasClass(d)&&(13===t.keyCode||32===t.keyCode||"click"===t.type)){for(o=i.getAttribute("data-draggroups").split(" "),n=!1,r=0;r') .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; }, '@VERSION@', {"requires": ["base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification"]}); build/moodle-core-dragdrop/moodle-core-dragdrop-debug.js000064400000060372152265472450017323 0ustar00YUI.add('moodle-core-dragdrop', function (Y, NAME) { /* 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; }, '@VERSION@', {"requires": ["base", "node", "io", "dom", "dd", "event-key", "event-focus", "moodle-core-notification"]}); build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js000064400000013365152265472450026573 0ustar00YUI.add('moodle-core-notification-ajaxexception', function (Y, NAME) { /* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; /** * A dialogue type designed to display an appropriate error when an error * thrown in the Moodle codebase was reported during an AJAX request. * * @module moodle-core-notification * @submodule moodle-core-notification-ajaxexception */ var AJAXEXCEPTION_NAME = 'Moodle AJAX exception', AJAXEXCEPTION; /** * Extends core Dialogue to show the exception dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.ajaxException * @extends M.core.dialogue */ AJAXEXCEPTION = function(config) { config.name = config.name || M.util.get_string('error', 'moodle'); config.closeButton = true; AJAXEXCEPTION.superclass.constructor.apply(this, [config]); }; Y.extend(AJAXEXCEPTION, M.core.notification.info, { _keypress: null, initializer: function(config) { var content, self = this, delay = this.get('hideTimeoutDelay'), labelsep = M.util.get_string('labelsep', 'langconfig'); this.get(BASE).addClass('moodle-dialogue-exception'); this.setStdModContent(Y.WidgetStdMod.HEADER, '
        ' + Y.Escape.html(config.name) + '
        ', Y.WidgetStdMod.REPLACE); content = Y.Node.create('
        ') .append(Y.Node.create('
        ' + Y.Escape.html(this.get('error')) + '
        ')) .append(Y.Node.create('')) .append(Y.Node.create('')) .append(Y.Node.create('')); if (M.cfg.developerdebug) { content.all('.moodle-exception-param').removeClass('hidden'); } this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); if (delay) { this._hideTimeout = setTimeout(function() { self.hide(); }, delay); } this.after('visibleChange', this.errorVisibilityChanged, this); this._keypress = Y.on('key', this.hide, window, 'down:13, 27', this); this.centerDialogue(); }, errorVisibilityChanged: function(e) { if (e.attrName === 'visible' && e.prevVal && !e.newVal) { var self = this; this._keypress.detach(); setTimeout(function() { self.destroy(); }, 1000); } } }, { NAME: AJAXEXCEPTION_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The error message given in the exception. * * @attribute error * @type String * @default 'Unknown error' * @optional */ error: { validator: Y.Lang.isString, value: M.util.get_string('unknownerror', 'moodle') }, /** * Any additional debug information given in the exception. * * @attribute stacktrace * @type String|null * @default null * @optional */ debuginfo: { value: null }, /** * The complete stack trace provided in the exception. * * @attribute stacktrace * @type String|null * @default null * @optional */ stacktrace: { value: null }, /** * A link which may be used by support staff to replicate the issue. * * @attribute reproductionlink * @type String * @default null * @optional */ reproductionlink: { setter: function(link) { if (link !== null) { link = Y.Escape.html(link); link = '' + link.replace(M.cfg.wwwroot, '') + ''; } return link; }, value: null }, /** * If set, the dialogue is hidden after the specified timeout period. * * @attribute hideTimeoutDelay * @type Number * @default null * @optional */ hideTimeoutDelay: { validator: Y.Lang.isNumber, value: null } } }); M.core.ajaxException = AJAXEXCEPTION; }, '@VERSION@', {"requires": ["moodle-core-notification-dialogue"]}); build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js000064400000013365152265472450025507 0ustar00YUI.add('moodle-core-notification-ajaxexception', function (Y, NAME) { /* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; /** * A dialogue type designed to display an appropriate error when an error * thrown in the Moodle codebase was reported during an AJAX request. * * @module moodle-core-notification * @submodule moodle-core-notification-ajaxexception */ var AJAXEXCEPTION_NAME = 'Moodle AJAX exception', AJAXEXCEPTION; /** * Extends core Dialogue to show the exception dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.ajaxException * @extends M.core.dialogue */ AJAXEXCEPTION = function(config) { config.name = config.name || M.util.get_string('error', 'moodle'); config.closeButton = true; AJAXEXCEPTION.superclass.constructor.apply(this, [config]); }; Y.extend(AJAXEXCEPTION, M.core.notification.info, { _keypress: null, initializer: function(config) { var content, self = this, delay = this.get('hideTimeoutDelay'), labelsep = M.util.get_string('labelsep', 'langconfig'); this.get(BASE).addClass('moodle-dialogue-exception'); this.setStdModContent(Y.WidgetStdMod.HEADER, '
        ' + Y.Escape.html(config.name) + '
        ', Y.WidgetStdMod.REPLACE); content = Y.Node.create('
        ') .append(Y.Node.create('
        ' + Y.Escape.html(this.get('error')) + '
        ')) .append(Y.Node.create('')) .append(Y.Node.create('')) .append(Y.Node.create('')); if (M.cfg.developerdebug) { content.all('.moodle-exception-param').removeClass('hidden'); } this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); if (delay) { this._hideTimeout = setTimeout(function() { self.hide(); }, delay); } this.after('visibleChange', this.errorVisibilityChanged, this); this._keypress = Y.on('key', this.hide, window, 'down:13, 27', this); this.centerDialogue(); }, errorVisibilityChanged: function(e) { if (e.attrName === 'visible' && e.prevVal && !e.newVal) { var self = this; this._keypress.detach(); setTimeout(function() { self.destroy(); }, 1000); } } }, { NAME: AJAXEXCEPTION_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The error message given in the exception. * * @attribute error * @type String * @default 'Unknown error' * @optional */ error: { validator: Y.Lang.isString, value: M.util.get_string('unknownerror', 'moodle') }, /** * Any additional debug information given in the exception. * * @attribute stacktrace * @type String|null * @default null * @optional */ debuginfo: { value: null }, /** * The complete stack trace provided in the exception. * * @attribute stacktrace * @type String|null * @default null * @optional */ stacktrace: { value: null }, /** * A link which may be used by support staff to replicate the issue. * * @attribute reproductionlink * @type String * @default null * @optional */ reproductionlink: { setter: function(link) { if (link !== null) { link = Y.Escape.html(link); link = '' + link.replace(M.cfg.wwwroot, '') + ''; } return link; }, value: null }, /** * If set, the dialogue is hidden after the specified timeout period. * * @attribute hideTimeoutDelay * @type Number * @default null * @optional */ hideTimeoutDelay: { validator: Y.Lang.isNumber, value: null } } }); M.core.ajaxException = AJAXEXCEPTION; }, '@VERSION@', {"requires": ["moodle-core-notification-dialogue"]}); build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js000064400000004467152265472450026273 0ustar00YUI.add("moodle-core-notification-ajaxexception",function(a,e){var t;M.core=M.core||{},a.extend(t=function(e){e.name=e.name||M.util.get_string("error","moodle"),e.closeButton=!0,t.superclass.constructor.apply(this,[e])},M.core.notification.info,{_keypress:null,initializer:function(e){var t=this,i=this.get("hideTimeoutDelay"),o=M.util.get_string("labelsep","langconfig");this.get("notificationBase").addClass("moodle-dialogue-exception"),this.setStdModContent(a.WidgetStdMod.HEADER,'
        '+a.Escape.html(e.name)+"
        ",a.WidgetStdMod.REPLACE),e=a.Node.create('
        ').append(a.Node.create('
        '+a.Escape.html(this.get("error"))+"
        ")).append(a.Node.create('")).append(a.Node.create('")).append(a.Node.create('")),M.cfg.developerdebug&&e.all(".moodle-exception-param").removeClass("hidden"),this.setStdModContent(a.WidgetStdMod.BODY,e,a.WidgetStdMod.REPLACE),i&&(this._hideTimeout=setTimeout(function(){t.hide()},i)),this.after("visibleChange",this.errorVisibilityChanged,this),this._keypress=a.on("key",this.hide,window,"down:13, 27",this),this.centerDialogue()},errorVisibilityChanged:function(e){var t;"visible"===e.attrName&&e.prevVal&&!e.newVal&&((t=this)._keypress.detach(),setTimeout(function(){t.destroy()},1e3))}},{NAME:"Moodle AJAX exception",CSS_PREFIX:"moodle-dialogue",ATTRS:{error:{validator:a.Lang.isString,value:M.util.get_string("unknownerror","moodle")},debuginfo:{value:null},stacktrace:{value:null},reproductionlink:{setter:function(e){return e=null!==e?''+e.replace(M.cfg.wwwroot,"")+"":e},value:null},hideTimeoutDelay:{validator:a.Lang.isNumber,value:null}}}),M.core.ajaxException=t},"@VERSION@",{requires:["moodle-core-notification-dialogue"]});build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js000064400000032065152265472450021161 0ustar00YUI.add('moodle-core-chooserdialogue', function (Y, NAME) { /** * A type of dialogue used as for choosing options. * * @module moodle-core-chooserdialogue */ /** * A type of dialogue used as for choosing options. * * @constructor * @class M.core.chooserdialogue */ var CHOOSERDIALOGUE = function() { CHOOSERDIALOGUE.superclass.constructor.apply(this, arguments); }; Y.extend(CHOOSERDIALOGUE, Y.Base, { // The panel widget panel: null, // The submit button - we disable this until an element is set submitbutton: null, // The chooserdialogue container container: null, // Any event listeners we may need to cancel later listenevents: [], bodycontent: null, headercontent: null, instanceconfig: null, // The hidden field storing the disabled element values for submission. hiddenRadioValue: null, setup_chooser_dialogue: function(bodycontent, headercontent, config) { this.bodycontent = bodycontent; this.headercontent = headercontent; this.instanceconfig = config; }, prepare_chooser: function() { if (this.panel) { return; } // Ensure that we're showing the JS version of the chooser. Y.one(Y.config.doc.body).addClass('jschooser'); // Set Default options var paramkey, params = { bodyContent: this.bodycontent.get('innerHTML'), headerContent: this.headercontent.get('innerHTML'), width: '540px', draggable: true, visible: false, // Hide by default zindex: 100, // Display in front of other items modal: true, // This dialogue should be modal. shim: true, closeButtonTitle: this.get('closeButtonTitle'), focusOnPreviousTargetAfterHide: true, render: false, extraClasses: this._getClassNames() }; // Override with additional options for (paramkey in this.instanceconfig) { params[paramkey] = this.instanceconfig[paramkey]; } // Create the panel this.panel = new M.core.dialogue(params); // Remove the template for the chooser this.bodycontent.remove(); this.headercontent.remove(); // Hide and then render the panel this.panel.hide(); this.panel.render(); // Set useful links. this.container = this.panel.get('boundingBox').one('.choosercontainer'); this.options = this.container.all('.option input[type=radio]'); // The hidden form element we use when submitting. this.hiddenRadioValue = Y.Node.create(''); this.container.one('form').appendChild(this.hiddenRadioValue); // Add the chooserdialogue class to the container for styling this.panel.get('boundingBox').addClass('chooserdialogue'); }, /** * Display the module chooser * * @method display_chooser * @param {EventFacade} e Triggering Event */ display_chooser: function(e) { var bb, dialogue, thisevent; this.prepare_chooser(); // Stop the default event actions before we proceed e.preventDefault(); bb = this.panel.get('boundingBox'); dialogue = this.container.one('.alloptions'); // This will detect a change in orientation and retrigger centering thisevent = Y.one('document').on('orientationchange', function() { this.center_dialogue(dialogue); }, this); this.listenevents.push(thisevent); // Detect window resizes (most browsers) thisevent = Y.one('window').on('resize', function() { this.center_dialogue(dialogue); }, this); this.listenevents.push(thisevent); // These will trigger a check_options call to display the correct help thisevent = this.container.on('click', this.check_options, this); this.listenevents.push(thisevent); thisevent = this.container.on('key_up', this.check_options, this); this.listenevents.push(thisevent); thisevent = this.container.on('dblclick', function(e) { if (e.target.ancestor('div.option')) { this.check_options(); // Prevent duplicate submissions this.submitbutton.setAttribute('disabled', 'disabled'); this.options.setAttribute('disabled', 'disabled'); this.cancel_listenevents(); this.container.one('form').submit(); } }, this); this.listenevents.push(thisevent); this.container.one('form').on('submit', function() { // Prevent duplicate submissions on submit this.submitbutton.setAttribute('disabled', 'disabled'); this.options.setAttribute('disabled', 'disabled'); this.cancel_listenevents(); }, this); // Hook onto the cancel button to hide the form thisevent = this.container.one('.addcancel').on('click', this.cancel_popup, this); this.listenevents.push(thisevent); // Hide will be managed by cancel_popup after restoring the body overflow thisevent = bb.one('button.closebutton').on('click', this.cancel_popup, this); this.listenevents.push(thisevent); // Grab global keyup events and handle them thisevent = Y.one('document').on('keydown', this.handle_key_press, this); this.listenevents.push(thisevent); // Add references to various elements we adjust this.submitbutton = this.container.one('.submitbutton'); // Disable the submit element until the user makes a selection this.submitbutton.set('disabled', 'true'); // Ensure that the options are shown this.options.removeAttribute('disabled'); // Display the panel this.panel.show(e); // Re-centre the dialogue after we've shown it. this.center_dialogue(dialogue); // Finally, focus the first radio element - this enables form selection via the keyboard this.container.one('.option input[type=radio]').focus(); // Trigger check_options to set the initial jumpurl this.check_options(); }, /** * Cancel any listen events in the listenevents queue * * Several locations add event handlers which should only be called before the form is submitted. This provides * a way of cancelling those events. * * @method cancel_listenevents */ cancel_listenevents: function() { // Detach all listen events to prevent duplicate triggers var thisevent; while (this.listenevents.length) { thisevent = this.listenevents.shift(); thisevent.detach(); } }, /** * Calculate the optimum height of the chooser dialogue * * This tries to set a sensible maximum and minimum to ensure that some options are always shown, and preferably * all, whilst fitting the box within the current viewport. * * @method center_dialogue * @param Node {dialogue} Y.Node The dialogue */ center_dialogue: function(dialogue) { var bb = this.panel.get('boundingBox'), winheight = bb.get('winHeight'), newheight, totalheight; if (this.panel.shouldResizeFullscreen()) { dialogue.setStyle('maxHeight', '100%'); dialogue.setStyle('height', 'auto'); this.panel.makeResponsive(); return; } // Try and set a sensible max-height -- this must be done before setting the top // Set a default height of 640px newheight = this.get('maxheight'); if (winheight <= newheight) { // Deal with smaller window sizes if (winheight <= this.get('minheight')) { newheight = this.get('minheight'); } else { newheight = winheight; } } // If the dialogue is larger than a reasonable minimum height, we // disable the page scrollbars. if (newheight > this.get('minheight')) { // Disable the page scrollbars. if (this.panel.lockScroll && !this.panel.lockScroll.isActive()) { this.panel.lockScroll.enableScrollLock(true); } } else { // Re-enable the page scrollbars. if (this.panel.lockScroll && this.panel.lockScroll.isActive()) { this.panel.lockScroll.disableScrollLock(); } } // Take off 15px top and bottom for borders, plus 69px for the title and 57px for the // button area before setting the new max-height. totalheight = newheight; newheight = newheight - (69 + 57 + 15 + 15); dialogue.setStyle('maxHeight', newheight + 'px'); var dialogueheight = bb.getStyle('height'); if (dialogueheight.match(/.*px$/)) { dialogueheight = dialogueheight.replace(/px$/, ''); } else { dialogueheight = totalheight; } if (dialogueheight < this.get('baseheight')) { dialogueheight = this.get('baseheight'); dialogue.setStyle('height', dialogueheight + 'px'); } else { dialogue.setStyle('height', 'auto'); } this.panel.centerDialogue(); }, handle_key_press: function(e) { if (e.keyCode === 27) { this.cancel_popup(e); } }, cancel_popup: function(e) { // Prevent normal form submission before hiding e.preventDefault(); this.hide(); }, hide: function() { // Cancel all listen events this.cancel_listenevents(); this.container.detachAll(); this.panel.hide(); }, check_options: function() { // Check which options are set, and change the parent class // to show/hide help as required this.options.each(function(thisoption) { var optiondiv = thisoption.get('parentNode').get('parentNode'); if (thisoption.get('checked')) { optiondiv.addClass('selected'); // Trigger any events for this option this.option_selected(thisoption); // Ensure that the form may be submitted this.submitbutton.removeAttribute('disabled'); // Ensure that the radio remains focus so that keyboard navigation is still possible thisoption.focus(); } else { optiondiv.removeClass('selected'); } }, this); }, option_selected: function(e) { // Set a hidden input field with the value and name of the radio button. When we submit the form, we // disable the radios to prevent duplicate submission. This has the result however that the value is never // submitted so we set this value to a hidden field instead this.hiddenRadioValue.setAttrs({ value: e.get('value'), name: e.get('name') }); }, /** * Return an array of class names prefixed with 'chooserdialogue-' and * the name of the type of dialogue. * * Note: Class name are converted to lower-case. * * If an array of arguments is supplied, each of these is prefixed and * lower-cased also. * * If no arguments are supplied, then the prefix is returned on it's * own. * * @method _getClassNames * @param {Array} [args] Any additional names to prefix and lower-case. * @return {Array} * @private */ _getClassNames: function(args) { var prefix = 'chooserdialogue-' + this.name, results = []; results.push(prefix.toLowerCase()); if (args) { var arg; for (arg in args) { results.push((prefix + '-' + arg).toLowerCase()); } } return results; } }, { NAME: 'moodle-core-chooserdialogue', ATTRS: { /** * The minimum height (in pixels) before resizing is prevented and scroll * locking disabled. * * @attribute minheight * @type Number * @default 300 */ minheight: { value: 300 }, /** * The base height?? * * @attribute baseheight * @type Number * @default 400 */ baseheight: { value: 400 }, /** * The maximum height (in pixels) at which we stop resizing. * * @attribute maxheight * @type Number * @default 300 */ maxheight: { value: 660 }, /** * The title of the close button. * * @attribute closeButtonTitle * @type String * @default 'Close' */ closeButtonTitle: { validator: Y.Lang.isString, value: 'Close' } } }); M.core = M.core || {}; M.core.chooserdialogue = CHOOSERDIALOGUE; }, '@VERSION@', {"requires": ["base", "panel", "moodle-core-notification"]}); build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js000064400000032065152265472450022245 0ustar00YUI.add('moodle-core-chooserdialogue', function (Y, NAME) { /** * A type of dialogue used as for choosing options. * * @module moodle-core-chooserdialogue */ /** * A type of dialogue used as for choosing options. * * @constructor * @class M.core.chooserdialogue */ var CHOOSERDIALOGUE = function() { CHOOSERDIALOGUE.superclass.constructor.apply(this, arguments); }; Y.extend(CHOOSERDIALOGUE, Y.Base, { // The panel widget panel: null, // The submit button - we disable this until an element is set submitbutton: null, // The chooserdialogue container container: null, // Any event listeners we may need to cancel later listenevents: [], bodycontent: null, headercontent: null, instanceconfig: null, // The hidden field storing the disabled element values for submission. hiddenRadioValue: null, setup_chooser_dialogue: function(bodycontent, headercontent, config) { this.bodycontent = bodycontent; this.headercontent = headercontent; this.instanceconfig = config; }, prepare_chooser: function() { if (this.panel) { return; } // Ensure that we're showing the JS version of the chooser. Y.one(Y.config.doc.body).addClass('jschooser'); // Set Default options var paramkey, params = { bodyContent: this.bodycontent.get('innerHTML'), headerContent: this.headercontent.get('innerHTML'), width: '540px', draggable: true, visible: false, // Hide by default zindex: 100, // Display in front of other items modal: true, // This dialogue should be modal. shim: true, closeButtonTitle: this.get('closeButtonTitle'), focusOnPreviousTargetAfterHide: true, render: false, extraClasses: this._getClassNames() }; // Override with additional options for (paramkey in this.instanceconfig) { params[paramkey] = this.instanceconfig[paramkey]; } // Create the panel this.panel = new M.core.dialogue(params); // Remove the template for the chooser this.bodycontent.remove(); this.headercontent.remove(); // Hide and then render the panel this.panel.hide(); this.panel.render(); // Set useful links. this.container = this.panel.get('boundingBox').one('.choosercontainer'); this.options = this.container.all('.option input[type=radio]'); // The hidden form element we use when submitting. this.hiddenRadioValue = Y.Node.create(''); this.container.one('form').appendChild(this.hiddenRadioValue); // Add the chooserdialogue class to the container for styling this.panel.get('boundingBox').addClass('chooserdialogue'); }, /** * Display the module chooser * * @method display_chooser * @param {EventFacade} e Triggering Event */ display_chooser: function(e) { var bb, dialogue, thisevent; this.prepare_chooser(); // Stop the default event actions before we proceed e.preventDefault(); bb = this.panel.get('boundingBox'); dialogue = this.container.one('.alloptions'); // This will detect a change in orientation and retrigger centering thisevent = Y.one('document').on('orientationchange', function() { this.center_dialogue(dialogue); }, this); this.listenevents.push(thisevent); // Detect window resizes (most browsers) thisevent = Y.one('window').on('resize', function() { this.center_dialogue(dialogue); }, this); this.listenevents.push(thisevent); // These will trigger a check_options call to display the correct help thisevent = this.container.on('click', this.check_options, this); this.listenevents.push(thisevent); thisevent = this.container.on('key_up', this.check_options, this); this.listenevents.push(thisevent); thisevent = this.container.on('dblclick', function(e) { if (e.target.ancestor('div.option')) { this.check_options(); // Prevent duplicate submissions this.submitbutton.setAttribute('disabled', 'disabled'); this.options.setAttribute('disabled', 'disabled'); this.cancel_listenevents(); this.container.one('form').submit(); } }, this); this.listenevents.push(thisevent); this.container.one('form').on('submit', function() { // Prevent duplicate submissions on submit this.submitbutton.setAttribute('disabled', 'disabled'); this.options.setAttribute('disabled', 'disabled'); this.cancel_listenevents(); }, this); // Hook onto the cancel button to hide the form thisevent = this.container.one('.addcancel').on('click', this.cancel_popup, this); this.listenevents.push(thisevent); // Hide will be managed by cancel_popup after restoring the body overflow thisevent = bb.one('button.closebutton').on('click', this.cancel_popup, this); this.listenevents.push(thisevent); // Grab global keyup events and handle them thisevent = Y.one('document').on('keydown', this.handle_key_press, this); this.listenevents.push(thisevent); // Add references to various elements we adjust this.submitbutton = this.container.one('.submitbutton'); // Disable the submit element until the user makes a selection this.submitbutton.set('disabled', 'true'); // Ensure that the options are shown this.options.removeAttribute('disabled'); // Display the panel this.panel.show(e); // Re-centre the dialogue after we've shown it. this.center_dialogue(dialogue); // Finally, focus the first radio element - this enables form selection via the keyboard this.container.one('.option input[type=radio]').focus(); // Trigger check_options to set the initial jumpurl this.check_options(); }, /** * Cancel any listen events in the listenevents queue * * Several locations add event handlers which should only be called before the form is submitted. This provides * a way of cancelling those events. * * @method cancel_listenevents */ cancel_listenevents: function() { // Detach all listen events to prevent duplicate triggers var thisevent; while (this.listenevents.length) { thisevent = this.listenevents.shift(); thisevent.detach(); } }, /** * Calculate the optimum height of the chooser dialogue * * This tries to set a sensible maximum and minimum to ensure that some options are always shown, and preferably * all, whilst fitting the box within the current viewport. * * @method center_dialogue * @param Node {dialogue} Y.Node The dialogue */ center_dialogue: function(dialogue) { var bb = this.panel.get('boundingBox'), winheight = bb.get('winHeight'), newheight, totalheight; if (this.panel.shouldResizeFullscreen()) { dialogue.setStyle('maxHeight', '100%'); dialogue.setStyle('height', 'auto'); this.panel.makeResponsive(); return; } // Try and set a sensible max-height -- this must be done before setting the top // Set a default height of 640px newheight = this.get('maxheight'); if (winheight <= newheight) { // Deal with smaller window sizes if (winheight <= this.get('minheight')) { newheight = this.get('minheight'); } else { newheight = winheight; } } // If the dialogue is larger than a reasonable minimum height, we // disable the page scrollbars. if (newheight > this.get('minheight')) { // Disable the page scrollbars. if (this.panel.lockScroll && !this.panel.lockScroll.isActive()) { this.panel.lockScroll.enableScrollLock(true); } } else { // Re-enable the page scrollbars. if (this.panel.lockScroll && this.panel.lockScroll.isActive()) { this.panel.lockScroll.disableScrollLock(); } } // Take off 15px top and bottom for borders, plus 69px for the title and 57px for the // button area before setting the new max-height. totalheight = newheight; newheight = newheight - (69 + 57 + 15 + 15); dialogue.setStyle('maxHeight', newheight + 'px'); var dialogueheight = bb.getStyle('height'); if (dialogueheight.match(/.*px$/)) { dialogueheight = dialogueheight.replace(/px$/, ''); } else { dialogueheight = totalheight; } if (dialogueheight < this.get('baseheight')) { dialogueheight = this.get('baseheight'); dialogue.setStyle('height', dialogueheight + 'px'); } else { dialogue.setStyle('height', 'auto'); } this.panel.centerDialogue(); }, handle_key_press: function(e) { if (e.keyCode === 27) { this.cancel_popup(e); } }, cancel_popup: function(e) { // Prevent normal form submission before hiding e.preventDefault(); this.hide(); }, hide: function() { // Cancel all listen events this.cancel_listenevents(); this.container.detachAll(); this.panel.hide(); }, check_options: function() { // Check which options are set, and change the parent class // to show/hide help as required this.options.each(function(thisoption) { var optiondiv = thisoption.get('parentNode').get('parentNode'); if (thisoption.get('checked')) { optiondiv.addClass('selected'); // Trigger any events for this option this.option_selected(thisoption); // Ensure that the form may be submitted this.submitbutton.removeAttribute('disabled'); // Ensure that the radio remains focus so that keyboard navigation is still possible thisoption.focus(); } else { optiondiv.removeClass('selected'); } }, this); }, option_selected: function(e) { // Set a hidden input field with the value and name of the radio button. When we submit the form, we // disable the radios to prevent duplicate submission. This has the result however that the value is never // submitted so we set this value to a hidden field instead this.hiddenRadioValue.setAttrs({ value: e.get('value'), name: e.get('name') }); }, /** * Return an array of class names prefixed with 'chooserdialogue-' and * the name of the type of dialogue. * * Note: Class name are converted to lower-case. * * If an array of arguments is supplied, each of these is prefixed and * lower-cased also. * * If no arguments are supplied, then the prefix is returned on it's * own. * * @method _getClassNames * @param {Array} [args] Any additional names to prefix and lower-case. * @return {Array} * @private */ _getClassNames: function(args) { var prefix = 'chooserdialogue-' + this.name, results = []; results.push(prefix.toLowerCase()); if (args) { var arg; for (arg in args) { results.push((prefix + '-' + arg).toLowerCase()); } } return results; } }, { NAME: 'moodle-core-chooserdialogue', ATTRS: { /** * The minimum height (in pixels) before resizing is prevented and scroll * locking disabled. * * @attribute minheight * @type Number * @default 300 */ minheight: { value: 300 }, /** * The base height?? * * @attribute baseheight * @type Number * @default 400 */ baseheight: { value: 400 }, /** * The maximum height (in pixels) at which we stop resizing. * * @attribute maxheight * @type Number * @default 300 */ maxheight: { value: 660 }, /** * The title of the close button. * * @attribute closeButtonTitle * @type String * @default 'Close' */ closeButtonTitle: { validator: Y.Lang.isString, value: 'Close' } } }); M.core = M.core || {}; M.core.chooserdialogue = CHOOSERDIALOGUE; }, '@VERSION@', {"requires": ["base", "panel", "moodle-core-notification"]}); build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js000064400000011112152265472450021730 0ustar00YUI.add("moodle-core-chooserdialogue",function(s,e){var t=function(){t.superclass.constructor.apply(this,arguments)};s.extend(t,s.Base,{panel:null,submitbutton:null,container:null,listenevents:[],bodycontent:null,headercontent:null,instanceconfig:null,hiddenRadioValue:null,setup_chooser_dialogue:function(e,t,i){this.bodycontent=e,this.headercontent=t,this.instanceconfig=i},prepare_chooser:function(){if(!this.panel){s.one(s.config.doc.body).addClass("jschooser");var e,t={bodyContent:this.bodycontent.get("innerHTML"),headerContent:this.headercontent.get("innerHTML"),width:"540px",draggable:!0,visible:!1,zindex:100,modal:!0,shim:!0,closeButtonTitle:this.get("closeButtonTitle"),focusOnPreviousTargetAfterHide:!0,render:!1,extraClasses:this._getClassNames()};for(e in this.instanceconfig)t[e]=this.instanceconfig[e];this.panel=new M.core.dialogue(t),this.bodycontent.remove(),this.headercontent.remove(),this.panel.hide(),this.panel.render(),this.container=this.panel.get("boundingBox").one(".choosercontainer"),this.options=this.container.all(".option input[type=radio]"),this.hiddenRadioValue=s.Node.create(''),this.container.one("form").appendChild(this.hiddenRadioValue),this.panel.get("boundingBox").addClass("chooserdialogue")}},display_chooser:function(e){var t,i,n;this.prepare_chooser(),e.preventDefault(),t=this.panel.get("boundingBox"),i=this.container.one(".alloptions"),n=s.one("document").on("orientationchange",function(){this.center_dialogue(i)},this),this.listenevents.push(n),n=s.one("window").on("resize",function(){this.center_dialogue(i)},this),this.listenevents.push(n),n=this.container.on("click",this.check_options,this),this.listenevents.push(n),n=this.container.on("key_up",this.check_options,this),this.listenevents.push(n),n=this.container.on("dblclick",function(e){e.target.ancestor("div.option")&&(this.check_options(),this.submitbutton.setAttribute("disabled","disabled"),this.options.setAttribute("disabled","disabled"),this.cancel_listenevents(),this.container.one("form").submit())},this),this.listenevents.push(n),this.container.one("form").on("submit",function(){this.submitbutton.setAttribute("disabled","disabled"),this.options.setAttribute("disabled","disabled"),this.cancel_listenevents()},this),n=this.container.one(".addcancel").on("click",this.cancel_popup,this),this.listenevents.push(n),n=t.one("button.closebutton").on("click",this.cancel_popup,this),this.listenevents.push(n),n=s.one("document").on("keydown",this.handle_key_press,this),this.listenevents.push(n),this.submitbutton=this.container.one(".submitbutton"),this.submitbutton.set("disabled","true"),this.options.removeAttribute("disabled"),this.panel.show(e),this.center_dialogue(i),this.container.one(".option input[type=radio]").focus(),this.check_options()},cancel_listenevents:function(){for(;this.listenevents.length;)this.listenevents.shift().detach()},center_dialogue:function(e){var t,i=this.panel.get("boundingBox"),n=i.get("winHeight");if(this.panel.shouldResizeFullscreen())return e.setStyle("maxHeight","100%"),e.setStyle("height","auto"),void this.panel.makeResponsive();(t=n<=(t=this.get("maxheight"))?n<=this.get("minheight")?this.get("minheight"):n:t)>this.get("minheight")?this.panel.lockScroll&&!this.panel.lockScroll.isActive()&&this.panel.lockScroll.enableScrollLock(!0):this.panel.lockScroll&&this.panel.lockScroll.isActive()&&this.panel.lockScroll.disableScrollLock(),n=t,e.setStyle("maxHeight",(t-=156)+"px"),(t=(t=i.getStyle("height")).match(/.*px$/)?t.replace(/px$/,""):n)'), content = Y.Node.create('
        ') .append(Y.Node.create('
        ' + this.get('message') + '
        ')) .append(Y.Node.create('
        ') .append(yes)); this.get(BASE).addClass('moodle-dialogue-confirm'); setTimeout((function() { this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); }).bind(this), 100); this.setStdModContent(Y.WidgetStdMod.HEADER, '
        ' + this.get(TITLE) + '
        ', Y.WidgetStdMod.REPLACE); this._closeEvents.push( Y.on('key', this.submit, window, 'down:13', this), yes.on('click', this.submit, this) ); var closeButton = this.get('boundingBox').one('.closebutton'); if (closeButton) { // The close button should act exactly like the 'No' button. this._closeEvents.push( closeButton.on('click', this.submit, this) ); } }, submit: function() { new Y.EventHandle(this._closeEvents).detach(); this.fire('complete'); this.hide(); this.destroy(); } }, { NAME: ALERT_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The title of the alert. * * @attribute title * @type String * @default 'Alert' */ title: { validator: Y.Lang.isString, value: 'Alert' }, /** * The message of the alert. * * @attribute message * @type String * @default 'Confirm' */ message: { validator: Y.Lang.isString, value: 'Confirm' }, /** * The button text to use to accept the alert. * * @attribute yesLabel * @type String * @default 'OK' */ yesLabel: { validator: Y.Lang.isString, setter: function(txt) { if (!txt) { txt = 'OK'; } return txt; }, value: 'OK' } } }); M.core.alert = ALERT; }, '@VERSION@', {"requires": ["moodle-core-notification-dialogue"]}); build/moodle-core-notification-alert/moodle-core-notification-alert-min.js000064400000003154152265472450022775 0ustar00YUI.add("moodle-core-notification-alert",function(i,t){var e;M.core=M.core||{},i.extend(e=function(t){t.closeButton=!1,e.superclass.constructor.apply(this,[t])},M.core.notification.info,{_closeEvents:null,initializer:function(){var t,e;this._closeEvents=[],this.publish("complete"),e=i.Node.create(''),t=i.Node.create('
        ').append(i.Node.create('
        '+this.get("message")+"
        ")).append(i.Node.create('
        ').append(e)),this.get("notificationBase").addClass("moodle-dialogue-confirm"),setTimeout(function(){this.setStdModContent(i.WidgetStdMod.BODY,t,i.WidgetStdMod.REPLACE)}.bind(this),100),this.setStdModContent(i.WidgetStdMod.HEADER,'
        '+this.get("title")+"
        ",i.WidgetStdMod.REPLACE),this._closeEvents.push(i.on("key",this.submit,window,"down:13",this),e.on("click",this.submit,this)),(e=this.get("boundingBox").one(".closebutton"))&&this._closeEvents.push(e.on("click",this.submit,this))},submit:function(){new i.EventHandle(this._closeEvents).detach(),this.fire("complete"),this.hide(),this.destroy()}},{NAME:"Moodle alert",CSS_PREFIX:"moodle-dialogue",ATTRS:{title:{validator:i.Lang.isString,value:"Alert"},message:{validator:i.Lang.isString,value:"Confirm"},yesLabel:{validator:i.Lang.isString,setter:function(t){return t=t||"OK"},value:"OK"}}}),M.core.alert=e},"@VERSION@",{requires:["moodle-core-notification-dialogue"]});build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js000064400000010262152265472450023276 0ustar00YUI.add('moodle-core-notification-alert', function (Y, NAME) { /* eslint-disable no-unused-vars, no-unused-expressions */ var DIALOGUE_PREFIX, BASE, CONFIRMYES, CONFIRMNO, TITLE, QUESTION, CSS_CLASSES; DIALOGUE_PREFIX = 'moodle-dialogue'; BASE = 'notificationBase'; CONFIRMYES = 'yesLabel'; CONFIRMNO = 'noLabel'; TITLE = 'title'; QUESTION = 'question'; CSS_CLASSES = { BASE: 'moodle-dialogue-base', WRAP: 'moodle-dialogue-wrap', HEADER: 'moodle-dialogue-hd', BODY: 'moodle-dialogue-bd', CONTENT: 'moodle-dialogue-content', FOOTER: 'moodle-dialogue-ft', HIDDEN: 'hidden', LIGHTBOX: 'moodle-dialogue-lightbox' }; // Set up the namespace once. M.core = M.core || {}; /** * A dialogue type designed to display an alert to the user. * * @module moodle-core-notification * @submodule moodle-core-notification-alert */ var ALERT_NAME = 'Moodle alert', ALERT; /** * Extends core Dialogue to show the alert dialogue. * * @param {Object} config Object literal specifying the dialogue configuration properties. * @constructor * @class M.core.alert * @extends M.core.dialogue */ ALERT = function(config) { config.closeButton = false; ALERT.superclass.constructor.apply(this, [config]); }; Y.extend(ALERT, M.core.notification.info, { /** * The list of events to detach when destroying this dialogue. * * @property _closeEvents * @type EventHandle[] * @private */ _closeEvents: null, initializer: function() { this._closeEvents = []; this.publish('complete'); var yes = Y.Node.create(''), content = Y.Node.create('
        ') .append(Y.Node.create('
        ' + this.get('message') + '
        ')) .append(Y.Node.create('
        ') .append(yes)); this.get(BASE).addClass('moodle-dialogue-confirm'); setTimeout((function() { this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE); }).bind(this), 100); this.setStdModContent(Y.WidgetStdMod.HEADER, '
        ' + this.get(TITLE) + '
        ', Y.WidgetStdMod.REPLACE); this._closeEvents.push( Y.on('key', this.submit, window, 'down:13', this), yes.on('click', this.submit, this) ); var closeButton = this.get('boundingBox').one('.closebutton'); if (closeButton) { // The close button should act exactly like the 'No' button. this._closeEvents.push( closeButton.on('click', this.submit, this) ); } }, submit: function() { new Y.EventHandle(this._closeEvents).detach(); this.fire('complete'); this.hide(); this.destroy(); } }, { NAME: ALERT_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { /** * The title of the alert. * * @attribute title * @type String * @default 'Alert' */ title: { validator: Y.Lang.isString, value: 'Alert' }, /** * The message of the alert. * * @attribute message * @type String * @default 'Confirm' */ message: { validator: Y.Lang.isString, value: 'Confirm' }, /** * The button text to use to accept the alert. * * @attribute yesLabel * @type String * @default 'OK' */ yesLabel: { validator: Y.Lang.isString, setter: function(txt) { if (!txt) { txt = 'OK'; } return txt; }, value: 'OK' } } }); M.core.alert = ALERT; }, '@VERSION@', {"requires": ["moodle-core-notification-dialogue"]}); build/moodle-core-blocks/moodle-core-blocks-debug.js000064400000101773152265472450016452 0ustar00YUI.add('moodle-core-blocks', function (Y, NAME) { var MANAGER; var BLOCKREGION; var DRAGBLOCK; /** * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ var AJAXURL = '/lib/ajax/blocks.php', CSS = { BLOCK: 'block', BLOCKREGION: 'block-region', BLOCKADMINBLOCK: 'block_adminblock', EDITINGMOVE: 'editing_move', HEADER: 'header', LIGHTBOX: 'lightbox', REGIONCONTENT: 'region-content', SKIPBLOCK: 'skip-block', SKIPBLOCKTO: 'skip-block-to', MYINDEX: 'page-my-index', REGIONMAIN: 'region-main', BLOCKSMOVING: 'blocks-moving' }; var SELECTOR = { DRAGHANDLE: '.' + CSS.HEADER + ' .commands .moodle-core-dragdrop-draghandle' }; /** * Legacy drag and drop manager. * This drag and drop manager is specifically designed for themes using side-pre and side-post * that do not make use of the block output methods introduced by MDL-39824. * * @namespace M.core.blockdraganddrop * @class LegacyManager * @constructor * @extends M.core.dragdrop */ DRAGBLOCK = function() { DRAGBLOCK.superclass.constructor.apply(this, arguments); }; Y.extend(DRAGBLOCK, M.core.dragdrop, { skipnodetop: null, skipnodebottom: null, dragsourceregion: null, initializer: function() { // Set group for parent class this.groups = ['block']; this.samenodeclass = CSS.BLOCK; this.parentnodeclass = CSS.REGIONCONTENT; // Add relevant classes and ID to 'content' block region on Dashboard page. var myhomecontent = Y.Node.all('body#' + CSS.MYINDEX + ' #' + CSS.REGIONMAIN + ' > .' + CSS.REGIONCONTENT); if (myhomecontent.size() > 0) { var contentregion = myhomecontent.item(0); contentregion.addClass(CSS.BLOCKREGION); contentregion.set('id', CSS.REGIONCONTENT); contentregion.one('div').addClass(CSS.REGIONCONTENT); } // Initialise blocks dragging // Find all block regions on the page var blockregionlist = Y.Node.all('div.' + CSS.BLOCKREGION); if (blockregionlist.size() === 0) { return false; } // See if we are missing either of block regions, // if yes we need to add an empty one to use as target if (blockregionlist.size() !== this.get('regions').length) { var blockregion = Y.Node.create('
        ') .addClass(CSS.BLOCKREGION); var regioncontent = Y.Node.create('
        ') .addClass(CSS.REGIONCONTENT); blockregion.appendChild(regioncontent); var pre = blockregionlist.filter('#region-pre'); var post = blockregionlist.filter('#region-post'); if (pre.size() === 0 && post.size() === 1) { // pre block is missing, instert it before post blockregion.setAttrs({id: 'region-pre'}); post.item(0).insert(blockregion, 'before'); blockregionlist.unshift(blockregion); } else if (post.size() === 0 && pre.size() === 1) { // post block is missing, instert it after pre blockregion.setAttrs({id: 'region-post'}); pre.item(0).insert(blockregion, 'after'); blockregionlist.push(blockregion); } } blockregionlist.each(function(blockregionnode) { // Setting blockregion as droptarget (the case when it is empty) // The region-post (the right one) // is very narrow, so add extra padding on the left to drop block on it. new Y.DD.Drop({ node: blockregionnode.one('div.' + CSS.REGIONCONTENT), groups: this.groups, padding: '40 240 40 240' }); // Make each div element in the list of blocks draggable var del = new Y.DD.Delegate({ container: blockregionnode, nodes: '.' + CSS.BLOCK, target: true, handles: [SELECTOR.DRAGHANDLE], invalid: '.block-hider-hide, .block-hider-show, .moveto', 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.DDWinScroll); var blocklist = blockregionnode.all('.' + CSS.BLOCK); blocklist.each(function(blocknode) { var move = blocknode.one('a.' + CSS.EDITINGMOVE); if (move) { move.replace(this.get_drag_handle(move.getAttribute('title'), '', 'iconsmall', true)); blocknode.one(SELECTOR.DRAGHANDLE).setStyle('cursor', 'move'); } }, this); }, this); }, get_block_id: function(node) { return Number(node.get('id').replace(/inst/i, '')); }, get_block_region: function(node) { var region = node.ancestor('div.' + CSS.BLOCKREGION).get('id').replace(/region-/i, ''); if (Y.Array.indexOf(this.get('regions'), region) === -1) { // Must be standard side-X if (window.right_to_left()) { if (region === 'post') { region = 'pre'; } else if (region === 'pre') { region = 'post'; } } return 'side-' + region; } // Perhaps custom region return region; }, get_region_id: function(node) { return node.get('id').replace(/region-/i, ''); }, drag_start: function(e) { // Get our drag object var drag = e.target; // Store the parent node of original drag node (block) // we will need it later for show/hide empty regions this.dragsourceregion = drag.get('node').ancestor('div.' + CSS.BLOCKREGION); // Determine skipnodes and store them if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) { this.skipnodetop = drag.get('node').previous(); } if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) { this.skipnodebottom = drag.get('node').next(); } // Add the blocks-moving class so that the theme can respond if need be. Y.one('body').addClass(CSS.BLOCKSMOVING); }, drop_over: function(e) { // Get a reference to our drag and drop nodes var drag = e.drag.get('node'); var drop = e.drop.get('node'); // We need to fix the case when parent drop over event has determined // 'goingup' and appended the drag node after admin-block. if (drop.hasClass(this.parentnodeclass) && drop.one('.' + CSS.BLOCKADMINBLOCK) && drop.one('.' + CSS.BLOCKADMINBLOCK).next('.' + CSS.BLOCK)) { drop.prepend(drag); } // Block is moved within the same region // stop here, no need to modify anything. if (this.dragsourceregion.contains(drop)) { return false; } // TODO: Hiding-displaying block region only works for base theme blocks // (region-pre, region-post) at the moment. It should be improved // to work with custom block regions as well. // TODO: Fix this for the case when user drag block towards empty section, // then the section appears, then user chnages his mind and moving back to // original section. The opposite section remains opened and empty. var documentbody = Y.one('body'); // Moving block towards hidden region-content, display it var regionname = this.get_region_id(this.dragsourceregion); if (documentbody.hasClass('side-' + regionname + '-only')) { documentbody.removeClass('side-' + regionname + '-only'); } // Moving from empty region-content towards the opposite one, // hide empty one (only for region-pre, region-post areas at the moment). regionname = this.get_region_id(drop.ancestor('div.' + CSS.BLOCKREGION)); if (this.dragsourceregion.all('.' + CSS.BLOCK).size() === 0 && this.dragsourceregion.get('id').match(/(region-pre|region-post)/i)) { if (!documentbody.hasClass('side-' + regionname + '-only')) { documentbody.addClass('side-' + regionname + '-only'); } } }, drag_end: function() { // clear variables this.skipnodetop = null; this.skipnodebottom = null; this.dragsourceregion = null; // Remove the blocks moving class once the drag-drop is over. Y.one('body').removeClass(CSS.BLOCKSMOVING); }, 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); }, drop_hit: function(e) { var drag = e.drag; // Get a reference to our drag node var dragnode = drag.get('node'); var dropnode = e.drop.get('node'); // Amend existing skipnodes if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) { // the one that belongs to block below move below dragnode.insert(dragnode.previous(), 'after'); } // Move original skipnodes if (this.skipnodetop) { dragnode.insert(this.skipnodetop, 'before'); } if (this.skipnodebottom) { dragnode.insert(this.skipnodebottom, 'after'); } // Add lightbox if it not there var lightbox = M.util.add_lightbox(Y, dragnode); // Prepare request parameters var params = { sesskey: M.cfg.sesskey, pagehash: this.get('pagehash'), action: 'move', bui_moveid: this.get_block_id(dragnode), bui_newregion: this.get_block_region(dropnode) }; if (this.get('cmid')) { params.cmid = this.get('cmid'); } if (dragnode.next('.' + this.samenodeclass) && !dragnode.next('.' + this.samenodeclass).hasClass(CSS.BLOCKADMINBLOCK)) { params.bui_beforeid = this.get_block_id(dragnode.next('.' + this.samenodeclass)); } // Do AJAX request Y.io(M.cfg.wwwroot + AJAXURL, { method: 'POST', data: params, on: { start: function() { lightbox.show(); }, success: function(tid, response) { window.setTimeout(function() { lightbox.hide(); }, 250); try { var responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); } }, context: this }); } }, { NAME: 'core-blocks-dragdrop', ATTRS: { pagehash: { value: null }, regions: { value: null } } }); M.core = M.core || {}; M.core.blockdraganddrop = M.core.blockdraganddrop || {}; /** * True if the page is using the new blocks methods. * @private * @static * @property M.core.blockdraganddrop._isusingnewblocksmethod * @type Boolean * @default null */ M.core.blockdraganddrop._isusingnewblocksmethod = null; /** * Returns true if the page is using the new blocks methods. * @static * @method M.core.blockdraganddrop.is_using_blocks_render_method * @return Boolean */ M.core.blockdraganddrop.is_using_blocks_render_method = function() { if (this._isusingnewblocksmethod === null) { var goodregions = Y.all('.block-region[data-blockregion]').size(); var allregions = Y.all('.block-region').size(); this._isusingnewblocksmethod = (allregions === goodregions); if (goodregions > 0 && allregions > 0 && goodregions !== allregions) { Y.log('Both core_renderer::blocks and core_renderer::blocks_for_region have been used.', 'warn', 'moodle-core_blocks'); } } return this._isusingnewblocksmethod; }; /** * Initialises a drag and drop manager. * This should only ever be called once for a page. * @static * @method M.core.blockdraganddrop.init * @param {Object} params * @return Manager */ M.core.blockdraganddrop.init = function(params) { if (this.is_using_blocks_render_method()) { Y.log('Block drag and drop initialised for the blocks method.', 'info', 'moodle-core_blocks'); new MANAGER(params); } else { Y.log('Block drag and drop initialised with the legacy manager (blocks_for_region used).', 'info', 'moodle-core_blocks'); new DRAGBLOCK(params); } }; /* * Legacy code to keep things working. */ M.core_blocks = M.core_blocks || {}; M.core_blocks.init_dragdrop = function(params) { M.core.blockdraganddrop.init(params); }; /** * This file contains the drag and drop manager class. * * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ /** * Constructs a new Block drag and drop manager. * * @namespace M.core.blockdraganddrop * @class Manager * @constructor * @extends M.core.dragdrop */ MANAGER = function() { MANAGER.superclass.constructor.apply(this, arguments); }; MANAGER.prototype = { /** * The skip block link from above the block being dragged while a drag is in progress. * Required by the M.core.dragdrop from whom this class extends. * @private * @property skipnodetop * @type Node * @default null */ skipnodetop: null, /** * The skip block link from below the block being dragged while a drag is in progress. * Required by the M.core.dragdrop from whom this class extends. * @private * @property skipnodebottom * @type Node * @default null */ skipnodebottom: null, /** * An associative object of regions and the * @property regionobjects * @type {Object} Primitive object mocking an associative array. * @type {BLOCKREGION} [regionname]* Each item uses the region name as the key with the value being * an instance of the BLOCKREGION class. */ regionobjects: {}, /** * Called during the initialisation process of the object. * @method initializer */ initializer: function() { Y.log('Initialising drag and drop for blocks.', 'info'); var regionnames = this.get('regions'); var i = 0; var dragContainer; var dragdelegation; var region; var regionContainer; var regionname; // Evil required by M.core.dragdrop. this.groups = ['block']; this.samenodeclass = CSS.BLOCK; this.parentnodeclass = CSS.BLOCKREGION; // Detect the direction of travel. this.detectkeyboarddirection = true; // Add relevant classes and ID to 'content' block region on Dashboard page. var myhomecontent = Y.Node.all('body#' + CSS.MYINDEX + ' #' + CSS.REGIONMAIN + ' > .' + CSS.REGIONCONTENT); if (myhomecontent.size() > 0) { var contentregion = myhomecontent.item(0); contentregion.addClass(CSS.BLOCKREGION); contentregion.set('id', CSS.REGIONCONTENT); contentregion.one('div').addClass(CSS.REGIONCONTENT); } for (i in regionnames) { regionname = regionnames[i]; regionContainer = Y.one('#block-region-' + regionname); region = new BLOCKREGION({ manager: this, region: regionname, node: regionContainer, }); this.regionobjects[regionname] = region; // Setting blockregion as droptarget (the case when it is empty) // The region-post (the right one) // is very narrow, so add extra padding on the left to drop block on it. new Y.DD.Drop({ node: region.get_droptarget(), groups: this.groups, padding: '40 240 40 240' }); // Make each div element in the list of blocks draggable dragdelegation = new Y.DD.Delegate({ container: region.get_droptarget(), nodes: '.' + CSS.BLOCK, target: true, handles: [SELECTOR.DRAGHANDLE], invalid: '.block-hider-hide, .block-hider-show, .moveto, .block_fake', dragConfig: {groups: this.groups} }); dragdelegation.dd.plug(Y.Plugin.DDProxy, { // Don't move the node at the end of the drag moveOnEnd: false }); if (regionContainer === null) { dragdelegation.dd.plug(Y.Plugin.DDWinScroll); } else { dragContainer = regionContainer.ancestor('.drag-container', true); if (dragContainer) { dragdelegation.dd.plug(Y.Plugin.DDNodeScroll, { node: dragContainer, }); } else { dragdelegation.dd.plug(Y.Plugin.DDWinScroll); } } // On the DD Manager start operation, we enable all block regions so that they can be drop targets. This // must be done *before* drag:start but after dragging has been initialised. Y.DD.DDM.on('ddm:start', this.enable_all_regions, this); region.change_block_move_icons(this); } Y.log('Initialisation of drag and drop for blocks complete.', 'info'); }, /** * Returns the ID of the block the given node represents. * @method get_block_id * @param {Node} node * @return {int} The blocks ID in the database. */ get_block_id: function(node) { return Number(node.get('id').replace(/inst/i, '')); }, /** * Returns the block region that the node is part of or belonging to. * @method get_block_region * @param {Y.Node} node * @return {string} The region name. */ get_block_region: function(node) { if (!node.test('[data-blockregion]')) { node = node.ancestor('[data-blockregion]'); } return node.getData('blockregion'); }, /** * Returns the BLOCKREGION instance that represents the block region the given node is part of. * @method get_region_object * @param {Y.Node} node * @return {BLOCKREGION} */ get_region_object: function(node) { return this.regionobjects[this.get_block_region(node)]; }, /** * Enables all fo the regions so that they are all visible while dragging is occuring. * * @method enable_all_regions */ enable_all_regions: function() { var groups = Y.DD.DDM.activeDrag.get('groups'); // As we're called by Y.DD.DDM, we can't be certain that the call // relates specifically to a block drag/drop operation. Test // whether the relevant group applies here. if (!groups || Y.Array.indexOf(groups, 'block') === -1) { return; } var i; for (i in this.regionobjects) { if (!this.regionobjects.hasOwnProperty(i)) { continue; } this.regionobjects[i].enable(); } }, /** * Disables enabled regions if they contain no blocks. * @method disable_regions_if_required */ disable_regions_if_required: function() { var i = 0; for (i in this.regionobjects) { this.regionobjects[i].disable_if_required(); } }, /** * Called by M.core.dragdrop.global_drag_start when dragging starts. * @method drag_start * @param {Event} e */ drag_start: function(e) { // Get our drag object var drag = e.target; // Store the parent node of original drag node (block) // we will need it later for show/hide empty regions // Determine skipnodes and store them if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) { this.skipnodetop = drag.get('node').previous(); } if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) { this.skipnodebottom = drag.get('node').next(); } }, dragOver: function(e) { var nearestRegion = e.drop.get('node').ancestor('.drag-container', true); if (nearestRegion) { if (e.drag[Y.Plugin.DDNodeScroll]) { if (e.drag[Y.Plugin.DDNodeScroll].get('node') === nearestRegion) { // Do not bother resetting the region - it has not changed. return; } else { e.drag.unplug(Y.Plugin.DDNodeScroll); } } e.drag.plug(Y.Plugin.DDNodeScroll, { node: nearestRegion, }); } }, /** * Called by M.core.dragdrop.global_drop_over when something is dragged over a drop target. * @method drop_over * @param {Event} e */ drop_over: function(e) { // Get a reference to our drag and drop nodes var drag = e.drag.get('node'); var drop = e.drop.get('node'); // We need to fix the case when parent drop over event has determined // 'goingup' and appended the drag node after admin-block. if (drop.hasClass(CSS.REGIONCONTENT) && drop.one('.' + CSS.BLOCKADMINBLOCK) && drop.one('.' + CSS.BLOCKADMINBLOCK).next('.' + CSS.BLOCK)) { drop.prepend(drag); } }, /** * Called by M.core.dragdrop.global_drop_end when a drop has been completed. * @method drop_end */ drop_end: function() { // Clear variables. this.skipnodetop = null; this.skipnodebottom = null; this.disable_regions_if_required(); }, /** * Called by M.core.dragdrop.global_drag_dropmiss when something has been dropped on a node that isn't contained by * a drop target. * * @method drag_dropmiss * @param {Event} e */ 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); }, /** * Called by M.core.dragdrop.global_drag_hit when something has been dropped on a drop target. * @method drop_hit * @param {Event} e */ drop_hit: function(e) { // Get a reference to our drag node var dragnode = e.drag.get('node'); var dropnode = e.drop.get('node'); // Amend existing skipnodes if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) { // the one that belongs to block below move below dragnode.insert(dragnode.previous(), 'after'); } // Move original skipnodes if (this.skipnodetop) { dragnode.insert(this.skipnodetop, 'before'); } if (this.skipnodebottom) { dragnode.insert(this.skipnodebottom, 'after'); } // Add lightbox if it not there var lightbox = M.util.add_lightbox(Y, dragnode); // Prepare request parameters var params = { sesskey: M.cfg.sesskey, pagehash: this.get('pagehash'), action: 'move', bui_moveid: this.get_block_id(dragnode), bui_newregion: this.get_block_region(dropnode) }; if (this.get('cmid')) { params.cmid = this.get('cmid'); } if (dragnode.next('.' + CSS.BLOCK) && !dragnode.next('.' + CSS.BLOCK).hasClass(CSS.BLOCKADMINBLOCK)) { params.bui_beforeid = this.get_block_id(dragnode.next('.' + CSS.BLOCK)); } // Do AJAX request Y.io(M.cfg.wwwroot + AJAXURL, { method: 'POST', data: params, on: { start: function() { lightbox.show(); }, success: function(tid, response) { window.setTimeout(function() { lightbox.hide(); }, 250); try { var responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); }, complete: function() { this.disable_regions_if_required(); } }, context: this }); } }; Y.extend(MANAGER, M.core.dragdrop, MANAGER.prototype, { NAME: 'core-blocks-dragdrop-manager', ATTRS: { /** * The page identifier. * @attribute pagehash * @type string|null * @default null */ pagehash: { value: null }, /** * An array of block regions that are present on the page. * @attribute regions * @type array|null * @default Array[] */ regions: { value: [] } } }); /** * This file contains the Block Region class used by the drag and drop manager. * * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ /** * Constructs a new block region object. * * @namespace M.core.blockdraganddrop * @class BlockRegion * @constructor * @extends Base */ BLOCKREGION = function() { BLOCKREGION.superclass.constructor.apply(this, arguments); }; BLOCKREGION.prototype = { /** * Called during the initialisation process of the object. * @method initializer */ initializer: function() { var node = this.get('node'); Y.log('Block region `' + this.get('region') + '` initialising', 'info'); if (!node) { Y.log('block region known about but no HTML structure found for it. Guessing structure.', 'warn'); node = this.create_and_add_node(); } var body = Y.one('body'), hasblocks = node.all('.' + CSS.BLOCK).size() > 0, hasregionclass = this.get_has_region_class(); this.set('hasblocks', hasblocks); if (!body.hasClass(hasregionclass)) { body.addClass(hasregionclass); } body.addClass((hasblocks) ? this.get_used_region_class() : this.get_empty_region_class()); body.removeClass((hasblocks) ? this.get_empty_region_class() : this.get_used_region_class()); }, /** * Creates a generic block region node and adds it to the DOM at the best guess location. * Any calling of this method is an unfortunate circumstance. * @method create_and_add_node * @return Node The newly created Node */ create_and_add_node: function() { var c = Y.Node.create, region = this.get('region'), node = c('
        ') .addClass(CSS.BLOCKREGION) .setData('blockregion', region), regions = this.get('manager').get('regions'), i, haspre = false, haspost = false, added = false, pre, post; for (i in regions) { if (regions[i].match(/(pre|left)/)) { haspre = regions[i]; } else if (regions[i].match(/(post|right)/)) { haspost = regions[i]; } } if (haspre !== false && haspost !== false) { if (region === haspre) { post = Y.one('#block-region-' + haspost); if (post) { post.insert(node, 'before'); added = true; } } else { pre = Y.one('#block-region-' + haspre); if (pre) { pre.insert(node, 'after'); added = true; } } } if (added === false) { Y.one('body').append(node); } this.set('node', node); return node; }, /** * Change the move icons to enhanced drag handles and changes the cursor to a move icon when over the header. * @param M.core.dragdrop the block manager * @method change_block_move_icons */ change_block_move_icons: function(manager) { var handle; this.get('node').all('.' + CSS.BLOCK + ' a.' + CSS.EDITINGMOVE).each(function(moveicon) { moveicon.setStyle('cursor', 'move'); handle = manager.get_drag_handle(moveicon.getAttribute('title'), '', 'icon', true); // Dragdrop module assigns this with a button role by default. // However, the block move icon is part of a menubar, so it should have a menuitem role. handle.setAttribute('role', 'menuitem'); moveicon.replace(handle); }); }, /** * Returns the class name on the body that signifies the document knows about this region. * @method get_has_region_class * @return String */ get_has_region_class: function() { return 'has-region-' + this.get('region'); }, /** * Returns the class name to use on the body if the region contains no blocks. * @method get_empty_region_class * @return String */ get_empty_region_class: function() { return 'empty-region-' + this.get('region'); }, /** * Returns the class name to use on the body if the region contains blocks. * @method get_used_region_class * @return String */ get_used_region_class: function() { return 'used-region-' + this.get('region'); }, /** * Returns the node to use as the drop target for this region. * @method get_droptarget * @return Node */ get_droptarget: function() { var node = this.get('node'); if (node.test('[data-droptarget="1"]')) { return node; } return node.one('[data-droptarget="1"]'); }, /** * Enables the block region so that we can be sure the user can see it. * This is done even if it is empty. * @method enable */ enable: function() { Y.one('body').addClass(this.get_used_region_class()).removeClass(this.get_empty_region_class()); }, /** * Disables the region if it contains no blocks, essentially hiding it from the user. * @method disable_if_required */ disable_if_required: function() { if (this.get('node').all('.' + CSS.BLOCK).size() === 0) { Y.one('body').addClass(this.get_empty_region_class()).removeClass(this.get_used_region_class()); } } }; Y.extend(BLOCKREGION, Y.Base, BLOCKREGION.prototype, { NAME: 'core-blocks-dragdrop-blockregion', ATTRS: { /** * The drag and drop manager that created this block region instance. * @attribute manager * @type M.core.blockdraganddrop.Manager * @writeOnce */ manager: { // Can only be set during initialisation and must be set then. writeOnce: 'initOnly', validator: function(value) { return Y.Lang.isObject(value) && value instanceof MANAGER; } }, /** * The name of the block region this object represents. * @attribute region * @type String * @writeOnce */ region: { // Can only be set during initialisation and must be set then. writeOnce: 'initOnly', validator: function(value) { return Y.Lang.isString(value); } }, /** * The node the block region HTML starts at.s * @attribute region * @type Y.Node */ node: { validator: function(value) { return Y.Lang.isObject(value) || Y.Lang.isNull(value); } }, /** * True if the block region currently contains blocks. * @attribute hasblocks * @type Boolean * @default false */ hasblocks: { value: false, validator: function(value) { return Y.Lang.isBoolean(value); } } } }); }, '@VERSION@', { "requires": [ "base", "node", "io", "dom", "dd", "dd-scroll", "moodle-core-dragdrop", "moodle-core-notification" ] }); build/moodle-core-blocks/moodle-core-blocks-min.js000064400000024320152265472450016137 0ustar00YUI.add("moodle-core-blocks",function(d,e){var o,a,r="/lib/ajax/blocks.php",g="block",l="block-region",s="block_adminblock",n="editing_move",h="header",c="region-content",t="skip-block",p="skip-block-to",_="page-my-index",b="region-main",f="blocks-moving",u={DRAGHANDLE:"."+h+" .commands .moodle-core-dragdrop-draghandle"},i=function(){i.superclass.constructor.apply(this,arguments)};d.extend(i,M.core.dragdrop,{skipnodetop:null,skipnodebottom:null,dragsourceregion:null,initializer:function(){var e,o,t,i;if(this.groups=["block"],this.samenodeclass=g,this.parentnodeclass=c,0<(e=d.Node.all("body#"+_+" #"+b+" > ."+c)).size()&&((e=e.item(0)).addClass(l),e.set("id",c),e.one("div").addClass(c)),0===(e=d.Node.all("div."+l)).size())return!1;e.size()!==this.get("regions").length&&(o=d.Node.create("
        ").addClass(l),t=d.Node.create("
        ").addClass(c),o.appendChild(t),t=e.filter("#region-pre"),i=e.filter("#region-post"),0===t.size()&&1===i.size()?(o.setAttrs({id:"region-pre"}),i.item(0).insert(o,"before"),e.unshift(o)):0===i.size()&&1===t.size()&&(o.setAttrs({id:"region-post"}),t.item(0).insert(o,"after"),e.push(o))),e.each(function(e){var o;new d.DD.Drop({node:e.one("div."+c),groups:this.groups,padding:"40 240 40 240"}),(o=new d.DD.Delegate({container:e,nodes:"."+g,target:!0,handles:[u.DRAGHANDLE],invalid:".block-hider-hide, .block-hider-show, .moveto",dragConfig:{groups:this.groups}})).dd.plug(d.Plugin.DDProxy,{moveOnEnd:!1}),o.dd.plug(d.Plugin.DDWinScroll),e.all("."+g).each(function(e){var o=e.one("a."+n);o&&(o.replace(this.get_drag_handle(o.getAttribute("title"),"","iconsmall",!0)),e.one(u.DRAGHANDLE).setStyle("cursor","move"))},this)},this)},get_block_id:function(e){return Number(e.get("id").replace(/inst/i,""))},get_block_region:function(e){e=e.ancestor("div."+l).get("id").replace(/region-/i,"");return-1===d.Array.indexOf(this.get("regions"),e)?(window.right_to_left()&&("post"===e?e="pre":"pre"===e&&(e="post")),"side-"+e):e},get_region_id:function(e){return e.get("id").replace(/region-/i,"")},drag_start:function(e){e=e.target;this.dragsourceregion=e.get("node").ancestor("div."+l),e.get("node").previous()&&e.get("node").previous().hasClass(t)&&(this.skipnodetop=e.get("node").previous()),e.get("node").next()&&e.get("node").next().hasClass(p)&&(this.skipnodebottom=e.get("node").next()),d.one("body").addClass(f)},drop_over:function(e){var o,t=e.drag.get("node"),e=e.drop.get("node");if(e.hasClass(this.parentnodeclass)&&e.one("."+s)&&e.one("."+s).next("."+g)&&e.prepend(t),this.dragsourceregion.contains(e))return!1;t=d.one("body"),o=this.get_region_id(this.dragsourceregion),t.hasClass("side-"+o+"-only")&&t.removeClass("side-"+o+"-only"),o=this.get_region_id(e.ancestor("div."+l)),0===this.dragsourceregion.all("."+g).size()&&this.dragsourceregion.get("id").match(/(region-pre|region-post)/i)&&(t.hasClass("side-"+o+"-only")||t.addClass("side-"+o+"-only"))},drag_end:function(){this.skipnodetop=null,this.skipnodebottom=null,this.dragsourceregion=null,d.one("body").removeClass(f)},drag_dropmiss:function(e){this.drop_hit(e)},drop_hit:function(i){var n,e=i.drag,e=e.get("node"),o=i.drop.get("node");e.previous()&&e.previous().hasClass(t)&&e.insert(e.previous(),"after"),this.skipnodetop&&e.insert(this.skipnodetop,"before"),this.skipnodebottom&&e.insert(this.skipnodebottom,"after"),n=M.util.add_lightbox(d,e),o={sesskey:M.cfg.sesskey,pagehash:this.get("pagehash"),action:"move",bui_moveid:this.get_block_id(e),bui_newregion:this.get_block_region(o)},this.get("cmid")&&(o.cmid=this.get("cmid")),e.next("."+this.samenodeclass)&&!e.next("."+this.samenodeclass).hasClass(s)&&(o.bui_beforeid=this.get_block_id(e.next("."+this.samenodeclass))),d.io(M.cfg.wwwroot+r,{method:"POST",data:o,on:{start:function(){n.show()},success:function(e,o){window.setTimeout(function(){n.hide()},250);try{var t=d.JSON.parse(o.responseText);t.error&&new M.core.ajaxException(t)}catch(i){}},failure:function(e,o){this.ajax_failure(o),n.hide()}},context:this})}},{NAME:"core-blocks-dragdrop",ATTRS:{pagehash:{value:null},regions:{value:null}}}),M.core=M.core||{},M.core.blockdraganddrop=M.core.blockdraganddrop||{},M.core.blockdraganddrop._isusingnewblocksmethod=null,M.core.blockdraganddrop.is_using_blocks_render_method=function(){var e,o;return null===this._isusingnewblocksmethod&&(e=d.all(".block-region[data-blockregion]").size(),o=d.all(".block-region").size(),this._isusingnewblocksmethod=o===e),this._isusingnewblocksmethod},M.core.blockdraganddrop.init=function(e){new(this.is_using_blocks_render_method()?o:i)(e)},M.core_blocks=M.core_blocks||{},M.core_blocks.init_dragdrop=function(e){M.core.blockdraganddrop.init(e)},(o=function(){o.superclass.constructor.apply(this,arguments)}).prototype={skipnodetop:null,skipnodebottom:null,regionobjects:{},initializer:function(){var e,o,t,i,n,s=this.get("regions"),r=0;for(r in this.groups=["block"],this.samenodeclass=g,this.parentnodeclass=l,this.detectkeyboarddirection=!0,0<(n=d.Node.all("body#"+_+" #"+b+" > ."+c)).size()&&((n=n.item(0)).addClass(l),n.set("id",c),n.one("div").addClass(c)),s)i=s[r],t=d.one("#block-region-"+i),o=new a({manager:this,region:i,node:t}),this.regionobjects[i]=o,new d.DD.Drop({node:o.get_droptarget(),groups:this.groups,padding:"40 240 40 240"}),(i=new d.DD.Delegate({container:o.get_droptarget(),nodes:"."+g,target:!0,handles:[u.DRAGHANDLE],invalid:".block-hider-hide, .block-hider-show, .moveto, .block_fake",dragConfig:{groups:this.groups}})).dd.plug(d.Plugin.DDProxy,{moveOnEnd:!1}),null!==t&&(e=t.ancestor(".drag-container",!0))?i.dd.plug(d.Plugin.DDNodeScroll,{node:e}):i.dd.plug(d.Plugin.DDWinScroll),d.DD.DDM.on("ddm:start",this.enable_all_regions,this),o.change_block_move_icons(this)},get_block_id:function(e){return Number(e.get("id").replace(/inst/i,""))},get_block_region:function(e){return(e=e.test("[data-blockregion]")?e:e.ancestor("[data-blockregion]")).getData("blockregion")},get_region_object:function(e){return this.regionobjects[this.get_block_region(e)]}, enable_all_regions:function(){var e,o=d.DD.DDM.activeDrag.get("groups");if(o&&-1!==d.Array.indexOf(o,"block"))for(e in this.regionobjects)this.regionobjects.hasOwnProperty(e)&&this.regionobjects[e].enable()},disable_regions_if_required:function(){var e=0;for(e in this.regionobjects)this.regionobjects[e].disable_if_required()},drag_start:function(e){e=e.target;e.get("node").previous()&&e.get("node").previous().hasClass(t)&&(this.skipnodetop=e.get("node").previous()),e.get("node").next()&&e.get("node").next().hasClass(p)&&(this.skipnodebottom=e.get("node").next())},dragOver:function(e){var o=e.drop.get("node").ancestor(".drag-container",!0);if(o){if(e.drag[d.Plugin.DDNodeScroll]){if(e.drag[d.Plugin.DDNodeScroll].get("node")===o)return;e.drag.unplug(d.Plugin.DDNodeScroll)}e.drag.plug(d.Plugin.DDNodeScroll,{node:o})}},drop_over:function(e){var o=e.drag.get("node"),e=e.drop.get("node");e.hasClass(c)&&e.one("."+s)&&e.one("."+s).next("."+g)&&e.prepend(o)},drop_end:function(){this.skipnodetop=null,this.skipnodebottom=null,this.disable_regions_if_required()},drag_dropmiss:function(e){this.drop_hit(e)},drop_hit:function(i){var n,e=i.drag.get("node"),o=i.drop.get("node");e.previous()&&e.previous().hasClass(t)&&e.insert(e.previous(),"after"),this.skipnodetop&&e.insert(this.skipnodetop,"before"),this.skipnodebottom&&e.insert(this.skipnodebottom,"after"),n=M.util.add_lightbox(d,e),o={sesskey:M.cfg.sesskey,pagehash:this.get("pagehash"),action:"move",bui_moveid:this.get_block_id(e),bui_newregion:this.get_block_region(o)},this.get("cmid")&&(o.cmid=this.get("cmid")),e.next("."+g)&&!e.next("."+g).hasClass(s)&&(o.bui_beforeid=this.get_block_id(e.next("."+g))),d.io(M.cfg.wwwroot+r,{method:"POST",data:o,on:{start:function(){n.show()},success:function(e,o){window.setTimeout(function(){n.hide()},250);try{var t=d.JSON.parse(o.responseText);t.error&&new M.core.ajaxException(t)}catch(i){}},failure:function(e,o){this.ajax_failure(o),n.hide()},complete:function(){this.disable_regions_if_required()}},context:this})}},d.extend(o,M.core.dragdrop,o.prototype,{NAME:"core-blocks-dragdrop-manager",ATTRS:{pagehash:{value:null},regions:{value:[]}}}),(a=function(){a.superclass.constructor.apply(this,arguments)}).prototype={initializer:function(){var e=this.get("node"),e=e||this.create_and_add_node(),o=d.one("body"),e=0
        ').addClass(l).setData("blockregion",t),i=this.get("manager").get("regions"),n=!1,s=!1,r=!1;for(e in i)i[e].match(/(pre|left)/)?n=i[e]:i[e].match(/(post|right)/)&&(s=i[e]);return!1!==n&&!1!==s&&(t===n?(t=d.one("#block-region-"+s))&&(t.insert(o,"before"),r=!0):(t=d.one("#block-region-"+n))&&(t.insert(o,"after"),r=!0)),!1===r&&d.one("body").append(o),this.set("node",o),o},change_block_move_icons:function(o){var t;this.get("node").all("."+g+" a."+n).each(function(e){e.setStyle("cursor","move"),(t=o.get_drag_handle(e.getAttribute("title"),"","icon",!0)).setAttribute("role","menuitem"),e.replace(t)})},get_has_region_class:function(){return"has-region-"+this.get("region")},get_empty_region_class:function(){return"empty-region-"+this.get("region")},get_used_region_class:function(){return"used-region-"+this.get("region")},get_droptarget:function(){var e=this.get("node");return e.test('[data-droptarget="1"]')?e:e.one('[data-droptarget="1"]')},enable:function(){d.one("body").addClass(this.get_used_region_class()).removeClass(this.get_empty_region_class())},disable_if_required:function(){0===this.get("node").all("."+g).size()&&d.one("body").addClass(this.get_empty_region_class()).removeClass(this.get_used_region_class())}},d.extend(a,d.Base,a.prototype,{NAME:"core-blocks-dragdrop-blockregion",ATTRS:{manager:{writeOnce:"initOnly",validator:function(e){return d.Lang.isObject(e)&&e instanceof o}},region:{writeOnce:"initOnly",validator:function(e){return d.Lang.isString(e)}},node:{validator:function(e){return d.Lang.isObject(e)||d.Lang.isNull(e)}},hasblocks:{value:!1,validator:function(e){return d.Lang.isBoolean(e)}}}})},"@VERSION@",{requires:["base","node","io","dom","dd","dd-scroll","moodle-core-dragdrop","moodle-core-notification"]});build/moodle-core-blocks/moodle-core-blocks.js000064400000100476152265472450015365 0ustar00YUI.add('moodle-core-blocks', function (Y, NAME) { var MANAGER; var BLOCKREGION; var DRAGBLOCK; /** * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ var AJAXURL = '/lib/ajax/blocks.php', CSS = { BLOCK: 'block', BLOCKREGION: 'block-region', BLOCKADMINBLOCK: 'block_adminblock', EDITINGMOVE: 'editing_move', HEADER: 'header', LIGHTBOX: 'lightbox', REGIONCONTENT: 'region-content', SKIPBLOCK: 'skip-block', SKIPBLOCKTO: 'skip-block-to', MYINDEX: 'page-my-index', REGIONMAIN: 'region-main', BLOCKSMOVING: 'blocks-moving' }; var SELECTOR = { DRAGHANDLE: '.' + CSS.HEADER + ' .commands .moodle-core-dragdrop-draghandle' }; /** * Legacy drag and drop manager. * This drag and drop manager is specifically designed for themes using side-pre and side-post * that do not make use of the block output methods introduced by MDL-39824. * * @namespace M.core.blockdraganddrop * @class LegacyManager * @constructor * @extends M.core.dragdrop */ DRAGBLOCK = function() { DRAGBLOCK.superclass.constructor.apply(this, arguments); }; Y.extend(DRAGBLOCK, M.core.dragdrop, { skipnodetop: null, skipnodebottom: null, dragsourceregion: null, initializer: function() { // Set group for parent class this.groups = ['block']; this.samenodeclass = CSS.BLOCK; this.parentnodeclass = CSS.REGIONCONTENT; // Add relevant classes and ID to 'content' block region on Dashboard page. var myhomecontent = Y.Node.all('body#' + CSS.MYINDEX + ' #' + CSS.REGIONMAIN + ' > .' + CSS.REGIONCONTENT); if (myhomecontent.size() > 0) { var contentregion = myhomecontent.item(0); contentregion.addClass(CSS.BLOCKREGION); contentregion.set('id', CSS.REGIONCONTENT); contentregion.one('div').addClass(CSS.REGIONCONTENT); } // Initialise blocks dragging // Find all block regions on the page var blockregionlist = Y.Node.all('div.' + CSS.BLOCKREGION); if (blockregionlist.size() === 0) { return false; } // See if we are missing either of block regions, // if yes we need to add an empty one to use as target if (blockregionlist.size() !== this.get('regions').length) { var blockregion = Y.Node.create('
        ') .addClass(CSS.BLOCKREGION); var regioncontent = Y.Node.create('
        ') .addClass(CSS.REGIONCONTENT); blockregion.appendChild(regioncontent); var pre = blockregionlist.filter('#region-pre'); var post = blockregionlist.filter('#region-post'); if (pre.size() === 0 && post.size() === 1) { // pre block is missing, instert it before post blockregion.setAttrs({id: 'region-pre'}); post.item(0).insert(blockregion, 'before'); blockregionlist.unshift(blockregion); } else if (post.size() === 0 && pre.size() === 1) { // post block is missing, instert it after pre blockregion.setAttrs({id: 'region-post'}); pre.item(0).insert(blockregion, 'after'); blockregionlist.push(blockregion); } } blockregionlist.each(function(blockregionnode) { // Setting blockregion as droptarget (the case when it is empty) // The region-post (the right one) // is very narrow, so add extra padding on the left to drop block on it. new Y.DD.Drop({ node: blockregionnode.one('div.' + CSS.REGIONCONTENT), groups: this.groups, padding: '40 240 40 240' }); // Make each div element in the list of blocks draggable var del = new Y.DD.Delegate({ container: blockregionnode, nodes: '.' + CSS.BLOCK, target: true, handles: [SELECTOR.DRAGHANDLE], invalid: '.block-hider-hide, .block-hider-show, .moveto', 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.DDWinScroll); var blocklist = blockregionnode.all('.' + CSS.BLOCK); blocklist.each(function(blocknode) { var move = blocknode.one('a.' + CSS.EDITINGMOVE); if (move) { move.replace(this.get_drag_handle(move.getAttribute('title'), '', 'iconsmall', true)); blocknode.one(SELECTOR.DRAGHANDLE).setStyle('cursor', 'move'); } }, this); }, this); }, get_block_id: function(node) { return Number(node.get('id').replace(/inst/i, '')); }, get_block_region: function(node) { var region = node.ancestor('div.' + CSS.BLOCKREGION).get('id').replace(/region-/i, ''); if (Y.Array.indexOf(this.get('regions'), region) === -1) { // Must be standard side-X if (window.right_to_left()) { if (region === 'post') { region = 'pre'; } else if (region === 'pre') { region = 'post'; } } return 'side-' + region; } // Perhaps custom region return region; }, get_region_id: function(node) { return node.get('id').replace(/region-/i, ''); }, drag_start: function(e) { // Get our drag object var drag = e.target; // Store the parent node of original drag node (block) // we will need it later for show/hide empty regions this.dragsourceregion = drag.get('node').ancestor('div.' + CSS.BLOCKREGION); // Determine skipnodes and store them if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) { this.skipnodetop = drag.get('node').previous(); } if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) { this.skipnodebottom = drag.get('node').next(); } // Add the blocks-moving class so that the theme can respond if need be. Y.one('body').addClass(CSS.BLOCKSMOVING); }, drop_over: function(e) { // Get a reference to our drag and drop nodes var drag = e.drag.get('node'); var drop = e.drop.get('node'); // We need to fix the case when parent drop over event has determined // 'goingup' and appended the drag node after admin-block. if (drop.hasClass(this.parentnodeclass) && drop.one('.' + CSS.BLOCKADMINBLOCK) && drop.one('.' + CSS.BLOCKADMINBLOCK).next('.' + CSS.BLOCK)) { drop.prepend(drag); } // Block is moved within the same region // stop here, no need to modify anything. if (this.dragsourceregion.contains(drop)) { return false; } // TODO: Hiding-displaying block region only works for base theme blocks // (region-pre, region-post) at the moment. It should be improved // to work with custom block regions as well. // TODO: Fix this for the case when user drag block towards empty section, // then the section appears, then user chnages his mind and moving back to // original section. The opposite section remains opened and empty. var documentbody = Y.one('body'); // Moving block towards hidden region-content, display it var regionname = this.get_region_id(this.dragsourceregion); if (documentbody.hasClass('side-' + regionname + '-only')) { documentbody.removeClass('side-' + regionname + '-only'); } // Moving from empty region-content towards the opposite one, // hide empty one (only for region-pre, region-post areas at the moment). regionname = this.get_region_id(drop.ancestor('div.' + CSS.BLOCKREGION)); if (this.dragsourceregion.all('.' + CSS.BLOCK).size() === 0 && this.dragsourceregion.get('id').match(/(region-pre|region-post)/i)) { if (!documentbody.hasClass('side-' + regionname + '-only')) { documentbody.addClass('side-' + regionname + '-only'); } } }, drag_end: function() { // clear variables this.skipnodetop = null; this.skipnodebottom = null; this.dragsourceregion = null; // Remove the blocks moving class once the drag-drop is over. Y.one('body').removeClass(CSS.BLOCKSMOVING); }, 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); }, drop_hit: function(e) { var drag = e.drag; // Get a reference to our drag node var dragnode = drag.get('node'); var dropnode = e.drop.get('node'); // Amend existing skipnodes if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) { // the one that belongs to block below move below dragnode.insert(dragnode.previous(), 'after'); } // Move original skipnodes if (this.skipnodetop) { dragnode.insert(this.skipnodetop, 'before'); } if (this.skipnodebottom) { dragnode.insert(this.skipnodebottom, 'after'); } // Add lightbox if it not there var lightbox = M.util.add_lightbox(Y, dragnode); // Prepare request parameters var params = { sesskey: M.cfg.sesskey, pagehash: this.get('pagehash'), action: 'move', bui_moveid: this.get_block_id(dragnode), bui_newregion: this.get_block_region(dropnode) }; if (this.get('cmid')) { params.cmid = this.get('cmid'); } if (dragnode.next('.' + this.samenodeclass) && !dragnode.next('.' + this.samenodeclass).hasClass(CSS.BLOCKADMINBLOCK)) { params.bui_beforeid = this.get_block_id(dragnode.next('.' + this.samenodeclass)); } // Do AJAX request Y.io(M.cfg.wwwroot + AJAXURL, { method: 'POST', data: params, on: { start: function() { lightbox.show(); }, success: function(tid, response) { window.setTimeout(function() { lightbox.hide(); }, 250); try { var responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); } }, context: this }); } }, { NAME: 'core-blocks-dragdrop', ATTRS: { pagehash: { value: null }, regions: { value: null } } }); M.core = M.core || {}; M.core.blockdraganddrop = M.core.blockdraganddrop || {}; /** * True if the page is using the new blocks methods. * @private * @static * @property M.core.blockdraganddrop._isusingnewblocksmethod * @type Boolean * @default null */ M.core.blockdraganddrop._isusingnewblocksmethod = null; /** * Returns true if the page is using the new blocks methods. * @static * @method M.core.blockdraganddrop.is_using_blocks_render_method * @return Boolean */ M.core.blockdraganddrop.is_using_blocks_render_method = function() { if (this._isusingnewblocksmethod === null) { var goodregions = Y.all('.block-region[data-blockregion]').size(); var allregions = Y.all('.block-region').size(); this._isusingnewblocksmethod = (allregions === goodregions); if (goodregions > 0 && allregions > 0 && goodregions !== allregions) { } } return this._isusingnewblocksmethod; }; /** * Initialises a drag and drop manager. * This should only ever be called once for a page. * @static * @method M.core.blockdraganddrop.init * @param {Object} params * @return Manager */ M.core.blockdraganddrop.init = function(params) { if (this.is_using_blocks_render_method()) { new MANAGER(params); } else { new DRAGBLOCK(params); } }; /* * Legacy code to keep things working. */ M.core_blocks = M.core_blocks || {}; M.core_blocks.init_dragdrop = function(params) { M.core.blockdraganddrop.init(params); }; /** * This file contains the drag and drop manager class. * * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ /** * Constructs a new Block drag and drop manager. * * @namespace M.core.blockdraganddrop * @class Manager * @constructor * @extends M.core.dragdrop */ MANAGER = function() { MANAGER.superclass.constructor.apply(this, arguments); }; MANAGER.prototype = { /** * The skip block link from above the block being dragged while a drag is in progress. * Required by the M.core.dragdrop from whom this class extends. * @private * @property skipnodetop * @type Node * @default null */ skipnodetop: null, /** * The skip block link from below the block being dragged while a drag is in progress. * Required by the M.core.dragdrop from whom this class extends. * @private * @property skipnodebottom * @type Node * @default null */ skipnodebottom: null, /** * An associative object of regions and the * @property regionobjects * @type {Object} Primitive object mocking an associative array. * @type {BLOCKREGION} [regionname]* Each item uses the region name as the key with the value being * an instance of the BLOCKREGION class. */ regionobjects: {}, /** * Called during the initialisation process of the object. * @method initializer */ initializer: function() { var regionnames = this.get('regions'); var i = 0; var dragContainer; var dragdelegation; var region; var regionContainer; var regionname; // Evil required by M.core.dragdrop. this.groups = ['block']; this.samenodeclass = CSS.BLOCK; this.parentnodeclass = CSS.BLOCKREGION; // Detect the direction of travel. this.detectkeyboarddirection = true; // Add relevant classes and ID to 'content' block region on Dashboard page. var myhomecontent = Y.Node.all('body#' + CSS.MYINDEX + ' #' + CSS.REGIONMAIN + ' > .' + CSS.REGIONCONTENT); if (myhomecontent.size() > 0) { var contentregion = myhomecontent.item(0); contentregion.addClass(CSS.BLOCKREGION); contentregion.set('id', CSS.REGIONCONTENT); contentregion.one('div').addClass(CSS.REGIONCONTENT); } for (i in regionnames) { regionname = regionnames[i]; regionContainer = Y.one('#block-region-' + regionname); region = new BLOCKREGION({ manager: this, region: regionname, node: regionContainer, }); this.regionobjects[regionname] = region; // Setting blockregion as droptarget (the case when it is empty) // The region-post (the right one) // is very narrow, so add extra padding on the left to drop block on it. new Y.DD.Drop({ node: region.get_droptarget(), groups: this.groups, padding: '40 240 40 240' }); // Make each div element in the list of blocks draggable dragdelegation = new Y.DD.Delegate({ container: region.get_droptarget(), nodes: '.' + CSS.BLOCK, target: true, handles: [SELECTOR.DRAGHANDLE], invalid: '.block-hider-hide, .block-hider-show, .moveto, .block_fake', dragConfig: {groups: this.groups} }); dragdelegation.dd.plug(Y.Plugin.DDProxy, { // Don't move the node at the end of the drag moveOnEnd: false }); if (regionContainer === null) { dragdelegation.dd.plug(Y.Plugin.DDWinScroll); } else { dragContainer = regionContainer.ancestor('.drag-container', true); if (dragContainer) { dragdelegation.dd.plug(Y.Plugin.DDNodeScroll, { node: dragContainer, }); } else { dragdelegation.dd.plug(Y.Plugin.DDWinScroll); } } // On the DD Manager start operation, we enable all block regions so that they can be drop targets. This // must be done *before* drag:start but after dragging has been initialised. Y.DD.DDM.on('ddm:start', this.enable_all_regions, this); region.change_block_move_icons(this); } }, /** * Returns the ID of the block the given node represents. * @method get_block_id * @param {Node} node * @return {int} The blocks ID in the database. */ get_block_id: function(node) { return Number(node.get('id').replace(/inst/i, '')); }, /** * Returns the block region that the node is part of or belonging to. * @method get_block_region * @param {Y.Node} node * @return {string} The region name. */ get_block_region: function(node) { if (!node.test('[data-blockregion]')) { node = node.ancestor('[data-blockregion]'); } return node.getData('blockregion'); }, /** * Returns the BLOCKREGION instance that represents the block region the given node is part of. * @method get_region_object * @param {Y.Node} node * @return {BLOCKREGION} */ get_region_object: function(node) { return this.regionobjects[this.get_block_region(node)]; }, /** * Enables all fo the regions so that they are all visible while dragging is occuring. * * @method enable_all_regions */ enable_all_regions: function() { var groups = Y.DD.DDM.activeDrag.get('groups'); // As we're called by Y.DD.DDM, we can't be certain that the call // relates specifically to a block drag/drop operation. Test // whether the relevant group applies here. if (!groups || Y.Array.indexOf(groups, 'block') === -1) { return; } var i; for (i in this.regionobjects) { if (!this.regionobjects.hasOwnProperty(i)) { continue; } this.regionobjects[i].enable(); } }, /** * Disables enabled regions if they contain no blocks. * @method disable_regions_if_required */ disable_regions_if_required: function() { var i = 0; for (i in this.regionobjects) { this.regionobjects[i].disable_if_required(); } }, /** * Called by M.core.dragdrop.global_drag_start when dragging starts. * @method drag_start * @param {Event} e */ drag_start: function(e) { // Get our drag object var drag = e.target; // Store the parent node of original drag node (block) // we will need it later for show/hide empty regions // Determine skipnodes and store them if (drag.get('node').previous() && drag.get('node').previous().hasClass(CSS.SKIPBLOCK)) { this.skipnodetop = drag.get('node').previous(); } if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) { this.skipnodebottom = drag.get('node').next(); } }, dragOver: function(e) { var nearestRegion = e.drop.get('node').ancestor('.drag-container', true); if (nearestRegion) { if (e.drag[Y.Plugin.DDNodeScroll]) { if (e.drag[Y.Plugin.DDNodeScroll].get('node') === nearestRegion) { // Do not bother resetting the region - it has not changed. return; } else { e.drag.unplug(Y.Plugin.DDNodeScroll); } } e.drag.plug(Y.Plugin.DDNodeScroll, { node: nearestRegion, }); } }, /** * Called by M.core.dragdrop.global_drop_over when something is dragged over a drop target. * @method drop_over * @param {Event} e */ drop_over: function(e) { // Get a reference to our drag and drop nodes var drag = e.drag.get('node'); var drop = e.drop.get('node'); // We need to fix the case when parent drop over event has determined // 'goingup' and appended the drag node after admin-block. if (drop.hasClass(CSS.REGIONCONTENT) && drop.one('.' + CSS.BLOCKADMINBLOCK) && drop.one('.' + CSS.BLOCKADMINBLOCK).next('.' + CSS.BLOCK)) { drop.prepend(drag); } }, /** * Called by M.core.dragdrop.global_drop_end when a drop has been completed. * @method drop_end */ drop_end: function() { // Clear variables. this.skipnodetop = null; this.skipnodebottom = null; this.disable_regions_if_required(); }, /** * Called by M.core.dragdrop.global_drag_dropmiss when something has been dropped on a node that isn't contained by * a drop target. * * @method drag_dropmiss * @param {Event} e */ 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); }, /** * Called by M.core.dragdrop.global_drag_hit when something has been dropped on a drop target. * @method drop_hit * @param {Event} e */ drop_hit: function(e) { // Get a reference to our drag node var dragnode = e.drag.get('node'); var dropnode = e.drop.get('node'); // Amend existing skipnodes if (dragnode.previous() && dragnode.previous().hasClass(CSS.SKIPBLOCK)) { // the one that belongs to block below move below dragnode.insert(dragnode.previous(), 'after'); } // Move original skipnodes if (this.skipnodetop) { dragnode.insert(this.skipnodetop, 'before'); } if (this.skipnodebottom) { dragnode.insert(this.skipnodebottom, 'after'); } // Add lightbox if it not there var lightbox = M.util.add_lightbox(Y, dragnode); // Prepare request parameters var params = { sesskey: M.cfg.sesskey, pagehash: this.get('pagehash'), action: 'move', bui_moveid: this.get_block_id(dragnode), bui_newregion: this.get_block_region(dropnode) }; if (this.get('cmid')) { params.cmid = this.get('cmid'); } if (dragnode.next('.' + CSS.BLOCK) && !dragnode.next('.' + CSS.BLOCK).hasClass(CSS.BLOCKADMINBLOCK)) { params.bui_beforeid = this.get_block_id(dragnode.next('.' + CSS.BLOCK)); } // Do AJAX request Y.io(M.cfg.wwwroot + AJAXURL, { method: 'POST', data: params, on: { start: function() { lightbox.show(); }, success: function(tid, response) { window.setTimeout(function() { lightbox.hide(); }, 250); try { var responsetext = Y.JSON.parse(response.responseText); if (responsetext.error) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); }, complete: function() { this.disable_regions_if_required(); } }, context: this }); } }; Y.extend(MANAGER, M.core.dragdrop, MANAGER.prototype, { NAME: 'core-blocks-dragdrop-manager', ATTRS: { /** * The page identifier. * @attribute pagehash * @type string|null * @default null */ pagehash: { value: null }, /** * An array of block regions that are present on the page. * @attribute regions * @type array|null * @default Array[] */ regions: { value: [] } } }); /** * This file contains the Block Region class used by the drag and drop manager. * * Provides drag and drop functionality for blocks. * * @module moodle-core-blockdraganddrop */ /** * Constructs a new block region object. * * @namespace M.core.blockdraganddrop * @class BlockRegion * @constructor * @extends Base */ BLOCKREGION = function() { BLOCKREGION.superclass.constructor.apply(this, arguments); }; BLOCKREGION.prototype = { /** * Called during the initialisation process of the object. * @method initializer */ initializer: function() { var node = this.get('node'); if (!node) { node = this.create_and_add_node(); } var body = Y.one('body'), hasblocks = node.all('.' + CSS.BLOCK).size() > 0, hasregionclass = this.get_has_region_class(); this.set('hasblocks', hasblocks); if (!body.hasClass(hasregionclass)) { body.addClass(hasregionclass); } body.addClass((hasblocks) ? this.get_used_region_class() : this.get_empty_region_class()); body.removeClass((hasblocks) ? this.get_empty_region_class() : this.get_used_region_class()); }, /** * Creates a generic block region node and adds it to the DOM at the best guess location. * Any calling of this method is an unfortunate circumstance. * @method create_and_add_node * @return Node The newly created Node */ create_and_add_node: function() { var c = Y.Node.create, region = this.get('region'), node = c('
        ') .addClass(CSS.BLOCKREGION) .setData('blockregion', region), regions = this.get('manager').get('regions'), i, haspre = false, haspost = false, added = false, pre, post; for (i in regions) { if (regions[i].match(/(pre|left)/)) { haspre = regions[i]; } else if (regions[i].match(/(post|right)/)) { haspost = regions[i]; } } if (haspre !== false && haspost !== false) { if (region === haspre) { post = Y.one('#block-region-' + haspost); if (post) { post.insert(node, 'before'); added = true; } } else { pre = Y.one('#block-region-' + haspre); if (pre) { pre.insert(node, 'after'); added = true; } } } if (added === false) { Y.one('body').append(node); } this.set('node', node); return node; }, /** * Change the move icons to enhanced drag handles and changes the cursor to a move icon when over the header. * @param M.core.dragdrop the block manager * @method change_block_move_icons */ change_block_move_icons: function(manager) { var handle; this.get('node').all('.' + CSS.BLOCK + ' a.' + CSS.EDITINGMOVE).each(function(moveicon) { moveicon.setStyle('cursor', 'move'); handle = manager.get_drag_handle(moveicon.getAttribute('title'), '', 'icon', true); // Dragdrop module assigns this with a button role by default. // However, the block move icon is part of a menubar, so it should have a menuitem role. handle.setAttribute('role', 'menuitem'); moveicon.replace(handle); }); }, /** * Returns the class name on the body that signifies the document knows about this region. * @method get_has_region_class * @return String */ get_has_region_class: function() { return 'has-region-' + this.get('region'); }, /** * Returns the class name to use on the body if the region contains no blocks. * @method get_empty_region_class * @return String */ get_empty_region_class: function() { return 'empty-region-' + this.get('region'); }, /** * Returns the class name to use on the body if the region contains blocks. * @method get_used_region_class * @return String */ get_used_region_class: function() { return 'used-region-' + this.get('region'); }, /** * Returns the node to use as the drop target for this region. * @method get_droptarget * @return Node */ get_droptarget: function() { var node = this.get('node'); if (node.test('[data-droptarget="1"]')) { return node; } return node.one('[data-droptarget="1"]'); }, /** * Enables the block region so that we can be sure the user can see it. * This is done even if it is empty. * @method enable */ enable: function() { Y.one('body').addClass(this.get_used_region_class()).removeClass(this.get_empty_region_class()); }, /** * Disables the region if it contains no blocks, essentially hiding it from the user. * @method disable_if_required */ disable_if_required: function() { if (this.get('node').all('.' + CSS.BLOCK).size() === 0) { Y.one('body').addClass(this.get_empty_region_class()).removeClass(this.get_used_region_class()); } } }; Y.extend(BLOCKREGION, Y.Base, BLOCKREGION.prototype, { NAME: 'core-blocks-dragdrop-blockregion', ATTRS: { /** * The drag and drop manager that created this block region instance. * @attribute manager * @type M.core.blockdraganddrop.Manager * @writeOnce */ manager: { // Can only be set during initialisation and must be set then. writeOnce: 'initOnly', validator: function(value) { return Y.Lang.isObject(value) && value instanceof MANAGER; } }, /** * The name of the block region this object represents. * @attribute region * @type String * @writeOnce */ region: { // Can only be set during initialisation and must be set then. writeOnce: 'initOnly', validator: function(value) { return Y.Lang.isString(value); } }, /** * The node the block region HTML starts at.s * @attribute region * @type Y.Node */ node: { validator: function(value) { return Y.Lang.isObject(value) || Y.Lang.isNull(value); } }, /** * True if the block region currently contains blocks. * @attribute hasblocks * @type Boolean * @default false */ hasblocks: { value: false, validator: function(value) { return Y.Lang.isBoolean(value); } } } }); }, '@VERSION@', { "requires": [ "base", "node", "io", "dom", "dd", "dd-scroll", "moodle-core-dragdrop", "moodle-core-notification" ] }); build/moodle-core-actionmenu/moodle-core-actionmenu-debug.js000064400000045256152265472450020227 0ustar00YUI.add('moodle-core-actionmenu', function (Y, NAME) { /** * Provides drop down menus for list of action links. * * @module moodle-core-actionmenu */ var BODY = Y.one(Y.config.doc.body), CSS = { MENUSHOWN: 'action-menu-shown' }, SELECTOR = { CAN_RECEIVE_FOCUS_SELECTOR: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]', MENU: '.moodle-actionmenu[data-enhance=moodle-core-actionmenu]', MENUBAR: '[role="menubar"]', MENUITEM: '[role="menuitem"]', MENUCONTENT: '.menu[data-rel=menu-content]', MENUCONTENTCHILD: 'li a', MENUCHILD: '.menu li a', TOGGLE: '.toggle-display', KEEPOPEN: '[data-keepopen="1"]', MENUBARITEMS: [ '[role="menubar"] > [role="menuitem"]', '[role="menubar"] > [role="presentation"] > [role="menuitem"]' ], MENUITEMS: [ '> [role="menuitem"]', '> [role="presentation"] > [role="menuitem"]' ] }, ACTIONMENU, ALIGN = { TL: 'tl', TR: 'tr', BL: 'bl', BR: 'br' }; /** * Action menu support. * This converts a generic list of links into a drop down menu opened by hovering or clicking * on a menu icon. * * @namespace M.core.actionmenu * @class ActionMenu * @constructor * @extends Base */ ACTIONMENU = function() { ACTIONMENU.superclass.constructor.apply(this, arguments); }; ACTIONMENU.prototype = { /** * The dialogue used for all action menu displays. * @property type * @type M.core.dialogue * @protected */ dialogue: null, /** * An array of events attached during the display of the dialogue. * @property events * @type Object * @protected */ events: [], /** * The node that owns the currently displayed menu. * * @property owner * @type Node * @default null */ owner: null, /** * The menu button that toggles this open. * * @property menulink * @type Node * @protected */ menulink: null, /** * The set of menu nodes. * * @property menuChildren * @type NodeList * @protected */ menuChildren: null, /** * The first menu item. * * @property firstMenuChild * @type Node * @protected */ firstMenuChild: null, /** * The last menu item. * * @property lastMenuChild * @type Node * @protected */ lastMenuChild: null, /** * Called during the initialisation process of the object. * * @method initializer */ initializer: function() { Y.log('Initialising the action menu manager', 'debug', ACTIONMENU.NAME); Y.all(SELECTOR.MENU).each(this.enhance, this); BODY.delegate('key', this.moveMenuItem, 'down:37,39', SELECTOR.MENUBARITEMS.join(','), this); BODY.delegate('click', this.toggleMenu, SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); BODY.delegate('key', this.showIfHidden, 'down:enter,38,40', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); // Ensure that we toggle on menuitems when the spacebar is pressed. BODY.delegate('key', function(e) { e.currentTarget.simulate('click'); e.preventDefault(); }, 'down:32', SELECTOR.MENUBARITEMS.join(',')); }, /** * Enhances a menu adding aria attributes and flagging it as functional. * * @method enhance * @param {Node} menu * @return boolean */ enhance: function(menu) { var menucontent = menu.one(SELECTOR.MENUCONTENT), align; if (!menucontent) { return false; } align = menucontent.getData('align') || this.get('align').join('-'); menu.one(SELECTOR.TOGGLE).set('aria-haspopup', true); menucontent.set('aria-hidden', true); if (!menucontent.hasClass('align-' + align)) { menucontent.addClass('align-' + align); } if (menucontent.getDOMNode().childElementCount) { menu.setAttribute('data-enhanced', '1'); } }, /** * Handle movement between menu items in a menubar. * * @method moveMenuItem * @param {EventFacade} e The event generating the move request * @chainable */ moveMenuItem: function(e) { var nextFocus, menuitem = e.target.ancestor(SELECTOR.MENUITEM, true); if (e.keyCode === 37) { nextFocus = this.getMenuItem(menuitem, true); } else if (e.keyCode === 39) { nextFocus = this.getMenuItem(menuitem); } if (nextFocus) { nextFocus.focus(); } return this; }, /** * Get the next menuitem in a menubar. * * @method getMenuItem * @param {Node} currentItem The currently focused item in the menubar * @param {Boolean} [previous=false] Move backwards in the menubar instead of forwards * @return {Node|null} The next item, or null if none was found */ getMenuItem: function(currentItem, previous) { var menubar = currentItem.ancestor(SELECTOR.MENUBAR), menuitems, next; if (!menubar) { return null; } menuitems = menubar.all(SELECTOR.MENUITEMS.join(',')); if (!menuitems) { return null; } var childCount = menuitems.size(); if (childCount === 1) { // Only one item, exit now because we should already be on it. return null; } // Determine the next child. var index = 0, direction = 1, checkCount = 0; // Work out the index of the currently selected item. for (index = 0; index < childCount; index++) { if (menuitems.item(index) === currentItem) { break; } } // Check that the menu item was found - otherwise return null. if (menuitems.item(index) !== currentItem) { return null; } // Reverse the direction if we want the previous item. if (previous) { direction = -1; } do { // Update the index in the direction of travel. index += direction; next = menuitems.item(index); // Check that we don't loop multiple times. checkCount++; } while (next && next.hasAttribute('hidden')); return next; }, /** * Hides the menu if it is visible. * @param {EventFacade} e * @method hideMenu */ hideMenu: function(e) { if (this.dialogue) { Y.log('Hiding an action menu', 'debug', ACTIONMENU.NAME); this.dialogue.removeClass('show'); this.dialogue.one(SELECTOR.MENUCONTENT).set('aria-hidden', true); this.dialogue = null; } for (var i in this.events) { if (this.events[i].detach) { this.events[i].detach(); } } this.events = []; if (this.owner) { this.owner.removeClass(CSS.MENUSHOWN); this.owner = null; } if (this.menulink) { if (e.type != 'click') { this.menulink.focus(); } this.menulink = null; } }, showIfHidden: function(e) { var menu = e.target.ancestor(SELECTOR.MENU), menuvisible = (menu.hasClass('show')); if (!menuvisible) { e.preventDefault(); this.showMenu(e, menu); } return this; }, /** * Toggles the display of the menu. * @method toggleMenu * @param {EventFacade} e */ toggleMenu: function(e) { var menu = e.target.ancestor(SELECTOR.MENU), menuvisible = (menu.hasClass('show')); // Prevent event propagation as it will trigger the hideIfOutside event handler in certain situations. e.halt(true); this.hideMenu(e); if (menuvisible) { // The menu was visible and the user has clicked to toggle it again. return; } this.showMenu(e, menu); }, /** * Handle keyboard events when the menu is open. We respond to: * * escape (exit) * * tab (move to next menu item) * * up/down (move to previous/next menu item) * * @method handleKeyboardEvent * @param {EventFacade} e The key event */ handleKeyboardEvent: function(e) { var next; var markEventHandled = function(e) { e.preventDefault(); e.stopPropagation(); }; // Handle when the menu is still selected. if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) { if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) { this.firstMenuChild.focus(); markEventHandled(e); } else if (e.keyCode === 38 && this.lastMenuChild) { this.lastMenuChild.focus(); markEventHandled(e); } else if (e.keyCode === 9 && e.shiftKey) { this.hideMenu(e); markEventHandled(e); } return this; } if (e.keyCode === 27) { // The escape key was pressed so close the menu. this.hideMenu(e); markEventHandled(e); } else if (e.keyCode === 32) { // The space bar was pressed. Trigger a click. markEventHandled(e); e.currentTarget.simulate('click'); } else if (e.keyCode === 9) { // The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options. // We only override the Shift + Tab on the first option, and Tab on the last option to change where the // focus is moved to. if (e.target === this.firstMenuChild && e.shiftKey) { this.hideMenu(e); markEventHandled(e); } else if (e.target === this.lastMenuChild && !e.shiftKey) { if (this.hideMenu(e)) { // Determine the next selector and focus on it. next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR); if (next) { next.focus(); markEventHandled(e); } } } } else if (e.keyCode === 38 || e.keyCode === 40) { // The up (38) or down (40) key was pushed. // On cursor moves we loops through the menu rather than exiting it as in the tab behaviour. var found = false, index = 0, direction = 1, checkCount = 0; // Determine which menu item is currently selected. while (!found && index < this.menuChildren.size()) { if (this.menuChildren.item(index) === e.currentTarget) { found = true; } else { index++; } } if (!found) { Y.log("Unable to find this menu item in the list of menu children", 'debug', 'moodle-core-actionmenu'); return; } if (e.keyCode === 38) { // Moving up so reverse the direction. direction = -1; } // Try to find the next do { index += direction; if (index < 0) { index = this.menuChildren.size() - 1; } else if (index >= this.menuChildren.size()) { // Handle wrapping. index = 0; } next = this.menuChildren.item(index); // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item. checkCount++; } while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden')); if (next) { next.focus(); markEventHandled(e); } } }, /** * Hides the menu if the event happened outside the menu. * * @protected * @method hideIfOutside * @param {EventFacade} e */ hideIfOutside: function(e) { if (!e.target.ancestor(SELECTOR.MENUCONTENT, true)) { this.hideMenu(e); } }, /** * Displays the menu with the given content and alignment. * * @method showMenu * @param {EventFacade} e * @param {Node} menu * @return M.core.dialogue */ showMenu: function(e, menu) { Y.log('Displaying an action menu', 'debug', ACTIONMENU.NAME); var ownerselector = menu.getData('owner'), menucontent = menu.one(SELECTOR.MENUCONTENT); this.owner = (ownerselector) ? menu.ancestor(ownerselector) : null; this.dialogue = menu; menu.addClass('show'); if (this.owner) { this.owner.addClass(CSS.MENUSHOWN); this.menulink = this.owner.one(SELECTOR.TOGGLE); } else { this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true); } this.constrain(menucontent.set('aria-hidden', false)); this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD); if (this.menuChildren.size() > 0) { this.firstMenuChild = this.menuChildren.item(0); this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1); this.firstMenuChild.focus(); } // Close the menu if the user presses escape. this.events.push(BODY.on('key', this.hideMenu, 'esc', this)); // Close the menu if the user clicks outside the menu. this.events.push(BODY.on('click', this.hideIfOutside, this)); // Close the menu if the user focuses outside the menu. this.events.push(BODY.delegate('focus', this.hideIfOutside, '*', this)); // Check keyboard changes. this.events.push( menu.delegate('key', this.handleKeyboardEvent, 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this) ); // Close the menu after a button was pushed. this.events.push(menu.delegate('click', function(e) { if (e.currentTarget.test(SELECTOR.KEEPOPEN)) { return; } this.hideMenu(e); }, SELECTOR.MENUCHILD, this)); return true; }, /** * Constrains the node to its the page width. * * @method constrain * @param {Node} node */ constrain: function(node) { var selector = node.getData('constraint'), nx = node.getX(), ny = node.getY(), nwidth = node.get('offsetWidth'), nheight = node.get('offsetHeight'), cx = 0, cy = 0, cwidth, cheight, coverflow = 'auto', newwidth = null, newheight = null, newleft = null, newtop = null, boxshadow = null; if (selector) { selector = node.ancestor(selector); } if (selector) { cwidth = selector.get('offsetWidth'); cheight = selector.get('offsetHeight'); cx = selector.getX(); cy = selector.getY(); coverflow = selector.getStyle('overflow') || 'auto'; } else { cwidth = node.get('docWidth'); cheight = node.get('docHeight'); } // Constrain X. // First up if the width is more than the constrain its easily full width + full height. if (nwidth > cwidth) { // The width of the constraint. newwidth = nwidth = cwidth; // The constraints xpoint. newleft = nx = cx; } else { if (nx < cx) { // If nx is less than cx we need to move it right. newleft = nx = cx; } else if (nx + nwidth >= cx + cwidth) { // The top right of the node is outside of the constraint, move it in. newleft = cx + cwidth - nwidth; } } // Constrain Y. if (nheight > cheight && coverflow.toLowerCase() === 'hidden') { // The node extends over the constrained area and would be clipped. // Reduce the height of the node and force its overflow to scroll. newheight = nheight = cheight; node.setStyle('overflow', 'auto'); } // If the node is below the top of the constraint AND // the node is longer than the constraint allows. if (ny >= cy && ny + nheight > cy + cheight) { // Move it up. newtop = cy + cheight - nheight; try { boxshadow = node.getStyle('boxShadow').replace(/.*? (\d+)px \d+px$/, '$1'); if (new RegExp(/^\d+$/).test(boxshadow) && newtop - cy > boxshadow) { newtop -= boxshadow; } } catch (ex) { Y.log('Failed to determine box-shadow margin.', 'warn', ACTIONMENU.NAME); } } if (newleft !== null) { node.setX(newleft); } if (newtop !== null) { node.setY(newtop); } if (newwidth !== null) { node.setStyle('width', newwidth.toString() + 'px'); } if (newheight !== null) { node.setStyle('height', newheight.toString() + 'px'); } } }; Y.extend(ACTIONMENU, Y.Base, ACTIONMENU.prototype, { NAME: 'moodle-core-actionmenu', ATTRS: { align: { value: [ ALIGN.TR, // The dialogue. ALIGN.BR // The button ] } } }); M.core = M.core || {}; M.core.actionmenu = M.core.actionmenu || {}; /** * * @static * @property M.core.actionmenu.instance * @type {ACTIONMENU} */ M.core.actionmenu.instance = null; /** * Init function - will only ever create one instance of the actionmenu class. * * @method M.core.actionmenu.init * @static * @param {Object} params */ M.core.actionmenu.init = M.core.actionmenu.init || function(params) { M.core.actionmenu.instance = M.core.actionmenu.instance || new ACTIONMENU(params); }; /** * Registers a new DOM node with the action menu causing it to be enhanced if required. * * @method M.core.actionmenu.newDOMNode * @param node * @return {boolean} */ M.core.actionmenu.newDOMNode = function(node) { if (M.core.actionmenu.instance === null) { return true; } node.all(SELECTOR.MENU).each(M.core.actionmenu.instance.enhance, M.core.actionmenu.instance); }; }, '@VERSION@', {"requires": ["base", "event", "node-event-simulate"]}); build/moodle-core-actionmenu/moodle-core-actionmenu-min.js000064400000013605152265472450017715 0ustar00YUI.add("moodle-core-actionmenu",function(e,t){var s=e.one(e.config.doc.body),o="action-menu-shown",a={CAN_RECEIVE_FOCUS_SELECTOR:'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',MENU:".moodle-actionmenu[data-enhance=moodle-core-actionmenu]",MENUBAR:'[role="menubar"]',MENUITEM:'[role="menuitem"]',MENUCONTENT:".menu[data-rel=menu-content]",MENUCONTENTCHILD:"li a",MENUCHILD:".menu li a",TOGGLE:".toggle-display",KEEPOPEN:'[data-keepopen="1"]',MENUBARITEMS:['[role="menubar"] > [role="menuitem"]','[role="menubar"] > [role="presentation"] > [role="menuitem"]'],MENUITEMS:['> [role="menuitem"]','> [role="presentation"] > [role="menuitem"]']},n="tr",i="br",u=function(){u.superclass.constructor.apply(this,arguments)};e.extend(u,e.Base,u.prototype={dialogue:null,events:[],owner:null,menulink:null,menuChildren:null,firstMenuChild:null,lastMenuChild:null,initializer:function(){e.all(a.MENU).each(this.enhance,this),s.delegate("key",this.moveMenuItem,"down:37,39",a.MENUBARITEMS.join(","),this),s.delegate("click",this.toggleMenu,a.MENU+" "+a.TOGGLE,this),s.delegate("key",this.showIfHidden,"down:enter,38,40",a.MENU+" "+a.TOGGLE,this),s.delegate("key",function(e){e.currentTarget.simulate("click"),e.preventDefault()},"down:32",a.MENUBARITEMS.join(","))},enhance:function(e){var t,n=e.one(a.MENUCONTENT);if(!n)return!1;t=n.getData("align")||this.get("align").join("-"),e.one(a.TOGGLE).set("aria-haspopup",!0),n.set("aria-hidden",!0),n.hasClass("align-"+t)||n.addClass("align-"+t),n.getDOMNode().childElementCount&&e.setAttribute("data-enhanced","1")},moveMenuItem:function(e){var t,n=e.target.ancestor(a.MENUITEM,!0);return 37===e.keyCode?t=this.getMenuItem(n,!0):39===e.keyCode&&(t=this.getMenuItem(n)),t&&t.focus(),this},getMenuItem:function(e,t){var n,i,s,o,u,h=e.ancestor(a.MENUBAR);if(!h)return null;if(!(n=h.all(a.MENUITEMS.join(","))))return null;if(1===(s=n.size()))return null;for(u=1,o=o=0;o=this.menuChildren.size()&&(i=0),t=this.menuChildren.item(i),++o [role="menuitem"]', '[role="menubar"] > [role="presentation"] > [role="menuitem"]' ], MENUITEMS: [ '> [role="menuitem"]', '> [role="presentation"] > [role="menuitem"]' ] }, ACTIONMENU, ALIGN = { TL: 'tl', TR: 'tr', BL: 'bl', BR: 'br' }; /** * Action menu support. * This converts a generic list of links into a drop down menu opened by hovering or clicking * on a menu icon. * * @namespace M.core.actionmenu * @class ActionMenu * @constructor * @extends Base */ ACTIONMENU = function() { ACTIONMENU.superclass.constructor.apply(this, arguments); }; ACTIONMENU.prototype = { /** * The dialogue used for all action menu displays. * @property type * @type M.core.dialogue * @protected */ dialogue: null, /** * An array of events attached during the display of the dialogue. * @property events * @type Object * @protected */ events: [], /** * The node that owns the currently displayed menu. * * @property owner * @type Node * @default null */ owner: null, /** * The menu button that toggles this open. * * @property menulink * @type Node * @protected */ menulink: null, /** * The set of menu nodes. * * @property menuChildren * @type NodeList * @protected */ menuChildren: null, /** * The first menu item. * * @property firstMenuChild * @type Node * @protected */ firstMenuChild: null, /** * The last menu item. * * @property lastMenuChild * @type Node * @protected */ lastMenuChild: null, /** * Called during the initialisation process of the object. * * @method initializer */ initializer: function() { Y.all(SELECTOR.MENU).each(this.enhance, this); BODY.delegate('key', this.moveMenuItem, 'down:37,39', SELECTOR.MENUBARITEMS.join(','), this); BODY.delegate('click', this.toggleMenu, SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); BODY.delegate('key', this.showIfHidden, 'down:enter,38,40', SELECTOR.MENU + ' ' + SELECTOR.TOGGLE, this); // Ensure that we toggle on menuitems when the spacebar is pressed. BODY.delegate('key', function(e) { e.currentTarget.simulate('click'); e.preventDefault(); }, 'down:32', SELECTOR.MENUBARITEMS.join(',')); }, /** * Enhances a menu adding aria attributes and flagging it as functional. * * @method enhance * @param {Node} menu * @return boolean */ enhance: function(menu) { var menucontent = menu.one(SELECTOR.MENUCONTENT), align; if (!menucontent) { return false; } align = menucontent.getData('align') || this.get('align').join('-'); menu.one(SELECTOR.TOGGLE).set('aria-haspopup', true); menucontent.set('aria-hidden', true); if (!menucontent.hasClass('align-' + align)) { menucontent.addClass('align-' + align); } if (menucontent.getDOMNode().childElementCount) { menu.setAttribute('data-enhanced', '1'); } }, /** * Handle movement between menu items in a menubar. * * @method moveMenuItem * @param {EventFacade} e The event generating the move request * @chainable */ moveMenuItem: function(e) { var nextFocus, menuitem = e.target.ancestor(SELECTOR.MENUITEM, true); if (e.keyCode === 37) { nextFocus = this.getMenuItem(menuitem, true); } else if (e.keyCode === 39) { nextFocus = this.getMenuItem(menuitem); } if (nextFocus) { nextFocus.focus(); } return this; }, /** * Get the next menuitem in a menubar. * * @method getMenuItem * @param {Node} currentItem The currently focused item in the menubar * @param {Boolean} [previous=false] Move backwards in the menubar instead of forwards * @return {Node|null} The next item, or null if none was found */ getMenuItem: function(currentItem, previous) { var menubar = currentItem.ancestor(SELECTOR.MENUBAR), menuitems, next; if (!menubar) { return null; } menuitems = menubar.all(SELECTOR.MENUITEMS.join(',')); if (!menuitems) { return null; } var childCount = menuitems.size(); if (childCount === 1) { // Only one item, exit now because we should already be on it. return null; } // Determine the next child. var index = 0, direction = 1, checkCount = 0; // Work out the index of the currently selected item. for (index = 0; index < childCount; index++) { if (menuitems.item(index) === currentItem) { break; } } // Check that the menu item was found - otherwise return null. if (menuitems.item(index) !== currentItem) { return null; } // Reverse the direction if we want the previous item. if (previous) { direction = -1; } do { // Update the index in the direction of travel. index += direction; next = menuitems.item(index); // Check that we don't loop multiple times. checkCount++; } while (next && next.hasAttribute('hidden')); return next; }, /** * Hides the menu if it is visible. * @param {EventFacade} e * @method hideMenu */ hideMenu: function(e) { if (this.dialogue) { this.dialogue.removeClass('show'); this.dialogue.one(SELECTOR.MENUCONTENT).set('aria-hidden', true); this.dialogue = null; } for (var i in this.events) { if (this.events[i].detach) { this.events[i].detach(); } } this.events = []; if (this.owner) { this.owner.removeClass(CSS.MENUSHOWN); this.owner = null; } if (this.menulink) { if (e.type != 'click') { this.menulink.focus(); } this.menulink = null; } }, showIfHidden: function(e) { var menu = e.target.ancestor(SELECTOR.MENU), menuvisible = (menu.hasClass('show')); if (!menuvisible) { e.preventDefault(); this.showMenu(e, menu); } return this; }, /** * Toggles the display of the menu. * @method toggleMenu * @param {EventFacade} e */ toggleMenu: function(e) { var menu = e.target.ancestor(SELECTOR.MENU), menuvisible = (menu.hasClass('show')); // Prevent event propagation as it will trigger the hideIfOutside event handler in certain situations. e.halt(true); this.hideMenu(e); if (menuvisible) { // The menu was visible and the user has clicked to toggle it again. return; } this.showMenu(e, menu); }, /** * Handle keyboard events when the menu is open. We respond to: * * escape (exit) * * tab (move to next menu item) * * up/down (move to previous/next menu item) * * @method handleKeyboardEvent * @param {EventFacade} e The key event */ handleKeyboardEvent: function(e) { var next; var markEventHandled = function(e) { e.preventDefault(); e.stopPropagation(); }; // Handle when the menu is still selected. if (e.currentTarget.ancestor(SELECTOR.TOGGLE, true)) { if ((e.keyCode === 40 || (e.keyCode === 9 && !e.shiftKey)) && this.firstMenuChild) { this.firstMenuChild.focus(); markEventHandled(e); } else if (e.keyCode === 38 && this.lastMenuChild) { this.lastMenuChild.focus(); markEventHandled(e); } else if (e.keyCode === 9 && e.shiftKey) { this.hideMenu(e); markEventHandled(e); } return this; } if (e.keyCode === 27) { // The escape key was pressed so close the menu. this.hideMenu(e); markEventHandled(e); } else if (e.keyCode === 32) { // The space bar was pressed. Trigger a click. markEventHandled(e); e.currentTarget.simulate('click'); } else if (e.keyCode === 9) { // The tab key was pressed. Tab moves forwards, Shift + Tab moves backwards through the menu options. // We only override the Shift + Tab on the first option, and Tab on the last option to change where the // focus is moved to. if (e.target === this.firstMenuChild && e.shiftKey) { this.hideMenu(e); markEventHandled(e); } else if (e.target === this.lastMenuChild && !e.shiftKey) { if (this.hideMenu(e)) { // Determine the next selector and focus on it. next = this.menulink.next(SELECTOR.CAN_RECEIVE_FOCUS_SELECTOR); if (next) { next.focus(); markEventHandled(e); } } } } else if (e.keyCode === 38 || e.keyCode === 40) { // The up (38) or down (40) key was pushed. // On cursor moves we loops through the menu rather than exiting it as in the tab behaviour. var found = false, index = 0, direction = 1, checkCount = 0; // Determine which menu item is currently selected. while (!found && index < this.menuChildren.size()) { if (this.menuChildren.item(index) === e.currentTarget) { found = true; } else { index++; } } if (!found) { return; } if (e.keyCode === 38) { // Moving up so reverse the direction. direction = -1; } // Try to find the next do { index += direction; if (index < 0) { index = this.menuChildren.size() - 1; } else if (index >= this.menuChildren.size()) { // Handle wrapping. index = 0; } next = this.menuChildren.item(index); // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item. checkCount++; } while (checkCount < this.menuChildren.size() && next !== e.currentTarget && next.hasClass('hidden')); if (next) { next.focus(); markEventHandled(e); } } }, /** * Hides the menu if the event happened outside the menu. * * @protected * @method hideIfOutside * @param {EventFacade} e */ hideIfOutside: function(e) { if (!e.target.ancestor(SELECTOR.MENUCONTENT, true)) { this.hideMenu(e); } }, /** * Displays the menu with the given content and alignment. * * @method showMenu * @param {EventFacade} e * @param {Node} menu * @return M.core.dialogue */ showMenu: function(e, menu) { var ownerselector = menu.getData('owner'), menucontent = menu.one(SELECTOR.MENUCONTENT); this.owner = (ownerselector) ? menu.ancestor(ownerselector) : null; this.dialogue = menu; menu.addClass('show'); if (this.owner) { this.owner.addClass(CSS.MENUSHOWN); this.menulink = this.owner.one(SELECTOR.TOGGLE); } else { this.menulink = e.target.ancestor(SELECTOR.TOGGLE, true); } this.constrain(menucontent.set('aria-hidden', false)); this.menuChildren = this.dialogue.all(SELECTOR.MENUCHILD); if (this.menuChildren.size() > 0) { this.firstMenuChild = this.menuChildren.item(0); this.lastMenuChild = this.menuChildren.item(this.menuChildren.size() - 1); this.firstMenuChild.focus(); } // Close the menu if the user presses escape. this.events.push(BODY.on('key', this.hideMenu, 'esc', this)); // Close the menu if the user clicks outside the menu. this.events.push(BODY.on('click', this.hideIfOutside, this)); // Close the menu if the user focuses outside the menu. this.events.push(BODY.delegate('focus', this.hideIfOutside, '*', this)); // Check keyboard changes. this.events.push( menu.delegate('key', this.handleKeyboardEvent, 'down:9, 27, 38, 40, 32', SELECTOR.MENUCHILD + ', ' + SELECTOR.TOGGLE, this) ); // Close the menu after a button was pushed. this.events.push(menu.delegate('click', function(e) { if (e.currentTarget.test(SELECTOR.KEEPOPEN)) { return; } this.hideMenu(e); }, SELECTOR.MENUCHILD, this)); return true; }, /** * Constrains the node to its the page width. * * @method constrain * @param {Node} node */ constrain: function(node) { var selector = node.getData('constraint'), nx = node.getX(), ny = node.getY(), nwidth = node.get('offsetWidth'), nheight = node.get('offsetHeight'), cx = 0, cy = 0, cwidth, cheight, coverflow = 'auto', newwidth = null, newheight = null, newleft = null, newtop = null, boxshadow = null; if (selector) { selector = node.ancestor(selector); } if (selector) { cwidth = selector.get('offsetWidth'); cheight = selector.get('offsetHeight'); cx = selector.getX(); cy = selector.getY(); coverflow = selector.getStyle('overflow') || 'auto'; } else { cwidth = node.get('docWidth'); cheight = node.get('docHeight'); } // Constrain X. // First up if the width is more than the constrain its easily full width + full height. if (nwidth > cwidth) { // The width of the constraint. newwidth = nwidth = cwidth; // The constraints xpoint. newleft = nx = cx; } else { if (nx < cx) { // If nx is less than cx we need to move it right. newleft = nx = cx; } else if (nx + nwidth >= cx + cwidth) { // The top right of the node is outside of the constraint, move it in. newleft = cx + cwidth - nwidth; } } // Constrain Y. if (nheight > cheight && coverflow.toLowerCase() === 'hidden') { // The node extends over the constrained area and would be clipped. // Reduce the height of the node and force its overflow to scroll. newheight = nheight = cheight; node.setStyle('overflow', 'auto'); } // If the node is below the top of the constraint AND // the node is longer than the constraint allows. if (ny >= cy && ny + nheight > cy + cheight) { // Move it up. newtop = cy + cheight - nheight; try { boxshadow = node.getStyle('boxShadow').replace(/.*? (\d+)px \d+px$/, '$1'); if (new RegExp(/^\d+$/).test(boxshadow) && newtop - cy > boxshadow) { newtop -= boxshadow; } } catch (ex) { } } if (newleft !== null) { node.setX(newleft); } if (newtop !== null) { node.setY(newtop); } if (newwidth !== null) { node.setStyle('width', newwidth.toString() + 'px'); } if (newheight !== null) { node.setStyle('height', newheight.toString() + 'px'); } } }; Y.extend(ACTIONMENU, Y.Base, ACTIONMENU.prototype, { NAME: 'moodle-core-actionmenu', ATTRS: { align: { value: [ ALIGN.TR, // The dialogue. ALIGN.BR // The button ] } } }); M.core = M.core || {}; M.core.actionmenu = M.core.actionmenu || {}; /** * * @static * @property M.core.actionmenu.instance * @type {ACTIONMENU} */ M.core.actionmenu.instance = null; /** * Init function - will only ever create one instance of the actionmenu class. * * @method M.core.actionmenu.init * @static * @param {Object} params */ M.core.actionmenu.init = M.core.actionmenu.init || function(params) { M.core.actionmenu.instance = M.core.actionmenu.instance || new ACTIONMENU(params); }; /** * Registers a new DOM node with the action menu causing it to be enhanced if required. * * @method M.core.actionmenu.newDOMNode * @param node * @return {boolean} */ M.core.actionmenu.newDOMNode = function(node) { if (M.core.actionmenu.instance === null) { return true; } node.all(SELECTOR.MENU).each(M.core.actionmenu.instance.enhance, M.core.actionmenu.instance); }; }, '@VERSION@', {"requires": ["base", "event", "node-event-simulate"]}); build/moodle-core-handlebars/moodle-core-handlebars-min.js000064400000000552152265472450017614 0ustar00YUI.add("moodle-core-handlebars",function(e,r){e.Handlebars.registerHelper("get_string",function(){var r=new e.Array(arguments);return r.pop(),r.push(arguments[arguments.length-1].hash),M.util.get_string.apply(this,r)}),e.Handlebars.registerHelper("image_url",function(){var r=new e.Array(arguments);return r.pop(),M.util.image_url.apply(this,r)})},"@VERSION@");build/moodle-core-handlebars/moodle-core-handlebars-debug.js000064400000001576152265472450020126 0ustar00YUI.add('moodle-core-handlebars', function (Y, NAME) { // Handlebars only exists in the global namespace and we do not want to // Clone the entire thing. // It should be reasonably safe within Moodle to add some core // functionality to Handlebars at this level. Y.Handlebars.registerHelper('get_string', function() { var args = new Y.Array(arguments); // Pop off the Handlebars object at the end. args.pop(); // Add the Handlebars hashtable to support get_string arguments. args.push(arguments[arguments.length - 1].hash); // Perform the get_string. return M.util.get_string.apply(this, args); }); Y.Handlebars.registerHelper('image_url', function() { var args = new Y.Array(arguments); // Pop off the Handlebars object at the end. args.pop(); // Perform the get_string. return M.util.image_url.apply(this, args); }); }, '@VERSION@'); build/moodle-core-handlebars/moodle-core-handlebars.js000064400000001576152265472450017042 0ustar00YUI.add('moodle-core-handlebars', function (Y, NAME) { // Handlebars only exists in the global namespace and we do not want to // Clone the entire thing. // It should be reasonably safe within Moodle to add some core // functionality to Handlebars at this level. Y.Handlebars.registerHelper('get_string', function() { var args = new Y.Array(arguments); // Pop off the Handlebars object at the end. args.pop(); // Add the Handlebars hashtable to support get_string arguments. args.push(arguments[arguments.length - 1].hash); // Perform the get_string. return M.util.get_string.apply(this, args); }); Y.Handlebars.registerHelper('image_url', function() { var args = new Y.Array(arguments); // Pop off the Handlebars object at the end. args.pop(); // Perform the get_string. return M.util.image_url.apply(this, args); }); }, '@VERSION@'); license.txt000064400000003071152265472450006744 0ustar00Copyright (c) 2009, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Yahoo! Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of Yahoo! Inc. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. http://developer.yahoo.net/yui/license.html src/toolboxes/build.json000064400000000365152266401210011346 0ustar00{ "name": "moodle-mod_quiz-toolboxes", "builds": { "moodle-mod_quiz-toolboxes": { "jsfiles": [ "toolbox.js", "resource.js", "section.js" ] } } } src/toolboxes/meta/toolboxes.json000064400000000473152266401210013213 0ustar00{ "moodle-mod_quiz-toolboxes": { "requires": [ "base", "node", "event", "event-key", "io", "moodle-mod_quiz-quizbase", "moodle-mod_quiz-util-slot", "moodle-core-notification-ajaxexception" ] } } src/toolboxes/js/toolboxes.js000064400000016046152266401210012347 0ustar00/** * 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); }; build/moodle-message_airnotifier-toolboxes/moodle-message_airnotifier-toolboxes-min.js000064400000004047152266401210025606 0ustar00YUI.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"]});build/moodle-message_airnotifier-toolboxes/moodle-message_airnotifier-toolboxes-debug.js000064400000016245152266401210026114 0ustar00YUI.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"]}); build/moodle-message_airnotifier-toolboxes/moodle-message_airnotifier-toolboxes.js000064400000016245152266401210025030 0ustar00YUI.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"]}); src/management/build.json000064400000000371152266410660011452 0ustar00{ "name": "moodle-course-management", "builds": { "moodle-course-management": { "jsfiles": [ "shared.js", "console.js", "dd.js", "item.js", "category.js", "course.js" ] } } } src/management/meta/management.json000064400000000452152266410660013415 0ustar00{ "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" ] } }src/management/js/item.js000064400000026552152266410660011401 0ustar00/** * 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); src/management/js/course.js000064400000013004152266410660011727 0ustar00/** * 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); src/management/js/console.js000064400000041432152266410660012077 0ustar00/** * 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); }; src/management/js/dd.js000064400000020336152266410660011024 0ustar00/** * 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); src/management/js/category.js000064400000042767152266410660012266 0ustar00/** * 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); src/management/js/shared.js000064400000000077152266410660011703 0ustar00var Category; var Console; var Course; var DragDrop; var Item; src/dragdrop/js/resource.js000064400000017316152266410660011756 0ustar00/** * 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); }; src/dragdrop/js/section.js000064400000023154152266410660011570 0ustar00/** * 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); }; src/categoryexpander/build.json000064400000000251152266410660012677 0ustar00{ "name": "moodle-course-categoryexpander", "builds": { "moodle-course-categoryexpander": { "jsfiles": [ "categoryexpander.js" ] } } } src/categoryexpander/meta/categoryexpander.json000064400000000152152266410660016072 0ustar00{ "moodle-course-categoryexpander": { "requires": [ "node", "event-key" ] } } src/categoryexpander/js/categoryexpander.js000064400000040372152266410660015233 0ustar00/** * 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; }; src/util/build.json000064400000000501152266410660010306 0ustar00{ "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" ] } } } src/util/meta/util.json000064400000001045152266410660011116 0ustar00{ "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" ] } } } } src/util/js/cm.js000064400000004400152266410660007667 0ustar00/** * 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; } }; src/util/js/base.js000064400000000432152266410660010203 0ustar00/** * 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 */ src/util/js/section.js000064400000002401152266410660010733 0ustar00/** * 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; } }; src/coursebase/build.json000064400000000271152266410660011470 0ustar00{ "name": "moodle-course-coursebase", "builds": { "moodle-course-coursebase": { "jsfiles": [ "coursebase.js" ] } } } src/coursebase/meta/toolboxes.json000064400000000163152266410660013335 0ustar00{ "moodle-course-coursebase": { "requires": [ "base", "node" ] } } src/coursebase/js/coursebase.js000064400000016750152266410660012614 0ustar00/** * 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'); } }; build/moodle-course-categoryexpander/moodle-course-categoryexpander-min.js000064400000012112152266410660023225 0ustar00YUI.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"]});build/moodle-course-categoryexpander/moodle-course-categoryexpander-debug.js000064400000040562152266410660023542 0ustar00YUI.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"]}); build/moodle-course-categoryexpander/moodle-course-categoryexpander.js000064400000040405152266410660022452 0ustar00YUI.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); 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"]}); build/moodle-course-util-section/moodle-course-util-section.js000064400000002343152266410660020577 0ustar00YUI.add('moodle-course-util-section', function (Y, NAME) { /** * 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'); /** * 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; } }; }, '@VERSION@', {"requires": ["node", "moodle-course-util-base"]}); build/moodle-course-util-section/moodle-course-util-section-min.js000064400000000562152266410660021361 0ustar00YUI.add("moodle-course-util-section",function(e,o){e.namespace("Moodle.core_course.util.section"),e.Moodle.core_course.util.section={CONSTANTS:{SECTIONIDPREFIX:"section-"},getId:function(e){e=e.get("id").replace(this.CONSTANTS.SECTIONIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e}}},"@VERSION@",{requires:["node","moodle-course-util-base"]});build/moodle-course-util-section/moodle-course-util-section-debug.js000064400000002603152266410660021662 0ustar00YUI.add('moodle-course-util-section', function (Y, NAME) { /** * 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; } }; }, '@VERSION@', {"requires": ["node", "moodle-course-util-base"]}); build/moodle-course-coursebase/moodle-course-coursebase-debug.js000064400000017125152266410660021117 0ustar00YUI.add('moodle-course-coursebase', function (Y, NAME) { /** * 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'); } }; }, '@VERSION@', {"requires": ["base", "node"]}); build/moodle-course-coursebase/moodle-course-coursebase.js000064400000015562152266410660020036 0ustar00YUI.add('moodle-course-coursebase', function (Y, NAME) { /** * 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); }; /** * 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; } 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 { } }; /** * 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 { } }; /** * 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 { } }; /** * 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 { } }; }, '@VERSION@', {"requires": ["base", "node"]}); build/moodle-course-coursebase/moodle-course-coursebase-min.js000064400000004505152266410660020612 0ustar00YUI.add("moodle-course-coursebase",function(e,o){var r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,e.Base,{registermodules:[],register_module:function(e){return this.registermodules.push(e),this},invoke_function:function(e,o){for(var r in this.registermodules)e in this.registermodules[r]&&this.registermodules[r][e](o);return this}},{NAME:"course-coursebase",ATTRS:{}}),M.course=M.course||{},M.course.coursebase=M.course.coursebase||new r,M.course.format=M.course.format||{},M.course.format.swap_sections=M.course.format.swap_sections||function(){return null},M.course.format.process_sections=M.course.format.process_sections||function(){return null},M.course.format.get_config=M.course.format.get_config||function(){return{container_node:null,container_class:null,section_wrapper_node:null,section_wrapper_class:null,section_node:null,section_class:null}},M.course.format.get_section_selector=M.course.format.get_section_selector||function(){var e=M.course.format.get_config();return e.section_node&&e.section_class?e.section_node+"."+e.section_class:null},M.course.format.get_section_wrapper=M.course.format.get_section_wrapper||function(e){var o=M.course.format.get_config();return o.section_wrapper_node&&o.section_wrapper_class?o.section_wrapper_node+"."+o.section_wrapper_class:M.course.format.get_section_selector(e)},M.course.format.get_containernode=M.course.format.get_containernode||function(){var e=M.course.format.get_config();if(e.container_node)return e.container_node},M.course.format.get_containerclass=M.course.format.get_containerclass||function(){var e=M.course.format.get_config();if(e.container_class)return e.container_class},M.course.format.get_sectionwrappernode=M.course.format.get_sectionwrappernode||function(){var e=M.course.format.get_config();return e.section_wrapper_node||e.section_node},M.course.format.get_sectionwrapperclass=M.course.format.get_sectionwrapperclass||function(){var e=M.course.format.get_config();return e.section_wrapper_class||e.section_class},M.course.format.get_sectionnode=M.course.format.get_sectionnode||function(){var e=M.course.format.get_config();if(e.section_node)return e.section_node},M.course.format.get_sectionclass=M.course.format.get_sectionclass||function(){var e=M.course.format.get_config();if(e.section_class)return e.section_class}},"@VERSION@",{requires:["base","node"]});build/moodle-course-util-cm/moodle-course-util-cm.js000064400000004335152266410660016470 0ustar00YUI.add('moodle-course-util-cm', function (Y, NAME) { /** * 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'); /** * 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; } }; }, '@VERSION@', {"requires": ["node", "moodle-course-util-base"]}); build/moodle-course-util-cm/moodle-course-util-cm-min.js000064400000001155152266410660017246 0ustar00YUI.add("moodle-course-util-cm",function(t,e){t.namespace("Moodle.core_course.util.cm"),t.Moodle.core_course.util.cm={CONSTANTS:{MODULEIDPREFIX:"module-"},SELECTORS:{COURSEMODULE:".activity",INSTANCENAME:".instancename"},getCourseModuleFromComponent:function(e){return t.one(e).ancestor(this.SELECTORS.COURSEMODULE,!0)},getId:function(e){e=e.get("id").replace(this.CONSTANTS.MODULEIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},getName:function(e){e=e.one(this.SELECTORS.INSTANCENAME);return e?e.get("firstChild").get("data"):null}}},"@VERSION@",{requires:["node","moodle-course-util-base"]});build/moodle-course-util-cm/moodle-course-util-cm-debug.js000064400000004575152266410660017562 0ustar00YUI.add('moodle-course-util-cm', function (Y, NAME) { /** * 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; } }; }, '@VERSION@', {"requires": ["node", "moodle-course-util-base"]}); build/moodle-course-dragdrop/moodle-course-dragdrop-debug.js000064400000053057152266410660020241 0ustar00YUI.add('moodle-course-dragdrop', function (Y, NAME) { /* eslint-disable no-unused-vars */ /** * Drag and Drop for course sections and course modules. * * TODO: remove this module as part of MDL-83627. * * @module moodle-course-dragdrop */ Y.log( 'YUI moodle-course-dragdrop is deprecated. Please, add support_components to your course format.', 'warn', 'moodle-course-coursebase' ); var CSS = { ACTIONAREA: '.actions', ACTIVITY: 'activity', ACTIVITYINSTANCE: 'activityinstance', CONTENT: 'content', COURSECONTENT: 'course-content', EDITINGMOVE: 'editing_move', ICONCLASS: 'iconsmall', JUMPMENU: 'jumpmenu', LEFT: 'left', LIGHTBOX: 'lightbox', MOVEDOWN: 'movedown', MOVEUP: 'moveup', PAGECONTENT: 'page-content', RIGHT: 'right', SECTION: 'section', SECTIONADDMENUS: 'section_add_menus', SECTIONHANDLE: 'section-handle', SUMMARY: 'summary', SECTIONDRAGGABLE: 'sectiondraggable' }; M.course = M.course || {}; /** * Section drag and drop. * * TODO: remove this module as part of MDL-83627. * * @class M.course.dragdrop.section * @constructor * @extends M.core.dragdrop */ var DRAGSECTION = function() { DRAGSECTION.superclass.constructor.apply(this, arguments); }; Y.log( 'YUI M.course.dragdrop.section is deprecated. Please, add support_components to your course format.', 'warn', 'moodle-course-coursebase' ); Y.extend(DRAGSECTION, M.core.dragdrop, { sectionlistselector: null, initializer: function() { // Set group for parent class this.groups = [CSS.SECTIONDRAGGABLE]; this.samenodeclass = M.course.format.get_sectionwrapperclass(); this.parentnodeclass = M.course.format.get_containerclass(); // Detect the direction of travel. this.detectkeyboarddirection = true; // Check if we are in single section mode if (Y.Node.one('.' + CSS.JUMPMENU)) { return false; } // Initialise sections dragging this.sectionlistselector = M.course.format.get_section_wrapper(Y); 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 .course-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) { if (moveup.previous('br')) { moveup.previous('br').remove(); } else if (moveup.next('br')) { moveup.next('br').remove(); } if (moveup.ancestor('.section_action_menu') && moveup.ancestor().get('nodeName').toLowerCase() == 'li') { moveup.ancestor().remove(); } else { moveup.remove(); } } if (movedown) { if (movedown.previous('br')) { movedown.previous('br').remove(); } else if (movedown.next('br')) { movedown.next('br').remove(); } var movedownParentType = movedown.ancestor().get('nodeName').toLowerCase(); if (movedown.ancestor('.section_action_menu') && movedownParentType == 'li') { movedown.ancestor().remove(); } else { 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; // This is the node that the user started to drag. var node = drag.get('node'); // This is the container node that will follow the mouse around, // or during a keyboard drag and drop the original node. var dragnode = drag.get('dragNode'); if (node === dragnode) { return; } // Creat a dummy structure of the outer elemnents for clean styles application var containernode = Y.Node.create('<' + M.course.format.get_containernode() + '>'); containernode.addClass(M.course.format.get_containerclass()); var sectionnode = Y.Node.create('<' + M.course.format.get_sectionwrappernode() + '>'); sectionnode.addClass(M.course.format.get_sectionwrapperclass()); sectionnode.setStyle('margin', 0); sectionnode.setContent(node.get('innerHTML')); containernode.appendChild(sectionnode); dragnode.setContent(containernode); 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 + ' ' + M.course.format.get_section_selector(Y), 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-course-dragdrop'); return; } Y.log("Moving from position " + dragnodeid + " to position " + dropnodeindex, 'debug', 'moodle-course-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['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.course.format.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))); // 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.course.format.swap_sections(Y, index - 1, index); // Update flag. swapped = true; } sectionlist.item(index).setAttribute('data-sectionid', Y.Moodle.core_course.util.section.getId(sectionlist.item(index))); } loopend = loopend - 1; } while (swapped); window.setTimeout(function() { lightbox.hide(); }, 250); // Update course state. M.course.coursebase.invoke_function('updateMovedSectionState'); }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); } }, context: this }); } }, { NAME: 'course-dragdrop-section', ATTRS: { courseid: { value: null }, ajaxurl: { value: 0 }, config: { value: 0 } } }); M.course = M.course || {}; M.course.init_section_dragdrop = function(params) { new DRAGSECTION(params); }; /** * Resource drag and drop. * * TODO: remove this module as part of MDL-83627. * * @class M.course.dragdrop.resource * @constructor * @extends M.core.dragdrop */ var DRAGRESOURCE = function() { DRAGRESOURCE.superclass.constructor.apply(this, arguments); }; Y.log( 'YUI M.course.dragdrop.resource is deprecated. Please, add support_components to your course format.', 'warn', 'moodle-course-coursebase' ); 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: 'afterresource', component: 'moodle' }; this.parentnodelabel = { identifier: 'totopofsection', component: 'moodle' }; // Go through all sections var sectionlistselector = M.course.format.get_section_selector(Y); if (sectionlistselector) { sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector; this.setup_for_section(sectionlistselector); // Initialise drag & drop for all resources/activities var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' 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 .course-content constrain: '#' + CSS.PAGECONTENT }); del.dd.plug(Y.Plugin.DDWinScroll); M.course.coursebase.register_module(this); M.course.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(baseselector) { Y.Node.all(baseselector).each(function(sectionnode) { var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION); // See if resources ul exists, if not create one if (!resources) { resources = Y.Node.create('
              '); resources.addClass(CSS.SECTION); sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after'); } 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('#' + sectionnode.get('id') + ' li.' + CSS.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) { var draggroups = resourcesnode.getData('draggroups'); if (!draggroups) { // This Drop Node has not been set up. Configure it now. resourcesnode.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: resourcesnode, groups: this.groups, padding: '20 0 20 0' }); } // Replace move icons var move = resourcesnode.one('a.' + CSS.EDITINGMOVE); if (move) { var sr = move.getData('sectionreturn'); move.replace(this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'), CSS.EDITINGMOVE, CSS.ICONCLASS, true).setAttribute('data-sectionreturn', sr)); } }, this); }, drag_start: function(e) { // Get our drag object var drag = e.target; if (drag.get('dragNode') === drag.get('node')) { // We do not want to modify the contents of the real node. // They will be the same during a keyboard drag and drop. return; } drag.get('dragNode').setContent(drag.get('node').get('innerHTML')); drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline'); }, 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); }, drop_hit: function(e) { var drag = e.drag; // Get a reference to our drag node var dragnode = drag.get('node'); var dropnode = e.drop.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]; } // Variables needed to update the course state. var cmid = Number(Y.Moodle.core_course.util.cm.getId(dragnode)); var beforeid = null; // Prepare request parameters params.sesskey = M.cfg.sesskey; params.courseId = this.get('courseid'); params['class'] = 'resource'; params.field = 'move'; params.id = cmid; params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true)); if (dragnode.next()) { beforeid = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next())); params.beforeId = beforeid; } // 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); // Update course state. M.course.coursebase.invoke_function( 'updateMovedCmState', { cmid: cmid, beforeid: beforeid, visible: responsetext.visible, } ); // Set visibility in course content. var params = {element: dragnode, visible: responsetext.visible}; M.course.coursebase.invoke_function('set_visibility_resource_ui', params); this.unlock_drag_handle(drag, CSS.EDITINGMOVE); window.setTimeout(function() { spinner.hide(); }, 250); }, failure: function(tid, response) { this.ajax_failure(response); this.unlock_drag_handle(drag, CSS.SECTIONHANDLE); spinner.hide(); // TODO: revert nodes location } }, context: this }); } }, { NAME: 'course-dragdrop-resource', ATTRS: { courseid: { value: null }, ajaxurl: { value: 0 }, config: { value: 0 } } }); M.course = M.course || {}; M.course.init_resource_dragdrop = function(params) { new DRAGRESOURCE(params); }; }, '@VERSION@', { "requires": [ "base", "node", "io", "dom", "dd", "dd-scroll", "moodle-core-dragdrop", "moodle-core-notification", "moodle-course-coursebase", "moodle-course-util" ] }); build/moodle-course-dragdrop/moodle-course-dragdrop.js000064400000051144152266410660017150 0ustar00YUI.add('moodle-course-dragdrop', function (Y, NAME) { /* eslint-disable no-unused-vars */ /** * Drag and Drop for course sections and course modules. * * TODO: remove this module as part of MDL-83627. * * @module moodle-course-dragdrop */ var CSS = { ACTIONAREA: '.actions', ACTIVITY: 'activity', ACTIVITYINSTANCE: 'activityinstance', CONTENT: 'content', COURSECONTENT: 'course-content', EDITINGMOVE: 'editing_move', ICONCLASS: 'iconsmall', JUMPMENU: 'jumpmenu', LEFT: 'left', LIGHTBOX: 'lightbox', MOVEDOWN: 'movedown', MOVEUP: 'moveup', PAGECONTENT: 'page-content', RIGHT: 'right', SECTION: 'section', SECTIONADDMENUS: 'section_add_menus', SECTIONHANDLE: 'section-handle', SUMMARY: 'summary', SECTIONDRAGGABLE: 'sectiondraggable' }; M.course = M.course || {}; /** * Section drag and drop. * * TODO: remove this module as part of MDL-83627. * * @class M.course.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 = M.course.format.get_sectionwrapperclass(); this.parentnodeclass = M.course.format.get_containerclass(); // Detect the direction of travel. this.detectkeyboarddirection = true; // Check if we are in single section mode if (Y.Node.one('.' + CSS.JUMPMENU)) { return false; } // Initialise sections dragging this.sectionlistselector = M.course.format.get_section_wrapper(Y); 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 .course-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) { if (moveup.previous('br')) { moveup.previous('br').remove(); } else if (moveup.next('br')) { moveup.next('br').remove(); } if (moveup.ancestor('.section_action_menu') && moveup.ancestor().get('nodeName').toLowerCase() == 'li') { moveup.ancestor().remove(); } else { moveup.remove(); } } if (movedown) { if (movedown.previous('br')) { movedown.previous('br').remove(); } else if (movedown.next('br')) { movedown.next('br').remove(); } var movedownParentType = movedown.ancestor().get('nodeName').toLowerCase(); if (movedown.ancestor('.section_action_menu') && movedownParentType == 'li') { movedown.ancestor().remove(); } else { 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; // This is the node that the user started to drag. var node = drag.get('node'); // This is the container node that will follow the mouse around, // or during a keyboard drag and drop the original node. var dragnode = drag.get('dragNode'); if (node === dragnode) { return; } // Creat a dummy structure of the outer elemnents for clean styles application var containernode = Y.Node.create('<' + M.course.format.get_containernode() + '>'); containernode.addClass(M.course.format.get_containerclass()); var sectionnode = Y.Node.create('<' + M.course.format.get_sectionwrappernode() + '>'); sectionnode.addClass(M.course.format.get_sectionwrapperclass()); sectionnode.setStyle('margin', 0); sectionnode.setContent(node.get('innerHTML')); containernode.appendChild(sectionnode); dragnode.setContent(containernode); 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 + ' ' + M.course.format.get_section_selector(Y), 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) { return; } 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['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.course.format.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))) { // 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.course.format.swap_sections(Y, index - 1, index); // Update flag. swapped = true; } sectionlist.item(index).setAttribute('data-sectionid', Y.Moodle.core_course.util.section.getId(sectionlist.item(index))); } loopend = loopend - 1; } while (swapped); window.setTimeout(function() { lightbox.hide(); }, 250); // Update course state. M.course.coursebase.invoke_function('updateMovedSectionState'); }, failure: function(tid, response) { this.ajax_failure(response); lightbox.hide(); } }, context: this }); } }, { NAME: 'course-dragdrop-section', ATTRS: { courseid: { value: null }, ajaxurl: { value: 0 }, config: { value: 0 } } }); M.course = M.course || {}; M.course.init_section_dragdrop = function(params) { new DRAGSECTION(params); }; /** * Resource drag and drop. * * TODO: remove this module as part of MDL-83627. * * @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: 'afterresource', component: 'moodle' }; this.parentnodelabel = { identifier: 'totopofsection', component: 'moodle' }; // Go through all sections var sectionlistselector = M.course.format.get_section_selector(Y); if (sectionlistselector) { sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector; this.setup_for_section(sectionlistselector); // Initialise drag & drop for all resources/activities var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' 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 .course-content constrain: '#' + CSS.PAGECONTENT }); del.dd.plug(Y.Plugin.DDWinScroll); M.course.coursebase.register_module(this); M.course.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(baseselector) { Y.Node.all(baseselector).each(function(sectionnode) { var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION); // See if resources ul exists, if not create one if (!resources) { resources = Y.Node.create('
                '); resources.addClass(CSS.SECTION); sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after'); } 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('#' + sectionnode.get('id') + ' li.' + CSS.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) { var draggroups = resourcesnode.getData('draggroups'); if (!draggroups) { // This Drop Node has not been set up. Configure it now. resourcesnode.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: resourcesnode, groups: this.groups, padding: '20 0 20 0' }); } // Replace move icons var move = resourcesnode.one('a.' + CSS.EDITINGMOVE); if (move) { var sr = move.getData('sectionreturn'); move.replace(this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'), CSS.EDITINGMOVE, CSS.ICONCLASS, true).setAttribute('data-sectionreturn', sr)); } }, this); }, drag_start: function(e) { // Get our drag object var drag = e.target; if (drag.get('dragNode') === drag.get('node')) { // We do not want to modify the contents of the real node. // They will be the same during a keyboard drag and drop. return; } drag.get('dragNode').setContent(drag.get('node').get('innerHTML')); drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline'); }, 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); }, drop_hit: function(e) { var drag = e.drag; // Get a reference to our drag node var dragnode = drag.get('node'); var dropnode = e.drop.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]; } // Variables needed to update the course state. var cmid = Number(Y.Moodle.core_course.util.cm.getId(dragnode)); var beforeid = null; // Prepare request parameters params.sesskey = M.cfg.sesskey; params.courseId = this.get('courseid'); params['class'] = 'resource'; params.field = 'move'; params.id = cmid; params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.course.format.get_section_wrapper(Y), true)); if (dragnode.next()) { beforeid = Number(Y.Moodle.core_course.util.cm.getId(dragnode.next())); params.beforeId = beforeid; } // 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); // Update course state. M.course.coursebase.invoke_function( 'updateMovedCmState', { cmid: cmid, beforeid: beforeid, visible: responsetext.visible, } ); // Set visibility in course content. var params = {element: dragnode, visible: responsetext.visible}; M.course.coursebase.invoke_function('set_visibility_resource_ui', params); this.unlock_drag_handle(drag, CSS.EDITINGMOVE); window.setTimeout(function() { spinner.hide(); }, 250); }, failure: function(tid, response) { this.ajax_failure(response); this.unlock_drag_handle(drag, CSS.SECTIONHANDLE); spinner.hide(); // TODO: revert nodes location } }, context: this }); } }, { NAME: 'course-dragdrop-resource', ATTRS: { courseid: { value: null }, ajaxurl: { value: 0 }, config: { value: 0 } } }); M.course = M.course || {}; M.course.init_resource_dragdrop = function(params) { new DRAGRESOURCE(params); }; }, '@VERSION@', { "requires": [ "base", "node", "io", "dom", "dd", "dd-scroll", "moodle-core-dragdrop", "moodle-core-notification", "moodle-course-coursebase", "moodle-course-util" ] }); build/moodle-course-dragdrop/moodle-course-dragdrop-min.js000064400000015734152266410660017736 0ustar00YUI.add("moodle-course-dragdrop",function(g,e){var o,t,l=".actions",s="activity",r="content",p="course-content",u="editing_move",a="iconsmall",d="jumpmenu",i="left",f="movedown",m="moveup",_="page-content",h="right",n="section",v="section-handle",b="summary",c="sectiondraggable";M.course=M.course||{},g.extend(o=function(){o.superclass.constructor.apply(this,arguments)},M.core.dragdrop,{sectionlistselector:null,initializer:function(){if(this.groups=[c],this.samenodeclass=M.course.format.get_sectionwrapperclass(),this.parentnodeclass=M.course.format.get_containerclass(),this.detectkeyboarddirection=!0,g.Node.one("."+d))return!1;var e;this.sectionlistselector=M.course.format.get_section_wrapper(g),this.sectionlistselector&&(this.sectionlistselector="."+p+" "+this.sectionlistselector,this.setup_for_section(this.sectionlistselector),(e=new g.DD.Delegate({container:"."+p,nodes:"."+c,target:!0,handles:["."+i],dragConfig:{groups:this.groups}})).dd.plug(g.Plugin.DDProxy,{moveOnEnd:!1}),e.dd.plug(g.Plugin.DDConstrained,{constrain:"#"+_,stickY:!0}),e.dd.plug(g.Plugin.DDWinScroll))},setup_for_section:function(e){g.Node.all(e).each(function(e){var o,t,s,r=g.Moodle.core_course.util.section.getId(e);0")).addClass(M.course.format.get_containerclass()),(t=g.Node.create("<"+M.course.format.get_sectionwrappernode()+">")).addClass(M.course.format.get_sectionwrapperclass()),t.setStyle("margin",0),t.setContent(s.get("innerHTML")),o.appendChild(t),e.setContent(o),e.addClass(p))},drag_dropmiss:function(e){this.drop_hit(e)},get_section_index:function(e){var o="."+p+" "+M.course.format.get_section_selector(g),o=g.all(o);return o.indexOf(e)-o.indexOf(g.one("#section-0"))},drop_hit:function(n){var c,a,e,i,o,t=n.drag,l=t.get("node"),s=g.Moodle.core_course.util.section.getId(l),d=s,r=this.get_section_index(l),u=r;if(s!==r){for(o in ug.Moodle.core_course.util.section.getId(c.item(s))&&(i=c.item(s-1).get("id"),c.item(s-1).set("id",c.item(s).get("id")),c.item(s).set("id",i),M.course.format.swap_sections(g,s-1,s),r=!0),c.item(s).setAttribute("data-sectionid",g.Moodle.core_course.util.section.getId(c.item(s)))}while(u-=1,r);window.setTimeout(function(){a.hide()},250),M.course.coursebase.invoke_function("updateMovedSectionState")},failure:function(e,o){this.ajax_failure(o),a.hide()}},context:this})}}},{NAME:"course-dragdrop-section",ATTRS:{courseid:{value:null},ajaxurl:{value:0},config:{value:0}}}),M.course=M.course||{},M.course.init_section_dragdrop=function(e){new o(e)},g.extend(t=function(){t.superclass.constructor.apply(this,arguments)},M.core.dragdrop,{initializer:function(){var e;this.groups=["resource"],this.samenodeclass=s,this.parentnodeclass=n,this.samenodelabel={identifier:"afterresource",component:"moodle"},this.parentnodelabel={identifier:"totopofsection",component:"moodle"},(e=M.course.format.get_section_selector(g))&&(e="."+p+" "+e,this.setup_for_section(e),e=e.slice(p.length+2)+" li."+s,(e=new g.DD.Delegate({container:"."+p,nodes:e,target:!0,handles:["."+u],dragConfig:{groups:this.groups}})).dd.plug(g.Plugin.DDProxy,{moveOnEnd:!1,cloneNode:!0}),e.dd.plug(g.Plugin.DDConstrained,{constrain:"#"+_}),e.dd.plug(g.Plugin.DDWinScroll),M.course.coursebase.register_module(this),M.course.dragres=this)},setup_for_section:function(e){g.Node.all(e).each(function(e){var o=e.one("."+r+" ul."+n);o||((o=g.Node.create("
                  ")).addClass(n),e.one("."+r+" div."+b).insert(o,"after")),o.setAttribute("data-draggroups",this.groups.join(" ")),new g.DD.Drop({node:o,groups:this.groups,padding:"20 0 20 0"}),this.setup_for_resource("#"+e.get("id")+" li."+s)},this)},setup_for_resource:function(e){g.Node.all(e).each(function(e){var o=e.getData("draggroups");o||(e.setAttribute("data-draggroups",this.groups.join(" ")),new g.DD.Drop({node:e,groups:this.groups,padding:"20 0 20 0"})),(o=e.one("a."+u))&&(e=o.getData("sectionreturn"),o.replace(this.get_drag_handle(M.util.get_string("movecoursemodule","moodle"),u,a,!0).setAttribute("data-sectionreturn",e)))},this)},drag_start:function(e){e=e.target;e.get("dragNode")!==e.get("node")&&(e.get("dragNode").setContent(e.get("node").get("innerHTML")),e.get("dragNode").all("img.iconsmall").setStyle("vertical-align","baseline"))},drag_dropmiss:function(e){this.drop_hit(e)},drop_hit:function(e){var o,t,s,r=e.drag,i=r.get("node"),e=e.drop.get("node"),n=i.one(l),c=M.util.add_spinner(g,n),a={},d=this.get("config").pageparams;for(o in d)a[o]=d[o];t=Number(g.Moodle.core_course.util.cm.getId(i)),s=null,a.sesskey=M.cfg.sesskey,a.courseId=this.get( "courseid"),a["class"]="resource",a.field="move",a.id=t,a.sectionId=g.Moodle.core_course.util.section.getId(e.ancestor(M.course.format.get_section_wrapper(g),!0)),i.next()&&(s=Number(g.Moodle.core_course.util.cm.getId(i.next())),a.beforeId=s),n=M.cfg.wwwroot+this.get("ajaxurl"),g.io(n,{method:"POST",data:a,on:{start:function(){this.lock_drag_handle(r,u),c.show()},success:function(e,o){var o=g.JSON.parse(o.responseText);M.course.coursebase.invoke_function("updateMovedCmState",{cmid:t,beforeid:s,visible:o.visible}),o={element:i,visible:o.visible},M.course.coursebase.invoke_function("set_visibility_resource_ui",o),this.unlock_drag_handle(r,u),window.setTimeout(function(){c.hide()},250)},failure:function(e,o){this.ajax_failure(o),this.unlock_drag_handle(r,v),c.hide()}},context:this})}},{NAME:"course-dragdrop-resource",ATTRS:{courseid:{value:null},ajaxurl:{value:0},config:{value:0}}}),M.course=M.course||{},M.course.init_resource_dragdrop=function(e){new t(e)}},"@VERSION@",{requires:["base","node","io","dom","dd","dd-scroll","moodle-core-dragdrop","moodle-core-notification","moodle-course-coursebase","moodle-course-util"]});build/moodle-course-management/moodle-course-management-min.js000064400000045143152266410660020557 0ustar00YUI.add("moodle-course-management",function(s,e){var i,o,r,t,n=function(){n.superclass.constructor.apply(this,arguments)};n.NAME="moodle-course-management",n.CSS_PREFIX="management",n.ATTRS={element:{setter:function(e){return e="string"==typeof e?s.one("#"+e):e}},categorylisting:{value:null},courselisting:{value:null},coursedetails:{value:null},activecategoryid:{value:null},activecourseid:{value:null},categories:{setter:function(e,t){if(s.Lang.isArray(e))return e;t=this.get(t);return t.push(e),t},value:[]},courses:{validator:function(e){return s.Lang.isArray(e)},value:[]},page:{getter:function(e,t){return null===e&&(e=this.get("element").getData(t),this.set(t,e)),e},value:null},totalpages:{getter:function(e,t){return null===e&&(e=this.get("element").getData(t),this.set(t,e)),e},value:null},totalcourses:{getter:function(e,t){return null===e&&(e=this.get("element").getData(t),this.set(t,e)),e},value:null},ajaxurl:{getter:function(e){return e=null===e?M.cfg.wwwroot+"/course/ajax/management.php":e},value:null},dragdrop:{value:null}},s.extend(n,s.Base,n.prototype={categoriesinit:!1,initializer:function(){this.set("element","coursecat-management");var e=this.get("element"),t=e.one("#category-listing"),i=e.one("#course-listing"),o=null,n=null;t&&(o=t.one('.listitem[data-selected="1"]')),i&&(n=i.one('.listitem[data-selected="1"]')),this.set("categorylisting",t),this.set("courselisting",i),this.set("coursedetails",e.one("#course-detail")),o&&this.set("activecategoryid",o.getData("id")),n&&this.set("activecourseid",n.getData("id")),this.initialiseCategories(t),this.initialiseCourses(),i&&this.set("dragdrop",new r({console:this}))},initialiseCategories:function(e){var t;if(!e)return!1;(t=e.one("#menumovecategoriesto"))&&t.setAttribute("disabled",!0),(t=e.one("#menuresortcategoriesby"))&&t.setAttribute("disabled",!0),(t=e.one("#menuresortcoursesby"))&&t.setAttribute("disabled",!0),e.all(".listitem[data-id]").each(function(e){this.set("categories",new i({node:e,console:this})),0},this),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=!0)},initialiseCourses:function(){var e,t=this.getCategoryById(this.get("activecategoryid")),i=this.get("courselisting");if(!i)return!1;(e=i.one("#menumovecoursesto"))&&e.setAttribute("disabled",!0),i.all(".listitem[data-id]").each(function(e){this.registerCourse(new o({node:e,console:this,category:t})),0},this),i.delegate("click",this.handleCourseDelegation,"a[data-action]",this),i.delegate("click",this.handleCourseDelegation,'input[name="bc[]"]',this)},registerCourse:function(e){var t=this.get("courses");t.push(e),this.set("courses",t)},handleCourseDelegation:function(e){var t=e.currentTarget,i=t.getData("action"),t=t.ancestor(".listitem").getData("id"),t=this.getCourseById(t);t&&t.handle(i,e)},handleCategoryDelegation:function(e){var t=e.currentTarget,i=t.getData("action"),t=t.ancestor(".listitem").getData("id"),t=this.getCategoryById(t);t&&t.handle(i,e)},isCourseSelected:function(e){var t,i,o,n=!1;if(e&&e.get("checked"))n=!0;else for(o=(i=this.get("courses")).length,t=0;t .course-listing"),t=t?t.one("ul.category-list"):null,o=i?i.one("ul.course-list"):null,i=!!i&&i.getData("canmoveoutof"),n=i?e:o;if(!o)return!1;for(;0===n.get("scrollHeight")&&!n.compareTo(window.document.body);)n=n.get("parentNode");o.all("> li").each(function(e){this.initCourseListing(e,n)},this),o.setData("dd",new s.DD.Drop({node:o})),i&&t&&t.all("li > div").each(function(e){this.initCategoryListitem(e)},this),s.DD.DDM.on("drag:start",this.dragStart, this),s.DD.DDM.on("drag:end",this.dragEnd,this),s.DD.DDM.on("drag:drag",this.dragDrag,this),s.DD.DDM.on("drop:over",this.dropOver,this),s.DD.DDM.on("drop:enter",this.dropEnter,this),s.DD.DDM.on("drop:exit",this.dropExit,this),s.DD.DDM.on("drop:hit",this.dropHit,this)},initCourseListing:function(e,t){e.setData("dd",new s.DD.Drag({node:e,target:{padding:"0 0 0 20"}}).addHandle(".drag-handle").plug(s.Plugin.DDProxy,{moveOnEnd:!1,borderStyle:!1}).plug(s.Plugin.DDConstrained,{constrain2node:t}))},initCategoryListitem:function(e){e.setData("dd",new s.DD.Drop({node:e}))},dragStart:function(e){var e=e.target,t=e.get("node"),e=e.get("dragNode");t.addClass("course-being-dragged"),e.addClass("course-being-dragged-proxy").set("innerHTML",t.one("a.coursename").get("innerHTML")),this.previoussibling=t.get("previousSibling")},dragEnd:function(e){e.target.get("node").removeClass("course-being-dragged"),this.get("console").get("element").all("#category-listing li.highlight").removeClass("highlight")},dragDrag:function(e){e=e.target.lastXY[1];e div a.action-moveup"),n=e.one(" > div a.action-movedown"),i&&n||(o=e.one(" > div a.action-moveup"),t=t.one(" > div a.action-movedown"),i||n?n||o.insert(t,"after"):(i=s.Node.create(' '),t.replace(i),o.replace(t),i.replace(o),i.destroy())),(o=e.one(" > div a.action-moveup"))?o.focus():(n=e.one(" > div a.action-movedown"))&&n.focus(),this.updated(!0)):window.location.reload()},movedown:function(e,t,i){var o,n;if(!1===this.checkAjaxResponse(e,t,i))return!1;(t=(e=this.get("node")).next(".listitem"))?(e.insert(t,"before"),i=t.one(" > div a.action-movedown"),o=e.one(" > div a.action-moveup"),i&&o||(t=t.one(" > div a.action-moveup"),n=e.one(" > div a.action-movedown"),i||o?o||n.insert(t,"before"):(i=s.Node.create(' '),t.replace(i),n.replace(t),i.replace(n),i.destroy())),(n=e.one(" > div a.action-movedown"))?n.focus():(o=e.one(" > div a.action-moveup"))&&o.focus(),this.updated(!0)):window.location.reload()},show:function(e,t,i){if(!1===this.checkAjaxResponse(e,t,i))return!1;this.markVisible(),(e=this.get("node").one("a[data-action=hide]"))&&e.focus(),this.updated()},markVisible:function(){return this.get("node").setAttribute("data-visible","1"),!0},hide:function(e,t,i){if(!1===this.checkAjaxResponse(e,t,i))return!1;this.markHidden(),(e=this.get("node").one("a[data-action=show]"))&&e.focus(),this.updated()},markHidden:function(){return this.get("node").setAttribute("data-visible","0"),!0},updated:function(e){e&&this.highlight()},highlight:function(){var e=this.get("node");e.siblings(".highlight").removeClass("highlight"),e.addClass("highlight"),this.highlighttimeout&&window.clearTimeout(this.highlighttimeout),this.highlighttimeout=window.setTimeout(function(){e.removeClass("highlight")},2500)}}),(i=function(){i.superclass.constructor.apply(this,arguments)}).NAME="moodle-course-management-category",i.CSS_PREFIX="management-category",i.ATTRS={categoryid:{getter:function(e,t){return null===e&&(e=this.get("node").getData("id"),this.set(t,e)),e},value:null,writeOnce:!0},selected:{getter:function(e,t){return null===e&&(e=this.get("node").getData(t),this.set(t,e=null===e?!1:e)),e},value:null},courses:{validator:function(e){return s.Lang.isArray(e)},value:[]}},i.prototype={initializer:function(){this.set("itemname","category")},getName:function(){return this.get("node").one("a.categoryname").get("innerHTML")},registerCourse:function(e){var t=this.get("courses");t.push(e),this.set("courses",t)},handle:function(e,t){var i,o,n,s,r={categoryid:this.get("categoryid")},a=this.get("console").get("activecategoryid");switch(a&&a!==r.categoryid&&(r.selectedcategory=a),e){case"moveup":t.preventDefault(),this.get("console").performAjaxAction("movecategoryup",r,this.moveup,this);break;case"movedown":t.preventDefault(),this.get("console").performAjaxAction("movecategorydown",r,this.movedown,this);break;case"show":t.preventDefault(),this.get("console").performAjaxAction("showcategory",r,this.show,this);break;case"hide":if(t.preventDefault(),"0"===(n=this.get("node").getData("course-count"))){this.get("console").performAjaxAction("hidecategory",r,this.hide, this);break}i="1"===n?"hidecategoryone":"hidecategorymany",o={category:this.get("node").getData("category-name"),coursecount:n},require(["core/notification","core/str"],function(e,t){e.saveCancelPromise(t.get_string("hidecategory"),t.get_string(i,"core",o),t.get_string("hide")).then(function(){this.get("console").performAjaxAction("hidecategory",r,this.hide,this)}.bind(this))["catch"](function(){})}.bind(this));break;case"expand":t.preventDefault(),"0"===this.get("node").getData("expanded")&&(this.get("node").setAttribute("data-expanded","1").setData("expanded","true"),this.get("console").performAjaxAction("getsubcategorieshtml",r,this.loadSubcategories,this)),this.expand();break;case"collapse":t.preventDefault(),this.collapse();break;case"select":(s=(n=this.get("console")).get("categorylisting").one("#menumovecategoriesto"))&&(n.isCategorySelected(t.currentTarget)&&1 .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); /** * 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); /** * 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); /** * 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); }, '@VERSION@', { "requires": [ "base", "node", "io-base", "moodle-core-notification-exception", "json-parse", "dd-constrain", "dd-proxy", "dd-drop", "dd-delegate", "node-event-delegate" ] }); build/moodle-course-management/moodle-course-management.js000064400000156351152266410660020002 0ustar00YUI.add('moodle-course-management', function (Y, NAME) { var Category; var Console; var Course; var DragDrop; var Item; /** * 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() { 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; } else { } }, /** * 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); }, /** * 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 { } }, /** * 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 { } }, /** * 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() { }; } 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); }; /** * 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')) { return false; } courseid = drag.getData('id'); if (iscategory) { categoryid = drop.ancestor('.listitem-category').getData('id'); 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.extend(DragDrop, Y.Base, DragDrop.prototype); /** * 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) { return false; } if (transactionid === null || args === null) { 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) { 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); } else { // Aha it succeeded but this is the top item in the list. Pagination is in play! // Refresh to update the state of things. 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) { 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); } else { // Aha it succeeded but this is the bottom item in the list. Pagination is in play! // Refresh to update the state of things. 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) { return false; } this.markVisible(); hidebtn = this.get('node').one('a[data-action=hide]'); if (hidebtn) { hidebtn.focus(); } this.updated(); }, /** * Marks the item as visible * @method markVisible */ markVisible: function() { this.get('node').setAttribute('data-visible', '1'); 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) { return false; } this.markHidden(); showbtn = this.get('node').one('a[data-action=show]'); if (showbtn) { showbtn.focus(); } this.updated(); }, /** * Marks the item as hidden. * @method makeHidden */ markHidden: function() { this.get('node').setAttribute('data-visible', '0'); 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); /** * 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: 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) { return false; } 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) { return false; } course = managementconsole.getCourseById(args.courseid); if (!course) { return false; } 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) { 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(); }, /** * 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) { 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(); }, /** * Updates the visibility of child courses if required. * @method updateCourseVisiblity * @chainable * @param courses */ updateCourseVisiblity: function(courses) { var managementconsole = this.get('console'), key, course; 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) { } return this; }, /** * Updates the visibility of subcategories if required. * @method updateChildVisibility * @chainable * @param categories */ updateChildVisibility: function(categories) { var managementconsole = this.get('console'), key, category; 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) { } return this; } }; Y.extend(Category, Item, Category.prototype); /** * 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: 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 + ']'); 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; } this.highlight(); } }; Y.extend(Course, Item, Course.prototype); }, '@VERSION@', { "requires": [ "base", "node", "io-base", "moodle-core-notification-exception", "json-parse", "dd-constrain", "dd-proxy", "dd-drop", "dd-delegate", "node-event-delegate" ] }); build/moodle-course-util-base/moodle-course-util-base-debug.js000064400000001104152266410660020371 0ustar00YUI.add('moodle-course-util-base', function (Y, NAME) { /** * The Moodle.core_course.util classes provide course-related utility functions. * * TODO: remove this module as part of MDL-83627. * * @module moodle-course-util * @main */ Y.namespace('Moodle.core_course.util'); Y.log( 'YUI Moodle.core_course.util is deprecated. Please, add support_components to your course format.', 'warn', 'moodle-course-coursebase' ); /** * A collection of general utility functions for use in course. * * @class Moodle.core_course.util * @static */ }, '@VERSION@'); build/moodle-course-util-base/moodle-course-util-base.js000064400000000647152266410660017320 0ustar00YUI.add('moodle-course-util-base', function (Y, NAME) { /** * The Moodle.core_course.util classes provide course-related utility functions. * * TODO: remove this module as part of MDL-83627. * * @module moodle-course-util * @main */ Y.namespace('Moodle.core_course.util'); /** * A collection of general utility functions for use in course. * * @class Moodle.core_course.util * @static */ }, '@VERSION@'); build/moodle-course-util-base/moodle-course-util-base-min.js000064400000000145152266410660020072 0ustar00YUI.add("moodle-course-util-base",function(e,o){e.namespace("Moodle.core_course.util")},"@VERSION@");otherusersmanager/assets/skins/sam/otherusersmanager.css000064400000011641152266447110017776 0ustar00/* stylelint-disable unit-disallowed-list */ /************************************** Structure of the other user role assignment panel .other-user-manager-panel(.visible) .oump-wrap .oump-header .oump-content .oump-ajax-content .oump-search-results .oump-total-users .oump-users .oump-user.clearfix(.odd|.even)(.enrolled) .count .oump-user-details .oump-user-picture .oump-user-specifics .oump-user-fullname .oump-user-extrafields .oump-role-options .label .oump-assignable-role .oump-more-results .oump-loading-lightbox(.hidden) .loading-icon .oump-footer .oump-search input **************************************/ .other-user-manager-panel { width: 400px; background-color: #666; position: absolute; top: 10%; left: 10%; border: 1px solid #666; border-width: 0 5px 5px 0; } .other-user-manager-panel.hidden { display: none; } .other-user-manager-panel .oump-wrap { margin-top: -5px; margin-left: -5px; background-color: #fff; border: 1px solid #999; height: inherit; } .other-user-manager-panel .oump-header { background-color: #eee; padding: 1px; } .other-user-manager-panel .oump-header h2 { margin: 3px 1em 0.5em 1em; font-size: 1em; } .other-user-manager-panel .oump-header .oump-panel-close { width: 25px; height: 15px; position: absolute; top: 2px; right: 1em; cursor: pointer; background: url("sprite.png") no-repeat scroll 0 0 transparent; } .other-user-manager-panel .oump-content { text-align: center; position: relative; width: 100%; border-top: 1px solid #999; border-bottom: 1px solid #999; } .other-user-manager-panel .oump-ajax-content { height: 375px; overflow: auto; } .other-user-manager-panel .oump-search-results .oump-total-users { background-color: #eee; padding: 5px; border-bottom: 1px solid #bbb; font-size: 7pt; font-weight: bold; } .other-user-manager-panel .oump-search-results .oump-user { width: 100%; text-align: left; font-size: 9pt; background-color: #ddd; border-bottom: 1px solid #aaa; } .other-user-manager-panel .oump-search-results .oump-user .oump-user-details { background-color: #fff; margin-left: 25px; border-left: 1px solid #bbb; } .other-user-manager-panel .oump-search-results .oump-user.odd .oump-user-details { background-color: #f9f9f9; } .other-user-manager-panel .oump-search-results .oump-user .count { width: 20px; font-size: 7pt; line-height: 100%; text-align: right; float: left; padding: 5px 5px 2px 2px; } .other-user-manager-panel .oump-search-results .oump-user .oump-user-details .oump-user-picture { display: inline-block; margin: 3px; } .other-user-manager-panel .oump-search-results .oump-user .oump-user-details .oump-user-specifics { width: 250px; display: inline-block; margin: 3px; vertical-align: top; } .other-user-manager-panel .oump-search-results .oump-user .oump-user-details .oump-role-options { font-size: 8pt; margin-top: 2px; text-align: right; margin-right: 2px; } .other-user-manager-panel .oump-search-results .oump-user .oump-user-details .oump-role-options .oump-assignable-role { display: inline-block; margin: 0; padding: 3px 4px; cursor: pointer; } .other-user-manager-panel .oump-search-results .oump-user.assignment-in-progress .oump-assignable-role { color: #666; cursor: default; } .other-user-manager-panel .oump-search-results .oump-more-results { background-color: #eee; padding: 5px; cursor: pointer; } .other-user-manager-panel .oump-search-results .oump-user.oump-has-all-roles { background-color: #ccc; } .other-user-manager-panel .oump-search-results .oump-user.oump-has-all-roles .count { width: 40px; } .other-user-manager-panel .oump-loading-lightbox { position: absolute; width: 100%; height: 100%; top: 0; left: 0; background-color: #fff; min-width: 50px; min-height: 50px; } .other-user-manager-panel .oump-loading-lightbox.hidden { display: none; } .other-user-manager-panel .oump-loading-lightbox .loading-icon { margin: auto; vertical-align: middle; margin-top: 125px; } .other-user-manager-panel .oump-footer { padding: 3px; background-color: #ddd; } .other-user-manager-panel .oump-search { margin: 3px; } .other-user-manager-panel .oump-search label { padding-right: 8px; } .other-user-manager-panel .oump-search input { width: 70%; } otherusersmanager/assets/skins/sam/sprite.png000064400000001642152266447110015542 0ustar00PNG  IHDR&PLTEmmmBoAn3[?jх"""/>غ߳0V.R@l@m9aĸ7_¬=g;e9c@k(L+N44wX(1z;=)7%׳( sss5:nGG8<$$RGK?IIIhs.7La^B~1ڣ $*jjjF| 4;C3CĖguuКDYj-+kSM׆׾ $\~yt%+UUjjHȢYαLĀa U~Rc)"$w
                • ') .append(Y.Node.create('
                  ') .append(Y.Node.create('
                  ') .append(Y.Node.create('
                  ')) .append(Y.Node.create('

                  '+M.util.get_string('usersearch', 'enrol')+'

                  '))) .append(Y.Node.create('
                  ') .append(Y.Node.create('
                  ')) .append(Y.Node.create('
                  ') .append(Y.Node.create('loading') .setAttribute('src', M.util.image_url('i/loading', 'moodle'))) .setStyle('opacity', 0.5))) .append(Y.Node.create('
                  ') .append(Y.Node.create('
                  ') .append(Y.Node.create('')) ) ) ) ); this.set(SEARCH, this.get(BASE).one('#oump-usersearch')); Y.all('.assignuserrole input').each(function(node){ if (node.getAttribute('type', 'submit')) { node.on('click', this.show, this); } }, this); this.get(BASE).one('.'+CSS.HEADER+' .'+CSS.CLOSE).on('click', this.hide, this); this._loadingNode = this.get(BASE).one('.'+CSS.CONTENT+' .'+CSS.LIGHTBOX); Y.on('key', this.getUsers, this.get(SEARCH), 'down:13', this); Y.one(document.body).append(this.get(BASE)); var base = this.get(BASE); base.plug(Y.Plugin.Drag); base.dd.addHandle('.'+CSS.HEADER+' h2'); base.one('.'+CSS.HEADER+' h2').setStyle('cursor', 'move'); this.getAssignableRoles(); }, show : function(e) { e.preventDefault(); e.halt(); var base = this.get(BASE); base.removeClass(CSS.HIDDEN); var x = (base.get('winWidth') - 400)/2; var y = (parseInt(base.get('winHeight'))-base.get('offsetHeight'))/2 + parseInt(base.get('docScrollY')); if (y < parseInt(base.get('winHeight'))*0.1) { y = parseInt(base.get('winHeight'))*0.1; } base.setXY([x,y]); if (this.get(USERS)===null) { this.getUsers(e, false); } this._escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this); }, hide : function() { if (this._escCloseEvent) { this._escCloseEvent.detach(); this._escCloseEvent = null; } this.get(BASE).addClass(CSS.HIDDEN); if (this.get(REQUIREREFRESH)) { window.location = this.get(URL); } }, getUsers : function(e, append) { if (e) { e.halt(); e.preventDefault(); } var on, params; if (append) { this.set(PAGE, this.get(PAGE)+1); } else { this.set(USERCOUNT, 0); } params = []; params['id'] = this.get(COURSEID); params['sesskey'] = M.cfg.sesskey; params['action'] = 'searchotherusers'; params['search'] = this.get(SEARCH).get('value'); params['page'] = this.get(PAGE); Y.io(M.cfg.wwwroot+this.get(AJAXURL), { method:'POST', data:build_querystring(params), on : { start : this.displayLoading, complete: this.processSearchResults, end : this.removeLoading }, context:this, arguments:{ append:append, params:params } }); }, displayLoading : function() { this._loadingNode.removeClass(CSS.HIDDEN); }, removeLoading : function() { this._loadingNode.addClass(CSS.HIDDEN); }, processSearchResults : function(tid, outcome, args) { var result; try { result = Y.JSON.parse(outcome.responseText); if (result.error) { return new M.core.ajaxException(result); } } catch (e) { new M.core.exception(e); } if (!result.success) { this.setContent = M.util.get_string('errajaxsearch', 'enrol'); } var usersnode, users = [], i=0, count=0, user; if (!args.append) { usersnode = Y.Node.create('
                  '); } else { usersnode = this.get(BASE).one('.'+CSS.SEARCHRESULTS+' .'+CSS.USERS); } count = this.get(USERCOUNT); for (i in result.response.users) { count++; user = new OTHERUSER(result.response.users[i], count, this); usersnode.append(user.toHTML()); users[user.get(USERID)] = user; } this.set(USERCOUNT, count); if (!args.append) { var usersstr = ''; if (this.get(USERCOUNT) === 1) { usersstr = M.util.get_string('ajaxoneuserfound', 'enrol'); } else if (result.response.moreusers) { usersstr = M.util.get_string('ajaxxmoreusersfound', 'enrol', this.get(USERCOUNT)); } else { usersstr = M.util.get_string('ajaxxusersfound', 'enrol', this.get(USERCOUNT)); } var content = Y.Node.create('
                  ') .append(Y.Node.create('
                  '+usersstr+'
                  ')) .append(usersnode); if (result.response.moreusers) { var fetchmore = Y.Node.create(''); fetchmore.on('click', this.getUsers, this, true); content.append(fetchmore) } this.setContent(content); } else { if (!result.response.moreusers) { this.get(BASE).one('.'+CSS.MORERESULTS).remove(); } } }, setContent : function(content) { this.get(BASE).one('.'+CSS.CONTENT+' .'+CSS.AJAXCONTENT).setContent(content); }, getAssignableRoles : function() { Y.io(M.cfg.wwwroot+'/enrol/ajax.php', { method:'POST', data:'id='+this.get(COURSEID)+'&action=getassignable&otherusers=true&sesskey='+M.cfg.sesskey, on: { complete: function(tid, outcome, args) { try { var roles = Y.JSON.parse(outcome.responseText); if (roles.error) { new M.core.ajaxException(roles); } else { this.set(ASSIGNABLEROLES, roles.response); } } catch (e) { new M.core.exception(e); } this.getAssignableRoles = function() { this.fire('assignablerolesloaded'); }; this.getAssignableRoles(); } }, context:this }); } }, { NAME : OUMANAGERNAME, ATTRS : { courseId : { }, ajaxUrl : { validator : Y.Lang.isString }, url : { validator : Y.Lang.isString }, roles : { validator :Y.Lang.isArray, value : [] }, base : { setter : function(node) { var n = Y.one(node); if (!n) { Y.fail(OUMANAGERNAME+': invalid base node set'); } return n; } }, search : { setter : function(node) { var n = Y.one(node); if (!n) { Y.fail(OUMANAGERNAME+': invalid base node set'); } return n; } }, requiresRefresh : { validator : Y.Lang.isBoolean, value : false }, users : { validator : Y.Lang.isArray, value : null }, page : { validator : Y.Lang.isNumber, value : 0 }, userCount : { validator : Y.Lang.isNumber, value : 0 }, assignableRoles : { value : [] } } }); var OTHERUSER = function(config, count, manager) { this._count = count; this._manager = manager; OTHERUSER.superclass.constructor.apply(this, arguments); }; Y.extend(OTHERUSER, Y.Base, { _count : 0, _manager : null, _node : null, _assignmentInProgress : false, initializer : function(config) { this.publish('assignrole:success'); this.publish('assignrole:failure'); }, toHTML : function() { this._node = Y.Node.create('
                  ') .addClass((this._count%2)?CSS.ODD:CSS.EVEN) .append(Y.Node.create('
                  '+this._count+'
                  ')) .append(Y.Node.create('
                  ') .append(Y.Node.create('
                  ') .append(Y.Node.create(this.get(PICTURE))) ) .append(Y.Node.create('
                  ') .append(Y.Node.create('
                  '+this.get(FULLNAME)+'
                  ')) .append(Y.Node.create('
                  '+this.get(EXTRAFIELDS)+'
                  ')) ) .append(Y.Node.create('
                  '+M.util.get_string('assignrole', 'role')+':
                  ')) ); var roles = this._manager.get(ASSIGNABLEROLES); for (var i in roles) { var role = Y.Node.create('' + roles[i].name + ''); role.on('click', this.assignRoleToUser, this, roles[i].id, role); this._node.one('.'+CSS.OPTIONS).append(role); } return this._node; }, assignRoleToUser : function(e, roleid, node) { e.halt(); if (this._assignmentInProgress) { return true; } this._node.addClass('assignment-in-progress'); this._assignmentInProgress = true; Y.io(M.cfg.wwwroot+'/enrol/ajax.php', { method:'POST', data:'id='+this._manager.get(COURSEID)+'&action=assign&sesskey='+M.cfg.sesskey+'&roleid='+roleid+'&user='+this.get(USERID), on: { complete: function(tid, outcome, args) { try { var o = Y.JSON.parse(outcome.responseText); if (o.success) { var options = args.node.ancestor('.'+CSS.OPTIONS); if (options.all('.'+CSS.ROLEOPTION).size() == 1) { // This is the last node so remove the options div if (options.ancestor('.'+CSS.USER)) { options.ancestor('.'+CSS.USER).addClass(CSS.ALLROLESASSIGNED); } options.remove(); } else { // There are still more assignable roles args.node.remove(); } this._manager.set(REQUIREREFRESH, true); } } catch (e) { new M.core.exception(e); } this._assignmentInProgress = false; this._node.removeClass('assignment-in-progress'); } }, context:this, arguments:{ roleid : roleid, node : node } }); return true; } }, { NAME : OTHERUSERNAME, ATTRS : { userId : { }, fullname : { validator : Y.Lang.isString }, extrafields : { validator : Y.Lang.isString }, picture : { validator : Y.Lang.isString } } }); Y.augment(OTHERUSER, Y.EventTarget); M.enrol = M.enrol || {}; M.enrol.otherusersmanager = { init : function(cfg) { new OUMANAGER(cfg); } } }, '@VERSION@', {requires:['base','node', 'overlay', 'io-base', 'test', 'json-parse', 'event-delegate', 'dd-plugin', 'event-key', 'moodle-core-notification']}); rolemanager/rolemanager.js000064400000041741152266447110011711 0ustar00YUI.add('moodle-enrol-rolemanager', function(Y) { var MOD_NAME = 'Moodle role manager', MOD_USER = 'Moodle role user', MOD_PANEL = 'Moodle role assignment panel', USERIDS = 'userIds', COURSEID = 'courseId', USERID = 'userId', CONTAINER = 'container', CONTAINERID = 'containerId', ASSIGNABLEROLES = 'assignableRoles', ASSIGNROLELINK = 'assignRoleLink', ASSIGNROLELINKSELECTOR = 'assignRoleLinkSelector', UNASSIGNROLELINKS = 'unassignRoleLinks', UNASSIGNROLELINKSSELECTOR = 'unassignRoleLinksSelector', MANIPULATOR = 'manipulator', CURRENTROLES = 'currentroles', OTHERUSERS = 'otherusers'; var ROLE = function(config) { ROLE.superclass.constructor.apply(this, arguments); }; ROLE.NAME = MOD_NAME; ROLE.ATTRS = { containerId : { validator: Y.Lang.isString }, container : { setter : function(node) { var n = Y.one(node); if (!n) { Y.fail(MOD_NAME+': invalid container set'); } return n; } }, courseId : { value: 0, setter : function(courseId) { if (!(/^\d+$/.test(courseId))) { Y.fail(MOD_NAME+': Invalid course id specified'); } return courseId; } }, userIds : { validator: Y.Lang.isArray }, assignableRoles : { value : [] }, otherusers : { value : false } }; Y.extend(ROLE, Y.Base, { users : [], roleAssignmentPanel : null, rolesLoadedEvent : null, escCloseEvent : null, initializer : function(config) { var i; var container = Y.one('#'+this.get(CONTAINERID)); container.addClass('ajaxactive'); this.set(CONTAINER, container); var userids = this.get(USERIDS); for (i in userids) { this.users[userids[i]] = new ROLEUSER({userId:userids[i],manipulator:this}).wire(); } }, addRole : function(e, user) { e.halt(); this.rolesLoadedEvent = this.on('assignablerolesloaded', function(){ this.rolesLoadedEvent.detach(); var panel = this._getRoleAssignmentPanel(); panel.hide(); panel.submitevent = panel.on('submit', this.addRoleCallback, this); panel.display(user); }, this); this._loadAssignableRoles(); }, addRoleCallback : function(e, roleid, userid) { var panel = this._getRoleAssignmentPanel(); panel.submitevent.detach(); panel.submitevent = null; Y.io(M.cfg.wwwroot+'/enrol/ajax.php', { method:'POST', data:'id='+this.get(COURSEID)+'&action=assign&sesskey='+M.cfg.sesskey+'&roleid='+roleid+'&user='+userid, on: { complete: function(tid, outcome, args) { try { var o = Y.JSON.parse(outcome.responseText); if (o.error) { new M.core.ajaxException(o); } else { this.users[userid].addRoleToDisplay(args.roleid, this._getAssignableRole(args.roleid)); } } catch (e) { new M.core.exception(e); } panel.hide(); } }, context:this, arguments:{ roleid : roleid } }); }, removeRole: function(e, user, roleid) { e.halt(); require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirmation', 'admin'), M.util.get_string('confirmunassign', 'role'), M.util.get_string('confirmunassignyes', 'role') ).then(function() { return this.removeRoleCallback(user.get(USERID), roleid); }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); this._loadAssignableRoles(); }, removeRoleCallback: function(userid, roleid) { Y.io(M.cfg.wwwroot+'/enrol/ajax.php', { method:'POST', data:'id='+this.get(COURSEID)+'&action=unassign&sesskey='+M.cfg.sesskey+'&role='+roleid+'&user='+userid, on: { complete: function(tid, outcome, args) { var o; try { o = Y.JSON.parse(outcome.responseText); if (o.error) { new M.core.ajaxException(o); } else { this.users[userid].removeRoleFromDisplay(args.roleid); } } catch (e) { new M.core.exception(e); } } }, context:this, arguments:{ roleid : roleid } }); }, _getAssignableRole: function(roleid) { var roles = this.get(ASSIGNABLEROLES); for (var i in roles) { if (roles[i].id == roleid) { return roles[i].name; } } return null; }, _loadAssignableRoles : function() { var c = this.get(COURSEID), params = { id : this.get(COURSEID), otherusers : (this.get(OTHERUSERS))?'true':'false', action : 'getassignable', sesskey : M.cfg.sesskey }; Y.io(M.cfg.wwwroot+'/enrol/ajax.php', { method:'POST', data:build_querystring(params), on: { complete: function(tid, outcome, args) { try { var roles = Y.JSON.parse(outcome.responseText); this.set(ASSIGNABLEROLES, roles.response); } catch (e) { new M.core.exception(e); } this._loadAssignableRoles = function() { this.fire('assignablerolesloaded'); }; this._loadAssignableRoles(); } }, context:this }); }, _getRoleAssignmentPanel : function() { if (this.roleAssignmentPanel === null) { this.roleAssignmentPanel = new ROLEPANEL({manipulator:this}); } return this.roleAssignmentPanel; } }); Y.augment(ROLE, Y.EventTarget); var ROLEUSER = function(config) { ROLEUSER.superclass.constructor.apply(this, arguments); }; ROLEUSER.NAME = MOD_USER; ROLEUSER.ATTRS = { userId : { validator: Y.Lang.isNumber }, manipulator : { validator: Y.Lang.isObject }, container : { setter : function(node) { var n = Y.one(node); if (!n) { Y.fail(MOD_USER+': invalid container set '+node); } return n; } }, assignableroles : { value : [] }, currentroles : { value : [], validator: Y.Lang.isArray }, assignRoleLink : { setter : function(node) { if (node===false) { return node; } var n = Y.one(node); if (!n) { Y.fail(MOD_NAME+': invalid assign role link given '+node); } return n; }, value : false }, assignRoleLinkSelector : { value : '.assignrolelink', validator : Y.Lang.isString }, unassignRoleLinks : { }, unassignRoleLinksSelector : { value : '.unassignrolelink', validator : Y.Lang.isString } }; Y.extend(ROLEUSER, Y.Base, { initializer : function() { var container = this.get(MANIPULATOR).get(CONTAINER).one('#user_'+this.get(USERID)); this.set(CONTAINER, container); var assignrole = container.one(this.get(ASSIGNROLELINKSELECTOR)); if (assignrole) { this.set(ASSIGNROLELINK, assignrole.ancestor()); } this.set(UNASSIGNROLELINKS , container.all(this.get(UNASSIGNROLELINKSSELECTOR))); }, wire : function() { var container = this.get(MANIPULATOR).get(CONTAINER).one('#user_'+this.get(USERID)); var arl = this.get(ASSIGNROLELINK); var uarls = this.get(UNASSIGNROLELINKS); var m = this.get(MANIPULATOR); if (arl) { arl.ancestor().on('click', m.addRole, m, this); } var currentroles = []; if (uarls.size() > 0) { uarls.each(function(link){ link.roleId = link.getAttribute('rel'); link.on('click', m.removeRole, m, this, link.roleId); currentroles[link.roleId] = true; }, this); } container.all('.role.unchangeable').each(function(node){ currentroles[node.getAttribute('rel')] = true; }, this); this.set(CURRENTROLES, currentroles); return this; }, _checkIfHasAllRoles : function() { var roles = this.get(MANIPULATOR).get(ASSIGNABLEROLES); var current = this.get(CURRENTROLES); var allroles = true, i = 0; for (i in roles) { if (!current[roles[i].id]) { allroles = false; break; } } var link = this.get(ASSIGNROLELINK); if (allroles) { this.get(CONTAINER).addClass('hasAllRoles'); } else { this.get(CONTAINER).removeClass('hasAllRoles'); } }, addRoleToDisplay : function(roleId, roleTitle) { var m = this.get(MANIPULATOR); var container = this.get(CONTAINER); window.require(['core/templates'], function(Templates) { Templates.renderPix('t/delete', 'core').then(function(pix) { var role = Y.Node.create('
                  ' + roleTitle + '' + pix + '
                  '); var link = role.one('.unassignrolelink'); link.roleId = roleId; link.on('click', m.removeRole, m, this, link.roleId); container.one('.col_role .roles').append(role); this._toggleCurrentRole(link.roleId, true); }.bind(this)); }.bind(this)); }, removeRoleFromDisplay : function(roleId) { var container = this.get(CONTAINER); container.all('.role_'+roleId).remove(); this._toggleCurrentRole(roleId, false); }, _toggleCurrentRole : function(roleId, hasRole) { var roles = this.get(CURRENTROLES); if (hasRole) { roles[roleId] = true; } else { roles[roleId] = false; } this.set(CURRENTROLES, roles); this._checkIfHasAllRoles(); } }); var ROLEPANEL = function(config) { ROLEPANEL.superclass.constructor.apply(this, arguments); }; ROLEPANEL.NAME = MOD_PANEL; ROLEPANEL.ATTRS = { elementNode : { setter : function(node) { var n = Y.one(node); if (!n) { Y.fail(MOD_PANEL+': Invalid element node'); } return n; } }, contentNode : { setter : function(node) { var n = Y.one(node); if (!n) { Y.fail(MOD_PANEL+': Invalid content node'); } return n; } }, manipulator : { validator: Y.Lang.isObject } }; Y.extend(ROLEPANEL, Y.Base, { user : null, roles : [], submitevent : null, initializer : function() { var i, m = this.get(MANIPULATOR); var element = Y.Node.create('
                  ' + '
                  ' + '
                  ' + '
                  ' + '

                  '+M.util.get_string('assignroles', 'role')+'

                  ' + '
                  '); var content = element.one('.content'); var roles = m.get(ASSIGNABLEROLES); for (i in roles) { var buttonid = 'add_assignable_role_' + roles[i].id; var buttonhtml = ''; var button = Y.Node.create(buttonhtml); button.on('click', this.submit, this, roles[i].id); content.append(button); } Y.one(document.body).append(element); this.set('elementNode', element); this.set('contentNode', content); element.one('.header .btn-close').on('click', this.hide, this); }, display : function(user) { var currentroles = user.get(CURRENTROLES), node = null; for (var i in currentroles) { if (currentroles[i] === true) { if (node = this.get('contentNode').one('#add_assignable_role_'+i)) { node.setAttribute('disabled', 'disabled'); } this.roles.push(i); } } this.user = user; var roles = this.user.get(CONTAINER).one('.col_role .roles'); var x = roles.getX() + 10; var y = roles.getY() + this.user.get(CONTAINER).get('offsetHeight') - 10; if ( Y.one(document.body).hasClass('dir-rtl') ) { this.get('elementNode').setStyle('right', x - 20).setStyle('top', y); } else { this.get('elementNode').setStyle('left', x).setStyle('top', y); } this.get('elementNode').setStyle('display', 'block'); this.escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this); this.displayed = true; }, hide : function() { if (this._escCloseEvent) { this._escCloseEvent.detach(); this._escCloseEvent = null; } var node = null; for (var i in this.roles) { if (node = this.get('contentNode').one('#add_assignable_role_'+this.roles[i])) { node.removeAttribute('disabled'); } } this.roles = []; this.user = null; this.get('elementNode').setStyle('display', 'none'); if (this.submitevent) { this.submitevent.detach(); this.submitevent = null; } this.displayed = false; return this; }, submit : function(e, roleid) { this.fire('submit', roleid, this.user.get(USERID)); } }); Y.augment(ROLEPANEL, Y.EventTarget); M.enrol = M.enrol || {}; M.enrol.rolemanager = { instance : null, init : function(config) { M.enrol.rolemanager.instance = new ROLE(config); return M.enrol.rolemanager.instance; } } }, '@VERSION@', {requires:['base','node','io-base','json-parse','test','moodle-core-notification']}); src/userselector/build.json000064400000000331152266570530012054 0ustar00{ "name": "moodle-gradereport_history-userselector", "builds": { "moodle-gradereport_history-userselector": { "jsfiles": [ "userselector.js" ] } } } src/userselector/meta/userselector.json000064400000000443152266570530014426 0ustar00{ "moodle-gradereport_history-userselector": { "requires": [ "escape", "event-delegate", "event-key", "handlebars", "io-base", "json-parse", "moodle-core-notification-dialogue" ] } } src/userselector/js/userselector.js000064400000063025152266570530013564 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * The User Selector for the grade history report. * * @module moodle-gradereport_history-userselector * @package gradereport_history * @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @main moodle-gradereport_history-userselector */ /** * @module moodle-gradereport_history-userselector */ var COMPONENT = 'gradereport_history'; var USP = { AJAXURL: 'ajaxurl', BASE: 'base', CHECKBOX_NAME_PREFIX: 'usp-u', COURSEID: 'courseid', DIALOGUE_PREFIX: 'moodle-dialogue', NAME: 'gradereport_history_usp', PAGE: 'page', PARAMS: 'params', PERPAGE: 'perPage', SEARCH: 'search', SEARCHBTN: 'searchbtn', SELECTEDUSERS: 'selectedUsers', URL: 'url', USERCOUNT: 'userCount' }; var CSS = { ACCESSHIDE: 'accesshide', AJAXCONTENT: 'usp-ajax-content', CHECKBOX: 'usp-checkbox', CLOSE: 'close', CLOSEBTN: 'usp-finish', CONTENT: 'usp-content', DETAILS: 'details', EXTRAFIELDS: 'extrafields', FIRSTADDED: 'usp-first-added', FULLNAME: 'fullname', HEADER: 'usp-header', HIDDEN: 'hidden', LIGHTBOX: 'usp-loading-lightbox', LOADINGICON: 'loading-icon icon', MORERESULTS: 'usp-more-results', OPTIONS: 'options', PICTURE: 'usp-picture', RESULTSCOUNT: 'usp-results-count', SEARCH: 'usp-search', SEARCHBTN: 'usp-search-btn', SEARCHFIELD: 'usp-search-field', SEARCHRESULTS: 'usp-search-results', SELECTED: 'selected', USER: 'usp-user', USERS: 'usp-users', WRAP: 'usp-wrap' }; var SELECTORS = { AJAXCONTENT: '.' + CSS.AJAXCONTENT, FINISHBTN: '.' + CSS.CLOSEBTN + ' input', FIRSTADDED: '.' + CSS.FIRSTADDED, FULLNAME: '.' + CSS.FULLNAME + ' label', LIGHTBOX: '.' + CSS.LIGHTBOX, MORERESULTS: '.' + CSS.MORERESULTS, OPTIONS: '.' + CSS.OPTIONS, PICTURE: '.' + CSS.USER + ' .userpicture', RESULTSCOUNT: '.' + CSS.RESULTSCOUNT, RESULTSUSERS: '.' + CSS.SEARCHRESULTS + ' .' + CSS.USERS, SEARCHBTN: '.' + CSS.SEARCHBTN, SEARCHFIELD: '.' + CSS.SEARCHFIELD, SELECTEDNAMES: '.felement .selectednames', TRIGGER: '.gradereport_history_plugin input.selectortrigger', USER: '.' + CSS.USER, USERFULLNAMES: 'input[name="userfullnames"]', USERIDS: 'input[name="userids"]', USERSELECT: '.' + CSS.CHECKBOX + ' input[type=checkbox]' }; /** * User Selector. * * @namespace M.gradereport_history * @class UserSelector * @constructor */ var USERSELECTOR = function() { USERSELECTOR.superclass.constructor.apply(this, arguments); }; Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.core.dialogue, { /** * Whether or not this is the first time the user displays the dialogue within that request. * * @property _firstDisplay * @type Boolean * @private */ _firstDisplay: true, /** * The list of all the users selected while the dialogue is open. * * @type Object * @property _usersBufferList * @private */ _usersBufferList: null, /** * The Node on which the focus is set. * * @property _userTabFocus * @type Node * @private */ _userTabFocus: null, /** * Compiled template function for a user node. * * @property _userTemplate * @type Function * @private */ _userTemplate: null, initializer: function() { var bb = this.get('boundingBox'), content, params, tpl; tpl = Y.Handlebars.compile( '
                  ' + '
                  ' + '' + '
                  ' + '
                  ' + '
                  ' + '
                  ' + '
                  ' + '{{get_string ' + '
                  ' + '
                  ' + '' + '
                  ' + '
                  ' + '
                  ' + '
                  '); content = Y.Node.create( tpl({ COMPONENT: COMPONENT, CSS: CSS, loadingIcon: M.util.image_url('i/loading', 'moodle') }) ); // Set the title and content. this.getStdModNode(Y.WidgetStdMod.HEADER).prepend(Y.Node.create('

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

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

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

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

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

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

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

                  ")),this.setStdModContent(T.WidgetStdMod.BODY,s,T.WidgetStdMod.REPLACE),this.get("boundingBox").one(".moodle-dialogue-wrap").addClass("moodle-dialogue-content"),T.one(C.TRIGGER).on("click",this.show,this),e.one(C.FINISHBTN).on("click",this.finishSelectingUsers,this),e.delegate("key",this.userKeyboardNavigation,"down:38,40",C.AJAXCONTENT,this),T.delegate("click",this.selectUser,C.AJAXCONTENT,C.USERSELECT,this),T.delegate("click",this.selectUser,C.AJAXCONTENT,C.PICTURE,this),(s=this.get(p.PARAMS)).id=this.get(p.COURSEID),this.set(p.PARAMS,s),e.one(C.SEARCHBTN).on("click",this.search,this,!1)},show:function(e){var t;return this._usersBufferList=T.clone(this.get(p.SELECTEDUSERS)),this._firstDisplay?(this._firstDisplay=!1,this.search(e,!1)):((t=this.get("boundingBox")).all(C.USER).each(function(e){this.markUserNode(e,!1)},this),T.Object.each(this._usersBufferList,function(e,s){s=t.one(C.USER+'[data-userid="'+s+'"]');s&&this.markUserNode(s,!0)},this),this.setUserTabFocus(t.one(C.USER))),T.namespace("M.gradereport_history.UserSelector").superclass.show.call(this)},search:function(e,s){e&&e.preventDefault(),s?this.set(p.PAGE,this.get(p.PAGE)+1):(this.set(p.USERCOUNT,0),this.set(p.PAGE,0)),(e=this.get(p.PARAMS)).sesskey=M.cfg.sesskey,e.action="searchusers",e.search=this.get("boundingBox").one(C.SEARCHFIELD).get("value"),e.page=this.get(p.PAGE),e.perpage=this.get(p.PERPAGE),T.io(M.cfg.wwwroot+this.get(p.AJAXURL),{method:"POST",data:window.build_querystring(e),on:{start:this.preSearch,complete:this.processSearchResults,end:this.postSearch},context:this,arguments:{append:s}})},preSearch:function(e,s){var t=this.get("boundingBox");t.one(C.LIGHTBOX).removeClass(R.HIDDEN),s.append||t.one(C.RESULTSCOUNT).setHTML(M.util.get_string("loading","admin"))},postSearch:function(e,s){var t=this.get("boundingBox"),i=t.one(C.FIRSTADDED);t.one(C.LIGHTBOX).addClass(R.HIDDEN),s.append&&i?(this.setUserTabFocus(i),i.one(C.USERSELECT).focus()):(s=t.one(C.USER))&&this.setUserTabFocus(s)},processSearchResults:function(S,e,s){var t,d,i,r,E,a,n,o=!1,u=!1,l=this.get("boundingBox"),c=!0;try{(o=T.JSON.parse(e.responseText)).success&&!o.error||(u=!0)}catch(h){u=!0}if(u)return this.setContent(""),void l.one(C.RESULTSCOUNT).setHTML(M.util.get_string("errajaxsearch",g));for(E in t=s.append?l.one(C.RESULTSUSERS):T.Node.create('
                  '),this._userTemplate||(this._userTemplate=T.Handlebars.compile('
                  {{{picture}}}
                  {{{extrafields}}}
                  ')),d=this._userTemplate,i=this.get(p.USERCOUNT),r="",o.response.users)i++, n=o.response.users[E],r=!!T.Object.hasKey(this._usersBufferList,n.userid),n=T.Node.create(d({checkboxId:T.guid(),COMPONENT:g,count:i,CSS:R,extrafields:n.extrafields,extraFieldsId:T.guid(),fullname:n.fullname,picture:n.picture,userId:n.userid,USP:p})),this.markUserNode(n,r),s.append&&c&&(t.all(C.FIRSTADDED).removeClass(R.FIRSTADDED),n.addClass(R.FIRSTADDED),c=!1),t.append(n);this.set(p.USERCOUNT,i),e=parseInt(o.response.totalusers,10),s.append?e<=(this.get(p.PAGE)+1)*this.get(p.PERPAGE)&&l.one(C.MORERESULTS).remove():(0===e?(l.one(C.RESULTSCOUNT).setHTML(M.util.get_string("noresults","moodle")),a=""):(1===e?l.one(C.RESULTSCOUNT).setHTML(M.util.get_string("foundoneuser",g)):l.one(C.RESULTSCOUNT).setHTML(M.util.get_string("foundnusers",g,e)),a=T.Node.create('
                  ').append(t),o.response.totalusers>(this.get(p.PAGE)+1)*this.get(p.PERPAGE)&&((u=T.Node.create('")).one("a").on("click",this.search,this,!0),u.one("a").on("key",this.search,"space",this,!0),a.append(u))),this.setContent(a))},finishSelectingUsers:function(e){e.preventDefault(),this.applySelection(),this.hide()},applySelection:function(){var e=T.Object.keys(this._usersBufferList);this.set(p.SELECTEDUSERS,T.clone(this._usersBufferList)).setNameDisplay(),T.one(C.USERIDS).set("value",e.join())},selectUser:function(e){var s=e.currentTarget.ancestor(C.USER),t=s.one(C.USERSELECT),i=s.one(C.FULLNAME).get("innerHTML"),r=t.get("checked"),a=s.getData("userid");(r=e.currentTarget!==t?!r:r)?this._usersBufferList[a]=i:(delete this._usersBufferList[a],delete this._usersBufferList[parseInt(a,10)]),this.markUserNode(s,r)},markUserNode:function(e,s){return s?e.addClass(R.SELECTED).set("aria-selected",!0).one(C.USERSELECT).set("checked",!0):e.removeClass(R.SELECTED).set("aria-selected",!1).one(C.USERSELECT).set("checked",!1),this},setContent:function(e){return this.get("boundingBox").one(C.AJAXCONTENT).setHTML(e),this},setNameDisplay:function(){var e=T.Object.values(this.get(p.SELECTEDUSERS));T.one(C.SELECTEDNAMES).set("innerHTML",e.join(", ")),T.one(C.USERFULLNAMES).set("value",e.join())},userKeyboardNavigation:function(e){var s=this.get("boundingBox").all(C.USER),t=1,i=e.target.ancestor(C.USER,!0);38===e.keyCode&&(t=-1),(s=this.findFocusableUser(s,i,t))&&(e.preventDefault(),s.one(C.USERSELECT).focus(),this.setUserTabFocus(s))},findFocusableUser:function(e,s,t){s=e.indexOf(s);return e.size()<1?null:s<0?e.item(0):((s+=t)<0?s=e.size()-1:s>=e.size()&&(s=0),e.item(s))},setUserTabFocus:function(e){this._userTabFocus&&this._userTabFocus.setAttribute("tabindex","-1"),e&&(this._userTabFocus=e.one(C.USERSELECT),this._userTabFocus.setAttribute("tabindex","0"),this.get("boundingBox").one(C.RESULTSUSERS).setAttribute("aria-activedescendant",this._userTabFocus.generateID()))}},{NAME:p.NAME,CSS_PREFIX:p.CSS_PREFIX,ATTRS:{title:{validator:T.Lang.isString,valueFn:function(){return M.util.get_string("selectusers",g)}},url:{validator:T.Lang.isString,value:null},ajaxurl:{validator:T.Lang.isString,value:null},selectedUsers:{validator:T.Lang.isObject,value:null,getter:function(e){return null===e?{}:e}},courseid:{value:null},params:{validator:T.Lang.isArray,value:[]},page:{validator:T.Lang.isNumber,value:0},userCount:{value:0,validator:T.Lang.isNumber},perPage:{value:25,Validator:T.Lang.isNumber}}}),T.Base.modifyAttrs(T.namespace("M.gradereport_history.UserSelector"),{extraClasses:{value:["gradereport_history_usp"]},focusOnPreviousTargetAfterHide:{value:!0},width:{value:"500px"},visible:{value:!1},modal:{value:!0},draggable:{value:!0}}),T.namespace("M.gradereport_history.UserSelector").init=function(e){return new s(e)}},"@VERSION@",{requires:["escape","event-delegate","event-key","handlebars","io-base","json-parse","moodle-core-notification-dialogue"]});src/autosave/build.json000064400000000267152267015160011166 0ustar00{ "name": "moodle-mod_quiz-autosave", "builds": { "moodle-mod_quiz-autosave": { "jsfiles": [ "autosave.js" ] } } } src/autosave/meta/autosave.json000064400000000407152267015160012640 0ustar00{ "moodle-mod_quiz-autosave": { "requires": [ "base", "node", "event", "event-valuechange", "node-event-delegate", "io-form", "datatype-date-format" ] } } src/autosave/js/autosave.js000064400000034035152267015160011775 0ustar00// 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 . /** * Auto-save functionality for during quiz attempts. * * @module moodle-mod_quiz-autosave */ /** * Auto-save functionality for during quiz attempts. * * @class M.mod_quiz.autosave */ M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.autosave = { /** * The amount of time (in milliseconds) to wait between TinyMCE detections. * * @property TINYMCE_DETECTION_DELAY * @type Number * @default 500 * @private */ TINYMCE_DETECTION_DELAY: 500, /** * The number of times to try redetecting TinyMCE. * * @property TINYMCE_DETECTION_REPEATS * @type Number * @default 20 * @private */ TINYMCE_DETECTION_REPEATS: 20, /** * The delay (in milliseconds) between checking hidden input fields. * * @property WATCH_HIDDEN_DELAY * @type Number * @default 1000 * @private */ WATCH_HIDDEN_DELAY: 1000, /** * The number of failures to ignore before notifying the user. * * @property FAILURES_BEFORE_NOTIFY * @type Number * @default 1 * @private */ FAILURES_BEFORE_NOTIFY: 1, /** * The value to use when resetting the successful save counter. * * @property FIRST_SUCCESSFUL_SAVE * @static * @type Number * @default -1 * @private */ FIRST_SUCCESSFUL_SAVE: -1, /** * The selectors used throughout this class. * * @property SELECTORS * @private * @type Object * @static */ SELECTORS: { QUIZ_FORM: '#responseform', VALUE_CHANGE_ELEMENTS: 'input, textarea, [contenteditable="true"]', CHANGE_ELEMENTS: 'input, select', HIDDEN_INPUTS: 'input[type=hidden]', CONNECTION_ERROR: '#connection-error', CONNECTION_OK: '#connection-ok' }, /** * The script which handles the autosaves. * * @property AUTOSAVE_HANDLER * @type String * @default M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php' * @private */ AUTOSAVE_HANDLER: M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php', /** * The delay (in milliseconds) between a change being made, and it being auto-saved. * * @property delay * @type Number * @default 120000 * @private */ delay: 120000, /** * A Node reference to the form we are monitoring. * * @property form * @type Node * @default null */ form: null, /** * Whether the form has been modified since the last save started. * * @property dirty * @type boolean * @default false */ dirty: false, /** * Timer object for the delay between form modifaction and the save starting. * * @property delay_timer * @type Object * @default null * @private */ delay_timer: null, /** * Y.io transaction for the save ajax request. * * @property save_transaction * @type object * @default null */ save_transaction: null, /** * Failed saves count. * * @property savefailures * @type Number * @default 0 * @private */ savefailures: 0, /** * Properly bound key change handler. * * @property editor_change_handler * @type EventHandle * @default null * @private */ editor_change_handler: null, /** * Record of the value of all the hidden fields, last time they were checked. * * @property hidden_field_values * @type Object * @default {} */ hidden_field_values: {}, /** * Initialise the autosave code. * * @method init * @param {Number} delay the delay, in seconds, between a change being detected, and * a save happening. */ init: function(delay) { this.form = Y.one(this.SELECTORS.QUIZ_FORM); if (!this.form) { Y.log('No response form found. Why did you try to set up autosave?', 'debug', 'moodle-mod_quiz-autosave'); return; } this.delay = delay * 1000; this.form.delegate('valuechange', this.value_changed, this.SELECTORS.VALUE_CHANGE_ELEMENTS, this); this.form.delegate('change', this.value_changed, this.SELECTORS.CHANGE_ELEMENTS, this); this.form.on('submit', this.stop_autosaving, this); require(['core_form/events'], function(FormEvent) { window.addEventListener(FormEvent.eventTypes.uploadChanged, this.value_changed.bind(this)); }.bind(this)); this.init_tinymce(this.TINYMCE_DETECTION_REPEATS); this.save_hidden_field_values(); this.watch_hidden_fields(); }, save_hidden_field_values: function() { this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) { var name = hidden.get('name'); if (!name) { return; } this.hidden_field_values[name] = hidden.get('value'); }, this); }, watch_hidden_fields: function() { this.detect_hidden_field_changes(); Y.later(this.WATCH_HIDDEN_DELAY, this, this.watch_hidden_fields); }, detect_hidden_field_changes: function() { this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) { var name = hidden.get('name'), value = hidden.get('value'); if (!name) { return; } if (!(name in this.hidden_field_values) || value !== this.hidden_field_values[name]) { this.hidden_field_values[name] = value; this.value_changed({target: hidden}); } }, this); }, /** * Initialise watching of TinyMCE specifically. * * Because TinyMCE might load slowly, and after us, we need to keep * trying, until we detect TinyMCE is there, or enough time has passed. * This is based on the TINYMCE_DETECTION_DELAY and * TINYMCE_DETECTION_REPEATS properties. * * * @method init_tinymce * @param {Number} repeatcount The number of attempts made so far. */ init_tinymce: function(repeatcount) { if (typeof window.tinyMCE === 'undefined') { if (repeatcount > 0) { Y.later(this.TINYMCE_DETECTION_DELAY, this, this.init_tinymce, [repeatcount - 1]); } else { Y.log('Gave up looking for TinyMCE.', 'debug', 'moodle-mod_quiz-autosave'); } return; } Y.log('Found TinyMCE.', 'debug', 'moodle-mod_quiz-autosave'); this.editor_change_handler = Y.bind(this.editor_changed, this); if (window.tinyMCE.onAddEditor) { window.tinyMCE.onAddEditor.add(Y.bind(this.init_tinymce_editor, this)); } else if (window.tinyMCE.on) { var startSaveTimer = this.start_save_timer_if_necessary.bind(this); window.tinyMCE.on('AddEditor', function(event) { event.editor.on('Change Undo Redo keydown', startSaveTimer); }); // One or more editors might already have been added, so we have to attach // the event handlers to these as well. window.tinyMCE.get().forEach(function(editor) { editor.on('Change Undo Redo keydown', startSaveTimer); }); } }, /** * Initialise watching of a specific TinyMCE editor. * * @method init_tinymce_editor * @param {EventFacade} e * @param {Object} editor The TinyMCE editor object */ init_tinymce_editor: function(e, editor) { Y.log('Found TinyMCE editor ' + editor.id + '.', 'debug', 'moodle-mod_quiz-autosave'); editor.onChange.add(this.editor_change_handler); editor.onRedo.add(this.editor_change_handler); editor.onUndo.add(this.editor_change_handler); editor.onKeyDown.add(this.editor_change_handler); }, value_changed: function(e) { var name = e.target.getAttribute('name'); if (name === 'thispage' || name === 'scrollpos' || (name && name.match(/_:flagged$/))) { return; // Not interesting. } // Fallback to the ID when the name is not present (in the case of content editable). name = name || '#' + e.target.getAttribute('id'); Y.log('Detected a value change in element ' + name + '.', 'debug', 'moodle-mod_quiz-autosave'); this.start_save_timer_if_necessary(); }, editor_changed: function(editor) { Y.log('Detected a value change in editor ' + editor.id + '.', 'debug', 'moodle-mod_quiz-autosave'); this.start_save_timer_if_necessary(); }, start_save_timer_if_necessary: function() { this.dirty = true; if (this.delay_timer || this.save_transaction) { // Already counting down or daving. return; } this.start_save_timer(); }, start_save_timer: function() { this.cancel_delay(); this.delay_timer = Y.later(this.delay, this, this.save_changes); }, cancel_delay: function() { if (this.delay_timer && this.delay_timer !== true) { this.delay_timer.cancel(); } this.delay_timer = null; }, save_changes: function() { this.cancel_delay(); this.dirty = false; if (this.is_time_nearly_over()) { Y.log('No more saving, time is nearly over.', 'debug', 'moodle-mod_quiz-autosave'); this.stop_autosaving(); return; } Y.log('Doing a save.', 'debug', 'moodle-mod_quiz-autosave'); if (typeof window.tinyMCE !== 'undefined') { window.tinyMCE.triggerSave(); } // YUI io.form incorrectly (in my opinion) sends the value of all submit // buttons in the ajax request. We don't want any submit buttons. // Therefore, temporarily change the type. // (Yes, this is a nasty hack. One day this will be re-written as AMD, hopefully). var allsubmitbuttons = this.form.all('input[type=submit], button[type=submit]'); allsubmitbuttons.setAttribute('type', 'button'); this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, { method: 'POST', form: {id: this.form}, on: { success: this.save_done, failure: this.save_failed }, context: this }); // Change the button types back. allsubmitbuttons.setAttribute('type', 'submit'); }, save_done: function(transactionid, response) { var autosavedata = JSON.parse(response.responseText); if (autosavedata.status !== 'OK') { // Because IIS is useless, Moodle can't send proper HTTP response // codes, so we have to detect failures manually. this.save_failed(transactionid, response); return; } if (typeof autosavedata.timeleft !== 'undefined') { Y.log('Updating timer: ' + autosavedata.timeleft + ' seconds remain.', 'debug', 'moodle-mod_quiz-timer'); M.mod_quiz.timer.updateEndTime(autosavedata.timeleft); } this.update_saved_time_display(); Y.log('Save completed.', 'debug', 'moodle-mod_quiz-autosave'); this.save_transaction = null; if (this.dirty) { Y.log('Dirty after save.', 'debug', 'moodle-mod_quiz-autosave'); this.start_save_timer(); } if (this.savefailures > 0) { Y.one(this.SELECTORS.CONNECTION_ERROR).hide(); Y.one(this.SELECTORS.CONNECTION_OK).show(); this.savefailures = this.FIRST_SUCCESSFUL_SAVE; } else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) { Y.one(this.SELECTORS.CONNECTION_OK).hide(); this.savefailures = 0; } }, save_failed: function() { Y.log('Save failed.', 'debug', 'moodle-mod_quiz-autosave'); this.save_transaction = null; // We want to retry soon. this.start_save_timer(); this.savefailures = Math.max(1, this.savefailures + 1); if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) { Y.one(this.SELECTORS.CONNECTION_ERROR).show(); Y.one(this.SELECTORS.CONNECTION_OK).hide(); } }, /** * Inform the user that their answers have been saved. * * @method update_saved_time_display */ update_saved_time_display: function() { // We fetch the current language's preferred time format from the language pack. require(['core/user_date', 'core/notification'], function(UserDate, Notification) { UserDate.get([{ timestamp: Math.floor(Date.now() / 1000), format: M.util.get_string('strftimedatetimeshortaccurate', 'langconfig'), }]).then(function(dateStrs) { var infoDiv = Y.one('#mod_quiz_navblock .othernav .autosave_info'); infoDiv.set('text', M.util.get_string('lastautosave', 'quiz', dateStrs[0])); infoDiv.show(); return; }).catch(Notification.exception); }); }, is_time_nearly_over: function() { return M.mod_quiz.timer && M.mod_quiz.timer.endtime && (new Date().getTime() + 2 * this.delay) > M.mod_quiz.timer.endtime; }, stop_autosaving: function() { this.cancel_delay(); this.delay_timer = true; if (this.save_transaction) { this.save_transaction.abort(); } } }; src/util/js/slot.js000064400000034233152267015160010255 0ustar00/** * A collection of utility classes for use with slots. * * @module moodle-mod_quiz-util * @submodule moodle-mod_quiz-util-slot */ Y.namespace('Moodle.mod_quiz.util.slot'); /** * A collection of utility classes for use with slots. * * @class Moodle.mod_quiz.util.slot * @static */ Y.Moodle.mod_quiz.util.slot = { CSS: { SLOT: 'slot', QUESTIONTYPEDESCRIPTION: 'qtype_description', CANNOT_DEPEND: 'question_dependency_cannot_depend' }, CONSTANTS: { SLOTIDPREFIX: 'slot-', QUESTION: M.util.get_string('question', 'moodle') }, SELECTORS: { SLOT: 'li.slot', INSTANCENAME: '.instancename', NUMBER: 'span.slotnumber', PAGECONTENT: 'div#page-content', PAGEBREAK: 'span.page_split_join_wrapper', ICON: '.icon', QUESTIONTYPEDESCRIPTION: '.qtype_description', SECTIONUL: 'ul.section', DEPENDENCY_WRAPPER: '.question_dependency_wrapper', DEPENDENCY_LINK: '.question_dependency_wrapper .cm-edit-action', DEPENDENCY_ICON: '.question_dependency_wrapper .icon' }, /** * Retrieve the slot item from one of it's child Nodes. * * @method getSlotFromComponent * @param slotcomponent {Node} The component Node. * @return {Node|null} The Slot Node. */ getSlotFromComponent: function(slotcomponent) { return Y.one(slotcomponent).ancestor(this.SELECTORS.SLOT, true); }, /** * Determines the slot ID for the provided slot. * * @method getId * @param slot {Node} The slot to find an ID for. * @return {Number|false} The ID of the slot in question or false if no ID was found. */ getId: function(slot) { // We perform a simple substitution operation to get the ID. var id = slot.get('id').replace( this.CONSTANTS.SLOTIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Determines the slot name for the provided slot. * * @method getName * @param slot {Node} The slot to find a name for. * @return {string|false} The name of the slot in question or false if no ID was found. */ getName: function(slot) { var instance = slot.one(this.SELECTORS.INSTANCENAME); if (instance) { return instance.get('firstChild').get('data'); } return null; }, /** * Determines the slot number for the provided slot. * * @method getNumber * @param slot {Node} The slot to find the number for. * @return {Number|false} The number of the slot in question or false if no number was found. */ getNumber: function(slot) { if (!slot) { return false; } // We perform a simple substitution operation to get the number. var number = slot.one(this.SELECTORS.NUMBER).get('text').replace( this.CONSTANTS.QUESTION, ''); // Attempt to validate the ID. number = parseInt(number, 10); if (typeof number === 'number' && isFinite(number)) { return number; } return false; }, /** * Updates the slot number for the provided slot. * * @method setNumber * @param slot {Node} The slot to update the number for. * @return void */ setNumber: function(slot, number) { var numbernode = slot.one(this.SELECTORS.NUMBER); numbernode.setHTML('' + this.CONSTANTS.QUESTION + ' ' + number); }, /** * Returns a list of all slot elements on the page. * * @method getSlots * @return {node[]} An array containing slot nodes. */ getSlots: function() { return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT); }, /** * Returns a list of all slot elements on the page that have numbers. Excudes description questions. * * @method getSlots * @return {node[]} An array containing slot nodes. */ getNumberedSlots: function() { var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL; selector += ' ' + this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'; return Y.all(selector); }, /** * Returns the previous slot to the given slot. * * @method getPrevious * @param slot Slot node * @return {node|false} The previous slot node or false. */ getPrevious: function(slot) { return slot.previous(this.SELECTORS.SLOT); }, /** * Returns the previous numbered slot to the given slot. * * Ignores slots containing description question types. * * @method getPrevious * @param slot Slot node * @return {node|false} The previous slot node or false. */ getPreviousNumbered: function(slot) { var previous = slot.previous(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'); if (previous) { return previous; } var section = slot.ancestor('li.section').previous('li.section'); while (section) { var questions = section.all(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'); if (questions.size() > 0) { return questions.item(questions.size() - 1); } section = section.previous('li.section'); } return false; }, /** * Reset the order of the numbers given to each slot. * * @method reorderSlots * @return void */ reorderSlots: function() { // Get list of slot nodes. var slots = this.getSlots(); // Loop through slots incrementing the number each time. slots.each(function(slot) { if (!Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot)) { // Move the next page to the front. var nextpage = slot.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE); slot.swap(nextpage); } var previousSlot = this.getPreviousNumbered(slot), previousslotnumber = 0; if (slot.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION)) { return; } if (previousSlot) { previousslotnumber = this.getNumber(previousSlot); } // Set slot number. this.setNumber(slot, previousslotnumber + 1); }, this); }, /** * Add class only-has-one-slot to those sections that need it. * * @method updateOneSlotSections * @return void */ updateOneSlotSections: function() { Y.all('.mod-quiz-edit-content ul.slots li.section').each(function(section) { if (section.all(this.SELECTORS.SLOT).size() > 1) { section.removeClass('only-has-one-slot'); } else { section.addClass('only-has-one-slot'); } }, this); }, /** * Remove a slot and related elements from the list of slots. * * @method remove * @param slot Slot node * @return void */ remove: function(slot) { var page = Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot); slot.remove(); // Is the page empty. if (!Y.Moodle.mod_quiz.util.page.isEmpty(page)) { return; } // If so remove it. Including add menu and page break. Y.Moodle.mod_quiz.util.page.remove(page); }, /** * Returns a list of all page break elements on the page. * * @method getPageBreaks * @return {node[]} An array containing page break nodes. */ getPageBreaks: function() { var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL; selector += ' ' + this.SELECTORS.SLOT + this.SELECTORS.PAGEBREAK; return Y.all(selector); }, /** * Retrieve the page break element item from the given slot. * * @method getPageBreak * @param slot Slot node * @return {Node|null} The Page Break Node. */ getPageBreak: function(slot) { return Y.one(slot).one(this.SELECTORS.PAGEBREAK); }, /** * Add a page break and related elements to the list of slots. * * @method addPageBreak * @param beforenode Int | Node | HTMLElement | String to add * @return pagebreak PageBreak node */ addPageBreak: function(slot) { var nodetext = M.mod_quiz.resource_toolbox.get('config').addpageiconhtml; nodetext = nodetext.replace('%%SLOT%%', this.getNumber(slot)); var pagebreak = Y.Node.create(nodetext); slot.one('div').insert(pagebreak, 'after'); return pagebreak; }, /** * Remove a pagebreak from the given slot. * * @method removePageBreak * @param slot Slot node * @return boolean */ removePageBreak: function(slot) { var pagebreak = this.getPageBreak(slot); if (!pagebreak) { return false; } pagebreak.remove(); return true; }, /** * Reorder each pagebreak by iterating through each related slot. * * @method reorderPageBreaks * @return void */ reorderPageBreaks: function() { // Get list of slot nodes. var slots = this.getSlots(); var slotnumber = 0; // Loop through slots incrementing the number each time. slots.each(function(slot, key) { slotnumber++; var pagebreak = this.getPageBreak(slot); var nextitem = slot.next('li.activity'); if (!nextitem) { // Last slot in a section. Should not have an icon. return; } // No pagebreak and not last slot. Add one. if (!pagebreak) { pagebreak = this.addPageBreak(slot); } // Remove last page break if there is one. if (pagebreak && key === slots.size() - 1) { this.removePageBreak(slot); } // Get page break anchor element. var pagebreaklink = pagebreak.get('childNodes').item(0); // Get the correct title. var action = ''; var iconname = ''; if (Y.Moodle.mod_quiz.util.page.isPage(nextitem)) { action = 'removepagebreak'; iconname = 'e/remove_page_break'; } else { action = 'addpagebreak'; iconname = 'e/insert_page_break'; } // Update the link and image titles pagebreaklink.set('title', M.util.get_string(action, 'quiz')); pagebreaklink.setData('action', action); // Update the image title. var icon = pagebreaklink.one(this.SELECTORS.ICON); icon.set('title', M.util.get_string(action, 'quiz')); icon.set('alt', M.util.get_string(action, 'quiz')); // Update the image src. icon.set('src', M.util.image_url(iconname)); // Get anchor url parameters as an associative array. var params = Y.QueryString.parse(pagebreaklink.get('href')); // Update slot number. params.slot = slotnumber; // Create the new url. var newurl = ''; for (var index in params) { if (newurl.length) { newurl += "&"; } newurl += index + "=" + params[index]; } // Update the anchor. pagebreaklink.set('href', newurl); }, this); }, /** * Update the dependency icons. * * @method updateAllDependencyIcons * @return void */ updateAllDependencyIcons: function() { // Get list of slot nodes. var slots = this.getSlots(), slotnumber = 0, previousslot = null; // Loop through slots incrementing the number each time. slots.each(function(slot) { slotnumber++; if (slotnumber == 1 || previousslot.getData('canfinish') === '0') { slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).addClass(this.CSS.CANNOT_DEPEND); } else { slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).removeClass(this.CSS.CANNOT_DEPEND); } this.updateDependencyIcon(slot, null); previousslot = slot; }, this); }, /** * Update the slot icon to indicate the new requiresprevious state. * * @method slot Slot node * @method requiresprevious Whether this node now requires the previous one. * @return void */ updateDependencyIcon: function(slot, requiresprevious) { var link = slot.one(this.SELECTORS.DEPENDENCY_LINK); var icon = slot.one(this.SELECTORS.DEPENDENCY_ICON); var previousSlot = this.getPrevious(slot); var a = {thisq: this.getNumber(slot)}; if (previousSlot) { a.previousq = this.getNumber(previousSlot); } if (requiresprevious === null) { requiresprevious = link.getData('action') === 'removedependency'; } if (requiresprevious) { link.set('title', M.util.get_string('questiondependencyremove', 'quiz', a)); link.setData('action', 'removedependency'); window.require(['core/templates'], function(Templates) { Templates.renderPix('t/locked', 'core', M.util.get_string('questiondependsonprevious', 'quiz')).then( function(html) { icon.replace(html); } ); }); } else { link.set('title', M.util.get_string('questiondependencyadd', 'quiz', a)); link.setData('action', 'adddependency'); window.require(['core/templates'], function(Templates) { Templates.renderPix('t/unlocked', 'core', M.util.get_string('questiondependencyfree', 'quiz')).then( function(html) { icon.replace(html); } ); }); } } }; src/util/js/page.js000064400000024203152267015160010204 0ustar00/* global YUI */ /** * A collection of utility classes for use with pages. * * @module moodle-mod_quiz-util * @submodule moodle-mod_quiz-util-page */ Y.namespace('Moodle.mod_quiz.util.page'); /** * A collection of utility classes for use with pages. * * @class Moodle.mod_quiz.util.page * @static */ Y.Moodle.mod_quiz.util.page = { CSS: { PAGE: 'page' }, CONSTANTS: { ACTIONMENUIDPREFIX: 'action-menu-', ACTIONMENUBARIDSUFFIX: '-menubar', ACTIONMENUMENUIDSUFFIX: '-menu', PAGEIDPREFIX: 'page-', PAGENUMBERPREFIX: M.util.get_string('page', 'moodle') + ' ' }, SELECTORS: { ACTIONMENU: 'div.moodle-actionmenu', ACTIONMENUBAR: '.menubar', ACTIONMENUMENU: '.menu', ADDASECTION: '[data-action="addasection"]', PAGE: 'li.page', INSTANCENAME: '.instancename', NUMBER: 'h4' }, /** * Retrieve the page item from one of it's child Nodes. * * @method getPageFromComponent * @param pagecomponent {Node} The component Node. * @return {Node|null} The Page Node. */ getPageFromComponent: function(pagecomponent) { return Y.one(pagecomponent).ancestor(this.SELECTORS.PAGE, true); }, /** * Retrieve the page item from one of it's previous siblings. * * @method getPageFromSlot * @param pagecomponent {Node} The component Node. * @return {Node|null} The Page Node. */ getPageFromSlot: function(slot) { return Y.one(slot).previous(this.SELECTORS.PAGE); }, /** * Returns the page ID for the provided page. * * @method getId * @param page {Node} The page to find an ID for. * @return {Number|false} The ID of the page in question or false if no ID was found. */ getId: function(page) { // We perform a simple substitution operation to get the ID. var id = page.get('id').replace( this.CONSTANTS.PAGEIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Updates the page id for the provided page. * * @method setId * @param page {Node} The page to update the number for. * @param id int The id value. * @return void */ setId: function(page, id) { page.set('id', this.CONSTANTS.PAGEIDPREFIX + id); }, /** * Determines the page name for the provided page. * * @method getName * @param page {Node} The page to find a name for. * @return {string|false} The name of the page in question or false if no ID was found. */ getName: function(page) { var instance = page.one(this.SELECTORS.INSTANCENAME); if (instance) { return instance.get('firstChild').get('data'); } return null; }, /** * Determines the page number for the provided page. * * @method getNumber * @param page {Node} The page to find a number for. * @return {Number|false} The number of the page in question or false if no number was found. */ getNumber: function(page) { // We perform a simple substitution operation to get the number. var number = page.one(this.SELECTORS.NUMBER).get('text').replace( this.CONSTANTS.PAGENUMBERPREFIX, ''); // Attempt to validate the ID. number = parseInt(number, 10); if (typeof number === 'number' && isFinite(number)) { return number; } return false; }, /** * Updates the page number for the provided page. * * @method setNumber * @param page {Node} The page to update the number for. * @return void */ setNumber: function(page, number) { page.one(this.SELECTORS.NUMBER).set('text', this.CONSTANTS.PAGENUMBERPREFIX + number); }, /** * Returns a list of all page elements. * * @method getPages * @return {node[]} An array containing page nodes. */ getPages: function() { return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' + Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.PAGE); }, /** * Is the given element a page element? * * @method isPage * @param page Page node * @return boolean */ isPage: function(page) { if (!page) { return false; } return page.hasClass(this.CSS.PAGE); }, /** * Does the page have atleast one slot? * * @method isEmpty * @param page Page node * @return boolean */ isEmpty: function(page) { var activity = page.next('li.activity'); if (!activity) { return true; } return !activity.hasClass('slot'); }, /** * Add a page and related elements to the list of slots. * * @method add * @param beforenode Int | Node | HTMLElement | String to add * @return page Page node */ add: function(beforenode) { var pagenumber = this.getNumber(this.getPageFromSlot(beforenode)) + 1; var pagehtml = M.mod_quiz.resource_toolbox.get('config').pagehtml; // Normalise the page number. pagehtml = pagehtml.replace(/%%PAGENUMBER%%/g, pagenumber); // Create the page node. var page = Y.Node.create(pagehtml); // Assign is as a drop target. YUI().use('dd-drop', function(Y) { var drop = new Y.DD.Drop({ node: page, groups: M.mod_quiz.dragres.groups }); page.drop = drop; }); // Insert in the correct place. beforenode.insert(page, 'after'); // Enhance the add menu to make if fully visible and clickable. if (typeof M.core.actionmenu !== "undefined") { M.core.actionmenu.newDOMNode(page); } return page; }, /** * Remove a page and related elements from the list of slots. * * @method remove * @param page Page node * @return void */ remove: function(page, keeppagebreak) { // Remove page break from previous slot. var previousslot = page.previous(Y.Moodle.mod_quiz.util.slot.SELECTORS.SLOT); if (!keeppagebreak && previousslot) { Y.Moodle.mod_quiz.util.slot.removePageBreak(previousslot); } page.remove(); }, /** * Reset the order of the numbers given to each page. * * @method reorderPages * @return void */ reorderPages: function() { // Get list of page nodes. var pages = this.getPages(); var currentpagenumber = 0; // Loop through pages incrementing the number each time. pages.each(function(page) { // Is the page empty? if (this.isEmpty(page)) { var keeppagebreak = page.next('li.slot') ? true : false; this.remove(page, keeppagebreak); return; } currentpagenumber++; // Set page number. this.setNumber(page, currentpagenumber); this.setId(page, currentpagenumber); }, this); // Reorder action menus this.reorderActionMenus(); }, /** * Reset the order of the numbers given to each action menu. * * @method reorderActionMenus * @return void */ reorderActionMenus: function() { // Get list of action menu nodes. var actionmenus = this.getActionMenus(); // Loop through pages incrementing the number each time. actionmenus.each(function(actionmenu, key) { var previousActionMenu = actionmenus.item(key - 1), previousActionMenunumber = 0; if (previousActionMenu) { previousActionMenunumber = this.getActionMenuId(previousActionMenu); } var id = previousActionMenunumber + 1; // Set menu id. this.setActionMenuId(actionmenu, id); // Update action-menu-1-menubar var menubar = actionmenu.one(this.SELECTORS.ACTIONMENUBAR); menubar.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUBARIDSUFFIX); // Update action-menu-1-menu var menumenu = actionmenu.one(this.SELECTORS.ACTIONMENUMENU); menumenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUMENUIDSUFFIX); // Update the URL of the add-section action. menumenu.one(this.SELECTORS.ADDASECTION).set('href', menumenu.one(this.SELECTORS.ADDASECTION).get('href').replace(/\baddsectionatpage=\d+\b/, 'addsectionatpage=' + id)); }, this); }, /** * Returns a list of all page elements. * * @method getActionMenus * @return {node[]} An array containing page nodes. */ getActionMenus: function() { return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' + Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.ACTIONMENU); }, /** * Returns the ID for the provided action menu. * * @method getId * @param actionmenu {Node} The actionmenu to find an ID for. * @return {Number|false} The ID of the actionmenu in question or false if no ID was found. */ getActionMenuId: function(actionmenu) { // We perform a simple substitution operation to get the ID. var id = actionmenu.get('id').replace( this.CONSTANTS.ACTIONMENUIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Updates the page id for the provided page. * * @method setId * @param page {Node} The page to update the number for. * @param id int The id value. * @return void */ setActionMenuId: function(actionmenu, id) { actionmenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id); } }; src/questionchooser/build.json000064400000000314152267015160012562 0ustar00{ "name": "moodle-mod_quiz-questionchooser", "builds": { "moodle-mod_quiz-questionchooser": { "jsfiles": [ "questionchooser.js" ] } } } src/questionchooser/meta/questionchooser.json000064400000000302152267015160015640 0ustar00{ "moodle-mod_quiz-questionchooser": { "requires": [ "moodle-core-chooserdialogue", "moodle-mod_quiz-util", "querystring-parse" ] } } src/questionchooser/js/questionchooser.js000064400000004767152267015160015014 0ustar00var CSS = { ADDNEWQUESTIONBUTTONS: '.menu [data-action="addquestion"]', CREATENEWQUESTION: 'div.createnewquestion', CHOOSERDIALOGUE: 'div.chooserdialoguebody', CHOOSERHEADER: 'div.choosertitle' }; /** * The questionchooser class is responsible for instantiating and displaying the question chooser * when viewing a quiz in editing mode. * * @class questionchooser * @constructor * @protected * @extends M.core.chooserdialogue */ var QUESTIONCHOOSER = function() { QUESTIONCHOOSER.superclass.constructor.apply(this, arguments); }; Y.extend(QUESTIONCHOOSER, M.core.chooserdialogue, { initializer: function() { Y.one('body').delegate('click', this.display_dialogue, CSS.ADDNEWQUESTIONBUTTONS, this); }, display_dialogue: function(e) { e.preventDefault(); var dialogue = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERDIALOGUE), header = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.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 parameters = Y.QueryString.parse(e.currentTarget.get('search').substring(1)); var form = this.container.one('form'); this.parameters_to_hidden_input(parameters, form, 'returnurl'); this.parameters_to_hidden_input(parameters, form, 'cmid'); this.parameters_to_hidden_input(parameters, form, 'category'); this.parameters_to_hidden_input(parameters, form, 'addonpage'); this.parameters_to_hidden_input(parameters, form, 'appendqnumstring'); // Display the chooser dialogue. this.display_chooser(e); }, parameters_to_hidden_input: function(parameters, form, name) { var value; if (parameters.hasOwnProperty(name)) { value = parameters[name]; } else { value = ''; } var input = form.one('input[name=' + name + ']'); if (!input) { input = form.appendChild(''); input.set('name', name); } input.set('value', value); } }, { NAME: 'mod_quiz-questionchooser' }); M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.init_questionchooser = function() { M.mod_quiz.question_chooser = new QUESTIONCHOOSER({}); return M.mod_quiz.question_chooser; }; src/quizbase/build.json000064400000000267152267015160011162 0ustar00{ "name": "moodle-mod_quiz-quizbase", "builds": { "moodle-mod_quiz-quizbase": { "jsfiles": [ "quizbase.js" ] } } } src/quizbase/meta/quizbase.json000064400000000163152267015160012627 0ustar00{ "moodle-mod_quiz-quizbase": { "requires": [ "base", "node" ] } } src/quizbase/js/quizbase.js000064400000010412152267015160011756 0ustar00/** * The quizbase class to provide shared functionality to Modules within Moodle. * * @module moodle-mod_quiz-quizbase */ var QUIZBASENAME = 'mod_quiz-quizbase'; var QUIZBASE = function() { QUIZBASE.superclass.constructor.apply(this, arguments); }; /** * The coursebase class to provide shared functionality to Modules within * Moodle. * * @class M.course.coursebase * @constructor */ Y.extend(QUIZBASE, 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: QUIZBASENAME, ATTRS: {} }); // Ensure that M.course exists and that coursebase is initialised correctly M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.quizbase = M.mod_quiz.quizbase || new QUIZBASE(); // Abstract functions that needs to be defined per format (course/format/somename/format.js) M.mod_quiz.edit = M.mod_quiz.edit || {}; /** * Swap section (should be defined in format.js if requred) * * @param {YUI} Y YUI3 instance * @param {string} node1 node to swap to * @param {string} node2 node to swap with * @return {NodeList} section list */ M.mod_quiz.edit.swap_sections = function(Y, node1, node2) { var CSS = { COURSECONTENT: 'mod-quiz-edit-content', SECTIONADDMENUS: 'section_add_menus' }; var sectionlist = Y.Node.all('.' + CSS.COURSECONTENT + ' li.section'); // Swap menus. sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS)); }; /** * 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. * * @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 * @return void */ M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) { var CSS = { SECTIONNAME: 'sectionname' }, SELECTORS = { SECTIONLEFTSIDE: '.left .section-handle .icon' }; if (response.action === 'move') { // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates. if (sectionfrom > sectionto) { var temp = sectionto; sectionto = sectionfrom; sectionfrom = temp; } // Update titles and move icons in all affected sections. var ele, str, stridx, newstr; for (var i = sectionfrom; i <= sectionto; i++) { // Update section title. sectionlist.item(i).one('.' + CSS.SECTIONNAME).setContent(response.sectiontitles[i]); // Update move icon. ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE); str = ele.getAttribute('alt'); stridx = str.lastIndexOf(' '); newstr = str.substr(0, stridx + 1) + i; ele.setAttribute('alt', newstr); ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed. // Remove the current class as section has been moved. sectionlist.item(i).removeClass('current'); } // If there is a current section, apply corresponding class in order to highlight it. if (response.current !== -1) { // Add current class to the required section. sectionlist.item(response.current).addClass('current'); } } }; src/toolboxes/js/resource.js000064400000052530152267015160012164 0ustar00/** * Resource and activity toolbox class. * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a quiz in editing mode. * * @module mod_quiz-resource-toolbox * @namespace M.mod_quiz.resource_toolbox */ /** * Resource and activity toolbox class. * * This is a class extending TOOLBOX containing code specific to resources * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a quiz in editing mode. * * @class resources * @constructor * @extends M.course.toolboxes.toolbox */ var RESOURCETOOLBOX = function() { RESOURCETOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(RESOURCETOOLBOX, TOOLBOX, { /** * An Array of events added when editing a max mark field. * These should all be detached when editing is complete. * * @property editmaxmarkevents * @protected * @type Array * @protected */ editmaxmarkevents: [], /** * */ NODE_PAGE: 1, NODE_SLOT: 2, NODE_JOIN: 3, /** * Initialize the resource toolbox * * For each activity the commands are updated and a reference to the activity is attached. * This way it doesn't matter where the commands are going to called from they have a reference to the * activity that they relate to. * This is essential as some of the actions are displayed in an actionmenu which removes them from the * page flow. * * This function also creates a single event delegate to manage all AJAX actions for all activities on * the page. * * @method initializer * @protected */ initializer: function() { M.mod_quiz.quizbase.register_module(this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this); this.initialise_select_multiple(); }, /** * Initialize the select multiple options * * Add actions to the buttons that enable multiple slots to be selected and managed at once. * * @method initialise_select_multiple * @protected */ initialise_select_multiple: function() { // Click select multiple button to show the select all options. Y.one(SELECTOR.SELECTMULTIPLEBUTTON).on('click', function(e) { e.preventDefault(); Y.one('body').addClass(CSS.SELECTMULTIPLE); }); // Click cancel button to show the select all options. Y.one(SELECTOR.SELECTMULTIPLECANCELBUTTON).on('click', function(e) { e.preventDefault(); Y.one('body').removeClass(CSS.SELECTMULTIPLE); }); // Assign the delete method to the delete multiple button. Y.delegate('click', this.delete_multiple_action, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this); }, /** * Handles the delegation event. When this is fired someone has triggered an action. * * Note not all actions will result in an AJAX enhancement. * * @protected * @method handle_data_action * @param {EventFacade} ev The event that was triggered. * @returns {boolean} */ handle_data_action: function(ev) { // We need to get the anchor element that triggered this event. var node = ev.target; if (!node.test('a')) { node = node.ancestor(SELECTOR.ACTIVITYACTION); } // From the anchor we can get both the activity (added during initialisation) and the action being // performed (added by the UI as a data attribute). var action = node.getData('action'), activity = node.ancestor(SELECTOR.ACTIVITYLI); if (!node.test('a') || !action || !activity) { // It wasn't a valid action node. return; } // Switch based upon the action and do the desired thing. switch (action) { case 'editmaxmark': // The user wishes to edit the maxmark of the resource. this.edit_maxmark(ev, node, activity, action); break; case 'delete': // The user is deleting the activity. this.delete_with_confirmation(ev, node, activity, action); break; case 'addpagebreak': case 'removepagebreak': // The user is adding or removing a page break. this.update_page_break(ev, node, activity, action); break; case 'adddependency': case 'removedependency': // The user is adding or removing a dependency between questions. this.update_dependency(ev, node, activity, action); break; default: // Nothing to do here! break; } }, /** * Add a loading icon to the specified activity. * * The icon is added within the action area. * * @method add_spinner * @param {Node} activity The activity to add a loading icon to * @return {Node|null} The newly created icon, or null if the action area was not found. */ add_spinner: function(activity) { var actionarea = activity.one(SELECTOR.ACTIONAREA); if (actionarea) { return M.util.add_spinner(Y, actionarea); } return null; }, /** * Deletes the given activity or resource after confirmation. * * @protected * @method delete_with_confirmation * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. */ delete_with_confirmation: function(ev, button, activity) { // Prevent the default button action. ev.preventDefault(); // Get the element we're working on. var element = activity; // Create confirm string (different if element has or does not have name) var qtypename = M.util.get_string( 'pluginname', 'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1] ); // Create the confirmation dialogue. require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('confirmremovequestion', 'quiz', qtypename), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = this.add_spinner(element); var data = { 'class': 'resource', 'action': 'DELETE', 'id': Y.Moodle.mod_quiz.util.slot.getId(element) }; this.send_request(data, spinner, function(response) { if (response.deleted) { // Actually remove the element. Y.Moodle.mod_quiz.util.slot.remove(element); this.reorganise_edit_page(); if (M.core.actionmenu && M.core.actionmenu.instance) { M.core.actionmenu.instance.hideMenu(ev); } } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Finds the section that would become empty if we remove the selected slots. * * @protected * @method find_sections_that_would_become_empty * @returns {String} The name of the first section found */ find_sections_that_would_become_empty: function() { var section; var sectionnodes = Y.all(SELECTOR.SECTIONLI); if (sectionnodes.size() > 1) { sectionnodes.some(function(node) { var sectionname = node.one(SELECTOR.INSTANCESECTION).getContent(); var checked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked'); var unchecked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':not(:checked)'); if (!checked.isEmpty() && unchecked.isEmpty()) { section = sectionname; } return section; }); } return section; }, /** * Takes care of what needs to happen when the user clicks on the delete multiple button. * * @protected * @method delete_multiple_action * @param {EventFacade} ev The event that was fired. */ delete_multiple_action: function(ev) { var problemsection = this.find_sections_that_would_become_empty(); if (typeof problemsection !== 'undefined') { require(['core/notification'], function(Notification) { Notification.alert( M.util.get_string('cannotremoveslots', 'quiz'), M.util.get_string('cannotremoveallsectionslots', 'quiz', problemsection) ); }); } else { this.delete_multiple_with_confirmation(ev); } }, /** * Deletes the given activities or resources after confirmation. * * @protected * @method delete_multiple_with_confirmation * @param {EventFacade} ev The event that was fired. */ delete_multiple_with_confirmation: function(ev) { ev.preventDefault(); var ids = ''; var slots = []; Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) { var slot = Y.Moodle.mod_quiz.util.slot.getSlotFromComponent(node); ids += ids === '' ? '' : ','; ids += Y.Moodle.mod_quiz.util.slot.getId(slot); slots.push(slot); }); var element = Y.one('div.mod-quiz-edit-content'); // Do nothing if no slots are selected. if (!slots || !slots.length) { return; } require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('areyousureremoveselected', 'quiz'), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = this.add_spinner(element); var data = { 'class': 'resource', field: 'deletemultiple', ids: ids }; // Delete items on server. this.send_request(data, spinner, function(response) { // Delete locally if deleted on server. if (response.deleted) { // Actually remove the element. Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) { Y.Moodle.mod_quiz.util.slot.remove(node.ancestor('li.activity')); }); // Update the page numbers and sections. this.reorganise_edit_page(); // Remove the select multiple options. Y.one('body').removeClass(CSS.SELECTMULTIPLE); } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Edit the maxmark for the resource * * @protected * @method edit_maxmark * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action that has been requested. * @return Boolean */ edit_maxmark: function(ev, button, activity) { // Get the element we're working on var instancemaxmark = activity.one(SELECTOR.INSTANCEMAXMARK), instance = activity.one(SELECTOR.ACTIVITYINSTANCE), currentmaxmark = instancemaxmark.get('firstChild'), oldmaxmark = currentmaxmark.get('data'), maxmarktext = oldmaxmark, thisevent, anchor = instancemaxmark, // Grab the anchor so that we can swap it with the edit form. data = { 'class': 'resource', 'field': 'getmaxmark', 'id': Y.Moodle.mod_quiz.util.slot.getId(activity) }; // Prevent the default actions. ev.preventDefault(); this.send_request(data, null, function(response) { if (M.core.actionmenu && M.core.actionmenu.instance) { M.core.actionmenu.instance.hideMenu(ev); } // Try to retrieve the existing string from the server. if (response.instancemaxmark) { maxmarktext = response.instancemaxmark; } // Create the editor and submit button. var editform = Y.Node.create('
                  '); var editinstructions = Y.Node.create('') .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle')); var editor = Y.Node.create('').setAttrs({ 'value': maxmarktext, 'autocomplete': 'off', 'aria-describedby': 'id_editinstructions', 'maxLength': '12', 'size': parseInt(this.get('config').questiondecimalpoints, 10) + 2 }); // Clear the existing content and put the editor in. editform.appendChild(editor); editform.setData('anchor', anchor); instance.insert(editinstructions, 'before'); anchor.replace(editform); // We hide various components whilst editing: activity.addClass(CSS.EDITINGMAXMARK); // Focus and select the editor text. editor.focus().select(); // Cancel the edit if we lose focus or the escape key is pressed. thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false); this.editmaxmarkevents.push(thisevent); thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true); this.editmaxmarkevents.push(thisevent); // Handle form submission. thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark); this.editmaxmarkevents.push(thisevent); }); }, /** * Handles the submit event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_submit * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {String} originalmaxmark The original maxmark the activity or resource had. */ edit_maxmark_submit: function(ev, activity, originalmaxmark) { // We don't actually want to submit anything. ev.preventDefault(); var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value')); var spinner = this.add_spinner(activity); this.edit_maxmark_clear(activity); activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark); if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) { var data = { 'class': 'resource', 'field': 'updatemaxmark', 'maxmark': newmaxmark, 'id': Y.Moodle.mod_quiz.util.slot.getId(activity) }; this.send_request(data, spinner, function(response) { if (response.instancemaxmark) { activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark); } }); } }, /** * Handles the cancel event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_cancel * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {Boolean} preventdefault If true we should prevent the default action from occuring. */ edit_maxmark_cancel: function(ev, activity, preventdefault) { if (preventdefault) { ev.preventDefault(); } this.edit_maxmark_clear(activity); }, /** * Handles clearing the editing UI and returning things to the original state they were in. * * @protected * @method edit_maxmark_clear * @param {Node} activity The activity whose maxmark we were altering. */ edit_maxmark_clear: function(activity) { // Detach all listen events to prevent duplicate triggers new Y.EventHandle(this.editmaxmarkevents).detach(); var editform = activity.one(SELECTOR.ACTIVITYFORM), instructions = activity.one('#id_editinstructions'); if (editform) { editform.replace(editform.getData('anchor')); } if (instructions) { instructions.remove(); } // Remove the editing class again to revert the display. activity.removeClass(CSS.EDITINGMAXMARK); // Refocus the link which was clicked originally so the user can continue using keyboard nav. Y.later(100, this, function() { activity.one(SELECTOR.EDITMAXMARK).focus(); }); // TODO MDL-50768 This hack is to keep Behat happy until they release a version of // MinkSelenium2Driver that fixes // https://github.com/Behat/MinkSelenium2Driver/issues/80. if (!Y.one('input[name=maxmark')) { Y.one('body').append(''); } }, /** * Joins or separates the given slot with the page of the previous slot. Reorders the pages of * the other slots * * @protected * @method update_page_break * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action, addpagebreak or removepagebreak. * @chainable */ update_page_break: function(ev, button, activity, action) { // Prevent the default button action ev.preventDefault(); var nextactivity = activity.next('li.activity.slot'); var spinner = this.add_spinner(nextactivity); var value = action === 'removepagebreak' ? 1 : 2; var data = { 'class': 'resource', 'field': 'updatepagebreak', 'id': Y.Moodle.mod_quiz.util.slot.getId(nextactivity), 'value': value }; this.send_request(data, spinner, function(response) { if (response.slots) { if (action === 'addpagebreak') { Y.Moodle.mod_quiz.util.page.add(activity); } else { var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE); Y.Moodle.mod_quiz.util.page.remove(page, true); } this.reorganise_edit_page(); } }); return this; }, /** * Updates a slot to either require the question in the previous slot to * have been answered, or not, * * @protected * @method update_page_break * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action, adddependency or removedependency. * @chainable */ update_dependency: function(ev, button, activity, action) { // Prevent the default button action. ev.preventDefault(); var spinner = this.add_spinner(activity); var data = { 'class': 'resource', 'field': 'updatedependency', 'id': Y.Moodle.mod_quiz.util.slot.getId(activity), 'value': action === 'adddependency' ? 1 : 0 }; this.send_request(data, spinner, function(response) { if (response.hasOwnProperty('requireprevious')) { Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious); } }); return this; }, /** * Reorganise the UI after every edit action. * * @protected * @method reorganise_edit_page */ reorganise_edit_page: function() { Y.Moodle.mod_quiz.util.slot.reorderSlots(); Y.Moodle.mod_quiz.util.slot.reorderPageBreaks(); Y.Moodle.mod_quiz.util.page.reorderPages(); Y.Moodle.mod_quiz.util.slot.updateOneSlotSections(); Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons(); }, NAME: 'mod_quiz-resource-toolbox', ATTRS: { courseid: { 'value': 0 }, quizid: { 'value': 0 } } }); M.mod_quiz.resource_toolbox = null; M.mod_quiz.init_resource_toolbox = function(config) { M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config); return M.mod_quiz.resource_toolbox; }; src/toolboxes/js/section.js000064400000034255152267015160012005 0ustar00/** * Section toolbox class. * * This class is responsible for managing AJAX interactions with sections * when adding, editing, removing section headings. * * @module moodle-mod_quiz-toolboxes * @namespace M.mod_quiz.toolboxes */ /** * Section toolbox class. * * This class is responsible for managing AJAX interactions with sections * when adding, editing, removing section headings when editing a quiz. * * @class section * @constructor * @extends M.mod_quiz.toolboxes.toolbox */ var SECTIONTOOLBOX = function() { SECTIONTOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(SECTIONTOOLBOX, TOOLBOX, { /** * An Array of events added when editing a max mark field. * These should all be detached when editing is complete. * * @property editsectionevents * @protected * @type Array * @protected */ editsectionevents: [], /** * Initialize the section toolboxes module. * * Updates all span.commands with relevant handlers and other required changes. * * @method initializer * @protected */ initializer: function() { M.mod_quiz.quizbase.register_module(this); BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); Y.delegate('change', this.handle_data_action, BODY, SELECTOR.EDITSHUFFLEQUESTIONSACTION, this); }, /** * Handles the delegation event. When this is fired someone has triggered an action. * * Note not all actions will result in an AJAX enhancement. * * @protected * @method handle_data_action * @param {EventFacade} ev The event that was triggered. * @returns {boolean} */ handle_data_action: function(ev) { // We need to get the anchor element that triggered this event. var node = ev.target; if (!node.test('a') && !node.test('input[data-action]')) { node = node.ancestor(SELECTOR.ACTIVITYACTION); } // From the anchor we can get both the activity (added during initialisation) and the action being // performed (added by the UI as a data attribute). var action = node.getData('action'), activity = node.ancestor(SELECTOR.ACTIVITYLI); if ((!node.test('a') && !node.test('input[data-action]')) || !action || !activity) { // It wasn't a valid action node. return; } // Switch based upon the action and do the desired thing. switch (action) { case 'edit_section_title': // The user wishes to edit the section headings. this.edit_section_title(ev, node, activity, action); break; case 'shuffle_questions': // The user wishes to edit the shuffle questions of the section (resource). this.edit_shuffle_questions(ev, node, activity, action); break; case 'deletesection': // The user is deleting the activity. this.delete_section_with_confirmation(ev, node, activity, action); break; default: // Nothing to do here! break; } }, /** * Deletes the given section heading after confirmation. * * @protected * @method delete_section_with_confirmation * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @chainable */ delete_section_with_confirmation: function(ev, button, activity) { ev.preventDefault(); require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('confirmremovesectionheading', 'quiz', activity.getData('sectionname')), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.ACTIONAREA)); var data = { 'class': 'section', 'action': 'DELETE', 'id': activity.get('id').replace('section-', '') }; this.send_request(data, spinner, function(response) { if (response.deleted) { window.location.reload(true); } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Edit the edit section title for the section * * @protected * @method edit_section_title * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action that has been requested. * @return Boolean */ edit_section_title: function(ev, button, activity) { // Get the element we're working on var activityid = activity.get('id').replace('section-', ''), instancesection = activity.one(SELECTOR.INSTANCESECTION), thisevent, anchor = instancesection, // Grab the anchor so that we can swap it with the edit form. data = { 'class': 'section', 'field': 'getsectiontitle', 'id': activityid }; // Prevent the default actions. ev.preventDefault(); this.send_request(data, null, function(response) { // Try to retrieve the existing string from the server. var oldtext = response.instancesection; // Create the editor and submit button. var editform = Y.Node.create(''); var editinstructions = Y.Node.create('') .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle')); var editor = Y.Node.create('').setAttrs({ 'value': oldtext, 'autocomplete': 'off', 'aria-describedby': 'id_editinstructions', 'maxLength': '255' // This is the maxlength in DB. }); // Clear the existing content and put the editor in. editform.appendChild(editor); editform.setData('anchor', anchor); instancesection.insert(editinstructions, 'before'); anchor.replace(editform); // Focus and select the editor text. editor.focus().select(); // Cancel the edit if we lose focus or the escape key is pressed. thisevent = editor.on('blur', this.edit_section_title_cancel, this, activity, false); this.editsectionevents.push(thisevent); thisevent = editor.on('key', this.edit_section_title_cancel, 'esc', this, activity, true); this.editsectionevents.push(thisevent); // Handle form submission. thisevent = editform.on('submit', this.edit_section_title_submit, this, activity, oldtext); this.editsectionevents.push(thisevent); }); }, /** * Handles the submit event when editing section heading. * * @protected * @method edit_section_title_submiy * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {String} oldtext The original maxmark the activity or resource had. */ edit_section_title_submit: function(ev, activity, oldtext) { // We don't actually want to submit anything. ev.preventDefault(); var newtext = Y.Lang.trim(activity.one(SELECTOR.SECTIONFORM + ' ' + SELECTOR.SECTIONINPUT).get('value')); var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.INSTANCESECTIONAREA)); this.edit_section_title_clear(activity); if (newtext !== null && newtext !== oldtext) { var instancesection = activity.one(SELECTOR.INSTANCESECTION); var instancesectiontext = newtext; if (newtext.trim() === '') { // Add a visually-hidden default section heading text to make sure we don't end up with an empty section heading. instancesectiontext = M.util.get_string('sectionnoname', 'quiz'); instancesection.addClass('visually-hidden'); } else { // Show the section heading when a non-empty value is set. instancesection.removeClass('visually-hidden'); } instancesection.setContent(instancesectiontext); var data = { 'class': 'section', 'field': 'updatesectiontitle', 'newheading': newtext, 'id': activity.get('id').replace('section-', '') }; this.send_request(data, spinner, function(response) { if (response) { // Set the content of the section heading if for some reason the response is different from the new text. // e.g. filters were applied, the update failed, etc. if (newtext !== response.instancesection) { if (response.instancesection.trim() === '') { // Add a visually-hidden default section heading text. instancesectiontext = M.util.get_string('sectionnoname', 'quiz'); instancesection.addClass('visually-hidden'); } else { instancesectiontext = response.instancesection; // Show the section heading when a non-empty value is set. instancesection.removeClass('visually-hidden'); } instancesection.setContent(instancesectiontext); } activity.one(SELECTOR.EDITSECTIONICON).set('title', M.util.get_string('sectionheadingedit', 'quiz', response.instancesection)); activity.one(SELECTOR.EDITSECTIONICON).set('alt', M.util.get_string('sectionheadingedit', 'quiz', response.instancesection)); var deleteicon = activity.one(SELECTOR.DELETESECTIONICON); if (deleteicon) { deleteicon.set('title', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection)); deleteicon.set('alt', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection)); } } }); } }, /** * Handles the cancel event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_cancel * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {Boolean} preventdefault If true we should prevent the default action from occuring. */ edit_section_title_cancel: function(ev, activity, preventdefault) { if (preventdefault) { ev.preventDefault(); } this.edit_section_title_clear(activity); }, /** * Handles clearing the editing UI and returning things to the original state they were in. * * @protected * @method edit_maxmark_clear * @param {Node} activity The activity whose maxmark we were altering. */ edit_section_title_clear: function(activity) { // Detach all listen events to prevent duplicate triggers new Y.EventHandle(this.editsectionevents).detach(); var editform = activity.one(SELECTOR.SECTIONFORM), instructions = activity.one('#id_editinstructions'); if (editform) { editform.replace(editform.getData('anchor')); } if (instructions) { instructions.remove(); } // Refocus the link which was clicked originally so the user can continue using keyboard nav. Y.later(100, this, function() { activity.one(SELECTOR.EDITSECTION).focus(); }); // This hack is to keep Behat happy until they release a version of // MinkSelenium2Driver that fixes // https://github.com/Behat/MinkSelenium2Driver/issues/80. if (!Y.one('input[name=section]')) { Y.one('body').append(''); } }, /** * Edit the edit shuffle questions for the section * * @protected * @method edit_shuffle_questions * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @return Boolean */ edit_shuffle_questions: function(ev, button, activity) { var newvalue; if (activity.one(SELECTOR.EDITSHUFFLEQUESTIONSACTION).get('checked')) { newvalue = 1; activity.addClass('shuffled'); } else { newvalue = 0; activity.removeClass('shuffled'); } // Prevent the default actions. ev.preventDefault(); // Get the element we're working on var data = { 'class': 'section', 'field': 'updateshufflequestions', 'id': activity.get('id').replace('section-', ''), 'newshuffle': newvalue }; // Send request. var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.EDITSHUFFLEAREA)); this.send_request(data, spinner); } }, { NAME: 'mod_quiz-section-toolbox', ATTRS: { courseid: { 'value': 0 }, quizid: { 'value': 0 } } }); M.mod_quiz.init_section_toolbox = function(config) { return new SECTIONTOOLBOX(config); }; src/toolboxes/js/toolbox.js000064400000016502152267015160012022 0ustar00/* eslint-disable no-unused-vars */ /** * Resource and activity toolbox class. * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a course in editing mode. * * @module moodle-course-toolboxes * @namespace M.course.toolboxes */ // The CSS classes we use. var CSS = { ACTIVITYINSTANCE: 'activityinstance', AVAILABILITYINFODIV: 'div.availabilityinfo', CONTENTWITHOUTLINK: 'contentwithoutlink', CONDITIONALHIDDEN: 'conditionalhidden', DIMCLASS: 'dimmed', DIMMEDTEXT: 'dimmed_text', EDITINSTRUCTIONS: 'editinstructions', EDITINGMAXMARK: 'editor_displayed', HIDE: 'hide', JOIN: 'page_join', MODINDENTCOUNT: 'mod-indent-', MODINDENTHUGE: 'mod-indent-huge', PAGE: 'page', SECTIONHIDDENCLASS: 'hidden', SECTIONIDPREFIX: 'section-', SELECTMULTIPLE: 'select-multiple', SLOT: 'slot', SHOW: 'editing_show', TITLEEDITOR: 'titleeditor' }, // The CSS selectors we use. SELECTOR = { ACTIONAREA: '.actions', ACTIONLINKTEXT: '.actionlinktext', ACTIVITYACTION: 'a.cm-edit-action[data-action], a.editing_maxmark, a.editing_section, input.shuffle_questions', ACTIVITYFORM: 'span.instancemaxmarkcontainer form', ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE, SECTIONINSTANCE: '.sectioninstance', ACTIVITYLI: 'li.activity, li.section', ACTIVITYMAXMARK: 'input[name=maxmark]', COMMANDSPAN: '.commands', CONTENTAFTERLINK: 'div.contentafterlink', CONTENTWITHOUTLINK: 'div.contentwithoutlink', DELETESECTIONICON: 'a.editing_delete .icon', EDITMAXMARK: 'a.editing_maxmark', EDITSECTION: 'a.editing_section', EDITSECTIONICON: 'a.editing_section .icon', EDITSHUFFLEQUESTIONSACTION: 'input.cm-edit-action[data-action]', EDITSHUFFLEAREA: '.instanceshufflequestions .shuffle-progress', HIDE: 'a.editing_hide', HIGHLIGHT: 'a.editing_highlight', INSTANCENAME: 'span.instancename', INSTANCEMAXMARK: 'span.instancemaxmark', INSTANCESECTION: 'span.instancesection', INSTANCESECTIONAREA: 'div.section-heading', MODINDENTDIV: '.mod-indent', MODINDENTOUTER: '.mod-indent-outer', NUMQUESTIONS: '.numberofquestions', PAGECONTENT: 'div#page-content', PAGELI: 'li.page', SECTIONLI: 'li.section', SECTIONUL: 'ul.section', SECTIONFORM: '.instancesectioncontainer form', SECTIONINPUT: 'input[name=section]', SELECTMULTIPLEBUTTON: '#selectmultiplecommand', SELECTMULTIPLECANCELBUTTON: '#selectmultiplecancelcommand', SELECTMULTIPLECHECKBOX: '.select-multiple-checkbox', SELECTMULTIPLEDELETEBUTTON: '#selectmultipledeletecommand', SELECTALL: '#questionselectall', SHOW: 'a.' + CSS.SHOW, SLOTLI: 'li.slot', SUMMARKS: '.mod_quiz_summarks' }, BODY = Y.one(document.body); // Setup the basic namespace. M.mod_quiz = M.mod_quiz || {}; /** * The toolbox class is a generic class which should never be directly * instantiated. Please extend it instead. * * @class toolbox * @constructor * @protected * @extends Base */ var TOOLBOX = function() { TOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(TOOLBOX, Y.Base, { /** * Send a request using the REST API * * @method send_request * @param {Object} data The data to submit with the AJAX request * @param {Node} [statusspinner] A statusspinner which may contain a section loader * @param {Function} success_callback The callback to use on success * @param {Object} [optionalconfig] Any additional configuration to submit * @chainable */ send_request: function(data, statusspinner, success_callback, optionalconfig) { // 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]; } data.sesskey = M.cfg.sesskey; data.courseid = this.get('courseid'); data.quizid = this.get('quizid'); 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) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } // Run the callback if we have one. if (responsetext.hasOwnProperty('newsummarks')) { Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks); } if (responsetext.hasOwnProperty('newnumquestions')) { Y.one(SELECTOR.NUMQUESTIONS).setHTML( M.util.get_string('numquestionsx', 'quiz', responsetext.newnumquestions) ); } if (success_callback) { Y.bind(success_callback, this, responsetext)(); } if (statusspinner) { window.setTimeout(function() { statusspinner.hide(); }, 400); } }, failure: function(tid, response) { if (statusspinner) { statusspinner.hide(); } new M.core.ajaxException(response); } }, context: this }; // Apply optional config if (optionalconfig) { for (varname in optionalconfig) { config[varname] = optionalconfig[varname]; } } if (statusspinner) { statusspinner.show(); } // Send the request Y.io(uri, config); return this; } }, { NAME: 'mod_quiz-toolbox', ATTRS: { /** * The ID of the Moodle Course being edited. * * @attribute courseid * @default 0 * @type Number */ courseid: { 'value': 0 }, /** * The Moodle course format. * * @attribute format * @default 'topics' * @type String */ quizid: { 'value': 0 }, /** * The URL to use when submitting requests. * @attribute ajaxurl * @default null * @type String */ ajaxurl: { 'value': null }, /** * Any additional configuration passed when creating the instance. * * @attribute config * @default {} * @type Object */ config: { 'value': {} } } } ); src/modform/build.json000064400000000264152267015160010777 0ustar00{ "name": "moodle-mod_quiz-modform", "builds": { "moodle-mod_quiz-modform": { "jsfiles": [ "modform.js" ] } } } src/modform/meta/modform.json000064400000000207152267015160012266 0ustar00{ "moodle-mod_quiz-modform": { "requires": [ "base", "node", "event" ] } } src/modform/js/modform.js000064400000002560152267015160011423 0ustar00/** * The modform class has all the JavaScript specific to mod/quiz/mod_form.php. * * @module moodle-mod_quiz-modform */ var MODFORM = function() { MODFORM.superclass.constructor.apply(this, arguments); }; /** * The coursebase class to provide shared functionality to Modules within * Moodle. * * @class M.course.coursebase * @constructor */ Y.extend(MODFORM, Y.Base, { repaginateCheckbox: null, qppSelect: null, qppInitialValue: 0, initializer: function() { this.repaginateCheckbox = Y.one('#id_repaginatenow'); if (!this.repaginateCheckbox) { // The checkbox only appears when editing an existing quiz. return; } this.qppSelect = Y.one('#id_questionsperpage'); this.qppInitialValue = this.qppSelect.get('value'); this.qppSelect.on('change', this.qppChanged, this); }, qppChanged: function() { Y.later(50, this, function() { if (!this.repaginateCheckbox.get('disabled')) { this.repaginateCheckbox.set('checked', this.qppSelect.get('value') !== this.qppInitialValue); } }); } }); // Ensure that M.mod_quiz exists and that coursebase is initialised correctly M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.modform = M.mod_quiz.modform || new MODFORM(); M.mod_quiz.modform.init = function() { return new MODFORM(); }; build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base-debug.js000064400000000550152267015160021250 0ustar00YUI.add('moodle-mod_quiz-util-base', function (Y, NAME) { /** * 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 */ }, '@VERSION@'); build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base.js000064400000000550152267015160020164 0ustar00YUI.add('moodle-mod_quiz-util-base', function (Y, NAME) { /** * 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 */ }, '@VERSION@'); build/moodle-mod_quiz-util-base/moodle-mod_quiz-util-base-min.js000064400000000144152267015160020744 0ustar00YUI.add("moodle-mod_quiz-util-base",function(o,d){o.namespace("Moodle.mod_quiz.util")},"@VERSION@");build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page.js000064400000024406152267015160020176 0ustar00YUI.add('moodle-mod_quiz-util-page', function (Y, NAME) { /* global YUI */ /** * A collection of utility classes for use with pages. * * @module moodle-mod_quiz-util * @submodule moodle-mod_quiz-util-page */ Y.namespace('Moodle.mod_quiz.util.page'); /** * A collection of utility classes for use with pages. * * @class Moodle.mod_quiz.util.page * @static */ Y.Moodle.mod_quiz.util.page = { CSS: { PAGE: 'page' }, CONSTANTS: { ACTIONMENUIDPREFIX: 'action-menu-', ACTIONMENUBARIDSUFFIX: '-menubar', ACTIONMENUMENUIDSUFFIX: '-menu', PAGEIDPREFIX: 'page-', PAGENUMBERPREFIX: M.util.get_string('page', 'moodle') + ' ' }, SELECTORS: { ACTIONMENU: 'div.moodle-actionmenu', ACTIONMENUBAR: '.menubar', ACTIONMENUMENU: '.menu', ADDASECTION: '[data-action="addasection"]', PAGE: 'li.page', INSTANCENAME: '.instancename', NUMBER: 'h4' }, /** * Retrieve the page item from one of it's child Nodes. * * @method getPageFromComponent * @param pagecomponent {Node} The component Node. * @return {Node|null} The Page Node. */ getPageFromComponent: function(pagecomponent) { return Y.one(pagecomponent).ancestor(this.SELECTORS.PAGE, true); }, /** * Retrieve the page item from one of it's previous siblings. * * @method getPageFromSlot * @param pagecomponent {Node} The component Node. * @return {Node|null} The Page Node. */ getPageFromSlot: function(slot) { return Y.one(slot).previous(this.SELECTORS.PAGE); }, /** * Returns the page ID for the provided page. * * @method getId * @param page {Node} The page to find an ID for. * @return {Number|false} The ID of the page in question or false if no ID was found. */ getId: function(page) { // We perform a simple substitution operation to get the ID. var id = page.get('id').replace( this.CONSTANTS.PAGEIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Updates the page id for the provided page. * * @method setId * @param page {Node} The page to update the number for. * @param id int The id value. * @return void */ setId: function(page, id) { page.set('id', this.CONSTANTS.PAGEIDPREFIX + id); }, /** * Determines the page name for the provided page. * * @method getName * @param page {Node} The page to find a name for. * @return {string|false} The name of the page in question or false if no ID was found. */ getName: function(page) { var instance = page.one(this.SELECTORS.INSTANCENAME); if (instance) { return instance.get('firstChild').get('data'); } return null; }, /** * Determines the page number for the provided page. * * @method getNumber * @param page {Node} The page to find a number for. * @return {Number|false} The number of the page in question or false if no number was found. */ getNumber: function(page) { // We perform a simple substitution operation to get the number. var number = page.one(this.SELECTORS.NUMBER).get('text').replace( this.CONSTANTS.PAGENUMBERPREFIX, ''); // Attempt to validate the ID. number = parseInt(number, 10); if (typeof number === 'number' && isFinite(number)) { return number; } return false; }, /** * Updates the page number for the provided page. * * @method setNumber * @param page {Node} The page to update the number for. * @return void */ setNumber: function(page, number) { page.one(this.SELECTORS.NUMBER).set('text', this.CONSTANTS.PAGENUMBERPREFIX + number); }, /** * Returns a list of all page elements. * * @method getPages * @return {node[]} An array containing page nodes. */ getPages: function() { return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' + Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.PAGE); }, /** * Is the given element a page element? * * @method isPage * @param page Page node * @return boolean */ isPage: function(page) { if (!page) { return false; } return page.hasClass(this.CSS.PAGE); }, /** * Does the page have atleast one slot? * * @method isEmpty * @param page Page node * @return boolean */ isEmpty: function(page) { var activity = page.next('li.activity'); if (!activity) { return true; } return !activity.hasClass('slot'); }, /** * Add a page and related elements to the list of slots. * * @method add * @param beforenode Int | Node | HTMLElement | String to add * @return page Page node */ add: function(beforenode) { var pagenumber = this.getNumber(this.getPageFromSlot(beforenode)) + 1; var pagehtml = M.mod_quiz.resource_toolbox.get('config').pagehtml; // Normalise the page number. pagehtml = pagehtml.replace(/%%PAGENUMBER%%/g, pagenumber); // Create the page node. var page = Y.Node.create(pagehtml); // Assign is as a drop target. YUI().use('dd-drop', function(Y) { var drop = new Y.DD.Drop({ node: page, groups: M.mod_quiz.dragres.groups }); page.drop = drop; }); // Insert in the correct place. beforenode.insert(page, 'after'); // Enhance the add menu to make if fully visible and clickable. if (typeof M.core.actionmenu !== "undefined") { M.core.actionmenu.newDOMNode(page); } return page; }, /** * Remove a page and related elements from the list of slots. * * @method remove * @param page Page node * @return void */ remove: function(page, keeppagebreak) { // Remove page break from previous slot. var previousslot = page.previous(Y.Moodle.mod_quiz.util.slot.SELECTORS.SLOT); if (!keeppagebreak && previousslot) { Y.Moodle.mod_quiz.util.slot.removePageBreak(previousslot); } page.remove(); }, /** * Reset the order of the numbers given to each page. * * @method reorderPages * @return void */ reorderPages: function() { // Get list of page nodes. var pages = this.getPages(); var currentpagenumber = 0; // Loop through pages incrementing the number each time. pages.each(function(page) { // Is the page empty? if (this.isEmpty(page)) { var keeppagebreak = page.next('li.slot') ? true : false; this.remove(page, keeppagebreak); return; } currentpagenumber++; // Set page number. this.setNumber(page, currentpagenumber); this.setId(page, currentpagenumber); }, this); // Reorder action menus this.reorderActionMenus(); }, /** * Reset the order of the numbers given to each action menu. * * @method reorderActionMenus * @return void */ reorderActionMenus: function() { // Get list of action menu nodes. var actionmenus = this.getActionMenus(); // Loop through pages incrementing the number each time. actionmenus.each(function(actionmenu, key) { var previousActionMenu = actionmenus.item(key - 1), previousActionMenunumber = 0; if (previousActionMenu) { previousActionMenunumber = this.getActionMenuId(previousActionMenu); } var id = previousActionMenunumber + 1; // Set menu id. this.setActionMenuId(actionmenu, id); // Update action-menu-1-menubar var menubar = actionmenu.one(this.SELECTORS.ACTIONMENUBAR); menubar.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUBARIDSUFFIX); // Update action-menu-1-menu var menumenu = actionmenu.one(this.SELECTORS.ACTIONMENUMENU); menumenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUMENUIDSUFFIX); // Update the URL of the add-section action. menumenu.one(this.SELECTORS.ADDASECTION).set('href', menumenu.one(this.SELECTORS.ADDASECTION).get('href').replace(/\baddsectionatpage=\d+\b/, 'addsectionatpage=' + id)); }, this); }, /** * Returns a list of all page elements. * * @method getActionMenus * @return {node[]} An array containing page nodes. */ getActionMenus: function() { return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' + Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.ACTIONMENU); }, /** * Returns the ID for the provided action menu. * * @method getId * @param actionmenu {Node} The actionmenu to find an ID for. * @return {Number|false} The ID of the actionmenu in question or false if no ID was found. */ getActionMenuId: function(actionmenu) { // We perform a simple substitution operation to get the ID. var id = actionmenu.get('id').replace( this.CONSTANTS.ACTIONMENUIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Updates the page id for the provided page. * * @method setId * @param page {Node} The page to update the number for. * @param id int The id value. * @return void */ setActionMenuId: function(actionmenu, id) { actionmenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id); } }; }, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]}); build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-debug.js000064400000024406152267015160021262 0ustar00YUI.add('moodle-mod_quiz-util-page', function (Y, NAME) { /* global YUI */ /** * A collection of utility classes for use with pages. * * @module moodle-mod_quiz-util * @submodule moodle-mod_quiz-util-page */ Y.namespace('Moodle.mod_quiz.util.page'); /** * A collection of utility classes for use with pages. * * @class Moodle.mod_quiz.util.page * @static */ Y.Moodle.mod_quiz.util.page = { CSS: { PAGE: 'page' }, CONSTANTS: { ACTIONMENUIDPREFIX: 'action-menu-', ACTIONMENUBARIDSUFFIX: '-menubar', ACTIONMENUMENUIDSUFFIX: '-menu', PAGEIDPREFIX: 'page-', PAGENUMBERPREFIX: M.util.get_string('page', 'moodle') + ' ' }, SELECTORS: { ACTIONMENU: 'div.moodle-actionmenu', ACTIONMENUBAR: '.menubar', ACTIONMENUMENU: '.menu', ADDASECTION: '[data-action="addasection"]', PAGE: 'li.page', INSTANCENAME: '.instancename', NUMBER: 'h4' }, /** * Retrieve the page item from one of it's child Nodes. * * @method getPageFromComponent * @param pagecomponent {Node} The component Node. * @return {Node|null} The Page Node. */ getPageFromComponent: function(pagecomponent) { return Y.one(pagecomponent).ancestor(this.SELECTORS.PAGE, true); }, /** * Retrieve the page item from one of it's previous siblings. * * @method getPageFromSlot * @param pagecomponent {Node} The component Node. * @return {Node|null} The Page Node. */ getPageFromSlot: function(slot) { return Y.one(slot).previous(this.SELECTORS.PAGE); }, /** * Returns the page ID for the provided page. * * @method getId * @param page {Node} The page to find an ID for. * @return {Number|false} The ID of the page in question or false if no ID was found. */ getId: function(page) { // We perform a simple substitution operation to get the ID. var id = page.get('id').replace( this.CONSTANTS.PAGEIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Updates the page id for the provided page. * * @method setId * @param page {Node} The page to update the number for. * @param id int The id value. * @return void */ setId: function(page, id) { page.set('id', this.CONSTANTS.PAGEIDPREFIX + id); }, /** * Determines the page name for the provided page. * * @method getName * @param page {Node} The page to find a name for. * @return {string|false} The name of the page in question or false if no ID was found. */ getName: function(page) { var instance = page.one(this.SELECTORS.INSTANCENAME); if (instance) { return instance.get('firstChild').get('data'); } return null; }, /** * Determines the page number for the provided page. * * @method getNumber * @param page {Node} The page to find a number for. * @return {Number|false} The number of the page in question or false if no number was found. */ getNumber: function(page) { // We perform a simple substitution operation to get the number. var number = page.one(this.SELECTORS.NUMBER).get('text').replace( this.CONSTANTS.PAGENUMBERPREFIX, ''); // Attempt to validate the ID. number = parseInt(number, 10); if (typeof number === 'number' && isFinite(number)) { return number; } return false; }, /** * Updates the page number for the provided page. * * @method setNumber * @param page {Node} The page to update the number for. * @return void */ setNumber: function(page, number) { page.one(this.SELECTORS.NUMBER).set('text', this.CONSTANTS.PAGENUMBERPREFIX + number); }, /** * Returns a list of all page elements. * * @method getPages * @return {node[]} An array containing page nodes. */ getPages: function() { return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' + Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.PAGE); }, /** * Is the given element a page element? * * @method isPage * @param page Page node * @return boolean */ isPage: function(page) { if (!page) { return false; } return page.hasClass(this.CSS.PAGE); }, /** * Does the page have atleast one slot? * * @method isEmpty * @param page Page node * @return boolean */ isEmpty: function(page) { var activity = page.next('li.activity'); if (!activity) { return true; } return !activity.hasClass('slot'); }, /** * Add a page and related elements to the list of slots. * * @method add * @param beforenode Int | Node | HTMLElement | String to add * @return page Page node */ add: function(beforenode) { var pagenumber = this.getNumber(this.getPageFromSlot(beforenode)) + 1; var pagehtml = M.mod_quiz.resource_toolbox.get('config').pagehtml; // Normalise the page number. pagehtml = pagehtml.replace(/%%PAGENUMBER%%/g, pagenumber); // Create the page node. var page = Y.Node.create(pagehtml); // Assign is as a drop target. YUI().use('dd-drop', function(Y) { var drop = new Y.DD.Drop({ node: page, groups: M.mod_quiz.dragres.groups }); page.drop = drop; }); // Insert in the correct place. beforenode.insert(page, 'after'); // Enhance the add menu to make if fully visible and clickable. if (typeof M.core.actionmenu !== "undefined") { M.core.actionmenu.newDOMNode(page); } return page; }, /** * Remove a page and related elements from the list of slots. * * @method remove * @param page Page node * @return void */ remove: function(page, keeppagebreak) { // Remove page break from previous slot. var previousslot = page.previous(Y.Moodle.mod_quiz.util.slot.SELECTORS.SLOT); if (!keeppagebreak && previousslot) { Y.Moodle.mod_quiz.util.slot.removePageBreak(previousslot); } page.remove(); }, /** * Reset the order of the numbers given to each page. * * @method reorderPages * @return void */ reorderPages: function() { // Get list of page nodes. var pages = this.getPages(); var currentpagenumber = 0; // Loop through pages incrementing the number each time. pages.each(function(page) { // Is the page empty? if (this.isEmpty(page)) { var keeppagebreak = page.next('li.slot') ? true : false; this.remove(page, keeppagebreak); return; } currentpagenumber++; // Set page number. this.setNumber(page, currentpagenumber); this.setId(page, currentpagenumber); }, this); // Reorder action menus this.reorderActionMenus(); }, /** * Reset the order of the numbers given to each action menu. * * @method reorderActionMenus * @return void */ reorderActionMenus: function() { // Get list of action menu nodes. var actionmenus = this.getActionMenus(); // Loop through pages incrementing the number each time. actionmenus.each(function(actionmenu, key) { var previousActionMenu = actionmenus.item(key - 1), previousActionMenunumber = 0; if (previousActionMenu) { previousActionMenunumber = this.getActionMenuId(previousActionMenu); } var id = previousActionMenunumber + 1; // Set menu id. this.setActionMenuId(actionmenu, id); // Update action-menu-1-menubar var menubar = actionmenu.one(this.SELECTORS.ACTIONMENUBAR); menubar.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUBARIDSUFFIX); // Update action-menu-1-menu var menumenu = actionmenu.one(this.SELECTORS.ACTIONMENUMENU); menumenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id + this.CONSTANTS.ACTIONMENUMENUIDSUFFIX); // Update the URL of the add-section action. menumenu.one(this.SELECTORS.ADDASECTION).set('href', menumenu.one(this.SELECTORS.ADDASECTION).get('href').replace(/\baddsectionatpage=\d+\b/, 'addsectionatpage=' + id)); }, this); }, /** * Returns a list of all page elements. * * @method getActionMenus * @return {node[]} An array containing page nodes. */ getActionMenus: function() { return Y.all(Y.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT + ' ' + Y.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.ACTIONMENU); }, /** * Returns the ID for the provided action menu. * * @method getId * @param actionmenu {Node} The actionmenu to find an ID for. * @return {Number|false} The ID of the actionmenu in question or false if no ID was found. */ getActionMenuId: function(actionmenu) { // We perform a simple substitution operation to get the ID. var id = actionmenu.get('id').replace( this.CONSTANTS.ACTIONMENUIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Updates the page id for the provided page. * * @method setId * @param page {Node} The page to update the number for. * @param id int The id value. * @return void */ setActionMenuId: function(actionmenu, id) { actionmenu.set('id', this.CONSTANTS.ACTIONMENUIDPREFIX + id); } }; }, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]}); build/moodle-mod_quiz-util-page/moodle-mod_quiz-util-page-min.js000064400000006355152267015160020762 0ustar00YUI.add("moodle-mod_quiz-util-page",function(n,e){n.namespace("Moodle.mod_quiz.util.page"),n.Moodle.mod_quiz.util.page={CSS:{PAGE:"page"},CONSTANTS:{ACTIONMENUIDPREFIX:"action-menu-",ACTIONMENUBARIDSUFFIX:"-menubar",ACTIONMENUMENUIDSUFFIX:"-menu",PAGEIDPREFIX:"page-",PAGENUMBERPREFIX:M.util.get_string("page","moodle")+" "},SELECTORS:{ACTIONMENU:"div.moodle-actionmenu",ACTIONMENUBAR:".menubar",ACTIONMENUMENU:".menu",ADDASECTION:'[data-action="addasection"]',PAGE:"li.page",INSTANCENAME:".instancename",NUMBER:"h4"},getPageFromComponent:function(e){return n.one(e).ancestor(this.SELECTORS.PAGE,!0)},getPageFromSlot:function(e){return n.one(e).previous(this.SELECTORS.PAGE)},getId:function(e){e=e.get("id").replace(this.CONSTANTS.PAGEIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},setId:function(e,t){e.set("id",this.CONSTANTS.PAGEIDPREFIX+t)},getName:function(e){e=e.one(this.SELECTORS.INSTANCENAME);return e?e.get("firstChild").get("data"):null},getNumber:function(e){e=e.one(this.SELECTORS.NUMBER).get("text").replace(this.CONSTANTS.PAGENUMBERPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},setNumber:function(e,t){e.one(this.SELECTORS.NUMBER).set("text",this.CONSTANTS.PAGENUMBERPREFIX+t)},getPages:function(){return n.all(n.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT+" "+n.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL+" "+this.SELECTORS.PAGE)},isPage:function(e){return!!e&&e.hasClass(this.CSS.PAGE)},isEmpty:function(e){e=e.next("li.activity");return!e||!e.hasClass("slot")},add:function(e){var t=this.getNumber(this.getPageFromSlot(e))+1,i=M.mod_quiz.resource_toolbox.get("config").pagehtml,i=i.replace(/%%PAGENUMBER%%/g,t),o=n.Node.create(i);return YUI().use("dd-drop",function(e){e=new e.DD.Drop({node:o,groups:M.mod_quiz.dragres.groups});o.drop=e}),e.insert(o,"after"),"undefined"!=typeof M.core.actionmenu&&M.core.actionmenu.newDOMNode(o),o},remove:function(e,t){var i=e.previous(n.Moodle.mod_quiz.util.slot.SELECTORS.SLOT);!t&&i&&n.Moodle.mod_quiz.util.slot.removePageBreak(i),e.remove()},reorderPages:function(){var e=this.getPages(),i=0;e.each(function(e){var t;if(this.isEmpty(e))return t=!!e.next("li.slot"),void this.remove(e,t);i++,this.setNumber(e,i),this.setId(e,i)},this),this.reorderActionMenus()},reorderActionMenus:function(){var o=this.getActionMenus();o.each(function(e,t){var t=o.item(t-1),i=0;t&&(i=this.getActionMenuId(t)),this.setActionMenuId(e,t=i+1),e.one(this.SELECTORS.ACTIONMENUBAR).set("id",this.CONSTANTS.ACTIONMENUIDPREFIX+t+this.CONSTANTS.ACTIONMENUBARIDSUFFIX),(i=e.one(this.SELECTORS.ACTIONMENUMENU)).set("id",this.CONSTANTS.ACTIONMENUIDPREFIX+t+this.CONSTANTS.ACTIONMENUMENUIDSUFFIX),i.one(this.SELECTORS.ADDASECTION).set("href",i.one(this.SELECTORS.ADDASECTION).get("href").replace(/\baddsectionatpage=\d+\b/,"addsectionatpage="+t))},this)},getActionMenus:function(){return n.all(n.Moodle.mod_quiz.util.slot.SELECTORS.PAGECONTENT+" "+n.Moodle.mod_quiz.util.slot.SELECTORS.SECTIONUL+" "+this.SELECTORS.ACTIONMENU)},getActionMenuId:function(e){e=e.get("id").replace(this.CONSTANTS.ACTIONMENUIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},setActionMenuId:function(e,t){e.set("id",this.CONSTANTS.ACTIONMENUIDPREFIX+t)}}},"@VERSION@",{requires:["node","moodle-mod_quiz-util-base"]});build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js000064400000126200152267015160020447 0ustar00YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { /* eslint-disable no-unused-vars */ /** * Resource and activity toolbox class. * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a course in editing mode. * * @module moodle-course-toolboxes * @namespace M.course.toolboxes */ // The CSS classes we use. var CSS = { ACTIVITYINSTANCE: 'activityinstance', AVAILABILITYINFODIV: 'div.availabilityinfo', CONTENTWITHOUTLINK: 'contentwithoutlink', CONDITIONALHIDDEN: 'conditionalhidden', DIMCLASS: 'dimmed', DIMMEDTEXT: 'dimmed_text', EDITINSTRUCTIONS: 'editinstructions', EDITINGMAXMARK: 'editor_displayed', HIDE: 'hide', JOIN: 'page_join', MODINDENTCOUNT: 'mod-indent-', MODINDENTHUGE: 'mod-indent-huge', PAGE: 'page', SECTIONHIDDENCLASS: 'hidden', SECTIONIDPREFIX: 'section-', SELECTMULTIPLE: 'select-multiple', SLOT: 'slot', SHOW: 'editing_show', TITLEEDITOR: 'titleeditor' }, // The CSS selectors we use. SELECTOR = { ACTIONAREA: '.actions', ACTIONLINKTEXT: '.actionlinktext', ACTIVITYACTION: 'a.cm-edit-action[data-action], a.editing_maxmark, a.editing_section, input.shuffle_questions', ACTIVITYFORM: 'span.instancemaxmarkcontainer form', ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE, SECTIONINSTANCE: '.sectioninstance', ACTIVITYLI: 'li.activity, li.section', ACTIVITYMAXMARK: 'input[name=maxmark]', COMMANDSPAN: '.commands', CONTENTAFTERLINK: 'div.contentafterlink', CONTENTWITHOUTLINK: 'div.contentwithoutlink', DELETESECTIONICON: 'a.editing_delete .icon', EDITMAXMARK: 'a.editing_maxmark', EDITSECTION: 'a.editing_section', EDITSECTIONICON: 'a.editing_section .icon', EDITSHUFFLEQUESTIONSACTION: 'input.cm-edit-action[data-action]', EDITSHUFFLEAREA: '.instanceshufflequestions .shuffle-progress', HIDE: 'a.editing_hide', HIGHLIGHT: 'a.editing_highlight', INSTANCENAME: 'span.instancename', INSTANCEMAXMARK: 'span.instancemaxmark', INSTANCESECTION: 'span.instancesection', INSTANCESECTIONAREA: 'div.section-heading', MODINDENTDIV: '.mod-indent', MODINDENTOUTER: '.mod-indent-outer', NUMQUESTIONS: '.numberofquestions', PAGECONTENT: 'div#page-content', PAGELI: 'li.page', SECTIONLI: 'li.section', SECTIONUL: 'ul.section', SECTIONFORM: '.instancesectioncontainer form', SECTIONINPUT: 'input[name=section]', SELECTMULTIPLEBUTTON: '#selectmultiplecommand', SELECTMULTIPLECANCELBUTTON: '#selectmultiplecancelcommand', SELECTMULTIPLECHECKBOX: '.select-multiple-checkbox', SELECTMULTIPLEDELETEBUTTON: '#selectmultipledeletecommand', SELECTALL: '#questionselectall', SHOW: 'a.' + CSS.SHOW, SLOTLI: 'li.slot', SUMMARKS: '.mod_quiz_summarks' }, BODY = Y.one(document.body); // Setup the basic namespace. M.mod_quiz = M.mod_quiz || {}; /** * The toolbox class is a generic class which should never be directly * instantiated. Please extend it instead. * * @class toolbox * @constructor * @protected * @extends Base */ var TOOLBOX = function() { TOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(TOOLBOX, Y.Base, { /** * Send a request using the REST API * * @method send_request * @param {Object} data The data to submit with the AJAX request * @param {Node} [statusspinner] A statusspinner which may contain a section loader * @param {Function} success_callback The callback to use on success * @param {Object} [optionalconfig] Any additional configuration to submit * @chainable */ send_request: function(data, statusspinner, success_callback, optionalconfig) { // 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]; } data.sesskey = M.cfg.sesskey; data.courseid = this.get('courseid'); data.quizid = this.get('quizid'); 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) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } // Run the callback if we have one. if (responsetext.hasOwnProperty('newsummarks')) { Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks); } if (responsetext.hasOwnProperty('newnumquestions')) { Y.one(SELECTOR.NUMQUESTIONS).setHTML( M.util.get_string('numquestionsx', 'quiz', responsetext.newnumquestions) ); } if (success_callback) { Y.bind(success_callback, this, responsetext)(); } if (statusspinner) { window.setTimeout(function() { statusspinner.hide(); }, 400); } }, failure: function(tid, response) { if (statusspinner) { statusspinner.hide(); } new M.core.ajaxException(response); } }, context: this }; // Apply optional config if (optionalconfig) { for (varname in optionalconfig) { config[varname] = optionalconfig[varname]; } } if (statusspinner) { statusspinner.show(); } // Send the request Y.io(uri, config); return this; } }, { NAME: 'mod_quiz-toolbox', ATTRS: { /** * The ID of the Moodle Course being edited. * * @attribute courseid * @default 0 * @type Number */ courseid: { 'value': 0 }, /** * The Moodle course format. * * @attribute format * @default 'topics' * @type String */ quizid: { 'value': 0 }, /** * The URL to use when submitting requests. * @attribute ajaxurl * @default null * @type String */ ajaxurl: { 'value': null }, /** * Any additional configuration passed when creating the instance. * * @attribute config * @default {} * @type Object */ config: { 'value': {} } } } ); /** * Resource and activity toolbox class. * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a quiz in editing mode. * * @module mod_quiz-resource-toolbox * @namespace M.mod_quiz.resource_toolbox */ /** * Resource and activity toolbox class. * * This is a class extending TOOLBOX containing code specific to resources * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a quiz in editing mode. * * @class resources * @constructor * @extends M.course.toolboxes.toolbox */ var RESOURCETOOLBOX = function() { RESOURCETOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(RESOURCETOOLBOX, TOOLBOX, { /** * An Array of events added when editing a max mark field. * These should all be detached when editing is complete. * * @property editmaxmarkevents * @protected * @type Array * @protected */ editmaxmarkevents: [], /** * */ NODE_PAGE: 1, NODE_SLOT: 2, NODE_JOIN: 3, /** * Initialize the resource toolbox * * For each activity the commands are updated and a reference to the activity is attached. * This way it doesn't matter where the commands are going to called from they have a reference to the * activity that they relate to. * This is essential as some of the actions are displayed in an actionmenu which removes them from the * page flow. * * This function also creates a single event delegate to manage all AJAX actions for all activities on * the page. * * @method initializer * @protected */ initializer: function() { M.mod_quiz.quizbase.register_module(this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this); this.initialise_select_multiple(); }, /** * Initialize the select multiple options * * Add actions to the buttons that enable multiple slots to be selected and managed at once. * * @method initialise_select_multiple * @protected */ initialise_select_multiple: function() { // Click select multiple button to show the select all options. Y.one(SELECTOR.SELECTMULTIPLEBUTTON).on('click', function(e) { e.preventDefault(); Y.one('body').addClass(CSS.SELECTMULTIPLE); }); // Click cancel button to show the select all options. Y.one(SELECTOR.SELECTMULTIPLECANCELBUTTON).on('click', function(e) { e.preventDefault(); Y.one('body').removeClass(CSS.SELECTMULTIPLE); }); // Assign the delete method to the delete multiple button. Y.delegate('click', this.delete_multiple_action, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this); }, /** * Handles the delegation event. When this is fired someone has triggered an action. * * Note not all actions will result in an AJAX enhancement. * * @protected * @method handle_data_action * @param {EventFacade} ev The event that was triggered. * @returns {boolean} */ handle_data_action: function(ev) { // We need to get the anchor element that triggered this event. var node = ev.target; if (!node.test('a')) { node = node.ancestor(SELECTOR.ACTIVITYACTION); } // From the anchor we can get both the activity (added during initialisation) and the action being // performed (added by the UI as a data attribute). var action = node.getData('action'), activity = node.ancestor(SELECTOR.ACTIVITYLI); if (!node.test('a') || !action || !activity) { // It wasn't a valid action node. return; } // Switch based upon the action and do the desired thing. switch (action) { case 'editmaxmark': // The user wishes to edit the maxmark of the resource. this.edit_maxmark(ev, node, activity, action); break; case 'delete': // The user is deleting the activity. this.delete_with_confirmation(ev, node, activity, action); break; case 'addpagebreak': case 'removepagebreak': // The user is adding or removing a page break. this.update_page_break(ev, node, activity, action); break; case 'adddependency': case 'removedependency': // The user is adding or removing a dependency between questions. this.update_dependency(ev, node, activity, action); break; default: // Nothing to do here! break; } }, /** * Add a loading icon to the specified activity. * * The icon is added within the action area. * * @method add_spinner * @param {Node} activity The activity to add a loading icon to * @return {Node|null} The newly created icon, or null if the action area was not found. */ add_spinner: function(activity) { var actionarea = activity.one(SELECTOR.ACTIONAREA); if (actionarea) { return M.util.add_spinner(Y, actionarea); } return null; }, /** * Deletes the given activity or resource after confirmation. * * @protected * @method delete_with_confirmation * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. */ delete_with_confirmation: function(ev, button, activity) { // Prevent the default button action. ev.preventDefault(); // Get the element we're working on. var element = activity; // Create confirm string (different if element has or does not have name) var qtypename = M.util.get_string( 'pluginname', 'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1] ); // Create the confirmation dialogue. require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('confirmremovequestion', 'quiz', qtypename), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = this.add_spinner(element); var data = { 'class': 'resource', 'action': 'DELETE', 'id': Y.Moodle.mod_quiz.util.slot.getId(element) }; this.send_request(data, spinner, function(response) { if (response.deleted) { // Actually remove the element. Y.Moodle.mod_quiz.util.slot.remove(element); this.reorganise_edit_page(); if (M.core.actionmenu && M.core.actionmenu.instance) { M.core.actionmenu.instance.hideMenu(ev); } } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Finds the section that would become empty if we remove the selected slots. * * @protected * @method find_sections_that_would_become_empty * @returns {String} The name of the first section found */ find_sections_that_would_become_empty: function() { var section; var sectionnodes = Y.all(SELECTOR.SECTIONLI); if (sectionnodes.size() > 1) { sectionnodes.some(function(node) { var sectionname = node.one(SELECTOR.INSTANCESECTION).getContent(); var checked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked'); var unchecked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':not(:checked)'); if (!checked.isEmpty() && unchecked.isEmpty()) { section = sectionname; } return section; }); } return section; }, /** * Takes care of what needs to happen when the user clicks on the delete multiple button. * * @protected * @method delete_multiple_action * @param {EventFacade} ev The event that was fired. */ delete_multiple_action: function(ev) { var problemsection = this.find_sections_that_would_become_empty(); if (typeof problemsection !== 'undefined') { require(['core/notification'], function(Notification) { Notification.alert( M.util.get_string('cannotremoveslots', 'quiz'), M.util.get_string('cannotremoveallsectionslots', 'quiz', problemsection) ); }); } else { this.delete_multiple_with_confirmation(ev); } }, /** * Deletes the given activities or resources after confirmation. * * @protected * @method delete_multiple_with_confirmation * @param {EventFacade} ev The event that was fired. */ delete_multiple_with_confirmation: function(ev) { ev.preventDefault(); var ids = ''; var slots = []; Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) { var slot = Y.Moodle.mod_quiz.util.slot.getSlotFromComponent(node); ids += ids === '' ? '' : ','; ids += Y.Moodle.mod_quiz.util.slot.getId(slot); slots.push(slot); }); var element = Y.one('div.mod-quiz-edit-content'); // Do nothing if no slots are selected. if (!slots || !slots.length) { return; } require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('areyousureremoveselected', 'quiz'), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = this.add_spinner(element); var data = { 'class': 'resource', field: 'deletemultiple', ids: ids }; // Delete items on server. this.send_request(data, spinner, function(response) { // Delete locally if deleted on server. if (response.deleted) { // Actually remove the element. Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) { Y.Moodle.mod_quiz.util.slot.remove(node.ancestor('li.activity')); }); // Update the page numbers and sections. this.reorganise_edit_page(); // Remove the select multiple options. Y.one('body').removeClass(CSS.SELECTMULTIPLE); } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Edit the maxmark for the resource * * @protected * @method edit_maxmark * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action that has been requested. * @return Boolean */ edit_maxmark: function(ev, button, activity) { // Get the element we're working on var instancemaxmark = activity.one(SELECTOR.INSTANCEMAXMARK), instance = activity.one(SELECTOR.ACTIVITYINSTANCE), currentmaxmark = instancemaxmark.get('firstChild'), oldmaxmark = currentmaxmark.get('data'), maxmarktext = oldmaxmark, thisevent, anchor = instancemaxmark, // Grab the anchor so that we can swap it with the edit form. data = { 'class': 'resource', 'field': 'getmaxmark', 'id': Y.Moodle.mod_quiz.util.slot.getId(activity) }; // Prevent the default actions. ev.preventDefault(); this.send_request(data, null, function(response) { if (M.core.actionmenu && M.core.actionmenu.instance) { M.core.actionmenu.instance.hideMenu(ev); } // Try to retrieve the existing string from the server. if (response.instancemaxmark) { maxmarktext = response.instancemaxmark; } // Create the editor and submit button. var editform = Y.Node.create(''); var editinstructions = Y.Node.create('') .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle')); var editor = Y.Node.create('').setAttrs({ 'value': maxmarktext, 'autocomplete': 'off', 'aria-describedby': 'id_editinstructions', 'maxLength': '12', 'size': parseInt(this.get('config').questiondecimalpoints, 10) + 2 }); // Clear the existing content and put the editor in. editform.appendChild(editor); editform.setData('anchor', anchor); instance.insert(editinstructions, 'before'); anchor.replace(editform); // We hide various components whilst editing: activity.addClass(CSS.EDITINGMAXMARK); // Focus and select the editor text. editor.focus().select(); // Cancel the edit if we lose focus or the escape key is pressed. thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false); this.editmaxmarkevents.push(thisevent); thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true); this.editmaxmarkevents.push(thisevent); // Handle form submission. thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark); this.editmaxmarkevents.push(thisevent); }); }, /** * Handles the submit event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_submit * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {String} originalmaxmark The original maxmark the activity or resource had. */ edit_maxmark_submit: function(ev, activity, originalmaxmark) { // We don't actually want to submit anything. ev.preventDefault(); var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value')); var spinner = this.add_spinner(activity); this.edit_maxmark_clear(activity); activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark); if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) { var data = { 'class': 'resource', 'field': 'updatemaxmark', 'maxmark': newmaxmark, 'id': Y.Moodle.mod_quiz.util.slot.getId(activity) }; this.send_request(data, spinner, function(response) { if (response.instancemaxmark) { activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark); } }); } }, /** * Handles the cancel event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_cancel * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {Boolean} preventdefault If true we should prevent the default action from occuring. */ edit_maxmark_cancel: function(ev, activity, preventdefault) { if (preventdefault) { ev.preventDefault(); } this.edit_maxmark_clear(activity); }, /** * Handles clearing the editing UI and returning things to the original state they were in. * * @protected * @method edit_maxmark_clear * @param {Node} activity The activity whose maxmark we were altering. */ edit_maxmark_clear: function(activity) { // Detach all listen events to prevent duplicate triggers new Y.EventHandle(this.editmaxmarkevents).detach(); var editform = activity.one(SELECTOR.ACTIVITYFORM), instructions = activity.one('#id_editinstructions'); if (editform) { editform.replace(editform.getData('anchor')); } if (instructions) { instructions.remove(); } // Remove the editing class again to revert the display. activity.removeClass(CSS.EDITINGMAXMARK); // Refocus the link which was clicked originally so the user can continue using keyboard nav. Y.later(100, this, function() { activity.one(SELECTOR.EDITMAXMARK).focus(); }); // TODO MDL-50768 This hack is to keep Behat happy until they release a version of // MinkSelenium2Driver that fixes // https://github.com/Behat/MinkSelenium2Driver/issues/80. if (!Y.one('input[name=maxmark')) { Y.one('body').append(''); } }, /** * Joins or separates the given slot with the page of the previous slot. Reorders the pages of * the other slots * * @protected * @method update_page_break * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action, addpagebreak or removepagebreak. * @chainable */ update_page_break: function(ev, button, activity, action) { // Prevent the default button action ev.preventDefault(); var nextactivity = activity.next('li.activity.slot'); var spinner = this.add_spinner(nextactivity); var value = action === 'removepagebreak' ? 1 : 2; var data = { 'class': 'resource', 'field': 'updatepagebreak', 'id': Y.Moodle.mod_quiz.util.slot.getId(nextactivity), 'value': value }; this.send_request(data, spinner, function(response) { if (response.slots) { if (action === 'addpagebreak') { Y.Moodle.mod_quiz.util.page.add(activity); } else { var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE); Y.Moodle.mod_quiz.util.page.remove(page, true); } this.reorganise_edit_page(); } }); return this; }, /** * Updates a slot to either require the question in the previous slot to * have been answered, or not, * * @protected * @method update_page_break * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action, adddependency or removedependency. * @chainable */ update_dependency: function(ev, button, activity, action) { // Prevent the default button action. ev.preventDefault(); var spinner = this.add_spinner(activity); var data = { 'class': 'resource', 'field': 'updatedependency', 'id': Y.Moodle.mod_quiz.util.slot.getId(activity), 'value': action === 'adddependency' ? 1 : 0 }; this.send_request(data, spinner, function(response) { if (response.hasOwnProperty('requireprevious')) { Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious); } }); return this; }, /** * Reorganise the UI after every edit action. * * @protected * @method reorganise_edit_page */ reorganise_edit_page: function() { Y.Moodle.mod_quiz.util.slot.reorderSlots(); Y.Moodle.mod_quiz.util.slot.reorderPageBreaks(); Y.Moodle.mod_quiz.util.page.reorderPages(); Y.Moodle.mod_quiz.util.slot.updateOneSlotSections(); Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons(); }, NAME: 'mod_quiz-resource-toolbox', ATTRS: { courseid: { 'value': 0 }, quizid: { 'value': 0 } } }); M.mod_quiz.resource_toolbox = null; M.mod_quiz.init_resource_toolbox = function(config) { M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config); return M.mod_quiz.resource_toolbox; }; /** * Section toolbox class. * * This class is responsible for managing AJAX interactions with sections * when adding, editing, removing section headings. * * @module moodle-mod_quiz-toolboxes * @namespace M.mod_quiz.toolboxes */ /** * Section toolbox class. * * This class is responsible for managing AJAX interactions with sections * when adding, editing, removing section headings when editing a quiz. * * @class section * @constructor * @extends M.mod_quiz.toolboxes.toolbox */ var SECTIONTOOLBOX = function() { SECTIONTOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(SECTIONTOOLBOX, TOOLBOX, { /** * An Array of events added when editing a max mark field. * These should all be detached when editing is complete. * * @property editsectionevents * @protected * @type Array * @protected */ editsectionevents: [], /** * Initialize the section toolboxes module. * * Updates all span.commands with relevant handlers and other required changes. * * @method initializer * @protected */ initializer: function() { M.mod_quiz.quizbase.register_module(this); BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); Y.delegate('change', this.handle_data_action, BODY, SELECTOR.EDITSHUFFLEQUESTIONSACTION, this); }, /** * Handles the delegation event. When this is fired someone has triggered an action. * * Note not all actions will result in an AJAX enhancement. * * @protected * @method handle_data_action * @param {EventFacade} ev The event that was triggered. * @returns {boolean} */ handle_data_action: function(ev) { // We need to get the anchor element that triggered this event. var node = ev.target; if (!node.test('a') && !node.test('input[data-action]')) { node = node.ancestor(SELECTOR.ACTIVITYACTION); } // From the anchor we can get both the activity (added during initialisation) and the action being // performed (added by the UI as a data attribute). var action = node.getData('action'), activity = node.ancestor(SELECTOR.ACTIVITYLI); if ((!node.test('a') && !node.test('input[data-action]')) || !action || !activity) { // It wasn't a valid action node. return; } // Switch based upon the action and do the desired thing. switch (action) { case 'edit_section_title': // The user wishes to edit the section headings. this.edit_section_title(ev, node, activity, action); break; case 'shuffle_questions': // The user wishes to edit the shuffle questions of the section (resource). this.edit_shuffle_questions(ev, node, activity, action); break; case 'deletesection': // The user is deleting the activity. this.delete_section_with_confirmation(ev, node, activity, action); break; default: // Nothing to do here! break; } }, /** * Deletes the given section heading after confirmation. * * @protected * @method delete_section_with_confirmation * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @chainable */ delete_section_with_confirmation: function(ev, button, activity) { ev.preventDefault(); require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('confirmremovesectionheading', 'quiz', activity.getData('sectionname')), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.ACTIONAREA)); var data = { 'class': 'section', 'action': 'DELETE', 'id': activity.get('id').replace('section-', '') }; this.send_request(data, spinner, function(response) { if (response.deleted) { window.location.reload(true); } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Edit the edit section title for the section * * @protected * @method edit_section_title * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action that has been requested. * @return Boolean */ edit_section_title: function(ev, button, activity) { // Get the element we're working on var activityid = activity.get('id').replace('section-', ''), instancesection = activity.one(SELECTOR.INSTANCESECTION), thisevent, anchor = instancesection, // Grab the anchor so that we can swap it with the edit form. data = { 'class': 'section', 'field': 'getsectiontitle', 'id': activityid }; // Prevent the default actions. ev.preventDefault(); this.send_request(data, null, function(response) { // Try to retrieve the existing string from the server. var oldtext = response.instancesection; // Create the editor and submit button. var editform = Y.Node.create(''); var editinstructions = Y.Node.create('') .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle')); var editor = Y.Node.create('').setAttrs({ 'value': oldtext, 'autocomplete': 'off', 'aria-describedby': 'id_editinstructions', 'maxLength': '255' // This is the maxlength in DB. }); // Clear the existing content and put the editor in. editform.appendChild(editor); editform.setData('anchor', anchor); instancesection.insert(editinstructions, 'before'); anchor.replace(editform); // Focus and select the editor text. editor.focus().select(); // Cancel the edit if we lose focus or the escape key is pressed. thisevent = editor.on('blur', this.edit_section_title_cancel, this, activity, false); this.editsectionevents.push(thisevent); thisevent = editor.on('key', this.edit_section_title_cancel, 'esc', this, activity, true); this.editsectionevents.push(thisevent); // Handle form submission. thisevent = editform.on('submit', this.edit_section_title_submit, this, activity, oldtext); this.editsectionevents.push(thisevent); }); }, /** * Handles the submit event when editing section heading. * * @protected * @method edit_section_title_submiy * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {String} oldtext The original maxmark the activity or resource had. */ edit_section_title_submit: function(ev, activity, oldtext) { // We don't actually want to submit anything. ev.preventDefault(); var newtext = Y.Lang.trim(activity.one(SELECTOR.SECTIONFORM + ' ' + SELECTOR.SECTIONINPUT).get('value')); var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.INSTANCESECTIONAREA)); this.edit_section_title_clear(activity); if (newtext !== null && newtext !== oldtext) { var instancesection = activity.one(SELECTOR.INSTANCESECTION); var instancesectiontext = newtext; if (newtext.trim() === '') { // Add a visually-hidden default section heading text to make sure we don't end up with an empty section heading. instancesectiontext = M.util.get_string('sectionnoname', 'quiz'); instancesection.addClass('visually-hidden'); } else { // Show the section heading when a non-empty value is set. instancesection.removeClass('visually-hidden'); } instancesection.setContent(instancesectiontext); var data = { 'class': 'section', 'field': 'updatesectiontitle', 'newheading': newtext, 'id': activity.get('id').replace('section-', '') }; this.send_request(data, spinner, function(response) { if (response) { // Set the content of the section heading if for some reason the response is different from the new text. // e.g. filters were applied, the update failed, etc. if (newtext !== response.instancesection) { if (response.instancesection.trim() === '') { // Add a visually-hidden default section heading text. instancesectiontext = M.util.get_string('sectionnoname', 'quiz'); instancesection.addClass('visually-hidden'); } else { instancesectiontext = response.instancesection; // Show the section heading when a non-empty value is set. instancesection.removeClass('visually-hidden'); } instancesection.setContent(instancesectiontext); } activity.one(SELECTOR.EDITSECTIONICON).set('title', M.util.get_string('sectionheadingedit', 'quiz', response.instancesection)); activity.one(SELECTOR.EDITSECTIONICON).set('alt', M.util.get_string('sectionheadingedit', 'quiz', response.instancesection)); var deleteicon = activity.one(SELECTOR.DELETESECTIONICON); if (deleteicon) { deleteicon.set('title', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection)); deleteicon.set('alt', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection)); } } }); } }, /** * Handles the cancel event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_cancel * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {Boolean} preventdefault If true we should prevent the default action from occuring. */ edit_section_title_cancel: function(ev, activity, preventdefault) { if (preventdefault) { ev.preventDefault(); } this.edit_section_title_clear(activity); }, /** * Handles clearing the editing UI and returning things to the original state they were in. * * @protected * @method edit_maxmark_clear * @param {Node} activity The activity whose maxmark we were altering. */ edit_section_title_clear: function(activity) { // Detach all listen events to prevent duplicate triggers new Y.EventHandle(this.editsectionevents).detach(); var editform = activity.one(SELECTOR.SECTIONFORM), instructions = activity.one('#id_editinstructions'); if (editform) { editform.replace(editform.getData('anchor')); } if (instructions) { instructions.remove(); } // Refocus the link which was clicked originally so the user can continue using keyboard nav. Y.later(100, this, function() { activity.one(SELECTOR.EDITSECTION).focus(); }); // This hack is to keep Behat happy until they release a version of // MinkSelenium2Driver that fixes // https://github.com/Behat/MinkSelenium2Driver/issues/80. if (!Y.one('input[name=section]')) { Y.one('body').append(''); } }, /** * Edit the edit shuffle questions for the section * * @protected * @method edit_shuffle_questions * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @return Boolean */ edit_shuffle_questions: function(ev, button, activity) { var newvalue; if (activity.one(SELECTOR.EDITSHUFFLEQUESTIONSACTION).get('checked')) { newvalue = 1; activity.addClass('shuffled'); } else { newvalue = 0; activity.removeClass('shuffled'); } // Prevent the default actions. ev.preventDefault(); // Get the element we're working on var data = { 'class': 'section', 'field': 'updateshufflequestions', 'id': activity.get('id').replace('section-', ''), 'newshuffle': newvalue }; // Send request. var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.EDITSHUFFLEAREA)); this.send_request(data, spinner); } }, { NAME: 'mod_quiz-section-toolbox', ATTRS: { courseid: { 'value': 0 }, quizid: { 'value': 0 } } }); M.mod_quiz.init_section_toolbox = function(config) { return new SECTIONTOOLBOX(config); }; }, '@VERSION@', { "requires": [ "base", "node", "event", "event-key", "io", "moodle-mod_quiz-quizbase", "moodle-mod_quiz-util-slot", "moodle-core-notification-ajaxexception" ] }); build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js000064400000126200152267015160021533 0ustar00YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { /* eslint-disable no-unused-vars */ /** * Resource and activity toolbox class. * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a course in editing mode. * * @module moodle-course-toolboxes * @namespace M.course.toolboxes */ // The CSS classes we use. var CSS = { ACTIVITYINSTANCE: 'activityinstance', AVAILABILITYINFODIV: 'div.availabilityinfo', CONTENTWITHOUTLINK: 'contentwithoutlink', CONDITIONALHIDDEN: 'conditionalhidden', DIMCLASS: 'dimmed', DIMMEDTEXT: 'dimmed_text', EDITINSTRUCTIONS: 'editinstructions', EDITINGMAXMARK: 'editor_displayed', HIDE: 'hide', JOIN: 'page_join', MODINDENTCOUNT: 'mod-indent-', MODINDENTHUGE: 'mod-indent-huge', PAGE: 'page', SECTIONHIDDENCLASS: 'hidden', SECTIONIDPREFIX: 'section-', SELECTMULTIPLE: 'select-multiple', SLOT: 'slot', SHOW: 'editing_show', TITLEEDITOR: 'titleeditor' }, // The CSS selectors we use. SELECTOR = { ACTIONAREA: '.actions', ACTIONLINKTEXT: '.actionlinktext', ACTIVITYACTION: 'a.cm-edit-action[data-action], a.editing_maxmark, a.editing_section, input.shuffle_questions', ACTIVITYFORM: 'span.instancemaxmarkcontainer form', ACTIVITYINSTANCE: '.' + CSS.ACTIVITYINSTANCE, SECTIONINSTANCE: '.sectioninstance', ACTIVITYLI: 'li.activity, li.section', ACTIVITYMAXMARK: 'input[name=maxmark]', COMMANDSPAN: '.commands', CONTENTAFTERLINK: 'div.contentafterlink', CONTENTWITHOUTLINK: 'div.contentwithoutlink', DELETESECTIONICON: 'a.editing_delete .icon', EDITMAXMARK: 'a.editing_maxmark', EDITSECTION: 'a.editing_section', EDITSECTIONICON: 'a.editing_section .icon', EDITSHUFFLEQUESTIONSACTION: 'input.cm-edit-action[data-action]', EDITSHUFFLEAREA: '.instanceshufflequestions .shuffle-progress', HIDE: 'a.editing_hide', HIGHLIGHT: 'a.editing_highlight', INSTANCENAME: 'span.instancename', INSTANCEMAXMARK: 'span.instancemaxmark', INSTANCESECTION: 'span.instancesection', INSTANCESECTIONAREA: 'div.section-heading', MODINDENTDIV: '.mod-indent', MODINDENTOUTER: '.mod-indent-outer', NUMQUESTIONS: '.numberofquestions', PAGECONTENT: 'div#page-content', PAGELI: 'li.page', SECTIONLI: 'li.section', SECTIONUL: 'ul.section', SECTIONFORM: '.instancesectioncontainer form', SECTIONINPUT: 'input[name=section]', SELECTMULTIPLEBUTTON: '#selectmultiplecommand', SELECTMULTIPLECANCELBUTTON: '#selectmultiplecancelcommand', SELECTMULTIPLECHECKBOX: '.select-multiple-checkbox', SELECTMULTIPLEDELETEBUTTON: '#selectmultipledeletecommand', SELECTALL: '#questionselectall', SHOW: 'a.' + CSS.SHOW, SLOTLI: 'li.slot', SUMMARKS: '.mod_quiz_summarks' }, BODY = Y.one(document.body); // Setup the basic namespace. M.mod_quiz = M.mod_quiz || {}; /** * The toolbox class is a generic class which should never be directly * instantiated. Please extend it instead. * * @class toolbox * @constructor * @protected * @extends Base */ var TOOLBOX = function() { TOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(TOOLBOX, Y.Base, { /** * Send a request using the REST API * * @method send_request * @param {Object} data The data to submit with the AJAX request * @param {Node} [statusspinner] A statusspinner which may contain a section loader * @param {Function} success_callback The callback to use on success * @param {Object} [optionalconfig] Any additional configuration to submit * @chainable */ send_request: function(data, statusspinner, success_callback, optionalconfig) { // 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]; } data.sesskey = M.cfg.sesskey; data.courseid = this.get('courseid'); data.quizid = this.get('quizid'); 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) { new M.core.ajaxException(responsetext); } } catch (e) { // Ignore. } // Run the callback if we have one. if (responsetext.hasOwnProperty('newsummarks')) { Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks); } if (responsetext.hasOwnProperty('newnumquestions')) { Y.one(SELECTOR.NUMQUESTIONS).setHTML( M.util.get_string('numquestionsx', 'quiz', responsetext.newnumquestions) ); } if (success_callback) { Y.bind(success_callback, this, responsetext)(); } if (statusspinner) { window.setTimeout(function() { statusspinner.hide(); }, 400); } }, failure: function(tid, response) { if (statusspinner) { statusspinner.hide(); } new M.core.ajaxException(response); } }, context: this }; // Apply optional config if (optionalconfig) { for (varname in optionalconfig) { config[varname] = optionalconfig[varname]; } } if (statusspinner) { statusspinner.show(); } // Send the request Y.io(uri, config); return this; } }, { NAME: 'mod_quiz-toolbox', ATTRS: { /** * The ID of the Moodle Course being edited. * * @attribute courseid * @default 0 * @type Number */ courseid: { 'value': 0 }, /** * The Moodle course format. * * @attribute format * @default 'topics' * @type String */ quizid: { 'value': 0 }, /** * The URL to use when submitting requests. * @attribute ajaxurl * @default null * @type String */ ajaxurl: { 'value': null }, /** * Any additional configuration passed when creating the instance. * * @attribute config * @default {} * @type Object */ config: { 'value': {} } } } ); /** * Resource and activity toolbox class. * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a quiz in editing mode. * * @module mod_quiz-resource-toolbox * @namespace M.mod_quiz.resource_toolbox */ /** * Resource and activity toolbox class. * * This is a class extending TOOLBOX containing code specific to resources * * This class is responsible for managing AJAX interactions with activities and resources * when viewing a quiz in editing mode. * * @class resources * @constructor * @extends M.course.toolboxes.toolbox */ var RESOURCETOOLBOX = function() { RESOURCETOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(RESOURCETOOLBOX, TOOLBOX, { /** * An Array of events added when editing a max mark field. * These should all be detached when editing is complete. * * @property editmaxmarkevents * @protected * @type Array * @protected */ editmaxmarkevents: [], /** * */ NODE_PAGE: 1, NODE_SLOT: 2, NODE_JOIN: 3, /** * Initialize the resource toolbox * * For each activity the commands are updated and a reference to the activity is attached. * This way it doesn't matter where the commands are going to called from they have a reference to the * activity that they relate to. * This is essential as some of the actions are displayed in an actionmenu which removes them from the * page flow. * * This function also creates a single event delegate to manage all AJAX actions for all activities on * the page. * * @method initializer * @protected */ initializer: function() { M.mod_quiz.quizbase.register_module(this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this); this.initialise_select_multiple(); }, /** * Initialize the select multiple options * * Add actions to the buttons that enable multiple slots to be selected and managed at once. * * @method initialise_select_multiple * @protected */ initialise_select_multiple: function() { // Click select multiple button to show the select all options. Y.one(SELECTOR.SELECTMULTIPLEBUTTON).on('click', function(e) { e.preventDefault(); Y.one('body').addClass(CSS.SELECTMULTIPLE); }); // Click cancel button to show the select all options. Y.one(SELECTOR.SELECTMULTIPLECANCELBUTTON).on('click', function(e) { e.preventDefault(); Y.one('body').removeClass(CSS.SELECTMULTIPLE); }); // Assign the delete method to the delete multiple button. Y.delegate('click', this.delete_multiple_action, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this); }, /** * Handles the delegation event. When this is fired someone has triggered an action. * * Note not all actions will result in an AJAX enhancement. * * @protected * @method handle_data_action * @param {EventFacade} ev The event that was triggered. * @returns {boolean} */ handle_data_action: function(ev) { // We need to get the anchor element that triggered this event. var node = ev.target; if (!node.test('a')) { node = node.ancestor(SELECTOR.ACTIVITYACTION); } // From the anchor we can get both the activity (added during initialisation) and the action being // performed (added by the UI as a data attribute). var action = node.getData('action'), activity = node.ancestor(SELECTOR.ACTIVITYLI); if (!node.test('a') || !action || !activity) { // It wasn't a valid action node. return; } // Switch based upon the action and do the desired thing. switch (action) { case 'editmaxmark': // The user wishes to edit the maxmark of the resource. this.edit_maxmark(ev, node, activity, action); break; case 'delete': // The user is deleting the activity. this.delete_with_confirmation(ev, node, activity, action); break; case 'addpagebreak': case 'removepagebreak': // The user is adding or removing a page break. this.update_page_break(ev, node, activity, action); break; case 'adddependency': case 'removedependency': // The user is adding or removing a dependency between questions. this.update_dependency(ev, node, activity, action); break; default: // Nothing to do here! break; } }, /** * Add a loading icon to the specified activity. * * The icon is added within the action area. * * @method add_spinner * @param {Node} activity The activity to add a loading icon to * @return {Node|null} The newly created icon, or null if the action area was not found. */ add_spinner: function(activity) { var actionarea = activity.one(SELECTOR.ACTIONAREA); if (actionarea) { return M.util.add_spinner(Y, actionarea); } return null; }, /** * Deletes the given activity or resource after confirmation. * * @protected * @method delete_with_confirmation * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. */ delete_with_confirmation: function(ev, button, activity) { // Prevent the default button action. ev.preventDefault(); // Get the element we're working on. var element = activity; // Create confirm string (different if element has or does not have name) var qtypename = M.util.get_string( 'pluginname', 'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1] ); // Create the confirmation dialogue. require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('confirmremovequestion', 'quiz', qtypename), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = this.add_spinner(element); var data = { 'class': 'resource', 'action': 'DELETE', 'id': Y.Moodle.mod_quiz.util.slot.getId(element) }; this.send_request(data, spinner, function(response) { if (response.deleted) { // Actually remove the element. Y.Moodle.mod_quiz.util.slot.remove(element); this.reorganise_edit_page(); if (M.core.actionmenu && M.core.actionmenu.instance) { M.core.actionmenu.instance.hideMenu(ev); } } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Finds the section that would become empty if we remove the selected slots. * * @protected * @method find_sections_that_would_become_empty * @returns {String} The name of the first section found */ find_sections_that_would_become_empty: function() { var section; var sectionnodes = Y.all(SELECTOR.SECTIONLI); if (sectionnodes.size() > 1) { sectionnodes.some(function(node) { var sectionname = node.one(SELECTOR.INSTANCESECTION).getContent(); var checked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked'); var unchecked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':not(:checked)'); if (!checked.isEmpty() && unchecked.isEmpty()) { section = sectionname; } return section; }); } return section; }, /** * Takes care of what needs to happen when the user clicks on the delete multiple button. * * @protected * @method delete_multiple_action * @param {EventFacade} ev The event that was fired. */ delete_multiple_action: function(ev) { var problemsection = this.find_sections_that_would_become_empty(); if (typeof problemsection !== 'undefined') { require(['core/notification'], function(Notification) { Notification.alert( M.util.get_string('cannotremoveslots', 'quiz'), M.util.get_string('cannotremoveallsectionslots', 'quiz', problemsection) ); }); } else { this.delete_multiple_with_confirmation(ev); } }, /** * Deletes the given activities or resources after confirmation. * * @protected * @method delete_multiple_with_confirmation * @param {EventFacade} ev The event that was fired. */ delete_multiple_with_confirmation: function(ev) { ev.preventDefault(); var ids = ''; var slots = []; Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) { var slot = Y.Moodle.mod_quiz.util.slot.getSlotFromComponent(node); ids += ids === '' ? '' : ','; ids += Y.Moodle.mod_quiz.util.slot.getId(slot); slots.push(slot); }); var element = Y.one('div.mod-quiz-edit-content'); // Do nothing if no slots are selected. if (!slots || !slots.length) { return; } require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('areyousureremoveselected', 'quiz'), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = this.add_spinner(element); var data = { 'class': 'resource', field: 'deletemultiple', ids: ids }; // Delete items on server. this.send_request(data, spinner, function(response) { // Delete locally if deleted on server. if (response.deleted) { // Actually remove the element. Y.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked').each(function(node) { Y.Moodle.mod_quiz.util.slot.remove(node.ancestor('li.activity')); }); // Update the page numbers and sections. this.reorganise_edit_page(); // Remove the select multiple options. Y.one('body').removeClass(CSS.SELECTMULTIPLE); } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Edit the maxmark for the resource * * @protected * @method edit_maxmark * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action that has been requested. * @return Boolean */ edit_maxmark: function(ev, button, activity) { // Get the element we're working on var instancemaxmark = activity.one(SELECTOR.INSTANCEMAXMARK), instance = activity.one(SELECTOR.ACTIVITYINSTANCE), currentmaxmark = instancemaxmark.get('firstChild'), oldmaxmark = currentmaxmark.get('data'), maxmarktext = oldmaxmark, thisevent, anchor = instancemaxmark, // Grab the anchor so that we can swap it with the edit form. data = { 'class': 'resource', 'field': 'getmaxmark', 'id': Y.Moodle.mod_quiz.util.slot.getId(activity) }; // Prevent the default actions. ev.preventDefault(); this.send_request(data, null, function(response) { if (M.core.actionmenu && M.core.actionmenu.instance) { M.core.actionmenu.instance.hideMenu(ev); } // Try to retrieve the existing string from the server. if (response.instancemaxmark) { maxmarktext = response.instancemaxmark; } // Create the editor and submit button. var editform = Y.Node.create(''); var editinstructions = Y.Node.create('') .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle')); var editor = Y.Node.create('').setAttrs({ 'value': maxmarktext, 'autocomplete': 'off', 'aria-describedby': 'id_editinstructions', 'maxLength': '12', 'size': parseInt(this.get('config').questiondecimalpoints, 10) + 2 }); // Clear the existing content and put the editor in. editform.appendChild(editor); editform.setData('anchor', anchor); instance.insert(editinstructions, 'before'); anchor.replace(editform); // We hide various components whilst editing: activity.addClass(CSS.EDITINGMAXMARK); // Focus and select the editor text. editor.focus().select(); // Cancel the edit if we lose focus or the escape key is pressed. thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false); this.editmaxmarkevents.push(thisevent); thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true); this.editmaxmarkevents.push(thisevent); // Handle form submission. thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark); this.editmaxmarkevents.push(thisevent); }); }, /** * Handles the submit event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_submit * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {String} originalmaxmark The original maxmark the activity or resource had. */ edit_maxmark_submit: function(ev, activity, originalmaxmark) { // We don't actually want to submit anything. ev.preventDefault(); var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value')); var spinner = this.add_spinner(activity); this.edit_maxmark_clear(activity); activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark); if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) { var data = { 'class': 'resource', 'field': 'updatemaxmark', 'maxmark': newmaxmark, 'id': Y.Moodle.mod_quiz.util.slot.getId(activity) }; this.send_request(data, spinner, function(response) { if (response.instancemaxmark) { activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark); } }); } }, /** * Handles the cancel event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_cancel * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {Boolean} preventdefault If true we should prevent the default action from occuring. */ edit_maxmark_cancel: function(ev, activity, preventdefault) { if (preventdefault) { ev.preventDefault(); } this.edit_maxmark_clear(activity); }, /** * Handles clearing the editing UI and returning things to the original state they were in. * * @protected * @method edit_maxmark_clear * @param {Node} activity The activity whose maxmark we were altering. */ edit_maxmark_clear: function(activity) { // Detach all listen events to prevent duplicate triggers new Y.EventHandle(this.editmaxmarkevents).detach(); var editform = activity.one(SELECTOR.ACTIVITYFORM), instructions = activity.one('#id_editinstructions'); if (editform) { editform.replace(editform.getData('anchor')); } if (instructions) { instructions.remove(); } // Remove the editing class again to revert the display. activity.removeClass(CSS.EDITINGMAXMARK); // Refocus the link which was clicked originally so the user can continue using keyboard nav. Y.later(100, this, function() { activity.one(SELECTOR.EDITMAXMARK).focus(); }); // TODO MDL-50768 This hack is to keep Behat happy until they release a version of // MinkSelenium2Driver that fixes // https://github.com/Behat/MinkSelenium2Driver/issues/80. if (!Y.one('input[name=maxmark')) { Y.one('body').append(''); } }, /** * Joins or separates the given slot with the page of the previous slot. Reorders the pages of * the other slots * * @protected * @method update_page_break * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action, addpagebreak or removepagebreak. * @chainable */ update_page_break: function(ev, button, activity, action) { // Prevent the default button action ev.preventDefault(); var nextactivity = activity.next('li.activity.slot'); var spinner = this.add_spinner(nextactivity); var value = action === 'removepagebreak' ? 1 : 2; var data = { 'class': 'resource', 'field': 'updatepagebreak', 'id': Y.Moodle.mod_quiz.util.slot.getId(nextactivity), 'value': value }; this.send_request(data, spinner, function(response) { if (response.slots) { if (action === 'addpagebreak') { Y.Moodle.mod_quiz.util.page.add(activity); } else { var page = activity.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE); Y.Moodle.mod_quiz.util.page.remove(page, true); } this.reorganise_edit_page(); } }); return this; }, /** * Updates a slot to either require the question in the previous slot to * have been answered, or not, * * @protected * @method update_page_break * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action, adddependency or removedependency. * @chainable */ update_dependency: function(ev, button, activity, action) { // Prevent the default button action. ev.preventDefault(); var spinner = this.add_spinner(activity); var data = { 'class': 'resource', 'field': 'updatedependency', 'id': Y.Moodle.mod_quiz.util.slot.getId(activity), 'value': action === 'adddependency' ? 1 : 0 }; this.send_request(data, spinner, function(response) { if (response.hasOwnProperty('requireprevious')) { Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious); } }); return this; }, /** * Reorganise the UI after every edit action. * * @protected * @method reorganise_edit_page */ reorganise_edit_page: function() { Y.Moodle.mod_quiz.util.slot.reorderSlots(); Y.Moodle.mod_quiz.util.slot.reorderPageBreaks(); Y.Moodle.mod_quiz.util.page.reorderPages(); Y.Moodle.mod_quiz.util.slot.updateOneSlotSections(); Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons(); }, NAME: 'mod_quiz-resource-toolbox', ATTRS: { courseid: { 'value': 0 }, quizid: { 'value': 0 } } }); M.mod_quiz.resource_toolbox = null; M.mod_quiz.init_resource_toolbox = function(config) { M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config); return M.mod_quiz.resource_toolbox; }; /** * Section toolbox class. * * This class is responsible for managing AJAX interactions with sections * when adding, editing, removing section headings. * * @module moodle-mod_quiz-toolboxes * @namespace M.mod_quiz.toolboxes */ /** * Section toolbox class. * * This class is responsible for managing AJAX interactions with sections * when adding, editing, removing section headings when editing a quiz. * * @class section * @constructor * @extends M.mod_quiz.toolboxes.toolbox */ var SECTIONTOOLBOX = function() { SECTIONTOOLBOX.superclass.constructor.apply(this, arguments); }; Y.extend(SECTIONTOOLBOX, TOOLBOX, { /** * An Array of events added when editing a max mark field. * These should all be detached when editing is complete. * * @property editsectionevents * @protected * @type Array * @protected */ editsectionevents: [], /** * Initialize the section toolboxes module. * * Updates all span.commands with relevant handlers and other required changes. * * @method initializer * @protected */ initializer: function() { M.mod_quiz.quizbase.register_module(this); BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this); Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); Y.delegate('change', this.handle_data_action, BODY, SELECTOR.EDITSHUFFLEQUESTIONSACTION, this); }, /** * Handles the delegation event. When this is fired someone has triggered an action. * * Note not all actions will result in an AJAX enhancement. * * @protected * @method handle_data_action * @param {EventFacade} ev The event that was triggered. * @returns {boolean} */ handle_data_action: function(ev) { // We need to get the anchor element that triggered this event. var node = ev.target; if (!node.test('a') && !node.test('input[data-action]')) { node = node.ancestor(SELECTOR.ACTIVITYACTION); } // From the anchor we can get both the activity (added during initialisation) and the action being // performed (added by the UI as a data attribute). var action = node.getData('action'), activity = node.ancestor(SELECTOR.ACTIVITYLI); if ((!node.test('a') && !node.test('input[data-action]')) || !action || !activity) { // It wasn't a valid action node. return; } // Switch based upon the action and do the desired thing. switch (action) { case 'edit_section_title': // The user wishes to edit the section headings. this.edit_section_title(ev, node, activity, action); break; case 'shuffle_questions': // The user wishes to edit the shuffle questions of the section (resource). this.edit_shuffle_questions(ev, node, activity, action); break; case 'deletesection': // The user is deleting the activity. this.delete_section_with_confirmation(ev, node, activity, action); break; default: // Nothing to do here! break; } }, /** * Deletes the given section heading after confirmation. * * @protected * @method delete_section_with_confirmation * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @chainable */ delete_section_with_confirmation: function(ev, button, activity) { ev.preventDefault(); require(['core/notification'], function(Notification) { Notification.saveCancelPromise( M.util.get_string('confirm', 'moodle'), M.util.get_string('confirmremovesectionheading', 'quiz', activity.getData('sectionname')), M.util.get_string('yes', 'moodle') ).then(function() { var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.ACTIONAREA)); var data = { 'class': 'section', 'action': 'DELETE', 'id': activity.get('id').replace('section-', '') }; this.send_request(data, spinner, function(response) { if (response.deleted) { window.location.reload(true); } }); return; }.bind(this)).catch(function() { // User cancelled. }); }.bind(this)); }, /** * Edit the edit section title for the section * * @protected * @method edit_section_title * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @param {String} action The action that has been requested. * @return Boolean */ edit_section_title: function(ev, button, activity) { // Get the element we're working on var activityid = activity.get('id').replace('section-', ''), instancesection = activity.one(SELECTOR.INSTANCESECTION), thisevent, anchor = instancesection, // Grab the anchor so that we can swap it with the edit form. data = { 'class': 'section', 'field': 'getsectiontitle', 'id': activityid }; // Prevent the default actions. ev.preventDefault(); this.send_request(data, null, function(response) { // Try to retrieve the existing string from the server. var oldtext = response.instancesection; // Create the editor and submit button. var editform = Y.Node.create(''); var editinstructions = Y.Node.create('') .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle')); var editor = Y.Node.create('').setAttrs({ 'value': oldtext, 'autocomplete': 'off', 'aria-describedby': 'id_editinstructions', 'maxLength': '255' // This is the maxlength in DB. }); // Clear the existing content and put the editor in. editform.appendChild(editor); editform.setData('anchor', anchor); instancesection.insert(editinstructions, 'before'); anchor.replace(editform); // Focus and select the editor text. editor.focus().select(); // Cancel the edit if we lose focus or the escape key is pressed. thisevent = editor.on('blur', this.edit_section_title_cancel, this, activity, false); this.editsectionevents.push(thisevent); thisevent = editor.on('key', this.edit_section_title_cancel, 'esc', this, activity, true); this.editsectionevents.push(thisevent); // Handle form submission. thisevent = editform.on('submit', this.edit_section_title_submit, this, activity, oldtext); this.editsectionevents.push(thisevent); }); }, /** * Handles the submit event when editing section heading. * * @protected * @method edit_section_title_submiy * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {String} oldtext The original maxmark the activity or resource had. */ edit_section_title_submit: function(ev, activity, oldtext) { // We don't actually want to submit anything. ev.preventDefault(); var newtext = Y.Lang.trim(activity.one(SELECTOR.SECTIONFORM + ' ' + SELECTOR.SECTIONINPUT).get('value')); var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.INSTANCESECTIONAREA)); this.edit_section_title_clear(activity); if (newtext !== null && newtext !== oldtext) { var instancesection = activity.one(SELECTOR.INSTANCESECTION); var instancesectiontext = newtext; if (newtext.trim() === '') { // Add a visually-hidden default section heading text to make sure we don't end up with an empty section heading. instancesectiontext = M.util.get_string('sectionnoname', 'quiz'); instancesection.addClass('visually-hidden'); } else { // Show the section heading when a non-empty value is set. instancesection.removeClass('visually-hidden'); } instancesection.setContent(instancesectiontext); var data = { 'class': 'section', 'field': 'updatesectiontitle', 'newheading': newtext, 'id': activity.get('id').replace('section-', '') }; this.send_request(data, spinner, function(response) { if (response) { // Set the content of the section heading if for some reason the response is different from the new text. // e.g. filters were applied, the update failed, etc. if (newtext !== response.instancesection) { if (response.instancesection.trim() === '') { // Add a visually-hidden default section heading text. instancesectiontext = M.util.get_string('sectionnoname', 'quiz'); instancesection.addClass('visually-hidden'); } else { instancesectiontext = response.instancesection; // Show the section heading when a non-empty value is set. instancesection.removeClass('visually-hidden'); } instancesection.setContent(instancesectiontext); } activity.one(SELECTOR.EDITSECTIONICON).set('title', M.util.get_string('sectionheadingedit', 'quiz', response.instancesection)); activity.one(SELECTOR.EDITSECTIONICON).set('alt', M.util.get_string('sectionheadingedit', 'quiz', response.instancesection)); var deleteicon = activity.one(SELECTOR.DELETESECTIONICON); if (deleteicon) { deleteicon.set('title', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection)); deleteicon.set('alt', M.util.get_string('sectionheadingremove', 'quiz', response.instancesection)); } } }); } }, /** * Handles the cancel event when editing the activity or resources maxmark. * * @protected * @method edit_maxmark_cancel * @param {EventFacade} ev The event that triggered this. * @param {Node} activity The activity whose maxmark we are altering. * @param {Boolean} preventdefault If true we should prevent the default action from occuring. */ edit_section_title_cancel: function(ev, activity, preventdefault) { if (preventdefault) { ev.preventDefault(); } this.edit_section_title_clear(activity); }, /** * Handles clearing the editing UI and returning things to the original state they were in. * * @protected * @method edit_maxmark_clear * @param {Node} activity The activity whose maxmark we were altering. */ edit_section_title_clear: function(activity) { // Detach all listen events to prevent duplicate triggers new Y.EventHandle(this.editsectionevents).detach(); var editform = activity.one(SELECTOR.SECTIONFORM), instructions = activity.one('#id_editinstructions'); if (editform) { editform.replace(editform.getData('anchor')); } if (instructions) { instructions.remove(); } // Refocus the link which was clicked originally so the user can continue using keyboard nav. Y.later(100, this, function() { activity.one(SELECTOR.EDITSECTION).focus(); }); // This hack is to keep Behat happy until they release a version of // MinkSelenium2Driver that fixes // https://github.com/Behat/MinkSelenium2Driver/issues/80. if (!Y.one('input[name=section]')) { Y.one('body').append(''); } }, /** * Edit the edit shuffle questions for the section * * @protected * @method edit_shuffle_questions * @param {EventFacade} ev The event that was fired. * @param {Node} button The button that triggered this action. * @param {Node} activity The activity node that this action will be performed on. * @return Boolean */ edit_shuffle_questions: function(ev, button, activity) { var newvalue; if (activity.one(SELECTOR.EDITSHUFFLEQUESTIONSACTION).get('checked')) { newvalue = 1; activity.addClass('shuffled'); } else { newvalue = 0; activity.removeClass('shuffled'); } // Prevent the default actions. ev.preventDefault(); // Get the element we're working on var data = { 'class': 'section', 'field': 'updateshufflequestions', 'id': activity.get('id').replace('section-', ''), 'newshuffle': newvalue }; // Send request. var spinner = M.util.add_spinner(Y, activity.one(SELECTOR.EDITSHUFFLEAREA)); this.send_request(data, spinner); } }, { NAME: 'mod_quiz-section-toolbox', ATTRS: { courseid: { 'value': 0 }, quizid: { 'value': 0 } } }); M.mod_quiz.init_section_toolbox = function(config) { return new SECTIONTOOLBOX(config); }; }, '@VERSION@', { "requires": [ "base", "node", "event", "event-key", "io", "moodle-mod_quiz-quizbase", "moodle-mod_quiz-util-slot", "moodle-core-notification-ajaxexception" ] }); build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js000064400000033442152267015160021235 0ustar00YUI.add("moodle-mod_quiz-toolboxes",function(u,a){var e,t,i,n="activityinstance",r="editinstructions",m="editor_displayed",o="select-multiple",c="editing_show",h="titleeditor",_={ACTIONAREA:".actions",ACTIONLINKTEXT:".actionlinktext",ACTIVITYACTION:"a.cm-edit-action[data-action], a.editing_maxmark, a.editing_section, input.shuffle_questions",ACTIVITYFORM:"span.instancemaxmarkcontainer form",ACTIVITYINSTANCE:"."+n,SECTIONINSTANCE:".sectioninstance",ACTIVITYLI:"li.activity, li.section",ACTIVITYMAXMARK:"input[name=maxmark]",COMMANDSPAN:".commands",CONTENTAFTERLINK:"div.contentafterlink",CONTENTWITHOUTLINK:"div.contentwithoutlink",DELETESECTIONICON:"a.editing_delete .icon",EDITMAXMARK:"a.editing_maxmark",EDITSECTION:"a.editing_section",EDITSECTIONICON:"a.editing_section .icon",EDITSHUFFLEQUESTIONSACTION:"input.cm-edit-action[data-action]",EDITSHUFFLEAREA:".instanceshufflequestions .shuffle-progress",HIDE:"a.editing_hide",HIGHLIGHT:"a.editing_highlight",INSTANCENAME:"span.instancename",INSTANCEMAXMARK:"span.instancemaxmark",INSTANCESECTION:"span.instancesection",INSTANCESECTIONAREA:"div.section-heading",MODINDENTDIV:".mod-indent",MODINDENTOUTER:".mod-indent-outer",NUMQUESTIONS:".numberofquestions",PAGECONTENT:"div#page-content",PAGELI:"li.page",SECTIONLI:"li.section",SECTIONUL:"ul.section",SECTIONFORM:".instancesectioncontainer form",SECTIONINPUT:"input[name=section]",SELECTMULTIPLEBUTTON:"#selectmultiplecommand",SELECTMULTIPLECANCELBUTTON:"#selectmultiplecancelcommand",SELECTMULTIPLECHECKBOX:".select-multiple-checkbox",SELECTMULTIPLEDELETEBUTTON:"#selectmultipledeletecommand",SELECTALL:"#questionselectall",SHOW:"a."+c,SLOTLI:"li.slot",SUMMARKS:".mod_quiz_summarks"},s=u.one(document.body);M.mod_quiz=M.mod_quiz||{},u.extend(e=function(){e.superclass.constructor.apply(this,arguments)},u.Base,{send_request:function(e,n,o,t){var i,s,a,c,d;for(s in e=e||{},i=this.get("config").pageparams)e[s]=i[s];if(e.sesskey=M.cfg.sesskey,e.courseid=this.get("courseid"),e.quizid=this.get("quizid"),a=M.cfg.wwwroot+this.get("ajaxurl"),c=[],d={method:"POST",data:e,on:{success:function(e,t){try{(c=u.JSON.parse(t.responseText)).error&&new M.core.ajaxException(c)}catch(i){}c.hasOwnProperty("newsummarks")&&u.one(_.SUMMARKS).setHTML(c.newsummarks),c.hasOwnProperty("newnumquestions")&&u.one(_.NUMQUESTIONS).setHTML(M.util.get_string("numquestionsx","quiz",c.newnumquestions)),o&&u.bind(o,this,c)(),n&&window.setTimeout(function(){n.hide()},400)},failure:function(e,t){n&&n.hide(),new M.core.ajaxException(t)}},context:this},t)for(s in t)d[s]=t[s];return n&&n.show(),u.io(a,d),this}},{NAME:"mod_quiz-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0},ajaxurl:{value:null},config:{value:{}}}}),u.extend(t=function(){t.superclass.constructor.apply(this,arguments)},e,{editmaxmarkevents:[],NODE_PAGE:1,NODE_SLOT:2,NODE_JOIN:3,initializer:function(){M.mod_quiz.quizbase.register_module(this),u.delegate("click",this.handle_data_action,s,_.ACTIVITYACTION,this),u.delegate("click",this.handle_data_action,s,_.DEPENDENCY_LINK,this),this.initialise_select_multiple()},initialise_select_multiple:function(){u.one(_.SELECTMULTIPLEBUTTON).on("click",function(e){e.preventDefault(),u.one("body").addClass(o)}),u.one(_.SELECTMULTIPLECANCELBUTTON).on("click",function(e){e.preventDefault(),u.one("body").removeClass(o)}),u.delegate("click",this.delete_multiple_action,s,_.SELECTMULTIPLEDELETEBUTTON,this)},handle_data_action:function(e){var t=e.target,i=(t=t.test("a")?t:t.ancestor(_.ACTIVITYACTION)).getData("action"),n=t.ancestor(_.ACTIVITYLI);if(t.test("a")&&i&&n)switch(i){case"editmaxmark":this.edit_maxmark(e,t,n,i);break;case"delete":this.delete_with_confirmation(e,t,n,i);break;case"addpagebreak":case"removepagebreak":this.update_page_break(e,t,n,i);break;case"adddependency":case"removedependency":this.update_dependency(e,t,n,i)}},add_spinner:function(e){e=e.one(_.ACTIONAREA);return e?M.util.add_spinner(u,e):null},delete_with_confirmation:function(i,e,t){var n,o;i.preventDefault(),n=t,o=M.util.get_string("pluginname","qtype_"+n.getAttribute("class").match(/qtype_([^\s]*)/)[1]),require(["core/notification"],function(e){e.saveCancelPromise(M.util.get_string("confirm","moodle"),M.util.get_string("confirmremovequestion","quiz",o),M.util.get_string("yes","moodle")).then(function(){var e=this.add_spinner(n),t={"class":"resource",action:"DELETE",id:u.Moodle.mod_quiz.util.slot.getId(n)};this.send_request(t,e,function(e){e.deleted&&(u.Moodle.mod_quiz.util.slot.remove(n),this.reorganise_edit_page(),M.core.actionmenu&&M.core.actionmenu.instance&&M.core.actionmenu.instance.hideMenu(i))})}.bind(this))["catch"](function(){})}.bind(this))},find_sections_that_would_become_empty:function(){var n,e=u.all(_.SECTIONLI);return 1'),t=u.Node.create('').set("innerHTML",M.util.get_string("edittitleinstructions","moodle")),i=u.Node.create('').setAttrs({value:d,autocomplete:"off","aria-describedby":"id_editinstructions",maxLength:"12",size:parseInt(this.get("config").questiondecimalpoints,10)+2}),e.appendChild(i),e.setData("anchor",l),a.insert(t,"before"),l.replace(e),o.addClass(m),i.focus().select(),s=i.on("blur",this.edit_maxmark_cancel,this,o,!1),this.editmaxmarkevents.push(s),s=i.on("key",this.edit_maxmark_cancel,"esc",this,o,!0),this.editmaxmarkevents.push(s),s=e.on("submit",this.edit_maxmark_submit,this,o,c),this.editmaxmarkevents.push(s)})},edit_maxmark_submit:function(e,t,i){var n;e.preventDefault(),e=u.Lang.trim(t.one(_.ACTIVITYFORM+" "+_.ACTIVITYMAXMARK).get("value")),n=this.add_spinner(t),this.edit_maxmark_clear(t),t.one(_.INSTANCEMAXMARK).setContent(e),null!==e&&""!==e&&e!==i&&(i={"class":"resource",field:"updatemaxmark",maxmark:e,id:u.Moodle.mod_quiz.util.slot.getId(t)},this.send_request(i,n,function(e){e.instancemaxmark&&t.one(_.INSTANCEMAXMARK).setContent(e.instancemaxmark)}))},edit_maxmark_cancel:function(e,t,i){i&&e.preventDefault(),this.edit_maxmark_clear(t)},edit_maxmark_clear:function(e){new u.EventHandle(this.editmaxmarkevents).detach();var t=e.one(_.ACTIVITYFORM),i=e.one("#id_editinstructions");t&&t.replace(t.getData("anchor")),i&&i.remove(),e.removeClass(m),u.later(100,this,function(){e.one(_.EDITMAXMARK).focus()}),u.one("input[name=maxmark")||u.one("body").append('')},update_page_break:function(e,t,i,n){var o,s;return e.preventDefault(),e=i.next("li.activity.slot"),o=this.add_spinner(e),s="removepagebreak"===n?1:2,e={"class":"resource",field:"updatepagebreak",id:u.Moodle.mod_quiz.util.slot.getId(e),value:s},this.send_request(e,o,function(e){e.slots&&("addpagebreak"===n?u.Moodle.mod_quiz.util.page.add(i):(e=i.next(u.Moodle.mod_quiz.util.page.SELECTORS.PAGE),u.Moodle.mod_quiz.util.page.remove(e,!0)),this.reorganise_edit_page())}),this},update_dependency:function(e,t,i,n){return e.preventDefault(),e=this.add_spinner(i),n={"class":"resource",field:"updatedependency",id:u.Moodle.mod_quiz.util.slot.getId(i),value:"adddependency"===n?1:0},this.send_request(n,e,function(e){e.hasOwnProperty("requireprevious")&&u.Moodle.mod_quiz.util.slot.updateDependencyIcon(i,e.requireprevious)}),this},reorganise_edit_page:function(){u.Moodle.mod_quiz.util.slot.reorderSlots(),u.Moodle.mod_quiz.util.slot.reorderPageBreaks(),u.Moodle.mod_quiz.util.page.reorderPages(),u.Moodle.mod_quiz.util.slot.updateOneSlotSections(),u.Moodle.mod_quiz.util.slot.updateAllDependencyIcons()},NAME:"mod_quiz-resource-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0}}}),M.mod_quiz.resource_toolbox=null,M.mod_quiz.init_resource_toolbox=function(e){return M.mod_quiz.resource_toolbox=new t(e),M.mod_quiz.resource_toolbox},u.extend(i=function(){i.superclass.constructor.apply(this,arguments)},e,{editsectionevents:[],initializer:function(){M.mod_quiz.quizbase.register_module(this),s.delegate("key",this.handle_data_action,"down:enter",_.ACTIVITYACTION,this),u.delegate("click",this.handle_data_action,s,_.ACTIVITYACTION,this),u.delegate("change",this.handle_data_action,s,_.EDITSHUFFLEQUESTIONSACTION,this)},handle_data_action:function(e){var t=e.target,i=(t=t.test("a")||t.test("input[data-action]")?t:t.ancestor(_.ACTIVITYACTION)).getData("action"),n=t.ancestor(_.ACTIVITYLI);if((t.test("a")||t.test("input[data-action]"))&&i&&n)switch(i){case"edit_section_title":this.edit_section_title(e,t,n,i);break;case"shuffle_questions":this.edit_shuffle_questions(e,t,n,i);break;case"deletesection":this.delete_section_with_confirmation(e,t,n,i)}},delete_section_with_confirmation:function(e,t,i){e.preventDefault(),require(["core/notification"],function(e){e.saveCancelPromise(M.util.get_string("confirm","moodle"),M.util.get_string("confirmremovesectionheading","quiz",i.getData("sectionname")),M.util.get_string("yes","moodle")).then(function(){var e=M.util.add_spinner(u,i.one(_.ACTIONAREA)),t={"class":"section",action:"DELETE",id:i.get("id").replace("section-","")};this.send_request(t,e,function(e){e.deleted&&window.location.reload(!0)})}.bind(this))["catch"](function(){})}.bind(this))},edit_section_title:function(e,t,o){var s,i=o.get("id").replace("section-",""),a=o.one(_.INSTANCESECTION),c=a,i={"class":"section",field:"getsectiontitle",id:i};e.preventDefault(),this.send_request(i,null,function(e){var e=e.instancesection,t=u.Node.create(''),i=u.Node.create('').set("innerHTML",M.util.get_string("edittitleinstructions","moodle")),n=u.Node.create('').setAttrs({value:e,autocomplete:"off","aria-describedby":"id_editinstructions",maxLength:"255"});t.appendChild(n),t.setData("anchor",c),a.insert(i,"before"),c.replace(t),n.focus().select(),s=n.on("blur",this.edit_section_title_cancel,this,o,!1),this.editsectionevents.push(s),s=n.on("key",this.edit_section_title_cancel,"esc",this,o,!0),this.editsectionevents.push(s),s=t.on("submit",this.edit_section_title_submit,this,o,e),this.editsectionevents.push(s)})},edit_section_title_submit:function(e,i,t){var n,o,s;e.preventDefault(),n=u.Lang.trim(i.one(_.SECTIONFORM+" "+_.SECTIONINPUT).get("value")),e=M.util.add_spinner(u, i.one(_.INSTANCESECTIONAREA)),this.edit_section_title_clear(i),null!==n&&n!==t&&(o=i.one(_.INSTANCESECTION),""===(s=n).trim()?(s=M.util.get_string("sectionnoname","quiz"),o.addClass("visually-hidden")):o.removeClass("visually-hidden"),o.setContent(s),t={"class":"section",field:"updatesectiontitle",newheading:n,id:i.get("id").replace("section-","")},this.send_request(t,e,function(e){var t;e&&(n!==e.instancesection&&(""===e.instancesection.trim()?(s=M.util.get_string("sectionnoname","quiz"),o.addClass("visually-hidden")):(s=e.instancesection,o.removeClass("visually-hidden")),o.setContent(s)),i.one(_.EDITSECTIONICON).set("title",M.util.get_string("sectionheadingedit","quiz",e.instancesection)),i.one(_.EDITSECTIONICON).set("alt",M.util.get_string("sectionheadingedit","quiz",e.instancesection)),(t=i.one(_.DELETESECTIONICON))&&(t.set("title",M.util.get_string("sectionheadingremove","quiz",e.instancesection)),t.set("alt",M.util.get_string("sectionheadingremove","quiz",e.instancesection))))}))},edit_section_title_cancel:function(e,t,i){i&&e.preventDefault(),this.edit_section_title_clear(t)},edit_section_title_clear:function(e){new u.EventHandle(this.editsectionevents).detach();var t=e.one(_.SECTIONFORM),i=e.one("#id_editinstructions");t&&t.replace(t.getData("anchor")),i&&i.remove(),u.later(100,this,function(){e.one(_.EDITSECTION).focus()}),u.one("input[name=section]")||u.one("body").append('')},edit_shuffle_questions:function(e,t,i){var n;i.one(_.EDITSHUFFLEQUESTIONSACTION).get("checked")?(n=1,i.addClass("shuffled")):(n=0,i.removeClass("shuffled")),e.preventDefault(),e={"class":"section",field:"updateshufflequestions",id:i.get("id").replace("section-",""),newshuffle:n},n=M.util.add_spinner(u,i.one(_.EDITSHUFFLEAREA)),this.send_request(e,n)}},{NAME:"mod_quiz-section-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0}}}),M.mod_quiz.init_section_toolbox=function(e){return new i(e)}},"@VERSION@",{requires:["base","node","event","event-key","io","moodle-mod_quiz-quizbase","moodle-mod_quiz-util-slot","moodle-core-notification-ajaxexception"]});build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser-min.js000064400000002515152267015160023662 0ustar00YUI.add("moodle-mod_quiz-questionchooser",function(t,e){var o='.menu [data-action="addquestion"]',n="div.createnewquestion",r="div.chooserdialoguebody",s="div.choosertitle",i=function(){i.superclass.constructor.apply(this,arguments)};t.extend(i,M.core.chooserdialogue,{initializer:function(){t.one("body").delegate("click",this.display_dialogue,o,this)},display_dialogue:function(e){var o,i;e.preventDefault(),o=t.one(n+" "+r),i=t.one(n+" "+s),null===this.container&&(this.setup_chooser_dialogue(o,i,{}),this.prepare_chooser()),o=t.QueryString.parse(e.currentTarget.get("search").substring(1)),i=this.container.one("form"),this.parameters_to_hidden_input(o,i,"returnurl"),this.parameters_to_hidden_input(o,i,"cmid"),this.parameters_to_hidden_input(o,i,"category"),this.parameters_to_hidden_input(o,i,"addonpage"),this.parameters_to_hidden_input(o,i,"appendqnumstring"),this.display_chooser(e)},parameters_to_hidden_input:function(e,o,i){var e=e.hasOwnProperty(i)?e[i]:"",t=o.one("input[name="+i+"]");t||(t=o.appendChild('')).set("name",i),t.set("value",e)}},{NAME:"mod_quiz-questionchooser"}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.init_questionchooser=function(){return M.mod_quiz.question_chooser=new i({}),M.mod_quiz.question_chooser}},"@VERSION@",{requires:["moodle-core-chooserdialogue","moodle-mod_quiz-util","querystring-parse"]});build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser-debug.js000064400000005247152267015160024172 0ustar00YUI.add('moodle-mod_quiz-questionchooser', function (Y, NAME) { var CSS = { ADDNEWQUESTIONBUTTONS: '.menu [data-action="addquestion"]', CREATENEWQUESTION: 'div.createnewquestion', CHOOSERDIALOGUE: 'div.chooserdialoguebody', CHOOSERHEADER: 'div.choosertitle' }; /** * The questionchooser class is responsible for instantiating and displaying the question chooser * when viewing a quiz in editing mode. * * @class questionchooser * @constructor * @protected * @extends M.core.chooserdialogue */ var QUESTIONCHOOSER = function() { QUESTIONCHOOSER.superclass.constructor.apply(this, arguments); }; Y.extend(QUESTIONCHOOSER, M.core.chooserdialogue, { initializer: function() { Y.one('body').delegate('click', this.display_dialogue, CSS.ADDNEWQUESTIONBUTTONS, this); }, display_dialogue: function(e) { e.preventDefault(); var dialogue = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERDIALOGUE), header = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.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 parameters = Y.QueryString.parse(e.currentTarget.get('search').substring(1)); var form = this.container.one('form'); this.parameters_to_hidden_input(parameters, form, 'returnurl'); this.parameters_to_hidden_input(parameters, form, 'cmid'); this.parameters_to_hidden_input(parameters, form, 'category'); this.parameters_to_hidden_input(parameters, form, 'addonpage'); this.parameters_to_hidden_input(parameters, form, 'appendqnumstring'); // Display the chooser dialogue. this.display_chooser(e); }, parameters_to_hidden_input: function(parameters, form, name) { var value; if (parameters.hasOwnProperty(name)) { value = parameters[name]; } else { value = ''; } var input = form.one('input[name=' + name + ']'); if (!input) { input = form.appendChild(''); input.set('name', name); } input.set('value', value); } }, { NAME: 'mod_quiz-questionchooser' }); M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.init_questionchooser = function() { M.mod_quiz.question_chooser = new QUESTIONCHOOSER({}); return M.mod_quiz.question_chooser; }; }, '@VERSION@', {"requires": ["moodle-core-chooserdialogue", "moodle-mod_quiz-util", "querystring-parse"]}); build/moodle-mod_quiz-questionchooser/moodle-mod_quiz-questionchooser.js000064400000005247152267015160023106 0ustar00YUI.add('moodle-mod_quiz-questionchooser', function (Y, NAME) { var CSS = { ADDNEWQUESTIONBUTTONS: '.menu [data-action="addquestion"]', CREATENEWQUESTION: 'div.createnewquestion', CHOOSERDIALOGUE: 'div.chooserdialoguebody', CHOOSERHEADER: 'div.choosertitle' }; /** * The questionchooser class is responsible for instantiating and displaying the question chooser * when viewing a quiz in editing mode. * * @class questionchooser * @constructor * @protected * @extends M.core.chooserdialogue */ var QUESTIONCHOOSER = function() { QUESTIONCHOOSER.superclass.constructor.apply(this, arguments); }; Y.extend(QUESTIONCHOOSER, M.core.chooserdialogue, { initializer: function() { Y.one('body').delegate('click', this.display_dialogue, CSS.ADDNEWQUESTIONBUTTONS, this); }, display_dialogue: function(e) { e.preventDefault(); var dialogue = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERDIALOGUE), header = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.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 parameters = Y.QueryString.parse(e.currentTarget.get('search').substring(1)); var form = this.container.one('form'); this.parameters_to_hidden_input(parameters, form, 'returnurl'); this.parameters_to_hidden_input(parameters, form, 'cmid'); this.parameters_to_hidden_input(parameters, form, 'category'); this.parameters_to_hidden_input(parameters, form, 'addonpage'); this.parameters_to_hidden_input(parameters, form, 'appendqnumstring'); // Display the chooser dialogue. this.display_chooser(e); }, parameters_to_hidden_input: function(parameters, form, name) { var value; if (parameters.hasOwnProperty(name)) { value = parameters[name]; } else { value = ''; } var input = form.one('input[name=' + name + ']'); if (!input) { input = form.appendChild(''); input.set('name', name); } input.set('value', value); } }, { NAME: 'mod_quiz-questionchooser' }); M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.init_questionchooser = function() { M.mod_quiz.question_chooser = new QUESTIONCHOOSER({}); return M.mod_quiz.question_chooser; }; }, '@VERSION@', {"requires": ["moodle-core-chooserdialogue", "moodle-mod_quiz-util", "querystring-parse"]}); build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-debug.js000064400000010567152267015160021155 0ustar00YUI.add('moodle-mod_quiz-quizbase', function (Y, NAME) { /** * The quizbase class to provide shared functionality to Modules within Moodle. * * @module moodle-mod_quiz-quizbase */ var QUIZBASENAME = 'mod_quiz-quizbase'; var QUIZBASE = function() { QUIZBASE.superclass.constructor.apply(this, arguments); }; /** * The coursebase class to provide shared functionality to Modules within * Moodle. * * @class M.course.coursebase * @constructor */ Y.extend(QUIZBASE, 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: QUIZBASENAME, ATTRS: {} }); // Ensure that M.course exists and that coursebase is initialised correctly M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.quizbase = M.mod_quiz.quizbase || new QUIZBASE(); // Abstract functions that needs to be defined per format (course/format/somename/format.js) M.mod_quiz.edit = M.mod_quiz.edit || {}; /** * Swap section (should be defined in format.js if requred) * * @param {YUI} Y YUI3 instance * @param {string} node1 node to swap to * @param {string} node2 node to swap with * @return {NodeList} section list */ M.mod_quiz.edit.swap_sections = function(Y, node1, node2) { var CSS = { COURSECONTENT: 'mod-quiz-edit-content', SECTIONADDMENUS: 'section_add_menus' }; var sectionlist = Y.Node.all('.' + CSS.COURSECONTENT + ' li.section'); // Swap menus. sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS)); }; /** * 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. * * @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 * @return void */ M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) { var CSS = { SECTIONNAME: 'sectionname' }, SELECTORS = { SECTIONLEFTSIDE: '.left .section-handle .icon' }; if (response.action === 'move') { // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates. if (sectionfrom > sectionto) { var temp = sectionto; sectionto = sectionfrom; sectionfrom = temp; } // Update titles and move icons in all affected sections. var ele, str, stridx, newstr; for (var i = sectionfrom; i <= sectionto; i++) { // Update section title. sectionlist.item(i).one('.' + CSS.SECTIONNAME).setContent(response.sectiontitles[i]); // Update move icon. ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE); str = ele.getAttribute('alt'); stridx = str.lastIndexOf(' '); newstr = str.substr(0, stridx + 1) + i; ele.setAttribute('alt', newstr); ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed. // Remove the current class as section has been moved. sectionlist.item(i).removeClass('current'); } // If there is a current section, apply corresponding class in order to highlight it. if (response.current !== -1) { // Add current class to the required section. sectionlist.item(response.current).addClass('current'); } } }; }, '@VERSION@', {"requires": ["base", "node"]}); build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase.js000064400000010567152267015160020071 0ustar00YUI.add('moodle-mod_quiz-quizbase', function (Y, NAME) { /** * The quizbase class to provide shared functionality to Modules within Moodle. * * @module moodle-mod_quiz-quizbase */ var QUIZBASENAME = 'mod_quiz-quizbase'; var QUIZBASE = function() { QUIZBASE.superclass.constructor.apply(this, arguments); }; /** * The coursebase class to provide shared functionality to Modules within * Moodle. * * @class M.course.coursebase * @constructor */ Y.extend(QUIZBASE, 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: QUIZBASENAME, ATTRS: {} }); // Ensure that M.course exists and that coursebase is initialised correctly M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.quizbase = M.mod_quiz.quizbase || new QUIZBASE(); // Abstract functions that needs to be defined per format (course/format/somename/format.js) M.mod_quiz.edit = M.mod_quiz.edit || {}; /** * Swap section (should be defined in format.js if requred) * * @param {YUI} Y YUI3 instance * @param {string} node1 node to swap to * @param {string} node2 node to swap with * @return {NodeList} section list */ M.mod_quiz.edit.swap_sections = function(Y, node1, node2) { var CSS = { COURSECONTENT: 'mod-quiz-edit-content', SECTIONADDMENUS: 'section_add_menus' }; var sectionlist = Y.Node.all('.' + CSS.COURSECONTENT + ' li.section'); // Swap menus. sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS)); }; /** * 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. * * @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 * @return void */ M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) { var CSS = { SECTIONNAME: 'sectionname' }, SELECTORS = { SECTIONLEFTSIDE: '.left .section-handle .icon' }; if (response.action === 'move') { // If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates. if (sectionfrom > sectionto) { var temp = sectionto; sectionto = sectionfrom; sectionfrom = temp; } // Update titles and move icons in all affected sections. var ele, str, stridx, newstr; for (var i = sectionfrom; i <= sectionto; i++) { // Update section title. sectionlist.item(i).one('.' + CSS.SECTIONNAME).setContent(response.sectiontitles[i]); // Update move icon. ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE); str = ele.getAttribute('alt'); stridx = str.lastIndexOf(' '); newstr = str.substr(0, stridx + 1) + i; ele.setAttribute('alt', newstr); ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed. // Remove the current class as section has been moved. sectionlist.item(i).removeClass('current'); } // If there is a current section, apply corresponding class in order to highlight it. if (response.current !== -1) { // Add current class to the required section. sectionlist.item(response.current).addClass('current'); } } }; }, '@VERSION@', {"requires": ["base", "node"]}); build/moodle-mod_quiz-quizbase/moodle-mod_quiz-quizbase-min.js000064400000002230152267015160020636 0ustar00YUI.add("moodle-mod_quiz-quizbase",function(e,t){var i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,e.Base,{registermodules:[],register_module:function(e){return this.registermodules.push(e),this},invoke_function:function(e,t){for(var i in this.registermodules)e in this.registermodules[i]&&this.registermodules[i][e](t);return this}},{NAME:"mod_quiz-quizbase",ATTRS:{}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.quizbase=M.mod_quiz.quizbase||new i,M.mod_quiz.edit=M.mod_quiz.edit||{},M.mod_quiz.edit.swap_sections=function(e,t,i){var s="mod-quiz-edit-content",o="section_add_menus",s=e.Node.all("."+s+" li.section");s.item(t).one("."+o).swap(s.item(i).one("."+o))},M.mod_quiz.edit.process_sections=function(a,e,t,i,s){var o,n,u,r,d,m="sectionname",c=".left .section-handle .icon";if("move"===t.action){for(s 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); }; /** * 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); }; }, '@VERSION@', { "requires": [ "base", "node", "io", "dom", "dd", "dd-scroll", "moodle-core-dragdrop", "moodle-core-notification", "moodle-mod_quiz-quizbase", "moodle-mod_quiz-util-base", "moodle-mod_quiz-util-page", "moodle-mod_quiz-util-slot", "moodle-course-util" ] }); build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop.js000064400000043744152267015160020032 0ustar00YUI.add('moodle-mod_quiz-dragdrop', function (Y, NAME) { /* eslint-disable no-unused-vars */ /** * Drag and Drop for Quiz sections and slots. * * @module moodle-mod-quiz-dragdrop */ var CSS = { ACTIONAREA: '.actions', ACTIVITY: 'activity', ACTIVITYINSTANCE: 'activityinstance', CONTENT: 'content', COURSECONTENT: 'mod-quiz-edit-content', EDITINGMOVE: 'editing_move', ICONCLASS: 'iconsmall', JUMPMENU: 'jumpmenu', LEFT: 'left', LIGHTBOX: 'lightbox', MOVEDOWN: 'movedown', MOVEUP: 'moveup', PAGE: 'page', PAGECONTENT: 'page-content', RIGHT: 'right', SECTION: 'slots', SECTIONADDMENUS: 'section_add_menus', SECTIONHANDLE: 'section-handle', SLOTS: 'slots', SUMMARY: 'summary', SECTIONDRAGGABLE: 'sectiondraggable' }, // The CSS selectors we use. SELECTOR = { PAGE: 'li.page', SLOT: 'li.slot' }; /** * 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) { return; } 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))) { // 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); }; /** * 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); }; }, '@VERSION@', { "requires": [ "base", "node", "io", "dom", "dd", "dd-scroll", "moodle-core-dragdrop", "moodle-core-notification", "moodle-mod_quiz-quizbase", "moodle-mod_quiz-util-base", "moodle-mod_quiz-util-page", "moodle-mod_quiz-util-slot", "moodle-course-util" ] }); build/moodle-mod_quiz-dragdrop/moodle-mod_quiz-dragdrop-min.js000064400000014272152267015160020605 0ustar00YUI.add("moodle-mod_quiz-dragdrop",function(g,e){var o,u=".actions",t="activity",p="mod-quiz-edit-content",r="editing_move",s="iconsmall",c="jumpmenu",n="left",h="movedown",_="moveup",m="page-content",d="right",f="slots",a="section-handle",v="slots",l="sectiondraggable",z="li.page",q="li.slot",i=function(){i.superclass.constructor.apply(this,arguments)};g.extend(i,M.core.dragdrop,{sectionlistselector:null,initializer:function(){if(this.groups=[l],this.samenodeclass="section",this.parentnodeclass="slots",g.Node.one("."+c))return!1;var e;this.sectionlistselector="li.section",this.sectionlistselector&&(this.sectionlistselector="."+p+" "+this.sectionlistselector,this.setup_for_section(this.sectionlistselector),(e=new g.DD.Delegate({container:"."+p,nodes:"."+l,target:!0,handles:["."+n],dragConfig:{groups:this.groups}})).dd.plug(g.Plugin.DDProxy,{moveOnEnd:!1}),e.dd.plug(g.Plugin.DDConstrained,{constrain:"#"+m,stickY:!0}),e.dd.plug(g.Plugin.DDWinScroll))},setup_for_section:function(e){g.Node.all(e).each(function(e){var o,t,i,s=g.Moodle.core_course.util.section.getId(e);0'),t=g.Node.create('
                            ');t.setStyle("margin",0),t.setContent(e.get("node").get("innerHTML")),o.appendChild(t),e.get("dragNode").setContent(o),e.get("dragNode").addClass(p)},drag_dropmiss:function(e){this.drop_hit(e)},get_section_index:function(e){var o="."+p+" li.section",o=g.all(o);return o.indexOf(e)-o.indexOf(g.one("#section-0"))},drop_hit:function(d){var r,a,e,n,o,t=d.drag,c=t.get("node"),i=g.Moodle.core_course.util.section.getId(c),l=i,s=this.get_section_index(c),u=s;if(i!==s){for(o in ug.Moodle.core_course.util.section.getId(r.item(i))&&(n=r.item(i-1).get("id"),r.item(i-1).set("id",r.item(i).get("id")),r.item(i).set("id",n),M.mod_quiz.edit.swap_sections(g,i-1,i),s=!0)}while(u-=1,s);window.setTimeout(function(){a.hide()},250)},failure:function(e,o){this.ajax_failure(o),a.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(e){new i(e)},g.extend(o=function(){o.superclass.constructor.apply(this,arguments)},M.core.dragdrop,{initializer:function(){var e;this.groups=["resource"],this.samenodeclass=t,this.parentnodeclass=f,this.samenodelabel={identifier:"dragtoafter",component:"quiz"},this.parentnodelabel={identifier:"dragtostart",component:"quiz"},this.setup_for_section(),e="li."+t,(e=new g.DD.Delegate({container:"."+p,nodes:e,target:!0,handles:["."+r],dragConfig:{groups:this.groups}})).dd.plug(g.Plugin.DDProxy,{moveOnEnd:!1,cloneNode:!0}),e.dd.plug(g.Plugin.DDConstrained,{constrain:"#"+v}),e.dd.plug(g.Plugin.DDWinScroll),M.mod_quiz.quizbase.register_module(this),M.mod_quiz.dragres=this},setup_for_section:function(){g.Node.all(".mod-quiz-edit-content ul.slots ul.section").each(function(e){e.setAttribute("data-draggroups",this.groups.join(" ")),new g.DD.Drop({node:e,groups:this.groups,padding:"20 0 20 0"}),this.setup_for_resource("li.activity")},this)},setup_for_resource:function(e){g.Node.all(e).each(function(e){var o,e=e.one("a."+r);e&&(o=this.get_drag_handle(M.util.get_string("move","moodle"),r,s,!0),e.replace(o))},this)},drag_start:function(e){e=e.target;e.get("dragNode").setContent(e.get("node").get("innerHTML")),e.get("dragNode").all(".icon").setStyle("vertical-align","baseline")},drag_dropmiss:function(e){this.drop_hit(e)},drop_hit:function(e){var o,t=e.drag,i=t.get("node"),e=i.one(u),s=M.util.add_spinner(g,e),n={},d=this.get("config").pageparams;for(o in d)n[o]=d[o];n.sesskey=M.cfg.sesskey,n.courseid=this.get("courseid"),n.quizid=this.get("quizid"),n["class"]="resource",n.field="move",n.id=Number(g.Moodle.mod_quiz.util.slot.getId(i)),n.sectionId=g.Moodle.core_course.util.section.getId(i.ancestor("li.section",!0)),(e=i.previous(q))&&(n.previousid=Number(g.Moodle.mod_quiz.util.slot.getId(e))),(e=i.previous(z))&&(n.page=Number(g.Moodle.mod_quiz.util.page.getId(e))),e=M.cfg.wwwroot+this.get("ajaxurl"),g.io(e,{method:"POST",data:n,on:{start:function(){this.lock_drag_handle(t,r),s.show()},success:function(e,o){o=g.JSON.parse(o.responseText),o={element:i,visible:o.visible};M.mod_quiz.quizbase.invoke_function("set_visibility_resource_ui",o),this.unlock_drag_handle(t,r),window.setTimeout(function(){s.hide()},250),M.mod_quiz.resource_toolbox.reorganise_edit_page()},failure:function(e,o){this.ajax_failure(o),this.unlock_drag_handle(t,a),s.hide(),window.location.reload(!0)}},context:this})},global_drop_over:function(e){var o,t,i;e.drop&&e.drop.inGroup(this.groups)&&(o=e.drag.get("node"),t=e.drop.get("node"),this.lastdroptarget=e.drop,t.hasClass(this.samenodeclass)?(i=this.goingup?"before":"after",t.insert(o,i)):!t.hasClass(this.parentnodeclass)&&!t.test('[data-droptarget="1"]')||t.contains(o)||(this.goingup?t.append(o):t.prepend(o)),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(e){new o(e)}},"@VERSION@",{requires:["base","node","io","dom","dd","dd-scroll","moodle-core-dragdrop","moodle-core-notification","moodle-mod_quiz-quizbase","moodle-mod_quiz-util-base","moodle-mod_quiz-util-page","moodle-mod_quiz-util-slot","moodle-course-util"]});build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-min.js000064400000001424152267015160020302 0ustar00YUI.add("moodle-mod_quiz-modform",function(e,i){var t=function(){t.superclass.constructor.apply(this,arguments)};e.extend(t,e.Base,{repaginateCheckbox:null,qppSelect:null,qppInitialValue:0,initializer:function(){this.repaginateCheckbox=e.one("#id_repaginatenow"),this.repaginateCheckbox&&(this.qppSelect=e.one("#id_questionsperpage"),this.qppInitialValue=this.qppSelect.get("value"),this.qppSelect.on("change",this.qppChanged,this))},qppChanged:function(){e.later(50,this,function(){this.repaginateCheckbox.get("disabled")||this.repaginateCheckbox.set("checked",this.qppSelect.get("value")!==this.qppInitialValue)})}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.modform=M.mod_quiz.modform||new t,M.mod_quiz.modform.init=function(){return new t}},"@VERSION@",{requires:["base","node","event"]});build/moodle-mod_quiz-modform/moodle-mod_quiz-modform-debug.js000064400000002745152267015160020614 0ustar00YUI.add('moodle-mod_quiz-modform', function (Y, NAME) { /** * The modform class has all the JavaScript specific to mod/quiz/mod_form.php. * * @module moodle-mod_quiz-modform */ var MODFORM = function() { MODFORM.superclass.constructor.apply(this, arguments); }; /** * The coursebase class to provide shared functionality to Modules within * Moodle. * * @class M.course.coursebase * @constructor */ Y.extend(MODFORM, Y.Base, { repaginateCheckbox: null, qppSelect: null, qppInitialValue: 0, initializer: function() { this.repaginateCheckbox = Y.one('#id_repaginatenow'); if (!this.repaginateCheckbox) { // The checkbox only appears when editing an existing quiz. return; } this.qppSelect = Y.one('#id_questionsperpage'); this.qppInitialValue = this.qppSelect.get('value'); this.qppSelect.on('change', this.qppChanged, this); }, qppChanged: function() { Y.later(50, this, function() { if (!this.repaginateCheckbox.get('disabled')) { this.repaginateCheckbox.set('checked', this.qppSelect.get('value') !== this.qppInitialValue); } }); } }); // Ensure that M.mod_quiz exists and that coursebase is initialised correctly M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.modform = M.mod_quiz.modform || new MODFORM(); M.mod_quiz.modform.init = function() { return new MODFORM(); }; }, '@VERSION@', {"requires": ["base", "node", "event"]}); build/moodle-mod_quiz-modform/moodle-mod_quiz-modform.js000064400000002745152267015160017530 0ustar00YUI.add('moodle-mod_quiz-modform', function (Y, NAME) { /** * The modform class has all the JavaScript specific to mod/quiz/mod_form.php. * * @module moodle-mod_quiz-modform */ var MODFORM = function() { MODFORM.superclass.constructor.apply(this, arguments); }; /** * The coursebase class to provide shared functionality to Modules within * Moodle. * * @class M.course.coursebase * @constructor */ Y.extend(MODFORM, Y.Base, { repaginateCheckbox: null, qppSelect: null, qppInitialValue: 0, initializer: function() { this.repaginateCheckbox = Y.one('#id_repaginatenow'); if (!this.repaginateCheckbox) { // The checkbox only appears when editing an existing quiz. return; } this.qppSelect = Y.one('#id_questionsperpage'); this.qppInitialValue = this.qppSelect.get('value'); this.qppSelect.on('change', this.qppChanged, this); }, qppChanged: function() { Y.later(50, this, function() { if (!this.repaginateCheckbox.get('disabled')) { this.repaginateCheckbox.set('checked', this.qppSelect.get('value') !== this.qppInitialValue); } }); } }); // Ensure that M.mod_quiz exists and that coursebase is initialised correctly M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.modform = M.mod_quiz.modform || new MODFORM(); M.mod_quiz.modform.init = function() { return new MODFORM(); }; }, '@VERSION@', {"requires": ["base", "node", "event"]}); build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js000064400000012027152267015160021065 0ustar00YUI.add("moodle-mod_quiz-util-slot",function(u,e){u.namespace("Moodle.mod_quiz.util.slot"),u.Moodle.mod_quiz.util.slot={CSS:{SLOT:"slot",QUESTIONTYPEDESCRIPTION:"qtype_description",CANNOT_DEPEND:"question_dependency_cannot_depend"},CONSTANTS:{SLOTIDPREFIX:"slot-",QUESTION:M.util.get_string("question","moodle")},SELECTORS:{SLOT:"li.slot",INSTANCENAME:".instancename",NUMBER:"span.slotnumber",PAGECONTENT:"div#page-content",PAGEBREAK:"span.page_split_join_wrapper",ICON:".icon",QUESTIONTYPEDESCRIPTION:".qtype_description",SECTIONUL:"ul.section",DEPENDENCY_WRAPPER:".question_dependency_wrapper",DEPENDENCY_LINK:".question_dependency_wrapper .cm-edit-action",DEPENDENCY_ICON:".question_dependency_wrapper .icon"},getSlotFromComponent:function(e){return u.one(e).ancestor(this.SELECTORS.SLOT,!0)},getId:function(e){e=e.get("id").replace(this.CONSTANTS.SLOTIDPREFIX,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},getName:function(e){e=e.one(this.SELECTORS.INSTANCENAME);return e?e.get("firstChild").get("data"):null},getNumber:function(e){if(!e)return!1;e=e.one(this.SELECTORS.NUMBER).get("text").replace(this.CONSTANTS.QUESTION,"");return!("number"!=typeof(e=parseInt(e,10))||!isFinite(e))&&e},setNumber:function(e,t){e.one(this.SELECTORS.NUMBER).setHTML(''+this.CONSTANTS.QUESTION+" "+t)},getSlots:function(){return u.all(this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL+" "+this.SELECTORS.SLOT)},getNumberedSlots:function(){var e=this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL;return e+=" "+this.SELECTORS.SLOT+":not("+this.SELECTORS.QUESTIONTYPEDESCRIPTION+")",u.all(e)},getPrevious:function(e){return e.previous(this.SELECTORS.SLOT)},getPreviousNumbered:function(e){var t,i,n=e.previous(this.SELECTORS.SLOT+":not("+this.SELECTORS.QUESTIONTYPEDESCRIPTION+")");if(n)return n;for(t=e.ancestor("li.section").previous("li.section");t;){if(0<(i=t.all(this.SELECTORS.SLOT+":not("+this.SELECTORS.QUESTIONTYPEDESCRIPTION+")")).size())return i.item(i.size()-1);t=t.previous("li.section")}return!1},reorderSlots:function(){this.getSlots().each(function(e){var t,i;u.Moodle.mod_quiz.util.page.getPageFromSlot(e)||(t=e.next(u.Moodle.mod_quiz.util.page.SELECTORS.PAGE),e.swap(t)),t=this.getPreviousNumbered(e),i=0,e.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION)||(t&&(i=this.getNumber(t)),this.setNumber(e,i+1))},this)},updateOneSlotSections:function(){u.all(".mod-quiz-edit-content ul.slots li.section").each(function(e){1' + this.CONSTANTS.QUESTION + '
                            ' + number); }, /** * Returns a list of all slot elements on the page. * * @method getSlots * @return {node[]} An array containing slot nodes. */ getSlots: function() { return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT); }, /** * Returns a list of all slot elements on the page that have numbers. Excudes description questions. * * @method getSlots * @return {node[]} An array containing slot nodes. */ getNumberedSlots: function() { var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL; selector += ' ' + this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'; return Y.all(selector); }, /** * Returns the previous slot to the given slot. * * @method getPrevious * @param slot Slot node * @return {node|false} The previous slot node or false. */ getPrevious: function(slot) { return slot.previous(this.SELECTORS.SLOT); }, /** * Returns the previous numbered slot to the given slot. * * Ignores slots containing description question types. * * @method getPrevious * @param slot Slot node * @return {node|false} The previous slot node or false. */ getPreviousNumbered: function(slot) { var previous = slot.previous(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'); if (previous) { return previous; } var section = slot.ancestor('li.section').previous('li.section'); while (section) { var questions = section.all(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'); if (questions.size() > 0) { return questions.item(questions.size() - 1); } section = section.previous('li.section'); } return false; }, /** * Reset the order of the numbers given to each slot. * * @method reorderSlots * @return void */ reorderSlots: function() { // Get list of slot nodes. var slots = this.getSlots(); // Loop through slots incrementing the number each time. slots.each(function(slot) { if (!Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot)) { // Move the next page to the front. var nextpage = slot.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE); slot.swap(nextpage); } var previousSlot = this.getPreviousNumbered(slot), previousslotnumber = 0; if (slot.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION)) { return; } if (previousSlot) { previousslotnumber = this.getNumber(previousSlot); } // Set slot number. this.setNumber(slot, previousslotnumber + 1); }, this); }, /** * Add class only-has-one-slot to those sections that need it. * * @method updateOneSlotSections * @return void */ updateOneSlotSections: function() { Y.all('.mod-quiz-edit-content ul.slots li.section').each(function(section) { if (section.all(this.SELECTORS.SLOT).size() > 1) { section.removeClass('only-has-one-slot'); } else { section.addClass('only-has-one-slot'); } }, this); }, /** * Remove a slot and related elements from the list of slots. * * @method remove * @param slot Slot node * @return void */ remove: function(slot) { var page = Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot); slot.remove(); // Is the page empty. if (!Y.Moodle.mod_quiz.util.page.isEmpty(page)) { return; } // If so remove it. Including add menu and page break. Y.Moodle.mod_quiz.util.page.remove(page); }, /** * Returns a list of all page break elements on the page. * * @method getPageBreaks * @return {node[]} An array containing page break nodes. */ getPageBreaks: function() { var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL; selector += ' ' + this.SELECTORS.SLOT + this.SELECTORS.PAGEBREAK; return Y.all(selector); }, /** * Retrieve the page break element item from the given slot. * * @method getPageBreak * @param slot Slot node * @return {Node|null} The Page Break Node. */ getPageBreak: function(slot) { return Y.one(slot).one(this.SELECTORS.PAGEBREAK); }, /** * Add a page break and related elements to the list of slots. * * @method addPageBreak * @param beforenode Int | Node | HTMLElement | String to add * @return pagebreak PageBreak node */ addPageBreak: function(slot) { var nodetext = M.mod_quiz.resource_toolbox.get('config').addpageiconhtml; nodetext = nodetext.replace('%%SLOT%%', this.getNumber(slot)); var pagebreak = Y.Node.create(nodetext); slot.one('div').insert(pagebreak, 'after'); return pagebreak; }, /** * Remove a pagebreak from the given slot. * * @method removePageBreak * @param slot Slot node * @return boolean */ removePageBreak: function(slot) { var pagebreak = this.getPageBreak(slot); if (!pagebreak) { return false; } pagebreak.remove(); return true; }, /** * Reorder each pagebreak by iterating through each related slot. * * @method reorderPageBreaks * @return void */ reorderPageBreaks: function() { // Get list of slot nodes. var slots = this.getSlots(); var slotnumber = 0; // Loop through slots incrementing the number each time. slots.each(function(slot, key) { slotnumber++; var pagebreak = this.getPageBreak(slot); var nextitem = slot.next('li.activity'); if (!nextitem) { // Last slot in a section. Should not have an icon. return; } // No pagebreak and not last slot. Add one. if (!pagebreak) { pagebreak = this.addPageBreak(slot); } // Remove last page break if there is one. if (pagebreak && key === slots.size() - 1) { this.removePageBreak(slot); } // Get page break anchor element. var pagebreaklink = pagebreak.get('childNodes').item(0); // Get the correct title. var action = ''; var iconname = ''; if (Y.Moodle.mod_quiz.util.page.isPage(nextitem)) { action = 'removepagebreak'; iconname = 'e/remove_page_break'; } else { action = 'addpagebreak'; iconname = 'e/insert_page_break'; } // Update the link and image titles pagebreaklink.set('title', M.util.get_string(action, 'quiz')); pagebreaklink.setData('action', action); // Update the image title. var icon = pagebreaklink.one(this.SELECTORS.ICON); icon.set('title', M.util.get_string(action, 'quiz')); icon.set('alt', M.util.get_string(action, 'quiz')); // Update the image src. icon.set('src', M.util.image_url(iconname)); // Get anchor url parameters as an associative array. var params = Y.QueryString.parse(pagebreaklink.get('href')); // Update slot number. params.slot = slotnumber; // Create the new url. var newurl = ''; for (var index in params) { if (newurl.length) { newurl += "&"; } newurl += index + "=" + params[index]; } // Update the anchor. pagebreaklink.set('href', newurl); }, this); }, /** * Update the dependency icons. * * @method updateAllDependencyIcons * @return void */ updateAllDependencyIcons: function() { // Get list of slot nodes. var slots = this.getSlots(), slotnumber = 0, previousslot = null; // Loop through slots incrementing the number each time. slots.each(function(slot) { slotnumber++; if (slotnumber == 1 || previousslot.getData('canfinish') === '0') { slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).addClass(this.CSS.CANNOT_DEPEND); } else { slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).removeClass(this.CSS.CANNOT_DEPEND); } this.updateDependencyIcon(slot, null); previousslot = slot; }, this); }, /** * Update the slot icon to indicate the new requiresprevious state. * * @method slot Slot node * @method requiresprevious Whether this node now requires the previous one. * @return void */ updateDependencyIcon: function(slot, requiresprevious) { var link = slot.one(this.SELECTORS.DEPENDENCY_LINK); var icon = slot.one(this.SELECTORS.DEPENDENCY_ICON); var previousSlot = this.getPrevious(slot); var a = {thisq: this.getNumber(slot)}; if (previousSlot) { a.previousq = this.getNumber(previousSlot); } if (requiresprevious === null) { requiresprevious = link.getData('action') === 'removedependency'; } if (requiresprevious) { link.set('title', M.util.get_string('questiondependencyremove', 'quiz', a)); link.setData('action', 'removedependency'); window.require(['core/templates'], function(Templates) { Templates.renderPix('t/locked', 'core', M.util.get_string('questiondependsonprevious', 'quiz')).then( function(html) { icon.replace(html); } ); }); } else { link.set('title', M.util.get_string('questiondependencyadd', 'quiz', a)); link.setData('action', 'adddependency'); window.require(['core/templates'], function(Templates) { Templates.renderPix('t/unlocked', 'core', M.util.get_string('questiondependencyfree', 'quiz')).then( function(html) { icon.replace(html); } ); }); } } }; }, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]}); build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js000064400000034436152267015160020314 0ustar00YUI.add('moodle-mod_quiz-util-slot', function (Y, NAME) { /** * A collection of utility classes for use with slots. * * @module moodle-mod_quiz-util * @submodule moodle-mod_quiz-util-slot */ Y.namespace('Moodle.mod_quiz.util.slot'); /** * A collection of utility classes for use with slots. * * @class Moodle.mod_quiz.util.slot * @static */ Y.Moodle.mod_quiz.util.slot = { CSS: { SLOT: 'slot', QUESTIONTYPEDESCRIPTION: 'qtype_description', CANNOT_DEPEND: 'question_dependency_cannot_depend' }, CONSTANTS: { SLOTIDPREFIX: 'slot-', QUESTION: M.util.get_string('question', 'moodle') }, SELECTORS: { SLOT: 'li.slot', INSTANCENAME: '.instancename', NUMBER: 'span.slotnumber', PAGECONTENT: 'div#page-content', PAGEBREAK: 'span.page_split_join_wrapper', ICON: '.icon', QUESTIONTYPEDESCRIPTION: '.qtype_description', SECTIONUL: 'ul.section', DEPENDENCY_WRAPPER: '.question_dependency_wrapper', DEPENDENCY_LINK: '.question_dependency_wrapper .cm-edit-action', DEPENDENCY_ICON: '.question_dependency_wrapper .icon' }, /** * Retrieve the slot item from one of it's child Nodes. * * @method getSlotFromComponent * @param slotcomponent {Node} The component Node. * @return {Node|null} The Slot Node. */ getSlotFromComponent: function(slotcomponent) { return Y.one(slotcomponent).ancestor(this.SELECTORS.SLOT, true); }, /** * Determines the slot ID for the provided slot. * * @method getId * @param slot {Node} The slot to find an ID for. * @return {Number|false} The ID of the slot in question or false if no ID was found. */ getId: function(slot) { // We perform a simple substitution operation to get the ID. var id = slot.get('id').replace( this.CONSTANTS.SLOTIDPREFIX, ''); // Attempt to validate the ID. id = parseInt(id, 10); if (typeof id === 'number' && isFinite(id)) { return id; } return false; }, /** * Determines the slot name for the provided slot. * * @method getName * @param slot {Node} The slot to find a name for. * @return {string|false} The name of the slot in question or false if no ID was found. */ getName: function(slot) { var instance = slot.one(this.SELECTORS.INSTANCENAME); if (instance) { return instance.get('firstChild').get('data'); } return null; }, /** * Determines the slot number for the provided slot. * * @method getNumber * @param slot {Node} The slot to find the number for. * @return {Number|false} The number of the slot in question or false if no number was found. */ getNumber: function(slot) { if (!slot) { return false; } // We perform a simple substitution operation to get the number. var number = slot.one(this.SELECTORS.NUMBER).get('text').replace( this.CONSTANTS.QUESTION, ''); // Attempt to validate the ID. number = parseInt(number, 10); if (typeof number === 'number' && isFinite(number)) { return number; } return false; }, /** * Updates the slot number for the provided slot. * * @method setNumber * @param slot {Node} The slot to update the number for. * @return void */ setNumber: function(slot, number) { var numbernode = slot.one(this.SELECTORS.NUMBER); numbernode.setHTML('' + this.CONSTANTS.QUESTION + ' ' + number); }, /** * Returns a list of all slot elements on the page. * * @method getSlots * @return {node[]} An array containing slot nodes. */ getSlots: function() { return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT); }, /** * Returns a list of all slot elements on the page that have numbers. Excudes description questions. * * @method getSlots * @return {node[]} An array containing slot nodes. */ getNumberedSlots: function() { var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL; selector += ' ' + this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'; return Y.all(selector); }, /** * Returns the previous slot to the given slot. * * @method getPrevious * @param slot Slot node * @return {node|false} The previous slot node or false. */ getPrevious: function(slot) { return slot.previous(this.SELECTORS.SLOT); }, /** * Returns the previous numbered slot to the given slot. * * Ignores slots containing description question types. * * @method getPrevious * @param slot Slot node * @return {node|false} The previous slot node or false. */ getPreviousNumbered: function(slot) { var previous = slot.previous(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'); if (previous) { return previous; } var section = slot.ancestor('li.section').previous('li.section'); while (section) { var questions = section.all(this.SELECTORS.SLOT + ':not(' + this.SELECTORS.QUESTIONTYPEDESCRIPTION + ')'); if (questions.size() > 0) { return questions.item(questions.size() - 1); } section = section.previous('li.section'); } return false; }, /** * Reset the order of the numbers given to each slot. * * @method reorderSlots * @return void */ reorderSlots: function() { // Get list of slot nodes. var slots = this.getSlots(); // Loop through slots incrementing the number each time. slots.each(function(slot) { if (!Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot)) { // Move the next page to the front. var nextpage = slot.next(Y.Moodle.mod_quiz.util.page.SELECTORS.PAGE); slot.swap(nextpage); } var previousSlot = this.getPreviousNumbered(slot), previousslotnumber = 0; if (slot.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION)) { return; } if (previousSlot) { previousslotnumber = this.getNumber(previousSlot); } // Set slot number. this.setNumber(slot, previousslotnumber + 1); }, this); }, /** * Add class only-has-one-slot to those sections that need it. * * @method updateOneSlotSections * @return void */ updateOneSlotSections: function() { Y.all('.mod-quiz-edit-content ul.slots li.section').each(function(section) { if (section.all(this.SELECTORS.SLOT).size() > 1) { section.removeClass('only-has-one-slot'); } else { section.addClass('only-has-one-slot'); } }, this); }, /** * Remove a slot and related elements from the list of slots. * * @method remove * @param slot Slot node * @return void */ remove: function(slot) { var page = Y.Moodle.mod_quiz.util.page.getPageFromSlot(slot); slot.remove(); // Is the page empty. if (!Y.Moodle.mod_quiz.util.page.isEmpty(page)) { return; } // If so remove it. Including add menu and page break. Y.Moodle.mod_quiz.util.page.remove(page); }, /** * Returns a list of all page break elements on the page. * * @method getPageBreaks * @return {node[]} An array containing page break nodes. */ getPageBreaks: function() { var selector = this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL; selector += ' ' + this.SELECTORS.SLOT + this.SELECTORS.PAGEBREAK; return Y.all(selector); }, /** * Retrieve the page break element item from the given slot. * * @method getPageBreak * @param slot Slot node * @return {Node|null} The Page Break Node. */ getPageBreak: function(slot) { return Y.one(slot).one(this.SELECTORS.PAGEBREAK); }, /** * Add a page break and related elements to the list of slots. * * @method addPageBreak * @param beforenode Int | Node | HTMLElement | String to add * @return pagebreak PageBreak node */ addPageBreak: function(slot) { var nodetext = M.mod_quiz.resource_toolbox.get('config').addpageiconhtml; nodetext = nodetext.replace('%%SLOT%%', this.getNumber(slot)); var pagebreak = Y.Node.create(nodetext); slot.one('div').insert(pagebreak, 'after'); return pagebreak; }, /** * Remove a pagebreak from the given slot. * * @method removePageBreak * @param slot Slot node * @return boolean */ removePageBreak: function(slot) { var pagebreak = this.getPageBreak(slot); if (!pagebreak) { return false; } pagebreak.remove(); return true; }, /** * Reorder each pagebreak by iterating through each related slot. * * @method reorderPageBreaks * @return void */ reorderPageBreaks: function() { // Get list of slot nodes. var slots = this.getSlots(); var slotnumber = 0; // Loop through slots incrementing the number each time. slots.each(function(slot, key) { slotnumber++; var pagebreak = this.getPageBreak(slot); var nextitem = slot.next('li.activity'); if (!nextitem) { // Last slot in a section. Should not have an icon. return; } // No pagebreak and not last slot. Add one. if (!pagebreak) { pagebreak = this.addPageBreak(slot); } // Remove last page break if there is one. if (pagebreak && key === slots.size() - 1) { this.removePageBreak(slot); } // Get page break anchor element. var pagebreaklink = pagebreak.get('childNodes').item(0); // Get the correct title. var action = ''; var iconname = ''; if (Y.Moodle.mod_quiz.util.page.isPage(nextitem)) { action = 'removepagebreak'; iconname = 'e/remove_page_break'; } else { action = 'addpagebreak'; iconname = 'e/insert_page_break'; } // Update the link and image titles pagebreaklink.set('title', M.util.get_string(action, 'quiz')); pagebreaklink.setData('action', action); // Update the image title. var icon = pagebreaklink.one(this.SELECTORS.ICON); icon.set('title', M.util.get_string(action, 'quiz')); icon.set('alt', M.util.get_string(action, 'quiz')); // Update the image src. icon.set('src', M.util.image_url(iconname)); // Get anchor url parameters as an associative array. var params = Y.QueryString.parse(pagebreaklink.get('href')); // Update slot number. params.slot = slotnumber; // Create the new url. var newurl = ''; for (var index in params) { if (newurl.length) { newurl += "&"; } newurl += index + "=" + params[index]; } // Update the anchor. pagebreaklink.set('href', newurl); }, this); }, /** * Update the dependency icons. * * @method updateAllDependencyIcons * @return void */ updateAllDependencyIcons: function() { // Get list of slot nodes. var slots = this.getSlots(), slotnumber = 0, previousslot = null; // Loop through slots incrementing the number each time. slots.each(function(slot) { slotnumber++; if (slotnumber == 1 || previousslot.getData('canfinish') === '0') { slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).addClass(this.CSS.CANNOT_DEPEND); } else { slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).removeClass(this.CSS.CANNOT_DEPEND); } this.updateDependencyIcon(slot, null); previousslot = slot; }, this); }, /** * Update the slot icon to indicate the new requiresprevious state. * * @method slot Slot node * @method requiresprevious Whether this node now requires the previous one. * @return void */ updateDependencyIcon: function(slot, requiresprevious) { var link = slot.one(this.SELECTORS.DEPENDENCY_LINK); var icon = slot.one(this.SELECTORS.DEPENDENCY_ICON); var previousSlot = this.getPrevious(slot); var a = {thisq: this.getNumber(slot)}; if (previousSlot) { a.previousq = this.getNumber(previousSlot); } if (requiresprevious === null) { requiresprevious = link.getData('action') === 'removedependency'; } if (requiresprevious) { link.set('title', M.util.get_string('questiondependencyremove', 'quiz', a)); link.setData('action', 'removedependency'); window.require(['core/templates'], function(Templates) { Templates.renderPix('t/locked', 'core', M.util.get_string('questiondependsonprevious', 'quiz')).then( function(html) { icon.replace(html); } ); }); } else { link.set('title', M.util.get_string('questiondependencyadd', 'quiz', a)); link.setData('action', 'adddependency'); window.require(['core/templates'], function(Templates) { Templates.renderPix('t/unlocked', 'core', M.util.get_string('questiondependencyfree', 'quiz')).then( function(html) { icon.replace(html); } ); }); } } }; }, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]}); build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-min.js000064400000012076152267015160020657 0ustar00YUI.add("moodle-mod_quiz-autosave",function(n,t){M.mod_quiz=M.mod_quiz||{},M.mod_quiz.autosave={TINYMCE_DETECTION_DELAY:500,TINYMCE_DETECTION_REPEATS:20,WATCH_HIDDEN_DELAY:1e3,FAILURES_BEFORE_NOTIFY:1,FIRST_SUCCESSFUL_SAVE:-1,SELECTORS:{QUIZ_FORM:"#responseform",VALUE_CHANGE_ELEMENTS:'input, textarea, [contenteditable="true"]',CHANGE_ELEMENTS:"input, select",HIDDEN_INPUTS:"input[type=hidden]",CONNECTION_ERROR:"#connection-error",CONNECTION_OK:"#connection-ok"},AUTOSAVE_HANDLER:M.cfg.wwwroot+"/mod/quiz/autosave.ajax.php",delay:12e4,form:null,dirty:!1,delay_timer:null,save_transaction:null,savefailures:0,editor_change_handler:null,hidden_field_values:{},init:function(t){this.form=n.one(this.SELECTORS.QUIZ_FORM),this.form&&(this.delay=1e3*t,this.form.delegate("valuechange",this.value_changed,this.SELECTORS.VALUE_CHANGE_ELEMENTS,this),this.form.delegate("change",this.value_changed,this.SELECTORS.CHANGE_ELEMENTS,this),this.form.on("submit",this.stop_autosaving,this),require(["core_form/events"],function(t){window.addEventListener(t.eventTypes.uploadChanged,this.value_changed.bind(this))}.bind(this)),this.init_tinymce(this.TINYMCE_DETECTION_REPEATS),this.save_hidden_field_values(),this.watch_hidden_fields())},save_hidden_field_values:function(){this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(t){var e=t.get("name");e&&(this.hidden_field_values[e]=t.get("value"))},this)},watch_hidden_fields:function(){this.detect_hidden_field_changes(),n.later(this.WATCH_HIDDEN_DELAY,this,this.watch_hidden_fields)},detect_hidden_field_changes:function(){this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(t){var e=t.get("name"),i=t.get("value");e&&(e in this.hidden_field_values&&i===this.hidden_field_values[e]||(this.hidden_field_values[e]=i,this.value_changed({target:t})))},this)},init_tinymce:function(t){var e;"undefined"!=typeof window.tinyMCE?(this.editor_change_handler=n.bind(this.editor_changed,this),window.tinyMCE.onAddEditor?window.tinyMCE.onAddEditor.add(n.bind(this.init_tinymce_editor,this)):window.tinyMCE.on&&(e=this.start_save_timer_if_necessary.bind(this),window.tinyMCE.on("AddEditor",function(t){t.editor.on("Change Undo Redo keydown",e)}),window.tinyMCE.get().forEach(function(t){t.on("Change Undo Redo keydown",e)}))):0M.mod_quiz.timer.endtime},stop_autosaving:function(){this.cancel_delay(),this.delay_timer=!0,this.save_transaction&&this.save_transaction.abort()}}},"@VERSION@",{requires:["base","node","event","event-valuechange","node-event-delegate","io-form","datatype-date-format"]});build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave.js000064400000032347152267015160020101 0ustar00YUI.add('moodle-mod_quiz-autosave', 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 . /** * Auto-save functionality for during quiz attempts. * * @module moodle-mod_quiz-autosave */ /** * Auto-save functionality for during quiz attempts. * * @class M.mod_quiz.autosave */ M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.autosave = { /** * The amount of time (in milliseconds) to wait between TinyMCE detections. * * @property TINYMCE_DETECTION_DELAY * @type Number * @default 500 * @private */ TINYMCE_DETECTION_DELAY: 500, /** * The number of times to try redetecting TinyMCE. * * @property TINYMCE_DETECTION_REPEATS * @type Number * @default 20 * @private */ TINYMCE_DETECTION_REPEATS: 20, /** * The delay (in milliseconds) between checking hidden input fields. * * @property WATCH_HIDDEN_DELAY * @type Number * @default 1000 * @private */ WATCH_HIDDEN_DELAY: 1000, /** * The number of failures to ignore before notifying the user. * * @property FAILURES_BEFORE_NOTIFY * @type Number * @default 1 * @private */ FAILURES_BEFORE_NOTIFY: 1, /** * The value to use when resetting the successful save counter. * * @property FIRST_SUCCESSFUL_SAVE * @static * @type Number * @default -1 * @private */ FIRST_SUCCESSFUL_SAVE: -1, /** * The selectors used throughout this class. * * @property SELECTORS * @private * @type Object * @static */ SELECTORS: { QUIZ_FORM: '#responseform', VALUE_CHANGE_ELEMENTS: 'input, textarea, [contenteditable="true"]', CHANGE_ELEMENTS: 'input, select', HIDDEN_INPUTS: 'input[type=hidden]', CONNECTION_ERROR: '#connection-error', CONNECTION_OK: '#connection-ok' }, /** * The script which handles the autosaves. * * @property AUTOSAVE_HANDLER * @type String * @default M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php' * @private */ AUTOSAVE_HANDLER: M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php', /** * The delay (in milliseconds) between a change being made, and it being auto-saved. * * @property delay * @type Number * @default 120000 * @private */ delay: 120000, /** * A Node reference to the form we are monitoring. * * @property form * @type Node * @default null */ form: null, /** * Whether the form has been modified since the last save started. * * @property dirty * @type boolean * @default false */ dirty: false, /** * Timer object for the delay between form modifaction and the save starting. * * @property delay_timer * @type Object * @default null * @private */ delay_timer: null, /** * Y.io transaction for the save ajax request. * * @property save_transaction * @type object * @default null */ save_transaction: null, /** * Failed saves count. * * @property savefailures * @type Number * @default 0 * @private */ savefailures: 0, /** * Properly bound key change handler. * * @property editor_change_handler * @type EventHandle * @default null * @private */ editor_change_handler: null, /** * Record of the value of all the hidden fields, last time they were checked. * * @property hidden_field_values * @type Object * @default {} */ hidden_field_values: {}, /** * Initialise the autosave code. * * @method init * @param {Number} delay the delay, in seconds, between a change being detected, and * a save happening. */ init: function(delay) { this.form = Y.one(this.SELECTORS.QUIZ_FORM); if (!this.form) { return; } this.delay = delay * 1000; this.form.delegate('valuechange', this.value_changed, this.SELECTORS.VALUE_CHANGE_ELEMENTS, this); this.form.delegate('change', this.value_changed, this.SELECTORS.CHANGE_ELEMENTS, this); this.form.on('submit', this.stop_autosaving, this); require(['core_form/events'], function(FormEvent) { window.addEventListener(FormEvent.eventTypes.uploadChanged, this.value_changed.bind(this)); }.bind(this)); this.init_tinymce(this.TINYMCE_DETECTION_REPEATS); this.save_hidden_field_values(); this.watch_hidden_fields(); }, save_hidden_field_values: function() { this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) { var name = hidden.get('name'); if (!name) { return; } this.hidden_field_values[name] = hidden.get('value'); }, this); }, watch_hidden_fields: function() { this.detect_hidden_field_changes(); Y.later(this.WATCH_HIDDEN_DELAY, this, this.watch_hidden_fields); }, detect_hidden_field_changes: function() { this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) { var name = hidden.get('name'), value = hidden.get('value'); if (!name) { return; } if (!(name in this.hidden_field_values) || value !== this.hidden_field_values[name]) { this.hidden_field_values[name] = value; this.value_changed({target: hidden}); } }, this); }, /** * Initialise watching of TinyMCE specifically. * * Because TinyMCE might load slowly, and after us, we need to keep * trying, until we detect TinyMCE is there, or enough time has passed. * This is based on the TINYMCE_DETECTION_DELAY and * TINYMCE_DETECTION_REPEATS properties. * * * @method init_tinymce * @param {Number} repeatcount The number of attempts made so far. */ init_tinymce: function(repeatcount) { if (typeof window.tinyMCE === 'undefined') { if (repeatcount > 0) { Y.later(this.TINYMCE_DETECTION_DELAY, this, this.init_tinymce, [repeatcount - 1]); } else { } return; } this.editor_change_handler = Y.bind(this.editor_changed, this); if (window.tinyMCE.onAddEditor) { window.tinyMCE.onAddEditor.add(Y.bind(this.init_tinymce_editor, this)); } else if (window.tinyMCE.on) { var startSaveTimer = this.start_save_timer_if_necessary.bind(this); window.tinyMCE.on('AddEditor', function(event) { event.editor.on('Change Undo Redo keydown', startSaveTimer); }); // One or more editors might already have been added, so we have to attach // the event handlers to these as well. window.tinyMCE.get().forEach(function(editor) { editor.on('Change Undo Redo keydown', startSaveTimer); }); } }, /** * Initialise watching of a specific TinyMCE editor. * * @method init_tinymce_editor * @param {EventFacade} e * @param {Object} editor The TinyMCE editor object */ init_tinymce_editor: function(e, editor) { editor.onChange.add(this.editor_change_handler); editor.onRedo.add(this.editor_change_handler); editor.onUndo.add(this.editor_change_handler); editor.onKeyDown.add(this.editor_change_handler); }, value_changed: function(e) { var name = e.target.getAttribute('name'); if (name === 'thispage' || name === 'scrollpos' || (name && name.match(/_:flagged$/))) { return; // Not interesting. } // Fallback to the ID when the name is not present (in the case of content editable). name = name || '#' + e.target.getAttribute('id'); this.start_save_timer_if_necessary(); }, editor_changed: function(editor) { this.start_save_timer_if_necessary(); }, start_save_timer_if_necessary: function() { this.dirty = true; if (this.delay_timer || this.save_transaction) { // Already counting down or daving. return; } this.start_save_timer(); }, start_save_timer: function() { this.cancel_delay(); this.delay_timer = Y.later(this.delay, this, this.save_changes); }, cancel_delay: function() { if (this.delay_timer && this.delay_timer !== true) { this.delay_timer.cancel(); } this.delay_timer = null; }, save_changes: function() { this.cancel_delay(); this.dirty = false; if (this.is_time_nearly_over()) { this.stop_autosaving(); return; } if (typeof window.tinyMCE !== 'undefined') { window.tinyMCE.triggerSave(); } // YUI io.form incorrectly (in my opinion) sends the value of all submit // buttons in the ajax request. We don't want any submit buttons. // Therefore, temporarily change the type. // (Yes, this is a nasty hack. One day this will be re-written as AMD, hopefully). var allsubmitbuttons = this.form.all('input[type=submit], button[type=submit]'); allsubmitbuttons.setAttribute('type', 'button'); this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, { method: 'POST', form: {id: this.form}, on: { success: this.save_done, failure: this.save_failed }, context: this }); // Change the button types back. allsubmitbuttons.setAttribute('type', 'submit'); }, save_done: function(transactionid, response) { var autosavedata = JSON.parse(response.responseText); if (autosavedata.status !== 'OK') { // Because IIS is useless, Moodle can't send proper HTTP response // codes, so we have to detect failures manually. this.save_failed(transactionid, response); return; } if (typeof autosavedata.timeleft !== 'undefined') { M.mod_quiz.timer.updateEndTime(autosavedata.timeleft); } this.update_saved_time_display(); this.save_transaction = null; if (this.dirty) { this.start_save_timer(); } if (this.savefailures > 0) { Y.one(this.SELECTORS.CONNECTION_ERROR).hide(); Y.one(this.SELECTORS.CONNECTION_OK).show(); this.savefailures = this.FIRST_SUCCESSFUL_SAVE; } else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) { Y.one(this.SELECTORS.CONNECTION_OK).hide(); this.savefailures = 0; } }, save_failed: function() { this.save_transaction = null; // We want to retry soon. this.start_save_timer(); this.savefailures = Math.max(1, this.savefailures + 1); if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) { Y.one(this.SELECTORS.CONNECTION_ERROR).show(); Y.one(this.SELECTORS.CONNECTION_OK).hide(); } }, /** * Inform the user that their answers have been saved. * * @method update_saved_time_display */ update_saved_time_display: function() { // We fetch the current language's preferred time format from the language pack. require(['core/user_date', 'core/notification'], function(UserDate, Notification) { UserDate.get([{ timestamp: Math.floor(Date.now() / 1000), format: M.util.get_string('strftimedatetimeshortaccurate', 'langconfig'), }]).then(function(dateStrs) { var infoDiv = Y.one('#mod_quiz_navblock .othernav .autosave_info'); infoDiv.set('text', M.util.get_string('lastautosave', 'quiz', dateStrs[0])); infoDiv.show(); return; }).catch(Notification.exception); }); }, is_time_nearly_over: function() { return M.mod_quiz.timer && M.mod_quiz.timer.endtime && (new Date().getTime() + 2 * this.delay) > M.mod_quiz.timer.endtime; }, stop_autosaving: function() { this.cancel_delay(); this.delay_timer = true; if (this.save_transaction) { this.save_transaction.abort(); } } }; }, '@VERSION@', { "requires": [ "base", "node", "event", "event-valuechange", "node-event-delegate", "io-form", "datatype-date-format" ] }); build/moodle-mod_quiz-autosave/moodle-mod_quiz-autosave-debug.js000064400000034446152267015160021167 0ustar00YUI.add('moodle-mod_quiz-autosave', 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 . /** * Auto-save functionality for during quiz attempts. * * @module moodle-mod_quiz-autosave */ /** * Auto-save functionality for during quiz attempts. * * @class M.mod_quiz.autosave */ M.mod_quiz = M.mod_quiz || {}; M.mod_quiz.autosave = { /** * The amount of time (in milliseconds) to wait between TinyMCE detections. * * @property TINYMCE_DETECTION_DELAY * @type Number * @default 500 * @private */ TINYMCE_DETECTION_DELAY: 500, /** * The number of times to try redetecting TinyMCE. * * @property TINYMCE_DETECTION_REPEATS * @type Number * @default 20 * @private */ TINYMCE_DETECTION_REPEATS: 20, /** * The delay (in milliseconds) between checking hidden input fields. * * @property WATCH_HIDDEN_DELAY * @type Number * @default 1000 * @private */ WATCH_HIDDEN_DELAY: 1000, /** * The number of failures to ignore before notifying the user. * * @property FAILURES_BEFORE_NOTIFY * @type Number * @default 1 * @private */ FAILURES_BEFORE_NOTIFY: 1, /** * The value to use when resetting the successful save counter. * * @property FIRST_SUCCESSFUL_SAVE * @static * @type Number * @default -1 * @private */ FIRST_SUCCESSFUL_SAVE: -1, /** * The selectors used throughout this class. * * @property SELECTORS * @private * @type Object * @static */ SELECTORS: { QUIZ_FORM: '#responseform', VALUE_CHANGE_ELEMENTS: 'input, textarea, [contenteditable="true"]', CHANGE_ELEMENTS: 'input, select', HIDDEN_INPUTS: 'input[type=hidden]', CONNECTION_ERROR: '#connection-error', CONNECTION_OK: '#connection-ok' }, /** * The script which handles the autosaves. * * @property AUTOSAVE_HANDLER * @type String * @default M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php' * @private */ AUTOSAVE_HANDLER: M.cfg.wwwroot + '/mod/quiz/autosave.ajax.php', /** * The delay (in milliseconds) between a change being made, and it being auto-saved. * * @property delay * @type Number * @default 120000 * @private */ delay: 120000, /** * A Node reference to the form we are monitoring. * * @property form * @type Node * @default null */ form: null, /** * Whether the form has been modified since the last save started. * * @property dirty * @type boolean * @default false */ dirty: false, /** * Timer object for the delay between form modifaction and the save starting. * * @property delay_timer * @type Object * @default null * @private */ delay_timer: null, /** * Y.io transaction for the save ajax request. * * @property save_transaction * @type object * @default null */ save_transaction: null, /** * Failed saves count. * * @property savefailures * @type Number * @default 0 * @private */ savefailures: 0, /** * Properly bound key change handler. * * @property editor_change_handler * @type EventHandle * @default null * @private */ editor_change_handler: null, /** * Record of the value of all the hidden fields, last time they were checked. * * @property hidden_field_values * @type Object * @default {} */ hidden_field_values: {}, /** * Initialise the autosave code. * * @method init * @param {Number} delay the delay, in seconds, between a change being detected, and * a save happening. */ init: function(delay) { this.form = Y.one(this.SELECTORS.QUIZ_FORM); if (!this.form) { Y.log('No response form found. Why did you try to set up autosave?', 'debug', 'moodle-mod_quiz-autosave'); return; } this.delay = delay * 1000; this.form.delegate('valuechange', this.value_changed, this.SELECTORS.VALUE_CHANGE_ELEMENTS, this); this.form.delegate('change', this.value_changed, this.SELECTORS.CHANGE_ELEMENTS, this); this.form.on('submit', this.stop_autosaving, this); require(['core_form/events'], function(FormEvent) { window.addEventListener(FormEvent.eventTypes.uploadChanged, this.value_changed.bind(this)); }.bind(this)); this.init_tinymce(this.TINYMCE_DETECTION_REPEATS); this.save_hidden_field_values(); this.watch_hidden_fields(); }, save_hidden_field_values: function() { this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) { var name = hidden.get('name'); if (!name) { return; } this.hidden_field_values[name] = hidden.get('value'); }, this); }, watch_hidden_fields: function() { this.detect_hidden_field_changes(); Y.later(this.WATCH_HIDDEN_DELAY, this, this.watch_hidden_fields); }, detect_hidden_field_changes: function() { this.form.all(this.SELECTORS.HIDDEN_INPUTS).each(function(hidden) { var name = hidden.get('name'), value = hidden.get('value'); if (!name) { return; } if (!(name in this.hidden_field_values) || value !== this.hidden_field_values[name]) { this.hidden_field_values[name] = value; this.value_changed({target: hidden}); } }, this); }, /** * Initialise watching of TinyMCE specifically. * * Because TinyMCE might load slowly, and after us, we need to keep * trying, until we detect TinyMCE is there, or enough time has passed. * This is based on the TINYMCE_DETECTION_DELAY and * TINYMCE_DETECTION_REPEATS properties. * * * @method init_tinymce * @param {Number} repeatcount The number of attempts made so far. */ init_tinymce: function(repeatcount) { if (typeof window.tinyMCE === 'undefined') { if (repeatcount > 0) { Y.later(this.TINYMCE_DETECTION_DELAY, this, this.init_tinymce, [repeatcount - 1]); } else { Y.log('Gave up looking for TinyMCE.', 'debug', 'moodle-mod_quiz-autosave'); } return; } Y.log('Found TinyMCE.', 'debug', 'moodle-mod_quiz-autosave'); this.editor_change_handler = Y.bind(this.editor_changed, this); if (window.tinyMCE.onAddEditor) { window.tinyMCE.onAddEditor.add(Y.bind(this.init_tinymce_editor, this)); } else if (window.tinyMCE.on) { var startSaveTimer = this.start_save_timer_if_necessary.bind(this); window.tinyMCE.on('AddEditor', function(event) { event.editor.on('Change Undo Redo keydown', startSaveTimer); }); // One or more editors might already have been added, so we have to attach // the event handlers to these as well. window.tinyMCE.get().forEach(function(editor) { editor.on('Change Undo Redo keydown', startSaveTimer); }); } }, /** * Initialise watching of a specific TinyMCE editor. * * @method init_tinymce_editor * @param {EventFacade} e * @param {Object} editor The TinyMCE editor object */ init_tinymce_editor: function(e, editor) { Y.log('Found TinyMCE editor ' + editor.id + '.', 'debug', 'moodle-mod_quiz-autosave'); editor.onChange.add(this.editor_change_handler); editor.onRedo.add(this.editor_change_handler); editor.onUndo.add(this.editor_change_handler); editor.onKeyDown.add(this.editor_change_handler); }, value_changed: function(e) { var name = e.target.getAttribute('name'); if (name === 'thispage' || name === 'scrollpos' || (name && name.match(/_:flagged$/))) { return; // Not interesting. } // Fallback to the ID when the name is not present (in the case of content editable). name = name || '#' + e.target.getAttribute('id'); Y.log('Detected a value change in element ' + name + '.', 'debug', 'moodle-mod_quiz-autosave'); this.start_save_timer_if_necessary(); }, editor_changed: function(editor) { Y.log('Detected a value change in editor ' + editor.id + '.', 'debug', 'moodle-mod_quiz-autosave'); this.start_save_timer_if_necessary(); }, start_save_timer_if_necessary: function() { this.dirty = true; if (this.delay_timer || this.save_transaction) { // Already counting down or daving. return; } this.start_save_timer(); }, start_save_timer: function() { this.cancel_delay(); this.delay_timer = Y.later(this.delay, this, this.save_changes); }, cancel_delay: function() { if (this.delay_timer && this.delay_timer !== true) { this.delay_timer.cancel(); } this.delay_timer = null; }, save_changes: function() { this.cancel_delay(); this.dirty = false; if (this.is_time_nearly_over()) { Y.log('No more saving, time is nearly over.', 'debug', 'moodle-mod_quiz-autosave'); this.stop_autosaving(); return; } Y.log('Doing a save.', 'debug', 'moodle-mod_quiz-autosave'); if (typeof window.tinyMCE !== 'undefined') { window.tinyMCE.triggerSave(); } // YUI io.form incorrectly (in my opinion) sends the value of all submit // buttons in the ajax request. We don't want any submit buttons. // Therefore, temporarily change the type. // (Yes, this is a nasty hack. One day this will be re-written as AMD, hopefully). var allsubmitbuttons = this.form.all('input[type=submit], button[type=submit]'); allsubmitbuttons.setAttribute('type', 'button'); this.save_transaction = Y.io(this.AUTOSAVE_HANDLER, { method: 'POST', form: {id: this.form}, on: { success: this.save_done, failure: this.save_failed }, context: this }); // Change the button types back. allsubmitbuttons.setAttribute('type', 'submit'); }, save_done: function(transactionid, response) { var autosavedata = JSON.parse(response.responseText); if (autosavedata.status !== 'OK') { // Because IIS is useless, Moodle can't send proper HTTP response // codes, so we have to detect failures manually. this.save_failed(transactionid, response); return; } if (typeof autosavedata.timeleft !== 'undefined') { Y.log('Updating timer: ' + autosavedata.timeleft + ' seconds remain.', 'debug', 'moodle-mod_quiz-timer'); M.mod_quiz.timer.updateEndTime(autosavedata.timeleft); } this.update_saved_time_display(); Y.log('Save completed.', 'debug', 'moodle-mod_quiz-autosave'); this.save_transaction = null; if (this.dirty) { Y.log('Dirty after save.', 'debug', 'moodle-mod_quiz-autosave'); this.start_save_timer(); } if (this.savefailures > 0) { Y.one(this.SELECTORS.CONNECTION_ERROR).hide(); Y.one(this.SELECTORS.CONNECTION_OK).show(); this.savefailures = this.FIRST_SUCCESSFUL_SAVE; } else if (this.savefailures === this.FIRST_SUCCESSFUL_SAVE) { Y.one(this.SELECTORS.CONNECTION_OK).hide(); this.savefailures = 0; } }, save_failed: function() { Y.log('Save failed.', 'debug', 'moodle-mod_quiz-autosave'); this.save_transaction = null; // We want to retry soon. this.start_save_timer(); this.savefailures = Math.max(1, this.savefailures + 1); if (this.savefailures === this.FAILURES_BEFORE_NOTIFY) { Y.one(this.SELECTORS.CONNECTION_ERROR).show(); Y.one(this.SELECTORS.CONNECTION_OK).hide(); } }, /** * Inform the user that their answers have been saved. * * @method update_saved_time_display */ update_saved_time_display: function() { // We fetch the current language's preferred time format from the language pack. require(['core/user_date', 'core/notification'], function(UserDate, Notification) { UserDate.get([{ timestamp: Math.floor(Date.now() / 1000), format: M.util.get_string('strftimedatetimeshortaccurate', 'langconfig'), }]).then(function(dateStrs) { var infoDiv = Y.one('#mod_quiz_navblock .othernav .autosave_info'); infoDiv.set('text', M.util.get_string('lastautosave', 'quiz', dateStrs[0])); infoDiv.show(); return; }).catch(Notification.exception); }); }, is_time_nearly_over: function() { return M.mod_quiz.timer && M.mod_quiz.timer.endtime && (new Date().getTime() + 2 * this.delay) > M.mod_quiz.timer.endtime; }, stop_autosaving: function() { this.cancel_delay(); this.delay_timer = true; if (this.save_transaction) { this.save_transaction.abort(); } } }; }, '@VERSION@', { "requires": [ "base", "node", "event", "event-valuechange", "node-event-delegate", "io-form", "datatype-date-format" ] }); listing/listing.js000064400000007503152267360450010243 0ustar00// 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 . /** * Form listing Javascript. * * It mainly handles loading the main content div when cliking on a tab/row. * @copyright 2012 Jerome Mouneyrac * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ M.form_listing = {}; M.form_listing.Y = null; M.form_listing.instances = []; /** * This function is called for each listing form on page. * * @param {Array} params : {int} hiddeninputid - the id of the hidden input element * {int} elementid - the id of the full form element * {Array} items - items has for key the value return by the form, and for content an array with two attributs: mainhtml and rowhtml. * {string} hideall - button label to hide all tabs(rows). * {string} showall - button label to show all tabs(rows). * {string} inputname - the name of the input element * {string} currentvalue - the currently selected tab(row) */ M.form_listing.init = function(Y, params) { if (params && params.hiddeninputid && params.elementid) { // Replace the radio buttons by a hidden input. Y.one('#formlistinginputcontainer_' + params.inputname).setHTML(''); var caption = Y.one('#'+params.elementid+'_caption'); var allitems = Y.one('#'+params.elementid+'_all'); var selecteditem = Y.one('#'+params.elementid+'_main'); var hiddeninput = Y.one('#'+params.hiddeninputid); // Do not display the listing by default. var show = 0; allitems.hide(); // Refresh the main item + set the hidden input to its value. var selectitem = function(e) { var index = this.get('id').replace(params.elementid+'_all_',""); hiddeninput.set('value', items[index]); selecteditem.setHTML(params.items[items[index]].mainhtml); } // Caption Onlick event to display/hide the listing. var onclick = function(e) { if (!show) { allitems.show(true); show = 1; caption.setHTML(params.hideall); } else { allitems.hide(true); show = 0; caption.setHTML(params.showall); } }; caption.on('click', onclick); // Fill the item rows with html + add event. // PS: we need to save the items into a temporary "items[]" array because params.items keys could be url. // This temporary items[] avoid not working calls like Y.one('#myitems_http:www.google.com'). var items = []; var itemindex = 0; for (itemid in params.items) { items[itemindex] = itemid; // Add the row. allitems.append("
                            " + params.items[itemid].rowhtml + "
                            "); // Add click event to the row. Y.one('#'+params.elementid+'_all_'+itemindex).on('click', selectitem); itemindex = itemindex + 1; } } }; src/shortforms/build.json000064400000000265152267360450011550 0ustar00{ "name": "moodle-form-shortforms", "builds": { "moodle-form-shortforms": { "jsfiles": [ "shortforms.js" ] } } } src/shortforms/meta/shortforms.json000064400000000257152267360450013606 0ustar00{ "moodle-form-shortforms": { "requires": [ "node", "base", "selector-css3", "moodle-core-event" ] } } src/shortforms/js/shortforms.js000064400000007504152267360450012741 0ustar00/** * Provides the form shortforms class. * * @module moodle-form-shortforms */ /** * A class for a shortforms. * * @class M.form.shortforms * @constructor * @extends Base */ function SHORTFORMS() { SHORTFORMS.superclass.constructor.apply(this, arguments); } var SELECTORS = { COLLAPSED: '.collapsed', FIELDSETCOLLAPSIBLE: 'fieldset.collapsible', FIELDSETLEGENDLINK: 'fieldset.collapsible .fheader', FHEADER: '.fheader', LEGENDFTOGGLER: 'legend.ftoggler' }, CSS = { COLLAPSEALL: 'collapse-all', COLLAPSED: 'collapsed', FHEADER: 'fheader' }, ATTRS = {}; /** * The form ID attribute definition. * * @attribute formid * @type String * @default '' * @writeOnce */ ATTRS.formid = { value: null }; Y.extend(SHORTFORMS, Y.Base, { /** * A reference to the form. * * @property form * @protected * @type Node * @default null */ form: null, /** * The initializer for the shortforms instance. * * @method initializer * @protected */ initializer: function() { var form = Y.one('#' + this.get('formid')); if (!form) { Y.log('Could not locate the form', 'warn', 'moodle-form-shortforms'); return; } // Stores the form in the object. this.form = form; // Subscribe collapsible fieldsets and buttons to click events. form.delegate('click', this.switch_state, SELECTORS.FIELDSETLEGENDLINK, this); // Handle event, when there's an error in collapsed section. Y.Global.on(M.core.globalEvents.FORM_ERROR, this.expand_fieldset, this); }, /** * Set the collapsed state for the specified fieldset. * * @method set_state * @param {Node} fieldset The Node relating to the fieldset to set state on. * @param {Boolean} [collapsed] Whether the fieldset is collapsed. * @chainable */ set_state: function(fieldset, collapsed) { var headerlink = fieldset.one(SELECTORS.FHEADER); if (collapsed) { fieldset.addClass(CSS.COLLAPSED); if (headerlink) { headerlink.setAttribute('aria-expanded', 'false'); } } else { fieldset.removeClass(CSS.COLLAPSED); if (headerlink) { headerlink.setAttribute('aria-expanded', 'true'); } } var statuselement = this.form.one('input[name=mform_isexpanded_' + fieldset.get('id') + ']'); if (!statuselement) { Y.log("M.form.shortforms::switch_state was called on an fieldset without a status field: '" + fieldset.get('id') + "'", 'debug', 'moodle-form-shortforms'); return this; } statuselement.set('value', collapsed ? 0 : 1); return this; }, /** * Toggle the state for the fieldset that was clicked. * * @method switch_state * @param {EventFacade} e */ switch_state: function(e) { e.preventDefault(); var fieldset = e.target.ancestor(SELECTORS.FIELDSETCOLLAPSIBLE); this.set_state(fieldset, !fieldset.hasClass(CSS.COLLAPSED)); }, /** * Expand the fieldset, which contains an error. * * @method expand_fieldset * @param {EventFacade} e */ expand_fieldset: function(e) { e.stopPropagation(); var formid = e.formid; if (formid === this.form.getAttribute('id')) { var errorfieldset = Y.one('#' + e.elementid).ancestor('fieldset'); if (errorfieldset) { this.set_state(errorfieldset, false); } } } }, { NAME: 'moodle-form-shortforms', ATTRS: ATTRS }); M.form = M.form || {}; M.form.shortforms = M.form.shortforms || function(params) { return new SHORTFORMS(params); }; src/dateselector/build.json000064400000000434152267360450012016 0ustar00{ "name": "moodle-form-dateselector", "builds": { "moodle-form-dateselector": { "jsfiles": [ "shared.js", "dateselector.js", "moodlecalendar.js", "calendar.js" ] } } } src/dateselector/meta/dateselector.json000064400000000242152267360450014320 0ustar00{ "moodle-form-dateselector": { "requires": [ "base", "node", "overlay", "calendar" ] } } src/dateselector/js/calendar.js000064400000021361152267360450012551 0ustar00/** * Provides the Calendar class. * * @module moodle-form-dateselector */ /** * Calendar class */ CALENDAR = function() { CALENDAR.superclass.constructor.apply(this, arguments); }; CALENDAR.prototype = { panel: null, yearselect: null, monthselect: null, dayselect: null, calendarimage: null, enablecheckbox: null, closepopup: true, initializer: function() { var controls = this.get('node').all('select'); controls.each(function(node) { if (node.get('name').match(/\[year\]/)) { this.yearselect = node; } else if (node.get('name').match(/\[month\]/)) { this.monthselect = node; } else if (node.get('name').match(/\[day\]/)) { this.dayselect = node; } node.after('change', this.handle_select_change, this); }, this); // Loop through the input fields. var inputs = this.get('node').all('input, button'); inputs.each(function(node) { // Check if the current node is a calendar image field. if (node.get('name').match(/\[calendar\]/)) { // Set it so that when the image is clicked the pop-up displays. node.on('click', this.focus_event, this); // Set the node to the calendarimage variable. this.calendarimage = node; } else { // Must be the enabled checkbox field. // If the enable checkbox is clicked we want to either disable/enable the calendar image. node.on('click', this.toggle_calendar_image, this); // Set the node to the enablecheckbox variable. this.enablecheckbox = node; } // Ensure that the calendarimage and enablecheckbox values have been set. if (this.calendarimage && this.enablecheckbox) { // Set the calendar icon status depending on the value of the checkbox. this.toggle_calendar_image(); } }, this); // Get the calendarimage element by its ID and check if any of its parents have the modal-dialog class to // know if the link is inside a modal, if so, set the aria-hidden and tabindex properties to the indicated values. var calendarimageelement = document.getElementById(this.calendarimage.get('id')); if (calendarimageelement.closest('.modal-dialog')) { this.calendarimage.set('aria-hidden', true); this.calendarimage.set('tabIndex', '-1'); } }, focus_event: function(e) { M.form.dateselector.cancel_any_timeout(); // If the current owner is set, then the pop-up is currently being displayed, so hide it. if (M.form.dateselector.currentowner === this) { this.release_calendar(); } else if ((this.enablecheckbox === null) || (this.enablecheckbox.get('checked'))) { // Must be hidden. If the field is enabled display the pop-up. this.claim_calendar(); } // Stop the input image field from submitting the form. e.preventDefault(); }, handle_select_change: function() { // It may seem as if the following variable is not used, however any call to set_date_from_selects will trigger a // call to set_selects_from_date if the calendar is open as the date has changed. Whenever the calendar is displayed // the set_selects_from_date function is set to trigger on any date change (see function connect_handlers). this.closepopup = false; this.set_date_from_selects(); this.closepopup = true; }, claim_calendar: function() { M.form.dateselector.cancel_any_timeout(); if (M.form.dateselector.currentowner === this) { return; } if (M.form.dateselector.currentowner) { M.form.dateselector.currentowner.release_calendar(); } if (M.form.dateselector.currentowner !== this) { this.connect_handlers(); this.set_date_from_selects(); } M.form.dateselector.currentowner = this; M.form.dateselector.calendar.set('minimumDate', new Date(this.yearselect.firstOptionValue(), 0, 1)); M.form.dateselector.calendar.set('maximumDate', new Date(this.yearselect.lastOptionValue(), 11, 31)); M.form.dateselector.panel.show(); M.form.dateselector.calendar.show(); M.form.dateselector.fix_position(); setTimeout(function() { M.form.dateselector.cancel_any_timeout(); }, 100); // Focus on the calendar. M.form.dateselector.calendar.focus(); // When the user tab out the calendar, close it. Y.one(document.body).on('keyup', function(e) { // If the calendar is open and we try to access it by pressing tab, we check if it is inside a Bootstrap dropdown-menu, // if so, we keep the dropdown open while navigation takes place in the calendar. if (M.form.dateselector.currentowner && e.keyCode === 9) { e.stopPropagation(); var calendarimageelement = document.getElementById(M.form.dateselector.currentowner.calendarimage.get('id')); if (M.form.dateselector.calendar.get('focused') && calendarimageelement.closest('.dropdown-menu') && !calendarimageelement.closest('.dropdown-menu').classList.contains("show")) { calendarimageelement.closest('.dropdown-menu').classList.add('show'); } } // hide the calendar if we press a key and the calendar is not focussed, or if we press ESC in the calendar. if ((M.form.dateselector.currentowner === this && !M.form.dateselector.calendar.get('focused')) || ((e.keyCode === 27) && M.form.dateselector.calendar.get('focused'))) { // Focus back on the calendar button. this.calendarimage.focus(); this.release_calendar(); } }, this); }, set_date_from_selects: function() { var year = parseInt(this.yearselect.get('value'), 10); var month = parseInt(this.monthselect.get('value'), 10) - 1; var day = parseInt(this.dayselect.get('value'), 10); var date = new Date(year, month, day); M.form.dateselector.calendar.deselectDates(); M.form.dateselector.calendar.selectDates([date]); M.form.dateselector.calendar.set("date", date); M.form.dateselector.calendar.render(); if (date.getDate() !== day) { // Must've selected the 29 to 31st of a month that doesn't have such dates. this.dayselect.set('value', date.getDate()); this.monthselect.set('value', date.getMonth() + 1); } }, set_selects_from_date: function(ev) { var date = ev.newSelection[0]; var newyear = Y.DataType.Date.format(date, {format: "%Y"}); var newindex = newyear - this.yearselect.firstOptionValue(); this.yearselect.set('selectedIndex', newindex); this.monthselect.set('selectedIndex', Y.DataType.Date.format(date, {format: "%m"}) - this.monthselect.firstOptionValue()); this.dayselect.set('selectedIndex', Y.DataType.Date.format(date, {format: "%d"}) - this.dayselect.firstOptionValue()); if (M.form.dateselector.currentowner && this.closepopup) { this.release_calendar(); } }, connect_handlers: function() { M.form.dateselector.calendar.on('selectionChange', this.set_selects_from_date, this, true); }, release_calendar: function(e) { var wasOwner = M.form.dateselector.currentowner === this; M.form.dateselector.panel.hide(); M.form.dateselector.calendar.detach('selectionChange', this.set_selects_from_date); M.form.dateselector.calendar.hide(); M.form.dateselector.currentowner = null; // Put the focus back to the image calendar that we clicked, only if it was visible. if (wasOwner && (e === null || typeof e === "undefined" || e.type !== "click")) { this.calendarimage.focus(); } }, toggle_calendar_image: function() { // If the enable checkbox is not checked, disable the calendar image and prevent focus. if (!this.enablecheckbox.get('checked') || this.enablecheckbox.get('disabled')) { this.calendarimage.setAttribute('disabled'); this.release_calendar(); } else { this.calendarimage.removeAttribute('disabled'); } } }; Y.extend(CALENDAR, Y.Base, CALENDAR.prototype, { NAME: 'Date Selector', ATTRS: { firstdayofweek: { validator: Y.Lang.isString }, node: { setter: function(node) { return Y.one(node); } } } }); src/dateselector/js/dateselector.js000064400000014670152267360450013463 0ustar00var DIALOGUE_SELECTOR = ' [role=dialog]', MENUBAR_SELECTOR = '[role=menubar]', DOT = '.', HAS_ZINDEX = 'moodle-has-zindex'; /** * Add some custom methods to the node class to make our lives a little * easier within this module. */ Y.mix(Y.Node.prototype, { /** * Gets the value of the first option in the select box */ firstOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.one('option').get('value'); }, /** * Gets the value of the last option in the select box */ lastOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.all('option').item(this.optionSize() - 1).get('value'); }, /** * Gets the number of options in the select box */ optionSize: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return parseInt(this.all('option').size(), 10); }, /** * Gets the value of the selected option in the select box */ selectedOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.all('option').item(this.get('selectedIndex')).get('value'); } }); M.form = M.form || {}; M.form.dateselector = { panel: null, calendar: null, currentowner: null, hidetimeout: null, repositiontimeout: null, init_date_selectors: function(config) { if (this.panel === null) { this.initPanel(config); } Y.all('.fdate_time_selector').each(function() { config.node = this; new CALENDAR(config); }); Y.all('.fdate_selector').each(function() { config.node = this; new CALENDAR(config); }); }, initPanel: function(config) { this.panel = new Y.Overlay({ visible: false, bodyContent: Y.Node.create('
                            '), id: 'dateselector-calendar-panel', constrain: true // constrain panel to viewport. }); this.panel.render(document.body); // Determine the correct zindex by looking at all existing dialogs and menubars in the page. this.panel.on('focus', function() { var highestzindex = 0; Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) { var zindex = this.findZIndex(node); if (zindex > highestzindex) { highestzindex = zindex; } }, this); // Only set the zindex if we found a wrapper. var zindexvalue = (highestzindex + 1).toString(); Y.one('#dateselector-calendar-panel').setStyle('zIndex', zindexvalue); }, this); this.panel.on('heightChange', this.fix_position, this); Y.one('#dateselector-calendar-panel').on('click', function(e) { e.halt(); }); Y.one(document.body).on('click', this.document_click, this); this.calendar = new MOODLECALENDAR({ contentBox: "#dateselector-calendar-content", width: "300px", showPrevMonth: true, showNextMonth: true, firstdayofweek: parseInt(config.firstdayofweek, 10), headerRenderer: function(date) { // We fetch the current language's preferred time format from the language pack. var calendar = this; require(['core/user_date', 'core/notification'], function(UserDate, Notification) { UserDate.get([{ timestamp: Math.floor(date.getTime() / 1000), format: M.util.get_string('strftimemonthyear', 'langconfig'), }]).then(function(dateStrs) { var headerNode = calendar.get('contentBox') .one('#' + calendar._calendarId + '_header'); if (headerNode) { headerNode.setContent(dateStrs[0]); } return dateStrs[0]; }).catch(Notification.exception); }); return ''; }, WEEKDAYS_MEDIUM: [ config.sun, config.mon, config.tue, config.wed, config.thu, config.fri, config.sat, ], }); }, findZIndex: function(node) { // In most cases the zindex is set on the parent of the dialog. var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex'); if (zindex) { return parseInt(zindex, 10); } return 0; }, cancel_any_timeout: function() { if (this.hidetimeout) { clearTimeout(this.hidetimeout); this.hidetimeout = null; } if (this.repositiontimeout) { clearTimeout(this.repositiontimeout); this.repositiontimeout = null; } }, delayed_reposition: function() { if (this.repositiontimeout) { clearTimeout(this.repositiontimeout); this.repositiontimeout = null; } this.repositiontimeout = setTimeout(this.fix_position, 500); }, fix_position: function() { if (this.currentowner) { var alignpoints = [ Y.WidgetPositionAlign.BL, Y.WidgetPositionAlign.TL ]; // Change the alignment if this is an RTL language. if (window.right_to_left()) { alignpoints = [ Y.WidgetPositionAlign.BR, Y.WidgetPositionAlign.TR ]; } this.panel.set('align', { node: this.currentowner.get('node').one('select'), points: alignpoints }); } }, document_click: function(e) { if (this.currentowner) { if (this.currentowner.get('node').ancestor('div').contains(e.target)) { setTimeout(function() { M.form.dateselector.cancel_any_timeout(); }, 100); } else { this.currentowner.release_calendar(e); } } } }; src/dateselector/js/moodlecalendar.js000064400000001416152267360450013750 0ustar00/** * Provides the Moodle Calendar class. * * @module moodle-form-dateselector */ /** * A class to overwrite the YUI3 Calendar in order to change the strings.. * * @class M.form_moodlecalendar * @constructor * @extends Calendar */ MOODLECALENDAR = function() { MOODLECALENDAR.superclass.constructor.apply(this, arguments); }; Y.extend(MOODLECALENDAR, Y.Calendar, { initializer: function(cfg) { this.set("strings.very_short_weekdays", cfg.WEEKDAYS_MEDIUM); this.set("strings.first_weekday", cfg.firstdayofweek); } }, { NAME: 'Calendar', ATTRS: {} } ); M.form_moodlecalendar = M.form_moodlecalendar || {}; M.form_moodlecalendar.initializer = function(params) { return new MOODLECALENDAR(params); }; src/dateselector/js/shared.js000064400000000042152267360450012237 0ustar00var CALENDAR; var MOODLECALENDAR; checkboxcontroller/checkboxcontroller.js000064400000010275152267360450014705 0ustar00// 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 . /** * Group of date and time input element * * Contains class for a group of elements used to input a date and time. * * @package core_form * @copyright 2012 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ YUI.add('moodle-form-checkboxcontroller', function(Y) { var checkboxcontroller = function() { checkboxcontroller.superclass.constructor.apply(this, arguments); } Y.extend(checkboxcontroller, Y.Base, { _controllervaluenode : null, _checkboxclass : null, /* * Initialize script if all params passed. * * @param object params values passed while initalizing script */ initializer : function(params) { if (params && params.checkboxcontroller && params.controllerbutton && params.checkboxclass) { // Id of controller node which keeps value in html. this._controllervaluenode = '#id_'+params.checkboxcontroller; // Checkboxes class name by which checkboxes will be selected this._checkboxclass = '.'+params.checkboxclass; // Replace submit button with link. this.replaceButton('#id_'+params.controllerbutton); } }, /** * Replace controller button with link and add event. * * @param string controllerbutton id of the controller button which needs to be replaced */ replaceButton : function(controllerbutton) { var controllerbutton = Y.one(controllerbutton); var linkname = controllerbutton.get('value'); // Link node which will replace controller button var link = Y.Node.create(''+linkname+''); // Attach onclick event to link link.on('click', this.onClick, this); // Hide controller button controllerbutton.hide(); // Insert link node controllerbutton.get('parentNode').insert(link, controllerbutton.get('lastNode')); }, /** * Onclick event will be handled. * * @param Event e */ onClick : function(e) { e.preventDefault(); this.switchGroupState(); }, /** * Toggles checkboxes status belong to a group */ switchGroupState : function() { if (this._checkboxclass) { // Value which should be set on checkboxes var newvalue = ''; // Get controller node which keeps value var controllervaluenode = Y.one(this._controllervaluenode); // Get all checkboxes with var checkboxes = Y.all(this._checkboxclass); // Toggle checkboxes in group, depending on conroller value if (controllervaluenode.get('value') == 1) { controllervaluenode.set('value', '0'); } else { controllervaluenode.set('value', '1'); newvalue = 'checked'; } checkboxes.each(function(checkbox){ if (!checkbox.get('disabled')) { checkbox.set('checked', newvalue); } }); } } }); M.form = M.form || {}; M.form.checkboxcontroller = function(params) { return new checkboxcontroller(params); } }, '@VERSION@', {requires:['base', 'node']}); build/moodle-form-shortforms/moodle-form-shortforms-debug.js000064400000007725152267360450020362 0ustar00YUI.add('moodle-form-shortforms', function (Y, NAME) { /** * Provides the form shortforms class. * * @module moodle-form-shortforms */ /** * A class for a shortforms. * * @class M.form.shortforms * @constructor * @extends Base */ function SHORTFORMS() { SHORTFORMS.superclass.constructor.apply(this, arguments); } var SELECTORS = { COLLAPSED: '.collapsed', FIELDSETCOLLAPSIBLE: 'fieldset.collapsible', FIELDSETLEGENDLINK: 'fieldset.collapsible .fheader', FHEADER: '.fheader', LEGENDFTOGGLER: 'legend.ftoggler' }, CSS = { COLLAPSEALL: 'collapse-all', COLLAPSED: 'collapsed', FHEADER: 'fheader' }, ATTRS = {}; /** * The form ID attribute definition. * * @attribute formid * @type String * @default '' * @writeOnce */ ATTRS.formid = { value: null }; Y.extend(SHORTFORMS, Y.Base, { /** * A reference to the form. * * @property form * @protected * @type Node * @default null */ form: null, /** * The initializer for the shortforms instance. * * @method initializer * @protected */ initializer: function() { var form = Y.one('#' + this.get('formid')); if (!form) { Y.log('Could not locate the form', 'warn', 'moodle-form-shortforms'); return; } // Stores the form in the object. this.form = form; // Subscribe collapsible fieldsets and buttons to click events. form.delegate('click', this.switch_state, SELECTORS.FIELDSETLEGENDLINK, this); // Handle event, when there's an error in collapsed section. Y.Global.on(M.core.globalEvents.FORM_ERROR, this.expand_fieldset, this); }, /** * Set the collapsed state for the specified fieldset. * * @method set_state * @param {Node} fieldset The Node relating to the fieldset to set state on. * @param {Boolean} [collapsed] Whether the fieldset is collapsed. * @chainable */ set_state: function(fieldset, collapsed) { var headerlink = fieldset.one(SELECTORS.FHEADER); if (collapsed) { fieldset.addClass(CSS.COLLAPSED); if (headerlink) { headerlink.setAttribute('aria-expanded', 'false'); } } else { fieldset.removeClass(CSS.COLLAPSED); if (headerlink) { headerlink.setAttribute('aria-expanded', 'true'); } } var statuselement = this.form.one('input[name=mform_isexpanded_' + fieldset.get('id') + ']'); if (!statuselement) { Y.log("M.form.shortforms::switch_state was called on an fieldset without a status field: '" + fieldset.get('id') + "'", 'debug', 'moodle-form-shortforms'); return this; } statuselement.set('value', collapsed ? 0 : 1); return this; }, /** * Toggle the state for the fieldset that was clicked. * * @method switch_state * @param {EventFacade} e */ switch_state: function(e) { e.preventDefault(); var fieldset = e.target.ancestor(SELECTORS.FIELDSETCOLLAPSIBLE); this.set_state(fieldset, !fieldset.hasClass(CSS.COLLAPSED)); }, /** * Expand the fieldset, which contains an error. * * @method expand_fieldset * @param {EventFacade} e */ expand_fieldset: function(e) { e.stopPropagation(); var formid = e.formid; if (formid === this.form.getAttribute('id')) { var errorfieldset = Y.one('#' + e.elementid).ancestor('fieldset'); if (errorfieldset) { this.set_state(errorfieldset, false); } } } }, { NAME: 'moodle-form-shortforms', ATTRS: ATTRS }); M.form = M.form || {}; M.form.shortforms = M.form.shortforms || function(params) { return new SHORTFORMS(params); }; }, '@VERSION@', {"requires": ["node", "base", "selector-css3", "moodle-core-event"]}); build/moodle-form-shortforms/moodle-form-shortforms.js000064400000007313152267360450017267 0ustar00YUI.add('moodle-form-shortforms', function (Y, NAME) { /** * Provides the form shortforms class. * * @module moodle-form-shortforms */ /** * A class for a shortforms. * * @class M.form.shortforms * @constructor * @extends Base */ function SHORTFORMS() { SHORTFORMS.superclass.constructor.apply(this, arguments); } var SELECTORS = { COLLAPSED: '.collapsed', FIELDSETCOLLAPSIBLE: 'fieldset.collapsible', FIELDSETLEGENDLINK: 'fieldset.collapsible .fheader', FHEADER: '.fheader', LEGENDFTOGGLER: 'legend.ftoggler' }, CSS = { COLLAPSEALL: 'collapse-all', COLLAPSED: 'collapsed', FHEADER: 'fheader' }, ATTRS = {}; /** * The form ID attribute definition. * * @attribute formid * @type String * @default '' * @writeOnce */ ATTRS.formid = { value: null }; Y.extend(SHORTFORMS, Y.Base, { /** * A reference to the form. * * @property form * @protected * @type Node * @default null */ form: null, /** * The initializer for the shortforms instance. * * @method initializer * @protected */ initializer: function() { var form = Y.one('#' + this.get('formid')); if (!form) { return; } // Stores the form in the object. this.form = form; // Subscribe collapsible fieldsets and buttons to click events. form.delegate('click', this.switch_state, SELECTORS.FIELDSETLEGENDLINK, this); // Handle event, when there's an error in collapsed section. Y.Global.on(M.core.globalEvents.FORM_ERROR, this.expand_fieldset, this); }, /** * Set the collapsed state for the specified fieldset. * * @method set_state * @param {Node} fieldset The Node relating to the fieldset to set state on. * @param {Boolean} [collapsed] Whether the fieldset is collapsed. * @chainable */ set_state: function(fieldset, collapsed) { var headerlink = fieldset.one(SELECTORS.FHEADER); if (collapsed) { fieldset.addClass(CSS.COLLAPSED); if (headerlink) { headerlink.setAttribute('aria-expanded', 'false'); } } else { fieldset.removeClass(CSS.COLLAPSED); if (headerlink) { headerlink.setAttribute('aria-expanded', 'true'); } } var statuselement = this.form.one('input[name=mform_isexpanded_' + fieldset.get('id') + ']'); if (!statuselement) { return this; } statuselement.set('value', collapsed ? 0 : 1); return this; }, /** * Toggle the state for the fieldset that was clicked. * * @method switch_state * @param {EventFacade} e */ switch_state: function(e) { e.preventDefault(); var fieldset = e.target.ancestor(SELECTORS.FIELDSETCOLLAPSIBLE); this.set_state(fieldset, !fieldset.hasClass(CSS.COLLAPSED)); }, /** * Expand the fieldset, which contains an error. * * @method expand_fieldset * @param {EventFacade} e */ expand_fieldset: function(e) { e.stopPropagation(); var formid = e.formid; if (formid === this.form.getAttribute('id')) { var errorfieldset = Y.one('#' + e.elementid).ancestor('fieldset'); if (errorfieldset) { this.set_state(errorfieldset, false); } } } }, { NAME: 'moodle-form-shortforms', ATTRS: ATTRS }); M.form = M.form || {}; M.form.shortforms = M.form.shortforms || function(params) { return new SHORTFORMS(params); }; }, '@VERSION@', {"requires": ["node", "base", "selector-css3", "moodle-core-event"]}); build/moodle-form-shortforms/moodle-form-shortforms-min.js000064400000002226152267360450020046 0ustar00YUI.add("moodle-form-shortforms",function(s,e){function t(){t.superclass.constructor.apply(this,arguments)}var o="fieldset.collapsible",r="fieldset.collapsible .fheader",i=".fheader",a="collapsed",n={formid:{value:null}};s.extend(t,s.Base,{form:null,initializer:function(){var e=s.one("#"+this.get("formid"));e&&((this.form=e).delegate("click",this.switch_state,r,this),s.Global.on(M.core.globalEvents.FORM_ERROR,this.expand_fieldset,this))},set_state:function(e,t){var s=e.one(i);return t?(e.addClass(a),s&&s.setAttribute("aria-expanded","false")):(e.removeClass(a),s&&s.setAttribute("aria-expanded","true")),(s=this.form.one("input[name=mform_isexpanded_"+e.get("id")+"]"))&&s.set("value",t?0:1),this},switch_state:function(e){e.preventDefault();e=e.target.ancestor(o);this.set_state(e,!e.hasClass(a))},expand_fieldset:function(e){var t;e.stopPropagation(),e.formid===this.form.getAttribute("id")&&(t=s.one("#"+e.elementid).ancestor("fieldset"))&&this.set_state(t,!1)}},{NAME:"moodle-form-shortforms",ATTRS:n}),M.form=M.form||{},M.form.shortforms=M.form.shortforms||function(e){return new t(e)}},"@VERSION@",{requires:["node","base","selector-css3","moodle-core-event"]});build/moodle-form-dateselector/moodle-form-dateselector-debug.js000064400000040135152267360450021072 0ustar00YUI.add('moodle-form-dateselector', function (Y, NAME) { var CALENDAR; var MOODLECALENDAR; var DIALOGUE_SELECTOR = ' [role=dialog]', MENUBAR_SELECTOR = '[role=menubar]', DOT = '.', HAS_ZINDEX = 'moodle-has-zindex'; /** * Add some custom methods to the node class to make our lives a little * easier within this module. */ Y.mix(Y.Node.prototype, { /** * Gets the value of the first option in the select box */ firstOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.one('option').get('value'); }, /** * Gets the value of the last option in the select box */ lastOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.all('option').item(this.optionSize() - 1).get('value'); }, /** * Gets the number of options in the select box */ optionSize: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return parseInt(this.all('option').size(), 10); }, /** * Gets the value of the selected option in the select box */ selectedOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.all('option').item(this.get('selectedIndex')).get('value'); } }); M.form = M.form || {}; M.form.dateselector = { panel: null, calendar: null, currentowner: null, hidetimeout: null, repositiontimeout: null, init_date_selectors: function(config) { if (this.panel === null) { this.initPanel(config); } Y.all('.fdate_time_selector').each(function() { config.node = this; new CALENDAR(config); }); Y.all('.fdate_selector').each(function() { config.node = this; new CALENDAR(config); }); }, initPanel: function(config) { this.panel = new Y.Overlay({ visible: false, bodyContent: Y.Node.create('
                            '), id: 'dateselector-calendar-panel', constrain: true // constrain panel to viewport. }); this.panel.render(document.body); // Determine the correct zindex by looking at all existing dialogs and menubars in the page. this.panel.on('focus', function() { var highestzindex = 0; Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) { var zindex = this.findZIndex(node); if (zindex > highestzindex) { highestzindex = zindex; } }, this); // Only set the zindex if we found a wrapper. var zindexvalue = (highestzindex + 1).toString(); Y.one('#dateselector-calendar-panel').setStyle('zIndex', zindexvalue); }, this); this.panel.on('heightChange', this.fix_position, this); Y.one('#dateselector-calendar-panel').on('click', function(e) { e.halt(); }); Y.one(document.body).on('click', this.document_click, this); this.calendar = new MOODLECALENDAR({ contentBox: "#dateselector-calendar-content", width: "300px", showPrevMonth: true, showNextMonth: true, firstdayofweek: parseInt(config.firstdayofweek, 10), headerRenderer: function(date) { // We fetch the current language's preferred time format from the language pack. var calendar = this; require(['core/user_date', 'core/notification'], function(UserDate, Notification) { UserDate.get([{ timestamp: Math.floor(date.getTime() / 1000), format: M.util.get_string('strftimemonthyear', 'langconfig'), }]).then(function(dateStrs) { var headerNode = calendar.get('contentBox') .one('#' + calendar._calendarId + '_header'); if (headerNode) { headerNode.setContent(dateStrs[0]); } return dateStrs[0]; }).catch(Notification.exception); }); return ''; }, WEEKDAYS_MEDIUM: [ config.sun, config.mon, config.tue, config.wed, config.thu, config.fri, config.sat, ], }); }, findZIndex: function(node) { // In most cases the zindex is set on the parent of the dialog. var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex'); if (zindex) { return parseInt(zindex, 10); } return 0; }, cancel_any_timeout: function() { if (this.hidetimeout) { clearTimeout(this.hidetimeout); this.hidetimeout = null; } if (this.repositiontimeout) { clearTimeout(this.repositiontimeout); this.repositiontimeout = null; } }, delayed_reposition: function() { if (this.repositiontimeout) { clearTimeout(this.repositiontimeout); this.repositiontimeout = null; } this.repositiontimeout = setTimeout(this.fix_position, 500); }, fix_position: function() { if (this.currentowner) { var alignpoints = [ Y.WidgetPositionAlign.BL, Y.WidgetPositionAlign.TL ]; // Change the alignment if this is an RTL language. if (window.right_to_left()) { alignpoints = [ Y.WidgetPositionAlign.BR, Y.WidgetPositionAlign.TR ]; } this.panel.set('align', { node: this.currentowner.get('node').one('select'), points: alignpoints }); } }, document_click: function(e) { if (this.currentowner) { if (this.currentowner.get('node').ancestor('div').contains(e.target)) { setTimeout(function() { M.form.dateselector.cancel_any_timeout(); }, 100); } else { this.currentowner.release_calendar(e); } } } }; /** * Provides the Moodle Calendar class. * * @module moodle-form-dateselector */ /** * A class to overwrite the YUI3 Calendar in order to change the strings.. * * @class M.form_moodlecalendar * @constructor * @extends Calendar */ MOODLECALENDAR = function() { MOODLECALENDAR.superclass.constructor.apply(this, arguments); }; Y.extend(MOODLECALENDAR, Y.Calendar, { initializer: function(cfg) { this.set("strings.very_short_weekdays", cfg.WEEKDAYS_MEDIUM); this.set("strings.first_weekday", cfg.firstdayofweek); } }, { NAME: 'Calendar', ATTRS: {} } ); M.form_moodlecalendar = M.form_moodlecalendar || {}; M.form_moodlecalendar.initializer = function(params) { return new MOODLECALENDAR(params); }; /** * Provides the Calendar class. * * @module moodle-form-dateselector */ /** * Calendar class */ CALENDAR = function() { CALENDAR.superclass.constructor.apply(this, arguments); }; CALENDAR.prototype = { panel: null, yearselect: null, monthselect: null, dayselect: null, calendarimage: null, enablecheckbox: null, closepopup: true, initializer: function() { var controls = this.get('node').all('select'); controls.each(function(node) { if (node.get('name').match(/\[year\]/)) { this.yearselect = node; } else if (node.get('name').match(/\[month\]/)) { this.monthselect = node; } else if (node.get('name').match(/\[day\]/)) { this.dayselect = node; } node.after('change', this.handle_select_change, this); }, this); // Loop through the input fields. var inputs = this.get('node').all('input, button'); inputs.each(function(node) { // Check if the current node is a calendar image field. if (node.get('name').match(/\[calendar\]/)) { // Set it so that when the image is clicked the pop-up displays. node.on('click', this.focus_event, this); // Set the node to the calendarimage variable. this.calendarimage = node; } else { // Must be the enabled checkbox field. // If the enable checkbox is clicked we want to either disable/enable the calendar image. node.on('click', this.toggle_calendar_image, this); // Set the node to the enablecheckbox variable. this.enablecheckbox = node; } // Ensure that the calendarimage and enablecheckbox values have been set. if (this.calendarimage && this.enablecheckbox) { // Set the calendar icon status depending on the value of the checkbox. this.toggle_calendar_image(); } }, this); // Get the calendarimage element by its ID and check if any of its parents have the modal-dialog class to // know if the link is inside a modal, if so, set the aria-hidden and tabindex properties to the indicated values. var calendarimageelement = document.getElementById(this.calendarimage.get('id')); if (calendarimageelement.closest('.modal-dialog')) { this.calendarimage.set('aria-hidden', true); this.calendarimage.set('tabIndex', '-1'); } }, focus_event: function(e) { M.form.dateselector.cancel_any_timeout(); // If the current owner is set, then the pop-up is currently being displayed, so hide it. if (M.form.dateselector.currentowner === this) { this.release_calendar(); } else if ((this.enablecheckbox === null) || (this.enablecheckbox.get('checked'))) { // Must be hidden. If the field is enabled display the pop-up. this.claim_calendar(); } // Stop the input image field from submitting the form. e.preventDefault(); }, handle_select_change: function() { // It may seem as if the following variable is not used, however any call to set_date_from_selects will trigger a // call to set_selects_from_date if the calendar is open as the date has changed. Whenever the calendar is displayed // the set_selects_from_date function is set to trigger on any date change (see function connect_handlers). this.closepopup = false; this.set_date_from_selects(); this.closepopup = true; }, claim_calendar: function() { M.form.dateselector.cancel_any_timeout(); if (M.form.dateselector.currentowner === this) { return; } if (M.form.dateselector.currentowner) { M.form.dateselector.currentowner.release_calendar(); } if (M.form.dateselector.currentowner !== this) { this.connect_handlers(); this.set_date_from_selects(); } M.form.dateselector.currentowner = this; M.form.dateselector.calendar.set('minimumDate', new Date(this.yearselect.firstOptionValue(), 0, 1)); M.form.dateselector.calendar.set('maximumDate', new Date(this.yearselect.lastOptionValue(), 11, 31)); M.form.dateselector.panel.show(); M.form.dateselector.calendar.show(); M.form.dateselector.fix_position(); setTimeout(function() { M.form.dateselector.cancel_any_timeout(); }, 100); // Focus on the calendar. M.form.dateselector.calendar.focus(); // When the user tab out the calendar, close it. Y.one(document.body).on('keyup', function(e) { // If the calendar is open and we try to access it by pressing tab, we check if it is inside a Bootstrap dropdown-menu, // if so, we keep the dropdown open while navigation takes place in the calendar. if (M.form.dateselector.currentowner && e.keyCode === 9) { e.stopPropagation(); var calendarimageelement = document.getElementById(M.form.dateselector.currentowner.calendarimage.get('id')); if (M.form.dateselector.calendar.get('focused') && calendarimageelement.closest('.dropdown-menu') && !calendarimageelement.closest('.dropdown-menu').classList.contains("show")) { calendarimageelement.closest('.dropdown-menu').classList.add('show'); } } // hide the calendar if we press a key and the calendar is not focussed, or if we press ESC in the calendar. if ((M.form.dateselector.currentowner === this && !M.form.dateselector.calendar.get('focused')) || ((e.keyCode === 27) && M.form.dateselector.calendar.get('focused'))) { // Focus back on the calendar button. this.calendarimage.focus(); this.release_calendar(); } }, this); }, set_date_from_selects: function() { var year = parseInt(this.yearselect.get('value'), 10); var month = parseInt(this.monthselect.get('value'), 10) - 1; var day = parseInt(this.dayselect.get('value'), 10); var date = new Date(year, month, day); M.form.dateselector.calendar.deselectDates(); M.form.dateselector.calendar.selectDates([date]); M.form.dateselector.calendar.set("date", date); M.form.dateselector.calendar.render(); if (date.getDate() !== day) { // Must've selected the 29 to 31st of a month that doesn't have such dates. this.dayselect.set('value', date.getDate()); this.monthselect.set('value', date.getMonth() + 1); } }, set_selects_from_date: function(ev) { var date = ev.newSelection[0]; var newyear = Y.DataType.Date.format(date, {format: "%Y"}); var newindex = newyear - this.yearselect.firstOptionValue(); this.yearselect.set('selectedIndex', newindex); this.monthselect.set('selectedIndex', Y.DataType.Date.format(date, {format: "%m"}) - this.monthselect.firstOptionValue()); this.dayselect.set('selectedIndex', Y.DataType.Date.format(date, {format: "%d"}) - this.dayselect.firstOptionValue()); if (M.form.dateselector.currentowner && this.closepopup) { this.release_calendar(); } }, connect_handlers: function() { M.form.dateselector.calendar.on('selectionChange', this.set_selects_from_date, this, true); }, release_calendar: function(e) { var wasOwner = M.form.dateselector.currentowner === this; M.form.dateselector.panel.hide(); M.form.dateselector.calendar.detach('selectionChange', this.set_selects_from_date); M.form.dateselector.calendar.hide(); M.form.dateselector.currentowner = null; // Put the focus back to the image calendar that we clicked, only if it was visible. if (wasOwner && (e === null || typeof e === "undefined" || e.type !== "click")) { this.calendarimage.focus(); } }, toggle_calendar_image: function() { // If the enable checkbox is not checked, disable the calendar image and prevent focus. if (!this.enablecheckbox.get('checked') || this.enablecheckbox.get('disabled')) { this.calendarimage.setAttribute('disabled'); this.release_calendar(); } else { this.calendarimage.removeAttribute('disabled'); } } }; Y.extend(CALENDAR, Y.Base, CALENDAR.prototype, { NAME: 'Date Selector', ATTRS: { firstdayofweek: { validator: Y.Lang.isString }, node: { setter: function(node) { return Y.one(node); } } } }); }, '@VERSION@', {"requires": ["base", "node", "overlay", "calendar"]}); build/moodle-form-dateselector/moodle-form-dateselector.js000064400000040135152267360450020006 0ustar00YUI.add('moodle-form-dateselector', function (Y, NAME) { var CALENDAR; var MOODLECALENDAR; var DIALOGUE_SELECTOR = ' [role=dialog]', MENUBAR_SELECTOR = '[role=menubar]', DOT = '.', HAS_ZINDEX = 'moodle-has-zindex'; /** * Add some custom methods to the node class to make our lives a little * easier within this module. */ Y.mix(Y.Node.prototype, { /** * Gets the value of the first option in the select box */ firstOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.one('option').get('value'); }, /** * Gets the value of the last option in the select box */ lastOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.all('option').item(this.optionSize() - 1).get('value'); }, /** * Gets the number of options in the select box */ optionSize: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return parseInt(this.all('option').size(), 10); }, /** * Gets the value of the selected option in the select box */ selectedOptionValue: function() { if (this.get('nodeName').toLowerCase() !== 'select') { return false; } return this.all('option').item(this.get('selectedIndex')).get('value'); } }); M.form = M.form || {}; M.form.dateselector = { panel: null, calendar: null, currentowner: null, hidetimeout: null, repositiontimeout: null, init_date_selectors: function(config) { if (this.panel === null) { this.initPanel(config); } Y.all('.fdate_time_selector').each(function() { config.node = this; new CALENDAR(config); }); Y.all('.fdate_selector').each(function() { config.node = this; new CALENDAR(config); }); }, initPanel: function(config) { this.panel = new Y.Overlay({ visible: false, bodyContent: Y.Node.create('
                            '), id: 'dateselector-calendar-panel', constrain: true // constrain panel to viewport. }); this.panel.render(document.body); // Determine the correct zindex by looking at all existing dialogs and menubars in the page. this.panel.on('focus', function() { var highestzindex = 0; Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) { var zindex = this.findZIndex(node); if (zindex > highestzindex) { highestzindex = zindex; } }, this); // Only set the zindex if we found a wrapper. var zindexvalue = (highestzindex + 1).toString(); Y.one('#dateselector-calendar-panel').setStyle('zIndex', zindexvalue); }, this); this.panel.on('heightChange', this.fix_position, this); Y.one('#dateselector-calendar-panel').on('click', function(e) { e.halt(); }); Y.one(document.body).on('click', this.document_click, this); this.calendar = new MOODLECALENDAR({ contentBox: "#dateselector-calendar-content", width: "300px", showPrevMonth: true, showNextMonth: true, firstdayofweek: parseInt(config.firstdayofweek, 10), headerRenderer: function(date) { // We fetch the current language's preferred time format from the language pack. var calendar = this; require(['core/user_date', 'core/notification'], function(UserDate, Notification) { UserDate.get([{ timestamp: Math.floor(date.getTime() / 1000), format: M.util.get_string('strftimemonthyear', 'langconfig'), }]).then(function(dateStrs) { var headerNode = calendar.get('contentBox') .one('#' + calendar._calendarId + '_header'); if (headerNode) { headerNode.setContent(dateStrs[0]); } return dateStrs[0]; }).catch(Notification.exception); }); return ''; }, WEEKDAYS_MEDIUM: [ config.sun, config.mon, config.tue, config.wed, config.thu, config.fri, config.sat, ], }); }, findZIndex: function(node) { // In most cases the zindex is set on the parent of the dialog. var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex'); if (zindex) { return parseInt(zindex, 10); } return 0; }, cancel_any_timeout: function() { if (this.hidetimeout) { clearTimeout(this.hidetimeout); this.hidetimeout = null; } if (this.repositiontimeout) { clearTimeout(this.repositiontimeout); this.repositiontimeout = null; } }, delayed_reposition: function() { if (this.repositiontimeout) { clearTimeout(this.repositiontimeout); this.repositiontimeout = null; } this.repositiontimeout = setTimeout(this.fix_position, 500); }, fix_position: function() { if (this.currentowner) { var alignpoints = [ Y.WidgetPositionAlign.BL, Y.WidgetPositionAlign.TL ]; // Change the alignment if this is an RTL language. if (window.right_to_left()) { alignpoints = [ Y.WidgetPositionAlign.BR, Y.WidgetPositionAlign.TR ]; } this.panel.set('align', { node: this.currentowner.get('node').one('select'), points: alignpoints }); } }, document_click: function(e) { if (this.currentowner) { if (this.currentowner.get('node').ancestor('div').contains(e.target)) { setTimeout(function() { M.form.dateselector.cancel_any_timeout(); }, 100); } else { this.currentowner.release_calendar(e); } } } }; /** * Provides the Moodle Calendar class. * * @module moodle-form-dateselector */ /** * A class to overwrite the YUI3 Calendar in order to change the strings.. * * @class M.form_moodlecalendar * @constructor * @extends Calendar */ MOODLECALENDAR = function() { MOODLECALENDAR.superclass.constructor.apply(this, arguments); }; Y.extend(MOODLECALENDAR, Y.Calendar, { initializer: function(cfg) { this.set("strings.very_short_weekdays", cfg.WEEKDAYS_MEDIUM); this.set("strings.first_weekday", cfg.firstdayofweek); } }, { NAME: 'Calendar', ATTRS: {} } ); M.form_moodlecalendar = M.form_moodlecalendar || {}; M.form_moodlecalendar.initializer = function(params) { return new MOODLECALENDAR(params); }; /** * Provides the Calendar class. * * @module moodle-form-dateselector */ /** * Calendar class */ CALENDAR = function() { CALENDAR.superclass.constructor.apply(this, arguments); }; CALENDAR.prototype = { panel: null, yearselect: null, monthselect: null, dayselect: null, calendarimage: null, enablecheckbox: null, closepopup: true, initializer: function() { var controls = this.get('node').all('select'); controls.each(function(node) { if (node.get('name').match(/\[year\]/)) { this.yearselect = node; } else if (node.get('name').match(/\[month\]/)) { this.monthselect = node; } else if (node.get('name').match(/\[day\]/)) { this.dayselect = node; } node.after('change', this.handle_select_change, this); }, this); // Loop through the input fields. var inputs = this.get('node').all('input, button'); inputs.each(function(node) { // Check if the current node is a calendar image field. if (node.get('name').match(/\[calendar\]/)) { // Set it so that when the image is clicked the pop-up displays. node.on('click', this.focus_event, this); // Set the node to the calendarimage variable. this.calendarimage = node; } else { // Must be the enabled checkbox field. // If the enable checkbox is clicked we want to either disable/enable the calendar image. node.on('click', this.toggle_calendar_image, this); // Set the node to the enablecheckbox variable. this.enablecheckbox = node; } // Ensure that the calendarimage and enablecheckbox values have been set. if (this.calendarimage && this.enablecheckbox) { // Set the calendar icon status depending on the value of the checkbox. this.toggle_calendar_image(); } }, this); // Get the calendarimage element by its ID and check if any of its parents have the modal-dialog class to // know if the link is inside a modal, if so, set the aria-hidden and tabindex properties to the indicated values. var calendarimageelement = document.getElementById(this.calendarimage.get('id')); if (calendarimageelement.closest('.modal-dialog')) { this.calendarimage.set('aria-hidden', true); this.calendarimage.set('tabIndex', '-1'); } }, focus_event: function(e) { M.form.dateselector.cancel_any_timeout(); // If the current owner is set, then the pop-up is currently being displayed, so hide it. if (M.form.dateselector.currentowner === this) { this.release_calendar(); } else if ((this.enablecheckbox === null) || (this.enablecheckbox.get('checked'))) { // Must be hidden. If the field is enabled display the pop-up. this.claim_calendar(); } // Stop the input image field from submitting the form. e.preventDefault(); }, handle_select_change: function() { // It may seem as if the following variable is not used, however any call to set_date_from_selects will trigger a // call to set_selects_from_date if the calendar is open as the date has changed. Whenever the calendar is displayed // the set_selects_from_date function is set to trigger on any date change (see function connect_handlers). this.closepopup = false; this.set_date_from_selects(); this.closepopup = true; }, claim_calendar: function() { M.form.dateselector.cancel_any_timeout(); if (M.form.dateselector.currentowner === this) { return; } if (M.form.dateselector.currentowner) { M.form.dateselector.currentowner.release_calendar(); } if (M.form.dateselector.currentowner !== this) { this.connect_handlers(); this.set_date_from_selects(); } M.form.dateselector.currentowner = this; M.form.dateselector.calendar.set('minimumDate', new Date(this.yearselect.firstOptionValue(), 0, 1)); M.form.dateselector.calendar.set('maximumDate', new Date(this.yearselect.lastOptionValue(), 11, 31)); M.form.dateselector.panel.show(); M.form.dateselector.calendar.show(); M.form.dateselector.fix_position(); setTimeout(function() { M.form.dateselector.cancel_any_timeout(); }, 100); // Focus on the calendar. M.form.dateselector.calendar.focus(); // When the user tab out the calendar, close it. Y.one(document.body).on('keyup', function(e) { // If the calendar is open and we try to access it by pressing tab, we check if it is inside a Bootstrap dropdown-menu, // if so, we keep the dropdown open while navigation takes place in the calendar. if (M.form.dateselector.currentowner && e.keyCode === 9) { e.stopPropagation(); var calendarimageelement = document.getElementById(M.form.dateselector.currentowner.calendarimage.get('id')); if (M.form.dateselector.calendar.get('focused') && calendarimageelement.closest('.dropdown-menu') && !calendarimageelement.closest('.dropdown-menu').classList.contains("show")) { calendarimageelement.closest('.dropdown-menu').classList.add('show'); } } // hide the calendar if we press a key and the calendar is not focussed, or if we press ESC in the calendar. if ((M.form.dateselector.currentowner === this && !M.form.dateselector.calendar.get('focused')) || ((e.keyCode === 27) && M.form.dateselector.calendar.get('focused'))) { // Focus back on the calendar button. this.calendarimage.focus(); this.release_calendar(); } }, this); }, set_date_from_selects: function() { var year = parseInt(this.yearselect.get('value'), 10); var month = parseInt(this.monthselect.get('value'), 10) - 1; var day = parseInt(this.dayselect.get('value'), 10); var date = new Date(year, month, day); M.form.dateselector.calendar.deselectDates(); M.form.dateselector.calendar.selectDates([date]); M.form.dateselector.calendar.set("date", date); M.form.dateselector.calendar.render(); if (date.getDate() !== day) { // Must've selected the 29 to 31st of a month that doesn't have such dates. this.dayselect.set('value', date.getDate()); this.monthselect.set('value', date.getMonth() + 1); } }, set_selects_from_date: function(ev) { var date = ev.newSelection[0]; var newyear = Y.DataType.Date.format(date, {format: "%Y"}); var newindex = newyear - this.yearselect.firstOptionValue(); this.yearselect.set('selectedIndex', newindex); this.monthselect.set('selectedIndex', Y.DataType.Date.format(date, {format: "%m"}) - this.monthselect.firstOptionValue()); this.dayselect.set('selectedIndex', Y.DataType.Date.format(date, {format: "%d"}) - this.dayselect.firstOptionValue()); if (M.form.dateselector.currentowner && this.closepopup) { this.release_calendar(); } }, connect_handlers: function() { M.form.dateselector.calendar.on('selectionChange', this.set_selects_from_date, this, true); }, release_calendar: function(e) { var wasOwner = M.form.dateselector.currentowner === this; M.form.dateselector.panel.hide(); M.form.dateselector.calendar.detach('selectionChange', this.set_selects_from_date); M.form.dateselector.calendar.hide(); M.form.dateselector.currentowner = null; // Put the focus back to the image calendar that we clicked, only if it was visible. if (wasOwner && (e === null || typeof e === "undefined" || e.type !== "click")) { this.calendarimage.focus(); } }, toggle_calendar_image: function() { // If the enable checkbox is not checked, disable the calendar image and prevent focus. if (!this.enablecheckbox.get('checked') || this.enablecheckbox.get('disabled')) { this.calendarimage.setAttribute('disabled'); this.release_calendar(); } else { this.calendarimage.removeAttribute('disabled'); } } }; Y.extend(CALENDAR, Y.Base, CALENDAR.prototype, { NAME: 'Date Selector', ATTRS: { firstdayofweek: { validator: Y.Lang.isString }, node: { setter: function(node) { return Y.one(node); } } } }); }, '@VERSION@', {"requires": ["base", "node", "overlay", "calendar"]}); build/moodle-form-dateselector/moodle-form-dateselector-min.js000064400000017326152267360450020575 0ustar00YUI.add("moodle-form-dateselector",function(n,e){var t,o;n.mix(n.Node.prototype,{firstOptionValue:function(){return"select"===this.get("nodeName").toLowerCase()&&this.one("option").get("value")},lastOptionValue:function(){return"select"===this.get("nodeName").toLowerCase()&&this.all("option").item(this.optionSize()-1).get("value")},optionSize:function(){return"select"===this.get("nodeName").toLowerCase()&&parseInt(this.all("option").size(),10)},selectedOptionValue:function(){return"select"===this.get("nodeName").toLowerCase()&&this.all("option").item(this.get("selectedIndex")).get("value")}}),M.form=M.form||{},M.form.dateselector={panel:null,calendar:null,currentowner:null,hidetimeout:null,repositiontimeout:null,init_date_selectors:function(e){null===this.panel&&this.initPanel(e),n.all(".fdate_time_selector").each(function(){e.node=this,new t(e)}),n.all(".fdate_selector").each(function(){e.node=this,new t(e)})},initPanel:function(e){this.panel=new n.Overlay({visible:!1,bodyContent:n.Node.create('
                            '),id:"dateselector-calendar-panel",constrain:!0}),this.panel.render(document.body),this.panel.on("focus",function(){var e,t=0;n.all(" [role=dialog], [role=menubar], .moodle-has-zindex").each(function(e){e=this.findZIndex(e);t