Fixed issue #656 - Shortcuts in different keyboard layout.

This commit is contained in:
Martin Edenhofer 2017-04-10 08:08:18 +02:00
parent b8ed5151fb
commit 5fd5f4318a
8 changed files with 279 additions and 105 deletions

View file

@ -13,6 +13,8 @@ class App.KeyboardShortcutModal extends App.ControllerModal
content: -> content: ->
App.view('keyboard_shortcuts')( App.view('keyboard_shortcuts')(
areas: App.Config.get('keyboard_shortcuts') areas: App.Config.get('keyboard_shortcuts')
magicKey: App.Browser.magicKey()
hotkeys: App.Browser.hotkeys().split('+').reverse()
) )
exists: => exists: =>
@ -36,7 +38,8 @@ class App.KeyboardShortcutWidget extends Spine.Module
) )
observerKeys: => observerKeys: =>
navigationHotkeys = 'alt+ctrl' navigationHotkeys = App.Browser.hotkeys()
areas = App.Config.get('keyboard_shortcuts') areas = App.Config.get('keyboard_shortcuts')
for area in areas for area in areas
for item in area.content for item in area.content
@ -401,25 +404,25 @@ App.Config.set(
shortcuts: [ shortcuts: [
{ {
key: 'u' key: 'u'
hotkeys: true magicKey: true
description: 'Format as _underlined_' description: 'Format as _underlined_'
globalEvent: 'richtext-underline' globalEvent: 'richtext-underline'
} }
{ {
key: 'b' key: 'b'
hotkeys: true magicKey: true
description: 'Format as |bold|' description: 'Format as |bold|'
globalEvent: 'richtext-bold' globalEvent: 'richtext-bold'
} }
{ {
key: 'i' key: 'i'
hotkeys: true magicKey: true
description: 'Format as ||italic||' description: 'Format as ||italic||'
globalEvent: 'richtext-italic' globalEvent: 'richtext-italic'
} }
{ {
key: 'v' key: 's'
hotkeys: true magicKey: true
description: 'Format as //strikethrough//' description: 'Format as //strikethrough//'
globalEvent: 'richtext-strikethrough' globalEvent: 'richtext-strikethrough'
} }

View file

@ -12,9 +12,17 @@ class Widget extends App.Controller
return if !@permissionCheck('admin.translation') return if !@permissionCheck('admin.translation')
# bind on key down # bind on key down
# if ctrl+alt+t is pressed, enable translation_inline and fire ui:rerender # if hotkeys+t is pressed, enable translation_inline and fire ui:rerender
browserHotkeys = App.Browser.hotkeys()
$(document).on('keydown.translation', (e) => $(document).on('keydown.translation', (e) =>
if e.altKey && e.ctrlKey && e.keyCode is 84 hotkeys = false
if browserHotkeys is 'ctrl+shift'
if !e.altKey && e.ctrlKey && !e.metaKey && e.shiftKey
hotkeys = true
else
if e.altKey && e.ctrlKey && !e.metaKey
hotkeys = true
if hotkeys && e.keyCode is 84
@toogle() @toogle()
) )

View file

@ -82,6 +82,20 @@ class App.Browser
localStorage.setItem('fingerprint', fingerprint) localStorage.setItem('fingerprint', fingerprint)
fingerprint fingerprint
@magicKey: ->
browser = @detection()
magicKey = 'ctrl'
if browser && browser.os && browser.os.name.toString().match(/mac/i)
magicKey = 'cmd'
magicKey
@hotkeys: ->
browser = @detection()
hotkeys = 'ctrl+shift'
if browser && browser.os && browser.os.name.toString().match(/mac/i)
hotkeys = 'alt+ctrl'
hotkeys
class Modal extends App.ControllerModal class Modal extends App.ControllerModal
buttonClose: false buttonClose: false
buttonCancel: false buttonCancel: false

View file

@ -39,6 +39,7 @@
66: true, // b 66: true, // b
73: true, // i 73: true, // i
85: true, // u 85: true, // u
83: true, // s
}, },
//maxlength: 20, //maxlength: 20,
}; };
@ -59,11 +60,9 @@
this.preventInput = false this.preventInput = false
// detect firefox / handle contenteditable issues // handle contenteditable issues
this.browser = undefined this.browserMagicKey = App.Browser.magicKey()
if ( navigator && navigator.userAgent && navigator.userAgent.search('Firefox') != -1) { this.browserHotkeys = App.Browser.hotkeys()
this.browser = 'ff'
}
this.init(); this.init();
} }
@ -112,28 +111,76 @@
} }
} }
// on zammad altKey + ctrlKey + i/b/u // on zammad magicKey + i/b/u/s
// altKey + ctrlKey + u -> Toggles the current selection between underlined and not underlined // hotkeys + u -> Toggles the current selection between underlined and not underlined
// altKey + ctrlKey + b -> Toggles the current selection between bold and non-bold // hotkeys + b -> Toggles the current selection between bold and non-bold
// altKey + ctrlKey + i -> Toggles the current selection between italic and non-italic // hotkeys + i -> Toggles the current selection between italic and non-italic
// altKey + ctrlKey + v -> Toggles the current selection between strike and non-strike // hotkeys + v -> Toggles the current selection between strike and non-strike
// altKey + ctrlKey + f -> Removes the formatting tags from the current selection // hotkeys + f -> Removes the formatting tags from the current selection
// altKey + ctrlKey + y -> Removes the formatting from while textarea // hotkeys + y -> Removes the formatting from while textarea
// altKey + ctrlKey + z -> Inserts a Horizontal Rule // hotkeys + z -> Inserts a Horizontal Rule
// altKey + ctrlKey + l -> Toggles the text selection between an unordered list and a normal block // hotkeys + l -> Toggles the text selection between an unordered list and a normal block
// altKey + ctrlKey + k -> Toggles the text selection between an ordered list and a normal block // hotkeys + k -> Toggles the text selection between an ordered list and a normal block
// altKey + ctrlKey + o -> Draws a line through the middle of the current selection // hotkeys + o -> Draws a line through the middle of the current selection
// altKey + ctrlKey + w -> Removes any hyperlink from the current selection // hotkeys + w -> Removes any hyperlink from the current selection
if ( e.altKey && e.ctrlKey && !e.metaKey && (_this.options.richTextFormatKey[ e.keyCode ] var richtTextControl = false
if (_this.browserMagicKey == 'cmd') {
if (!e.altKey && !e.ctrlKey && e.metaKey) {
richtTextControl = true
}
}
else {
if (!e.altKey && e.ctrlKey && !e.metaKey) {
richtTextControl = true
}
}
if (richtTextControl && _this.options.richTextFormatKey[ e.keyCode ]) {
e.preventDefault()
if (e.keyCode == 66) {
document.execCommand('bold')
return true
}
if (e.keyCode == 73) {
document.execCommand('italic')
return true
}
if (e.keyCode == 85) {
document.execCommand('underline')
return true
}
if (e.keyCode == 83) {
document.execCommand('strikeThrough')
return true
}
}
var hotkeys = false
if (_this.browserHotkeys == 'ctrl+shift') {
if (!e.altKey && e.ctrlKey && !e.metaKey && e.shiftKey) {
hotkeys = true
}
}
else {
if (e.altKey && e.ctrlKey && !e.metaKey) {
hotkeys = true
}
}
if (hotkeys && (_this.options.richTextFormatKey[ e.keyCode ]
|| e.keyCode == 49 || e.keyCode == 49
|| e.keyCode == 50 || e.keyCode == 50
|| e.keyCode == 51 || e.keyCode == 51
|| e.keyCode == 66
|| e.keyCode == 70 || e.keyCode == 70
|| e.keyCode == 90 || e.keyCode == 90
|| e.keyCode == 76 || e.keyCode == 70
|| e.keyCode == 73
|| e.keyCode == 75 || e.keyCode == 75
|| e.keyCode == 76
|| e.keyCode == 85
|| e.keyCode == 86 || e.keyCode == 86
|| e.keyCode == 87 || e.keyCode == 87
|| e.keyCode == 90
|| e.keyCode == 89)) { || e.keyCode == 89)) {
e.preventDefault() e.preventDefault()
@ -142,37 +189,6 @@
return return
} }
if (e.keyCode == 66) {
document.execCommand('bold')
}
if (e.keyCode == 73) {
document.execCommand('italic')
}
if (e.keyCode == 85) {
document.execCommand('underline')
}
if (e.keyCode == 70) {
document.execCommand('removeFormat')
}
if (e.keyCode == 89) {
var cleanHtml = App.Utils.htmlRemoveRichtext(_this.$element.html())
_this.$element.html(cleanHtml)
}
if (e.keyCode == 90) {
document.execCommand('insertHorizontalRule')
}
if (e.keyCode == 76) {
document.execCommand('insertUnorderedList')
}
if (e.keyCode == 75) {
document.execCommand('insertOrderedList')
}
if (e.keyCode == 86) {
document.execCommand('strikeThrough')
}
if (e.keyCode == 87) {
document.execCommand('unlink')
}
if (e.keyCode == 49) { if (e.keyCode == 49) {
_this.toggleBlock('h1') _this.toggleBlock('h1')
} }
@ -182,6 +198,37 @@
if (e.keyCode == 51) { if (e.keyCode == 51) {
_this.toggleBlock('h3') _this.toggleBlock('h3')
} }
if (e.keyCode == 66) {
document.execCommand('bold')
}
if (e.keyCode == 70) {
document.execCommand('removeFormat')
}
if (e.keyCode == 73) {
document.execCommand('italic')
}
if (e.keyCode == 75) {
document.execCommand('insertOrderedList')
}
if (e.keyCode == 76) {
document.execCommand('insertUnorderedList')
}
if (e.keyCode == 85) {
document.execCommand('underline')
}
if (e.keyCode == 86) {
document.execCommand('strikeThrough')
}
if (e.keyCode == 87) {
document.execCommand('unlink')
}
if (e.keyCode == 89) {
var cleanHtml = App.Utils.htmlRemoveRichtext(_this.$element.html())
_this.$element.html(cleanHtml)
}
if (e.keyCode == 90) {
document.execCommand('insertHorizontalRule')
}
_this.log('content editable richtext key', e.keyCode) _this.log('content editable richtext key', e.keyCode)
return true return true
} }
@ -391,22 +438,6 @@
return false return false
} }
// check if rich text key is pressed
Plugin.prototype.richTextKey = function(e) {
// e.altKey
// e.ctrlKey
// e.metaKey
// on mac block e.metaKey + i/b/u
if ( !e.altKey && e.metaKey && this.options.richTextFormatKey[ e.keyCode ] ) {
return true
}
// on win block e.ctrlKey + i/b/u
if ( !e.altKey && e.ctrlKey && this.options.richTextFormatKey[ e.keyCode ] ) {
return true
}
return false
}
// max length check // max length check
Plugin.prototype.maxLengthOk = function(typeAhead) { Plugin.prototype.maxLengthOk = function(typeAhead) {
if ( !this.options.maxlength ) { if ( !this.options.maxlength ) {

View file

@ -7,7 +7,7 @@
<% if item.where: %><p><i><%- @T(item.where) %></i></p><% end %> <% if item.where: %><p><i><%- @T(item.where) %></i></p><% end %>
<% for shortcut in item.shortcuts: %> <% for shortcut in item.shortcuts: %>
<% if shortcut.hotkeys: %> <% if shortcut.hotkeys: %>
<kbd>ctrl</kbd> <kbd>alt</kbd> <kbd><%- @hotkeys.join('</kbd> <kbd>') %></kbd>
<% end %> <% end %>
<% if shortcut.keyPrefix: %> <% if shortcut.keyPrefix: %>
<%= shortcut.keyPrefix %> <%= shortcut.keyPrefix %>
@ -33,7 +33,9 @@
<% if item.where: %><p><i><%- @T(item.where) %></i></p><% end %> <% if item.where: %><p><i><%- @T(item.where) %></i></p><% end %>
<% for shortcut in item.shortcuts: %> <% for shortcut in item.shortcuts: %>
<% if shortcut.hotkeys: %> <% if shortcut.hotkeys: %>
<kbd>ctrl</kbd> <kbd>alt</kbd> <kbd><%- @hotkeys.join('</kbd> <kbd>') %></kbd>
<% else if shortcut.magicKey: %>
<kbd><%= @magicKey %></kbd>
<% end %> <% end %>
<% if shortcut.keyPrefix: %> <% if shortcut.keyPrefix: %>
<%= shortcut.keyPrefix %> <%= shortcut.keyPrefix %>

View file

@ -2,7 +2,7 @@
require 'browser_test_helper' require 'browser_test_helper'
class AdminRoleTest < TestCase class AdminRoleTest < TestCase
def test_role def test_role_device
name = "some role #{rand(99_999_999)}" name = "some role #{rand(99_999_999)}"
@browser = browser_instance @browser = browser_instance
@ -125,4 +125,106 @@ class AdminRoleTest < TestCase
) )
end end
def test_role_admin_user
@browser = browser_instance
login(
username: 'agent1@example.com',
password: 'test',
url: browser_url,
)
# check if admin exists
exists_not(css: '[href="#manage"]')
logout()
# add admin.user to agent role
login(
username: 'master@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
role_edit(
data: {
name: 'Agent',
active: true,
permission: {
'admin.user' => true,
'chat.agent' => true,
'cti.agent' => true,
'ticket.agent' => true,
'user_preferences' => true,
},
}
)
logout()
# check if admin exists
login(
username: 'agent1@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
# create user
random = rand(999_999_999)
user_email = "admin.user.#{rand}@example.com"
user_create(
data: {
#login: "some login #{random}",
firstname: "Admin.User Firstname #{random}",
lastname: "Admin.User Lastname #{random}",
email: user_email,
password: 'some-pass',
},
)
# create ticket for user
ticket_create(
data: {
customer: user_email,
group: 'Users',
title: 'some changes',
body: 'some body 123äöü - admin.user',
},
)
# revoke admin.user
logout()
login(
username: 'master@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
role_edit(
data: {
name: 'Agent',
active: true,
permission: {
'admin.user' => false,
'chat.agent' => true,
'cti.agent' => true,
'ticket.agent' => true,
'user_preferences' => true,
},
}
)
logout()
login(
username: 'agent1@example.com',
password: 'test',
url: browser_url,
)
# check if admin exists
exists_not(css: '[href="#manage"]')
end
end end

View file

@ -109,9 +109,9 @@ class TranslationTest < TestCase
) )
@browser.action.key_down(:control) @browser.action.key_down(:control)
.key_down(:alt) .key_down(:shift)
.send_keys('t') .send_keys('t')
.key_up(:alt) .key_up(:shift)
.key_up(:control) .key_up(:control)
.perform .perform
@ -128,9 +128,9 @@ class TranslationTest < TestCase
sleep 5 sleep 5
@browser.action.key_down(:control) @browser.action.key_down(:control)
.key_down(:alt) .key_down(:shift)
.send_keys('t') .send_keys('t')
.key_up(:alt) .key_up(:shift)
.key_up(:control) .key_up(:control)
.perform .perform
@ -194,9 +194,9 @@ class TranslationTest < TestCase
sleep 5 sleep 5
@browser.action.key_down(:control) @browser.action.key_down(:control)
.key_down(:alt) .key_down(:shift)
.send_keys('t') .send_keys('t')
.key_up(:alt) .key_up(:shift)
.key_up(:control) .key_up(:control)
.perform .perform
@ -213,9 +213,9 @@ class TranslationTest < TestCase
sleep 5 sleep 5
@browser.action.key_down(:control) @browser.action.key_down(:control)
.key_down(:alt) .key_down(:shift)
.send_keys('t') .send_keys('t')
.key_up(:alt) .key_up(:shift)
.key_up(:control) .key_up(:control)
.perform .perform

View file

@ -1364,9 +1364,9 @@ wait untill text in selector disabppears
instance = params[:browser] || @browser instance = params[:browser] || @browser
screenshot(browser: instance, comment: 'shortcut_before') screenshot(browser: instance, comment: 'shortcut_before')
instance.action.key_down(:control) instance.action.key_down(:control)
.key_down(:alt) .key_down(:shift)
.send_keys(params[:key]) .send_keys(params[:key])
.key_up(:alt) .key_up(:shift)
.key_up(:control) .key_up(:control)
.perform .perform
screenshot(browser: instance, comment: 'shortcut_after') screenshot(browser: instance, comment: 'shortcut_after')
@ -2944,10 +2944,10 @@ wait untill text in selector disabppears
data: { data: {
name: 'some role' + random, name: 'some role' + random,
default_at_signup: false, default_at_signup: false,
permission: [ permission: {
'admin.group', 'admin.group' => true,
'preferences.password', 'preferences.password' => true,
], },
member: [ member: [
'some_user_login', 'some_user_login',
], ],
@ -2994,11 +2994,18 @@ wait untill text in selector disabppears
end end
if data.key?(:permission) if data.key?(:permission)
data[:permission].each { |permission_name| data[:permission].each { |permission_name, permission_value|
check( if permission_value == false
browser: instance, uncheck(
css: ".modal [data-permission-name=\"#{permission_name}\"]", browser: instance,
) css: ".modal [data-permission-name=\"#{permission_name}\"]",
)
else
check(
browser: instance,
css: ".modal [data-permission-name=\"#{permission_name}\"]",
)
end
} }
end end
@ -3046,10 +3053,10 @@ wait untill text in selector disabppears
data: { data: {
name: 'some role' + random, name: 'some role' + random,
default_at_signup: false, default_at_signup: false,
permission: [ permission: {
'admin.group', 'admin.group' => true,
'preferences.password', 'preferences.password' => true,
], },
member: [ member: [
'some_user_login', 'some_user_login',
], ],
@ -3093,11 +3100,18 @@ wait untill text in selector disabppears
end end
if data.key?(:permission) if data.key?(:permission)
data[:permission].each { |permission_name| data[:permission].each { |permission_name, permission_value|
check( if permission_value == false
browser: instance, uncheck(
css: ".modal [data-permission-name=\"#{permission_name}\"]", browser: instance,
) css: ".modal [data-permission-name=\"#{permission_name}\"]",
)
else
check(
browser: instance,
css: ".modal [data-permission-name=\"#{permission_name}\"]",
)
end
} }
end end