1 // This file is part of Moodle - http://moodle.org/
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 * This module provides a wrapper to encapsulate a lot of the common combinations of
18 * user interaction we use in Moodle.
20 * @module core/custom_interaction_events
21 * @class custom_interaction_events
23 * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 define(['jquery', 'core/key_codes'], function($, keyCodes) {
28 // The list of events provided by this module. Namespaced to avoid clashes.
30 activate: 'cie:activate',
31 keyboardActivate: 'cie:keyboardactivate',
38 previous: 'cie:previous',
39 asterix: 'cie:asterix',
40 scrollLock: 'cie:scrollLock',
41 scrollTop: 'cie:scrollTop',
42 scrollBottom: 'cie:scrollBottom',
43 ctrlPageUp: 'cie:ctrlPageUp',
44 ctrlPageDown: 'cie:ctrlPageDown',
47 // Static cache of jQuery events that have been handled. This should
48 // only be populated by JavaScript generated events (which will keep it
50 var triggeredEvents = {};
53 * Check if the caller has asked for the given event type to be
56 * @method shouldAddEvent
58 * @param {string} eventType name of the event (see events above)
59 * @param {array} include the list of events to be added
60 * @return {bool} true if the event should be added, false otherwise.
62 var shouldAddEvent = function(eventType, include) {
63 include = include || [];
65 if (include.length && include.indexOf(eventType) !== -1) {
73 * Check if any of the modifier keys have been pressed on the event.
75 * @method isModifierPressed
77 * @param {event} e jQuery event
78 * @return {bool} true if shift, meta (command on Mac), alt or ctrl are pressed
80 var isModifierPressed = function(e) {
81 return (e.shiftKey || e.metaKey || e.altKey || e.ctrlKey);
85 * Trigger the custom event for the given jQuery event.
87 * This function will only fire the custom event if one hasn't already been
88 * fired for the jQuery event.
90 * This is to prevent multiple custom event handlers triggering multiple
91 * custom events for a single jQuery event as it bubbles up the stack.
93 * @param {string} eventName The name of the custom event
94 * @param {event} e The jQuery event
97 var triggerEvent = function(eventName, e) {
98 var eventTypeKey = "";
100 if (!e.hasOwnProperty('originalEvent')) {
101 // This is a jQuery event generated from JavaScript not a browser event so
102 // we need to build the cache key for the event.
103 eventTypeKey = "" + eventName + e.type + e.timeStamp;
105 if (!triggeredEvents.hasOwnProperty(eventTypeKey)) {
106 // If we haven't seen this jQuery event before then fire a custom
107 // event for it and remember the event for later.
108 triggeredEvents[eventTypeKey] = true;
109 $(e.target).trigger(eventName, [{originalEvent: e}]);
114 eventTypeKey = "triggeredCustom_" + eventName;
115 if (!e.originalEvent.hasOwnProperty(eventTypeKey)) {
116 // If this is a jQuery event generated by the browser then set a
117 // property on the original event to track that we've seen it before.
118 // The property is set on the original event because it's the only part
119 // of the jQuery event that is maintained through multiple event handlers.
120 e.originalEvent[eventTypeKey] = true;
121 $(e.target).trigger(eventName, [{originalEvent: e}]);
127 * Register a keyboard event that ignores modifier keys.
129 * @method addKeyboardEvent
131 * @param {object} element A jQuery object of the element to bind events to
132 * @param {string} event The custom interaction event name
133 * @param {int} keyCode The key code.
135 var addKeyboardEvent = function(element, event, keyCode) {
136 element.off('keydown.' + event).on('keydown.' + event, function(e) {
137 if (!isModifierPressed(e)) {
138 if (e.keyCode == keyCode) {
139 triggerEvent(event, e);
146 * Trigger the activate event on the given element if it is clicked or the enter
147 * or space key are pressed without a modifier key.
149 * @method addActivateListener
151 * @param {object} element jQuery object to add event listeners to
153 var addActivateListener = function(element) {
154 element.off('click.cie.activate').on('click.cie.activate', function(e) {
155 triggerEvent(events.activate, e);
157 element.off('keydown.cie.activate').on('keydown.cie.activate', function(e) {
158 if (!isModifierPressed(e)) {
159 if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) {
160 triggerEvent(events.activate, e);
167 * Trigger the keyboard activate event on the given element if the enter
168 * or space key are pressed without a modifier key.
170 * @method addKeyboardActivateListener
172 * @param {object} element jQuery object to add event listeners to
174 var addKeyboardActivateListener = function(element) {
175 element.off('keydown.cie.keyboardactivate').on('keydown.cie.keyboardactivate', function(e) {
176 if (!isModifierPressed(e)) {
177 if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) {
178 triggerEvent(events.keyboardActivate, e);
185 * Trigger the escape event on the given element if the escape key is pressed
186 * without a modifier key.
188 * @method addEscapeListener
190 * @param {object} element jQuery object to add event listeners to
192 var addEscapeListener = function(element) {
193 addKeyboardEvent(element, events.escape, keyCodes.escape);
197 * Trigger the down event on the given element if the down arrow key is pressed
198 * without a modifier key.
200 * @method addDownListener
202 * @param {object} element jQuery object to add event listeners to
204 var addDownListener = function(element) {
205 addKeyboardEvent(element, events.down, keyCodes.arrowDown);
209 * Trigger the up event on the given element if the up arrow key is pressed
210 * without a modifier key.
212 * @method addUpListener
214 * @param {object} element jQuery object to add event listeners to
216 var addUpListener = function(element) {
217 addKeyboardEvent(element, events.up, keyCodes.arrowUp);
221 * Trigger the home event on the given element if the home key is pressed
222 * without a modifier key.
224 * @method addHomeListener
226 * @param {object} element jQuery object to add event listeners to
228 var addHomeListener = function(element) {
229 addKeyboardEvent(element, events.home, keyCodes.home);
233 * Trigger the end event on the given element if the end key is pressed
234 * without a modifier key.
236 * @method addEndListener
238 * @param {object} element jQuery object to add event listeners to
240 var addEndListener = function(element) {
241 addKeyboardEvent(element, events.end, keyCodes.end);
245 * Trigger the next event on the given element if the right arrow key is pressed
246 * without a modifier key in LTR mode or left arrow key in RTL mode.
248 * @method addNextListener
250 * @param {object} element jQuery object to add event listeners to
252 var addNextListener = function(element) {
253 // Left and right are flipped in RTL mode.
254 var keyCode = $('html').attr('dir') == "rtl" ? keyCodes.arrowLeft : keyCodes.arrowRight;
256 addKeyboardEvent(element, events.next, keyCode);
260 * Trigger the previous event on the given element if the left arrow key is pressed
261 * without a modifier key in LTR mode or right arrow key in RTL mode.
263 * @method addPreviousListener
265 * @param {object} element jQuery object to add event listeners to
267 var addPreviousListener = function(element) {
268 // Left and right are flipped in RTL mode.
269 var keyCode = $('html').attr('dir') == "rtl" ? keyCodes.arrowRight : keyCodes.arrowLeft;
271 addKeyboardEvent(element, events.previous, keyCode);
275 * Trigger the asterix event on the given element if the asterix key is pressed
276 * without a modifier key.
278 * @method addAsterixListener
280 * @param {object} element jQuery object to add event listeners to
282 var addAsterixListener = function(element) {
283 addKeyboardEvent(element, events.asterix, keyCodes.asterix);
288 * Trigger the scrollTop event on the given element if the user scrolls to
289 * the top of the given element.
291 * @method addScrollTopListener
293 * @param {object} element jQuery object to add event listeners to
295 var addScrollTopListener = function(element) {
296 element.off('scroll.cie.scrollTop').on('scroll.cie.scrollTop', function(e) {
297 var scrollTop = element.scrollTop();
298 if (scrollTop === 0) {
299 triggerEvent(events.scrollTop, e);
305 * Trigger the scrollBottom event on the given element if the user scrolls to
306 * the bottom of the given element.
308 * @method addScrollBottomListener
310 * @param {object} element jQuery object to add event listeners to
312 var addScrollBottomListener = function(element) {
313 element.off('scroll.cie.scrollBottom').on('scroll.cie.scrollBottom', function(e) {
314 var scrollTop = element.scrollTop();
315 var innerHeight = element.innerHeight();
316 var scrollHeight = element[0].scrollHeight;
318 if (scrollTop + innerHeight >= scrollHeight) {
319 triggerEvent(events.scrollBottom, e);
325 * Trigger the scrollLock event on the given element if the user scrolls to
326 * the bottom or top of the given element.
328 * @method addScrollLockListener
330 * @param {jQuery} element jQuery object to add event listeners to
332 var addScrollLockListener = function(element) {
333 // Lock mousewheel scrolling within the element to stop the annoying window scroll.
334 element.off('DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock')
335 .on('DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock', function(e) {
336 var scrollTop = element.scrollTop();
337 var scrollHeight = element[0].scrollHeight;
338 var height = element.height();
339 var delta = (e.type == 'DOMMouseScroll' ?
340 e.originalEvent.detail * -40 :
341 e.originalEvent.wheelDelta);
344 if (!up && -delta > scrollHeight - height - scrollTop) {
345 // Scrolling down past the bottom.
346 element.scrollTop(scrollHeight);
349 e.returnValue = false;
350 // Fire the scroll lock event.
351 triggerEvent(events.scrollLock, e);
354 } else if (up && delta > scrollTop) {
355 // Scrolling up past the top.
356 element.scrollTop(0);
359 e.returnValue = false;
360 // Fire the scroll lock event.
361 triggerEvent(events.scrollLock, e);
371 * Trigger the ctrlPageUp event on the given element if the user presses the
372 * control and page up key.
374 * @method addCtrlPageUpListener
376 * @param {object} element jQuery object to add event listeners to
378 var addCtrlPageUpListener = function(element) {
379 element.off('keydown.cie.ctrlpageup').on('keydown.cie.ctrlpageup', function(e) {
381 if (e.keyCode == keyCodes.pageUp) {
382 triggerEvent(events.ctrlPageUp, e);
389 * Trigger the ctrlPageDown event on the given element if the user presses the
390 * control and page down key.
392 * @method addCtrlPageDownListener
394 * @param {object} element jQuery object to add event listeners to
396 var addCtrlPageDownListener = function(element) {
397 element.off('keydown.cie.ctrlpagedown').on('keydown.cie.ctrlpagedown', function(e) {
399 if (e.keyCode == keyCodes.pageDown) {
400 triggerEvent(events.ctrlPageDown, e);
407 * Trigger the enter event on the given element if the enter key is pressed
408 * without a modifier key.
410 * @method addEnterListener
412 * @param {object} element jQuery object to add event listeners to
414 var addEnterListener = function(element) {
415 addKeyboardEvent(element, events.enter, keyCodes.enter);
419 * Get the list of events and their handlers.
421 * @method getHandlers
423 * @return {object} object key of event names and value of handler functions
425 var getHandlers = function() {
428 handlers[events.activate] = addActivateListener;
429 handlers[events.keyboardActivate] = addKeyboardActivateListener;
430 handlers[events.escape] = addEscapeListener;
431 handlers[events.down] = addDownListener;
432 handlers[events.up] = addUpListener;
433 handlers[events.home] = addHomeListener;
434 handlers[events.end] = addEndListener;
435 handlers[events.next] = addNextListener;
436 handlers[events.previous] = addPreviousListener;
437 handlers[events.asterix] = addAsterixListener;
438 handlers[events.scrollLock] = addScrollLockListener;
439 handlers[events.scrollTop] = addScrollTopListener;
440 handlers[events.scrollBottom] = addScrollBottomListener;
441 handlers[events.ctrlPageUp] = addCtrlPageUpListener;
442 handlers[events.ctrlPageDown] = addCtrlPageDownListener;
443 handlers[events.enter] = addEnterListener;
449 * Add all of the listeners on the given element for the requested events.
453 * @param {object} element the DOM element to register event listeners on
454 * @param {array} include the array of events to be triggered
456 var define = function(element, include) {
457 element = $(element);
458 include = include || [];
460 if (!element.length || !include.length) {
464 $.each(getHandlers(), function(eventType, handler) {
465 if (shouldAddEvent(eventType, include)) {
471 return /** @module core/custom_interaction_events */ {