Files
DocumentServer-v-9.2.0/web-apps/apps/common/main/lib/controller/Shortcuts.js
Yajbir Singh f1b860b25c
Some checks failed
check / markdownlint (push) Has been cancelled
check / spellchecker (push) Has been cancelled
updated
2025-12-11 19:03:17 +05:30

609 lines
24 KiB
JavaScript

/*
* (c) Copyright Ascensio System SIA 2010-2024
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation. In accordance with
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
* that Ascensio System SIA expressly excludes the warranty of non-infringement
* of any third-party rights.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
* street, Riga, Latvia, EU, LV-1050.
*
* The interactive user interfaces in modified source and object code versions
* of the Program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU AGPL version 3.
*
* Pursuant to Section 7(b) of the License you must retain the original Product
* logo when distributing the program. Pursuant to Section 7(e) we decline to
* grant you any rights under trademark law for use of our trademarks.
*
* All the Product's GUI elements, including illustrations and icon sets, as
* well as technical writing content are licensed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
*
*/
/**
* Created on 1/9/2025.
*/
define([
'core'
], function () {
'use strict';
Common.Controllers.Shortcuts = Backbone.Controller.extend(_.extend({
initialize: function() {
this.localStorageKey = '';
if(window.DE) {
this.localStorageKey = 'de-shortcuts';
} else if(window.PDFE) {
this.localStorageKey = 'pdfe-shortcuts';
} else if(window.PE) {
this.localStorageKey = 'pe-shortcuts';
} else if(window.SSE) {
this.localStorageKey = 'sse-shortcuts';
} else if(window.VE) {
this.localStorageKey = 've-shortcuts';
}
this.actionsMap = {};
this.eventsMap = {};
},
setApi: function(api) {
this.api = api;
this._fillActionsMap();
api && this._applyShortcutsInSDK();
this._eventsTrigger();
return this;
},
keyCodeToKeyName: function(code) {
const specialKeys = {
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
16: 'Shift',
17: 'Ctrl',
18: 'Alt',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: 'Space',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
44: 'PrintScreen',
45: 'Insert',
46: 'Delete',
91: 'Meta',
93: 'ContextMenu',
96: "Num 0",
97: "Num 1",
98: "Num 2",
99: "Num 3",
100: "Num 4",
101: "Num 5",
102: "Num 6",
103: "Num 7",
104: "Num 8",
105: "Num 9",
106: "Num *",
107: "Num +",
108: "Num Enter",
109: "Num -",
110: "Num .",
111: "Num /",
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
173: 'ff-',
61: 'ff=',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: '\'',
};
if (specialKeys[code]) {
return specialKeys[code];
}
return String.fromCharCode(code);
},
resetAllShortcuts: function() {
const me = this;
this.api.asc_resetAllShortcutTypes();
for (const actionType in Asc.c_oAscDefaultShortcuts) {
const actionItem = this.getActionsMap()[actionType];
actionItem.shortcuts = Asc.c_oAscDefaultShortcuts[actionType].map(function(ascShortcut) {
return {
keys: me._getAscShortcutKeys(ascShortcut),
isCustom: false,
ascShortcut: ascShortcut,
}
});
}
this._eventsTrigger();
Common.localStorage.setItem(this.localStorageKey, '');
Common.NotificationCenter.trigger('shortcuts:update');
},
updateShortcutsForAction: function(actionType, updatedShortcuts) {
let resultShortcuts = [];
const originalShortcuts = this.getActionsMap()[actionType].shortcuts;
// Update hidden status and create copies for those not in updatedShortcuts
originalShortcuts.forEach(function(item) {
const existsInUpdated = _.some(updatedShortcuts, function(el) {
return el.ascShortcut.asc_GetShortcutIndex() === item.ascShortcut.asc_GetShortcutIndex();
});
if(!existsInUpdated && !item.ascShortcut.asc_IsHidden()) {
const copyAscShortcut = new Asc.CAscShortcut();
copyAscShortcut.asc_FromJson(item.ascShortcut.asc_ToJson());
item.ascShortcut = copyAscShortcut;
}
item.ascShortcut.asc_SetIsHidden(!existsInUpdated);
resultShortcuts.push(item);
});
// Add new custom shortcuts that are not in originalShortcuts
updatedShortcuts.forEach(function(item) {
const existsInOriginal = _.some(originalShortcuts, function(el) {
return el.ascShortcut.asc_GetShortcutIndex() === item.ascShortcut.asc_GetShortcutIndex();
});
if(!existsInOriginal) {
item.isCustom = true;
resultShortcuts.push(item);
}
});
const action = this.getActionsMap()[actionType];
resultShortcuts = resultShortcuts.sort(this._sortComparator);
action && (action.shortcuts = resultShortcuts);
// Remove shortcuts from other actions if they conflict with updated shortcuts
const removableIndexes = updatedShortcuts.map(function(item) {
return item.ascShortcut.asc_GetShortcutIndex()
});
const removableFromStorage = {};
for (const type in this.getActionsMap()) {
if(removableIndexes.length == 0) break;
if(type == actionType) continue;
const shortcuts = this.getActionsMap()[type].shortcuts;
const foundIndex = _.findIndex(shortcuts, function(shortcut) {
const foundIndex = _.indexOf(removableIndexes, shortcut.ascShortcut.asc_GetShortcutIndex());
(foundIndex != -1) && removableIndexes.splice(foundIndex, 1);
return foundIndex != -1;
});
if(foundIndex != -1) {
const copyAscShortcut = new Asc.CAscShortcut();
copyAscShortcut.asc_FromJson(shortcuts[foundIndex].ascShortcut.asc_ToJson());
copyAscShortcut.asc_SetIsHidden(true);
shortcuts[foundIndex].ascShortcut = copyAscShortcut;
!removableFromStorage[type] && (removableFromStorage[type] = []);
removableFromStorage[type].push(shortcuts[foundIndex].ascShortcut);
}
}
this.api.asc_applyAscShortcuts(resultShortcuts.map(function(shortcut) {
return shortcut.ascShortcut;
}));
//Filter for save in local storage
const savedInStorage = {
[actionType] : _.filter(resultShortcuts, function(item) {
return item.isCustom !== item.ascShortcut.asc_IsHidden();
}).map(function(shortcut) {
return shortcut.ascShortcut;
})
};
this._saveModifiedShortcuts(savedInStorage, removableFromStorage);
Common.NotificationCenter.trigger('shortcuts:update');
this._eventsTrigger([actionType].concat(_.keys(removableFromStorage)));
},
getActionsMap: function() {
return this.actionsMap;
},
getDefaultShortcutActions: function() {
if(window.DE || window.PDFE) {
return Asc.c_oAscDocumentShortcutType;
} else if(window.PE) {
return Asc.c_oAscPresentationShortcutType;
} else if(window.SSE) {
return Asc.c_oAscSpreadsheetShortcutType;
} else if(window.VE) {
return Asc.c_oAscDiagramShortcutType;
}
return {};
},
/**
* Returns a list of shortcuts for the given action type.
*
* @param {string|number} actionType
* The action type identifier. Can be:
* - `string` — action type name ('Save', 'Undo', 'Copy' and so on)
* - `number` — numeric action type id
*
* @returns {Array<{keys: string[], isCustom: boolean, ascShortcut: CAscShortcut}>|null}
*
* @example
* getShortcutsByActionType('Save');
* getShortcutsByActionType(7);
*/
getShortcutsByActionType: function(actionType, withHidden) {
const actionTypeNumber = (typeof actionType === 'number')
? actionType
: this.getDefaultShortcutActions()[actionType];
const actionItem = this.actionsMap[actionTypeNumber];
let shortcuts = actionItem ? actionItem.shortcuts : null;
if(!withHidden && shortcuts) {
shortcuts = shortcuts.filter(function(shortcut) {
return !shortcut.ascShortcut.asc_IsHidden();
});
}
return shortcuts;
},
/**
* Updates button hints for shortcuts.
*
* For each action type, finds the first visible shortcut (non-hidden)
* and appends its key combination to the base label. Then applies the
* updated hint to the button or via a custom callback.
*
* @param {Object<string, {
* btn: Object,
* label: string,
* applyCallback?: function(Object, string):void,
* ignoreUpdates?: boolean
* }>} shortcutHints
*
* An object where:
* - key is the action type (actionType),
* - value is a config object:
*
* - `btn` {Object} — button object that provides `updateHint(string)` method.
* - `label` {string} — the base label text for the hint.
* - `applyCallback` [optional] — custom function to apply the generated hint text.
* Receives `(item, hintText)` as arguments.
* - `ignoreUpdates` [optional] — if `true`, will not be updated when shortcuts are updated
*
* @example
* updateShortcutHints({
* EditUndo: {
* btn: btnUndo,
* label: 'Undo'
* },
* EditRedo: {
* label: 'Redo',
* applyCallback: function(item, hintText) {
* console.log('Custom hint:', text);
* },
* ignoreUpdates: true
* }
* });
*/
updateShortcutHints: function(shortcutHints) {
for (const actionType in shortcutHints) {
const item = shortcutHints[actionType];
if(item) {
const callback = function() {
const defaultShortcutsActions = this.getDefaultShortcutActions();
const type = defaultShortcutsActions[actionType];
const action = type ? this.actionsMap[type] : null;
const firstShortcut = action
? _.find(action.shortcuts, (shortcut) => {
return !shortcut.ascShortcut.asc_IsHidden();
})
: null;
const hintText = (item.label || '') + (firstShortcut ? ' (' + firstShortcut.keys.join('+') + ')' : '');
if(item.applyCallback) {
item.applyCallback(item, hintText);
} else {
item.btn.updateHint(hintText);
}
}.bind(this);
callback();
if(!item.ignoreUpdates) {
!this.eventsMap[actionType] && (this.eventsMap[actionType] = []);
this.eventsMap[actionType].push(callback);
}
}
}
},
_getDefaultShortcutActionsInvert: function() {
if(!this._defaultShortcutsActionsInvert) {
this._defaultShortcutsActionsInvert = _.invert(this.getDefaultShortcutActions());
}
return this._defaultShortcutsActionsInvert;
},
_eventsTrigger: function(actionTypes) {
const me = this;
let lists = [];
if(actionTypes) {
lists = actionTypes.map(function(type) {
type = (typeof +type === 'number')
? me._getDefaultShortcutActionsInvert()[type]
: type;
return me.eventsMap[type];
});
} else {
lists = _.values(this.eventsMap);
}
_.each(lists, function(callbacks) {
_.each(callbacks, function(cb) {
cb && cb();
});
});
},
_fillActionsMap: function() {
this.actionsMap = {};
const me = this;
const shortcutActions = this.getDefaultShortcutActions();
const unlockedTypes = Asc.c_oAscUnlockedShortcutActionTypes || {};
for (let actionName in shortcutActions) {
const type = shortcutActions[actionName];
this.actionsMap[type] = {
action: {
name: this['txtLabel' + actionName],
description: this['txtDescription' + actionName],
type: type,
isLocked: !unlockedTypes[type]
},
shortcuts: []
}
}
_.pairs(Asc.c_oAscDefaultShortcuts).forEach(function(item) {
const actionType = item[0];
const shortcuts = item[1];
const actionItem = me.actionsMap[actionType];
if(actionItem) {
actionItem.shortcuts = shortcuts.map(function(ascShortcut) {
return {
keys: me._getAscShortcutKeys(ascShortcut),
isCustom: false,
ascShortcut: ascShortcut,
}
});
}
});
let removableIndexes = {};
_.pairs(me._getModifiedShortcuts()).forEach(function(item) {
const actionType = item[0];
const shortcuts = item[1];
const actionItem = me.actionsMap[actionType];
if(actionItem) {
shortcuts.forEach(function(ascShortcut) {
const ascShortcutIndex = ascShortcut.asc_GetShortcutIndex();
const defaultShortcutIndex = _.findIndex(actionItem.shortcuts, function(shortcut) {
return shortcut.ascShortcut.asc_GetShortcutIndex() == ascShortcutIndex;
});
if(defaultShortcutIndex != -1) {
actionItem.shortcuts[defaultShortcutIndex].ascShortcut = ascShortcut;
} else {
removableIndexes[ascShortcutIndex] = actionType;
actionItem.shortcuts.push({
keys: me._getAscShortcutKeys(ascShortcut),
isCustom: true,
ascShortcut: ascShortcut,
});
}
});
}
})
for (const actionType in this.actionsMap) {
const item = this.actionsMap[actionType];
const shortcuts = this.actionsMap[actionType].shortcuts;
if(shortcuts.length == 0 && item.action.isLocked) {
// Delete actions if it has no shortcuts and the action is locked
delete this.actionsMap[actionType];
} else if(Object.keys(removableIndexes).length > 0) {
// Remove shortcuts from other actions if they conflict with updated shortcuts
const foundIndex = _.findIndex(shortcuts, function(shortcut) {
const ascShortcutIndex = shortcut.ascShortcut.asc_GetShortcutIndex();
if(removableIndexes[ascShortcutIndex] && removableIndexes[ascShortcutIndex] != actionType) {
delete removableIndexes[ascShortcutIndex];
return true;
}
return false;
});
if(foundIndex != -1) {
const copyAscShortcut = new Asc.CAscShortcut();
copyAscShortcut.asc_FromJson(shortcuts[foundIndex].ascShortcut.asc_ToJson());
copyAscShortcut.asc_SetIsHidden(true);
shortcuts[foundIndex].ascShortcut = copyAscShortcut;
}
}
}
},
_applyShortcutsInSDK: function() {
const applyMethod = function(storage) {
storage = JSON.parse(storage || Common.localStorage.getItem(this.localStorageKey) || "{}");
for (const actionType in storage) {
storage[actionType] = storage[actionType].map(function(ascShortcutJson) {
const ascShortcut = new Asc.CAscShortcut();
ascShortcut.asc_FromJson(ascShortcutJson);
return ascShortcut;
});
}
this.api.asc_resetAllShortcutTypes();
const modifiedShortcuts = _.flatten(_.values(storage));
if(modifiedShortcuts.length) {
this.api.asc_applyAscShortcuts(modifiedShortcuts);
}
}.bind(this);
$(window).on('storage', function (e) {
if(e.key == this.localStorageKey) {
applyMethod(e.originalEvent.newValue);
this._fillActionsMap();
this._eventsTrigger();
Common.NotificationCenter.trigger('shortcuts:update');
}
}.bind(this))
applyMethod();
},
/**
* Retrieves user-modified shortcuts from localStorage.
* @returns {Object<number, CAscShortcut[]>}
* An object where keys are action types and values are arrays of ascShortcut instances.
*/
_getModifiedShortcuts: function() {
const storage = JSON.parse(Common.localStorage.getItem(this.localStorageKey) || "{}");
for (const actionType in storage) {
storage[actionType] = storage[actionType].map(function(ascShortcutJson) {
const ascShortcut = new Asc.CAscShortcut();
ascShortcut.asc_FromJson(ascShortcutJson);
return ascShortcut;
});
}
return storage;
},
/**
* Saves modified shortcuts to localStorage by applying added and removed changes.
* @param {Object<number, CAscShortcut[]>} savedActionsMap
* An object containing new or updated shortcuts, grouped by action type.
* @param {Object<number, CAscShortcut[]>} removedActionsMap
* An object containing shortcuts to be removed, grouped by action type.
* @example
* this._saveModifiedShortcuts(
* { "7": [ascShortcut1, ascShortcut2] },
* { "52": [ascShortcut3] }
* );
* // Result:
* // - For action type "7", shortcuts are replaced with [ascShortcut1, ascShortcut2]
* // - For action type "52", ascShortcut3 is removed
* // - The final shortcuts are saved to localStorage
*/
_saveModifiedShortcuts: function(savedActionsMap, removedActionsMap) {
const customShortcuts = _.extend({}, this._getModifiedShortcuts(), savedActionsMap || {});
for (const actionType in removedActionsMap || {}) {
if (!customShortcuts[actionType]) continue;
const removed = removedActionsMap[actionType];
customShortcuts[actionType] = _.filter(customShortcuts[actionType], function(ascShortcut) {
return !_.some(removed, function(r) {
return ascShortcut.asc_GetShortcutIndex() === r.asc_GetShortcutIndex();
});
});
}
for (const actionType in customShortcuts) {
customShortcuts[actionType] = customShortcuts[actionType].map(function(ascShortcut) {
return ascShortcut.asc_ToJson();
});
}
Common.localStorage.setItem(this.localStorageKey, JSON.stringify(customShortcuts));
},
_getAscShortcutKeys: function(ascShortcut) {
const keys = [];
ascShortcut.asc_IsCommand() && keys.push('⌘');
ascShortcut.asc_IsCtrl() && keys.push('Ctrl');
ascShortcut.asc_IsAlt() && keys.push('Alt');
ascShortcut.asc_IsShift() && keys.push('Shift');
keys.push(this.keyCodeToKeyName(ascShortcut.asc_GetKeyCode()));
return keys;
},
_sortComparator: function(first, second) {
const priorityModifierKeys = ['asc_IsCommand', 'asc_IsCtrl', 'asc_IsAlt', 'asc_IsShift'];
function getWeight(ascShortcut) {
// Search for the first modifier key
let keyIndex = priorityModifierKeys.length;
for (let i = 0; i < priorityModifierKeys.length; i++) {
if (ascShortcut[priorityModifierKeys[i]]()) {
keyIndex = i;
break;
}
}
if (keyIndex === priorityModifierKeys.length) return -1;
// Count extra modifier keys
let extras = 0;
for (let j = 0; j < priorityModifierKeys.length; j++) {
if (j !== keyIndex && ascShortcut[priorityModifierKeys[j]]()) extras++;
}
// weight = range for main key + “cost” of extra keys
return keyIndex * 100 + extras;
}
if (first.ascShortcut.asc_IsLocked() && !second.ascShortcut.asc_IsLocked()) return -1;
if (!first.ascShortcut.asc_IsLocked() && second.ascShortcut.asc_IsLocked()) return 1;
let wFirst = getWeight(first.ascShortcut);
let wSecond = getWeight(second.ascShortcut);
if (wFirst !== wSecond) return wFirst - wSecond;
return first.ascShortcut.asc_GetKeyCode() - second.ascShortcut.asc_GetKeyCode();
}
}, Common.Controllers.Shortcuts || {}));
});