weekly release 3.5dev
[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 };
47
48 /**
49 * Check if the caller has asked for the given event type to be
50 * registered.
51 *
52 * @method shouldAddEvent
53 * @private
54 * @param {string} eventType name of the event (see events above)
55 * @param {array} include the list of events to be added
2bcef559 56 * @return {bool} true if the event should be added, false otherwise.
e845b96b
RW
57 */
58 var shouldAddEvent = function(eventType, include) {
59 include = include || [];
60
61 if (include.length && include.indexOf(eventType) !== -1) {
62 return true;
63 }
64
65 return false;
66 };
67
68 /**
69 * Check if any of the modifier keys have been pressed on the event.
70 *
71 * @method isModifierPressed
72 * @private
73 * @param {event} e jQuery event
2bcef559 74 * @return {bool} true if shift, meta (command on Mac), alt or ctrl are pressed
e845b96b
RW
75 */
76 var isModifierPressed = function(e) {
77 return (e.shiftKey || e.metaKey || e.altKey || e.ctrlKey);
78 };
79
80 /**
81 * Register a keyboard event that ignores modifier keys.
82 *
83 * @method addKeyboardEvent
84 * @private
2bcef559 85 * @param {object} element A jQuery object of the element to bind events to
e845b96b
RW
86 * @param {string} event The custom interaction event name
87 * @param {int} keyCode The key code.
88 */
89 var addKeyboardEvent = function(element, event, keyCode) {
90 element.off('keydown.' + event).on('keydown.' + event, function(e) {
91 if (!isModifierPressed(e)) {
92 if (e.keyCode == keyCode) {
93 $(e.target).trigger(event, [{originalEvent: e}]);
94 }
95 }
96 });
97 };
98
99 /**
100 * Trigger the activate event on the given element if it is clicked or the enter
101 * or space key are pressed without a modifier key.
102 *
103 * @method addActivateListener
104 * @private
2bcef559 105 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
106 */
107 var addActivateListener = function(element) {
108 element.off('click.cie.activate').on('click.cie.activate', function(e) {
109 $(e.target).trigger(events.activate, [{originalEvent: e}]);
110 });
111 element.off('keydown.cie.activate').on('keydown.cie.activate', function(e) {
112 if (!isModifierPressed(e)) {
113 if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) {
114 $(e.target).trigger(events.activate, [{originalEvent: e}]);
115 }
116 }
117 });
118 };
119
120 /**
121 * Trigger the keyboard activate event on the given element if the enter
122 * or space key are pressed without a modifier key.
123 *
124 * @method addKeyboardActivateListener
125 * @private
2bcef559 126 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
127 */
128 var addKeyboardActivateListener = function(element) {
129 element.off('keydown.cie.keyboardactivate').on('keydown.cie.keyboardactivate', function(e) {
130 if (!isModifierPressed(e)) {
131 if (e.keyCode == keyCodes.enter || e.keyCode == keyCodes.space) {
132 $(e.target).trigger(events.keyboardActivate, [{originalEvent: e}]);
133 }
134 }
135 });
136 };
137
138 /**
139 * Trigger the escape event on the given element if the escape key is pressed
140 * without a modifier key.
141 *
142 * @method addEscapeListener
143 * @private
2bcef559 144 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
145 */
146 var addEscapeListener = function(element) {
147 addKeyboardEvent(element, events.escape, keyCodes.escape);
148 };
149
150 /**
151 * Trigger the down event on the given element if the down arrow key is pressed
152 * without a modifier key.
153 *
154 * @method addDownListener
155 * @private
2bcef559 156 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
157 */
158 var addDownListener = function(element) {
159 addKeyboardEvent(element, events.down, keyCodes.arrowDown);
160 };
161
162 /**
163 * Trigger the up event on the given element if the up arrow key is pressed
164 * without a modifier key.
165 *
166 * @method addUpListener
167 * @private
2bcef559 168 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
169 */
170 var addUpListener = function(element) {
171 addKeyboardEvent(element, events.up, keyCodes.arrowUp);
172 };
173
174 /**
175 * Trigger the home event on the given element if the home key is pressed
176 * without a modifier key.
177 *
178 * @method addHomeListener
179 * @private
2bcef559 180 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
181 */
182 var addHomeListener = function(element) {
183 addKeyboardEvent(element, events.home, keyCodes.home);
184 };
185
186 /**
187 * Trigger the end event on the given element if the end key is pressed
188 * without a modifier key.
189 *
190 * @method addEndListener
191 * @private
2bcef559 192 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
193 */
194 var addEndListener = function(element) {
195 addKeyboardEvent(element, events.end, keyCodes.end);
196 };
197
198 /**
199 * Trigger the next event on the given element if the right arrow key is pressed
200 * without a modifier key in LTR mode or left arrow key in RTL mode.
201 *
202 * @method addNextListener
203 * @private
2bcef559 204 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
205 */
206 var addNextListener = function(element) {
207 // Left and right are flipped in RTL mode.
208 var keyCode = $('html').attr('dir') == "rtl" ? keyCodes.arrowLeft : keyCodes.arrowRight;
209
210 addKeyboardEvent(element, events.next, keyCode);
211 };
212
213 /**
214 * Trigger the previous event on the given element if the left arrow key is pressed
215 * without a modifier key in LTR mode or right arrow key in RTL mode.
216 *
217 * @method addPreviousListener
218 * @private
2bcef559 219 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
220 */
221 var addPreviousListener = function(element) {
222 // Left and right are flipped in RTL mode.
223 var keyCode = $('html').attr('dir') == "rtl" ? keyCodes.arrowRight : keyCodes.arrowLeft;
224
225 addKeyboardEvent(element, events.previous, keyCode);
226 };
227
228 /**
229 * Trigger the asterix event on the given element if the asterix key is pressed
230 * without a modifier key.
231 *
232 * @method addAsterixListener
233 * @private
2bcef559 234 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
235 */
236 var addAsterixListener = function(element) {
237 addKeyboardEvent(element, events.asterix, keyCodes.asterix);
238 };
239
240
241 /**
242 * Trigger the scrollTop event on the given element if the user scrolls to
243 * the top of the given element.
244 *
245 * @method addScrollTopListener
246 * @private
2bcef559 247 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
248 */
249 var addScrollTopListener = function(element) {
99c7f0a7 250 element.off('scroll.cie.scrollTop').on('scroll.cie.scrollTop', function(e) {
e845b96b
RW
251 var scrollTop = element.scrollTop();
252 if (scrollTop === 0) {
99c7f0a7 253 element.trigger(events.scrollTop, [{originalEvent: e}]);
e845b96b
RW
254 }
255 });
256 };
257
258 /**
259 * Trigger the scrollBottom event on the given element if the user scrolls to
260 * the bottom of the given element.
261 *
262 * @method addScrollBottomListener
263 * @private
2bcef559 264 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
265 */
266 var addScrollBottomListener = function(element) {
99c7f0a7 267 element.off('scroll.cie.scrollBottom').on('scroll.cie.scrollBottom', function(e) {
e845b96b
RW
268 var scrollTop = element.scrollTop();
269 var innerHeight = element.innerHeight();
270 var scrollHeight = element[0].scrollHeight;
271
272 if (scrollTop + innerHeight >= scrollHeight) {
99c7f0a7 273 element.trigger(events.scrollBottom, [{originalEvent: e}]);
e845b96b
RW
274 }
275 });
276 };
277
99c7f0a7
RW
278 /**
279 * Trigger the scrollLock event on the given element if the user scrolls to
280 * the bottom or top of the given element.
281 *
282 * @method addScrollLockListener
283 * @private
7b55aaa1 284 * @param {jQuery} element jQuery object to add event listeners to
99c7f0a7
RW
285 */
286 var addScrollLockListener = function(element) {
287 // Lock mousewheel scrolling within the element to stop the annoying window scroll.
288 element.off('DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock')
289 .on('DOMMouseScroll.cie.DOMMouseScrollLock mousewheel.cie.mousewheelLock', function(e) {
290 var scrollTop = element.scrollTop();
291 var scrollHeight = element[0].scrollHeight;
292 var height = element.height();
293 var delta = (e.type == 'DOMMouseScroll' ?
294 e.originalEvent.detail * -40 :
295 e.originalEvent.wheelDelta);
296 var up = delta > 0;
297
298 if (!up && -delta > scrollHeight - height - scrollTop) {
299 // Scrolling down past the bottom.
300 element.scrollTop(scrollHeight);
301 e.stopPropagation();
302 e.preventDefault();
303 e.returnValue = false;
304 // Fire the scroll lock event.
305 element.trigger(events.scrollLock, [{originalEvent: e}]);
306
307 return false;
308 } else if (up && delta > scrollTop) {
309 // Scrolling up past the top.
310 element.scrollTop(0);
311 e.stopPropagation();
312 e.preventDefault();
313 e.returnValue = false;
314 // Fire the scroll lock event.
315 element.trigger(events.scrollLock, [{originalEvent: e}]);
316
317 return false;
318 }
7b55aaa1
MN
319
320 return true;
99c7f0a7
RW
321 });
322 };
323
e845b96b
RW
324 /**
325 * Trigger the ctrlPageUp event on the given element if the user presses the
326 * control and page up key.
327 *
328 * @method addCtrlPageUpListener
329 * @private
2bcef559 330 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
331 */
332 var addCtrlPageUpListener = function(element) {
333 element.off('keydown.cie.ctrlpageup').on('keydown.cie.ctrlpageup', function(e) {
334 if (e.ctrlKey) {
335 if (e.keyCode == keyCodes.pageUp) {
336 $(e.target).trigger(events.ctrlPageUp, [{originalEvent: e}]);
337 }
338 }
339 });
340 };
341
342 /**
343 * Trigger the ctrlPageDown event on the given element if the user presses the
344 * control and page down key.
345 *
346 * @method addCtrlPageDownListener
347 * @private
2bcef559 348 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
349 */
350 var addCtrlPageDownListener = function(element) {
351 element.off('keydown.cie.ctrlpagedown').on('keydown.cie.ctrlpagedown', function(e) {
352 if (e.ctrlKey) {
353 if (e.keyCode == keyCodes.pageDown) {
354 $(e.target).trigger(events.ctrlPageDown, [{originalEvent: e}]);
355 }
356 }
357 });
358 };
359
360 /**
361 * Trigger the enter event on the given element if the enter key is pressed
362 * without a modifier key.
363 *
364 * @method addEnterListener
365 * @private
2bcef559 366 * @param {object} element jQuery object to add event listeners to
e845b96b
RW
367 */
368 var addEnterListener = function(element) {
369 addKeyboardEvent(element, events.enter, keyCodes.enter);
370 };
371
372 /**
373 * Get the list of events and their handlers.
374 *
375 * @method getHandlers
376 * @private
2bcef559 377 * @return {object} object key of event names and value of handler functions
e845b96b
RW
378 */
379 var getHandlers = function() {
380 var handlers = {};
381
382 handlers[events.activate] = addActivateListener;
383 handlers[events.keyboardActivate] = addKeyboardActivateListener;
384 handlers[events.escape] = addEscapeListener;
385 handlers[events.down] = addDownListener;
386 handlers[events.up] = addUpListener;
387 handlers[events.home] = addHomeListener;
388 handlers[events.end] = addEndListener;
389 handlers[events.next] = addNextListener;
390 handlers[events.previous] = addPreviousListener;
391 handlers[events.asterix] = addAsterixListener;
99c7f0a7 392 handlers[events.scrollLock] = addScrollLockListener;
e845b96b
RW
393 handlers[events.scrollTop] = addScrollTopListener;
394 handlers[events.scrollBottom] = addScrollBottomListener;
395 handlers[events.ctrlPageUp] = addCtrlPageUpListener;
396 handlers[events.ctrlPageDown] = addCtrlPageDownListener;
397 handlers[events.enter] = addEnterListener;
398
399 return handlers;
400 };
401
402 /**
403 * Add all of the listeners on the given element for the requested events.
404 *
405 * @method define
406 * @public
2bcef559 407 * @param {object} element the DOM element to register event listeners on
e845b96b
RW
408 * @param {array} include the array of events to be triggered
409 */
410 var define = function(element, include) {
411 element = $(element);
412 include = include || [];
413
414 if (!element.length || !include.length) {
415 return;
416 }
417
418 $.each(getHandlers(), function(eventType, handler) {
419 if (shouldAddEvent(eventType, include)) {
420 handler(element);
421 }
422 });
423 };
424
425 return /** @module core/custom_interaction_events */ {
426 define: define,
427 events: events,
428 };
429});