diff --git a/apps/common/embed/lib/controller/Shortcuts.js b/apps/common/embed/lib/controller/Shortcuts.js new file mode 100644 index 0000000000..aec3dd8462 --- /dev/null +++ b/apps/common/embed/lib/controller/Shortcuts.js @@ -0,0 +1,350 @@ +/* + * (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 + * + */ + + ++function() { + !window.common && (window.common = {}); + !common.controller && (common.controller = {}); + + common.controller.Shortcuts = new(function() { + var localStorageKey = '', + actionsMap = {}, + api = null; + + + var setApi = function(newApi) { + api = newApi; + + if(window.DE) { + localStorageKey = 'de-shortcuts'; + } else if(window.PDFE) { + localStorageKey = 'pdfe-shortcuts'; + } else if(window.PE) { + localStorageKey = 'pe-shortcuts'; + } else if(window.SSE) { + localStorageKey = 'sse-shortcuts'; + } else if(window.VE) { + localStorageKey = 've-shortcuts'; + } + + _fillActionsMap(); + api && _applyShortcutsInSDK(); + }; + + var _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); + }; + + var _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 {}; + }; + + var _fillActionsMap = function() { + actionsMap = {}; + + const shortcutActions = _getDefaultShortcutActions(); + const unlockedTypes = Asc.c_oAscUnlockedShortcutActionTypes || {}; + for (let actionName in shortcutActions) { + const type = shortcutActions[actionName]; + actionsMap[type] = { + action: { + name: actionName, + type: type, + isLocked: !unlockedTypes[type] + }, + shortcuts: [] + } + } + + pairs(Asc.c_oAscDefaultShortcuts).forEach(function(item) { + const actionType = item[0]; + const shortcuts = item[1]; + const actionItem = actionsMap[actionType]; + + if(actionItem) { + actionItem.shortcuts = shortcuts.map(function(ascShortcut) { + return { + keys: _getAscShortcutKeys(ascShortcut), + isCustom: false, + ascShortcut: ascShortcut, + } + }); + } + }); + + let removableIndexes = {}; + pairs(_getModifiedShortcuts()).forEach(function(item) { + const actionType = item[0]; + const shortcuts = item[1]; + const actionItem = 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: _getAscShortcutKeys(ascShortcut), + isCustom: true, + ascShortcut: ascShortcut, + }); + } + }); + } + }) + + for (const actionType in actionsMap) { + const item = actionsMap[actionType]; + const shortcuts = actionsMap[actionType].shortcuts; + if(shortcuts.length == 0 && item.action.isLocked) { + // Delete actions if it has no shortcuts and the action is locked + delete 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; + } + } + } + }; + + var _applyShortcutsInSDK = function() { + const applyMethod = function(storage) { + storage = JSON.parse(storage || common.localStorage.getItem(localStorageKey) || "{}"); + for (const actionType in storage) { + storage[actionType] = storage[actionType].map(function(ascShortcutJson) { + const ascShortcut = new Asc.CAscShortcut(); + ascShortcut.asc_FromJson(ascShortcutJson); + return ascShortcut; + }); + } + + api.asc_resetAllShortcutTypes(); + + const modifiedShortcuts = flatten(values(storage)); + if(modifiedShortcuts.length) { + api.asc_applyAscShortcuts(modifiedShortcuts); + } + }; + + $(window).on('storage', function (e) { + if(e.key == localStorageKey) { + applyMethod(e.originalEvent.newValue); + _fillActionsMap(); + } + }); + + applyMethod(); + }; + + + /** + * Retrieves user-modified shortcuts from localStorage. + * @returns {Object} + * An object where keys are action types and values are arrays of ascShortcut instances. + */ + var _getModifiedShortcuts = function() { + const storage = JSON.parse(common.localStorage.getItem(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; + }; + + + var _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(_keyCodeToKeyName(ascShortcut.asc_GetKeyCode())); + return keys; + }; + + + // Utils + var pairs = function(obj) { + var result = []; + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + result.push([key, obj[key]]); + } + } + return result; + }; + + var flatten = function(array) { + var result = []; + + function flat(arr) { + for (var i = 0; i < arr.length; i++) { + var value = arr[i]; + if (Array.isArray(value)) { + flat(value); + } else { + result.push(value); + } + } + } + + flat(array); + return result; + }; + + var values = function(obj) { + var result = []; + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + result.push(obj[key]); + } + } + return result; + }; + + var findIndex = function(array, predicate) { + for (var i = 0; i < array.length; i++) { + if (predicate(array[i], i, array)) { + return i; + } + } + return -1; + }; + + return { + setApi: setApi + } + }); +}(); \ No newline at end of file diff --git a/apps/documenteditor/embed/index.html b/apps/documenteditor/embed/index.html index 04e1a1bc38..cc6b269050 100644 --- a/apps/documenteditor/embed/index.html +++ b/apps/documenteditor/embed/index.html @@ -260,6 +260,7 @@ + diff --git a/apps/documenteditor/embed/index_loader.html b/apps/documenteditor/embed/index_loader.html index 3a18e5a0f5..347eda1472 100644 --- a/apps/documenteditor/embed/index_loader.html +++ b/apps/documenteditor/embed/index_loader.html @@ -340,6 +340,7 @@ + diff --git a/apps/documenteditor/embed/js/ApplicationController.js b/apps/documenteditor/embed/js/ApplicationController.js index 031203a351..9aa57b451d 100644 --- a/apps/documenteditor/embed/js/ApplicationController.js +++ b/apps/documenteditor/embed/js/ApplicationController.js @@ -163,6 +163,7 @@ DE.ApplicationController = new(function(){ api.asc_setDocInfo(docInfo); api.asc_getEditorPermissions(config.licenseUrl, config.customerId); api.asc_enableKeyEvents(true); + common.controller.Shortcuts.setApi(api); Common.Analytics.trackEvent('Load', 'Start'); } diff --git a/apps/presentationeditor/embed/index.html b/apps/presentationeditor/embed/index.html index 4f92508e00..0dbf6721cc 100644 --- a/apps/presentationeditor/embed/index.html +++ b/apps/presentationeditor/embed/index.html @@ -276,6 +276,7 @@ + diff --git a/apps/presentationeditor/embed/index_loader.html b/apps/presentationeditor/embed/index_loader.html index 668604747c..d4d8a7e32b 100644 --- a/apps/presentationeditor/embed/index_loader.html +++ b/apps/presentationeditor/embed/index_loader.html @@ -337,6 +337,7 @@ + diff --git a/apps/presentationeditor/embed/js/ApplicationController.js b/apps/presentationeditor/embed/js/ApplicationController.js index db23f216af..96fe4b5414 100644 --- a/apps/presentationeditor/embed/js/ApplicationController.js +++ b/apps/presentationeditor/embed/js/ApplicationController.js @@ -155,6 +155,7 @@ PE.ApplicationController = new(function(){ api.asc_setDocInfo(docInfo); api.asc_getEditorPermissions(config.licenseUrl, config.customerId); api.asc_enableKeyEvents(true); + common.controller.Shortcuts.setApi(api); Common.Analytics.trackEvent('Load', 'Start'); } diff --git a/apps/spreadsheeteditor/embed/index.html b/apps/spreadsheeteditor/embed/index.html index 5d094869c9..f17e83118d 100644 --- a/apps/spreadsheeteditor/embed/index.html +++ b/apps/spreadsheeteditor/embed/index.html @@ -261,6 +261,7 @@ + diff --git a/apps/spreadsheeteditor/embed/index_loader.html b/apps/spreadsheeteditor/embed/index_loader.html index 3b29ca20fe..62449c306c 100644 --- a/apps/spreadsheeteditor/embed/index_loader.html +++ b/apps/spreadsheeteditor/embed/index_loader.html @@ -346,6 +346,7 @@ + diff --git a/apps/spreadsheeteditor/embed/js/ApplicationController.js b/apps/spreadsheeteditor/embed/js/ApplicationController.js index c88d49c7d1..8735473a90 100644 --- a/apps/spreadsheeteditor/embed/js/ApplicationController.js +++ b/apps/spreadsheeteditor/embed/js/ApplicationController.js @@ -171,6 +171,7 @@ SSE.ApplicationController = new(function(){ api.asc_setDocInfo(docInfo); api.asc_getEditorPermissions(config.licenseUrl, config.customerId); api.asc_enableKeyEvents(true); + common.controller.Shortcuts.setApi(api); Common.Analytics.trackEvent('Load', 'Start'); } diff --git a/build/documenteditor.json b/build/documenteditor.json index 9371e376bb..12d2c8f852 100644 --- a/build/documenteditor.json +++ b/build/documenteditor.json @@ -357,6 +357,7 @@ "../apps/common/embed/lib/view/LoadMask.js", "../apps/common/embed/lib/view/modals.js", "../apps/common/embed/lib/controller/modals.js", + "../apps/common/embed/lib/controller/Shortcuts.js", "../apps/common/embed/lib/view/SearchBar.js", "../apps/documenteditor/embed/js/SearchBar.js", "../apps/documenteditor/embed/js/ApplicationView.js", diff --git a/build/presentationeditor.json b/build/presentationeditor.json index 0c9850c2a9..76524582a9 100644 --- a/build/presentationeditor.json +++ b/build/presentationeditor.json @@ -355,6 +355,7 @@ "../apps/common/embed/lib/view/LoadMask.js", "../apps/common/embed/lib/view/modals.js", "../apps/common/embed/lib/controller/modals.js", + "../apps/common/embed/lib/controller/Shortcuts.js", "../apps/common/embed/lib/view/SearchBar.js", "../apps/presentationeditor/embed/js/SearchBar.js", "../apps/presentationeditor/embed/js/ApplicationView.js", diff --git a/build/spreadsheeteditor.json b/build/spreadsheeteditor.json index d36473ae89..91c3948220 100644 --- a/build/spreadsheeteditor.json +++ b/build/spreadsheeteditor.json @@ -369,6 +369,7 @@ "../apps/common/embed/lib/view/LoadMask.js", "../apps/common/embed/lib/view/modals.js", "../apps/common/embed/lib/controller/modals.js", + "../apps/common/embed/lib/controller/Shortcuts.js", "../apps/common/embed/lib/view/SearchBar.js", "../apps/spreadsheeteditor/embed/js/SearchBar.js", "../apps/spreadsheeteditor/embed/js/ApplicationView.js",