mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-25 03:28:45 +02:00
N°3524 Add keyboard shortcuts to global actions
This commit is contained in:
@@ -2771,4 +2771,28 @@ HTML;
|
||||
return $aMatchingClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return keyboard shortcuts config as an array
|
||||
*
|
||||
* @return array
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public static function GetKeyboardShortcutPref(): array
|
||||
{
|
||||
$aResultPref = [];
|
||||
$aShortcutPrefs = appUserPreferences::GetPref('keyboard_shortcuts', []);
|
||||
$aShortcutClasses = utils::GetClassesForInterface('iKeyboardShortcut','', array('/lib/', 'node_modules', 'test'));
|
||||
|
||||
foreach($aShortcutClasses as $cShortcutPlugin) {
|
||||
$sTriggeredElement = $cShortcutPlugin::GetShortcutTriggeredElementSelector();
|
||||
foreach ($cShortcutPlugin::GetShortcutKeys() as $aShortcutKey) {
|
||||
$sKey = isset($aShortcutPrefs[$aShortcutKey['id']]) ? $aShortcutPrefs[$aShortcutKey['id']] : $aShortcutKey['key'];
|
||||
$aResultPref[$aShortcutKey['id']] = ['key' => $sKey, 'label' => $aShortcutKey['label'], 'event' => $aShortcutKey['event'], 'triggered_element_selector' => $sTriggeredElement];
|
||||
}
|
||||
}
|
||||
return $aResultPref;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,19 @@ $ibo-preferences--user-preferences--picture-placeholder--image--background-color
|
||||
$ibo-preferences--user-preferences--picture-placeholder--image--active--border-color: $ibo-color-blue-800;
|
||||
$ibo-preferences--user-preferences--picture-placeholder--image--hover--border-color: $ibo-color-blue-600;
|
||||
|
||||
$ibo-keyboard-shortcut--shortcut--width: 30% !default;
|
||||
|
||||
$ibo-keyboard-shortcut--input--color: $ibo-color-grey-800 !default;
|
||||
$ibo-keyboard-shortcut--input--background-color: transparent !default;
|
||||
$ibo-keyboard-shortcut--input--border-color: $ibo-color-grey-500 !default;
|
||||
$ibo-keyboard-shortcut--input--border-radius: $ibo-border-radius-300 !default;
|
||||
$ibo-keyboard-shortcut--input--padding-y: 2px !default;
|
||||
$ibo-keyboard-shortcut--input--padding-x: 4px !default;
|
||||
$ibo-keyboard-shortcut--input--margin-bottom: 5px !default;
|
||||
|
||||
$ibo-keyboard-shortcut--input--is-focus--color: $ibo-color-primary-800 !default;
|
||||
$ibo-keyboard-shortcut--input--is-focus--border-color: $ibo-color-primary-600 !default;
|
||||
|
||||
#ibo-main-content >.ibo-panel{
|
||||
margin-left: $ibo-preferences--panel--margin-x;
|
||||
margin-right: $ibo-preferences--panel--margin-x;
|
||||
@@ -35,4 +48,35 @@ $ibo-preferences--user-preferences--picture-placeholder--image--hover--border-co
|
||||
}
|
||||
.ibo-preferences--user-preferences--picture-placeholder--image:hover{
|
||||
border-color: $ibo-preferences--user-preferences--picture-placeholder--image--hover--border-color;
|
||||
}
|
||||
|
||||
#ibo-form-for-user-interface-preferences > .ibo-keyboard-shortcut--shortcut{
|
||||
display: table;
|
||||
width: 100%;
|
||||
>*:not(.ibo-button){
|
||||
width: $ibo-keyboard-shortcut--shortcut--width;
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-keyboard-shortcut--input, .ibo-keyboard-shortcut--input:focus{
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
|
||||
color: $ibo-keyboard-shortcut--input--color;
|
||||
background-color: $ibo-keyboard-shortcut--input--background-color;
|
||||
border: 1px solid $ibo-keyboard-shortcut--input--border-color;
|
||||
border-bottom: 2px solid $ibo-keyboard-shortcut--input--border-color;
|
||||
border-radius: $ibo-keyboard-shortcut--input--border-radius;
|
||||
|
||||
padding: $ibo-keyboard-shortcut--input--padding-y $ibo-keyboard-shortcut--input--padding-x;
|
||||
margin-bottom: $ibo-keyboard-shortcut--input--margin-bottom;
|
||||
|
||||
&.ibo-is-focus{
|
||||
text-transform: none;
|
||||
color: $ibo-keyboard-shortcut--input--is-focus--color;
|
||||
border-color: $ibo-keyboard-shortcut--input--is-focus--border-color;
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,5 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Component:GlobalSearch:Recents:Title' => 'Recents',
|
||||
'UI:Component:GlobalSearch:LastQueries:NoQuery:Placeholder' => 'You haven\'t run any search yet',
|
||||
'UI:Component:GlobalSearch:HistoryDisabled' => 'History is disabled',
|
||||
'UI:Component:GlobalSearch:KeyboardShortcut:OpenDrawer' => 'Open global search',
|
||||
));
|
||||
@@ -24,4 +24,5 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Component:QuickCreate:Recents:Title' => 'Recents',
|
||||
'UI:Component:QuickCreate:LastClasses:NoClass:Placeholder' => 'You haven\'t create any object yet',
|
||||
'UI:Component:QuickCreate:HistoryDisabled' => 'History is disabled',
|
||||
'UI:Component:QuickCreate:KeyboardShortcut:OpenDrawer' => 'Open quick create',
|
||||
));
|
||||
@@ -28,6 +28,7 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Layout:NavigationMenu:MenuFilter:Input:Hint' => 'Matches from all menu groups will be displayed',
|
||||
'UI:Layout:NavigationMenu:MenuFilter:Placeholder:Hint' => 'No result for this menu filter',
|
||||
'UI:Layout:NavigationMenu:UserInfo:WelcomeMessage:Text' => 'Hi %1$s!',
|
||||
'UI:Layout:NavigationMenu:UserInfo:Picture:AltText' => '%1$s\'s contact picture'
|
||||
'UI:Layout:NavigationMenu:UserInfo:Picture:AltText' => '%1$s\'s contact picture',
|
||||
'UI:Layout:NavigationMenu:KeyboardShortcut:FocusFilter' => 'Filter menu entries'
|
||||
|
||||
));
|
||||
@@ -5,4 +5,8 @@
|
||||
*/
|
||||
|
||||
Dict::Add('EN US', 'English', 'English', [
|
||||
'UI:Layout:ObjectDetails:KeyboardShortcut:EditObject' => 'Edit displayed object',
|
||||
'UI:Layout:ObjectDetails:KeyboardShortcut:DeleteObject' => 'Delete displayed object',
|
||||
'UI:Layout:ObjectDetails:KeyboardShortcut:NewObject' => 'Create a new object (with same class as displayed object)',
|
||||
'UI:Layout:ObjectDetails:KeyboardShortcut:SaveObject' => 'Save displayed object',
|
||||
]);
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
// Navigation menu
|
||||
Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Preferences:Title' => 'Preferences',
|
||||
|
||||
'UI:Preferences:UserInterface:Title' => 'User interface',
|
||||
'UI:Preferences:Lists:Title' => 'Lists',
|
||||
'UI:Preferences:RichText:Title' => 'Rich text editor',
|
||||
@@ -30,6 +29,9 @@ Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Preferences:ActivityPanel:Title' => 'Activity panel',
|
||||
'UI:Preferences:ActivityPanel:EntryFormOpened' => 'Entry form opened by default',
|
||||
'UI:Preferences:ActivityPanel:EntryFormOpened+' => 'Whether the entry form will be opened when displaying an object. If unchecked, you will still be able to open it by clicking the compose button',
|
||||
'UI:Preferences:PersonalizeKeyboardShortcuts:Title' => 'Application keyboard shortcuts',
|
||||
'UI:Preferences:PersonalizeKeyboardShortcuts:Input:Hint' => 'Type a keyboard shortcut',
|
||||
'UI:Preferences:PersonalizeKeyboardShortcuts:Button:Tooltip' => 'Record a keyboard shortcut',
|
||||
'UI:Preferences:Tabs:Title' => 'Tabs',
|
||||
'UI:Preferences:Tabs:Layout:Label' => 'Layout',
|
||||
'UI:Preferences:Tabs:Layout:Horizontal' => 'Horizontal',
|
||||
|
||||
@@ -80,6 +80,9 @@ $(function()
|
||||
this.element.find(this.js_selectors.compartment_element).on('click', function(oEvent){
|
||||
me._onCompartmentElementClick(oEvent, $(this));
|
||||
});
|
||||
this.element.on('open_drawer', function(oEvent){
|
||||
me._onIconClick(oEvent);
|
||||
});
|
||||
// Mostly for outside clicks that should close elements
|
||||
oBodyElem.on('click', function(oEvent){
|
||||
me._onBodyClick(oEvent);
|
||||
@@ -134,20 +137,6 @@ $(function()
|
||||
},
|
||||
_onBodyKeyUp: function(oEvent)
|
||||
{
|
||||
// Note: We thought about extracting the oEvent.key in a variable to lower case it, but this would be done
|
||||
// on every single key up in the application, which might not be what we want... (time consuming)
|
||||
if((oEvent.altKey === true) && (oEvent.key === 'h' || oEvent.key === 'H'))
|
||||
{
|
||||
if(this._isDrawerOpened())
|
||||
{
|
||||
this._setFocusOnInput();
|
||||
}
|
||||
// If drawer is closed, we trigger the click on the icon in order for the other widget to behave like they should (eg. close themselves)
|
||||
else
|
||||
{
|
||||
this.element.find(this.js_selectors.icon).trigger('click');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Methods
|
||||
|
||||
@@ -86,6 +86,9 @@ $(function()
|
||||
this.element.find(this.js_selectors.input).on('change', function(oEvent){
|
||||
me._onInputOptionSelected(oEvent, $(this));
|
||||
});
|
||||
this.element.on('open_drawer', function(oEvent){
|
||||
me._onIconClick(oEvent);
|
||||
});
|
||||
// Mostly for outside clicks that should close elements
|
||||
oBodyElem.on('click', function(oEvent){
|
||||
me._onBodyClick(oEvent);
|
||||
|
||||
@@ -76,6 +76,9 @@ $(function()
|
||||
this.element.find(this.js_selectors.menu_group).on('click', function (oEvent) {
|
||||
me._onMenuGroupClick(oEvent, $(this))
|
||||
});
|
||||
this.element.on('filter_shortcut', function(oEvent){
|
||||
me._filterShortcut();
|
||||
});
|
||||
// Mostly for outside clicks that should close elements
|
||||
oBodyElem.on('click', function (oEvent) {
|
||||
me._onBodyClick(oEvent);
|
||||
@@ -140,18 +143,16 @@ $(function()
|
||||
},
|
||||
_onBodyKeyUp: function(oEvent)
|
||||
{
|
||||
// Note: We thought about extracting the oEvent.key in a variable to lower case it, but this would be done
|
||||
// on every single key up in the application, which might not be what we want... (time consuming)
|
||||
if((oEvent.altKey === true) && (oEvent.key === 'm' || oEvent.key === 'M'))
|
||||
},
|
||||
_filterShortcut: function()
|
||||
{
|
||||
if(this._getActiveMenuGroupId() === null)
|
||||
{
|
||||
if(this._getActiveMenuGroupId() === null)
|
||||
{
|
||||
const sFirstMenuGroupId = this.element.find(this.js_selectors.menu_group+':first').attr('data-menu-group-id');
|
||||
this._openDrawer(sFirstMenuGroupId);
|
||||
}
|
||||
|
||||
this._focusFilter();
|
||||
const sFirstMenuGroupId = this.element.find(this.js_selectors.menu_group+':first').attr('data-menu-group-id');
|
||||
this._openDrawer(sFirstMenuGroupId);
|
||||
}
|
||||
|
||||
this._focusFilter();
|
||||
},
|
||||
|
||||
_onFilterKeyUp: function(oEvent)
|
||||
|
||||
2
js/mousetrap/mousetrap-record.min.js
vendored
Normal file
2
js/mousetrap/mousetrap-record.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
(function(d){function n(b,a,h){if(this.recording)if("keydown"==h.type){1===b.length&&g&&k();for(i=0;i<a.length;++i)l(a[i]);l(b)}else"keyup"==h.type&&0<c.length&&k();else p.apply(this,arguments)}function l(b){var a;for(a=0;a<c.length;++a)if(c[a]===b)return;c.push(b);1===b.length&&(g=!0)}function k(){e.push(c);c=[];g=!1;clearTimeout(m);m=setTimeout(q,1E3)}function r(b){var a;for(a=0;a<b.length;++a)b[a].sort(function(a,b){return 1<a.length&&1===b.length?-1:1===a.length&&1<b.length?1:a>b?1:-1}),b[a]=
|
||||
b[a].join("+")}function q(){f&&(r(e),f(e));e=[];f=null;c=[]}var e=[],f=null,c=[],g=!1,m=null,p=d.prototype.handleKey;d.prototype.record=function(b){var a=this;a.recording=!0;f=function(){a.recording=!1;b.apply(a,arguments)}};d.prototype.handleKey=function(){n.apply(this,arguments)};d.init()})(Mousetrap);
|
||||
11
js/mousetrap/mousetrap.min.js
vendored
Normal file
11
js/mousetrap/mousetrap.min.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/* mousetrap v1.6.5 craig.is/killing/mice */
|
||||
(function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent("on"+b,g)}function z(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||
|
||||
"meta"==a}function A(a,b){var g,d=[];var e=a;"+"===e?e=["+"]:(e=e.replace(/\+{2}/g,"+plus"),e=e.split("+"));for(g=0;g<e.length;++g){var m=e[g];B[m]&&(m=B[m]);b&&"keypress"!=b&&C[m]&&(m=C[m],d.push("shift"));w(m)&&d.push(m)}e=m;g=b;if(!g){if(!p){p={};for(var c in n)95<c&&112>c||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a=
|
||||
a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l<k._callbacks[a].length;++l){var c=k._callbacks[a][l];if((f||!c.seq||p[c.seq]==c.level)&&h==c.action){var e;(e="keypress"==h&&!t.metaKey&&!t.ctrlKey)||(e=c.modifiers,e=b.sort().join(",")===e.sort().join(","));e&&(e=f&&c.seq==f&&c.level==d,(!f&&c.combo==g||e)&&k._callbacks[a].splice(l,1),E.push(c))}}return E}function c(a,b,c,f){k.stopCallback(b,
|
||||
b.target||b.srcElement,c,f)||!1!==a(b,c)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=z(a);b&&("keyup"==a.type&&y===b?y=!1:k.handleKey(b,F(a),a))}function m(a,g,t,f){function h(c){return function(){x=c;++p[a];clearTimeout(q);q=setTimeout(b,1E3)}}function l(g){c(t,g,a);"keyup"!==f&&(y=z(g));setTimeout(b,10)}for(var d=p[a]=0;d<g.length;++d){var e=d+1===g.length?l:h(f||
|
||||
A(g[d+1]).action);n(g[d],e,f,a,d)}}function n(a,b,c,f,d){k._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var e=a.split(" ");1<e.length?m(a,e,b,c):(c=A(a,c),k._callbacks[c.key]=k._callbacks[c.key]||[],g(c.key,c.modifiers,{type:c.action},f,a,d),k._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:d,combo:a}))}var k=this;a=a||u;if(!(k instanceof d))return new d(a);k.target=a;k._callbacks={};k._directMap={};var p={},q,y=!1,r=!1,x=!1;k._handleKey=function(a,
|
||||
d,e){var f=g(a,d,e),h;d={};var k=0,l=!1;for(h=0;h<f.length;++h)f[h].seq&&(k=Math.max(k,f[h].level));for(h=0;h<f.length;++h)f[h].seq?f[h].level==k&&(l=!0,d[f[h].seq]=1,c(f[h].callback,e,f[h].combo,f[h].seq)):l||c(f[h].callback,e,f[h].combo);f="keypress"==e.type&&r;e.type!=x||w(a)||f||b(d);r=l&&"keydown"==e.type};k._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)n(a[d],b,c)};v(a,"keypress",e);v(a,"keydown",e);v(a,"keyup",e)}if(q){var n={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",
|
||||
18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},r={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},C={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter",
|
||||
escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={};
|
||||
this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null};
|
||||
d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null);
|
||||
39
js/pages/backoffice/keyboard-shortcuts.js
Normal file
39
js/pages/backoffice/keyboard-shortcuts.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2020 Combodo SARL
|
||||
*
|
||||
* This file is part of iTop.
|
||||
*
|
||||
* iTop is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* iTop is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
$(function() {
|
||||
$.widget('itop.keyboard_shortcuts',
|
||||
{
|
||||
// default options
|
||||
options:
|
||||
{
|
||||
shortcuts: {}
|
||||
},
|
||||
_create: function(){
|
||||
this._initializeBinds();
|
||||
},
|
||||
_initializeBinds: function (){
|
||||
for(let sShortcutId in this.options.shortcuts){
|
||||
let aShortcut = this.options.shortcuts[sShortcutId];
|
||||
Mousetrap.bind(aShortcut.key, function() {
|
||||
$(aShortcut.triggered_element_selector).trigger(aShortcut.event);
|
||||
},'keyup');
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -132,6 +132,9 @@ CKEDITOR.plugins.add( 'disabler',
|
||||
|
||||
// Processing on each pages of the backoffice
|
||||
$(document).ready(function(){
|
||||
// Initialize global keyboard shortcuts
|
||||
$('body').keyboard_shortcuts({shortcuts: aKeyboardShortcuts});
|
||||
|
||||
// Enable tooltips based on existing HTML markup, won't work on markup added dynamically after DOM ready (AJAX, ...)
|
||||
$('[data-tooltip-content]:not([data-tooltip-instantiated="true"])').each(function () {
|
||||
CombodoTooltip.InitTooltipFromMarkup($(this));
|
||||
|
||||
@@ -2293,6 +2293,7 @@ return array(
|
||||
'iDBObjectSetIterator' => $baseDir . '/core/dbobjectiterator.php',
|
||||
'iDBObjectURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php',
|
||||
'iDisplay' => $baseDir . '/core/dbobject.class.php',
|
||||
'iKeyboardShortcut' => $baseDir . '/sources/application/UI/Hook/iKeyboardShortcut.php',
|
||||
'iLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php',
|
||||
'iLoginExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
'iLoginFSMExtension' => $baseDir . '/application/applicationextension.inc.php',
|
||||
|
||||
@@ -2523,6 +2523,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
|
||||
'iDBObjectSetIterator' => __DIR__ . '/../..' . '/core/dbobjectiterator.php',
|
||||
'iDBObjectURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php',
|
||||
'iDisplay' => __DIR__ . '/../..' . '/core/dbobject.class.php',
|
||||
'iKeyboardShortcut' => __DIR__ . '/../..' . '/sources/application/UI/Hook/iKeyboardShortcut.php',
|
||||
'iLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php',
|
||||
'iLoginExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
'iLoginFSMExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
|
||||
|
||||
@@ -308,6 +308,72 @@ JS
|
||||
$oContentLayout->AddMainBlock($oNewsroomBlock);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// User defined keyboard shortcut
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Panel
|
||||
$oKeyboardShortcutBlock = new Panel(Dict::S('UI:Preferences:PersonalizeKeyboardShortcuts:Title'), array(), 'grey', 'ibo_keyboard_shortcuts');
|
||||
// Form
|
||||
$oKeyboardShortcutForm = new Form('ibo-form-for-user-interface-preferences');
|
||||
$oKeyboardShortcutForm->AddSubBlock(InputUIBlockFactory::MakeForHidden('operation', 'apply_keyboard_shortcuts'))
|
||||
->AddSubBlock($oAppContext->GetForFormBlock());
|
||||
|
||||
$oKeyboardShortcutBlock->AddSubBlock($oKeyboardShortcutForm);
|
||||
|
||||
$sKeyboardShortcutBlockId = $oKeyboardShortcutBlock->GetId();
|
||||
// JS keyboard listener
|
||||
$oP->add_script(
|
||||
<<<JS
|
||||
function recordSequence$sKeyboardShortcutBlockId(fCallback) {
|
||||
Mousetrap.record(function(sequence) {
|
||||
fCallback(sequence.join(' '));
|
||||
});
|
||||
}
|
||||
JS
|
||||
);
|
||||
// For each existing shortcut keyboard existing in iTop
|
||||
$aKeyboardShortcuts = utils::GetKeyboardShortcutPref();
|
||||
$sKeyboardShortcutsInputHint = Dict::S('UI:Preferences:PersonalizeKeyboardShortcuts:Input:Hint');
|
||||
$sKeyboardShortcutsButtonTooltip = Dict::S('UI:Preferences:PersonalizeKeyboardShortcuts:Button:Tooltip');
|
||||
foreach($aKeyboardShortcuts as $sKeyboardShortcutId => $aKeyboardShortcut){
|
||||
// Recording button
|
||||
$oButton = ButtonUIBlockFactory::MakeForAlternativeSecondaryAction('');
|
||||
$oButton->SetIconClass('fas fa-pen')->SetTooltip($sKeyboardShortcutsButtonTooltip)->SetOnClickJsCode(
|
||||
<<<JS
|
||||
let oPanel = $(this).siblings('input');
|
||||
var fCallback = function(sVal){
|
||||
oPanel.removeClass('ibo-is-focus').val(sVal);
|
||||
}
|
||||
oPanel.addClass('ibo-is-focus').val('$sKeyboardShortcutsInputHint')
|
||||
recordSequence$sKeyboardShortcutBlockId(fCallback);
|
||||
JS
|
||||
);
|
||||
|
||||
$oInput = InputUIBlockFactory::MakeForInputWithLabel(Dict::S($aKeyboardShortcut['label']), $sKeyboardShortcutId, $aKeyboardShortcut['key'], $sKeyboardShortcutId, 'text');
|
||||
$oInput->GetInput()->AddCSSClasses(['ibo-keyboard-shortcut--input']);
|
||||
$oKeyboardShortcutForm->AddSubBlock(new Html('<div class="ibo-keyboard-shortcut--shortcut">'));
|
||||
$oKeyboardShortcutForm->AddSubBlock($oInput);
|
||||
$oKeyboardShortcutForm->AddSubBlock($oButton);
|
||||
$oKeyboardShortcutForm->AddSubBlock(new Html('</div>'));
|
||||
}
|
||||
|
||||
// Prepare buttons
|
||||
$oKeyboardShortcutToolbar = ToolbarUIBlockFactory::MakeForButton(null, ['ibo-is-fullwidth']);
|
||||
$oKeyboardShortcutForm->AddSubBlock($oKeyboardShortcutToolbar);
|
||||
|
||||
// - Cancel button
|
||||
$oKeyboardShortcutCancelButton = ButtonUIBlockFactory::MakeForCancel();
|
||||
$oKeyboardShortcutToolbar->AddSubBlock($oKeyboardShortcutCancelButton);
|
||||
$oKeyboardShortcutCancelButton->SetOnClickJsCode("window.location.href = '$sURL'");
|
||||
// - Submit button
|
||||
$oKeyboardShortcutSubmitButton = ButtonUIBlockFactory::MakeForPrimaryAction(Dict::S('UI:Button:Apply'), 'operation', 'apply_keyboard_shortcuts', true);
|
||||
$oKeyboardShortcutToolbar->AddSubBlock($oKeyboardShortcutSubmitButton);
|
||||
|
||||
$oContentLayout->AddMainBlock($oKeyboardShortcutBlock);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// User picture placeholder
|
||||
@@ -642,7 +708,19 @@ try {
|
||||
$sURL = utils::GetAbsoluteUrlAppRoot().'pages/preferences.php?'.$oAppContext->GetForLink();
|
||||
$oPage->add_header('Location: '.$sURL);
|
||||
break;
|
||||
|
||||
case 'apply_keyboard_shortcuts':
|
||||
$aShortcutClasses = utils::GetClassesForInterface('iKeyboardShortcut','', array('/lib/', 'node_modules', 'test'));
|
||||
$aShortcutPrefs = [];
|
||||
foreach($aShortcutClasses as $cShortcutPlugin) {
|
||||
foreach ($cShortcutPlugin::GetShortcutKeys() as $aShortcutKey) {
|
||||
$sKey = utils::ReadParam($aShortcutKey['id'], $aShortcutKey['key'], true,'raw_data');
|
||||
$aShortcutPrefs[$aShortcutKey['id']] = strtolower($sKey);
|
||||
}
|
||||
}
|
||||
appUserPreferences::SetPref('keyboard_shortcuts', $aShortcutPrefs);
|
||||
|
||||
DisplayPreferences($oPage);
|
||||
break;
|
||||
case 'apply_newsroom_preferences':
|
||||
$iCountProviders = 0;
|
||||
$oUser = UserRights::GetUserObject();
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Combodo\iTop\Application\UI\Base\Component\GlobalSearch;
|
||||
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\UIBlock;
|
||||
use iKeyboardShortcut;
|
||||
use MetaModel;
|
||||
use utils;
|
||||
|
||||
@@ -32,7 +33,7 @@ use utils;
|
||||
* @internal
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class GlobalSearch extends UIBlock
|
||||
class GlobalSearch extends UIBlock implements iKeyboardShortcut
|
||||
{
|
||||
// Overloaded constants
|
||||
public const BLOCK_CODE = 'ibo-global-search';
|
||||
@@ -172,4 +173,14 @@ class GlobalSearch extends UIBlock
|
||||
{
|
||||
return $this->iMaxHistoryResults;
|
||||
}
|
||||
|
||||
public static function GetShortcutKeys(): array
|
||||
{
|
||||
return [['id' => 'ibo-open-global-search', 'label' => 'UI:Component:GlobalSearch:KeyboardShortcut:OpenDrawer', 'key' => 'g', 'event' => 'open_drawer']];
|
||||
}
|
||||
|
||||
public static function GetShortcutTriggeredElementSelector(): string
|
||||
{
|
||||
return "[data-role='".static::BLOCK_CODE."']";
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ namespace Combodo\iTop\Application\UI\Base\Component\QuickCreate;
|
||||
|
||||
|
||||
use Combodo\iTop\Application\UI\Base\UIBlock;
|
||||
use iKeyboardShortcut;
|
||||
use MetaModel;
|
||||
use UserRights;
|
||||
use utils;
|
||||
@@ -33,7 +34,7 @@ use utils;
|
||||
* @internal
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class QuickCreate extends UIBlock
|
||||
class QuickCreate extends UIBlock implements iKeyboardShortcut
|
||||
{
|
||||
// Overloaded constants
|
||||
public const BLOCK_CODE = 'ibo-quick-create';
|
||||
@@ -201,4 +202,14 @@ class QuickCreate extends UIBlock
|
||||
{
|
||||
return $this->iMaxHistoryResults;
|
||||
}
|
||||
|
||||
public static function GetShortcutKeys(): array
|
||||
{
|
||||
return [['id' => 'ibo-open-quick-create', 'label' => 'UI:Component:QuickCreate:KeyboardShortcut:OpenDrawer', 'key' => 'c', 'event' => 'open_drawer']];
|
||||
}
|
||||
|
||||
public static function GetShortcutTriggeredElementSelector(): string
|
||||
{
|
||||
return "[data-role='".static::BLOCK_CODE."']";
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
|
||||
use Combodo\iTop\Application\UI\Base\UIBlock;
|
||||
use DBObjectSearch;
|
||||
use Dict;
|
||||
use iKeyboardShortcut;
|
||||
use MetaModel;
|
||||
use UIExtKeyWidget;
|
||||
use UserRights;
|
||||
@@ -43,7 +44,7 @@ use utils;
|
||||
* @internal
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class NavigationMenu extends UIBlock
|
||||
class NavigationMenu extends UIBlock implements iKeyboardShortcut
|
||||
{
|
||||
// Overloaded constants
|
||||
public const BLOCK_CODE = 'ibo-navigation-menu';
|
||||
@@ -444,4 +445,14 @@ JS;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public static function GetShortcutKeys(): array
|
||||
{
|
||||
return [['id' => 'ibo-open-menu-filter', 'label' => 'UI:Layout:NavigationMenu:KeyboardShortcut:FocusFilter', 'key'=> 'alt+m', 'event' => 'filter_shortcut']];
|
||||
}
|
||||
|
||||
public static function GetShortcutTriggeredElementSelector(): string
|
||||
{
|
||||
return "[data-role='".static::BLOCK_CODE."']";
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,15 @@ use cmdbAbstractObject;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Panel\Panel;
|
||||
use Combodo\iTop\Application\UI\Helper\UIHelper;
|
||||
use DBObject;
|
||||
use iKeyboardShortcut;
|
||||
use MetaModel;
|
||||
|
||||
class ObjectDetails extends Panel
|
||||
class ObjectDetails extends Panel implements iKeyboardShortcut
|
||||
{
|
||||
// Overloaded constants
|
||||
public const BLOCK_CODE = 'ibo-object-details';
|
||||
public const DEFAULT_HTML_TEMPLATE_REL_PATH = 'base/layouts/object/object-details/layout';
|
||||
public const DEFAULT_JS_TEMPLATE_REL_PATH = 'base/layouts/object/object-details/layout';
|
||||
|
||||
/** @var string Class name of the object (eg. "UserRequest") */
|
||||
protected $sClassName;
|
||||
@@ -197,4 +199,17 @@ class ObjectDetails extends Panel
|
||||
$this->sStatusColor = UIHelper::GetColorFromStatus($this->sClassName, $this->sStatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static function GetShortcutKeys(): array
|
||||
{
|
||||
return [['id' => 'ibo-edit-object', 'label' => 'UI:Layout:ObjectDetails:KeyboardShortcut:EditObject', 'key' => 'e', 'event' => 'edit_object'],
|
||||
['id' => 'ibo-delete-object', 'label' => 'UI:Layout:ObjectDetails:KeyboardShortcut:DeleteObject', 'key' => 'd', 'event' => 'delete_object'],
|
||||
['id' => 'ibo-new-object', 'label' => 'UI:Layout:ObjectDetails:KeyboardShortcut:NewObject', 'key' => 'n', 'event' => 'new_object'],
|
||||
['id' => 'ibo-save-object', 'label' => 'UI:Layout:ObjectDetails:KeyboardShortcut:SaveObject', 'key' => 's', 'event' => 'save_object']];
|
||||
}
|
||||
|
||||
public static function GetShortcutTriggeredElementSelector(): string
|
||||
{
|
||||
return "[data-role='".static::BLOCK_CODE."']";
|
||||
}
|
||||
}
|
||||
19
sources/application/UI/Hook/iKeyboardShortcut.php
Normal file
19
sources/application/UI/Hook/iKeyboardShortcut.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
interface iKeyboardShortcut
|
||||
{
|
||||
/**
|
||||
* Return default keys combination to trigger shortcut element
|
||||
* @return array
|
||||
*/
|
||||
public static function GetShortcutKeys(): array;
|
||||
|
||||
/**
|
||||
* Element to be triggered when shortcut key combination is pressed
|
||||
* @return string
|
||||
*/
|
||||
public static function GetShortcutTriggeredElementSelector(): string;
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
|
||||
$this->SetTopBarLayout(TopBarFactory::MakeStandard($this->GetBreadCrumbsNewEntry()));
|
||||
|
||||
utils::InitArchiveMode();
|
||||
|
||||
|
||||
$this->m_aMessages = array();
|
||||
$this->SetRootUrl(utils::GetAbsoluteUrlAppRoot());
|
||||
$this->add_header("Content-type: text/html; charset=".self::PAGES_CHARSET);
|
||||
@@ -134,6 +134,9 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.magnific-popup.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/moment-with-locales.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/showdown.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/mousetrap/mousetrap.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/mousetrap/mousetrap-record.min.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/pages/backoffice/keyboard-shortcuts.js');
|
||||
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/pages/backoffice/toolbox.js');
|
||||
}
|
||||
|
||||
@@ -176,6 +179,16 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
|
||||
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/selectize.default.css');
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
protected function InitializeKeyboardShortcuts(): void
|
||||
{
|
||||
$aShortcuts = utils::GetKeyboardShortcutPref();
|
||||
$sShortcuts = json_encode($aShortcuts);
|
||||
$this->add_script("aKeyboardShortcuts = $sShortcuts;");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -861,6 +874,8 @@ HTML;
|
||||
// Components
|
||||
// Note: For now all components are either included in the layouts above or put in page through the AddUiBlock() API, so there is no need to do anything more.
|
||||
|
||||
$this->InitializeKeyboardShortcuts();
|
||||
|
||||
// Variable content of the page
|
||||
$aData['aPage'] = array_merge(
|
||||
$aData['aPage'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav id="{{ oUIBlock.GetId() }}" class="ibo-navigation-menu {% if oUIBlock.IsExpanded() == true %}ibo-is-expanded{% endif %}">
|
||||
<nav id="{{ oUIBlock.GetId() }}" class="ibo-navigation-menu {% if oUIBlock.IsExpanded() == true %}ibo-is-expanded{% endif %}" data-role="ibo-navigation-menu">
|
||||
<div class="ibo-navigation-menu--body">
|
||||
<div class="ibo-navigation-menu--top-part">
|
||||
<a class="ibo-navigation-menu--square-company-logo" title="{{ oUIBlock.GetAppRevisionNumber() }}" href="{{ oUIBlock.GetAppIconLink() }}">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
data-object-class="{{ oUIBlock.GetClassName() }}"
|
||||
data-object-id="{{ oUIBlock.GetObjectId() }}"
|
||||
data-object-mode="{{ oUIBlock.GetObjectMode() }}"
|
||||
data-role="ibo-object-details"
|
||||
{% endblock %}
|
||||
|
||||
{% block iboPanelSubTitle %}
|
||||
|
||||
24
templates/base/layouts/object/object-details/layout.js.twig
Normal file
24
templates/base/layouts/object/object-details/layout.js.twig
Normal file
@@ -0,0 +1,24 @@
|
||||
{# @copyright Copyright (C) 2010-2020 Combodo SARL #}
|
||||
{# @license http://opensource.org/licenses/AGPL-3.0 #}
|
||||
|
||||
$('#{{ oUIBlock.GetId() }}').on('edit_object', function(){
|
||||
$(this).find('button[name="UI:Menu:Modify"]').click();
|
||||
});
|
||||
|
||||
|
||||
$('#{{ oUIBlock.GetId() }}').on('delete_object', function(){
|
||||
$(this).find('button[name="UI:Menu:Delete"]').click();
|
||||
});
|
||||
|
||||
$('#{{ oUIBlock.GetId() }}').on('new_object', function(){
|
||||
$(this).find('button[name="UI:Menu:New"]').click();
|
||||
});
|
||||
{% if oUIBlock.GetObjectMode() == constant('cmdbAbstractObject::ENUM_OBJECT_MODE_EDIT') or oUIBlock.GetObjectMode() == constant('cmdbAbstractObject::ENUM_OBJECT_MODE_CREATE') %}
|
||||
$('#{{ oUIBlock.GetId() }}').on('save_object', function(){
|
||||
$(this).find('button[type="submit"][name=""][value=""]').click();
|
||||
});
|
||||
{% elseif oUIBlock.GetObjectMode() == constant('cmdbAbstractObject::ENUM_OBJECT_MODE_STIMULUS') %}
|
||||
$('#{{ oUIBlock.GetId() }}').on('save_object', function(){
|
||||
$(this).find('button[type="submit"][name="submit"][value="submit"]').click();
|
||||
});
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user