Issues (149)

js/util/siteapp.util.keyboard.js (16 issues)

1
/*******************************************
2
 *                                         *
3
 * This util was created by Marius Olbertz *
4
 * Please thank Marius on GitHub /owlbertz *
5
 * or the web http://www.mariusolbertz.de/ *
6
 *                                         *
7
 ******************************************/
8
9
'use strict';
10
11
import { rtl as Rtl } from './siteapp.util.core';
12
13
const keyCodes = {
14
    9 : 'TAB',
15
    13: 'ENTER',
16
    27: 'ESCAPE',
17
    32: 'SPACE',
18
    35: 'END',
19
    36: 'HOME',
20
    37: 'ARROW_LEFT',
21
    38: 'ARROW_UP',
22
    39: 'ARROW_RIGHT',
23
    40: 'ARROW_DOWN'
24
}
25
26
let commands = {}
27
28
// Functions pulled out to be referenceable from internals
29
function findFocusable($element) {
30
    if(!$element) {return false; }
31
    return $element.find('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]').filter(function() {
32
        if (!$(this).is(':visible') || $(this).attr('tabindex') < 0) { return false; } //only have visible elements and those that have a tabindex greater or equal 0
33
        return true;
34
    });
35
}
36
37
function parseKey(event) {
38
    var key = keyCodes[event.which || event.keyCode] || String.fromCharCode(event.which).toUpperCase();
39
40
    // Remove un-printable characters, e.g. for `fromCharCode` calls for CTRL only events
41
    key = key.replace(/\W+/, '');
42
43
    if (event.shiftKey)  key = `SHIFT_${key}`;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
44
    if (event.ctrlKey)   key = `CTRL_${key}`;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
45
    if (event.altKey)    key = `ALT_${key}`;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
46
    if (event.metaKey)   key = `META_${key}`;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
47
48
    // Remove trailing underscore, in case only modifiers were used (e.g. only `CTRL_ALT`)
49
    key = key.replace(/_$/, '');
50
51
    return key;
52
}
53
54
const Keyboard = {
55
		
56
    /**
57
     * @var {object} keys - keycodes map
58
     */
59
    keys: getKeyCodes(keyCodes),
60
61
    /**
62
     * Parses the (keyboard) event and returns a String that represents its key
63
     * Can be used like Foundation.parseKey(event) === Foundation.keys.SPACE
64
     * @param {Event} event - the event generated by the event handler
65
     * @return String key - String that represents the key pressed
66
     */
67
    parseKey: parseKey,
68
69
    /**
70
     * Handles the given (keyboard) event
71
     * @param {Event} event - the event generated by the event handler
72
     * @param {String} component - Foundation component's name, e.g. Slider or Reveal
73
     * @param {Objects} functions - collection of functions that are to be executed
74
     */
75
    handleKey(event, component, functions) {
76
        var commandList = commands[component],
77
            keyCode = this.parseKey(event),
78
            cmds,
79
            command,
80
            fn;
81
82
        if (!commandList) return console.warn('Component not defined!');
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
83
84
        if (typeof commandList.ltr === 'undefined') { // this component does not differentiate between ltr and rtl
85
            cmds = commandList; // use plain list
86
        } else { // merge ltr and rtl: if document is rtl, rtl overwrites ltr and vice versa
87
            if (Rtl()) cmds = $.extend({}, commandList.ltr, commandList.rtl);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
88
89
            else cmds = $.extend({}, commandList.rtl, commandList.ltr);
90
        }
91
        command = cmds[keyCode];
92
93
        fn = functions[command];
94
        if (fn && typeof fn === 'function') { // execute function  if exists
95
            var returnValue = fn.apply(null, [event]);
96
            if (functions.handled || typeof functions.handled === 'function') { // execute function when event was handled
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if functions.handled || typ...handled === "function" is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
97
                functions.handled(returnValue);
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
98
            }
99
        } else {
100
            if (functions.unhandled || typeof functions.unhandled === 'function') { // execute function when event was not handled
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if functions.unhandled || t...handled === "function" is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
101
                functions.unhandled();
0 ignored issues
show
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
102
            }
103
        }
104
    },
105
106
    /**
107
     * Finds all focusable elements within the given `$element`
108
     * @param {jQuery} $element - jQuery object to search within
109
     * @return {jQuery} $focusable - all focusable elements within `$element`
110
     */
111
112
    findFocusable: findFocusable,
113
114
    /**
115
     * Stores set of commands mapped to handler names in a given namespace
116
     * 
117
     * @param {string} namespace - keyboard event/command namespace
118
     * @param {object} cmds - handler name to command map
119
     */
120
121
    register (namespace, cmds) {
122
        commands[namespace] = cmds;
123
        //console.//log('registered keyboard component:', namespace, cmds, commands);
124
    },
125
126
127
    // TODO9438: These references to Keyboard need to not require global. Will 'this' work in this context?
128
    //
129
    /**
130
     * Traps the focus in the given element.
131
     * @param  {jQuery} $element  jQuery object to trap the foucs into.
132
     */
133
    trapFocus($element) {
134
        var $focusable = findFocusable($element),
135
            $firstFocusable = $focusable.eq(0),
136
            $lastFocusable = $focusable.eq(-1);
137
138
        $element.on('keydown.'+Keyboard._ns+'.trapfocus', function(event) {
139
            if (event.target === $lastFocusable[0] && parseKey(event) === 'TAB') {
140
                event.preventDefault();
141
                $firstFocusable.focus();
142
            }
143
            else if (event.target === $firstFocusable[0] && parseKey(event) === 'SHIFT_TAB') {
144
                event.preventDefault();
145
                $lastFocusable.focus();
146
            }
147
        });
148
    },
149
  
150
    /**
151
     * Releases the trapped focus from the given element.
152
     * @param  {jQuery} $element  jQuery object to release the focus for.
153
     */
154
    releaseFocus($element) {
155
        $element.off('keydown.'+Keyboard._ns+'.trapfocus');
156
    },
157
    
158
    /**
159
     * Removes keyboard event handlers. If no namespace is given, all 
160
     * keyboard handlers will be removed.
161
     * 
162
     * @param {string} namespace - keyboard event namespace
163
     */
164
    unregister( namespace ) {
165
    	if (namespace != '') {
166
    		$(window).off('keydown.'+this.application.appName+'.'+namespace);
167
    		if (typeof commands[namespace] != 'undefined') {
168
    			delete commands[namespace];
169
    		}
170
    	} else {
171
    		$(window).off('keydown.'+this.application.appName);
172
    		commands = {};
173
    	}
174
    },
175
  
176
    /**
177
     * Adds keyboard event handlers.
178
     * 
179
     * handlers := {
180
     *    '[KEY(S)]' : function myAction (event e) {...},
181
     *    ...
182
     * }
183
     * 
184
     * @param {object} handlers - key(s) to action map
185
     * @param {string} namespace - keyboard event namespace
186
     */
187
    addKeyHandlers( handlers, namespace ) {
188
189
        var $this   = this,
190
            $app    = this._app,
0 ignored issues
show
The variable $app seems to be never used. Consider removing it.
Loading history...
191
            eventTrigger = 'keydown.'+this._ns,
192
            registeredKeys = {},
193
            registeredHandlers = {}
194
        ;
195
196
    	if ('' == namespace) {
197
    		let namespace = '_'+Siteapp.sys.genUUID(6);
0 ignored issues
show
The variable Siteapp seems to be never declared. If this is a global, consider adding a /** global: Siteapp */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
The variable namespace seems to be never used. Consider removing it.
Loading history...
198
    	}
199
		eventTrigger = eventTrigger+'.'+namespace;
200
        
201
    	// build 'reg keys' and 'hdl keys' maps
202
		for ( var key in handlers ) {
203
			if (handlers.hasOwnProperty(key)) {
204
				var handleName = namespace+''+key;
205
				registeredKeys[key] = handleName;
206
				registeredHandlers[handleName] = handlers[key];
207
			}
208
		}
209
    	
210
        $(window).on(eventTrigger, function(e){
211
212
    	    this.id = namespace+''+Siteapp.sys.genUUID(6);
0 ignored issues
show
The variable Siteapp seems to be never declared. If this is a global, consider adding a /** global: Siteapp */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
213
    	    
214
    	    // ignore [TAB] key
215
	        // if (e.which === 9) return;
216
	
217
	        // handle keyboard event with keyboard util
218
	        $this.handleKey(e, namespace, registeredHandlers);
219
220
	        
221
        });
222
223
        // register keyboard keys mapping
224
        this.register(namespace, registeredKeys);
225
        
226
    }
227
  
228
};
229
230
/*
231
 * Constants for easier comparing.
232
 * Can be used like Foundation.parseKey(event) === Foundation.keys.SPACE
233
 */
234
function getKeyCodes(kcs) {
235
    var k = {};
236
    for (var kc in kcs) k[kcs[kc]] = kcs[kc];
0 ignored issues
show
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
237
    return k;
238
}
239
240
export {Keyboard};
241