UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

389 lines (347 loc) 14.4 kB
/*! * 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; });