(function() {
/**
* @typedef {Help4.service.Service.Params} Help4.service.HotkeyService.Params
* @property {Help4.engine.crossorigin.CoreEngine} crossOriginEngine
* @property {Help4.service.CrossOriginMessageService} crossOriginService
* @property {Object} keyMap
* @property {Object} hotkeyMap
*/
/**
* @augments Help4.service.Service
*/
Help4.service.HotkeyService = class extends Help4.service.Service {
/**
* @override
* @constructor
* @param {Help4.service.HotkeyService.Params} params
*/
constructor(params) {
super(params, {
crossOriginEngine: null,
crossOriginService: null,
keyMap: {},
hotkeyMap: {}
});
const {_params, _observers} = this;
this._onCrossOrigin = event => _params.eventBus.fire(event); // simply propagate
_params.crossOriginEngine?.onHotkeyEvent.addListener(this._onCrossOrigin);
_params.crossOriginService?.onHotkeyEvent.addListener(this._onCrossOrigin);
_observers.window = new Help4.observer.WindowObserver(event => _onEvent.call(this, event));
this._enabledKeys = {};
}
/**
* keys that are ignored while editing input fields
* @memberof Help4.service.HotkeyService
* @type {string[]}
*/
static IGNORE_INPUT = [
'back',
'tab',
'enter',
'shift',
'ctrl',
'shift_*',
'ctrl_home',
'ctrl_end',
'ctrl_up',
'ctrl_down',
'ctrl_left',
'ctrl_right',
'ctrl_c',
'ctrl_x',
'ctrl_v',
'ctrl_a',
'capslock',
'space',
'end',
'home',
'up',
'left',
'down',
'right',
'ins',
'del'
]
/**
* @param {Object} [event = {}]
* @returns {?Element}
*/
static getActiveInputElement(event = {}) {
if (event._orig) event = event._orig;
/**
* @param {Element} startElement
* @returns {?Element}
*/
const search = startElement => {
if (startElement) {
if (startElement.tagName === 'INPUT' || startElement.tagName === 'TEXTAREA') return startElement;
if (startElement.shadowRoot) return search(startElement.shadowRoot.activeElement);
let element = startElement;
do {
if (Help4.Element.isContentEditable(element)) return element;
} while (element = Help4.Element.getParent(element));
}
return null;
}
const {document} = event?.view || window;
return search(document.activeElement);
}
/**
* @param {Object} event
* @returns {?string}
*/
static getShortcut(event) {
const key = this.getKey(event);
return Help4.HOTKEY_MAP_HTML[key];
}
/**
* @param {Object} event
* @returns {?string}
*/
static getKey(event) {
/** @type {Help4.controller.Controller} */ const controller = Help4.getController();
/** @type {Help4.service.HotkeyService} */ const hotkeyService = controller?.getService('hotkey');
return hotkeyService?.getKey(event);
}
/**
* @param {Object} event
* @param {Object} keyMap
* @returns {boolean}
*/
static cancelKeyEvent(event, keyMap) {
const {keyCode, ctrlKey, altKey} = event;
if (keyCode === 32) { // ENTER
if (!Help4.Element.getClassName(event.target).match(Help4.CLASS_PREFIX)) return true; // XRAY-2824
} else if (
!ctrlKey && !altKey && (keyCode < 112 || keyCode > 123) || // ctrl / alt not pressed and not a F-Key
keyCode >= 16 && keyCode <= 18 // pure modifier key - SHIFT, CTRL, ALT
) {
return true;
}
return Help4.Event.cancel(event);
}
/** destroys the instance */
destroy() {
const {_params} = this;
if (this._onCrossOrigin) {
_params.crossOriginEngine?.onHotkeyEvent?.removeListener?.(this._onCrossOrigin);
_params.crossOriginService?.onHotkeyEvent?.removeListener?.(this._onCrossOrigin);
delete this._onCrossOrigin;
}
delete this._enabledKeys;
super.destroy();
}
/**
* @param {Object} event
* @returns {string}
*/
getKey(event) {
const {keyMap} = this._params;
const {ctrlKey, altKey, shiftKey, keyCode} = event;
const key = [
ctrlKey ? 'ctrl' : '',
altKey ? 'alt' : '',
shiftKey ? 'shift' : '',
keyMap[keyCode] || String.fromCharCode(keyCode).toLowerCase()
].join('_');
return key
.replace(/_{2,}/g, '_')
.replace(/^_|_$/g, '');
}
/**
* @param {string} key
* @returns {boolean}
*/
hotkeyEnabled(key) {
return !!this._enabledKeys[key];
}
/**
* @param {string} keys
* @returns {Help4.service.HotkeyService}
*/
enableHotkey(...keys) {
const {_enabledKeys, _observers, _params: {crossOriginEngine, crossOriginService}} = this;
for (const key of keys) {
_enabledKeys[key] ??= 0;
_enabledKeys[key]++;
}
// sometimes DOM loads very slowly
// make sure observer is updated as much as possible
_observers.window
.disconnect()
.observeAll({
eventObserver: {
type: ['keydown', 'keyup'],
capture: true
}
});
if (crossOriginEngine) {
const {recording: type} = crossOriginEngine?.AGENT_TYPE;
const command = {type, command: 'enableHotkey', params: keys};
crossOriginEngine
.sendCommand(command)
.catch(Help4.noop);
crossOriginService
?.sendCommand(command)
.catch(Help4.noop);
}
return this;
}
/**
* @param {string} keys
* @returns {Help4.service.HotkeyService}
*/
disableHotkey(...keys) {
const {_enabledKeys, _observers, _params: {crossOriginEngine, crossOriginService}} = this;
for (const key of keys) {
if (!(--_enabledKeys[key])) delete _enabledKeys[key];
}
if (crossOriginEngine) {
const {recording: type} = crossOriginEngine.AGENT_TYPE;
const command = {type, command: 'disableHotkey', params: keys};
crossOriginEngine
.sendCommand(command)
.catch(Help4.noop);
crossOriginService
?.sendCommand(command)
.catch(Help4.noop);
}
return this;
}
}
/**
* @memberof Help4.service.HotkeyService#
* @private
* @param {Object} event
* @returns {boolean}
*/
function _onEvent(event) {
if (this.isDestroyed()) return true;
const {
/** @type {Help4.controller.Controller} */ controller,
/** @type {Help4.EventBus} */ eventBus,
/** @type {Object} */ hotkeyMap,
/** @type {Object} */ keyMap
} = this._params;
const {
/** @type {boolean} */ CMP4,
/** @type {boolean} */ isTourMode,
/** @type {boolean} */ isEditMode
} = /** @type {Help4.typedef.SystemConfiguration} */ controller.getConfiguration();
/** @type {string} */ const key = this.getKey(event);
/** @type {string} */ const hotkey = _keyToHotkey.call(this, key)
// hotkeys need to be enabled to be executed; XRAY-3266
if (!hotkey || !this.hotkeyEnabled(hotkey)) return true;
// some hotkeys only execute if WA is focussed
if (hotkeyMap[hotkey].focusRequired && !Help4.Element.isClassIncluded(document.activeElement, Help4.CLASS_PREFIX)) return true;
// tour automation requires a certain key handling; XRAY-3231
if (!isEditMode) {
if (CMP4) {
const instance = Help4.widget.getActiveInstance();
if (instance?.getName() === 'tour' && instance.getAutoProgressKeys().includes(key)) return true;
} else if (isTourMode) {
if (controller.getHandler().getAutoProgressKeys().includes(key)) return true;
}
}
// do not handle certain keys while actively typing into an input field
if (!_checkInputField.call(this, event, key)) return true;
if (event.type === 'keyup') eventBus.fire({type: Help4.EventBus.TYPES.hotkey, hotkey});
// abort keydown and keyup to ensure that no hotkey executes on two functionalities
return this.constructor.cancelKeyEvent(event, keyMap);
}
/**
* @memberof Help4.service.HotkeyService#
* @private
* @param {string} key
* @returns {?string}
*/
function _keyToHotkey(key) {
const {hotkeyMap} = this._params;
for (let [id, value] of Object.entries(hotkeyMap)) {
if (value.key === key) return id;
}
return null;
}
/**
* @memberof Help4.service.HotkeyService#
* @private
* @param {Object} event
* @param {string} key
* @returns {boolean}
*/
function _checkInputField(event, key) {
if (!this.constructor.getActiveInputElement(event)) return true; // not within input/textarea field: ok
// check for keys that are not to be observed during input field editing (such as normal letters or backspace)
const {IGNORE_INPUT} = this.constructor;
if (key.length === 1 || IGNORE_INPUT.includes(key)) return false;
// check for modifier combinations that are not to be observed
for (const ignored of IGNORE_INPUT) {
if (ignored.search(/_\*$/) >= 0) {
const mod = ignored.split('_')[0];
// mod and simple key is pressed
if (key.includes(mod + '_') && key.substring(mod.length + 1).length === 1) return false;
}
}
return true;
}
})();