// ---- /lib/amd/src/fragment.js ----
// 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 way to call HTML fragments to be inserted as required via JavaScript.
*
* @module core/fragment
* @class fragment
* @package core
* @copyright 2016 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
define('core/fragment', ['jquery', 'core/ajax'], function($, ajax) {
/**
* Loads an HTML fragment through a callback.
*
* @method loadFragment
* @param {string} component Component where callback is located.
* @param {string} callback Callback function name.
* @param {integer} contextid Context ID of the fragment.
* @param {object} params Parameters for the callback.
* @return {Promise} JQuery promise object resolved when the fragment has been loaded.
*/
var loadFragment = function(component, callback, contextid, params) {
// Change params into required webservice format.
var formattedparams = [];
for (var index in params) {
formattedparams.push({
name: index,
value: params[index]
});
}
// Ajax stuff.
var deferred = $.Deferred();
var promises = ajax.call([{
methodname: 'core_get_fragment',
args: {
component: component,
callback: callback,
contextid: contextid,
args: formattedparams
}
}], false);
promises[0].done(function(data) {
deferred.resolve(data);
}).fail(function(ex) {
deferred.reject(ex);
});
return deferred.promise();
};
return /** @alias module:core/fragment */{
/**
* Appends HTML and JavaScript fragments to specified nodes.
* Callbacks called by this AMD module are responsible for doing the appropriate security checks
* to access the information that is returned. This only does minimal validation on the context.
*
* @method fragmentAppend
* @param {string} component Component where callback is located.
* @param {string} callback Callback function name.
* @param {integer} contextid Context ID of the fragment.
* @param {object} params Parameters for the callback.
* @return {Deferred} new promise that is resolved with the html and js.
*/
loadFragment: function(component, callback, contextid, params) {
var promise = $.Deferred();
$.when(loadFragment(component, callback, contextid, params)).then(function(data) {
var jsNodes = $(data.javascript);
var allScript = '';
jsNodes.each(function(index, scriptNode) {
scriptNode = $(scriptNode);
var tagName = scriptNode.prop('tagName');
if (tagName && (tagName.toLowerCase() == 'script')) {
if (scriptNode.attr('src')) {
// We only reload the script if it was not loaded already.
var exists = false;
$('script').each(function(index, s) {
if ($(s).attr('src') == scriptNode.attr('src')) {
exists = true;
}
return !exists;
});
if (!exists) {
allScript += ' { ';
allScript += ' node = document.createElement("script"); ';
allScript += ' node.type = "text/javascript"; ';
allScript += ' node.src = decodeURI("' + encodeURI(scriptNode.attr('src')) + '"); ';
allScript += ' document.getElementsByTagName("head")[0].appendChild(node); ';
allScript += ' } ';
}
} else {
allScript += ' ' + scriptNode.text();
}
}
});
promise.resolve(data.html, allScript);
}).fail(function(ex) {
promise.reject(ex);
});
return promise.promise();
}
};
});
// ---- /lib/amd/src/modal_registry.js ----
// 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 registry for the different types of modal.
*
* @module core/modal_registry
* @class modal_registry
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/modal_registry', ['core/notification'], function(Notification) {
// A singleton registry for all modules to access. Allows types to be
// added at runtime.
var registry = {};
/**
* Get a registered type of modal.
*
* @method get
* @param {string} type The type of modal to get
* @return {object} The registered config for the modal
*/
var get = function(type) {
return registry[type];
};
/**
* Register a modal with the registry.
*
* @method register
* @param {string} type The type of modal (must be unique)
* @param {function} module The modal module (must be a constructor function of type core/modal)
* @param {string} template The template name of the modal
*/
var register = function(type, module, template) {
if (get(type)) {
Notification.exception({message: "Modal of type '" + type + "' is already registered"});
}
if (!module || typeof module !== 'function') {
Notification.exception({message: "You must provide a modal module"});
}
if (!template) {
Notification.exception({message: "You must provide a modal template"});
}
registry[type] = {
module: module,
template: template,
};
};
return {
register: register,
get: get,
};
});
// ---- /lib/amd/src/icon_system.js ----
// 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 .
/**
* Icon System base module.
*
* @package core
* @copyright 2017 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/icon_system', ['jquery'], function($) {
/**
* Icon System abstract class.
*
* Any icon system needs to define a module extending this one and return this module name from the php icon_system class.
*/
var IconSystem = function() {
};
/**
* Initialise the icon system.
*
* @return {Promise}
* @method init
*/
IconSystem.prototype.init = function() {
return $.when(this);
};
/**
* Render an icon.
*
* The key, component and title come from either the pix mustache helper tag, or the call to templates.renderIcon.
* The template is the pre-loaded template string matching the template from getTemplateName() in this class.
* This function must return a string (not a promise) because it is used during the internal rendering of the mustache
* template (which is unfortunately synchronous). To render the mustache template in this function call
* core/mustache.render() directly and do not use any partials, blocks or helper functions in the template.
*
* @param {String} key
* @param {String} component
* @param {String} title
* @param {String} template
* @return {String}
* @method renderIcon
*/
IconSystem.prototype.renderIcon = function(key, component, title, template) { // eslint-disable-line no-unused-vars
throw new Error('Abstract function not implemented.');
};
/**
* getTemplateName
*
* @return {String}
* @method getTemplateName
*/
IconSystem.prototype.getTemplateName = function() {
throw new Error('Abstract function not implemented.');
};
return /** @alias module:core/icon_system */ IconSystem;
});
// ---- /lib/amd/src/icon_system_fontawesome.js ----
// 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 .
/**
* Competency rule points module.
*
* @package core
* @copyright 2017 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/icon_system_fontawesome', ['core/icon_system', 'jquery', 'core/ajax', 'core/mustache', 'core/localstorage', 'core/url'],
function(IconSystem, $, Ajax, Mustache, LocalStorage, Url) {
var staticMap = null;
var fetchMap = null;
/**
* IconSystemFontawesome
*/
var IconSystemFontawesome = function() {
IconSystem.apply(this, arguments);
};
IconSystemFontawesome.prototype = Object.create(IconSystem.prototype);
/**
* Prefetch resources so later calls to renderIcon can be resolved synchronously.
*
* @method init
* @return {Promise}
*/
IconSystemFontawesome.prototype.init = function() {
if (staticMap) {
return $.when(this);
}
var map = LocalStorage.get('core/iconmap-fontawesome');
if (map) {
map = JSON.parse(map);
}
if (map) {
staticMap = map;
return $.when(this);
}
if (fetchMap === null) {
fetchMap = Ajax.call([{
methodname: 'core_output_load_fontawesome_icon_map',
args: []
}], true, false)[0];
}
return fetchMap.then(function(map) {
staticMap = {};
$.each(map, function(index, value) {
staticMap[value.component + '/' + value.pix] = value.to;
});
LocalStorage.set('core/iconmap-fontawesome', JSON.stringify(staticMap));
return this;
}.bind(this));
};
/**
* Render an icon.
*
* @param {String} key
* @param {String} component
* @param {String} title
* @param {String} template
* @return {String}
* @method renderIcon
*/
IconSystemFontawesome.prototype.renderIcon = function(key, component, title, template) {
var mappedIcon = staticMap[component + '/' + key];
var unmappedIcon = false;
if (typeof mappedIcon === "undefined") {
var url = Url.imageUrl(key, component);
unmappedIcon = {
attributes: [
{name: 'src', value: url},
{name: 'alt', value: title},
{name: 'title', value: title}
]
};
}
var context = {
key: mappedIcon,
title: title,
alt: title,
unmappedIcon: unmappedIcon
};
return Mustache.render(template, context);
};
/**
* Get the name of the template to pre-cache for this icon system.
*
* @return {String}
* @method getTemplateName
*/
IconSystemFontawesome.prototype.getTemplateName = function() {
return 'core/pix_icon_fontawesome';
};
return /** @alias module:core/icon_system_fontawesome */ IconSystemFontawesome;
});
// ---- /lib/amd/src/modal_events.js ----
// 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 .
/**
* Contain the events a modal can fire.
*
* @module core/modal_events
* @class modal_events
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/modal_events', [], function() {
return {
// Default events.
shown: 'modal:shown',
hidden: 'modal:hidden',
destroyed: 'modal:destroyed',
// ModalSaveCancel events.
save: 'modal-save-cancel:save',
cancel: 'modal-save-cancel:cancel',
// ModalConfirm events.
yes: 'modal-confirm:yes',
no: 'modal-confirm:no',
};
});
// ---- /lib/amd/src/key_codes.js ----
// 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 list of human readable names for the keycodes.
*
* @module core/key_codes
* @class key_codes
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.2
*/
define('core/key_codes', function() {
return /** @alias module:core/key_codes */ {
'tab': 9,
'enter': 13,
'escape': 27,
'space': 32,
'end': 35,
'home': 36,
'arrowLeft': 37,
'arrowUp': 38,
'arrowRight': 39,
'arrowDown': 40,
'8': 56,
'asterix': 106,
'pageUp': 33,
'pageDown': 34,
};
});
// ---- /lib/amd/src/custom_interaction_events.js ----
// 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 .
/**
* This module provides a wrapper to encapsulate a lot of the common combinations of
* user interaction we use in Moodle.
*
* @module core/custom_interaction_events
* @class custom_interaction_events
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.2
*/
define('core/custom_interaction_events', ['jquery', 'core/key_codes'], function($, keyCodes) {
// The list of events provided by this module. Namespaced to avoid clashes.
var events = {
activate: 'cie:activate',
keyboardActivate: 'cie:keyboardactivate',
escape: 'cie:escape',
down: 'cie:down',
up: 'cie:up',
home: 'cie:home',
end: 'cie:end',
next: 'cie:next',
previous: 'cie:previous',
asterix: 'cie:asterix',
scrollLock: 'cie:scrollLock',
scrollTop: 'cie:scrollTop',
scrollBottom: 'cie:scrollBottom',
ctrlPageUp: 'cie:ctrlPageUp',
ctrlPageDown: 'cie:ctrlPageDown',
enter: 'cie:enter',
};
/**
* Check if the caller has asked for the given event type to be
* registered.
*
* @method shouldAddEvent
* @private
* @param {string} eventType name of the event (see events above)
* @param {array} include the list of events to be added
* @return {bool} true if the event should be added, false otherwise.
*/
var shouldAddEvent = function(eventType, include) {
include = include || [];
if (include.length && include.indexOf(eventType) !== -1) {
return true;
}
return false;
};
/**
* Check if any of the modifier keys have been pressed on the event.
*
* @method isModifierPressed
* @private
* @param {event} e jQuery event
* @return {bool} true if shift, meta (command on Mac), alt or ctrl are pressed
*/
var isModifierPressed = function(e) {
return (e.shiftKey || e.metaKey || e.altKey || e.ctrlKey);
};
/**
* Register a keyboard event that ignores modifier keys.
*
* @method addKeyboardEvent
* @private
* @param {object} element A jQuery object of the element to bind events to
* @param {string} event The custom interaction event name
* @param {int} keyCode The key code.
*/
var addKeyboardEvent = function(element, event, keyCode) {
element.off('keydown.' + event).on('keydown.' + event, function(e) {
if (!isModifierPressed(e)) {
if (e.keyCode == keyCode) {
$(e.target).trigger(event, [{originalEvent: e}]);
}
}
});
};
/**
* Trigger the activate event on the given element if it is clicked or the enter
* or space key are pressed without a modifier key.
*
* @method addActivateListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addActivateListener = function(element) {
element.off('click.cie.activate').on('click.cie.activate', function(e) {
$(e.target).trigger(events.activate, [{originalEvent: e}]);
});
element.off('keydown.cie.activate').on('keydown.cie.activate', function(e) {
if (!isModifierPressed(e)) {
if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) {
$(e.target).trigger(events.activate, [{originalEvent: e}]);
}
}
});
};
/**
* Trigger the keyboard activate event on the given element if the enter
* or space key are pressed without a modifier key.
*
* @method addKeyboardActivateListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addKeyboardActivateListener = function(element) {
element.off('keydown.cie.keyboardactivate').on('keydown.cie.keyboardactivate', function(e) {
if (!isModifierPressed(e)) {
if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) {
$(e.target).trigger(events.keyboardActivate, [{originalEvent: e}]);
}
}
});
};
/**
* Trigger the escape event on the given element if the escape key is pressed
* without a modifier key.
*
* @method addEscapeListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addEscapeListener = function(element) {
addKeyboardEvent(element, events.escape, keyCodes.escape);
};
/**
* Trigger the down event on the given element if the down arrow key is pressed
* without a modifier key.
*
* @method addDownListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addDownListener = function(element) {
addKeyboardEvent(element, events.down, keyCodes.arrowDown);
};
/**
* Trigger the up event on the given element if the up arrow key is pressed
* without a modifier key.
*
* @method addUpListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addUpListener = function(element) {
addKeyboardEvent(element, events.up, keyCodes.arrowUp);
};
/**
* Trigger the home event on the given element if the home key is pressed
* without a modifier key.
*
* @method addHomeListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addHomeListener = function(element) {
addKeyboardEvent(element, events.home, keyCodes.home);
};
/**
* Trigger the end event on the given element if the end key is pressed
* without a modifier key.
*
* @method addEndListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addEndListener = function(element) {
addKeyboardEvent(element, events.end, keyCodes.end);
};
/**
* Trigger the next event on the given element if the right arrow key is pressed
* without a modifier key in LTR mode or left arrow key in RTL mode.
*
* @method addNextListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addNextListener = function(element) {
// Left and right are flipped in RTL mode.
var keyCode = $('html').attr('dir') == "rtl" ? keyCodes.arrowLeft : keyCodes.arrowRight;
addKeyboardEvent(element, events.next, keyCode);
};
/**
* Trigger the previous event on the given element if the left arrow key is pressed
* without a modifier key in LTR mode or right arrow key in RTL mode.
*
* @method addPreviousListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addPreviousListener = function(element) {
// Left and right are flipped in RTL mode.
var keyCode = $('html').attr('dir') == "rtl" ? keyCodes.arrowRight : keyCodes.arrowLeft;
addKeyboardEvent(element, events.previous, keyCode);
};
/**
* Trigger the asterix event on the given element if the asterix key is pressed
* without a modifier key.
*
* @method addAsterixListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addAsterixListener = function(element) {
addKeyboardEvent(element, events.asterix, keyCodes.asterix);
};
/**
* Trigger the scrollTop event on the given element if the user scrolls to
* the top of the given element.
*
* @method addScrollTopListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addScrollTopListener = function(element) {
element.off('scroll.cie.scrollTop').on('scroll.cie.scrollTop', function(e) {
var scrollTop = element.scrollTop();
if (scrollTop === 0) {
element.trigger(events.scrollTop, [{originalEvent: e}]);
}
});
};
/**
* Trigger the scrollBottom event on the given element if the user scrolls to
* the bottom of the given element.
*
* @method addScrollBottomListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addScrollBottomListener = function(element) {
element.off('scroll.cie.scrollBottom').on('scroll.cie.scrollBottom', function(e) {
var scrollTop = element.scrollTop();
var innerHeight = element.innerHeight();
var scrollHeight = element[0].scrollHeight;
if (scrollTop + innerHeight >= scrollHeight) {
element.trigger(events.scrollBottom, [{originalEvent: e}]);
}
});
};
/**
* Trigger the scrollLock event on the given element if the user scrolls to
* the bottom or top of the given element.
*
* @method addScrollLockListener
* @private
* @param {jQuery} element jQuery object to add event listeners to
*/
var addScrollLockListener = function(element) {
// Lock mousewheel scrolling within the element to stop the annoying window scroll.
element.off('DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock')
.on('DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock', function(e) {
var scrollTop = element.scrollTop();
var scrollHeight = element[0].scrollHeight;
var height = element.height();
var delta = (e.type == 'DOMMouseScroll' ?
e.originalEvent.detail * -40 :
e.originalEvent.wheelDelta);
var up = delta > 0;
if (!up && -delta > scrollHeight - height - scrollTop) {
// Scrolling down past the bottom.
element.scrollTop(scrollHeight);
e.stopPropagation();
e.preventDefault();
e.returnValue = false;
// Fire the scroll lock event.
element.trigger(events.scrollLock, [{originalEvent: e}]);
return false;
} else if (up && delta > scrollTop) {
// Scrolling up past the top.
element.scrollTop(0);
e.stopPropagation();
e.preventDefault();
e.returnValue = false;
// Fire the scroll lock event.
element.trigger(events.scrollLock, [{originalEvent: e}]);
return false;
}
return true;
});
};
/**
* Trigger the ctrlPageUp event on the given element if the user presses the
* control and page up key.
*
* @method addCtrlPageUpListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addCtrlPageUpListener = function(element) {
element.off('keydown.cie.ctrlpageup').on('keydown.cie.ctrlpageup', function(e) {
if (e.ctrlKey) {
if (e.keyCode == keyCodes.pageUp) {
$(e.target).trigger(events.ctrlPageUp, [{originalEvent: e}]);
}
}
});
};
/**
* Trigger the ctrlPageDown event on the given element if the user presses the
* control and page down key.
*
* @method addCtrlPageDownListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addCtrlPageDownListener = function(element) {
element.off('keydown.cie.ctrlpagedown').on('keydown.cie.ctrlpagedown', function(e) {
if (e.ctrlKey) {
if (e.keyCode == keyCodes.pageDown) {
$(e.target).trigger(events.ctrlPageDown, [{originalEvent: e}]);
}
}
});
};
/**
* Trigger the enter event on the given element if the enter key is pressed
* without a modifier key.
*
* @method addEnterListener
* @private
* @param {object} element jQuery object to add event listeners to
*/
var addEnterListener = function(element) {
addKeyboardEvent(element, events.enter, keyCodes.enter);
};
/**
* Get the list of events and their handlers.
*
* @method getHandlers
* @private
* @return {object} object key of event names and value of handler functions
*/
var getHandlers = function() {
var handlers = {};
handlers[events.activate] = addActivateListener;
handlers[events.keyboardActivate] = addKeyboardActivateListener;
handlers[events.escape] = addEscapeListener;
handlers[events.down] = addDownListener;
handlers[events.up] = addUpListener;
handlers[events.home] = addHomeListener;
handlers[events.end] = addEndListener;
handlers[events.next] = addNextListener;
handlers[events.previous] = addPreviousListener;
handlers[events.asterix] = addAsterixListener;
handlers[events.scrollLock] = addScrollLockListener;
handlers[events.scrollTop] = addScrollTopListener;
handlers[events.scrollBottom] = addScrollBottomListener;
handlers[events.ctrlPageUp] = addCtrlPageUpListener;
handlers[events.ctrlPageDown] = addCtrlPageDownListener;
handlers[events.enter] = addEnterListener;
return handlers;
};
/**
* Add all of the listeners on the given element for the requested events.
*
* @method define
* @public
* @param {object} element the DOM element to register event listeners on
* @param {array} include the array of events to be triggered
*/
var define = function(element, include) {
element = $(element);
include = include || [];
if (!element.length || !include.length) {
return;
}
$.each(getHandlers(), function(eventType, handler) {
if (shouldAddEvent(eventType, include)) {
handler(element);
}
});
};
return /** @module core/custom_interaction_events */ {
define: define,
events: events,
};
});
// ---- /lib/amd/src/tree.js ----
// 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 .
/**
* Implement an accessible aria tree widget, from a nested unordered list.
* Based on http://oaa-accessibility.org/example/41/.
*
* @module tool_lp/tree
* @package core
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/tree', ['jquery'], function($) {
// Private variables and functions.
var SELECTORS = {
ITEM: '[role=treeitem]',
GROUP: '[role=treeitem]:has([role=group]), [role=treeitem][aria-owns], [role=treeitem][data-requires-ajax=true]',
CLOSED_GROUP: '[role=treeitem]:has([role=group])[aria-expanded=false], [role=treeitem][aria-owns][aria-expanded=false], ' +
'[role=treeitem][data-requires-ajax=true][aria-expanded=false]',
FIRST_ITEM: '[role=treeitem]:first',
VISIBLE_ITEM: '[role=treeitem]:visible',
UNLOADED_AJAX_ITEM: '[role=treeitem][data-requires-ajax=true][data-loaded=false][aria-expanded=true]'
};
/**
* Constructor.
*
* @param {String} selector
* @param {function} selectCallback Called when the active node is changed.
*/
var Tree = function(selector, selectCallback) {
this.treeRoot = $(selector);
this.treeRoot.data('activeItem', null);
this.selectCallback = selectCallback;
this.keys = {
tab: 9,
enter: 13,
space: 32,
pageup: 33,
pagedown: 34,
end: 35,
home: 36,
left: 37,
up: 38,
right: 39,
down: 40,
asterisk: 106
};
// Apply the standard default initialisation for all nodes, starting with the tree root.
this.initialiseNodes(this.treeRoot);
// Make the first item the active item for the tree so that it is added to the tab order.
this.setActiveItem(this.treeRoot.find(SELECTORS.FIRST_ITEM));
// Create the cache of the visible items.
this.refreshVisibleItemsCache();
// Create the event handlers for the tree.
this.bindEventHandlers();
};
/**
* Find all visible tree items and save a cache of them on the tree object.
*
* @method refreshVisibleItemsCache
*/
Tree.prototype.refreshVisibleItemsCache = function() {
this.treeRoot.data('visibleItems', this.treeRoot.find(SELECTORS.VISIBLE_ITEM));
};
/**
* Get all visible tree items.
*
* @method getVisibleItems
* @return {Object} visible items
*/
Tree.prototype.getVisibleItems = function() {
return this.treeRoot.data('visibleItems');
};
/**
* Mark the given item as active within the tree and fire the callback for when the active item is set.
*
* @method setActiveItem
* @param {object} item jquery object representing an item on the tree.
*/
Tree.prototype.setActiveItem = function(item) {
var currentActive = this.treeRoot.data('activeItem');
if (item === currentActive) {
return;
}
// Remove previous active from tab order.
if (currentActive) {
currentActive.attr('tabindex', '-1');
currentActive.attr('aria-selected', 'false');
}
item.attr('tabindex', '0');
item.attr('aria-selected', 'true');
// Set the new active item.
this.treeRoot.data('activeItem', item);
if (typeof this.selectCallback === 'function') {
this.selectCallback(item);
}
};
/**
* Determines if the given item is a group item (contains child tree items) in the tree.
*
* @method isGroupItem
* @param {object} item jquery object representing an item on the tree.
* @returns {bool}
*/
Tree.prototype.isGroupItem = function(item) {
return item.is(SELECTORS.GROUP);
};
/**
* Determines if the given item is a group item (contains child tree items) in the tree.
*
* @method isGroupItem
* @param {object} item jquery object representing an item on the tree.
* @returns {bool}
*/
Tree.prototype.getGroupFromItem = function(item) {
return this.treeRoot.find('#' + item.attr('aria-owns')) || item.children('[role=group]');
};
/**
* Determines if the given group item (contains child tree items) is collapsed.
*
* @method isGroupCollapsed
* @param {object} item jquery object representing a group item on the tree.
* @returns {bool}
*/
Tree.prototype.isGroupCollapsed = function(item) {
return item.attr('aria-expanded') === 'false';
};
/**
* Determines if the given group item (contains child tree items) can be collapsed.
*
* @method isGroupCollapsible
* @param {object} item jquery object representing a group item on the tree.
* @returns {bool}
*/
Tree.prototype.isGroupCollapsible = function(item) {
return item.attr('data-collapsible') !== 'false';
};
/**
* Performs the tree initialisation for all child items from the given node,
* such as removing everything from the tab order and setting aria selected
* on items.
*
* @method initialiseNodes
* @param {object} node jquery object representing a node.
*/
Tree.prototype.initialiseNodes = function(node) {
this.removeAllFromTabOrder(node);
this.setAriaSelectedFalseOnItems(node);
// Get all ajax nodes that have been rendered as expanded but haven't loaded the child items yet.
var thisTree = this;
node.find(SELECTORS.UNLOADED_AJAX_ITEM).each(function() {
var unloadedNode = $(this);
// Collapse and then expand to trigger the ajax loading.
thisTree.collapseGroup(unloadedNode);
thisTree.expandGroup(unloadedNode);
});
};
/**
* Removes all child DOM elements of the given node from the tab order.
*
* @method removeAllFromTabOrder
* @param {object} node jquery object representing a node.
*/
Tree.prototype.removeAllFromTabOrder = function(node) {
node.find('*').attr('tabindex', '-1');
this.getGroupFromItem($(node)).find('*').attr('tabindex', '-1');
};
/**
* Find all child tree items from the given node and set the aria selected attribute to false.
*
* @method setAriaSelectedFalseOnItems
* @param {object} node jquery object representing a node.
*/
Tree.prototype.setAriaSelectedFalseOnItems = function(node) {
node.find(SELECTORS.ITEM).attr('aria-selected', 'false');
};
/**
* Expand all group nodes within the tree.
*
* @method expandAllGroups
*/
Tree.prototype.expandAllGroups = function() {
var thisTree = this;
this.treeRoot.find(SELECTORS.CLOSED_GROUP).each(function() {
var groupNode = $(this);
thisTree.expandGroup($(this)).done(function() {
thisTree.expandAllChildGroups(groupNode);
});
});
};
/**
* Find all child group nodes from the given node and expand them.
*
* @method expandAllChildGroups
* @param {Object} item is the jquery id of the group.
*/
Tree.prototype.expandAllChildGroups = function(item) {
var thisTree = this;
this.getGroupFromItem(item).find(SELECTORS.CLOSED_GROUP).each(function() {
var groupNode = $(this);
thisTree.expandGroup($(this)).done(function() {
thisTree.expandAllChildGroups(groupNode);
});
});
};
/**
* Expand a collapsed group.
*
* Handles expanding nodes that are ajax loaded (marked with a data-requires-ajax attribute).
*
* @method expandGroup
* @param {Object} item is the jquery id of the parent item of the group.
* @return {Object} a promise that is resolved when the group has been expanded.
*/
Tree.prototype.expandGroup = function(item) {
var promise = $.Deferred();
// Ignore nodes that are explicitly maked as not expandable or are already expanded.
if (item.attr('data-expandable') !== 'false' && this.isGroupCollapsed(item)) {
// If this node requires ajax load and we haven't already loaded it.
if (item.attr('data-requires-ajax') === 'true' && item.attr('data-loaded') !== 'true') {
item.attr('data-loaded', false);
// Get the closes ajax loading module specificed in the tree.
var moduleName = item.closest('[data-ajax-loader]').attr('data-ajax-loader');
var thisTree = this;
// Flag this node as loading.
item.addClass('loading');
// Require the ajax module (must be AMD) and try to load the items.
require([moduleName], function(loader) {
// All ajax module must implement a "load" method.
loader.load(item).done(function() {
item.attr('data-loaded', true);
// Set defaults on the newly constructed part of the tree.
thisTree.initialiseNodes(item);
thisTree.finishExpandingGroup(item);
// Make sure no child elements of the item we just loaded are tabbable.
item.removeClass('loading');
promise.resolve();
});
});
} else {
this.finishExpandingGroup(item);
promise.resolve();
}
} else {
promise.resolve();
}
return promise;
};
/**
* Perform the necessary DOM changes to display a group item.
*
* @method finishExpandingGroup
* @param {Object} item is the jquery id of the parent item of the group.
*/
Tree.prototype.finishExpandingGroup = function(item) {
// Expand the group.
var group = this.getGroupFromItem(item);
group.attr('aria-hidden', 'false');
item.attr('aria-expanded', 'true');
// Update the list of visible items.
this.refreshVisibleItemsCache();
};
/**
* Collapse an expanded group.
*
* @method collapseGroup
* @param {Object} item is the jquery id of the parent item of the group.
*/
Tree.prototype.collapseGroup = function(item) {
// If the item is not collapsible or already collapsed then do nothing.
if (!this.isGroupCollapsible(item) || this.isGroupCollapsed(item)) {
return;
}
// Collapse the group.
var group = this.getGroupFromItem(item);
group.attr('aria-hidden', 'true');
item.attr('aria-expanded', 'false');
// Update the list of visible items.
this.refreshVisibleItemsCache();
};
/**
* Expand or collapse a group.
*
* @method toggleGroup
* @param {Object} item is the jquery id of the parent item of the group.
*/
Tree.prototype.toggleGroup = function(item) {
if (item.attr('aria-expanded') === 'true') {
this.collapseGroup(item);
} else {
this.expandGroup(item);
}
};
/**
* Handle a key down event - ie navigate the tree.
*
* @method handleKeyDown
* @param {Object} item is the jquery id of the parent item of the group.
* @param {Event} e The event.
* @return {Boolean}
*/
// This function should be simplified. In the meantime..
// eslint-disable-next-line complexity
Tree.prototype.handleKeyDown = function(item, e) {
var currentIndex = this.getVisibleItems().index(item);
if ((e.altKey || e.ctrlKey || e.metaKey) || (e.shiftKey && e.keyCode != this.keys.tab)) {
// Do nothing.
return true;
}
switch (e.keyCode) {
case this.keys.home: {
// Jump to first item in tree.
this.getVisibleItems().first().focus();
e.stopPropagation();
return false;
}
case this.keys.end: {
// Jump to last visible item.
this.getVisibleItems().last().focus();
e.stopPropagation();
return false;
}
case this.keys.enter: {
var links = item.children('a').length ? item.children('a') : item.children().not(SELECTORS.GROUP).find('a');
if (links.length) {
window.location.href = links.first().attr('href');
} else if (this.isGroupItem(item)) {
this.toggleGroup(item, true);
}
e.stopPropagation();
return false;
}
case this.keys.space: {
if (this.isGroupItem(item)) {
this.toggleGroup(item, true);
}
e.stopPropagation();
return false;
}
case this.keys.left: {
var focusParent = function(tree) {
// Get the immediate visible parent group item that contains this element.
tree.getVisibleItems().filter(function() {
return tree.getGroupFromItem($(this)).has(item).length;
}).focus();
};
// If this is a goup item then collapse it and focus the parent group
// in accordance with the aria spec.
if (this.isGroupItem(item)) {
if (this.isGroupCollapsed(item)) {
focusParent(this);
} else {
this.collapseGroup(item);
}
} else {
focusParent(this);
}
e.stopPropagation();
return false;
}
case this.keys.right: {
// If this is a group item then expand it and focus the first child item
// in accordance with the aria spec.
if (this.isGroupItem(item)) {
if (this.isGroupCollapsed(item)) {
this.expandGroup(item);
} else {
// Move to the first item in the child group.
this.getGroupFromItem(item).find(SELECTORS.ITEM).first().focus();
}
}
e.stopPropagation();
return false;
}
case this.keys.up: {
if (currentIndex > 0) {
var prev = this.getVisibleItems().eq(currentIndex - 1);
prev.focus();
}
e.stopPropagation();
return false;
}
case this.keys.down: {
if (currentIndex < this.getVisibleItems().length - 1) {
var next = this.getVisibleItems().eq(currentIndex + 1);
next.focus();
}
e.stopPropagation();
return false;
}
case this.keys.asterisk: {
// Expand all groups.
this.expandAllGroups();
e.stopPropagation();
return false;
}
}
return true;
};
/**
* Handle a click (select).
*
* @method handleClick
* @param {Object} item The jquery id of the parent item of the group.
* @param {Event} e The event.
* @return {Boolean}
*/
Tree.prototype.handleClick = function(item, e) {
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
// Do nothing.
return true;
}
// Update the active item.
item.focus();
// If the item is a group node.
if (this.isGroupItem(item)) {
this.toggleGroup(item);
}
e.stopPropagation();
return true;
};
/**
* Handle a focus event.
*
* @method handleFocus
* @param {Object} item The jquery id of the parent item of the group.
* @param {Event} e The event.
* @return {Boolean}
*/
Tree.prototype.handleFocus = function(item, e) {
this.setActiveItem(item);
e.stopPropagation();
return true;
};
/**
* Bind the event listeners we require.
*
* @method bindEventHandlers
*/
Tree.prototype.bindEventHandlers = function() {
var thisObj = this;
// Bind event handlers to the tree items. Use event delegates to allow
// for dynamically loaded parts of the tree.
this.treeRoot.on({
click: function(e) {
return thisObj.handleClick($(this), e);
},
keydown: function(e) {
return thisObj.handleKeyDown($(this), e);
},
focus: function(e) {
return thisObj.handleFocus($(this), e);
},
}, SELECTORS.ITEM);
};
return /** @alias module:tool_lp/tree */ Tree;
});
// ---- /lib/amd/src/chart_builder.js ----
// 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 .
/**
* Chart builder.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/chart_builder', ['jquery'], function($) {
/**
* Chart builder.
*
* @exports core/chart_builder
*/
var module = {
/**
* Make a chart instance.
*
* This takes data, most likely generated in PHP, and creates a chart instance from it
* deferring most of the logic to {@link module:core/chart_base.create}.
*
* @param {Object} data The data.
* @return {Promise} A promise resolved with the chart instance.
*/
make: function(data) {
var deferred = $.Deferred();
require(['core/chart_' + data.type], function(Klass) {
var instance = Klass.prototype.create(Klass, data);
deferred.resolve(instance);
});
return deferred.promise();
}
};
return module;
});
// ---- /lib/amd/src/inplace_editable.js ----
// 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 .
/**
* AJAX helper for the inline editing a value.
*
* This script is automatically included from template core/inplace_editable
* It registers a click-listener on [data-inplaceeditablelink] link (the "inplace edit" icon),
* then replaces the displayed value with an input field. On "Enter" it sends a request
* to web service core_update_inplace_editable, which invokes the specified callback.
* Any exception thrown by the web service (or callback) is displayed as an error popup.
*
* @module core/inplace_editable
* @package core
* @copyright 2016 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
define('core/inplace_editable', ['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str', 'core/config', 'core/url'],
function($, ajax, templates, notification, str, cfg, url) {
$('body').on('click keypress', '[data-inplaceeditable] [data-inplaceeditablelink]', function(e) {
if (e.type === 'keypress' && e.keyCode !== 13) {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
var target = $(this),
mainelement = target.closest('[data-inplaceeditable]');
var addSpinner = function(element) {
element.addClass('updating');
var spinner = element.find('img.spinner');
if (spinner.length) {
spinner.show();
} else {
spinner = $(' ')
.attr('src', url.imageUrl('i/loading_small'))
.addClass('spinner').addClass('smallicon')
;
element.append(spinner);
}
};
var removeSpinner = function(element) {
element.removeClass('updating');
element.find('img.spinner').hide();
};
var updateValue = function(mainelement, value) {
var pendingId = [
mainelement.attr('data-itemid'),
mainelement.attr('data-component'),
mainelement.attr('data-itemtype'),
].join('-');
M.util.js_pending(pendingId);
addSpinner(mainelement);
ajax
.call([{
methodname: 'core_update_inplace_editable',
args: {
itemid: mainelement.attr('data-itemid'),
component: mainelement.attr('data-component'),
itemtype: mainelement.attr('data-itemtype'),
value: value
},
done: function(data) {
var oldvalue = mainelement.attr('data-value');
templates.render('core/inplace_editable', data).done(function(html, js) {
var newelement = $(html);
templates.replaceNode(mainelement, newelement, js);
newelement.find('[data-inplaceeditablelink]').focus();
newelement.trigger({type: 'updated', ajaxreturn: data, oldvalue: oldvalue});
M.util.js_complete(pendingId);
});
},
fail: function(ex) {
var e = $.Event('updatefailed', {
exception: ex,
newvalue: value
});
removeSpinner(mainelement);
M.util.js_complete(pendingId);
mainelement.trigger(e);
if (!e.isDefaultPrevented()) {
notification.exception(ex);
}
}
}], true);
};
var turnEditingOff = function(el) {
el.find('input').off();
el.find('select').off();
el.html(el.attr('data-oldcontent'));
el.removeAttr('data-oldcontent');
el.removeClass('inplaceeditingon');
el.find('[data-inplaceeditablelink]').focus();
};
var turnEditingOffEverywhere = function() {
$('span.inplaceeditable.inplaceeditingon').each(function() {
turnEditingOff($(this));
});
};
var uniqueId = function(prefix, idlength) {
var uniqid = prefix,
i;
for (i = 0; i < idlength; i++) {
uniqid += String(Math.floor(Math.random() * 10));
}
// Make sure this ID is not already taken by an existing element.
if ($("#" + uniqid).length === 0) {
return uniqid;
}
return uniqueId(prefix, idlength);
};
var turnEditingOnText = function(el) {
str.get_string('edittitleinstructions').done(function(s) {
var instr = $('' + s + ' ').
attr('id', uniqueId('id_editinstructions_', 20)),
inputelement = $(' ').
attr('id', uniqueId('id_inplacevalue_', 20)).
attr('value', el.attr('data-value')).
attr('aria-describedby', instr.attr('id')).
addClass('ignoredirty').
addClass('form-control'),
lbl = $('' + mainelement.attr('data-editlabel') + ' ').
attr('for', inputelement.attr('id'));
el.html('').append(instr).append(lbl).append(inputelement);
inputelement.focus();
inputelement.select();
inputelement.on('keyup keypress focusout', function(e) {
if (cfg.behatsiterunning && e.type === 'focusout') {
// Behat triggers focusout too often.
return;
}
if (e.type === 'keypress' && e.keyCode === 13) {
// We need 'keypress' event for Enter because keyup/keydown would catch Enter that was
// pressed in other fields.
var val = inputelement.val();
turnEditingOff(el);
updateValue(el, val);
}
if ((e.type === 'keyup' && e.keyCode === 27) || e.type === 'focusout') {
// We need 'keyup' event for Escape because keypress does not work with Escape.
turnEditingOff(el);
}
});
});
};
var turnEditingOnToggle = function(el, newvalue) {
turnEditingOff(el);
updateValue(el, newvalue);
};
var turnEditingOnSelect = function(el, options) {
var i,
inputelement = $(' ').
attr('id', uniqueId('id_inplacevalue_', 20)).
addClass('custom-select'),
lbl = $('' + mainelement.attr('data-editlabel') + ' ')
.attr('for', inputelement.attr('id'));
for (i in options) {
inputelement
.append($('')
.attr('value', options[i].key)
.html(options[i].value));
}
inputelement.val(el.attr('data-value'));
el.html('')
.append(lbl)
.append(inputelement);
inputelement.focus();
inputelement.select();
inputelement.on('keyup change focusout', function(e) {
if (cfg.behatsiterunning && e.type === 'focusout') {
// Behat triggers focusout too often.
return;
}
if (e.type === 'change') {
var val = inputelement.val();
turnEditingOff(el);
updateValue(el, val);
}
if ((e.type === 'keyup' && e.keyCode === 27) || e.type === 'focusout') {
// We need 'keyup' event for Escape because keypress does not work with Escape.
turnEditingOff(el);
}
});
};
var turnEditingOn = function(el) {
el.addClass('inplaceeditingon');
el.attr('data-oldcontent', el.html());
var type = el.attr('data-type');
var options = el.attr('data-options');
if (type === 'toggle') {
turnEditingOnToggle(el, options);
} else if (type === 'select') {
turnEditingOnSelect(el, $.parseJSON(options));
} else {
turnEditingOnText(el);
}
};
// Turn editing on for the current element and register handler for Enter/Esc keys.
turnEditingOffEverywhere();
turnEditingOn(mainelement);
});
return {};
});
// ---- /lib/amd/src/form-autocomplete.js ----
// 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 .
/**
* Autocomplete wrapper for select2 library.
*
* @module core/form-autocomplete
* @class autocomplete
* @package core
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.0
*/
/* globals require: false */
define('core/form-autocomplete', ['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'], function($, log, str, templates, notification) {
// Private functions and variables.
/** @var {Object} KEYS - List of keycode constants. */
var KEYS = {
DOWN: 40,
ENTER: 13,
SPACE: 32,
ESCAPE: 27,
COMMA: 188,
UP: 38
};
/**
* Make an item in the selection list "active".
*
* @method activateSelection
* @private
* @param {Number} index The index in the current (visible) list of selection.
* @param {Object} state State variables for this autocomplete element.
*/
var activateSelection = function(index, state) {
// Find the elements in the DOM.
var selectionElement = $(document.getElementById(state.selectionId));
// Count the visible items.
var length = selectionElement.children('[aria-selected=true]').length;
// Limit the index to the upper/lower bounds of the list (wrap in both directions).
index = index % length;
while (index < 0) {
index += length;
}
// Find the specified element.
var element = $(selectionElement.children('[aria-selected=true]').get(index));
// Create an id we can assign to this element.
var itemId = state.selectionId + '-' + index;
// Deselect all the selections.
selectionElement.children().attr('data-active-selection', false).attr('id', '');
// Select only this suggestion and assign it the id.
element.attr('data-active-selection', true).attr('id', itemId);
// Tell the input field it has a new active descendant so the item is announced.
selectionElement.attr('aria-activedescendant', itemId);
};
/**
* Remove the given item from the list of selected things.
*
* @method deselectItem
* @private
* @param {Object} options Original options for this autocomplete element.
* @param {Object} state State variables for this autocomplete element.
* @param {Element} The item to be deselected.
* @param {Element} originalSelect The original select list.
*/
var deselectItem = function(options, state, item, originalSelect) {
var selectedItemValue = $(item).attr('data-value');
// We can only deselect items if this is a multi-select field.
if (options.multiple) {
// Look for a match, and toggle the selected property if there is a match.
originalSelect.children('option').each(function(index, ele) {
if ($(ele).attr('value') == selectedItemValue) {
$(ele).prop('selected', false);
// We remove newly created custom tags from the suggestions list when they are deselected.
if ($(ele).attr('data-iscustom')) {
$(ele).remove();
}
}
});
}
// Rerender the selection list.
updateSelectionList(options, state, originalSelect);
};
/**
* Make an item in the suggestions "active" (about to be selected).
*
* @method activateItem
* @private
* @param {Number} index The index in the current (visible) list of suggestions.
* @param {Object} state State variables for this instance of autocomplete.
*/
var activateItem = function(index, state) {
// Find the elements in the DOM.
var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Count the visible items.
var length = suggestionsElement.children('[aria-hidden=false]').length;
// Limit the index to the upper/lower bounds of the list (wrap in both directions).
index = index % length;
while (index < 0) {
index += length;
}
// Find the specified element.
var element = $(suggestionsElement.children('[aria-hidden=false]').get(index));
// Find the index of this item in the full list of suggestions (including hidden).
var globalIndex = $(suggestionsElement.children('[role=option]')).index(element);
// Create an id we can assign to this element.
var itemId = state.suggestionsId + '-' + globalIndex;
// Deselect all the suggestions.
suggestionsElement.children().attr('aria-selected', false).attr('id', '');
// Select only this suggestion and assign it the id.
element.attr('aria-selected', true).attr('id', itemId);
// Tell the input field it has a new active descendant so the item is announced.
inputElement.attr('aria-activedescendant', itemId);
// Scroll it into view.
var scrollPos = element.offset().top
- suggestionsElement.offset().top
+ suggestionsElement.scrollTop()
- (suggestionsElement.height() / 2);
suggestionsElement.animate({
scrollTop: scrollPos
}, 100);
};
/**
* Find the index of the current active suggestion, and activate the next one.
*
* @method activateNextItem
* @private
* @param {Object} state State variable for this auto complete element.
*/
var activateNextItem = function(state) {
// Find the list of suggestions.
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Find the active one.
var element = suggestionsElement.children('[aria-selected=true]');
// Find it's index.
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
// Activate the next one.
activateItem(current+1, state);
};
/**
* Find the index of the current active selection, and activate the previous one.
*
* @method activatePreviousSelection
* @private
* @param {Object} state State variables for this instance of autocomplete.
*/
var activatePreviousSelection = function(state) {
// Find the list of selections.
var selectionsElement = $(document.getElementById(state.selectionId));
// Find the active one.
var element = selectionsElement.children('[data-active-selection=true]');
if (!element) {
activateSelection(0, state);
return;
}
// Find it's index.
var current = selectionsElement.children('[aria-selected=true]').index(element);
// Activate the next one.
activateSelection(current-1, state);
};
/**
* Find the index of the current active selection, and activate the next one.
*
* @method activateNextSelection
* @private
* @param {Object} state State variables for this instance of autocomplete.
*/
var activateNextSelection = function(state) {
// Find the list of selections.
var selectionsElement = $(document.getElementById(state.selectionId));
// Find the active one.
var element = selectionsElement.children('[data-active-selection=true]');
if (!element) {
activateSelection(0, state);
return;
}
// Find it's index.
var current = selectionsElement.children('[aria-selected=true]').index(element);
// Activate the next one.
activateSelection(current+1, state);
};
/**
* Find the index of the current active suggestion, and activate the previous one.
*
* @method activatePreviousItem
* @private
* @param {Object} state State variables for this autocomplete element.
*/
var activatePreviousItem = function(state) {
// Find the list of suggestions.
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Find the active one.
var element = suggestionsElement.children('[aria-selected=true]');
// Find it's index.
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
// Activate the next one.
activateItem(current-1, state);
};
/**
* Close the list of suggestions.
*
* @method closeSuggestions
* @private
* @param {Object} state State variables for this autocomplete element.
*/
var closeSuggestions = function(state) {
// Find the elements in the DOM.
var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Announce the list of suggestions was closed, and read the current list of selections.
inputElement.attr('aria-expanded', false).attr('aria-activedescendant', state.selectionId);
// Hide the suggestions list (from screen readers too).
suggestionsElement.hide().attr('aria-hidden', true);
};
/**
* Rebuild the list of suggestions based on the current values in the select list, and the query.
*
* @method updateSuggestions
* @private
* @param {Object} options The original options for this autocomplete.
* @param {Object} state The state variables for this autocomplete.
* @param {String} query The current text for the search string.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
*/
var updateSuggestions = function(options, state, query, originalSelect) {
// Find the elements in the DOM.
var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Used to track if we found any visible suggestions.
var matchingElements = false;
// Options is used by the context when rendering the suggestions from a template.
var suggestions = [];
originalSelect.children('option').each(function(index, option) {
if ($(option).prop('selected') !== true) {
suggestions[suggestions.length] = { label: option.innerHTML, value: $(option).attr('value') };
}
});
// Re-render the list of suggestions.
var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase();
var context = $.extend({ options: suggestions}, options, state);
templates.render(
'core/form_autocomplete_suggestions',
context
).done(function(newHTML) {
// We have the new template, insert it in the page.
suggestionsElement.replaceWith(newHTML);
// Get the element again.
suggestionsElement = $(document.getElementById(state.suggestionsId));
// Show it if it is hidden.
suggestionsElement.show().attr('aria-hidden', false);
// For each option in the list, hide it if it doesn't match the query.
suggestionsElement.children().each(function(index, node) {
node = $(node);
if ((options.caseSensitive && node.text().indexOf(searchquery) > -1) ||
(!options.caseSensitive && node.text().toLocaleLowerCase().indexOf(searchquery) > -1)) {
node.show().attr('aria-hidden', false);
matchingElements = true;
} else {
node.hide().attr('aria-hidden', true);
}
});
// If we found any matches, show the list.
inputElement.attr('aria-expanded', true);
if (matchingElements) {
// We only activate the first item in the list if tags is false,
// because otherwise "Enter" would select the first item, instead of
// creating a new tag.
if (!options.tags) {
activateItem(0, state);
}
} else {
// Nothing matches. Tell them that.
str.get_string('nosuggestions', 'form').done(function(nosuggestionsstr) {
suggestionsElement.html(nosuggestionsstr);
});
}
}).fail(notification.exception);
};
/**
* Create a new item for the list (a tag).
*
* @method createItem
* @private
* @param {Object} options The original options for the autocomplete.
* @param {Object} state State variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
*/
var createItem = function(options, state, originalSelect) {
// Find the element in the DOM.
var inputElement = $(document.getElementById(state.inputId));
// Get the current text in the input field.
var query = inputElement.val();
var tags = query.split(',');
var found = false;
$.each(tags, function(tagindex, tag) {
// If we can only select one at a time, deselect any current value.
tag = tag.trim();
if (tag !== '') {
if (!options.multiple) {
originalSelect.children('option').prop('selected', false);
}
// Look for an existing option in the select list that matches this new tag.
originalSelect.children('option').each(function(index, ele) {
if ($(ele).attr('value') == tag) {
found = true;
$(ele).prop('selected', true);
}
});
// Only create the item if it's new.
if (!found) {
var option = $('');
option.append(tag);
option.attr('value', tag);
originalSelect.append(option);
option.prop('selected', true);
// We mark newly created custom options as we handle them differently if they are "deselected".
option.attr('data-iscustom', true);
}
}
});
updateSelectionList(options, state, originalSelect);
// Clear the input field.
inputElement.val('');
// Close the suggestions list.
closeSuggestions(state);
};
/**
* Update the element that shows the currently selected items.
*
* @method updateSelectionList
* @private
* @param {Object} options Original options for this autocomplete element.
* @param {Object} state State variables for this autocomplete element.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
*/
var updateSelectionList = function(options, state, originalSelect) {
// Build up a valid context to re-render the template.
var items = [];
var newSelection = $(document.getElementById(state.selectionId));
var activeId = newSelection.attr('aria-activedescendant');
var activeValue = false;
if (activeId) {
activeValue = $(document.getElementById(activeId)).attr('data-value');
}
originalSelect.children('option').each(function(index, ele) {
if ($(ele).prop('selected')) {
items.push( { label: $(ele).html(), value: $(ele).attr('value') } );
}
});
var context = $.extend({ items: items }, options, state);
// Render the template.
templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
// Add it to the page.
newSelection.empty().append($(newHTML).html());
if (activeValue !== false) {
// Reselect any previously selected item.
newSelection.children('[aria-selected=true]').each(function(index, ele) {
if ($(ele).attr('data-value') === activeValue) {
activateSelection(index, state);
}
});
}
}).fail(notification.exception);
// Because this function get's called after changing the selection, this is a good place
// to trigger a change notification.
originalSelect.change();
};
/**
* Select the currently active item from the suggestions list.
*
* @method selectCurrentItem
* @private
* @param {Object} options The original options for the autocomplete.
* @param {Object} state State variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
*/
var selectCurrentItem = function(options, state, originalSelect) {
// Find the elements in the page.
var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Here loop through suggestions and set val to join of all selected items.
var selectedItemValue = suggestionsElement.children('[aria-selected=true]').attr('data-value');
// The select will either be a single or multi select, so the following will either
// select one or more items correctly.
// Take care to use 'prop' and not 'attr' for selected properties.
// If only one can be selected at a time, start by deselecting everything.
if (!options.multiple) {
originalSelect.children('option').prop('selected', false);
}
// Look for a match, and toggle the selected property if there is a match.
originalSelect.children('option').each(function(index, ele) {
if ($(ele).attr('value') == selectedItemValue) {
$(ele).prop('selected', true);
}
});
// Rerender the selection list.
updateSelectionList(options, state, originalSelect);
// Clear the input element.
inputElement.val('');
// Close the list of suggestions.
closeSuggestions(state);
};
/**
* Fetch a new list of options via ajax.
*
* @method updateAjax
* @private
* @param {Event} e The event that triggered this update.
* @param {Object} options The original options for the autocomplete.
* @param {Object} state The state variables for the autocomplete.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
* @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results.
*/
var updateAjax = function(e, options, state, originalSelect, ajaxHandler) {
// Get the query to pass to the ajax function.
var query = $(e.currentTarget).val();
// Call the transport function to do the ajax (name taken from Select2).
ajaxHandler.transport(options.selector, query, function(results) {
// We got a result - pass it through the translator before using it.
var processedResults = ajaxHandler.processResults(options.selector, results);
var existingValues = [];
// Now destroy all options that are not currently selected.
originalSelect.children('option').each(function(optionIndex, option) {
option = $(option);
if (!option.prop('selected')) {
option.remove();
} else {
existingValues.push(option.attr('value'));
}
});
// And add all the new ones returned from ajax.
$.each(processedResults, function(resultIndex, result) {
if (existingValues.indexOf(result.value) === -1) {
var option = $(' ');
option.append(result.label);
option.attr('value', result.value);
originalSelect.append(option);
}
});
// Update the list of suggestions now from the new values in the select list.
updateSuggestions(options, state, '', originalSelect);
}, notification.exception);
};
/**
* Add all the event listeners required for keyboard nav, blur clicks etc.
*
* @method addNavigation
* @private
* @param {Object} options The options used to create this autocomplete element.
* @param {Object} state State variables for this autocomplete element.
* @param {JQuery} originalSelect The JQuery object matching the hidden select list.
*/
var addNavigation = function(options, state, originalSelect) {
// Start with the input element.
var inputElement = $(document.getElementById(state.inputId));
// Add keyboard nav with keydown.
inputElement.on('keydown', function(e) {
switch (e.keyCode) {
case KEYS.DOWN:
// If the suggestion list is open, move to the next item.
if (!options.showSuggestions) {
// Do not consume this event.
return true;
} else if (inputElement.attr('aria-expanded') === "true") {
activateNextItem(state);
} else {
// Handle ajax population of suggestions.
if (!inputElement.val() && options.ajax) {
require([options.ajax], function(ajaxHandler) {
updateAjax(e, options, state, originalSelect, ajaxHandler);
});
} else {
// Else - open the suggestions list.
updateSuggestions(options, state, inputElement.val(), originalSelect);
}
}
// We handled this event, so prevent it.
e.preventDefault();
return false;
case KEYS.COMMA:
if (options.tags) {
// If we are allowing tags, comma should create a tag (or enter).
createItem(options, state, originalSelect);
}
// We handled this event, so prevent it.
e.preventDefault();
return false;
case KEYS.UP:
// Choose the previous active item.
activatePreviousItem(state);
// We handled this event, so prevent it.
e.preventDefault();
return false;
case KEYS.ENTER:
var suggestionsElement = $(document.getElementById(state.suggestionsId));
if ((inputElement.attr('aria-expanded') === "true") &&
(suggestionsElement.children('[aria-selected=true]').length > 0)) {
// If the suggestion list has an active item, select it.
selectCurrentItem(options, state, originalSelect);
} else if (options.tags) {
// If tags are enabled, create a tag.
createItem(options, state, originalSelect);
}
// We handled this event, so prevent it.
e.preventDefault();
return false;
case KEYS.ESCAPE:
if (inputElement.attr('aria-expanded') === "true") {
// If the suggestion list is open, close it.
closeSuggestions(state);
}
// We handled this event, so prevent it.
e.preventDefault();
return false;
}
return true;
});
// Handler used to force set the value from behat.
inputElement.on('behat:set-value', function() {
if (options.tags) {
createItem(options, state, originalSelect);
}
});
inputElement.on('blur', function() {
window.setTimeout(function() {
// Get the current element with focus.
var focusElement = $(document.activeElement);
// Only close the menu if the input hasn't regained focus.
if (focusElement.attr('id') != inputElement.attr('id')) {
if (options.tags) {
createItem(options, state, originalSelect);
}
closeSuggestions(state);
}
}, 500);
});
if (options.showSuggestions) {
var arrowElement = $(document.getElementById(state.downArrowId));
arrowElement.on('click', function() {
// Prevent the close timer, or we will open, then close the suggestions.
inputElement.focus();
// Show the suggestions list.
updateSuggestions(options, state, inputElement.val(), originalSelect);
});
}
var suggestionsElement = $(document.getElementById(state.suggestionsId));
suggestionsElement.parent().on('click', '[role=option]', function(e) {
// Handle clicks on suggestions.
var element = $(e.currentTarget).closest('[role=option]');
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Find the index of the clicked on suggestion.
var current = suggestionsElement.children('[aria-hidden=false]').index(element);
// Activate it.
activateItem(current, state);
// And select it.
selectCurrentItem(options, state, originalSelect);
});
var selectionElement = $(document.getElementById(state.selectionId));
// Handle clicks on the selected items (will unselect an item).
selectionElement.on('click', '[role=listitem]', function(e) {
// Get the item that was clicked.
var item = $(e.currentTarget);
// Remove it from the selection.
deselectItem(options, state, item, originalSelect);
});
// Keyboard navigation for the selection list.
selectionElement.on('keydown', function(e) {
switch (e.keyCode) {
case KEYS.DOWN:
// Choose the next selection item.
activateNextSelection(state);
// We handled this event, so prevent it.
e.preventDefault();
return false;
case KEYS.UP:
// Choose the previous selection item.
activatePreviousSelection(state);
// We handled this event, so prevent it.
e.preventDefault();
return false;
case KEYS.SPACE:
case KEYS.ENTER:
// Get the item that is currently selected.
var selectedItem = $(document.getElementById(state.selectionId)).children('[data-active-selection=true]');
if (selectedItem) {
// Unselect this item.
deselectItem(options, state, selectedItem, originalSelect);
// We handled this event, so prevent it.
e.preventDefault();
}
return false;
}
return true;
});
// Whenever the input field changes, update the suggestion list.
if (options.showSuggestions) {
inputElement.on('input', function(e) {
var query = $(e.currentTarget).val();
var last = $(e.currentTarget).data('last-value');
// IE11 fires many more input events than required - even when the value has not changed.
// We need to only do this for real value changed events or the suggestions will be
// unclickable on IE11 (because they will be rebuilt before the click event fires).
// Note - because of this we cannot close the list when the query is empty or it will break
// on IE11.
if (last !== query) {
updateSuggestions(options, state, query, originalSelect);
}
$(e.currentTarget).data('last-value', query);
});
}
};
return /** @alias module:core/form-autocomplete */ {
// Public variables and functions.
/**
* Turn a boring select box into an auto-complete beast.
*
* @method enhance
* @param {string} select The selector that identifies the select box.
* @param {boolean} tags Whether to allow support for tags (can define new entries).
* @param {string} ajax Name of an AMD module to handle ajax requests. If specified, the AMD
* module must expose 2 functions "transport" and "processResults".
* These are modeled on Select2 see: https://select2.github.io/options.html#ajax
* @param {String} placeholder - The text to display before a selection is made.
* @param {Boolean} caseSensitive - If search has to be made case sensitive.
*/
enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions) {
// Set some default values.
var options = {
selector: selector,
tags: false,
ajax: false,
placeholder: placeholder,
caseSensitive: false,
showSuggestions: true
};
if (typeof tags !== "undefined") {
options.tags = tags;
}
if (typeof ajax !== "undefined") {
options.ajax = ajax;
}
if (typeof caseSensitive !== "undefined") {
options.caseSensitive = caseSensitive;
}
if (typeof showSuggestions !== "undefined") {
options.showSuggestions = showSuggestions;
}
// Look for the select element.
var originalSelect = $(selector);
if (!originalSelect) {
log.debug('Selector not found: ' + selector);
return false;
}
// Hide the original select.
originalSelect.hide().attr('aria-hidden', true);
// Find or generate some ids.
var state = {
selectId: originalSelect.attr('id'),
inputId: 'form_autocomplete_input-' + $.now(),
suggestionsId: 'form_autocomplete_suggestions-' + $.now(),
selectionId: 'form_autocomplete_selection-' + $.now(),
downArrowId: 'form_autocomplete_downarrow-' + $.now()
};
options.multiple = originalSelect.attr('multiple');
var originalLabel = $('[for=' + state.selectId + ']');
// Create the new markup and insert it after the select.
var suggestions = [];
originalSelect.children('option').each(function(index, option) {
suggestions[index] = { label: option.innerHTML, value: $(option).attr('value') };
});
// Render all the parts of our UI.
var context = $.extend({}, options, state);
context.options = suggestions;
context.items = [];
var renderInput = templates.render('core/form_autocomplete_input', context);
var renderDatalist = templates.render('core/form_autocomplete_suggestions', context);
var renderSelection = templates.render('core/form_autocomplete_selection', context);
$.when(renderInput, renderDatalist, renderSelection).done(function(input, suggestions, selection) {
// Add our new UI elements to the page.
originalSelect.after(suggestions);
originalSelect.after(input);
originalSelect.after(selection);
// Update the form label to point to the text input.
originalLabel.attr('for', state.inputId);
// Add the event handlers.
addNavigation(options, state, originalSelect);
var inputElement = $(document.getElementById(state.inputId));
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Hide the suggestions by default.
suggestionsElement.hide().attr('aria-hidden', true);
// If this field uses ajax, set it up.
if (options.ajax) {
require([options.ajax], function(ajaxHandler) {
var handler = function(e) {
updateAjax(e, options, state, originalSelect, ajaxHandler);
};
// Trigger an ajax update after the text field value changes.
inputElement.on("input keypress", handler);
var arrowElement = $(document.getElementById(state.downArrowId));
arrowElement.on("click", handler);
});
}
// Show the current values in the selection list.
updateSelectionList(options, state, originalSelect);
});
}
};
});
// ---- /lib/amd/src/tooltip.js ----
define('core/tooltip', ['jquery'], function($) {
/**
* Tooltip class.
*
* @param {String} selector The css selector for the node(s) to enhance with tooltips.
*/
var Tooltip = function(selector) {
// Tooltip code matches: http://www.w3.org/WAI/PF/aria-practices/#tooltip
this._regionSelector = selector;
// For each node matching the selector - find an aria-describedby attribute pointing to an role="tooltip" element.
$(this._regionSelector).each(function(index, element) {
var tooltipId = $(element).attr('aria-describedby');
if (tooltipId) {
var tooltipele = document.getElementById(tooltipId);
if (tooltipele) {
var correctRole = $(tooltipele).attr('role') == 'tooltip';
if (correctRole) {
$(tooltipele).hide();
// Ensure the trigger for the tooltip is keyboard focusable.
$(element).attr('tabindex', '0');
}
// Attach listeners.
$(element).on('focus', this._handleFocus.bind(this));
$(element).on('mouseover', this._handleMouseOver.bind(this));
$(element).on('mouseout', this._handleMouseOut.bind(this));
$(element).on('blur', this._handleBlur.bind(this));
$(element).on('keydown', this._handleKeyDown.bind(this));
}
}
}.bind(this));
};
/** @type {String} Selector for the page region containing the user navigation. */
Tooltip.prototype._regionSelector = null;
/**
* Find the tooltip referred to by this element and show it.
*
* @param {Event} e
*/
Tooltip.prototype._showTooltip = function(e) {
var triggerElement = $(e.target);
var tooltipId = triggerElement.attr('aria-describedby');
if (tooltipId) {
var tooltipele = $(document.getElementById(tooltipId));
tooltipele.show();
tooltipele.attr('aria-hidden', 'false');
if (!tooltipele.is('.tooltip')) {
// Change the markup to a bootstrap tooltip.
var inner = $('
');
inner.append(tooltipele.contents());
tooltipele.append(inner);
tooltipele.addClass('tooltip');
tooltipele.addClass('bottom');
tooltipele.append('
');
}
var pos = triggerElement.offset();
pos.top += triggerElement.height() + 10;
$(tooltipele).offset(pos);
}
};
/**
* Find the tooltip referred to by this element and hide it.
*
* @param {Event} e
*/
Tooltip.prototype._hideTooltip = function(e) {
var triggerElement = $(e.target);
var tooltipId = triggerElement.attr('aria-describedby');
if (tooltipId) {
var tooltipele = document.getElementById(tooltipId);
$(tooltipele).hide();
$(tooltipele).attr('aria-hidden', 'true');
}
};
/**
* Listener for focus events.
* @param {Event} e
*/
Tooltip.prototype._handleFocus = function(e) {
this._showTooltip(e);
};
/**
* Listener for keydown events.
* @param {Event} e
*/
Tooltip.prototype._handleKeyDown = function(e) {
if (e.which == 27) {
this._hideTooltip(e);
}
};
/**
* Listener for mouseover events.
* @param {Event} e
*/
Tooltip.prototype._handleMouseOver = function(e) {
this._showTooltip(e);
};
/**
* Listener for mouseout events.
* @param {Event} e
*/
Tooltip.prototype._handleMouseOut = function(e) {
var triggerElement = $(e.target);
if (!triggerElement.is(":focus")) {
this._hideTooltip(e);
}
};
/**
* Listener for blur events.
* @param {Event} e
*/
Tooltip.prototype._handleBlur = function(e) {
this._hideTooltip(e);
};
return Tooltip;
});
// ---- /lib/amd/src/url.js ----
// 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 .
/**
* URL utility functions.
*
* @module core/url
* @package core
* @class url
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
define('core/url', ['core/config'], function(config) {
return /** @alias module:core/url */ {
// Public variables and functions.
/**
* Generate a style tag referencing this sheet and add it to the head of the page.
*
* @method fileUrl
* @param {string} sheet The style sheet name. Must exist in the theme, or one of it's parents.
* @return {string}
*/
fileUrl: function(relativeScript, slashArg) {
var url = config.wwwroot + relativeScript;
// Force a /
if (slashArg.charAt(0) != '/') {
slashArg = '/' + slashArg;
}
if (config.slasharguments) {
url += slashArg;
} else {
url += '?file=' + encodeURIComponent(slashArg);
}
return url;
},
/**
* Take a path relative to the moodle basedir and do some fixing (see class moodle_url in php).
*
* @method relativeUrl
* @param {string} relativePath The path relative to the moodle basedir.
* @return {string}
*/
relativeUrl: function(relativePath) {
if (relativePath.indexOf('http:') === 0 || relativePath.indexOf('https:') === 0 || relativePath.indexOf('://') >= 0) {
throw new Error('relativeUrl function does not accept absolute urls');
}
// Fix non-relative paths;
if (relativePath.charAt(0) != '/') {
relativePath = '/' + relativePath;
}
// Fix admin urls.
if (config.admin !== 'admin') {
relativePath = relativePath.replace(/^\/admin\//, '/' + config.admin + '/');
}
return config.wwwroot + relativePath;
},
/**
* Wrapper for image_url function.
*
* @method imageUrl
* @param {string} imagename The image name (e.g. t/edit).
* @param {string} component The component (e.g. mod_feedback).
* @return {string}
*/
imageUrl: function(imagename, component) {
return M.util.image_url(imagename, component);
}
};
});
// ---- /lib/amd/src/backoff_timer.js ----
// 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 timer that will execute a callback with decreasing frequency. Useful for
* doing polling on the server without overwhelming it with requests.
*
* @module core/backoff_timer
* @class backoff_timer
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/backoff_timer', function() {
/**
* Constructor for the back off timer.
*
* @param {function} callback The function to execute after each tick
* @param {function} backoffFunction The function to determine what the next timeout value should be
*/
var BackoffTimer = function(callback, backoffFunction) {
this.callback = callback;
this.backOffFunction = backoffFunction;
};
/**
* @type {function} callback The function to execute after each tick
*/
BackoffTimer.prototype.callback = null;
/**
* @type {function} backoffFunction The function to determine what the next timeout value should be
*/
BackoffTimer.prototype.backOffFunction = null;
/**
* @type {int} time The timeout value to use
*/
BackoffTimer.prototype.time = null;
/**
* @type {numeric} timeout The timeout identifier
*/
BackoffTimer.prototype.timeout = null;
/**
* Generate the next timeout in the back off time sequence
* for the timer.
*
* The back off function is called to calculate the next value.
* It is given the current value and an array of all previous values.
*
* @method generateNextTime
* @return {int} The new timeout value (in milliseconds)
*/
BackoffTimer.prototype.generateNextTime = function() {
var newTime = this.backOffFunction(this.time);
this.time = newTime;
return newTime;
};
/**
* Stop the current timer and clear the previous time values
*
* @method reset
* @return {object} this
*/
BackoffTimer.prototype.reset = function() {
this.time = null;
this.stop();
return this;
};
/**
* Clear the current timeout, if one is set.
*
* @method stop
* @return {object} this
*/
BackoffTimer.prototype.stop = function() {
if (this.timeout) {
window.clearTimeout(this.timeout);
this.timeout = null;
}
return this;
};
/**
* Start the current timer by generating the new timeout value and
* starting the ticks.
*
* This function recurses after each tick with a new timeout value
* generated each time.
*
* The callback function is called after each tick.
*
* @method start
* @return {object} this
*/
BackoffTimer.prototype.start = function() {
// If we haven't already started.
if (!this.timeout) {
var time = this.generateNextTime();
this.timeout = window.setTimeout(function() {
this.callback();
// Clear the existing timer.
this.stop();
// Start the next timer.
this.start();
}.bind(this), time);
}
return this;
};
/**
* Reset the timer and start it again from the initial timeout
* values
*
* @method restart
* @return {object} this
*/
BackoffTimer.prototype.restart = function() {
return this.reset().start();
};
/**
* Returns an incremental function for the timer.
*
* @param {int} minamount The minimum amount of time we wait before checking
* @param {int} incrementamount The amount to increment the timer by
* @param {int} maxamount The max amount to ever increment to
* @param {int} timeoutamount The timeout to use once we reach the max amount
* @return {function}
*/
BackoffTimer.getIncrementalCallback = function(minamount, incrementamount, maxamount, timeoutamount) {
/**
* An incremental function for the timer.
*
* @param {(int|null)} time The current timeout value or null if none set
* @return {int} The new timeout value
*/
return function(time) {
if (!time) {
return minamount;
}
// Don't go over the max amount.
if (time + incrementamount > maxamount) {
return timeoutamount;
}
return time + incrementamount;
};
};
return BackoffTimer;
});
// ---- /lib/amd/src/modal_save_cancel.js ----
// 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 .
/**
* Contain the logic for the save/cancel modal.
*
* @module core/modal_save_cancel
* @class modal_save_cancel
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/modal_save_cancel', ['jquery', 'core/notification', 'core/custom_interaction_events', 'core/modal', 'core/modal_events'],
function($, Notification, CustomEvents, Modal, ModalEvents) {
var SELECTORS = {
SAVE_BUTTON: '[data-action="save"]',
CANCEL_BUTTON: '[data-action="cancel"]',
};
/**
* Constructor for the Modal.
*
* @param {object} root The root jQuery element for the modal
*/
var ModalSaveCancel = function(root) {
Modal.call(this, root);
if (!this.getFooter().find(SELECTORS.SAVE_BUTTON).length) {
Notification.exception({message: 'No save button found'});
}
if (!this.getFooter().find(SELECTORS.CANCEL_BUTTON).length) {
Notification.exception({message: 'No cancel button found'});
}
};
ModalSaveCancel.prototype = Object.create(Modal.prototype);
ModalSaveCancel.prototype.constructor = ModalSaveCancel;
/**
* Override parent implementation to prevent changing the footer content.
*/
ModalSaveCancel.prototype.setFooter = function() {
Notification.exception({message: 'Can not change the footer of a save cancel modal'});
return;
};
/**
* Set up all of the event handling for the modal.
*
* @method registerEventListeners
*/
ModalSaveCancel.prototype.registerEventListeners = function() {
// Apply parent event listeners.
Modal.prototype.registerEventListeners.call(this);
this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
var saveEvent = $.Event(ModalEvents.save);
this.getRoot().trigger(saveEvent, this);
if (!saveEvent.isDefaultPrevented()) {
this.hide();
data.originalEvent.preventDefault();
}
}.bind(this));
this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, function(e, data) {
var cancelEvent = $.Event(ModalEvents.cancel);
this.getRoot().trigger(cancelEvent, this);
if (!cancelEvent.isDefaultPrevented()) {
this.hide();
data.originalEvent.preventDefault();
}
}.bind(this));
};
/**
* Allows to overwrite the text of "Save changes" button.
*
* @param {String} text
*/
ModalSaveCancel.prototype.setSaveButtonText = function(text) {
this.getFooter().find(SELECTORS.SAVE_BUTTON).text(text);
};
return ModalSaveCancel;
});
// ---- /lib/amd/src/config.js ----
// 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 .
/**
* Expose the M.cfg global variable.
*
* @module core/config
* @class config
* @package core
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
define('core/config', function() {
// This module exposes only the raw data from M.cfg;
return /** @alias module:core/config */ M.cfg;
});
// ---- /lib/amd/src/chart_line.js ----
// 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 .
/**
* Chart line.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @module core/chart_line
*/
define('core/chart_line', ['core/chart_base'], function(Base) {
/**
* Line chart.
*
* @alias module:core/chart_line
* @extends {module:core/chart_base}
* @class
*/
function Line() {
Base.prototype.constructor.apply(this, arguments);
}
Line.prototype = Object.create(Base.prototype);
/** @override */
Line.prototype.TYPE = 'line';
/**
* Whether the line should be smooth or not.
*
* By default the chart lines are not smooth.
*
* @type {Bool}
* @protected
*/
Line.prototype._smooth = false;
/** @override */
Line.prototype.create = function(Klass, data) {
var chart = Base.prototype.create.apply(this, arguments);
chart.setSmooth(data.smooth);
return chart;
};
/**
* Get whether the line should be smooth or not.
*
* @method getSmooth
* @returns {Bool}
*/
Line.prototype.getSmooth = function() {
return this._smooth;
};
/**
* Set whether the line should be smooth or not.
*
* @method setSmooth
* @param {Bool} smooth True if the line chart should be smooth, false otherwise.
*/
Line.prototype.setSmooth = function(smooth) {
this._smooth = Boolean(smooth);
};
return Line;
});
// ---- /lib/amd/src/mustache.js ----
// The MIT License
//
// Copyright (c) 2009 Chris Wanstrath (Ruby)
// Copyright (c) 2010-2014 Jan Lehnardt (JavaScript)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Description of import into Moodle:
// Checkout from https://github.com/moodle/custom-mustache.js
// Rebase onto latest release tag from https://github.com/janl/mustache.js
// Copy mustache.js into lib/amd/src/ in Moodle folder.
// Add the license as a comment to the file and these instructions.
// Add jshint tags so this file is not linted.
// Remove the "global define:" comment (hint for linter)
/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
/* jshint ignore:start */
(function defineMustache (global, factory) {
if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') {
factory(exports); // CommonJS
} else if (typeof define === 'function' && define.amd) {
define('core/mustache', ['exports'], factory); // AMD
} else {
global.Mustache = {};
factory(Mustache); // script, wsh, asp
}
}(this, function mustacheFactory (mustache) {
var objectToString = Object.prototype.toString;
var isArray = Array.isArray || function isArrayPolyfill (object) {
return objectToString.call(object) === '[object Array]';
};
function isFunction (object) {
return typeof object === 'function';
}
/**
* More correct typeof string handling array
* which normally returns typeof 'object'
*/
function typeStr (obj) {
return isArray(obj) ? 'array' : typeof obj;
}
function escapeRegExp (string) {
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
}
/**
* Null safe way of checking whether or not an object,
* including its prototype, has a given property
*/
function hasProperty (obj, propName) {
return obj != null && typeof obj === 'object' && (propName in obj);
}
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
// See https://github.com/janl/mustache.js/issues/189
var regExpTest = RegExp.prototype.test;
function testRegExp (re, string) {
return regExpTest.call(re, string);
}
var nonSpaceRe = /\S/;
function isWhitespace (string) {
return !testRegExp(nonSpaceRe, string);
}
var entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
function escapeHtml (string) {
return String(string).replace(/[&<>"'\/]/g, function fromEntityMap (s) {
return entityMap[s];
});
}
var whiteRe = /\s*/;
var spaceRe = /\s+/;
var equalsRe = /\s*=/;
var curlyRe = /\s*\}/;
var tagRe = /#|\^|\/|>|\{|&|=|!|\$|;
/**
* Breaks up the given `template` string into a tree of tokens. If the `tags`
* argument is given here it must be an array with two string values: the
* opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
* course, the default is to use mustaches (i.e. mustache.tags).
*
* A token is an array with at least 4 elements. The first element is the
* mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
* did not contain a symbol (i.e. {{myValue}}) this element is "name". For
* all text that appears outside a symbol this element is "text".
*
* The second element of a token is its "value". For mustache tags this is
* whatever else was inside the tag besides the opening symbol. For text tokens
* this is the text itself.
*
* The third and fourth elements of the token are the start and end indices,
* respectively, of the token in the original template.
*
* Tokens that are the root node of a subtree contain two more elements: 1) an
* array of tokens in the subtree and 2) the index in the original template at
* which the closing tag for that section begins.
*/
function parseTemplate (template, tags) {
if (!template)
return [];
var sections = []; // Stack to hold section tokens
var tokens = []; // Buffer to hold the tokens
var spaces = []; // Indices of whitespace tokens on the current line
var hasTag = false; // Is there a {{tag}} on the current line?
var nonSpace = false; // Is there a non-space char on the current line?
// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
function stripSpace () {
if (hasTag && !nonSpace) {
while (spaces.length)
delete tokens[spaces.pop()];
} else {
spaces = [];
}
hasTag = false;
nonSpace = false;
}
var openingTagRe, closingTagRe, closingCurlyRe;
function compileTags (tagsToCompile) {
if (typeof tagsToCompile === 'string')
tagsToCompile = tagsToCompile.split(spaceRe, 2);
if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
throw new Error('Invalid tags: ' + tagsToCompile);
openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
}
compileTags(tags || mustache.tags);
var scanner = new Scanner(template);
var start, type, value, chr, token, openSection;
while (!scanner.eos()) {
start = scanner.pos;
// Match any text between tags.
value = scanner.scanUntil(openingTagRe);
if (value) {
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
chr = value.charAt(i);
if (isWhitespace(chr)) {
spaces.push(tokens.length);
} else {
nonSpace = true;
}
tokens.push([ 'text', chr, start, start + 1 ]);
start += 1;
// Check for whitespace on the current line.
if (chr === '\n')
stripSpace();
}
}
// Match the opening tag.
if (!scanner.scan(openingTagRe))
break;
hasTag = true;
// Get the tag type.
type = scanner.scan(tagRe) || 'name';
scanner.scan(whiteRe);
// Get the tag value.
if (type === '=') {
value = scanner.scanUntil(equalsRe);
scanner.scan(equalsRe);
scanner.scanUntil(closingTagRe);
} else if (type === '{') {
value = scanner.scanUntil(closingCurlyRe);
scanner.scan(curlyRe);
scanner.scanUntil(closingTagRe);
type = '&';
} else {
value = scanner.scanUntil(closingTagRe);
}
// Match the closing tag.
if (!scanner.scan(closingTagRe))
throw new Error('Unclosed tag at ' + scanner.pos);
token = [ type, value, start, scanner.pos ];
tokens.push(token);
if (type === '#' || type === '^' || type === '$' || type === '<') {
sections.push(token);
} else if (type === '/') {
// Check section nesting.
openSection = sections.pop();
if (!openSection)
throw new Error('Unopened section "' + value + '" at ' + start);
if (openSection[1] !== value)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
} else if (type === 'name' || type === '{' || type === '&') {
nonSpace = true;
} else if (type === '=') {
// Set the tags for the next time around.
compileTags(value);
}
}
// Make sure there are no open sections when we're done.
openSection = sections.pop();
if (openSection)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
return nestTokens(squashTokens(tokens));
}
/**
* Combines the values of consecutive text tokens in the given `tokens` array
* to a single token.
*/
function squashTokens (tokens) {
var squashedTokens = [];
var token, lastToken;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
if (token) {
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
squashedTokens.push(token);
lastToken = token;
}
}
}
return squashedTokens;
}
/**
* Forms the given array of `tokens` into a nested tree structure where
* tokens that represent a section have two additional items: 1) an array of
* all tokens that appear in that section and 2) the index in the original
* template that represents the end of that section.
*/
function nestTokens (tokens) {
var nestedTokens = [];
var collector = nestedTokens;
var sections = [];
var token, section;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
case '$':
case '<':
case '#':
case '^':
collector.push(token);
sections.push(token);
collector = token[4] = [];
break;
case '/':
section = sections.pop();
section[5] = token[2];
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
}
/**
* A simple string scanner that is used by the template parser to find
* tokens in template strings.
*/
function Scanner (string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* Returns `true` if the tail is empty (end of string).
*/
Scanner.prototype.eos = function eos () {
return this.tail === '';
};
/**
* Tries to match the given regular expression at the current position.
* Returns the matched text if it can match, the empty string otherwise.
*/
Scanner.prototype.scan = function scan (re) {
var match = this.tail.match(re);
if (!match || match.index !== 0)
return '';
var string = match[0];
this.tail = this.tail.substring(string.length);
this.pos += string.length;
return string;
};
/**
* Skips all text until the given regular expression can be matched. Returns
* the skipped string, which is the entire tail if no match can be made.
*/
Scanner.prototype.scanUntil = function scanUntil (re) {
var index = this.tail.search(re), match;
switch (index) {
case -1:
match = this.tail;
this.tail = '';
break;
case 0:
match = '';
break;
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
this.pos += match.length;
return match;
};
/**
* Represents a rendering context by wrapping a view object and
* maintaining a reference to the parent context.
*/
function Context (view, parentContext) {
this.view = view;
this.blocks = {};
this.cache = { '.': this.view };
this.parent = parentContext;
}
/**
* Creates a new context using the given view with this context
* as the parent.
*/
Context.prototype.push = function push (view) {
return new Context(view, this);
};
/**
* Set a value in the current block context.
*/
Context.prototype.setBlockVar = function set (name, value) {
var blocks = this.blocks;
blocks[name] = value;
return value;
};
/**
* Clear all current block vars.
*/
Context.prototype.clearBlockVars = function clearBlockVars () {
this.blocks = {};
};
/**
* Get a value only from the current block context.
*/
Context.prototype.getBlockVar = function getBlockVar (name) {
var blocks = this.blocks;
var value;
if (blocks.hasOwnProperty(name)) {
value = blocks[name];
} else {
if (this.parent) {
value = this.parent.getBlockVar(name);
}
}
// Can return undefined.
return value;
};
/**
* Returns the value of the given name in this context, traversing
* up the context hierarchy if the value is absent in this context's view.
*/
Context.prototype.lookup = function lookup (name) {
var cache = this.cache;
var value;
if (cache.hasOwnProperty(name)) {
value = cache[name];
} else {
var context = this, names, index, lookupHit = false;
while (context) {
if (name.indexOf('.') > 0) {
value = context.view;
names = name.split('.');
index = 0;
/**
* Using the dot notion path in `name`, we descend through the
* nested objects.
*
* To be certain that the lookup has been successful, we have to
* check if the last object in the path actually has the property
* we are looking for. We store the result in `lookupHit`.
*
* This is specially necessary for when the value has been set to
* `undefined` and we want to avoid looking up parent contexts.
**/
while (value != null && index < names.length) {
if (index === names.length - 1)
lookupHit = hasProperty(value, names[index]);
value = value[names[index++]];
}
} else {
value = context.view[name];
lookupHit = hasProperty(context.view, name);
}
if (lookupHit)
break;
context = context.parent;
}
cache[name] = value;
}
if (isFunction(value))
value = value.call(this.view);
return value;
};
/**
* A Writer knows how to take a stream of tokens and render them to a
* string, given a context. It also maintains a cache of templates to
* avoid the need to parse the same template twice.
*/
function Writer () {
this.cache = {};
}
/**
* Clears all cached templates in this writer.
*/
Writer.prototype.clearCache = function clearCache () {
this.cache = {};
};
/**
* Parses and caches the given `template` and returns the array of tokens
* that is generated from the parse.
*/
Writer.prototype.parse = function parse (template, tags) {
var cache = this.cache;
var tokens = cache[template];
if (tokens == null)
tokens = cache[template] = parseTemplate(template, tags);
return tokens;
};
/**
* High-level method that is used to render the given `template` with
* the given `view`.
*
* The optional `partials` argument may be an object that contains the
* names and templates of partials that are used in the template. It may
* also be a function that is used to load partial templates on the fly
* that takes a single argument: the name of the partial.
*/
Writer.prototype.render = function render (template, view, partials) {
var tokens = this.parse(template);
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, partials, template);
};
/**
* Low-level method that renders the given array of `tokens` using
* the given `context` and `partials`.
*
* Note: The `originalTemplate` is only ever used to extract the portion
* of the original template that was contained in a higher-order section.
* If the template doesn't use higher-order sections, this argument may
* be omitted.
*/
Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate) {
var buffer = '';
var token, symbol, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
value = undefined;
token = tokens[i];
symbol = token[0];
if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate);
else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate);
else if (symbol === '>') value = this.renderPartial(token, context, partials, originalTemplate);
else if (symbol === '<') value = this.renderBlock(token, context, partials, originalTemplate);
else if (symbol === '$') value = this.renderBlockVariable(token, context, partials, originalTemplate);
else if (symbol === '&') value = this.unescapedValue(token, context);
else if (symbol === 'name') value = this.escapedValue(token, context);
else if (symbol === 'text') value = this.rawValue(token);
if (value !== undefined)
buffer += value;
}
return buffer;
};
Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
var self = this;
var buffer = '';
var value = context.lookup(token[1]);
// This function is used to render an arbitrary template
// in the current context by higher-order sections.
function subRender (template) {
return self.render(template, context, partials);
}
if (!value) return;
if (isArray(value)) {
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
}
} else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
} else if (isFunction(value)) {
if (typeof originalTemplate !== 'string')
throw new Error('Cannot use higher-order sections without the original template');
// Extract the portion of the original template that the section contains.
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
if (value != null)
buffer += value;
} else {
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
}
return buffer;
};
Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) {
var value = context.lookup(token[1]);
// Use JavaScript's definition of falsy. Include empty arrays.
// See https://github.com/janl/mustache.js/issues/186
if (!value || (isArray(value) && value.length === 0))
return this.renderTokens(token[4], context, partials, originalTemplate);
};
Writer.prototype.renderPartial = function renderPartial (token, context, partials) {
if (!partials) return;
var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
return this.renderTokens(this.parse(value), context, partials, value);
};
Writer.prototype.renderBlock = function renderBlock (token, context, partials, originalTemplate) {
if (!partials) return;
var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
// Ignore any wrongly set block vars before we started.
context.clearBlockVars();
// We are only rendering to record the default block variables.
this.renderTokens(token[4], context, partials, originalTemplate);
// Now we render and return the result.
var result = this.renderTokens(this.parse(value), context, partials, value);
// Don't leak the block variables outside this include.
context.clearBlockVars();
return result;
};
Writer.prototype.renderBlockVariable = function renderBlockVariable (token, context, partials, originalTemplate) {
var value = token[1];
var exists = context.getBlockVar(value);
if (!exists) {
context.setBlockVar(value, originalTemplate.slice(token[3], token[5]));
return this.renderTokens(token[4], context, partials, originalTemplate);
} else {
return this.renderTokens(this.parse(exists), context, partials, exists);
}
};
Writer.prototype.unescapedValue = function unescapedValue (token, context) {
var value = context.lookup(token[1]);
if (value != null)
return value;
};
Writer.prototype.escapedValue = function escapedValue (token, context) {
var value = context.lookup(token[1]);
if (value != null)
return mustache.escape(value);
};
Writer.prototype.rawValue = function rawValue (token) {
return token[1];
};
mustache.name = 'mustache.js';
mustache.version = '2.1.3';
mustache.tags = [ '{{', '}}' ];
// All high-level mustache.* functions use this writer.
var defaultWriter = new Writer();
/**
* Clears all cached templates in the default writer.
*/
mustache.clearCache = function clearCache () {
return defaultWriter.clearCache();
};
/**
* Parses and caches the given template in the default writer and returns the
* array of tokens it contains. Doing this ahead of time avoids the need to
* parse templates on the fly as they are rendered.
*/
mustache.parse = function parse (template, tags) {
return defaultWriter.parse(template, tags);
};
/**
* Renders the `template` with the given `view` and `partials` using the
* default writer.
*/
mustache.render = function render (template, view, partials) {
if (typeof template !== 'string') {
throw new TypeError('Invalid template! Template should be a "string" ' +
'but "' + typeStr(template) + '" was given as the first ' +
'argument for mustache#render(template, view, partials)');
}
return defaultWriter.render(template, view, partials);
};
// This is here for backwards compatibility with 0.4.x.,
/*eslint-disable */ // eslint wants camel cased function name
mustache.to_html = function to_html (template, view, partials, send) {
/*eslint-enable*/
var result = mustache.render(template, view, partials);
if (isFunction(send)) {
send(result);
} else {
return result;
}
};
// Export the escaping function so that the user may override it.
// See https://github.com/janl/mustache.js/issues/244
mustache.escape = escapeHtml;
// Export these mainly for testing, but also for advanced usage.
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
}));
/* jshint ignore:end */
// ---- /lib/amd/src/yui.js ----
// 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 .
/**
* Expose the global YUI variable. Note: This is only for scripts that are writing AMD
* wrappers for YUI functionality. This is not for plugins.
*
* @module core/yui
* @package core
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
define('core/yui', function() {
// This module exposes only the global yui instance.
return /** @alias module:core/yui */ Y;
});
// ---- /lib/amd/src/search-input.js ----
// 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 .
/**
* Search box.
*
* @module core/search-input
* @class search-input
* @package core
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.1
*/
define('core/search-input', ['jquery'], function($) {
/**
* This search box div node.
*
* @private
*/
var wrapper = null;
/**
* Toggles the form visibility.
*
* @param {Event} ev
* @method toggleForm
* @private
*/
var toggleForm = function(ev) {
if (wrapper.hasClass('expanded')) {
hideForm();
} else {
showForm(ev);
}
};
/**
* Shows the form or submits it depending on the window size.
*
* @param {Event} ev
* @method showForm
* @private
*/
var showForm = function(ev) {
var windowWidth = $(document).width();
// We are only interested in enter and space keys (accessibility).
if (ev.type === 'keydown' && ev.keyCode !== 13 && ev.keyCode !== 32) {
return;
}
if (windowWidth <= 767 && (ev.type === 'click' || ev.type === 'keydown')) {
// Move to the search page when using small window sizes as the input requires too much space.
submitForm();
return;
} else if (windowWidth <= 767) {
// Ignore mousedown events in while using small window sizes.
return;
}
if (ev.type === 'keydown') {
// We don't want to submit the form unless the user hits enter.
ev.preventDefault();
}
wrapper.addClass('expanded');
wrapper.find('form').addClass('expanded');
wrapper.find('input').focus();
};
/**
* Hides the form.
*
* @method hideForm
* @private
*/
var hideForm = function() {
wrapper.removeClass('expanded');
wrapper.find('form').removeClass('expanded');
};
/**
* Submits the form.
*
* @param {Event} ev
* @method submitForm
* @private
*/
var submitForm = function() {
wrapper.find('form').submit();
};
return /** @alias module:core/search-input */ {
// Public variables and functions.
/**
* Assigns listeners to the requested select box.
*
* @method init
* @param {Number} id The search wrapper div id
*/
init: function(id) {
wrapper = $('#' + id);
wrapper.on('click mouseover keydown', 'div', toggleForm);
}
};
});
// ---- /lib/amd/src/chart_base.js ----
// 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 .
/**
* Chart base.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @module core/chart_base
*/
define('core/chart_base', ['core/chart_series', 'core/chart_axis'], function(Series, Axis) {
/**
* Chart base.
*
* The constructor of a chart must never take any argument.
*
* {@link module:core/chart_base#_setDefault} to set the defaults on instantiation.
*
* @alias module:core/chart_base
* @class
*/
function Base() {
this._series = [];
this._labels = [];
this._xaxes = [];
this._yaxes = [];
this._setDefaults();
}
/**
* The series constituting this chart.
*
* @protected
* @type {module:core/chart_series[]}
*/
Base.prototype._series = null;
/**
* The labels of the X axis when categorised.
*
* @protected
* @type {String[]}
*/
Base.prototype._labels = null;
/**
* The title of the chart.
*
* @protected
* @type {String}
*/
Base.prototype._title = null;
/**
* The X axes.
*
* @protected
* @type {module:core/chart_axis[]}
*/
Base.prototype._xaxes = null;
/**
* The Y axes.
*
* @protected
* @type {module:core/chart_axis[]}
*/
Base.prototype._yaxes = null;
/**
* Colours to pick from when automatically assigning them.
*
* @const
* @type {String[]}
*/
Base.prototype.COLORSET = ['#f3c300', '#875692', '#f38400', '#a1caf1', '#be0032', '#c2b280', '#7f180d', '#008856',
'#e68fac', '#0067a5'];
/**
* Set of colours defined by setting $CFG->chart_colorset to be picked when automatically assigning them.
*
* @type {String[]}
* @protected
*/
Base.prototype._configColorSet = null;
/**
* The type of chart.
*
* @abstract
* @type {String}
* @const
*/
Base.prototype.TYPE = null;
/**
* Add a series to the chart.
*
* This will automatically assign a color to the series if it does not have one.
*
* @param {module:core/chart_series} series The series to add.
*/
Base.prototype.addSeries = function(series) {
this._validateSeries(series);
this._series.push(series);
// Give a default color from the set.
if (series.getColor() === null) {
var configColorSet = this.getConfigColorSet() || Base.prototype.COLORSET;
series.setColor(configColorSet[this._series.length % configColorSet.length]);
}
};
/**
* Create a new instance of a chart from serialised data.
*
* the serialised attributes they offer and support.
*
* @static
* @method create
* @param {module:core/chart_base} Klass The class oject representing the type of chart to instantiate.
* @param {Object} data The data of the chart.
* @return {module:core/chart_base}
*/
Base.prototype.create = function(Klass, data) {
// TODO Not convinced about the usage of Klass here but I can't figure out a way
// to have a reference to the class in the sub classes, in PHP I'd do new self().
var Chart = new Klass();
Chart.setConfigColorSet(data.config_colorset);
Chart.setLabels(data.labels);
Chart.setTitle(data.title);
data.series.forEach(function(seriesData) {
Chart.addSeries(Series.prototype.create(seriesData));
});
data.axes.x.forEach(function(axisData, i) {
Chart.setXAxis(Axis.prototype.create(axisData), i);
});
data.axes.y.forEach(function(axisData, i) {
Chart.setYAxis(Axis.prototype.create(axisData), i);
});
return Chart;
};
/**
* Get an axis.
*
* @private
* @param {String} xy Accepts the values 'x' or 'y'.
* @param {Number} [index=0] The index of the axis of its type.
* @param {Bool} [createIfNotExists=false] When true, create an instance if it does not exist.
* @return {module:core/chart_axis}
*/
Base.prototype.__getAxis = function(xy, index, createIfNotExists) {
var axes = xy === 'x' ? this._xaxes : this._yaxes,
setAxis = (xy === 'x' ? this.setXAxis : this.setYAxis).bind(this),
axis;
index = typeof index === 'undefined' ? 0 : index;
createIfNotExists = typeof createIfNotExists === 'undefined' ? false : createIfNotExists;
axis = axes[index];
if (typeof axis === 'undefined') {
if (!createIfNotExists) {
throw new Error('Unknown axis.');
}
axis = new Axis();
setAxis(axis, index);
}
return axis;
};
/**
* Get colours defined by setting.
*
* @return {String[]}
*/
Base.prototype.getConfigColorSet = function() {
return this._configColorSet;
};
/**
* Get the labels of the X axis.
*
* @return {String[]}
*/
Base.prototype.getLabels = function() {
return this._labels;
};
/**
* Get the series.
*
* @return {module:core/chart_series[]}
*/
Base.prototype.getSeries = function() {
return this._series;
};
/**
* Get the title of the chart.
*
* @return {String}
*/
Base.prototype.getTitle = function() {
return this._title;
};
/**
* Get the type of chart.
*
* @see module:core/chart_base#TYPE
* @return {String}
*/
Base.prototype.getType = function() {
if (!this.TYPE) {
throw new Error('The TYPE property has not been set.');
}
return this.TYPE;
};
/**
* Get the X axes.
*
* @return {module:core/chart_axis[]}
*/
Base.prototype.getXAxes = function() {
return this._xaxes;
};
/**
* Get an X axis.
*
* @param {Number} [index=0] The index of the axis.
* @param {Bool} [createIfNotExists=false] Create the instance of it does not exist at index.
* @return {module:core/chart_axis}
*/
Base.prototype.getXAxis = function(index, createIfNotExists) {
return this.__getAxis('x', index, createIfNotExists);
};
/**
* Get the Y axes.
*
* @return {module:core/chart_axis[]}
*/
Base.prototype.getYAxes = function() {
return this._yaxes;
};
/**
* Get an Y axis.
*
* @param {Number} [index=0] The index of the axis.
* @param {Bool} [createIfNotExists=false] Create the instance of it does not exist at index.
* @return {module:core/chart_axis}
*/
Base.prototype.getYAxis = function(index, createIfNotExists) {
return this.__getAxis('y', index, createIfNotExists);
};
/**
* Set colours defined by setting.
*
* @param {String[]} colorset An array of css colours.
* @protected
*/
Base.prototype.setConfigColorSet = function(colorset) {
this._configColorSet = colorset;
};
/**
* Set the defaults for this chart type.
*
* Child classes can extend this to set defaults values on instantiation.
*
* emphasize and self-document the defaults values set by the chart type.
*
* @protected
*/
Base.prototype._setDefaults = function() {
// For the children to extend.
};
/**
* Set the labels of the X axis.
*
* This requires for each series to contain strictly as many values as there
* are labels.
*
* @param {String[]} labels The labels.
*/
Base.prototype.setLabels = function(labels) {
if (labels.length && this._series.length && this._series[0].length != labels.length) {
throw new Error('Series must match label values.');
}
this._labels = labels;
};
/**
* Set the title of the chart.
*
* @param {String} title The title.
*/
Base.prototype.setTitle = function(title) {
this._title = title;
};
/**
* Set an X axis.
*
* Note that this will override any predefined axis without warning.
*
* @param {module:core/chart_axis} axis The axis.
* @param {Number} [index=0] The index of the axis.
*/
Base.prototype.setXAxis = function(axis, index) {
index = typeof index === 'undefined' ? 0 : index;
this._validateAxis('x', axis, index);
this._xaxes[index] = axis;
};
/**
* Set a Y axis.
*
* Note that this will override any predefined axis without warning.
*
* @param {module:core/chart_axis} axis The axis.
* @param {Number} [index=0] The index of the axis.
*/
Base.prototype.setYAxis = function(axis, index) {
index = typeof index === 'undefined' ? 0 : index;
this._validateAxis('y', axis, index);
this._yaxes[index] = axis;
};
/**
* Validate an axis.
*
* @protected
* @param {String} xy X or Y axis.
* @param {module:core/chart_axis} axis The axis to validate.
* @param {Number} [index=0] The index of the axis.
*/
Base.prototype._validateAxis = function(xy, axis, index) {
index = typeof index === 'undefined' ? 0 : index;
if (index > 0) {
var axes = xy == 'x' ? this._xaxes : this._yaxes;
if (typeof axes[index - 1] === 'undefined') {
throw new Error('Missing ' + xy + ' axis at index lower than ' + index);
}
}
};
/**
* Validate a series.
*
* @protected
* @param {module:core/chart_series} series The series to validate.
*/
Base.prototype._validateSeries = function(series) {
if (this._series.length && this._series[0].getCount() != series.getCount()) {
throw new Error('Series do not have an equal number of values.');
} else if (this._labels.length && this._labels.length != series.getCount()) {
throw new Error('Series must match label values.');
}
};
return Base;
});
// ---- /lib/amd/src/chart_pie.js ----
// 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 .
/**
* Chart pie.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @module core/chart_pie
*/
define('core/chart_pie', ['core/chart_base'], function(Base) {
/**
* Pie chart.
*
* @class
* @alias module:core/chart_pie
* @extends {module:core/chart_base}
*/
function Pie() {
Base.prototype.constructor.apply(this, arguments);
}
Pie.prototype = Object.create(Base.prototype);
/** @override */
Pie.prototype.TYPE = 'pie';
/**
* Whether the chart should be displayed as doughnut or not.
*
* @type {Bool}
* @protected
*/
Pie.prototype._doughnut = null;
/** @override */
Pie.prototype.create = function(Klass, data) {
var chart = Base.prototype.create.apply(this, arguments);
chart.setDoughnut(data.doughnut);
return chart;
};
/**
* Overridden to add appropriate colors to the series.
*
* @override
*/
Pie.prototype.addSeries = function(series) {
if (series.getColor() === null) {
var colors = [];
var configColorSet = this.getConfigColorSet() || Base.prototype.COLORSET;
for (var i = 0; i < series.getCount(); i++) {
colors.push(configColorSet[i % configColorSet.length]);
}
series.setColors(colors);
}
return Base.prototype.addSeries.apply(this, arguments);
};
/**
* Get whether the chart should be displayed as doughnut or not.
*
* @method getDoughnut
* @returns {Bool}
*/
Pie.prototype.getDoughnut = function() {
return this._doughnut;
};
/**
* Set whether the chart should be displayed as doughnut or not.
*
* @method setDoughnut
* @param {Bool} doughnut True for doughnut type, false for pie.
*/
Pie.prototype.setDoughnut = function(doughnut) {
this._doughnut = Boolean(doughnut);
};
/**
* Validate a series.
*
* Overrides parent implementation to validate that there is only
* one series per chart instance.
*
* @override
*/
Pie.prototype._validateSeries = function() {
if (this._series.length >= 1) {
throw new Error('Pie charts only support one serie.');
}
return Base.prototype._validateSeries.apply(this, arguments);
};
return Pie;
});
// ---- /lib/amd/src/chart_output_base.js ----
// 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 .
/**
* Chart output base.
*
* This takes a chart object and draws it.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @module core/chart_output_base
*/
define('core/chart_output_base', ['jquery'], function($) {
/**
* Chart output base.
*
* The constructor of an output class must instantly generate and display the
* chart. It is also the responsability of the output module to check that
* the node received is of the appropriate type, if not a new node can be
* added within.
*
* The output module has total control over the content of the node and can
* clear it or output anything to it at will. A node should not be shared by
* two simultaneous output modules.
*
* @class
* @alias module:core/chart_output_base
* @param {Node} node The node to output with/in.
* @param {Chart} chart A chart object.
*/
function Base(node, chart) {
this._node = $(node);
this._chart = chart;
}
/**
* Update method.
*
* This is the public method through which an output instance in informed
* that the chart instance has been updated and they need to update the
* chart rendering.
*
* @abstract
* @return {Void}
*/
Base.prototype.update = function() {
throw new Error('Not supported.');
};
return Base;
});
// ---- /lib/amd/src/permissionmanager.js ----
// 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 .
/*
* @package core
* @class permissionmanager
* @copyright 2015 Martin Mastny
* @since 3.0
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* @module admin/permissionmanager
*/
define('core/permissionmanager', ['jquery', 'core/config','core/notification', 'core/templates'], function($, config, notification, templates) {
/**
* Used CSS selectors
* @access private
*/
var SELECTORS = {
ADDROLE: 'a.allowlink, a.prohibitlink',
REMOVEROLE: 'a.preventlink, a.unprohibitlink',
UNPROHIBIT: 'a.unprohibitlink'
};
var rolesloadedevent = $.Event('rolesloaded');
var contextid;
var contextname;
var adminurl;
var overideableroles;
var panel = null;
/**
* Load all possible roles, which could be assigned from server
*
* @access private
* @method loadOverideableRoles
*/
var loadOverideableRoles = function() {
var params = {
contextid: contextid,
getroles: 1,
sesskey: config.sesskey
};
$.post(adminurl + 'roles/ajax.php', params, function() {})
.done(function(data) {
try {
overideableroles = data;
loadOverideableRoles = function() {
$('body').trigger(rolesloadedevent);
};
loadOverideableRoles();
}
catch(err) {
notification.exception(err);
}
})
.fail(function(jqXHR, status, error) {
notification.exception(error);
});
};
/**
* Perform the UI changes after server change
*
* @access private
* @method changePermissions
* @param {jquery node} row
* @param {int} roleid
* @param {string} action
*/
var changePermissions = function(row, roleid, action) {
var params = {
contextid: contextid,
roleid: roleid,
sesskey: M.cfg.sesskey,
action: action,
capability: row.data('name')
};
$.post(adminurl + 'roles/ajax.php', params, function() {})
.done(function(data) {
var action = data;
try {
var templatedata = {rolename: overideableroles[roleid],
roleid: roleid,
adminurl: adminurl,
imageurl: M.util.image_url('t/delete', 'moodle')
};
switch (action) {
case 'allow':
templatedata.spanclass = 'allowed';
templatedata.linkclass = 'preventlink';
templatedata.action = 'prevent';
break;
case 'prohibit':
templatedata.spanclass = 'forbidden';
templatedata.linkclass = 'unprohibitlink';
templatedata.action = 'unprohibit';
break;
case 'prevent':
row.find('a[data-role-id="' + roleid + '"]').first().closest('.allowed').remove();
return;
case 'unprohibit':
row.find('a[data-role-id="' + roleid + '"]').first().closest('.forbidden').remove();
return;
default:
return;
}
templates.render('core/permissionmanager_role',templatedata)
.done(function (content) {
if (action == 'allow'){
$(content).insertBefore(row.find('.allowmore:first'));
}else if (action == 'prohibit'){
$(content).insertBefore(row.find('.prohibitmore:first'));
// Remove allowed link
var allowedLink = row.find('.allowedroles').first().find('a[data-role-id="' + roleid + '"]');
if (allowedLink) {
allowedLink.first().closest('.allowed').remove();
}
}
panel.hide();
})
.fail(notification.exception);
}
catch(err) {
notification.exception(err);
}
})
.fail(function(jqXHR, status, error) {
notification.exception(error);
});
};
/**
* Prompts user for selecting a role which is permitted
*
* @access private
* @method handleAddRole
* @param {event} e
*/
var handleAddRole = function(e){
e.preventDefault();
$('body').one('rolesloaded', function() {
var link = $(e.currentTarget);
var action = link.data('action');
var row = link.closest('tr.rolecap');
var confirmationDetails = {
cap: row.data('humanname'),
context: contextname
};
var message = M.util.get_string('role' + action + 'info', 'core_role', confirmationDetails);
if (panel === null){
panel = new M.core.dialogue ({
draggable: true,
modal: true,
closeButton: true,
width: '450px'
});
}
panel.set('headerContent', M.util.get_string('role' + action + 'header', 'core_role'));
var i, existingrolelinks;
var roles = [];
switch (action){
case 'allow':
existingrolelinks = row.find(SELECTORS.REMOVEROLE);
break;
case 'prohibit':
existingrolelinks = row.find(SELECTORS.UNPROHIBIT);
break;
}
for (i in overideableroles) {
var disabled = '';
var disable = existingrolelinks.filter("[data-role-id='" + i + "']").length;
if (disable){
disabled = 'disabled';
}
var roledetails = {roleid:i, rolename: overideableroles[i], disabled:disabled};
roles.push(roledetails);
}
templates.render('core/permissionmanager_panelcontent',{message:message, roles:roles})
.done(function (content) {
panel.set('bodyContent', content);
panel.show();
$('div.role_buttons').delegate('input', 'click',function(e){
var roleid = $(e.currentTarget).data('role-id');
changePermissions(row, roleid, action);
});
})
.fail(notification.exception);
});
loadOverideableRoles();
};
/**
* Prompts user when removing permission
*
* @access private
* @method handleRemoveRole
* @param {event} e
*/
var handleRemoveRole = function(e){
e.preventDefault();
$('body').one('rolesloaded', function() {
var link = $(e.currentTarget);
var action = link.data('action');
var roleid = link.data('role-id');
var row = link.closest('tr.rolecap');
var questionDetails = {
role: overideableroles[roleid],
cap: row.data('humanname'),
context: contextname
};
notification.confirm(M.util.get_string('confirmunassigntitle', 'core_role'),
M.util.get_string('confirmrole' + action, 'core_role',questionDetails),
M.util.get_string('confirmunassignyes', 'core_role'),
M.util.get_string('confirmunassignno', 'core_role'),
function(){
changePermissions(row, roleid, action);
}
);
});
loadOverideableRoles();
};
return /** @alias module:core/permissionmanager */ {
/**
* Initialize permissionmanager
* @access public
* @param {int} contextid
* @param {string} contextname
* @param {string} adminurl
*/
initialize : function(args) {
contextid = args.contextid;
contextname = args.contextname;
adminurl = args.adminurl;
var body = $('body');
body.delegate(SELECTORS.ADDROLE, 'click', handleAddRole);
body.delegate(SELECTORS.REMOVEROLE, 'click', handleRemoveRole);
}
};
});
// ---- /lib/amd/src/log.js ----
// 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 .
/**
* This is an empty module, that is required before all other modules.
* Because every module is returned from a request for any other module, this
* forces the loading of all modules with a single request.
*
* @module core/log
* @package core
* @copyright 2015 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/log', ['core/loglevel'], function(log) {
var originalFactory = log.methodFactory;
log.methodFactory = function (methodName, logLevel) {
var rawMethod = originalFactory(methodName, logLevel);
return function (message, source) {
if (source) {
rawMethod(source + ": " + message);
} else {
rawMethod(message);
}
};
};
/**
* Set default config settings.
*
* @param {String} level The level to use.
* @method setConfig
*/
log.setConfig = function(config) {
if (typeof config.level !== "undefined") {
log.setLevel(config.level);
}
};
return log;
});
// ---- /lib/amd/src/chart_output.js ----
// 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 .
/**
* Chart output.
*
* Proxy to the default output module.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/chart_output', ['core/chart_output_chartjs'], function(Output) {
/**
* @exports module:core/chart_output
* @extends {module:core/chart_output_chartjs}
*/
var defaultModule = Output;
return defaultModule;
});
// ---- /lib/amd/src/chartjs.js ----
// 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 .
/**
* Chart.js loader.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/chartjs', ['core/chartjs-lazy'], function(ChartJS) {
return ChartJS;
});
// ---- /lib/amd/src/auto_rows.js ----
// 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 .
/**
* Enhance a textarea with auto growing rows to fit the content.
*
* @module core/auto_rows
* @class auto_rows
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.2
*/
define('core/auto_rows', ['jquery'], function($) {
var SELECTORS = {
ELEMENT: '[data-auto-rows]'
};
var EVENTS = {
ROW_CHANGE: 'autorows:rowchange',
};
/**
* Determine how many rows should be set for the given element.
*
* @method calculateRows
* @param {jQuery} element The textarea element
* @return {int} The number of rows for the element
* @private
*/
var calculateRows = function(element) {
var currentRows = element.attr('rows');
var maxRows = element.attr('data-max-rows');
var height = element.height();
var innerHeight = element.innerHeight();
var padding = innerHeight - height;
var scrollHeight = element[0].scrollHeight;
var rows = (scrollHeight - padding) / (height / currentRows);
// Remove the height styling to let the height be calculated automatically
// based on the row attribute.
element.css('height', '');
if (maxRows && rows >= maxRows) {
return maxRows;
} else {
return rows;
}
};
/**
* Add the event listeners for all text areas within the given element.
*
* @method init
* @param {jQuery|selector} root The container element of all enhanced text areas
* @public
*/
var init = function(root) {
$(root).on('input propertychange', SELECTORS.ELEMENT, function(e) {
var element = $(e.target);
var currentRows = element.attr('rows');
var rows = calculateRows(element);
if (rows != currentRows) {
element.attr('rows', rows);
$(root).trigger(EVENTS.ROW_CHANGE);
}
});
};
return /** @module core/auto_rows */ {
init: init,
events: EVENTS,
};
});
// ---- /lib/amd/src/ajax.js ----
// 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 .
/**
* Standard Ajax wrapper for Moodle. It calls the central Ajax script,
* which can call any existing webservice using the current session.
* In addition, it can batch multiple requests and return multiple responses.
*
* @module core/ajax
* @class ajax
* @package core
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
define('core/ajax', ['jquery', 'core/config'], function($, config) {
/**
* Success handler. Called when the ajax call succeeds. Checks each response and
* resolves or rejects the deferred from that request.
*
* @method requestSuccess
* @private
* @param {Object[]} responses Array of responses containing error, exception and data attributes.
*/
var requestSuccess = function(responses) {
// Call each of the success handlers.
var requests = this;
var exception = null;
var i = 0;
var request;
var response;
for (i = 0; i < requests.length; i++) {
request = requests[i];
response = responses[i];
// We may not have responses for all the requests.
if (typeof response !== "undefined") {
if (response.error === false) {
// Call the done handler if it was provided.
request.deferred.resolve(response.data);
} else {
exception = response.exception;
break;
}
} else {
// This is not an expected case.
exception = new Error('missing response');
break;
}
}
// Something failed, reject the remaining promises.
if (exception !== null) {
for (; i < requests.length; i++) {
request = requests[i];
request.deferred.reject(exception);
}
}
};
/**
* Fail handler. Called when the ajax call fails. Rejects all deferreds.
*
* @method requestFail
* @private
* @param {jqXHR} jqXHR The ajax object.
* @param {string} textStatus The status string.
*/
var requestFail = function(jqXHR, textStatus) {
// Reject all the promises.
var requests = this;
var i = 0;
for (i = 0; i < requests.length; i++) {
var request = requests[i];
if (typeof request.fail != "undefined") {
request.deferred.reject(textStatus);
}
}
};
return /** @alias module:core/ajax */ {
// Public variables and functions.
/**
* Make a series of ajax requests and return all the responses.
*
* @method call
* @param {Object[]} Array of requests with each containing methodname and args properties.
* done and fail callbacks can be set for each element in the array, or the
* can be attached to the promises returned by this function.
* @param {Boolean} async Optional, defaults to true.
* If false - this function will not return until the promises are resolved.
* @param {Boolean} loginrequired Optional, defaults to true.
* If false - this function will call the faster nologin ajax script - but
* will fail unless all functions have been marked as 'loginrequired' => false
* in services.php
* @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
*/
call: function(requests, async, loginrequired) {
var ajaxRequestData = [],
i,
promises = [];
if (typeof loginrequired === "undefined") {
loginrequired = true;
}
if (typeof async === "undefined") {
async = true;
}
for (i = 0; i < requests.length; i++) {
var request = requests[i];
ajaxRequestData.push({
index: i,
methodname: request.methodname,
args: request.args
});
request.deferred = $.Deferred();
promises.push(request.deferred.promise());
// Allow setting done and fail handlers as arguments.
// This is just a shortcut for the calling code.
if (typeof request.done !== "undefined") {
request.deferred.done(request.done);
}
if (typeof request.fail !== "undefined") {
request.deferred.fail(request.fail);
}
request.index = i;
}
ajaxRequestData = JSON.stringify(ajaxRequestData);
var settings = {
type: 'POST',
data: ajaxRequestData,
context: requests,
dataType: 'json',
processData: false,
async: async,
contentType: "application/json"
};
var script = config.wwwroot + '/lib/ajax/service.php?sesskey=' + config.sesskey;
if (!loginrequired) {
script = config.wwwroot + '/lib/ajax/service-nologin.php?sesskey=' + config.sesskey;
}
// Jquery deprecated done and fail with async=false so we need to do this 2 ways.
if (async) {
$.ajax(script, settings)
.done(requestSuccess)
.fail(requestFail);
} else {
settings.success = requestSuccess;
settings.error = requestFail;
$.ajax(script, settings);
}
return promises;
}
};
});
// ---- /lib/amd/src/modal_factory.js ----
// 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 .
/**
* Create a modal.
*
* @module core/modal_factory
* @class modal_factory
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/modal_factory', ['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
'core/modal_save_cancel', 'core/modal_confirm', 'core/modal_cancel',
'core/templates', 'core/notification', 'core/custom_interaction_events'],
function($, ModalEvents, ModalRegistry, Modal, ModalSaveCancel, ModalConfirm,
ModalCancel, Templates, Notification, CustomEvents) {
// The templates for each type of modal.
var TEMPLATES = {
DEFAULT: 'core/modal',
SAVE_CANCEL: 'core/modal_save_cancel',
CONFIRM: 'core/modal_confirm',
CANCEL: 'core/modal_cancel',
};
// The available types of modals.
var TYPES = {
DEFAULT: 'DEFAULT',
SAVE_CANCEL: 'SAVE_CANCEL',
CONFIRM: 'CONFIRM',
CANCEL: 'CANCEL',
};
// Register the common set of modals.
ModalRegistry.register(TYPES.DEFAULT, Modal, TEMPLATES.DEFAULT);
ModalRegistry.register(TYPES.SAVE_CANCEL, ModalSaveCancel, TEMPLATES.SAVE_CANCEL);
ModalRegistry.register(TYPES.CONFIRM, ModalConfirm, TEMPLATES.CONFIRM);
ModalRegistry.register(TYPES.CANCEL, ModalCancel, TEMPLATES.CANCEL);
/**
* Set up the events required to show the modal and return focus when the modal
* is closed.
*
* @method setUpTrigger
* @param {Promise} modalPromise The modal instance
* @param {object} triggerElement The jQuery element to open the modal
*/
var setUpTrigger = function(modalPromise, triggerElement) {
if (typeof triggerElement != 'undefined') {
// The element that actually shows the modal.
var actualTriggerElement = null;
CustomEvents.define(triggerElement, [CustomEvents.events.activate]);
triggerElement.on(CustomEvents.events.activate, function(e, data) {
actualTriggerElement = e.currentTarget;
modalPromise.then(function(modal) {
modal.show();
return modal;
});
data.originalEvent.preventDefault();
});
modalPromise.then(function(modal) {
modal.getRoot().on(ModalEvents.hidden, function() {
// Focus on the trigger element that actually launched the modal.
if (actualTriggerElement !== null) {
actualTriggerElement.focus();
}
});
return modal;
});
}
};
/**
* Create the correct instance of a modal based on the givem type. Sets up
* the trigger between the modal and the trigger element.
*
* @method createFromElement
* @param {object} registryConf A config from the ModalRegistry
* @param {object} modalElement The modal HTML jQuery object
* @param {object} triggerElement The trigger HTML jQuery object
* @return {object} Modal instance
*/
var createFromElement = function(registryConf, modalElement) {
modalElement = $(modalElement);
var module = registryConf.module;
var modal = new module(modalElement);
return modal;
};
/**
* Create the correct modal instance for the given type, including loading
* the correct template and setting up the trigger relationship with the
* trigger element.
*
* @method createFromType
* @param {object} registryConf A config from the ModalRegistry
* @param {object} triggerElement The trigger HTML jQuery object
* @return {promise} Resolved with a Modal instance
*/
var createFromType = function(registryConf, triggerElement) {
var templateName = registryConf.template;
var modalPromise = Templates.render(templateName, {})
.then(function(html) {
var modalElement = $(html);
return createFromElement(registryConf, modalElement);
})
.fail(Notification.exception);
setUpTrigger(modalPromise, triggerElement);
return modalPromise;
};
/**
* Create a Modal instance.
*
* @method create
* @param {object} modalConfig The configuration to create the modal instance
* @param {object} triggerElement The trigger HTML jQuery object
* @return {promise} Resolved with a Modal instance
*/
var create = function(modalConfig, triggerElement) {
var type = modalConfig.type || TYPES.DEFAULT;
var isLarge = modalConfig.large ? true : false;
var registryConf = null;
registryConf = ModalRegistry.get(type);
if (!registryConf) {
Notification.exception({message: 'Unable to find modal of type: ' + type});
}
return createFromType(registryConf, triggerElement)
.then(function(modal) {
if (typeof modalConfig.title != 'undefined') {
modal.setTitle(modalConfig.title);
}
if (typeof modalConfig.body != 'undefined') {
modal.setBody(modalConfig.body);
}
if (typeof modalConfig.footer != 'undefined') {
modal.setFooter(modalConfig.footer);
}
if (isLarge) {
modal.setLarge();
}
return modal;
});
};
return {
create: create,
types: TYPES,
};
});
// ---- /lib/amd/src/truncate.js ----
// 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 .
/**
* Description of import/upgrade into Moodle:
* 1.) Download from https://github.com/pathable/truncate
* 2.) Copy jquery.truncate.js into lib/amd/src/truncate.js
* 3.) Edit truncate.js to return the $.truncate function as truncate
* 4.) Apply Moodle changes from git commit 7172b33e241c4d42cff01f78bf8570408f43fdc2
*/
/**
* Module for text truncation.
*
* Implementation provided by Pathable (thanks!).
* See: https://github.com/pathable/truncate
*
* @module core/truncate
* @package core
* @class truncate
* @copyright 2017 Pathable
* 2017 Mathias Bynens
* 2017 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/truncate', ['jquery'], function($) {
// Matches trailing non-space characters.
var chop = /(\s*\S+|\s)$/;
// Matches the first word in the string.
var start = /^(\S*)/;
// Matches any space characters.
var space = /\s/;
// Special thanks to Mathias Bynens for the multi-byte char
// implementation. Much love.
// see: https://github.com/mathiasbynens/String.prototype.at/blob/master/at.js
var charLengthAt = function(text, position) {
if (this == null) {
throw TypeError();
}
var string = String(text);
var size = string.length;
// `ToInteger`
var index = position ? Number(position) : 0;
if (index != index) { // better `isNaN`
index = 0;
}
// Account for out-of-bounds indices
// The odd lower bound is because the ToInteger operation is
// going to round `n` to `0` for `-1 < n <= 0`.
if (index <= -1 || index >= size) {
return '';
}
// Second half of `ToInteger`
index = index | 0;
// Get the first code unit and code unit value
var cuFirst = string.charCodeAt(index);
var cuSecond;
var nextIndex = index + 1;
var len = 1;
if ( // Check if it’s the start of a surrogate pair.
cuFirst >= 0xD800 && cuFirst <= 0xDBFF && // high surrogate
size > nextIndex // there is a next code unit
) {
cuSecond = string.charCodeAt(nextIndex);
if (cuSecond >= 0xDC00 && cuSecond <= 0xDFFF) { // low surrogate
len = 2;
}
}
return len;
};
var lengthMultiByte = function(text) {
var count = 0;
for (var i = 0; i < text.length; i += charLengthAt(text, i)) {
count++;
}
return count;
};
var getSliceLength = function(text, amount) {
if (!text.length) {
return 0;
}
var length = 0;
var count = 0;
do {
length += charLengthAt(text, length);
count++;
} while (length < text.length && count < amount);
return length;
};
// Return a truncated html string. Delegates to $.fn.truncate.
$.truncate = function(html, options) {
return $('
').append(html).truncate(options).html();
};
// Truncate the contents of an element in place.
$.fn.truncate = function(options) {
if ($.isNumeric(options)) options = {length: options};
var o = $.extend({}, $.truncate.defaults, options);
return this.each(function() {
var self = $(this);
if (o.noBreaks) self.find('br').replaceWith(' ');
var ellipsisLength = o.ellipsis.length;
var text = self.text();
var textLength = lengthMultiByte(text);
var excess = textLength - o.length + ellipsisLength;
if (textLength < o.length) return;
if (o.stripTags) self.text(text);
// Chop off any partial words if appropriate.
if (o.words && excess > 0) {
var sliced = text.slice(0, getSliceLength(text, o.length - ellipsisLength) + 1);
var replaced = sliced.replace(chop, '');
var truncated = lengthMultiByte(replaced);
var oneWord = sliced.match(space) ? false : true;
if (o.keepFirstWord && truncated === 0) {
excess = textLength - lengthMultiByte(start.exec(text)[0]) - ellipsisLength;
} else if (oneWord && truncated === 0) {
excess = textLength - o.length + ellipsisLength;
} else {
excess = textLength - truncated - 1;
}
}
// The requested length is larger than the text. No need for ellipsis.
if (excess > textLength) {
excess = textLength - o.length;
}
if (excess < 0 || !excess && !o.truncated) return;
// Iterate over each child node in reverse, removing excess text.
$.each(self.contents().get().reverse(), function(i, el) {
var $el = $(el);
var text = $el.text();
var length = lengthMultiByte(text);
// If the text is longer than the excess, remove the node and continue.
if (length <= excess) {
o.truncated = true;
excess -= length;
$el.remove();
return;
}
// Remove the excess text and append the ellipsis.
if (el.nodeType === 3) {
var splitAmount = length - excess;
splitAmount = splitAmount >= 0 ? getSliceLength(text, splitAmount) : 0;
$(el.splitText(splitAmount)).replaceWith(o.ellipsis);
return false;
}
// Recursively truncate child nodes.
$el.truncate($.extend(o, {length: length - excess + ellipsisLength}));
return false;
});
});
};
$.truncate.defaults = {
// Strip all html elements, leaving only plain text.
stripTags: false,
// Only truncate at word boundaries.
words: false,
// When 'words' is active, keeps the first word in the string
// even if it's longer than a target length.
keepFirstWord: false,
// Replace instances of with a single space.
noBreaks: false,
// The maximum length of the truncated html.
length: Infinity,
// The character to use as the ellipsis. The word joiner (U+2060) can be
// used to prevent a hanging ellipsis, but displays incorrectly in Chrome
// on Windows 7.
// http://code.google.com/p/chromium/issues/detail?id=68323
//ellipsis: '\u2026' // '\u2060\u2026'
ellipsis: '\u2026' // '\u2060\u2026'
};
return {
truncate: $.truncate,
};
});
// ---- /lib/amd/src/user_date.js ----
// 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 .
/**
* Fetch and render dates from timestamps.
*
* @module core/user_date
* @package core
* @copyright 2017 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/user_date', ['jquery', 'core/ajax', 'core/sessionstorage', 'core/config'],
function($, Ajax, Storage, Config) {
/** @var {object} promisesCache Store all promises we've seen so far. */
var promisesCache = {};
/**
* Generate a cache key for the given request. The request should
* have a timestamp and format key.
*
* @param {object} request
* @return {string}
*/
var getKey = function(request) {
var language = $('html').attr('lang').replace(/-/g, '_');
return 'core_user_date/' +
language + '/' +
Config.usertimezone + '/' +
request.timestamp + '/' +
request.format;
};
/**
* Retrieve a transformed date from the browser's storage.
*
* @param {string} key
* @return {string}
*/
var getFromLocalStorage = function(key) {
return Storage.get(key);
};
/**
* Save the transformed date in the browser's storage.
*
* @param {string} key
* @param {string} value
*/
var addToLocalStorage = function(key, value) {
Storage.set(key, value);
};
/**
* Check if a key is in the module's cache.
*
* @param {string} key
* @return {bool}
*/
var inPromisesCache = function(key) {
return (typeof promisesCache[key] !== 'undefined');
};
/**
* Retrieve a promise from the module's cache.
*
* @param {string} key
* @return {object} jQuery promise
*/
var getFromPromisesCache = function(key) {
return promisesCache[key];
};
/**
* Save the given promise in the module's cache.
*
* @param {string} key
* @param {object} promise
*/
var addToPromisesCache = function(key, promise) {
promisesCache[key] = promise;
};
/**
* Send a request to the server for each of the required timestamp
* and format combinations.
*
* Resolves the date's deferred with the values returned from the
* server and saves the value in local storage.
*
* @param {array} dates
* @return {object} jQuery promise
*/
var loadDatesFromServer = function(dates) {
var args = dates.map(function(data) {
return {
timestamp: data.timestamp,
format: data.format
};
});
var request = {
methodname: 'core_get_user_dates',
args: {
contextid: Config.contextid,
timestamps: args
}
};
return Ajax.call([request], true, true)[0].then(function(results) {
results.dates.forEach(function(value, index) {
var date = dates[index];
var key = getKey(date);
addToLocalStorage(key, value);
date.deferred.resolve(value);
});
})
.fail(function(ex) {
// If we failed to retrieve the dates then reject the date's
// deferred objects to make sure they don't hang.
dates.forEach(function(date) {
date.deferred.reject(ex);
});
});
};
/**
* Takes an array of request objects and returns a promise that
* is resolved with an array of formatted dates.
*
* The values in the returned array will be ordered the same as
* the request array.
*
* This function will check both the module's static promises cache
* and the browser's session storage to see if the user dates have
* already been loaded in order to avoid sending a network request
* if possible.
*
* Only dates not found in either cache will be sent to the server
* for transforming.
*
* A request object must have a timestamp key and a format key.
*
* E.g.
* var request = [
* {
* timestamp: 1293876000,
* format: '%d %B %Y'
* },
* {
* timestamp: 1293876000,
* format: '%A, %d %B %Y, %I:%M %p'
* }
* ];
*
* UserDate.get(request).done(function(dates) {
* console.log(dates[0]); // prints "1 January 2011".
* console.log(dates[1]); // prints "Saturday, 1 January 2011, 10:00 AM".
* });
*
* @param {array} requests
* @return {object} jQuery promise
*/
var get = function(requests) {
var ajaxRequests = [];
var promises = [];
// Loop over each of the requested timestamp/format combos
// and add a promise to the promises array for them.
requests.forEach(function(request) {
var key = getKey(request);
// If we've already got a promise then use it.
if (inPromisesCache(key)) {
promises.push(getFromPromisesCache(key));
} else {
var deferred = $.Deferred();
var cached = getFromLocalStorage(key);
if (cached) {
// If we were able to get the value from session storage
// then we can resolve the deferred with that value. No
// need to ask the server to transform it for us.
deferred.resolve(cached);
} else {
// Add this request to the list of ones we need to load
// from the server. Include the deferred so that it can
// be resolved when the server has responded with the
// transformed values.
request.deferred = deferred;
ajaxRequests.push(request);
}
// Remember this promise for next time so that we can
// bail out early if it is requested again.
addToPromisesCache(key, deferred.promise());
promises.push(deferred.promise());
}
});
// If we have any requests that we couldn't resolve from the caches
// then let's ask the server to get them for us.
if (ajaxRequests.length) {
loadDatesFromServer(ajaxRequests);
}
// Wait for all of the promises to resolve. Some of them may be waiting
// for a response from the server.
return $.when.apply($, promises).then(function() {
// This looks complicated but it's just converting an unknown
// length of arguments into an array for the promise to resolve
// with.
return arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments);
});
};
return {
get: get
};
});
// ---- /lib/amd/src/localstorage.js ----
// 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 .
/**
* Simple API for set/get to localstorage, with cacherev expiration.
*
* @module core/localstorage
* @package core
* @class localstorage
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
define('core/localstorage', ['core/config'], function(config) {
// Private functions and variables.
/** @var {boolean} supported - Is localstorage supported in this browser? */
var supported = false;
/** @var {string} prefix - Prefix to use on all cache keys */
var prefix = '';
/** @var {jsrevPrefix} jsrevPrefix - Key to store the current jsrev version for the cache */
var jsrevPrefix = '';
/** @var {Object} localStorage - Browsers localStorage object */
var localStorage = null;
/**
* Check if the browser supports local storage.
*
* @method detectSupport
* @return {boolean} True if the browser supports local storage.
*/
var detectSupport = function() {
if (config.jsrev == -1) {
// Disable cache if debugging.
return false;
}
if (typeof(window.localStorage) === "undefined") {
return false;
}
var testKey = 'test';
try {
localStorage = window.localStorage;
if (localStorage === null) {
return false;
}
// MDL-51461 - Some browsers misreport availability of local storage
// so check it is actually usable.
localStorage.setItem(testKey, '1');
localStorage.removeItem(testKey);
return true;
} catch (ex) {
return false;
}
};
/**
* Add a unique prefix to all keys so multiple moodle sites do not share caches.
*
* @method prefixKey
* @param {string} key The cache key to prefix.
* @return {string} The new key
*/
var prefixKey = function(key) {
return prefix + key;
};
/**
* Check the current jsrev version and clear the cache if it has been bumped.
*
* @method validateCache
*/
var validateCache = function() {
var cacheVersion = localStorage.getItem(jsrevPrefix);
if (cacheVersion === null) {
localStorage.setItem(jsrevPrefix, config.jsrev);
return;
}
var moodleVersion = config.jsrev;
if (moodleVersion != cacheVersion) {
localStorage.clear();
localStorage.setItem(jsrevPrefix, config.jsrev);
}
};
/**
* Hash a string, used to make shorter key prefixes.
*
* @method hashString
* @param string source The string to hash
* @return int The int hash
*/
var hashString = function(source) {
// From http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery.
/* jshint bitwise: false */
var hash = 0, i, chr, len;
if (source.length === 0) {
return hash;
}
for (i = 0, len = source.length; i < len; i++) {
chr = source.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
/**
* Init this module.
*
* This computes the hash prefixes from jsrev and friends.
*/
var init = function() {
supported = detectSupport();
var hashSource = config.wwwroot + '/' + config.jsrev;
var hash = hashString(hashSource);
prefix = hash + '/';
hashSource = config.wwwroot + '/';
hash = hashString(hashSource);
jsrevPrefix = hash + '/jsrev';
};
// Run the module init.
init();
return /** @alias module:core/localstorage */ {
/**
* Get a value from local storage. Remember - all values must be strings.
*
* @method get
* @param {string} key The cache key to check.
* @return {boolean|string} False if the value is not in the cache, or some other error - a string otherwise.
*/
get: function(key) {
if (!supported) {
return false;
}
validateCache();
key = prefixKey(key);
return localStorage.getItem(key);
},
/**
* Set a value to local storage. Remember - all values must be strings.
*
* @method set
* @param {string} key The cache key to set.
* @param {string} value The value to set.
* @return {boolean} False if the value can't be saved in the cache, or some other error - true otherwise.
*/
set: function(key, value) {
if (!supported) {
return false;
}
validateCache();
key = prefixKey(key);
// This can throw exceptions when the storage limit is reached.
try {
localStorage.setItem(key, value);
} catch (e) {
return false;
}
return true;
}
};
});
// ---- /lib/amd/src/sessionstorage.js ----
// 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 .
/**
* Simple API for set/get to sessionstorage, with cacherev expiration.
*
* Session storage will only persist for as long as the browser window
* stays open.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
*
* @module core/sessionstorage
* @package core
* @copyright 2017 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/sessionstorage', ['core/config', 'core/storagewrapper'], function(config, StorageWrapper) {
// Private functions and variables.
/** @var {Object} StorageWrapper - Wraps browsers sessionStorage object */
var storage = new StorageWrapper(window.sessionStorage);
return /** @alias module:core/sessionstorage */ {
/**
* Get a value from session storage. Remember - all values must be strings.
*
* @method get
* @param {string} key The cache key to check.
* @return {boolean|string} False if the value is not in the cache, or some other error - a string otherwise.
*/
get: function(key) {
return storage.get(key);
},
/**
* Set a value to session storage. Remember - all values must be strings.
*
* @method set
* @param {string} key The cache key to set.
* @param {string} value The value to set.
* @return {boolean} False if the value can't be saved in the cache, or some other error - true otherwise.
*/
set: function(key, value) {
return storage.set(key, value);
}
};
});
// ---- /lib/amd/src/chart_output_chartjs.js ----
// 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 .
/**
* Chart output for chart.js.
*
* @package core
* @copyright 2016 Frédéric Massart - FMCorz.net
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @module core/chart_output_chartjs
*/
define('core/chart_output_chartjs', [
'jquery',
'core/chartjs',
'core/chart_axis',
'core/chart_bar',
'core/chart_output_base',
'core/chart_line',
'core/chart_pie',
'core/chart_series'
], function($, Chartjs, Axis, Bar, Base, Line, Pie, Series) {
/**
* Makes an axis ID.
*
* @param {String} xy Accepts 'x' and 'y'.
* @param {Number} index The axis index.
* @return {String}
*/
var makeAxisId = function(xy, index) {
return 'axis-' + xy + '-' + index;
};
/**
* Chart output for Chart.js.
*
* @class
* @alias module:core/chart_output_chartjs
* @extends {module:core/chart_output_base}
*/
function Output() {
Base.prototype.constructor.apply(this, arguments);
// Make sure that we've got a canvas tag.
this._canvas = this._node;
if (this._canvas.prop('tagName') != 'CANVAS') {
this._canvas = $('');
this._node.append(this._canvas);
}
this._build();
}
Output.prototype = Object.create(Base.prototype);
/**
* Reference to the chart config object.
*
* @type {Object}
* @protected
*/
Output.prototype._config = null;
/**
* Reference to the instance of chart.js.
*
* @type {Object}
* @protected
*/
Output.prototype._chartjs = null;
/**
* Reference to the canvas node.
*
* @type {Jquery}
* @protected
*/
Output.prototype._canvas = null;
/**
* Builds the config and the chart.
*
* @protected
*/
Output.prototype._build = function() {
this._config = this._makeConfig();
this._chartjs = new Chartjs(this._canvas[0], this._config);
};
/**
* Clean data.
*
* @param {(String|String[])} data A single string or an array of strings.
* @returns {(String|String[])}
* @protected
*/
Output.prototype._cleanData = function(data) {
if (data instanceof Array) {
return data.map(function(value) {
return $('').html(value).text();
});
} else {
return $('').html(data).text();
}
};
/**
* Get the chart type and handles the Chart.js specific chart types.
*
* By default returns the current chart TYPE value. Also does the handling of specific chart types, for example
* check if the bar chart should be horizontal and the pie chart should be displayed as a doughnut.
*
* @method getChartType
* @returns {String} the chart type.
* @protected
*/
Output.prototype._getChartType = function() {
var type = this._chart.getType();
// Bars can be displayed vertically and horizontally, defining horizontalBar type.
if (this._chart.getType() === Bar.prototype.TYPE && this._chart.getHorizontal() === true) {
type = 'horizontalBar';
} else if (this._chart.getType() === Pie.prototype.TYPE && this._chart.getDoughnut() === true) {
// Pie chart can be displayed as doughnut.
type = 'doughnut';
}
return type;
};
/**
* Make the axis config.
*
* @protected
* @param {module:core/chart_axis} axis The axis.
* @param {String} xy Accepts 'x' or 'y'.
* @param {Number} index The axis index.
* @return {Object} The axis config.
*/
Output.prototype._makeAxisConfig = function(axis, xy, index) {
var scaleData = {
id: makeAxisId(xy, index)
};
if (axis.getPosition() !== Axis.prototype.POS_DEFAULT) {
scaleData.position = axis.getPosition();
}
if (axis.getLabel() !== null) {
scaleData.scaleLabel = {
display: true,
labelString: this._cleanData(axis.getLabel())
};
}
if (axis.getStepSize() !== null) {
scaleData.ticks = scaleData.ticks || {};
scaleData.ticks.stepSize = axis.getStepSize();
}
if (axis.getMax() !== null) {
scaleData.ticks = scaleData.ticks || {};
scaleData.ticks.max = axis.getMax();
}
if (axis.getMin() !== null) {
scaleData.ticks = scaleData.ticks || {};
scaleData.ticks.min = axis.getMin();
}
return scaleData;
};
/**
* Make the config config.
*
* @protected
* @param {module:core/chart_axis} axis The axis.
* @return {Object} The axis config.
*/
Output.prototype._makeConfig = function() {
var config = {
type: this._getChartType(),
data: {
labels: this._cleanData(this._chart.getLabels()),
datasets: this._makeDatasetsConfig()
},
options: {
title: {
display: this._chart.getTitle() !== null,
text: this._cleanData(this._chart.getTitle())
}
}
};
this._chart.getXAxes().forEach(function(axis, i) {
var axisLabels = axis.getLabels();
config.options.scales = config.options.scales || {};
config.options.scales.xAxes = config.options.scales.xAxes || [];
config.options.scales.xAxes[i] = this._makeAxisConfig(axis, 'x', i);
if (axisLabels !== null) {
config.options.scales.xAxes[i].ticks.callback = function(value, index) {
return axisLabels[index] || '';
};
}
config.options.scales.xAxes[i].stacked = this._isStacked();
}.bind(this));
this._chart.getYAxes().forEach(function(axis, i) {
var axisLabels = axis.getLabels();
config.options.scales = config.options.scales || {};
config.options.scales.yAxes = config.options.scales.yAxes || [];
config.options.scales.yAxes[i] = this._makeAxisConfig(axis, 'y', i);
if (axisLabels !== null) {
config.options.scales.yAxes[i].ticks.callback = function(value) {
return axisLabels[parseInt(value, 10)] || '';
};
}
config.options.scales.yAxes[i].stacked = this._isStacked();
}.bind(this));
config.options.tooltips = {
callbacks: {
label: this._makeTooltip.bind(this)
}
};
return config;
};
/**
* Get the datasets configurations.
*
* @protected
* @return {Object[]}
*/
Output.prototype._makeDatasetsConfig = function() {
var sets = this._chart.getSeries().map(function(series) {
var colors = series.hasColoredValues() ? series.getColors() : series.getColor();
var dataset = {
label: this._cleanData(series.getLabel()),
data: series.getValues(),
type: series.getType(),
fill: false,
backgroundColor: colors,
// Pie charts look better without borders.
borderColor: this._chart.getType() == Pie.prototype.TYPE ? null : colors,
lineTension: this._isSmooth(series) ? 0.3 : 0
};
if (series.getXAxis() !== null) {
dataset.xAxisID = makeAxisId('x', series.getXAxis());
}
if (series.getYAxis() !== null) {
dataset.yAxisID = makeAxisId('y', series.getYAxis());
}
return dataset;
}.bind(this));
return sets;
};
/**
* Get the chart data, add labels and rebuild the tooltip.
*
* @param {Object[]} tooltipItem The tooltip item data.
* @param {Object[]} data The chart data.
* @returns {String}
* @protected
*/
Output.prototype._makeTooltip = function(tooltipItem, data) {
// Get series and chart data to rebuild the tooltip and add labels.
var series = this._chart.getSeries()[tooltipItem.datasetIndex];
var serieLabel = series.getLabel();
var serieLabels = series.getLabels();
var chartData = data.datasets[tooltipItem.datasetIndex].data;
var tooltipData = chartData[tooltipItem.index];
// Build default tooltip.
var tooltip = [];
// Pie and doughnut charts does not have axis.
if (tooltipItem.xLabel == '' && tooltipItem.yLabel == '') {
var chartLabels = this._cleanData(this._chart.getLabels());
tooltip.push(chartLabels[tooltipItem.index]);
}
// Add series labels to the tooltip if any.
if (serieLabels !== null) {
tooltip.push(this._cleanData(serieLabels[tooltipItem.index]));
} else {
tooltip.push(this._cleanData(serieLabel) + ': ' + tooltipData);
}
return tooltip;
};
/**
* Verify if the chart line is smooth or not.
*
* @protected
* @param {module:core/chart_series} series The series.
* @returns {Bool}
*/
Output.prototype._isSmooth = function(series) {
var smooth = false;
if (this._chart.getType() === Line.prototype.TYPE) {
smooth = series.getSmooth();
if (smooth === null) {
smooth = this._chart.getSmooth();
}
} else if (series.getType() === Series.prototype.TYPE_LINE) {
smooth = series.getSmooth();
}
return smooth;
};
/**
* Verify if the bar chart is stacked or not.
*
* @protected
* @returns {Bool}
*/
Output.prototype._isStacked = function() {
var stacked = false;
// Stacking is (currently) only supported for bar charts.
if (this._chart.getType() === Bar.prototype.TYPE) {
stacked = this._chart.getStacked();
}
return stacked;
};
/** @override */
Output.prototype.update = function() {
$.extend(true, this._config, this._makeConfig());
this._chartjs.update();
};
return Output;
});
// ---- /lib/amd/src/modal_backdrop.js ----
// 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 .
/**
* Contain the logic for modal backdrops.
*
* @module core/modal_backdrop
* @class modal_backdrop
* @package core
* @copyright 2016 Ryan Wyllie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('core/modal_backdrop', ['jquery', 'core/templates', 'core/notification'],
function($, Templates, Notification) {
var SELECTORS = {
ROOT: '[data-region="modal-backdrop"]',
};
/**
* Constructor for ModalBackdrop.
*
* @param {object} root The root element for the modal backdrop
*/
var ModalBackdrop = function(root) {
this.root = $(root);
this.isAttached = false;
if (!this.root.is(SELECTORS.ROOT)) {
Notification.exception({message: 'Element is not a modal backdrop'});
}
};
/**
* Get the root element of this modal backdrop.
*
* @method getRoot
* @return {object} jQuery object
*/
ModalBackdrop.prototype.getRoot = function() {
return this.root;
};
/**
* Add the modal backdrop to the page, if it hasn't already been added.
*
* @method attachToDOM
*/
ModalBackdrop.prototype.attachToDOM = function() {
if (this.isAttached) {
return;
}
$('body').append(this.root);
this.isAttached = true;
};
/**
* Set the z-index value for this backdrop.
*
* @method setZIndex
* @param {int} value The z-index value
*/
ModalBackdrop.prototype.setZIndex = function(value) {
this.root.css('z-index', value);
};
/**
* Check if this backdrop is visible.
*
* @method isVisible
* @return {bool}
*/
ModalBackdrop.prototype.isVisible = function() {
return this.root.hasClass('show');
};
/**
* Check if this backdrop has CSS transitions applied.
*
* @method hasTransitions
* @return {bool}
*/
ModalBackdrop.prototype.hasTransitions = function() {
return this.getRoot().hasClass('fade');
};
/**
* Display this backdrop. The backdrop will be attached to the DOM if it hasn't
* already been.
*
* @method show
*/
ModalBackdrop.prototype.show = function() {
if (this.isVisible()) {
return;
}
if (!this.isAttached) {
this.attachToDOM();
}
this.root.removeClass('hide').addClass('show');
};
/**
* Hide this backdrop.
*
* @method hide
*/
ModalBackdrop.prototype.hide = function() {
if (!this.isVisible()) {
return;
}
if (this.hasTransitions()) {
// Wait for CSS transitions to complete before hiding the element.
this.getRoot().one('transitionend webkitTransitionEnd oTransitionEnd', function() {
this.getRoot().removeClass('show').addClass('hide');
}.bind(this));
} else {
this.getRoot().removeClass('show').addClass('hide');
}
};
/**
* Remove this backdrop from the DOM.
*
* @method destroy
*/
ModalBackdrop.prototype.destroy = function() {
this.root.remove();
};
return ModalBackdrop;
});
// ---- /lib/amd/src/templates.js ----
// 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 .
/**
* Template renderer for Moodle. Load and render Moodle templates with Mustache.
*
* @module core/templates
* @package core
* @class templates
* @copyright 2015 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
define('core/templates', [ 'core/mustache',
'jquery',
'core/ajax',
'core/str',
'core/notification',
'core/url',
'core/config',
'core/localstorage',
'core/event'
],
function(mustache, $, ajax, str, notification, coreurl, config, storage, event) {
// Private variables and functions.
/** @var {string[]} templateCache - Cache of already loaded templates */
var templateCache = {};
/** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
var requiredStrings = [];
/** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
var requiredJS = [];
/** @var {Number} uniqid Incrementing value that is changed for every call to render */
var uniqid = 1;
/** @var {String} themeName for the current render */
var currentThemeName = '';
/**
* Render image icons.
*
* @method pixHelper
* @private
* @param {string} sectionText The text to parse arguments from.
* @param {function} helper Used to render the alt attribute of the text.
* @return {string}
*/
var pixHelper = function(sectionText, helper) {
var parts = sectionText.split(',');
var key = '';
var component = '';
var text = '';
var result;
if (parts.length > 0) {
key = parts.shift().trim();
}
if (parts.length > 0) {
component = parts.shift().trim();
}
if (parts.length > 0) {
text = parts.join(',').trim();
}
var url = coreurl.imageUrl(key, component);
var templatecontext = {
attributes: [
{ name: 'src', value: url},
{ name: 'alt', value: helper(text)},
{ name: 'class', value: 'smallicon'}
]
};
// We forced loading of this early, so it will be in the cache.
var template = templateCache[currentThemeName + '/core/pix_icon'];
result = mustache.render(template, templatecontext, partialHelper);
return result.trim();
};
/**
* Load a partial from the cache or ajax.
*
* @method partialHelper
* @private
* @param {string} name The partial name to load.
* @return {string}
*/
var partialHelper = function(name) {
var template = '';
getTemplate(name, false).done(
function(source) {
template = source;
}
).fail(notification.exception);
return template;
};
/**
* Render blocks of javascript and save them in an array.
*
* @method jsHelper
* @private
* @param {string} sectionText The text to save as a js block.
* @param {function} helper Used to render the block.
* @return {string}
*/
var jsHelper = function(sectionText, helper) {
requiredJS.push(helper(sectionText, this));
return '';
};
/**
* String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
* into a get_string call.
*
* @method stringHelper
* @private
* @param {string} sectionText The text to parse the arguments from.
* @param {function} helper Used to render subsections of the text.
* @return {string}
*/
var stringHelper = function(sectionText, helper) {
var parts = sectionText.split(',');
var key = '';
var component = '';
var param = '';
if (parts.length > 0) {
key = parts.shift().trim();
}
if (parts.length > 0) {
component = parts.shift().trim();
}
if (parts.length > 0) {
param = parts.join(',').trim();
}
if (param !== '') {
// Allow variable expansion in the param part only.
param = helper(param, this);
}
// Allow json formatted $a arguments.
if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
param = JSON.parse(param);
}
var index = requiredStrings.length;
requiredStrings.push({key: key, component: component, param: param});
return '{{_s' + index + '}}';
};
/**
* Quote helper used to wrap content in quotes, and escape all quotes present in the content.
*
* @method quoteHelper
* @private
* @param {string} sectionText The text to parse the arguments from.
* @param {function} helper Used to render subsections of the text.
* @return {string}
*/
var quoteHelper = function(sectionText, helper) {
var content = helper(sectionText.trim(), this);
// Escape the {{ and the ".
// This involves wrapping {{, and }} in change delimeter tags.
content = content
.replace('"', '\\"')
.replace(/([\{\}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>')
;
return '"' + content + '"';
};
/**
* Add some common helper functions to all context objects passed to templates.
* These helpers match exactly the helpers available in php.
*
* @method addHelpers
* @private
* @param {Object} context Simple types used as the context for the template.
* @param {String} themeName We set this multiple times, because there are async calls.
*/
var addHelpers = function(context, themeName) {
currentThemeName = themeName;
requiredStrings = [];
requiredJS = [];
context.uniqid = uniqid++;
context.str = function() { return stringHelper; };
context.pix = function() { return pixHelper; };
context.js = function() { return jsHelper; };
context.quote = function() { return quoteHelper; };
context.globals = { config : config };
context.currentTheme = themeName;
};
/**
* Get all the JS blocks from the last rendered template.
*
* @method getJS
* @private
* @param {string[]} strings Replacement strings.
* @return {string}
*/
var getJS = function(strings) {
var js = '';
if (requiredJS.length > 0) {
js = requiredJS.join(";\n");
}
var i = 0;
for (i = 0; i < strings.length; i++) {
js = js.replace('{{_s' + i + '}}', strings[i]);
}
// Re-render to get the final strings.
return js;
};
/**
* Render a template and then call the callback with the result.
*
* @method doRender
* @private
* @param {string} templateSource The mustache template to render.
* @param {Object} context Simple types used as the context for the template.
* @param {String} themeName Name of the current theme.
* @return {Promise} object
*/
var doRender = function(templateSource, context, themeName) {
var deferred = $.Deferred();
currentThemeName = themeName;
// Make sure we fetch this first.
var loadPixTemplate = getTemplate('core/pix_icon', true);
loadPixTemplate.done(
function() {
addHelpers(context, themeName);
var result = '';
try {
result = mustache.render(templateSource, context, partialHelper);
} catch (ex) {
deferred.reject(ex);
}
if (requiredStrings.length > 0) {
str.get_strings(requiredStrings).done(
function(strings) {
var i;
// Why do we not do another call the render here?
//
// Because that would expose DOS holes. E.g.
// I create an assignment called "{{fish" which
// would get inserted in the template in the first pass
// and cause the template to die on the second pass (unbalanced).
for (i = 0; i < strings.length; i++) {
result = result.replace('{{_s' + i + '}}', strings[i]);
}
deferred.resolve(result.trim(), getJS(strings));
}
).fail(
function(ex) {
deferred.reject(ex);
}
);
} else {
deferred.resolve(result.trim(), getJS([]));
}
}
).fail(
function(ex) {
deferred.reject(ex);
}
);
return deferred.promise();
};
/**
* Load a template from the cache or local storage or ajax request.
*
* @method getTemplate
* @private
* @param {string} templateName - should consist of the component and the name of the template like this:
* core/menu (lib/templates/menu.mustache) or
* tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
* @return {Promise} JQuery promise object resolved when the template has been fetched.
*/
var getTemplate = function(templateName, async) {
var deferred = $.Deferred();
var parts = templateName.split('/');
var component = parts.shift();
var name = parts.shift();
var searchKey = currentThemeName + '/' + templateName;
// First try request variables.
if (searchKey in templateCache) {
deferred.resolve(templateCache[searchKey]);
return deferred.promise();
}
// Now try local storage.
var cached = storage.get('core_template/' + searchKey);
if (cached) {
deferred.resolve(cached);
templateCache[searchKey] = cached;
return deferred.promise();
}
// Oh well - load via ajax.
var promises = ajax.call([{
methodname: 'core_output_load_template',
args:{
component: component,
template: name,
themename: currentThemeName
}
}], async, false);
promises[0].done(
function (templateSource) {
storage.set('core_template/' + searchKey, templateSource);
templateCache[searchKey] = templateSource;
deferred.resolve(templateSource);
}
).fail(
function (ex) {
deferred.reject(ex);
}
);
return deferred.promise();
};
/**
* Execute a block of JS returned from a template.
* Call this AFTER adding the template HTML into the DOM so the nodes can be found.
*
* @method runTemplateJS
* @param {string} source - A block of javascript.
*/
var runTemplateJS = function(source) {
if (source.trim() !== '') {
var newscript = $('