MDL-61138 javascript: stop duplicate custom events firing
[moodle.git] / lib / amd / src / custom_interaction_events.js
CommitLineData
e845b96b
RW
1// This file is part of Moodle - http://moodle.org/
2//
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.
7//
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.
12//
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/>.
15
16/**
17 * This module provides a wrapper to encapsulate a lot of the common combinations of
18 * user interaction we use in Moodle.
19 *
20 * @module core/custom_interaction_events
21 * @class custom_interaction_events
22 * @package core
23 * @copyright 2016 Ryan Wyllie <ryan@moodle.com>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 * @since 3.2
26 */
27define(['jquery', 'core/key_codes'], function($, keyCodes) {
28 // The list of events provided by this module. Namespaced to avoid clashes.
29 var events = {
30 activate: 'cie:activate',
31 keyboardActivate: 'cie:keyboardactivate',
32 escape: 'cie:escape',
33 down: 'cie:down',
34 up: 'cie:up',
35 home: 'cie:home',
36 end: 'cie:end',
37 next: 'cie:next',
38 previous: 'cie:previous',
39 asterix: 'cie:asterix',
99c7f0a7 40 scrollLock: 'cie:scrollLock',
e845b96b
RW
41 scrollTop: 'cie:scrollTop',
42 scrollBottom: 'cie:scrollBottom',
43 ctrlPageUp: 'cie:ctrlPageUp',
44 ctrlPageDown: 'cie:ctrlPageDown',
45 enter: 'cie:enter',
46 };
90d8c85e
RW
47 // Static cache of jQuery events that have been handled. This should
48 // only be populated by JavaScript generated events (which will keep it
49 // fairly small).
50 var triggeredEvents = {};
e845b96b
RW
51
52 /**
53 * Check if the caller has asked for the given event type to be
54 * registered.
55 *
56 * @method shouldAddEvent
57 * @private
58 * @param {string} eventType name of the event (see events above)
59 * @param {array} include the list of events to be added
2bcef559 60 * @return {bool} true if the event should be added, false otherwise.
e845b96b
RW
61 */
62 var shouldAddEvent = function(eventType, include) {
63 include = include || [];
64
65 if (include.length && include.indexOf(eventType) !== -1) {
66 return true;
67 }
68
69 return false;
70 };
71
72 /**
73 * Check if any of the modifier keys have been pressed on the event.
74 *
75 * @method isModifierPressed
76 * @private
77 * @param {event} e jQuery event
2bcef559 78 * @return {bool} true if shift, meta (command on Mac), alt or ctrl are pressed
e845b96b
RW
79 */
80 var isModifierPressed = function(e) {
81 return (e.shiftKey || e.metaKey || e.altKey || e.ctrlKey);
82 };
83
90d8c85e
RW
84 /**
85 * Trigger the custom event for the given jQuery event.
86 *
87 * This function will only fire the custom event if one hasn't already been
88 * fired for the jQuery event.
89 *
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.
92 *
93 * @param {string} eventName The name of the custom event
94 * @param {event} e The jQuery event
95 * @return {void}
96 */
97 var triggerEvent = function(eventName, e) {
98 var eventTypeKey = "";
99
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;
104
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}]);
110 }
111 return;
112 }
113
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}]);
122 return;
123 }
124 };
125
e845b96b
RW
126 /**
127 * Register a keyboard event that ignores modifier keys.
128 *
129 * @method addKeyboardEvent
130 * @private
2bcef559 131 * @param {object} element A jQuery object of the element to bind events to
e845b96b
RW
132 * @param {string} event The custom interaction event name
133 * @param {int} keyCode The key code.
134 */
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) {
90d8c85e 139 triggerEvent(event, e);
e845b96b
RW
140 }
141 }
142 });
143 };
144
145 /**
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.
148 *
149 * @method addActivateListener
150 * @private
2bcef559 151 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
152 */
153 var addActivateListener = function(element) {
154 element.off('click.cie.activate').on('click.cie.activate', function(e) {
90d8c85e 155 triggerEvent(events.activate, e);
e845b96b
RW
156 });
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) {
90d8c85e 160 triggerEvent(events.activate, e);
e845b96b
RW
161 }
162 }
163 });
164 };
165
166 /**
167 * Trigger the keyboard activate event on the given element if the enter
168 * or space key are pressed without a modifier key.
169 *
170 * @method addKeyboardActivateListener
171 * @private
2bcef559 172 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
173 */
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) {
90d8c85e 178 triggerEvent(events.keyboardActivate, e);
e845b96b
RW
179 }
180 }
181 });
182 };
183
184 /**
185 * Trigger the escape event on the given element if the escape key is pressed
186 * without a modifier key.
187 *
188 * @method addEscapeListener
189 * @private
2bcef559 190 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
191 */
192 var addEscapeListener = function(element) {
193 addKeyboardEvent(element, events.escape, keyCodes.escape);
194 };
195
196 /**
197 * Trigger the down event on the given element if the down arrow key is pressed
198 * without a modifier key.
199 *
200 * @method addDownListener
201 * @private
2bcef559 202 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
203 */
204 var addDownListener = function(element) {
205 addKeyboardEvent(element, events.down, keyCodes.arrowDown);
206 };
207
208 /**
209 * Trigger the up event on the given element if the up arrow key is pressed
210 * without a modifier key.
211 *
212 * @method addUpListener
213 * @private
2bcef559 214 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
215 */
216 var addUpListener = function(element) {
217 addKeyboardEvent(element, events.up, keyCodes.arrowUp);
218 };
219
220 /**
221 * Trigger the home event on the given element if the home key is pressed
222 * without a modifier key.
223 *
224 * @method addHomeListener
225 * @private
2bcef559 226 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
227 */
228 var addHomeListener = function(element) {
229 addKeyboardEvent(element, events.home, keyCodes.home);
230 };
231
232 /**
233 * Trigger the end event on the given element if the end key is pressed
234 * without a modifier key.
235 *
236 * @method addEndListener
237 * @private
2bcef559 238 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
239 */
240 var addEndListener = function(element) {
241 addKeyboardEvent(element, events.end, keyCodes.end);
242 };
243
244 /**
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.
247 *
248 * @method addNextListener
249 * @private
2bcef559 250 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
251 */
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;
255
256 addKeyboardEvent(element, events.next, keyCode);
257 };
258
259 /**
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.
262 *
263 * @method addPreviousListener
264 * @private
2bcef559 265 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
266 */
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;
270
271 addKeyboardEvent(element, events.previous, keyCode);
272 };
273
274 /**
275 * Trigger the asterix event on the given element if the asterix key is pressed
276 * without a modifier key.
277 *
278 * @method addAsterixListener
279 * @private
2bcef559 280 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
281 */
282 var addAsterixListener = function(element) {
283 addKeyboardEvent(element, events.asterix, keyCodes.asterix);
284 };
285
286
287 /**
288 * Trigger the scrollTop event on the given element if the user scrolls to
289 * the top of the given element.
290 *
291 * @method addScrollTopListener
292 * @private
2bcef559 293 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
294 */
295 var addScrollTopListener = function(element) {
99c7f0a7 296 element.off('scroll.cie.scrollTop').on('scroll.cie.scrollTop', function(e) {
e845b96b
RW
297 var scrollTop = element.scrollTop();
298 if (scrollTop === 0) {
90d8c85e 299 triggerEvent(events.scrollTop, e);
e845b96b
RW
300 }
301 });
302 };
303
304 /**
305 * Trigger the scrollBottom event on the given element if the user scrolls to
306 * the bottom of the given element.
307 *
308 * @method addScrollBottomListener
309 * @private
2bcef559 310 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
311 */
312 var addScrollBottomListener = function(element) {
99c7f0a7 313 element.off('scroll.cie.scrollBottom').on('scroll.cie.scrollBottom', function(e) {
e845b96b
RW
314 var scrollTop = element.scrollTop();
315 var innerHeight = element.innerHeight();
316 var scrollHeight = element[0].scrollHeight;
317
318 if (scrollTop + innerHeight >= scrollHeight) {
90d8c85e 319 triggerEvent(events.scrollBottom, e);
e845b96b
RW
320 }
321 });
322 };
323
99c7f0a7
RW
324 /**
325 * Trigger the scrollLock event on the given element if the user scrolls to
326 * the bottom or top of the given element.
327 *
328 * @method addScrollLockListener
329 * @private
7b55aaa1 330 * @param {jQuery} element jQuery object to add event listeners to
99c7f0a7
RW
331 */
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);
342 var up = delta > 0;
343
344 if (!up && -delta > scrollHeight - height - scrollTop) {
345 // Scrolling down past the bottom.
346 element.scrollTop(scrollHeight);
347 e.stopPropagation();
348 e.preventDefault();
349 e.returnValue = false;
350 // Fire the scroll lock event.
90d8c85e 351 triggerEvent(events.scrollLock, e);
99c7f0a7
RW
352
353 return false;
354 } else if (up && delta > scrollTop) {
355 // Scrolling up past the top.
356 element.scrollTop(0);
357 e.stopPropagation();
358 e.preventDefault();
359 e.returnValue = false;
360 // Fire the scroll lock event.
90d8c85e 361 triggerEvent(events.scrollLock, e);
99c7f0a7
RW
362
363 return false;
364 }
7b55aaa1
MN
365
366 return true;
99c7f0a7
RW
367 });
368 };
369
e845b96b
RW
370 /**
371 * Trigger the ctrlPageUp event on the given element if the user presses the
372 * control and page up key.
373 *
374 * @method addCtrlPageUpListener
375 * @private
2bcef559 376 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
377 */
378 var addCtrlPageUpListener = function(element) {
379 element.off('keydown.cie.ctrlpageup').on('keydown.cie.ctrlpageup', function(e) {
380 if (e.ctrlKey) {
381 if (e.keyCode == keyCodes.pageUp) {
90d8c85e 382 triggerEvent(events.ctrlPageUp, e);
e845b96b
RW
383 }
384 }
385 });
386 };
387
388 /**
389 * Trigger the ctrlPageDown event on the given element if the user presses the
390 * control and page down key.
391 *
392 * @method addCtrlPageDownListener
393 * @private
2bcef559 394 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
395 */
396 var addCtrlPageDownListener = function(element) {
397 element.off('keydown.cie.ctrlpagedown').on('keydown.cie.ctrlpagedown', function(e) {
398 if (e.ctrlKey) {
399 if (e.keyCode == keyCodes.pageDown) {
90d8c85e 400 triggerEvent(events.ctrlPageDown, e);
e845b96b
RW
401 }
402 }
403 });
404 };
405
406 /**
407 * Trigger the enter event on the given element if the enter key is pressed
408 * without a modifier key.
409 *
410 * @method addEnterListener
411 * @private
2bcef559 412 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
413 */
414 var addEnterListener = function(element) {
415 addKeyboardEvent(element, events.enter, keyCodes.enter);
416 };
417
418 /**
419 * Get the list of events and their handlers.
420 *
421 * @method getHandlers
422 * @private
2bcef559 423 * @return {object} object key of event names and value of handler functions
e845b96b
RW
424 */
425 var getHandlers = function() {
426 var handlers = {};
427
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;
99c7f0a7 438 handlers[events.scrollLock] = addScrollLockListener;
e845b96b
RW
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;
444
445 return handlers;
446 };
447
448 /**
449 * Add all of the listeners on the given element for the requested events.
450 *
451 * @method define
452 * @public
2bcef559 453 * @param {object} element the DOM element to register event listeners on
e845b96b
RW
454 * @param {array} include the array of events to be triggered
455 */
456 var define = function(element, include) {
457 element = $(element);
458 include = include || [];
459
460 if (!element.length || !include.length) {
461 return;
462 }
463
464 $.each(getHandlers(), function(eventType, handler) {
465 if (shouldAddEvent(eventType, include)) {
466 handler(element);
467 }
468 });
469 };
470
471 return /** @module core/custom_interaction_events */ {
472 define: define,
473 events: events,
474 };
475});