All files / platform/core/src/classes HotkeysManager.ts

46.98% Statements 39/83
44.82% Branches 13/29
43.47% Functions 10/23
46.34% Lines 38/82

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296                              34x 34x 34x 34x           34x 34x 34x     34x                                                                                                                       34x 34x 1258x                       2516x                         34x 34x     34x     34x 1258x   1258x             1258x     34x                   68x         68x 2516x 306x       68x                                                                                                                             1258x       1258x 1258x     1258x         1258x   1258x           1258x 1258x                       1258x 1258x       1258x 1258x   1258x                                                                  
import objectHash from 'object-hash';
import { hotkeys as mouseTrapAPI } from '../utils';
import Hotkey from './Hotkey';
import migrateOldHotkeyDefinitions from '../utils/hotkeys/migrateHotkeys';
 
/**
 *
 *
 * @typedef {Object} HotkeyDefinition
 * @property {String} commandName - Command to call
 * @property {Object} commandOptions - Command options
 * @property {String} label - Display name for hotkey
 * @property {String[]} keys - Keys to bind; Follows Mousetrap.js binding syntax
 */
export class HotkeysManager {
  private _servicesManager: AppTypes.ServicesManager;
  private _commandsManager: AppTypes.CommandsManager;
  private isEnabled: boolean = true;
  public hotkeyDefinitions: Record<string, any> = {};
  public hotkeyDefaults: any[] = [];
 
  constructor(
    commandsManager: AppTypes.CommandsManager,
    servicesManager: AppTypes.ServicesManager
  ) {
    this._servicesManager = servicesManager;
    this._commandsManager = commandsManager;
 
    // Check for old hotkey definitions format and migrate if needed
    migrateOldHotkeyDefinitions({
      generateHash: this.generateHash,
    });
  }
 
  /**
   * Exposes Mousetrap.js's `.record` method, added by the record plugin.
   *
   * @param {*} event
   */
  record(event) {
    return mouseTrapAPI.record(event);
  }
 
  cancel() {
    mouseTrapAPI.stopRecord();
    mouseTrapAPI.unpause();
  }
 
  /**
   * Disables all hotkeys. Hotkeys added while disabled will not listen for
   * input.
   */
  disable() {
    this.isEnabled = false;
    mouseTrapAPI.pause();
  }
 
  /**
   * Enables all hotkeys.
   */
  enable() {
    this.isEnabled = true;
    mouseTrapAPI.unpause();
  }
 
  /**
   * Uses most recent
   *
   * @returns {undefined}
   */
  restoreDefaultBindings() {
    this.setHotkeys(this.hotkeyDefaults);
  }
 
  /**
   *
   */
  destroy() {
    this.hotkeyDefaults = [];
    this.hotkeyDefinitions = {};
    mouseTrapAPI.reset();
  }
 
  /**
   * Registers a list of hotkey definitions.
   *
   * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
   */
  setHotkeys(hotkeyDefinitions = []) {
    try {
      const definitions = this.getValidDefinitions(hotkeyDefinitions);
      definitions.forEach(definition => this.registerHotkeys(definition));
    } catch (error) {
      const { uiNotificationService } = this._servicesManager.services;
      uiNotificationService.show({
        title: 'Hotkeys Manager',
        message: 'Error while setting hotkeys',
        type: 'error',
      });
    }
  }
 
  generateHash(definition) {
    return objectHash({
      commandName: definition.commandName,
      commandOptions: definition.commandOptions || {},
    });
  }
 
  /**
   * Set default hotkey bindings. These
   * values are used in `this.restoreDefaultBindings`.
   *
   * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
   */
  setDefaultHotKeys(hotkeyDefinitions = []) {
    const definitions = this.getValidDefinitions(hotkeyDefinitions);
    this.hotkeyDefaults = definitions;
 
    // Get user preferred keys from localStorage
    const userPreferredKeys = JSON.parse(localStorage.getItem('user-preferred-keys') || '{}');
 
    // Update definitions with user preferred keys before setting
    const updatedDefinitions = definitions.map(definition => {
      const commandHash = this.generateHash(definition);
      // If user has a preferred key binding, use it
      Iif (userPreferredKeys[commandHash]) {
        return {
          ...definition,
          keys: userPreferredKeys[commandHash],
        };
      }
 
      return definition;
    });
 
    this.setHotkeys(updatedDefinitions);
  }
 
  /**
   * Take hotkey definitions that can be an array or object and make sure that it
   * returns an array of hotkeys
   *
   * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
   */
  getValidDefinitions(hotkeyDefinitions) {
    const definitions = Array.isArray(hotkeyDefinitions)
      ? [...hotkeyDefinitions]
      : this._parseToArrayLike(hotkeyDefinitions);
 
    // make sure isEditable is true for all definitions if not provided
    definitions.forEach(definition => {
      if (definition.isEditable === undefined) {
        definition.isEditable = true;
      }
    });
 
    return definitions;
  }
 
  /**
   * Take hotkey definitions that can be an array and make sure that it
   * returns an object of hotkeys definitions
   *
   * @param {HotkeyDefinition[]} [hotkeyDefinitions=[]] Contains hotkeys definitions
   * @returns {Object}
   */
  getValidHotkeyDefinitions(hotkeyDefinitions) {
    const definitions = this.getValidDefinitions(hotkeyDefinitions);
    const objectDefinitions = {};
    definitions.forEach(definition => {
      const { commandName, commandOptions } = definition;
      const commandHash = objectHash({ commandName, commandOptions });
      objectDefinitions[commandHash] = definition;
    });
    return objectDefinitions;
  }
 
  /**
   * It parses given object containing hotkeyDefinition to array like.
   * Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
   *
   * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions
   * @returns {HotkeyDefinition[]}
   */
  _parseToArrayLike(hotkeyDefinitionsObj = {}) {
    const copy = { ...hotkeyDefinitionsObj };
    return Object.entries(copy).map(entryValue =>
      this._parseToHotKeyObj(entryValue[0], entryValue[1])
    );
  }
 
  /**
   * Return HotkeyDefinition object like based on given property name and property value
   * @param {string} propertyName property name of hotkey definition object
   * @param {object} propertyValue property value of hotkey definition object
   */
  _parseToHotKeyObj(propertyName, propertyValue) {
    return {
      commandName: propertyName,
      ...propertyValue,
    };
  }
 
  /**
   * (Unbinds and) binds the specified command to one or more key combinations.
   * When the hotkey combination is triggered, the command name and active contexts
   * are used to locate and execute the appropriate command.
   *
   * @param hotkey - The hotkey definition object.
   * @throws {Error} Throws an error if no commandName is provided.
   */
  registerHotkeys({
    commandName,
    commandOptions = {},
    context,
    keys,
    label,
    isEditable,
  }: Hotkey): void {
    Iif (!commandName) {
      throw new Error(`No command was defined for hotkey "${keys}"`);
    }
 
    const commandHash = this.generateHash({ commandName, commandOptions });
    const existingHotkey = this.hotkeyDefinitions[commandHash];
 
    // If the hotkey has already been registered with the same keys, skip re-registration.
    Iif (existingHotkey && existingHotkey.keys === keys) {
      console.debug('HotkeysManager: Identical hotkey registration skipped.');
      return;
    }
 
    const userPreferredKeys = JSON.parse(localStorage.getItem('user-preferred-keys') || '{}');
 
    Iif (existingHotkey) {
      userPreferredKeys[commandHash] = keys;
      localStorage.setItem('user-preferred-keys', JSON.stringify(userPreferredKeys));
      this._unbindHotkeys(commandName, existingHotkey.keys);
    }
 
    this.hotkeyDefinitions[commandHash] = { commandName, commandOptions, keys, label, isEditable };
    this._bindHotkeys(commandName, commandOptions, context, keys);
  }
 
  /**
   * Binds one or more set of hotkey combinations for a given command
   *
   * @private
   * @param {string} commandName - The name of the command to trigger when hotkeys are used
   * @param {string[]} keys - One or more key combinations that should trigger command
   * @returns {undefined}
   */
  _bindHotkeys(commandName, commandOptions = {}, context, keys) {
    const isKeyDefined = keys === '' || keys === undefined;
    Iif (isKeyDefined) {
      return;
    }
 
    const isKeyArray = keys instanceof Array;
    const combinedKeys = isKeyArray ? keys.join('+') : keys;
 
    mouseTrapAPI.bind(combinedKeys, evt => {
      evt.preventDefault();
      evt.stopPropagation();
      this._commandsManager.runCommand(commandName, { evt, ...commandOptions }, context);
    });
  }
 
  /**
   * unbinds one or more set of hotkey combinations for a given command
   *
   * @private
   * @param {string} commandName - The name of the previously bound command
   * @param {string[]} keys - One or more sets of previously bound keys
   * @returns {undefined}
   */
  _unbindHotkeys(commandName, keys) {
    const isKeyDefined = keys !== '' && keys !== undefined;
    Iif (!isKeyDefined) {
      return;
    }
 
    const isKeyArray = keys instanceof Array;
    Iif (isKeyArray) {
      const combinedKeys = keys.join('+');
      this._unbindHotkeys(commandName, combinedKeys);
      return;
    }
 
    mouseTrapAPI.unbind(keys);
  }
}
 
export default HotkeysManager;