Files
pathfinder/js/app/key.js
Mark Friedrich 6cd25021bc - improved error handling for PHP Runtime errors
- upgraded [_Raphaël_](https://dmitrybaranovskiy.github.io/raphael) js lib `v2.1.2` → `v2.2.8`
2019-07-20 17:19:33 +02:00

478 lines
17 KiB
JavaScript

define([
'jquery'
], ($) => {
'use strict';
let allCombo = {
// global -------------------------------------------------------------------------------------------
tabReload: {
group: 'global',
label: 'Close open dialog',
keyNames: ['ESC']
},
// map ----------------------------------------------------------------------------------------------
mapMove: {
group: 'map',
label: 'Move map section',
keyNames: ['space', 'drag']
},
// signature ----------------------------------------------------------------------------------------
signatureSelect: {
group: 'signatures',
label: 'Select multiple rows',
keyNames: ['CONTROL', 'CLICK']
},
signatureNavigate: {
group: 'signatures',
label: 'Table navigation',
keyNames: ['UP', 'RIGHT', 'DOWN', 'LEFT'],
list: true
}
};
let allEvents = {
// global -------------------------------------------------------------------------------------------
tabReload: {
group: 'global',
label: 'Reload tab',
keyNames: ['CONTROL', 'R']
},
clipboardPaste: {
group: 'global',
label: 'Update signatures/D-Scan from clipboard',
keyNames: ['CONTROL', 'V'],
alias: 'paste'
},
renameSystem: {
group: 'map',
label: 'Rename system',
keyNames: ['ALT', 'N']
},
newSignature: {
group: 'signatures',
label: 'New Signature',
keyNames: ['ALT', '3']
},
// map ----------------------------------------------------------------------------------------------
mapSystemAdd: {
group: 'map',
label: 'New system',
keyNames: ['ALT', '2']
},
mapSystemsSelect: {
group: 'map',
label: 'Select all systems',
keyNames: ['CONTROL', 'A']
},
mapSystemsDelete: {
group: 'map',
label: 'Delete selected systems',
keyNames: ['CONTROL', 'D']
}
};
let groups = {
global: {
label: 'Global'
},
map: {
label: 'Map'
},
signatures: {
label: 'Signature'
}
};
/**
* enables some debug output in console
* @type {boolean}
*/
let debug = false;
/**
* DOM data key for an element that lists all active events (comma separated)
* @type {string}
*/
let dataKeyEvents = 'key-events';
/**
* DOM data key prefix whether domElement that holds the trigger needs to be "focused"
* @type {string}
*/
let dataKeyFocusPrefix = 'key-focus-';
/**
* DOM data key that holds the callback function for that element
* @type {string}
*/
let dataKeyCallbackPrefix = 'key-callback-';
/**
* check if module is initiated
*/
let isInit = false;
/**
* global key map holds all active (hold down) keys
* @type {{}}
*/
let map = {};
/**
* show debug information in console
* @param msg
* @param element
*/
let debugWatchKey = (msg, element) => {
if(debug){
console.info(msg, element);
}
};
/**
* get all active (hold down) keys at this moment
* @returns {Array}
*/
let getActiveKeys = () => {
return Object.keys(map);
};
/**
* checks whether a key is currently active (keydown)
* @param key
* @returns {boolean}
*/
let isActive = key => map.hasOwnProperty(key) && map[key] === true;
/**
* callback function that compares two arrays
* @param element
* @param index
* @param array
*/
let compareKeyLists = function(element, index, array){
return this.find(x => x === element);
};
/**
* get event names that COULD lead to a "full" event (not all keys pressed yet)
* @param keyList
* @returns {Array}
*/
let checkEventNames = (keyList) => {
let incompleteEvents = [];
for(let event in allEvents){
// check if "some" or "all" keys are pressed for en event
if( keyList.every(compareKeyLists, allEvents[event].keyNames) ){
incompleteEvents.push(event);
}
}
return incompleteEvents;
};
/**
* get all event names
* @returns {Array}
*/
let getAllEventNames = () => {
let eventNames = [];
for(let event in allEvents){
eventNames.push(event);
}
return eventNames;
};
/**
* get all event names that matches a given keyList
* @param keyList
* @param checkEvents
* @returns {Array}
*/
let getMatchingEventNames = (keyList, checkEvents) => {
checkEvents = checkEvents || getAllEventNames();
let events = [];
for(let event of checkEvents){
// check if both key arrays are equal
if(
allEvents[event].keyNames.every(compareKeyLists, keyList) &&
keyList.every(compareKeyLists, allEvents[event].keyNames)
){
events.push(event);
}
}
return events;
};
/**
* init global keyWatch interval and check for event trigger (hotKey combinations)
*/
let init = () => {
if( !isInit ){
// key watch loop -------------------------------------------------------------------------------
let prevActiveKeys = [];
/**
*
* @param e
* @returns {number} 0: no keys hold, 1: invalid match, 2: partial match, 3: match, 4: alias match, 5: event(s) fired
*/
let checkForEvents = (e) => {
let status = 0;
// get all pressed keys
let activeKeys = getActiveKeys();
debugWatchKey('activeKeys', activeKeys);
// check if "active" keys has changes since last loop
if(activeKeys.length){
// check for "incomplete" events (not all keys pressed yet)
let incompleteEvents = checkEventNames(activeKeys);
if(incompleteEvents.length){
// "some" event keys pressed OR "all" keys pressed
status = 2;
// check if key combo matches a registered (valid) event
let events = getMatchingEventNames(activeKeys, incompleteEvents);
if(events.length){
status = 3;
// check events if there are attached elements to it
events.forEach((event) => {
// skip events that has an alias and should not be triggered by key combo
if( !allEvents[event].alias ){
if(allEvents[event].elements){
// search for callback functions attached to each element
allEvents[event].elements.forEach((domElement) => {
let domElementObj = $(domElement);
// check if event on this element requires active "focus"
let optFocus = domElementObj.data(dataKeyFocusPrefix + event);
if( !(
optFocus &&
document.activeElement !== domElement
)
){
// execute callback if valid
let callback = domElementObj.data(dataKeyCallbackPrefix + event);
if(typeof callback === 'function'){
status = 5;
callback.call(domElement, domElement, e);
}
}
});
}
}else{
status = 4;
}
});
}
}else{
// invalid combo
status = 1;
}
}
// store current keys for next loop check
prevActiveKeys = activeKeys;
return status;
};
// set key-events -------------------------------------------------------------------------------
let evKeyDown = (e) => {
// exclude some HTML Tags from watcher
if(
e.target.tagName !== 'INPUT' &&
e.target.tagName !== 'TEXTAREA' &&
!e.target.classList.contains('note-editable') // Summerstyle editor
){
let key = e.key.toUpperCase();
map[key] = true;
// check for any shortcut combo that triggers an event
let status = checkForEvents(e);
if(
status === 2 ||
status === 3 ||
status === 5
){
// prevent SOME browser default actions -> we want 'Pathfinder' shortcuts :)
e.preventDefault();
}
}
};
let evKeyUp = (e) => {
if(e.key){
let key = e.key.toUpperCase();
if(map.hasOwnProperty(key)){
delete map[key];
}
}
};
let container = $('body');
container.on('keydown', evKeyDown);
container.on('keyup', evKeyUp);
// global dom remove listener -------------------------------------------------------------------
// -> check whether the removed element had an event listener active and removes them.
new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if(mutation.type === 'childList'){
for(let i = 0; i < mutation.removedNodes.length; i++){
let removedNode = mutation.removedNodes[i];
if(typeof removedNode.getAttribute === 'function'){
let eventNames = removedNode.getAttribute(dataKeyEvents);
if(eventNames){
let events = eventNames.split(',');
for(let j = 0; i < events.length; j++){
let event = events[j];
let index = allEvents[event].elements.indexOf(removedNode);
if(index > -1){
// remove element from event list
allEvents[event].elements.splice(index, 1);
}
}
}
}
}
}
});
}).observe(document.body, { childList: true, subtree: true });
isInit = true;
}
};
/**
* add a new "shortCut" combination (event) to a DOM element
* @param event
* @param callback
* @param options
*/
$.fn.watchKey = function(event, callback, options){
// default options for keyWatcher on elements
let defaultOptions = {
focus: false, // element must be focused (active)
bubbling: true // elements deeper (children) in the DOM can bubble the event up
};
let customOptions = $.extend(true, {}, defaultOptions, options );
return this.each((i, domElement) => {
let element = $(domElement);
// init global key events
init();
// check if event is "valid" (exists) and is not already set for this element
let validEvent = false;
if(allEvents[event].elements){
if(allEvents[event].elements.indexOf(domElement) === -1){
validEvent = true;
}else{
console.warn('Event "' + event + '" already set');
}
}else{
validEvent = true;
allEvents[event].elements = [];
}
if(validEvent){
// store callback options to dom element
if(customOptions.focus){
let dataAttr = dataKeyFocusPrefix + event;
element.data(dataAttr, true);
// check if DOM element has "tabindex" attr -> required to manually set focus() to it
if(!domElement.hasAttribute('tabindex')){
domElement.setAttribute('tabindex', 0);
}
// element requires a "focus" listener
element.off('click.focusKeyWatcher').on('click.focusKeyWatcher', function(e){
if(
e.target === this ||
customOptions.bubbling
){
this.focus();
debugWatchKey('focus set:', this);
}
});
}
// check if is key combo has a native JS event that should be used instead
if(allEvents[event].alias){
element.on(allEvents[event].alias, callback);
}else{
// store callback function to dom element
let dataAttr = dataKeyCallbackPrefix + event;
element.data(dataAttr, callback);
}
// add eventName to dom element as attribute ----------------------------------------------------
let currentEventNames = element.attr(dataKeyEvents) ? element.attr(dataKeyEvents).split(',') : [];
currentEventNames.push(event);
element.attr(dataKeyEvents, currentEventNames.join(','));
// store domElement to event (global)
allEvents[event].elements.push(domElement);
debugWatchKey('new event "' + event + '" registered', domElement);
}
});
};
/**
* get a array with all available shortcut groups and their events
* @returns {any[]}
*/
let getGroupedShortcuts = () => {
let result = $.extend(true, {}, groups);
// add combos and events to groups
let allEntries = [allCombo, allEvents];
for(let entries of allEntries){
for(let [event, data] of Object.entries(entries)){
//format keyNames for UI
let keyNames = data.keyNames.map(key => {
if(key === 'CONTROL'){
key = 'ctrl';
}
return key;
});
let newEventData = {
label: data.label,
keyNames: keyNames,
list: data.list
};
if( result[data.group].events ){
result[data.group].events.push(newEventData);
}else{
result[data.group].events = [newEventData];
}
}
}
// convert obj into array
result = Object.values(result);
return result;
};
return {
isActive: isActive,
getGroupedShortcuts: getGroupedShortcuts
};
});