Commit | Line | Data |
---|---|---|
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 | */ | |
27 | define(['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', | |
505cc918 | 46 | accessibleChange: 'cie:accessibleChange', |
e845b96b | 47 | }; |
90d8c85e RW |
48 | // Static cache of jQuery events that have been handled. This should |
49 | // only be populated by JavaScript generated events (which will keep it | |
50 | // fairly small). | |
51 | var triggeredEvents = {}; | |
e845b96b RW |
52 | |
53 | /** | |
54 | * Check if the caller has asked for the given event type to be | |
55 | * registered. | |
56 | * | |
57 | * @method shouldAddEvent | |
58 | * @private | |
59 | * @param {string} eventType name of the event (see events above) | |
60 | * @param {array} include the list of events to be added | |
2bcef559 | 61 | * @return {bool} true if the event should be added, false otherwise. |
e845b96b RW |
62 | */ |
63 | var shouldAddEvent = function(eventType, include) { | |
64 | include = include || []; | |
65 | ||
66 | if (include.length && include.indexOf(eventType) !== -1) { | |
67 | return true; | |
68 | } | |
69 | ||
70 | return false; | |
71 | }; | |
72 | ||
73 | /** | |
74 | * Check if any of the modifier keys have been pressed on the event. | |
75 | * | |
76 | * @method isModifierPressed | |
77 | * @private | |
78 | * @param {event} e jQuery event | |
2bcef559 | 79 | * @return {bool} true if shift, meta (command on Mac), alt or ctrl are pressed |
e845b96b RW |
80 | */ |
81 | var isModifierPressed = function(e) { | |
82 | return (e.shiftKey || e.metaKey || e.altKey || e.ctrlKey); | |
83 | }; | |
84 | ||
90d8c85e RW |
85 | /** |
86 | * Trigger the custom event for the given jQuery event. | |
87 | * | |
88 | * This function will only fire the custom event if one hasn't already been | |
89 | * fired for the jQuery event. | |
90 | * | |
91 | * This is to prevent multiple custom event handlers triggering multiple | |
92 | * custom events for a single jQuery event as it bubbles up the stack. | |
93 | * | |
94 | * @param {string} eventName The name of the custom event | |
95 | * @param {event} e The jQuery event | |
96 | * @return {void} | |
97 | */ | |
98 | var triggerEvent = function(eventName, e) { | |
99 | var eventTypeKey = ""; | |
100 | ||
101 | if (!e.hasOwnProperty('originalEvent')) { | |
102 | // This is a jQuery event generated from JavaScript not a browser event so | |
103 | // we need to build the cache key for the event. | |
104 | eventTypeKey = "" + eventName + e.type + e.timeStamp; | |
105 | ||
106 | if (!triggeredEvents.hasOwnProperty(eventTypeKey)) { | |
107 | // If we haven't seen this jQuery event before then fire a custom | |
108 | // event for it and remember the event for later. | |
109 | triggeredEvents[eventTypeKey] = true; | |
110 | $(e.target).trigger(eventName, [{originalEvent: e}]); | |
111 | } | |
112 | return; | |
113 | } | |
114 | ||
115 | eventTypeKey = "triggeredCustom_" + eventName; | |
116 | if (!e.originalEvent.hasOwnProperty(eventTypeKey)) { | |
117 | // If this is a jQuery event generated by the browser then set a | |
118 | // property on the original event to track that we've seen it before. | |
119 | // The property is set on the original event because it's the only part | |
120 | // of the jQuery event that is maintained through multiple event handlers. | |
121 | e.originalEvent[eventTypeKey] = true; | |
122 | $(e.target).trigger(eventName, [{originalEvent: e}]); | |
123 | return; | |
124 | } | |
125 | }; | |
126 | ||
e845b96b RW |
127 | /** |
128 | * Register a keyboard event that ignores modifier keys. | |
129 | * | |
130 | * @method addKeyboardEvent | |
131 | * @private | |
2bcef559 | 132 | * @param {object} element A jQuery object of the element to bind events to |
e845b96b RW |
133 | * @param {string} event The custom interaction event name |
134 | * @param {int} keyCode The key code. | |
135 | */ | |
136 | var addKeyboardEvent = function(element, event, keyCode) { | |
137 | element.off('keydown.' + event).on('keydown.' + event, function(e) { | |
138 | if (!isModifierPressed(e)) { | |
139 | if (e.keyCode == keyCode) { | |
90d8c85e | 140 | triggerEvent(event, e); |
e845b96b RW |
141 | } |
142 | } | |
143 | }); | |
144 | }; | |
145 | ||
146 | /** | |
147 | * Trigger the activate event on the given element if it is clicked or the enter | |
148 | * or space key are pressed without a modifier key. | |
149 | * | |
150 | * @method addActivateListener | |
151 | * @private | |
2bcef559 | 152 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
153 | */ |
154 | var addActivateListener = function(element) { | |
155 | element.off('click.cie.activate').on('click.cie.activate', function(e) { | |
90d8c85e | 156 | triggerEvent(events.activate, e); |
e845b96b RW |
157 | }); |
158 | element.off('keydown.cie.activate').on('keydown.cie.activate', function(e) { | |
159 | if (!isModifierPressed(e)) { | |
160 | if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) { | |
90d8c85e | 161 | triggerEvent(events.activate, e); |
e845b96b RW |
162 | } |
163 | } | |
164 | }); | |
165 | }; | |
166 | ||
167 | /** | |
168 | * Trigger the keyboard activate event on the given element if the enter | |
169 | * or space key are pressed without a modifier key. | |
170 | * | |
171 | * @method addKeyboardActivateListener | |
172 | * @private | |
2bcef559 | 173 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
174 | */ |
175 | var addKeyboardActivateListener = function(element) { | |
176 | element.off('keydown.cie.keyboardactivate').on('keydown.cie.keyboardactivate', function(e) { | |
177 | if (!isModifierPressed(e)) { | |
178 | if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) { | |
90d8c85e | 179 | triggerEvent(events.keyboardActivate, e); |
e845b96b RW |
180 | } |
181 | } | |
182 | }); | |
183 | }; | |
184 | ||
185 | /** | |
186 | * Trigger the escape event on the given element if the escape key is pressed | |
187 | * without a modifier key. | |
188 | * | |
189 | * @method addEscapeListener | |
190 | * @private | |
2bcef559 | 191 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
192 | */ |
193 | var addEscapeListener = function(element) { | |
194 | addKeyboardEvent(element, events.escape, keyCodes.escape); | |
195 | }; | |
196 | ||
197 | /** | |
198 | * Trigger the down event on the given element if the down arrow key is pressed | |
199 | * without a modifier key. | |
200 | * | |
201 | * @method addDownListener | |
202 | * @private | |
2bcef559 | 203 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
204 | */ |
205 | var addDownListener = function(element) { | |
206 | addKeyboardEvent(element, events.down, keyCodes.arrowDown); | |
207 | }; | |
208 | ||
209 | /** | |
210 | * Trigger the up event on the given element if the up arrow key is pressed | |
211 | * without a modifier key. | |
212 | * | |
213 | * @method addUpListener | |
214 | * @private | |
2bcef559 | 215 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
216 | */ |
217 | var addUpListener = function(element) { | |
218 | addKeyboardEvent(element, events.up, keyCodes.arrowUp); | |
219 | }; | |
220 | ||
221 | /** | |
222 | * Trigger the home event on the given element if the home key is pressed | |
223 | * without a modifier key. | |
224 | * | |
225 | * @method addHomeListener | |
226 | * @private | |
2bcef559 | 227 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
228 | */ |
229 | var addHomeListener = function(element) { | |
230 | addKeyboardEvent(element, events.home, keyCodes.home); | |
231 | }; | |
232 | ||
233 | /** | |
234 | * Trigger the end event on the given element if the end key is pressed | |
235 | * without a modifier key. | |
236 | * | |
237 | * @method addEndListener | |
238 | * @private | |
2bcef559 | 239 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
240 | */ |
241 | var addEndListener = function(element) { | |
242 | addKeyboardEvent(element, events.end, keyCodes.end); | |
243 | }; | |
244 | ||
245 | /** | |
246 | * Trigger the next event on the given element if the right arrow key is pressed | |
247 | * without a modifier key in LTR mode or left arrow key in RTL mode. | |
248 | * | |
249 | * @method addNextListener | |
250 | * @private | |
2bcef559 | 251 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
252 | */ |
253 | var addNextListener = function(element) { | |
254 | // Left and right are flipped in RTL mode. | |
255 | var keyCode = $('html').attr('dir') == "rtl" ? keyCodes.arrowLeft : keyCodes.arrowRight; | |
256 | ||
257 | addKeyboardEvent(element, events.next, keyCode); | |
258 | }; | |
259 | ||
260 | /** | |
261 | * Trigger the previous event on the given element if the left arrow key is pressed | |
262 | * without a modifier key in LTR mode or right arrow key in RTL mode. | |
263 | * | |
264 | * @method addPreviousListener | |
265 | * @private | |
2bcef559 | 266 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
267 | */ |
268 | var addPreviousListener = function(element) { | |
269 | // Left and right are flipped in RTL mode. | |
270 | var keyCode = $('html').attr('dir') == "rtl" ? keyCodes.arrowRight : keyCodes.arrowLeft; | |
271 | ||
272 | addKeyboardEvent(element, events.previous, keyCode); | |
273 | }; | |
274 | ||
275 | /** | |
276 | * Trigger the asterix event on the given element if the asterix key is pressed | |
277 | * without a modifier key. | |
278 | * | |
279 | * @method addAsterixListener | |
280 | * @private | |
2bcef559 | 281 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
282 | */ |
283 | var addAsterixListener = function(element) { | |
284 | addKeyboardEvent(element, events.asterix, keyCodes.asterix); | |
285 | }; | |
286 | ||
287 | ||
288 | /** | |
289 | * Trigger the scrollTop event on the given element if the user scrolls to | |
290 | * the top of the given element. | |
291 | * | |
292 | * @method addScrollTopListener | |
293 | * @private | |
2bcef559 | 294 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
295 | */ |
296 | var addScrollTopListener = function(element) { | |
99c7f0a7 | 297 | element.off('scroll.cie.scrollTop').on('scroll.cie.scrollTop', function(e) { |
e845b96b RW |
298 | var scrollTop = element.scrollTop(); |
299 | if (scrollTop === 0) { | |
90d8c85e | 300 | triggerEvent(events.scrollTop, e); |
e845b96b RW |
301 | } |
302 | }); | |
303 | }; | |
304 | ||
305 | /** | |
306 | * Trigger the scrollBottom event on the given element if the user scrolls to | |
307 | * the bottom of the given element. | |
308 | * | |
309 | * @method addScrollBottomListener | |
310 | * @private | |
2bcef559 | 311 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
312 | */ |
313 | var addScrollBottomListener = function(element) { | |
99c7f0a7 | 314 | element.off('scroll.cie.scrollBottom').on('scroll.cie.scrollBottom', function(e) { |
e845b96b RW |
315 | var scrollTop = element.scrollTop(); |
316 | var innerHeight = element.innerHeight(); | |
317 | var scrollHeight = element[0].scrollHeight; | |
318 | ||
319 | if (scrollTop + innerHeight >= scrollHeight) { | |
90d8c85e | 320 | triggerEvent(events.scrollBottom, e); |
e845b96b RW |
321 | } |
322 | }); | |
323 | }; | |
324 | ||
99c7f0a7 RW |
325 | /** |
326 | * Trigger the scrollLock event on the given element if the user scrolls to | |
327 | * the bottom or top of the given element. | |
328 | * | |
329 | * @method addScrollLockListener | |
330 | * @private | |
7b55aaa1 | 331 | * @param {jQuery} element jQuery object to add event listeners to |
99c7f0a7 RW |
332 | */ |
333 | var addScrollLockListener = function(element) { | |
334 | // Lock mousewheel scrolling within the element to stop the annoying window scroll. | |
335 | element.off('DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock') | |
336 | .on('DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock', function(e) { | |
337 | var scrollTop = element.scrollTop(); | |
338 | var scrollHeight = element[0].scrollHeight; | |
339 | var height = element.height(); | |
340 | var delta = (e.type == 'DOMMouseScroll' ? | |
341 | e.originalEvent.detail * -40 : | |
342 | e.originalEvent.wheelDelta); | |
343 | var up = delta > 0; | |
344 | ||
345 | if (!up && -delta > scrollHeight - height - scrollTop) { | |
346 | // Scrolling down past the bottom. | |
347 | element.scrollTop(scrollHeight); | |
348 | e.stopPropagation(); | |
349 | e.preventDefault(); | |
350 | e.returnValue = false; | |
351 | // Fire the scroll lock event. | |
90d8c85e | 352 | triggerEvent(events.scrollLock, e); |
99c7f0a7 RW |
353 | |
354 | return false; | |
355 | } else if (up && delta > scrollTop) { | |
356 | // Scrolling up past the top. | |
357 | element.scrollTop(0); | |
358 | e.stopPropagation(); | |
359 | e.preventDefault(); | |
360 | e.returnValue = false; | |
361 | // Fire the scroll lock event. | |
90d8c85e | 362 | triggerEvent(events.scrollLock, e); |
99c7f0a7 RW |
363 | |
364 | return false; | |
365 | } | |
7b55aaa1 MN |
366 | |
367 | return true; | |
99c7f0a7 RW |
368 | }); |
369 | }; | |
370 | ||
e845b96b RW |
371 | /** |
372 | * Trigger the ctrlPageUp event on the given element if the user presses the | |
373 | * control and page up key. | |
374 | * | |
375 | * @method addCtrlPageUpListener | |
376 | * @private | |
2bcef559 | 377 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
378 | */ |
379 | var addCtrlPageUpListener = function(element) { | |
380 | element.off('keydown.cie.ctrlpageup').on('keydown.cie.ctrlpageup', function(e) { | |
381 | if (e.ctrlKey) { | |
382 | if (e.keyCode == keyCodes.pageUp) { | |
90d8c85e | 383 | triggerEvent(events.ctrlPageUp, e); |
e845b96b RW |
384 | } |
385 | } | |
386 | }); | |
387 | }; | |
388 | ||
389 | /** | |
390 | * Trigger the ctrlPageDown event on the given element if the user presses the | |
391 | * control and page down key. | |
392 | * | |
393 | * @method addCtrlPageDownListener | |
394 | * @private | |
2bcef559 | 395 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
396 | */ |
397 | var addCtrlPageDownListener = function(element) { | |
398 | element.off('keydown.cie.ctrlpagedown').on('keydown.cie.ctrlpagedown', function(e) { | |
399 | if (e.ctrlKey) { | |
400 | if (e.keyCode == keyCodes.pageDown) { | |
90d8c85e | 401 | triggerEvent(events.ctrlPageDown, e); |
e845b96b RW |
402 | } |
403 | } | |
404 | }); | |
405 | }; | |
406 | ||
407 | /** | |
408 | * Trigger the enter event on the given element if the enter key is pressed | |
409 | * without a modifier key. | |
410 | * | |
411 | * @method addEnterListener | |
412 | * @private | |
2bcef559 | 413 | * @param {object} element jQuery object to add event listeners to |
e845b96b RW |
414 | */ |
415 | var addEnterListener = function(element) { | |
416 | addKeyboardEvent(element, events.enter, keyCodes.enter); | |
417 | }; | |
418 | ||
505cc918 SR |
419 | /** |
420 | * Trigger the AccessibleChange event on the given element if the value of the element is changed. | |
421 | * | |
422 | * @method addAccessibleChangeListener | |
423 | * @private | |
424 | * @param {object} element jQuery object to add event listeners to | |
425 | */ | |
426 | var addAccessibleChangeListener = function(element) { | |
427 | var onMac = navigator.userAgent.indexOf('Macintosh') !== -1; | |
428 | var touchEnabled = ('ontouchstart' in window) || (('msMaxTouchPoints' in navigator) && (navigator.msMaxTouchPoints > 0)); | |
429 | if (onMac || touchEnabled) { | |
430 | element.on('change', function(e) { | |
431 | triggerEvent(events.accessibleChange, e); | |
432 | }); | |
433 | } else { | |
3caa6bfb AN |
434 | var nativeElement = element.get()[0]; |
435 | // The `focus` and `blur` events do not support bubbling. Use Event Capture instead. | |
436 | nativeElement.addEventListener('focus', function(e) { | |
15a349dd | 437 | $(e.target).data('initValue', e.target.value); |
3caa6bfb AN |
438 | }, true); |
439 | nativeElement.addEventListener('blur', function(e) { | |
15a349dd SR |
440 | var initValue = $(e.target).data('initValue'); |
441 | $(e.target).removeData('initValue'); | |
442 | if (e.target.value !== initValue) { | |
505cc918 SR |
443 | triggerEvent(events.accessibleChange, e); |
444 | } | |
3caa6bfb | 445 | }, true); |
505cc918 | 446 | element.on('keydown', function(e) { |
15a349dd | 447 | if ((e.which === keyCodes.enter) && e.target.value !== $(e.target).data('initValue')) { |
505cc918 SR |
448 | triggerEvent(events.accessibleChange, e); |
449 | } else if (e.which === keyCodes.escape) { | |
15a349dd | 450 | e.target.value = $(e.target).data('initValue'); |
505cc918 SR |
451 | } |
452 | }); | |
453 | element.on('click', function(e) { | |
15a349dd | 454 | var initValue = $(e.target).data('initValue'); |
505cc918 | 455 | // Some browsers trigger onclick before onblur, therefore it is possible that initValue is undefined. |
15a349dd | 456 | if (typeof initValue !== 'undefined' && initValue != e.target.value) { |
505cc918 SR |
457 | triggerEvent(events.accessibleChange, e); |
458 | } | |
459 | }); | |
460 | } | |
461 | }; | |
462 | ||
e845b96b RW |
463 | /** |
464 | * Get the list of events and their handlers. | |
465 | * | |
466 | * @method getHandlers | |
467 | * @private | |
2bcef559 | 468 | * @return {object} object key of event names and value of handler functions |
e845b96b RW |
469 | */ |
470 | var getHandlers = function() { | |
471 | var handlers = {}; | |
472 | ||
473 | handlers[events.activate] = addActivateListener; | |
474 | handlers[events.keyboardActivate] = addKeyboardActivateListener; | |
475 | handlers[events.escape] = addEscapeListener; | |
476 | handlers[events.down] = addDownListener; | |
477 | handlers[events.up] = addUpListener; | |
478 | handlers[events.home] = addHomeListener; | |
479 | handlers[events.end] = addEndListener; | |
480 | handlers[events.next] = addNextListener; | |
481 | handlers[events.previous] = addPreviousListener; | |
482 | handlers[events.asterix] = addAsterixListener; | |
99c7f0a7 | 483 | handlers[events.scrollLock] = addScrollLockListener; |
e845b96b RW |
484 | handlers[events.scrollTop] = addScrollTopListener; |
485 | handlers[events.scrollBottom] = addScrollBottomListener; | |
486 | handlers[events.ctrlPageUp] = addCtrlPageUpListener; | |
487 | handlers[events.ctrlPageDown] = addCtrlPageDownListener; | |
488 | handlers[events.enter] = addEnterListener; | |
505cc918 | 489 | handlers[events.accessibleChange] = addAccessibleChangeListener; |
e845b96b RW |
490 | |
491 | return handlers; | |
492 | }; | |
493 | ||
494 | /** | |
495 | * Add all of the listeners on the given element for the requested events. | |
496 | * | |
497 | * @method define | |
498 | * @public | |
2bcef559 | 499 | * @param {object} element the DOM element to register event listeners on |
e845b96b RW |
500 | * @param {array} include the array of events to be triggered |
501 | */ | |
502 | var define = function(element, include) { | |
503 | element = $(element); | |
504 | include = include || []; | |
505 | ||
506 | if (!element.length || !include.length) { | |
507 | return; | |
508 | } | |
509 | ||
510 | $.each(getHandlers(), function(eventType, handler) { | |
511 | if (shouldAddEvent(eventType, include)) { | |
512 | handler(element); | |
513 | } | |
514 | }); | |
515 | }; | |
516 | ||
517 | return /** @module core/custom_interaction_events */ { | |
518 | define: define, | |
519 | events: events, | |
520 | }; | |
521 | }); |