@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
389 lines (347 loc) • 14.4 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([
'sap/ui/Device',
'sap/ui/core/Lib',
'sap/ui/events/jquery/EventExtension'
], function(
Device,
Library
/*jQuery*/
) {
"use strict";
// translates from shortcut specification term in the API to the string given in the event.key property
var mKeyDefinitionFix = {
plus: "+",
space: " "
};
// translates key strings from some browsers (currently only Firefox) to standard strings
var mEventKeyFix = {
OS: "Meta" // Firefox only
};
// a very incomplete list of shortcuts which are a bad idea to register and are hence not allowed
var mDisallowedShortcuts = {
// a-z
"ctrl+l": "jump to address bar",
"ctrl+n": "new window, cannot be registered in Chrome",
"ctrl+shift+n": "new incognito window, cannot be registered in Chrome",
"ctrl+alt+shift+p": "UI5 Technical Info",
"ctrl+q": "quit Chrome in Mac",
"ctrl+alt+shift+s": "UI5 Support Popup",
"ctrl+t": "new tab, cannot be registered in Chrome",
"ctrl+shift+t": "reopen last tab, cannot be registered in Chrome",
"ctrl+w": "close tab, cannot be registered in Chrome",
"ctrl+shift+w": "close window, cannot be registered in Chrome",
// 0-9
"ctrl+0": "reset zoom",
// .,-*/=+
"ctrl+-": "zoom out",
"ctrl++": "zoom in",
"ctrl+shift+=": "cannot be handled",
// Tab|Space|Enter
"tab": "TAB-based keyboard navigation",
"shift+tab": "TAB-based keyboard navigation",
"ctrl+tab": "cycling through tabs, cannot be registered in Chrome",
"ctrl+shift+tab": "cycling through tabs, cannot be registered in Chrome",
// Backspace|Home|Delete|End|Pageup|Pagedown|Escape
"ctrl+alt+delete": "nice try",
"ctrl+pageup": "cycling through tabs, cannot be registered in Chrome",
"ctrl+pagedown": "cycling through tabs, cannot be registered in Chrome",
// F1-12
"f6": "F6-based group navigation",
"f11": "fullscreen, cannot be registered in Chrome",
"f12": "browser dev tools"
};
// make detectable at any time whether the last time the Alt key was pressed it was the left one or it was AltGr
var bLastAltWasLeftAlt = false;
document.addEventListener('keydown', function(e) {
try {
if (e.keyCode === 18) { // 'alt' Key
bLastAltWasLeftAlt = (typeof e.location !== "number" /* location isn't supported */ || e.location === 1 /* left */);
return;
}
} catch (err) {
// ignore any errors
}
});
var oShortcutHelper = {
/**
* Returns the existing registered matching shortcut on this control or undefined
*
* @param {sap.ui.core.Control} oScopeControl the control/region at which the shortcut was registered
* @param {object} oNormalizedShortcutSpec the normalized shortcut information
*
* @return {object} Shortcut data
* @private
*/
findShortcut: function(oScopeControl, oNormalizedShortcutSpec) {
var aRegisteredShortcutData = oScopeControl.data("sap.ui.core.Shortcut");
if (!aRegisteredShortcutData) {
return;
}
var aMatching = aRegisteredShortcutData.filter(function(oData){
var bMatches =
oData.shortcutSpec.key === oNormalizedShortcutSpec.key &&
oData.shortcutSpec.ctrlKey === oNormalizedShortcutSpec.ctrlKey &&
oData.shortcutSpec.altKey === oNormalizedShortcutSpec.altKey &&
oData.shortcutSpec.shiftKey === oNormalizedShortcutSpec.shiftKey &&
oData.shortcutSpec.metaKey === oNormalizedShortcutSpec.metaKey;
return bMatches;
});
return aMatching[0]; // there is either 0 or 1 matching shortcut;
},
/**
* Parses and normalizes the shortcut being registered
*
* @param {object|string} vShortcut the shortcut to normalize.
*
* @returns {object} normalized shortcut spec
* @private
*/
getNormalizedShortcutSpec: function(vShortcut) {
var oNormalizedShortcutSpec;
if (typeof vShortcut === "string") {
oNormalizedShortcutSpec = oShortcutHelper.parseShortcut(vShortcut);
} else { // spec object
var key = vShortcut.key;
var bValidShortcut = /^([a-z0-9\.,\-\*\/= +]|Tab|Enter|Backspace|Home|Delete|End|Pageup|Pagedown|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Escape|F[1-9]|F1[0-2])$/i.test(key);
if (!bValidShortcut) {
throw new Error("Shortcut key '" + key + "' is not a valid shortcut key. It must match /^([a-z0-9\.,\-\*\/= +]|Tab|Enter|Backspace|Home|Delete|End|Pageup|Pagedown|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Escape|F[1-9]|F1[0-2])$/i");
}
oNormalizedShortcutSpec = {
key: oShortcutHelper.translateRegisteredKeyToStandard(key).toLowerCase(),
ctrlKey: Device.os.macintosh ? false : !!vShortcut.ctrl,
ctrlRequested: vShortcut.ctrl,
altKey: !!vShortcut.alt,
shiftKey: !!vShortcut.shift,
metaKey: Device.os.macintosh ? !!vShortcut.ctrl : false
};
}
return oNormalizedShortcutSpec;
},
/**
* Parse shortcut string to shortcut object
*
* e.g.: 'CTRL + S' --> {key:'S', ctrlRequested:true, ...}
*
* @param {string} sShortcut A Shortcut string
* @private
*/
parseShortcut: function(sShortcut) {
this.validateShortcutString(sShortcut);
var aParts = sShortcut.toLowerCase().split("+");
return {
key: oShortcutHelper.translateRegisteredKeyToStandard(aParts.pop()),
ctrlKey: Device.os.macintosh ? false : aParts.indexOf("ctrl") > -1,
ctrlRequested: aParts.indexOf("ctrl") > -1,
altKey: aParts.indexOf("alt") > -1,
shiftKey: aParts.indexOf("shift") > -1,
metaKey: Device.os.macintosh ? aParts.indexOf("ctrl") > -1 : false
};
},
/**
* Convert shortcut key part to 'real' event.key character
*
* e.g.: 'Ctrl + Plus' --> 'Ctrl + +' - the same applies for 'Space'
*
* @param {string} sKeySpec The shortcut key in lower-case, e.g. "space" or "plus"
*
* @returns {string} Converted key character
* @private
*/
translateRegisteredKeyToStandard: function(sKeySpec) {
return mKeyDefinitionFix.hasOwnProperty(sKeySpec) ? mKeyDefinitionFix[sKeySpec] : sKeySpec;
},
/**
* Check whether the key combination to be registered is allowed.
*
* @param {string} sShortcut The shortcut string
* @throws {Error} Throws an Error if shortcut string is not valid
* @private
*/
validateShortcutString: function(sShortcut) {
var bValidShortcut = /^((Ctrl|Shift|Alt)\+){0,3}([a-z0-9\.,\-\*\/=]|Plus|Tab|Space|Enter|Backspace|Home|Delete|End|Pageup|Pagedown|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|F[1-9]|F1[0-2])$/i.test(sShortcut);
if (!bValidShortcut) {
throw new Error("Shortcut '" + sShortcut + "' is not a valid shortcut string. It must be a '+'-separated list of modifier keys and the actual key, like 'Ctrl+Alt+S'. Or more generally, it must match the expression /^((Ctrl|Shift|Alt)\+){0,3}([a-z0-9\.,\-\*\/=]|Plus|Tab|Space|Enter|Backspace|Home|Delete|End|Pageup|Pagedown|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Escape|F[1-9]|F1[0-2])$/i.");
}
},
/**
* Check whether the key combination to be registered is allowed.
*
* @param {object} oNormalizedShortcutSpec Normalized shortcut data
* @throws {Error} Throws an Error if shortcut is not allowed
* @private
*/
validateKeyCombination: function(oNormalizedShortcutSpec) {
var sNormalizedShortcut = oNormalizedShortcutSpec.ctrlRequested ? "ctrl+" : ""; // whether ctrl was registered, not the platform-dependent modifier
sNormalizedShortcut += oNormalizedShortcutSpec.altKey ? "alt+" : "";
sNormalizedShortcut += oNormalizedShortcutSpec.shiftKey ? "shift+" : "";
sNormalizedShortcut += oNormalizedShortcutSpec.key;
if (mDisallowedShortcuts[sNormalizedShortcut]) {
throw new Error("Registering the shortcut '" + sNormalizedShortcut + "' is not allowed (" + mDisallowedShortcuts[sNormalizedShortcut] + ").");
}
// disallow all combinations of "Shift" with those keys which are turened into something different when Shift is pressed (or where Shift is required)
if ([".", ",", "-", "+", "=", "*", "/"].indexOf(oNormalizedShortcutSpec.key) > -1 && sNormalizedShortcut.indexOf("shift") > -1) {
throw new Error("Registering the shortcut '" + sNormalizedShortcut + "' is not allowed because the 'Shift' modifier changes the meaning of the " + oNormalizedShortcutSpec.key + " key on many keyboards.");
}
},
/**
* Returns normalized shortcut string from shortcut data object.
*
* e.g.: {key:'s', ctrlRequested:true, altKey:false, shiftKey:true} --> 'ctrl+shift+s'
*
* @param {object} oNormalizedShortcutSpec Normalized shortcut data
*
* @returns {string} The normalized shortcut string
* @private
*/
getNormalizedShortcutString: function(oNormalizedShortcutSpec) {
var sNormalizedShortcut = oNormalizedShortcutSpec.ctrlRequested ? "ctrl+" : ""; // whether ctrl was registered, not the platform-dependent modifier
sNormalizedShortcut += oNormalizedShortcutSpec.altKey ? "alt+" : "";
sNormalizedShortcut += oNormalizedShortcutSpec.shiftKey ? "shift+" : "";
sNormalizedShortcut += oNormalizedShortcutSpec.key;
return sNormalizedShortcut;
},
/**
* Normalizes a shortcut string by trimming spaces, converting single character keys to uppercase,
* and adjusting modifier keys for the current platform (e.g., "Ctrl" to "Cmd" on Mac).
*
* @param {string} sShortcut The shortcut string to normalize, e.g., "ctrl+Alt+s" will be normalized to "Ctrl+Alt+S" on Windows and "Cmd+Option+S" on Mac.
* @returns {string} Normalized shortcut string
*/
normalizeShortcutText: function(sShortcut) {
const allParts = sShortcut.split('+').map((p) => p.trim());
const modifiers = {
ctrl: false,
alt: false,
shift: false
};
let key = null;
for (const part of allParts) {
const lower = part.toLowerCase();
if (lower in modifiers) {
modifiers[lower] = true;
} else if (!key) {
if (part.length === 1) {
key = part.toUpperCase(); // Single character keys are transformed to uppercase
} else {
key = part;
}
}
}
const result = [];
if (modifiers.ctrl) {
result.push(Device.os.macintosh ? 'Cmd' : 'Ctrl');
}
if (modifiers.alt) {
result.push(Device.os.macintosh ? 'Option' : 'Alt');
}
if (modifiers.shift) {
result.push('Shift');
}
if (key) {
result.push(key);
}
return result.join('+');
},
/**
* Translates a keyboard shortcut string by localizing each key segment.
* The shortcut string is expected to use '+' as a delimiter (e.g., "Ctrl+Shift+S").
* If a translation is not found, the original key is used.
*
* @param {string} sShortcut The shortcut string
* @return {string} The translated shortcut string
*/
localizeKeys: (sShortcut) => {
const oResourceBundle = Library.getResourceBundleFor("sap.ui.core");
return sShortcut
.split("+")
.map((key) => {
const sKey = key.trim();
const sPropertiesKey = `Keyboard.Shortcut.${sKey}`;
const sText = sKey.length > 1 ? oResourceBundle.getText(sPropertiesKey) : sKey;
return sText === sPropertiesKey ? key.trim() : sText;
}).join("+");
},
/**
* Check if shortcut key may be normally used for this kind of DOM node.
*
* e.g.: Arrow keys are normally used in inputs or textareas and shouldn' be used for shortcuts
*
* @param {object} oShortcutSpec Normalized shortcut data object
* @param {object} oDomElement A DOM node
*
* @return {boolean} true if shortcut shouldn't be used
* @private
*/
shortcutMayBeUsedHere: function(oShortcutSpec, oDomElement) {
var sTagName = oDomElement.tagName.toLowerCase();
if ((sTagName === "input" || sTagName === "textarea") &&
oShortcutSpec.key.includes("arrow")
) {
return false;
}
return true;
},
/**
* The handler executed for ALL keydown events passing a shortcut region
*
* @param {object} oShortcutSpec The normalized shortcut data object
* @param {string|object} vOriginalShortcut The original shortcut data passed from the caller
* @param {function} fnCallback The callback function to execute
* @param {object} oEvent The keydown browser event
*
* @private
*/
handleKeydown: function(oShortcutSpec, vOriginalShortcut, fnCallback, oEvent) {
// There are situations (SNOW: DINC0032372), where a keydown event is triggered which is no instance of "KeyboardEvent".
// Hence, this explicit check is required.
if (!oEvent.key) {
return;
}
// do not react to keydown of modifier keys
if (oEvent.key === "Control" || oEvent.key === "Shift" || oEvent.key === "Alt" || oEvent.key === "AltGraph" || oEvent.key === "Meta") {
return;
}
// do not react when the event has already been handled by a control
if (oEvent.isMarked()) {
return;
}
// AltGr triggers "Ctrl" and "Alt" flags on events, but we don't want AltGr to do the same as Ctrl+Alt
if (oEvent.altKey && !bLastAltWasLeftAlt) { // Alt is active, but it was actually the AltGr key; we don't support any AltGr shortcuts
return;
}
// handle some browser differences regarding reported keys
var key = mEventKeyFix.hasOwnProperty(oEvent.key) ? mEventKeyFix[oEvent.key] : oEvent.key;
key = key.toLowerCase(); // TODO: validate usage of toLowerCase
// check whether the shortcut matches
if (key !== oShortcutSpec.key ||
oEvent.ctrlKey !== oShortcutSpec.ctrlKey ||
oEvent.altKey !== oShortcutSpec.altKey ||
oEvent.shiftKey !== oShortcutSpec.shiftKey ||
oEvent.metaKey !== oShortcutSpec.metaKey) {
return; // do not react if key or modifiers don't match
}
// some keys may not be consumed here depending on the event target (e.g. arrow keys inside input fields)
if (!oShortcutHelper.shortcutMayBeUsedHere(oShortcutSpec, oEvent.target || oEvent.srcElement)) {
return;
}
// now we know this event matches the registered shortcut and should be handled
// do not trigger the browser default action for this shortcut or other UI5 actions
oEvent.preventDefault();
oEvent.setMarked();
oEvent.stopPropagation();
// the information passed into the callback
var oShortcutInfo = {
registeredShortcut: vOriginalShortcut,
originalBrowserEvent: oEvent.originalEvent || oEvent
};
// trigger the callback
fnCallback(oShortcutInfo);
}
};
return oShortcutHelper;
});