diff --git a/lib/amd/src/custom_interaction_events.js b/lib/amd/src/custom_interaction_events.js
new file mode 100644
index 00000000000..f6c9ca554d9
--- /dev/null
+++ b/lib/amd/src/custom_interaction_events.js
@@ -0,0 +1,380 @@
+// 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(['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',
+ 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 {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 {jQuery 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 {jQuery 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 {jQuery 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 {jQuery 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 {jQuery 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 {jQuery 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 {jQuery 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 {jQuery 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 {jQuery 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 {jQuery 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 {jQuery object} element jQuery object to add event listeners to
+ */
+ var addScrollTopListener = function(element) {
+ element.off('scroll.cie.scrollTop').on('scroll.cie.scrollTop', function() {
+ var scrollTop = element.scrollTop();
+ if (scrollTop === 0) {
+ element.trigger(events.scrollTop);
+ }
+ });
+ };
+
+ /**
+ * Trigger the scrollBottom event on the given element if the user scrolls to
+ * the bottom of the given element.
+ *
+ * @method addScrollBottomListener
+ * @private
+ * @param {jQuery object} element jQuery object to add event listeners to
+ */
+ var addScrollBottomListener = function(element) {
+ element.off('scroll.cie.scrollBottom').on('scroll.cie.scrollBottom', function() {
+ var scrollTop = element.scrollTop();
+ var innerHeight = element.innerHeight();
+ var scrollHeight = element[0].scrollHeight;
+
+ if (scrollTop + innerHeight >= scrollHeight) {
+ element.trigger(events.scrollBottom);
+ }
+ });
+ };
+
+ /**
+ * Trigger the ctrlPageUp event on the given element if the user presses the
+ * control and page up key.
+ *
+ * @method addCtrlPageUpListener
+ * @private
+ * @param {jQuery 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 {jQuery 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 {jQuery 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 {jQuery 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.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 {jQuery 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,
+ };
+});
diff --git a/lib/amd/src/key_codes.js b/lib/amd/src/key_codes.js
new file mode 100644
index 00000000000..13167acee59
--- /dev/null
+++ b/lib/amd/src/key_codes.js
@@ -0,0 +1,44 @@
+// 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(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,
+ };
+});