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', | |
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 | }); |