N°3524 Add keyboard shortcuts to global actions

This commit is contained in:
Stephen Abello
2021-03-17 10:56:18 +01:00
parent 8dd8f98b61
commit a8ede8b857
26 changed files with 345 additions and 33 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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',
));

View File

@@ -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',
));

View File

@@ -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'
));

View File

@@ -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',
]);

View File

@@ -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',

View File

@@ -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

View File

@@ -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);

View File

@@ -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
View 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
View 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);

View 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');
};
}
});
});

View File

@@ -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));

View File

@@ -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',

View File

@@ -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',

View File

@@ -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();

View File

@@ -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."']";
}
}

View File

@@ -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."']";
}
}

View File

@@ -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."']";
}
}

View File

@@ -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."']";
}
}

View 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;
}

View File

@@ -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'],

View File

@@ -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() }}">

View File

@@ -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 %}

View 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 %}