@alilc/lowcode-editor-core
Version:
Core Api for Ali lowCode engine
578 lines (542 loc) • 19.1 kB
JavaScript
"use strict";
exports.__esModule = true;
exports.Hotkey = void 0;
var _lodash = require("lodash");
var _di = require("./di");
function _createForOfIteratorHelperLoose(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (t) return (t = t.call(r)).next.bind(t); if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var o = 0; return function () { return o >= r.length ? { done: !0 } : { done: !1, value: r[o++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
var MAP = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'ins',
46: 'del',
91: 'meta',
93: 'meta',
224: 'meta'
};
var KEYCODE_MAP = {
106: '*',
107: '+',
109: '-',
110: '.',
111: '/',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: "'"
};
var SHIFT_MAP = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
$: '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
_: '-',
'+': '=',
':': ';',
'"': "'",
'<': ',',
'>': '.',
'?': '/',
'|': '\\'
};
var SPECIAL_ALIASES = {
option: 'alt',
command: 'meta',
"return": 'enter',
escape: 'esc',
plus: '+',
mod: /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
};
var REVERSE_MAP;
/**
* loop through the f keys, f1 to f19 and add them to the map
* programatically
*/
for (var i = 1; i < 20; ++i) {
MAP[111 + i] = "f" + i;
}
/**
* loop through to map numbers on the numeric keypad
*/
for (var _i = 0; _i <= 9; ++_i) {
MAP[_i + 96] = String(_i);
}
/**
* takes the event and returns the key character
*/
function characterFromEvent(e) {
var keyCode = e.keyCode || e.which;
// for keypress events we should return the character as is
if (e.type === 'keypress') {
var character = String.fromCharCode(keyCode);
// if the shift key is not pressed then it is safe to assume
// that we want the character to be lowercase. this means if
// you accidentally have caps lock on then your key bindings
// will continue to work
//
// the only side effect that might not be desired is if you
// bind something like 'A' cause you want to trigger an
// event when capital A is pressed caps lock will no longer
// trigger the event. shift+a will though.
if (!e.shiftKey) {
character = character.toLowerCase();
}
return character;
}
// for non keypress events the special maps are needed
if (MAP[keyCode]) {
return MAP[keyCode];
}
if (KEYCODE_MAP[keyCode]) {
return KEYCODE_MAP[keyCode];
}
// if it is not in the special map
// with keydown and keyup events the character seems to always
// come in as an uppercase character whether you are pressing shift
// or not. we should make sure it is always lowercase for comparisons
// tips: Q29weXJpZ2h0IChjKSAyMDIwLXByZXNlbnQgQWxpYmFiYSBJbmMuIFYy
return String.fromCharCode(keyCode).toLowerCase();
}
function isPressEvent(e) {
return e.type === 'keypress';
}
/**
* checks if two arrays are equal
*/
function modifiersMatch(modifiers1, modifiers2) {
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
/**
* takes a key event and figures out what the modifiers are
*/
function eventModifiers(e) {
var modifiers = [];
if (e.shiftKey) {
modifiers.push('shift');
}
if (e.altKey) {
modifiers.push('alt');
}
if (e.ctrlKey) {
modifiers.push('ctrl');
}
if (e.metaKey) {
modifiers.push('meta');
}
return modifiers;
}
/**
* determines if the keycode specified is a modifier key or not
*/
function isModifier(key) {
return key === 'shift' || key === 'ctrl' || key === 'alt' || key === 'meta';
}
/**
* reverses the map lookup so that we can look for specific keys
* to see what can and can't use keypress
*
* @return {Object}
*/
function getReverseMap() {
if (!REVERSE_MAP) {
REVERSE_MAP = {};
for (var _key in MAP) {
// pull out the numeric keypad from here cause keypress should
// be able to detect the keys from the character
if (Number(_key) > 95 && Number(_key) < 112) {
continue;
}
if (MAP.hasOwnProperty(_key)) {
REVERSE_MAP[MAP[_key]] = _key;
}
}
}
return REVERSE_MAP;
}
/**
* picks the best action based on the key combination
*/
function pickBestAction(key, modifiers, action) {
// if no action was picked in we should try to pick the one
// that we think would work best for this key
if (!action) {
action = getReverseMap()[key] ? 'keydown' : 'keypress';
}
// modifier keys don't work as expected with keypress,
// switch to keydown
if (action === 'keypress' && modifiers.length) {
action = 'keydown';
}
return action;
}
/**
* Converts from a string key combination to an array
*
* @param {string} combination like "command+shift+l"
* @return {Array}
*/
function keysFromString(combination) {
if (combination === '+') {
return ['+'];
}
combination = combination.replace(/\+{2}/g, '+plus');
return combination.split('+');
}
/**
* Gets info for a specific key combination
*
* @param combination key combination ("command+s" or "a" or "*")
*/
function getKeyInfo(combination, action) {
var keys = [];
var key = '';
var i;
var modifiers = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
keys = keysFromString(combination);
for (i = 0; i < keys.length; ++i) {
key = keys[i];
// normalize key names
if (SPECIAL_ALIASES[key]) {
key = SPECIAL_ALIASES[key];
}
// if this is not a keypress event then we should
// be smart about using shift keys
// this will only work for US keyboards however
if (action && action !== 'keypress' && SHIFT_MAP[key]) {
key = SHIFT_MAP[key];
modifiers.push('shift');
}
// if this key is a modifier then add it to the list of modifiers
if (isModifier(key)) {
modifiers.push(key);
}
}
// depending on what the key combination is
// we will try to pick the best event for it
action = pickBestAction(key, modifiers, action);
return {
key: key,
modifiers: modifiers,
action: action
};
}
/**
* actually calls the callback function
*
* if your callback function returns false this will use the jquery
* convention - prevent default and stop propogation on the event
*/
function fireCallback(callback, e, combo, sequence) {
try {
var _workspace$window, _designer$currentSele, _designer$currentSele2, _node$componentMeta, _node$componentMeta2;
var workspace = _di.globalContext.get('workspace');
var editor = workspace.isActive ? (_workspace$window = workspace.window) === null || _workspace$window === void 0 ? void 0 : _workspace$window.editor : _di.globalContext.get('editor');
var designer = editor === null || editor === void 0 ? void 0 : editor.get('designer');
var node = designer === null || designer === void 0 ? void 0 : (_designer$currentSele = designer.currentSelection) === null || _designer$currentSele === void 0 ? void 0 : (_designer$currentSele2 = _designer$currentSele.getNodes()) === null || _designer$currentSele2 === void 0 ? void 0 : _designer$currentSele2[0];
var npm = node === null || node === void 0 ? void 0 : (_node$componentMeta = node.componentMeta) === null || _node$componentMeta === void 0 ? void 0 : _node$componentMeta.npm;
var selected = [npm === null || npm === void 0 ? void 0 : npm["package"], npm === null || npm === void 0 ? void 0 : npm.componentName].filter(function (item) {
return !!item;
}).join('-') || (node === null || node === void 0 ? void 0 : (_node$componentMeta2 = node.componentMeta) === null || _node$componentMeta2 === void 0 ? void 0 : _node$componentMeta2.componentName) || '';
if (callback(e, combo) === false) {
e.preventDefault();
e.stopPropagation();
}
editor === null || editor === void 0 ? void 0 : editor.eventBus.emit('hotkey.callback.call', {
callback: callback,
e: e,
combo: combo,
sequence: sequence,
selected: selected
});
} catch (err) {
console.error(err.message);
}
}
var Hotkey = exports.Hotkey = /*#__PURE__*/function () {
function Hotkey(viewName) {
if (viewName === void 0) {
viewName = 'global';
}
this.viewName = viewName;
this.callBacks = {};
this.directMap = {};
this.sequenceLevels = {};
this.resetTimer = 0;
this.ignoreNextKeyup = false;
this.ignoreNextKeypress = false;
this.nextExpectedAction = false;
this.isActivate = true;
this.mount(window);
}
var _proto = Hotkey.prototype;
_proto.activate = function activate(_activate) {
this.isActivate = _activate;
};
_proto.mount = function mount(window) {
var document = window.document;
var handleKeyEvent = this.handleKeyEvent.bind(this);
document.addEventListener('keypress', handleKeyEvent, false);
document.addEventListener('keydown', handleKeyEvent, false);
document.addEventListener('keyup', handleKeyEvent, false);
return function () {
document.removeEventListener('keypress', handleKeyEvent, false);
document.removeEventListener('keydown', handleKeyEvent, false);
document.removeEventListener('keyup', handleKeyEvent, false);
};
};
_proto.bind = function bind(combos, callback, action) {
this.bindMultiple(Array.isArray(combos) ? combos : [combos], callback, action);
return this;
};
_proto.unbind = function unbind(combos, callback, action) {
var _this = this;
var combinations = Array.isArray(combos) ? combos : [combos];
combinations.forEach(function (combination) {
var info = getKeyInfo(combination, action);
var key = info.key,
modifiers = info.modifiers;
var idx = _this.callBacks[key].findIndex(function (info) {
return (0, _lodash.isEqual)(info.modifiers, modifiers) && info.callback === callback;
});
if (idx !== -1) {
_this.callBacks[key].splice(idx, 1);
}
});
}
/**
* resets all sequence counters except for the ones passed in
*/;
_proto.resetSequences = function resetSequences(doNotReset) {
// doNotReset = doNotReset || {};
var activeSequences = false;
var key = '';
for (key in this.sequenceLevels) {
if (doNotReset && doNotReset[key]) {
activeSequences = true;
} else {
this.sequenceLevels[key] = 0;
}
}
if (!activeSequences) {
this.nextExpectedAction = false;
}
}
/**
* finds all callbacks that match based on the keycode, modifiers,
* and action
*/;
_proto.getMatches = function getMatches(character, modifiers, e, sequenceName, combination, level) {
var i;
var callback;
var matches = [];
var action = e.type;
// if there are no events related to this keycode
if (!this.callBacks[character]) {
return [];
}
// if a modifier key is coming up on its own we should allow it
if (action === 'keyup' && isModifier(character)) {
modifiers = [character];
}
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0; i < this.callBacks[character].length; ++i) {
callback = this.callBacks[character][i];
// if a sequence name is not specified, but this is a sequence at
// the wrong level then move onto the next match
if (!sequenceName && callback.seq && this.sequenceLevels[callback.seq] !== callback.level) {
continue;
}
// if the action we are looking for doesn't match the action we got
// then we should keep going
if (action !== callback.action) {
continue;
}
// if this is a keypress event and the meta key and control key
// are not pressed that means that we need to only look at the
// character, otherwise check the modifiers as well
//
// chrome will not fire a keypress if meta or control is down
// safari will fire a keypress if meta or meta+shift is down
// firefox will fire a keypress if meta or control is down
if (isPressEvent(e) && !e.metaKey && !e.ctrlKey || modifiersMatch(modifiers, callback.modifiers)) {
var deleteCombo = !sequenceName && callback.combo === combination;
var deleteSequence = sequenceName && callback.seq === sequenceName && callback.level === level;
if (deleteCombo || deleteSequence) {
this.callBacks[character].splice(i, 1);
}
matches.push(callback);
}
}
return matches;
};
_proto.handleKey = function handleKey(character, modifiers, e) {
var callbacks = this.getMatches(character, modifiers, e);
var i;
var doNotReset = {};
var maxLevel = 0;
var processedSequenceCallback = false;
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
for (i = 0; i < callbacks.length; ++i) {
if (callbacks[i].seq) {
maxLevel = Math.max(maxLevel, callbacks[i].level || 0);
}
}
// loop through matching callbacks for this key event
for (i = 0; i < callbacks.length; ++i) {
// fire for all sequence callbacks
// this is because if for example you have multiple sequences
// bound such as "g i" and "g t" they both need to fire the
// callback for matching g cause otherwise you can only ever
// match the first one
if (callbacks[i].seq) {
// only fire callbacks for the maxLevel to prevent
// subsequences from also firing
//
// for example 'a option b' should not cause 'option b' to fire
// even though 'option b' is part of the other sequence
//
// any sequences that do not match here will be discarded
// below by the resetSequences call
if (callbacks[i].level !== maxLevel) {
continue;
}
processedSequenceCallback = true;
// keep a list of which sequences were matches for later
doNotReset[callbacks[i].seq || ''] = 1;
fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
continue;
}
// if there were no sequence matches but we are still here
// that means this is a regular match so we should fire that
if (!processedSequenceCallback) {
fireCallback(callbacks[i].callback, e, callbacks[i].combo);
}
}
var ignoreThisKeypress = e.type === 'keypress' && this.ignoreNextKeypress;
if (e.type === this.nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) {
this.resetSequences(doNotReset);
}
this.ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown';
};
_proto.handleKeyEvent = function handleKeyEvent(e) {
if (!this.isActivate) {
return;
}
var character = characterFromEvent(e);
// no character found then stop
if (!character) {
return;
}
// need to use === for the character check because the character can be 0
if (e.type === 'keyup' && this.ignoreNextKeyup === character) {
this.ignoreNextKeyup = false;
return;
}
this.handleKey(character, eventModifiers(e), e);
};
_proto.resetSequenceTimer = function resetSequenceTimer() {
if (this.resetTimer) {
clearTimeout(this.resetTimer);
}
this.resetTimer = window.setTimeout(this.resetSequences, 1000);
};
_proto.bindSequence = function bindSequence(combo, keys, callback, action) {
var _this2 = this;
// const self: any = this;
this.sequenceLevels[combo] = 0;
var increaseSequence = function increaseSequence(nextAction) {
return function () {
_this2.nextExpectedAction = nextAction;
++_this2.sequenceLevels[combo];
_this2.resetSequenceTimer();
};
};
var callbackAndReset = function callbackAndReset(e) {
fireCallback(callback, e, combo);
if (action !== 'keyup') {
_this2.ignoreNextKeyup = characterFromEvent(e);
}
setTimeout(_this2.resetSequences, 10);
};
for (var _i2 = 0; _i2 < keys.length; ++_i2) {
var isFinal = _i2 + 1 === keys.length;
var wrappedCallback = isFinal ? callbackAndReset : increaseSequence(action || getKeyInfo(keys[_i2 + 1]).action);
this.bindSingle(keys[_i2], wrappedCallback, action, combo, _i2);
}
};
_proto.bindSingle = function bindSingle(combination, callback, action, sequenceName, level) {
// store a direct mapped reference for use with HotKey.trigger
this.directMap[combination + ":" + action] = callback;
// make sure multiple spaces in a row become a single space
combination = combination.replace(/\s+/g, ' ');
var sequence = combination.split(' ');
// if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time
if (sequence.length > 1) {
this.bindSequence(combination, sequence, callback, action);
return;
}
var info = getKeyInfo(combination, action);
// make sure to initialize array if this is the first time
// a callback is added for this key
this.callBacks[info.key] = this.callBacks[info.key] || [];
// remove an existing match if there is one
this.getMatches(info.key, info.modifiers, {
type: info.action
}, sequenceName, combination, level);
// add this call back to the array
// if it is a sequence put it at the beginning
// if not put it at the end
//
// this is important because the way these are processed expects
// the sequence ones to come first
this.callBacks[info.key][sequenceName ? 'unshift' : 'push']({
callback: callback,
modifiers: info.modifiers,
action: info.action,
seq: sequenceName,
level: level,
combo: combination
});
};
_proto.bindMultiple = function bindMultiple(combinations, callback, action) {
for (var _iterator = _createForOfIteratorHelperLoose(combinations), _step; !(_step = _iterator()).done;) {
var item = _step.value;
this.bindSingle(item, callback, action);
}
};
return Hotkey;
}();