@ckeditor/ckeditor5-utils
Version:
Miscellaneous utilities used by CKEditor 5.
257 lines (256 loc) • 9.35 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
import CKEditorError from './ckeditorerror.js';
import env from './env.js';
const modifiersToGlyphsMac = {
ctrl: '⌃',
cmd: '⌘',
alt: '⌥',
shift: '⇧'
};
const modifiersToGlyphsNonMac = {
ctrl: 'Ctrl+',
alt: 'Alt+',
shift: 'Shift+'
};
const keyCodesToGlyphs = {
37: '←',
38: '↑',
39: '→',
40: '↓',
9: '⇥',
33: 'Page Up',
34: 'Page Down'
};
/**
* An object with `keyName => keyCode` pairs for a set of known keys.
*
* Contains:
*
* * `a-z`,
* * `0-9`,
* * `f1-f12`,
* * `` ` ``, `-`, `=`, `[`, `]`, `;`, `'`, `,`, `.`, `/`, `\`,
* * `arrow(left|up|right|bottom)`,
* * `backspace`, `delete`, `end`, `enter`, `esc`, `home`, `tab`,
* * `ctrl`, `cmd`, `shift`, `alt`.
*/
export const keyCodes = /* #__PURE__ */ generateKnownKeyCodes();
const keyCodeNames = /* #__PURE__ */ Object.fromEntries(
/* #__PURE__ */ Object.entries(keyCodes).map(([name, code]) => {
let prettyKeyName;
if (code in keyCodesToGlyphs) {
prettyKeyName = keyCodesToGlyphs[code];
}
else {
prettyKeyName = name.charAt(0).toUpperCase() + name.slice(1);
}
return [code, prettyKeyName];
}));
/**
* Converts a key name or {@link module:utils/keyboard~KeystrokeInfo keystroke info} into a key code.
*
* Note: Key names are matched with {@link module:utils/keyboard#keyCodes} in a case-insensitive way.
*
* @param key A key name (see {@link module:utils/keyboard#keyCodes}) or a keystroke data object.
* @returns Key or keystroke code.
*/
export function getCode(key) {
let keyCode;
if (typeof key == 'string') {
keyCode = keyCodes[key.toLowerCase()];
if (!keyCode) {
/**
* Unknown key name. Only key names included in the {@link module:utils/keyboard#keyCodes} can be used.
*
* @error keyboard-unknown-key
* @param {string} key Ths specified key name.
*/
throw new CKEditorError('keyboard-unknown-key', null, { key });
}
}
else {
keyCode = key.keyCode +
(key.altKey ? keyCodes.alt : 0) +
(key.ctrlKey ? keyCodes.ctrl : 0) +
(key.shiftKey ? keyCodes.shift : 0) +
(key.metaKey ? keyCodes.cmd : 0);
}
return keyCode;
}
/**
* Parses the keystroke and returns a keystroke code that will match the code returned by
* {@link module:utils/keyboard~getCode} for the corresponding {@link module:utils/keyboard~KeystrokeInfo keystroke info}.
*
* The keystroke can be passed in two formats:
*
* * as a single string – e.g. `ctrl + A`,
* * as an array of {@link module:utils/keyboard~keyCodes known key names} and key codes – e.g.:
* * `[ 'ctrl', 32 ]` (ctrl + space),
* * `[ 'ctrl', 'a' ]` (ctrl + A).
*
* Note: Key names are matched with {@link module:utils/keyboard#keyCodes} in a case-insensitive way.
*
* Note: Only keystrokes with a single non-modifier key are supported (e.g. `ctrl+A` is OK, but `ctrl+A+B` is not).
*
* Note: On macOS, keystroke handling is translating the `Ctrl` key to the `Cmd` key and handling only that keystroke.
* For example, a registered keystroke `Ctrl+A` will be translated to `Cmd+A` on macOS. To disable the translation of some keystroke,
* use the forced modifier: `Ctrl!+A` (note the exclamation mark).
*
* @param keystroke The keystroke definition.
* @returns Keystroke code.
*/
export function parseKeystroke(keystroke) {
if (typeof keystroke == 'string') {
keystroke = splitKeystrokeText(keystroke);
}
return keystroke
.map(key => (typeof key == 'string') ? getEnvKeyCode(key) : key)
.reduce((key, sum) => sum + key, 0);
}
/**
* Translates any keystroke string text like `"Ctrl+A"` to an
* environment–specific keystroke, i.e. `"⌘A"` on macOS.
*
* @param keystroke The keystroke text.
* @param [forcedEnv] The environment to force the key translation to. If not provided, the current environment is used.
* @returns The keystroke text specific for the environment.
*/
export function getEnvKeystrokeText(keystroke, forcedEnv) {
let keystrokeCode = parseKeystroke(keystroke);
const isMac = forcedEnv ? forcedEnv === 'Mac' : env.isMac || env.isiOS;
const modifiersToGlyphs = Object.entries(isMac ? modifiersToGlyphsMac : modifiersToGlyphsNonMac);
const modifiers = modifiersToGlyphs.reduce((modifiers, [name, glyph]) => {
// Modifier keys are stored as a bit mask so extract those from the keystroke code.
if ((keystrokeCode & keyCodes[name]) != 0) {
keystrokeCode &= ~keyCodes[name];
modifiers += glyph;
}
return modifiers;
}, '');
return modifiers + (keystrokeCode ? keyCodeNames[keystrokeCode] : '');
}
/**
* Returns `true` if the provided key code represents one of the arrow keys.
*
* @param keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
*/
export function isArrowKeyCode(keyCode) {
return keyCode == keyCodes.arrowright ||
keyCode == keyCodes.arrowleft ||
keyCode == keyCodes.arrowup ||
keyCode == keyCodes.arrowdown;
}
/**
* Returns the direction in which the {@link module:engine/model/documentselection~DocumentSelection selection}
* will move when the provided arrow key code is pressed considering the language direction of the editor content.
*
* For instance, in right–to–left (RTL) content languages, pressing the left arrow means moving the selection right (forward)
* in the model structure. Similarly, pressing the right arrow moves the selection left (backward).
*
* @param keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
* @param contentLanguageDirection The content language direction, corresponding to
* {@link module:utils/locale~Locale#contentLanguageDirection}.
* @returns Localized arrow direction or `undefined` for non-arrow key codes.
*/
export function getLocalizedArrowKeyCodeDirection(keyCode, contentLanguageDirection) {
const isLtrContent = contentLanguageDirection === 'ltr';
switch (keyCode) {
case keyCodes.arrowleft:
return isLtrContent ? 'left' : 'right';
case keyCodes.arrowright:
return isLtrContent ? 'right' : 'left';
case keyCodes.arrowup:
return 'up';
case keyCodes.arrowdown:
return 'down';
}
}
/**
* Converts a key name to the key code with mapping based on the env.
*
* See: {@link module:utils/keyboard~getCode}.
*
* @param key The key name (see {@link module:utils/keyboard#keyCodes}).
* @returns Key code.
*/
function getEnvKeyCode(key) {
// Don't remap modifier key for forced modifiers.
if (key.endsWith('!')) {
return getCode(key.slice(0, -1));
}
const code = getCode(key);
return (env.isMac || env.isiOS) && code == keyCodes.ctrl ? keyCodes.cmd : code;
}
/**
* Determines if the provided key code moves the {@link module:engine/model/documentselection~DocumentSelection selection}
* forward or backward considering the language direction of the editor content.
*
* For instance, in right–to–left (RTL) languages, pressing the left arrow means moving forward
* in the model structure. Similarly, pressing the right arrow moves the selection backward.
*
* @param keyCode A key code as in {@link module:utils/keyboard~KeystrokeInfo#keyCode}.
* @param contentLanguageDirection The content language direction, corresponding to
* {@link module:utils/locale~Locale#contentLanguageDirection}.
*/
export function isForwardArrowKeyCode(keyCode, contentLanguageDirection) {
const localizedKeyCodeDirection = getLocalizedArrowKeyCodeDirection(keyCode, contentLanguageDirection);
return localizedKeyCodeDirection === 'down' || localizedKeyCodeDirection === 'right';
}
function generateKnownKeyCodes() {
const keyCodes = {
pageup: 33,
pagedown: 34,
end: 35,
home: 36,
arrowleft: 37,
arrowup: 38,
arrowright: 39,
arrowdown: 40,
backspace: 8,
delete: 46,
enter: 13,
space: 32,
esc: 27,
tab: 9,
// The idea about these numbers is that they do not collide with any real key codes, so we can use them
// like bit masks.
ctrl: 0x110000,
shift: 0x220000,
alt: 0x440000,
cmd: 0x880000
};
// a-z
for (let code = 65; code <= 90; code++) {
const letter = String.fromCharCode(code);
keyCodes[letter.toLowerCase()] = code;
}
// 0-9
for (let code = 48; code <= 57; code++) {
keyCodes[code - 48] = code;
}
// F1-F12
for (let code = 112; code <= 123; code++) {
keyCodes['f' + (code - 111)] = code;
}
// other characters
Object.assign(keyCodes, {
'\'': 222,
',': 108,
'-': 109,
'.': 110,
'/': 111,
';': 186,
'=': 187,
'[': 219,
'\\': 220,
']': 221,
'`': 223
});
return keyCodes;
}
function splitKeystrokeText(keystroke) {
return keystroke.split('+').map(key => key.trim());
}