scrawl-canvas
Version:
Responsive, interactive and more accessible HTML5 canvas elements. Scrawl-canvas is a JavaScript library designed to make using the HTML5 canvas element easier, and more fun
259 lines (190 loc) • 9.15 kB
JavaScript
// # Keyboard zones.
// __makeKeyboardZone__ is an attempt to make setting up keyboard listeners (for accessibility) within a Scrawl-canvas stack or canvas as simple as possible. Similar to the drag zone functionality, SC tries to limit the number of keyboard event listeners attached to a particular DOM element to the bare minimum
//
// Required attribute of the argument object:
// + __.zone__ - either the String name of the Stack or Canvas artefact which will host the zone, or the Stack or Canvas artefact itself
//
// Additional, optional, attributes in the argument object
// + `'none', 'shiftOnly', 'altOnly', 'ctrlOnly', 'metaOnly', 'shiftAlt', 'shiftCtrl', 'shiftMeta', 'altCtrl', 'altMeta', 'ctrlMeta', 'shiftAltCtrl', 'shiftAltMeta', 'shiftCtrlMeta', 'altCtrlMeta', 'all'` - a set of objects containing `keyboard Key: function` attributes defining the group of actions to take when the user presses the associated key or key-combination.
// #### Imports
import { artefact } from "../core/library.js";
import { mergeDiscard, λnull, Ωempty } from "../helper/utilities.js";
import { addNativeListener, removeNativeListener } from "../core/events.js";
// Shared constants
import { $BODY, _keys, ACCEPTED_WRAPPERS, NONE } from '../helper/shared-vars.js';
// Local constants
const KEY_DOWN = 'keydown',
KEY_UP = 'keyup',
KEYBOARD_GROUPS = ['none', 'shiftOnly', 'altOnly', 'ctrlOnly', 'metaOnly', 'shiftAlt', 'shiftCtrl', 'shiftMeta', 'altCtrl', 'altMeta', 'ctrlMeta', 'shiftAltCtrl', 'shiftAltMeta', 'shiftCtrlMeta', 'altCtrlMeta', 'all'],
T_ESCAPE = 'Escape',
T_TAB = 'Tab';
const keyboardZones = {};
const processKeyboardZoneData = function (items = Ωempty, doAddListeners, doRemoveListeners) {
let zone = items.zone;
// `zone` is required
// + must be either a Canvas or Stack wrapper, or a wrapper's String name
if (!zone) return new Error('keyboardZone constructor - no zone supplied');
if (zone.substring) zone = artefact[zone];
if (!zone || !ACCEPTED_WRAPPERS.includes(zone.type)) return new Error('keyboardZone constructor - zone object is not a Stack or Canvas wrapper');
const target = zone.domElement;
if (!target) return new Error('keyboardZone constructor - zone does not contain a target DOM element');
let zoneItem = keyboardZones[zone.name];
if (!zoneItem) {
keyboardZones[zone.name] = {};
zoneItem = keyboardZones[zone.name];
doAddListeners(target);
}
if (!zoneItem.extraKeys) {
zoneItem.extraKeys = {
Shift: false,
Control: false,
Alt: false,
Meta: false,
};
}
if (!zoneItem.keyGroups) {
const groups = {};
KEYBOARD_GROUPS.forEach(g => groups[g] = {});
zoneItem.keyGroups = groups;
}
const KG = zoneItem.keyGroups;
KEYBOARD_GROUPS.forEach(g => {
const keymap = items[g];
if (keymap != null) mergeDiscard(KG[g], keymap);
});
if (!zoneItem.onKeyDown) {
zoneItem.onKeyDown = (e = Ωempty) => {
if (e && e.key) {
e.preventDefault();
const { extraKeys, keyGroups } = zoneItem;
const { key } = e;
// Tab, Esc
if (T_TAB === key || T_ESCAPE === key) {
target.blur();
return;
}
if (extraKeys[key] != null) {
extraKeys[key] = true;
return;
}
const { Shift, Control, Alt, Meta } = extraKeys;
let group = keyGroups.none;
if (Shift || Control || Alt || Meta) {
if (Shift) {
if (Alt) {
if (Control) {
if (Meta) group = keyGroups.all;
else group = keyGroups.shiftAltCtrl;
}
else {
if (Meta) group = keyGroups.shiftAltMeta;
else group = keyGroups.shiftAlt;
}
}
else {
if (Control) {
if (Meta) group = keyGroups.shiftCtrlMeta;
else group = keyGroups.shiftCtrl;
}
else {
if (Meta) group = keyGroups.shiftMeta;
else group = keyGroups.shiftOnly;
}
}
}
else {
if (Alt) {
if (Control) {
if (Meta) group = keyGroups.altCtrlMeta;
else group = keyGroups.altCtrl;
}
else {
if (Meta) group = keyGroups.altMeta;
else group = keyGroups.altOnly;
}
}
else {
if (Control) {
if (Meta) group = keyGroups.ctrlMeta;
else group = keyGroups.ctrlOnly;
}
else {
if (Meta) group = keyGroups.altOnly;
else group = keyGroups.none;
}
}
}
}
if (group[key]) group[key]();
}
};
}
if (!zoneItem.onKeyUp) {
zoneItem.onKeyUp = (e = Ωempty) => {
if (e && e.key) {
e.preventDefault();
const extraKeys = zoneItem.extraKeys;
const key = e.key;
if (extraKeys[key] != null) extraKeys[key] = false;
}
};
}
// __kill__ - A function to remove the internal key mappings associated with the target DOM element, and then remove the keyboard listeners attached to that element
if (!zoneItem.kill) {
zoneItem.kill = function () {
delete keyboardZones[zone.name];
doRemoveListeners(target);
};
}
// __getMappedKeys__ - A function which returns an Array of the defined keys for the given group, the name of which should be supplied as the function's argument
// + To update key mappings, invoke the `makeKeyboardZone` function again, including the new mappings as part of the argument object
const getMappedKeys = (keyGroup = NONE) => {
if (zoneItem.keyGroups[keyGroup] != null) {
return _keys(zoneItem.keyGroups[keyGroup]);
}
return [];
}
// The return includes both the `kill` and `getMappedKeys` functions
return {
kill: zoneItem.kill,
getMappedKeys,
}
}
// `Exported function` (to modules and the SC object). Add keyboard listenning functionality to a canvas or stack wrapper.
export const makeKeyboardZone = function (items = Ωempty) {
// The exposed `actionKeyDown` function will search for the target element (if user has started the drag while the mouse cursor was over a child of the Stack wrapper) and then apply the required keystroke actions while that element remains focussed
const actionKeyDown = (e = Ωempty) => {
if (e && e.target) {
let myTarget = e.target,
name = '';
while (!name) {
if (keyboardZones[myTarget.id]) name = myTarget.id;
if (myTarget.tagName === $BODY) break;
myTarget = myTarget.parentElement;
}
const actions = keyboardZones[name];
if (actions) {
actions.onKeyDown(e);
currentKeyUp = actions.onKeyUp;
}
else currentKeyUp = λnull;
}
};
// The exposed `actionKeyUp` function will change to match the equivalent funtions supplied by the zone selected during the `actionKeyDown` stage of the operation
let currentKeyUp = λnull;
const actionKeyUp = (e) => {
currentKeyUp(e);
};
// Listeners are added to the DOM element when the first keyboard zone is created for that target
const doAddListeners = (target) => {
addNativeListener(KEY_DOWN, actionKeyDown, target);
addNativeListener(KEY_UP, actionKeyUp, target);
};
// Listeners are only removed from the DOM element when all the keyboard zones associated with that target have been killed
const doRemoveListeners = (target) => {
removeNativeListener(KEY_DOWN, actionKeyDown, target);
removeNativeListener(KEY_UP, actionKeyUp, target);
};
// Returns an object containing the `kill` and `getMappedKeys` functions for this keyboard mapping
return processKeyboardZoneData(items, doAddListeners, doRemoveListeners);
};