Added keyboard shortcut support.

This commit is contained in:
Martin Edenhofer 2016-02-25 06:44:17 +01:00
parent 3d372543b2
commit ffe011553d
25 changed files with 784 additions and 119 deletions

View file

@ -128,6 +128,11 @@ Source: https://jqueryui.com
Copyright: 2014 jQuery Foundation Copyright: 2014 jQuery Foundation
License: MIT license License: MIT license
----------------------------------------------------------------------------- -----------------------------------------------------------------------------
jquery.hotkeys.js
Source: https://github.com/jeresig/jquery.hotkeys
Copyright 2010, John Resig
License: Dual licensed under the MIT or GPL Version 2 licenses.
-----------------------------------------------------------------------------
underscore.js underscore.js
Source: http://underscorejs.org Source: http://underscorejs.org
Copyright: 2009-2015 Jeremy Ashkenas, DocumentCloud Copyright: 2009-2015 Jeremy Ashkenas, DocumentCloud

View file

@ -458,6 +458,9 @@ class App.Controller extends Spine.Controller
if @userTicketPopupsList if @userTicketPopupsList
@userTicketPopupsList.popover('destroy') @userTicketPopupsList.popover('destroy')
anyPopoversDestroy: ->
$('.popover').remove()
recentView: (object, o_id) => recentView: (object, o_id) =>
params = params =
object: object object: object

View file

@ -351,6 +351,9 @@ class App.ControllerNavSidbar extends App.ControllerContent
constructor: (params) -> constructor: (params) ->
super super
if @authenticateRequired
return if !@authenticate()
@params = params @params = params
# get accessable groups # get accessable groups

View file

@ -0,0 +1,30 @@
class Index extends App.ControllerModal
large: true
head: 'Keyboard Shortcuts'
buttonClose: true
buttonCancel: false
buttonSubmit: false
constructor: (params = {}) ->
delete params.el # do attache to body
super(params)
return if !@authenticate()
content: ->
App.view('keyboard_shortcuts')(
areas: App.Config.get('keyboard_shortcuts')
)
onClosed: ->
window.history.go(-1)
onSubmit: ->
window.history.go(-1)
onCancel: ->
window.history.go(-1)
App.Config.set('keyboard_shortcuts', Index, 'Routes')
App.Config.set('KeyboardShortcuts', { prio: 1700, parent: '#current_user', name: 'Keyboard Shortcuts', translate: true, target: '#keyboard_shortcuts', role: [ 'Admin', 'Agent' ] }, 'NavBarRight')

View file

@ -2223,4 +2223,4 @@ class ChatToTicketRef extends App.ControllerContent
App.Config.set( 'layout_ref/chat_to_ticket', ChatToTicketRef, 'Routes' ) App.Config.set( 'layout_ref/chat_to_ticket', ChatToTicketRef, 'Routes' )
App.Config.set( 'LayoutRef', { prio: 1700, parent: '#current_user', name: 'Layout Reference', translate: true, target: '#layout_ref', role: [ 'Admin' ] }, 'NavBarRight' ) App.Config.set( 'LayoutRef', { prio: 1600, parent: '#current_user', name: 'Layout Reference', translate: true, target: '#layout_ref', role: [ 'Admin' ] }, 'NavBarRight' )

View file

@ -1,4 +1,5 @@
class IndexRouter extends App.ControllerNavSidbar class IndexRouter extends App.ControllerNavSidbar
authenticateRequired: true
configKey: 'NavBarAdmin' configKey: 'NavBarAdmin'
App.Config.set('manage', IndexRouter, 'Routes') App.Config.set('manage', IndexRouter, 'Routes')

View file

@ -179,11 +179,98 @@ class App.Navigation extends App.ControllerWidgetPermanent
# renderPersonal # renderPersonal
@renderPersonal() @renderPersonal()
searchFunction = => # observer search box
@$('#global-search').bind('focusout', (e) =>
# delay to be able to click x
update = =>
@$('.search').removeClass('focused')
@delay(update, 100, 'removeFocused')
)
@$('#global-search').bind('focusin', (e) =>
@query = '' # reset query cache
@$('.search').addClass('focused')
@anyPopoversDestroy()
@searchFunction(0)
)
@$('form.search').on('submit', (e) ->
e.preventDefault()
)
@$('#global-search').on('keydown', @navigate)
# bind to empty search
@$('.empty-search').on('click', =>
@emptyAndClose()
)
new App.OnlineNotificationWidget(
el: @el
)
navigate: (e) =>
if e.keyCode is 27 # close on esc
@emptyAndClose()
return
else if e.keyCode is 38 # up
@nudge(e, -1)
return
else if e.keyCode is 40 # down
@nudge(e, 1)
return
else if e.keyCode is 13 # enter
href = @$('#global-search-result .nav-tab.is-active').attr('href')
@locationExecute(href)
@emptyAndClose()
return
# on other keys, show result
@searchFunction(200)
nudge: (e, position) =>
# get current
navigationResult = @$('#global-search-result')
current = navigationResult.find('.nav-tab.is-active')
if !current.get(0)
navigationResult.find('.nav-tab').first().addClass('is-active')
return
if position is 1
next = current.closest('li').nextAll('li').not('.divider').first().find('.nav-tab')
if next.get(0)
current.removeClass('is-active').popover('hide')
next.addClass('is-active').popover('show')
else
prev = current.closest('li').prevAll('li').not('.divider').first().find('.nav-tab')
if prev.get(0)
current.removeClass('is-active').popover('hide')
prev.addClass('is-active').popover('show')
emptyAndClose: =>
@$('#global-search').val('').blur()
@$('.search').removeClass('filled').removeClass('open')
# remove not needed popovers
@delay(@anyPopoversDestroy, 100, 'removePopovers')
andClose: =>
@$('#global-search').blur()
@$('.search').removeClass('open')
@delay(@anyPopoversDestroy, 100, 'removePopovers')
searchFunction: (delay) =>
search = =>
query = @$('#global-search').val().trim()
return if !query
return if query is @query
@query = query
@$('.search').toggleClass('filled', !!@query)
# use cache for search result # use cache for search result
if @searchResultCache[@query] if @searchResultCache[@query]
@renderResult( @searchResultCache[@query] ) @renderResult(@searchResultCache[@query].result)
currentTime = new Date
return if @searchResultCache[@query].time > currentTime.setSeconds(currentTime.getSeconds() - 20)
App.Ajax.request( App.Ajax.request(
id: 'search' id: 'search'
@ -193,13 +280,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
query: @query query: @query
processData: true, processData: true,
success: (data, status, xhr) => success: (data, status, xhr) =>
# load assets
App.Collection.loadAssets(data.assets) App.Collection.loadAssets(data.assets)
# cache search result
@searchResultCache[@query] = data.result
result = {} result = {}
for item in data.result for item in data.result
if App[item.type] && App[item.type].find if App[item.type] && App[item.type].find
@ -214,84 +295,26 @@ class App.Navigation extends App.ControllerWidgetPermanent
else else
@log 'error', "No such model App.#{item.type}" @log 'error', "No such model App.#{item.type}"
diff = false
if @searchResultCache[@query]
diff = difference(@searchResultCache[@query].resultRaw, data.result)
# cache search result
@searchResultCache[@query] =
result: result
resultRaw: data.result
time: new Date
# if result hasn't changed, do not rerender
return if diff isnt false && _.isEmpty(diff)
@renderResult(result) @renderResult(result)
@$('#global-search-result').on('click', 'a', -> @$('#global-search-result').on('click', 'a', =>
close() @andClose()
) )
) )
@delay(search, 200, 'search')
removePopovers = ->
$('.popover').remove()
close = =>
@$('#global-search').blur()
@$('.search').removeClass('open')
# remove not needed popovers
@delay( removePopovers, 280, 'removePopovers' )
emptyAndClose = =>
@$('#global-search').val('').blur()
@$('.search').removeClass('filled').removeClass('open')
# remove not needed popovers
@delay( removePopovers, 280, 'removePopovers' )
# observer search box
@$('#global-search').bind( 'focusout', (e) =>
# delay to be able to click x
update = =>
@$('.search').removeClass('focused')
@delay( update, 180, 'removeFocused' )
)
@$('#global-search').bind( 'focusin', (e) =>
@$('.search').addClass('focused')
# remove not needed popovers
removePopovers()
# check if search is needed
query = @$('#global-search').val().trim()
return if !query
@query = query
@delay( searchFunction, 220, 'search' )
)
# prevent submit of search box
@$('form.search').on( 'submit', (e) ->
e.preventDefault()
)
# start search
@$('#global-search').on( 'keyup', (e) =>
# close on esc
if e.which == 27
emptyAndClose()
return
# on other keys, show result
query = @$('#global-search').val().trim()
return if !query
return if query is @query
@query = query
@$('.search').toggleClass('filled', !!@query)
@delay( searchFunction, 200, 'search' )
)
# bind to empty search
@$('.empty-search').on(
'click'
->
emptyAndClose()
)
new App.OnlineNotificationWidget(
el: @el
)
getItems: (data) -> getItems: (data) ->
navbar = _.values(data.navbar) navbar = _.values(data.navbar)

View file

@ -1,4 +1,5 @@
class Index extends App.ControllerNavSidbar class Index extends App.ControllerNavSidbar
authenticateRequired: true
configKey: 'NavBarProfile' configKey: 'NavBarProfile'
App.Config.set( 'profile', Index, 'Routes' ) App.Config.set( 'profile', Index, 'Routes' )

View file

@ -133,6 +133,9 @@ class App.TicketZoomArticleNew extends App.Controller
# preselect article type # preselect article type
@setArticleType(data.type.name) @setArticleType(data.type.name)
# set focus into field
@textarea.focus()
) )
# reset new article screen # reset new article screen

View file

@ -85,7 +85,7 @@ class Index extends App.ControllerContent
release: => release: =>
rerender = -> rerender = ->
App.Event.trigger('ui:rerender') App.Event.trigger('ui:rerender')
if @translationList.changes() if @translationList && @translationList.changes()
App.Delay.set(rerender, 400) App.Delay.set(rerender, 400)
showAction: => showAction: =>

View file

@ -1,10 +0,0 @@
class FFlt35
constructor: ->
data = App.Browser.detection()
if data.browser.name is 'Firefox' && data.browser.major && data.browser.major < 35
# for firefox lower 35 we need to set a class to hide own dropdown images
# whole file can be removed after dropping firefox 34 and lower support
$('html').addClass('ff-lt-35')
App.Config.set( 'aaa_ff-lt-35', FFlt35, 'Widgets' )

View file

@ -0,0 +1,293 @@
class App.KeyboardShortcutWidget extends Spine.Module
@include App.LogInclude
constructor: ->
@observerKeys()
observerKeys: =>
#jQuery.hotkeys.options.filterInputAcceptingElements = false
navigationHotkeys = 'alt+ctrl'
areas = App.Config.get('keyboard_shortcuts')
for area in areas
for item in area.content
for shortcut in item.shortcuts
modifier = ''
if shortcut.hotkeys
modifier += navigationHotkeys
if shortcut.key
if modifier isnt ''
modifier += '+'
modifier += shortcut.key
if shortcut.callback
@log 'debug', 'bind for', modifier
$(document).bind('keydown', modifier, shortcut.callback)
App.Config.set('keyboard_shortcuts', App.KeyboardShortcutWidget, 'Widgets')
App.Config.set(
'keyboard_shortcuts',
[
{
headline: 'Navigation'
location: 'left'
content: [
{
where: 'Used anywhere'
shortcuts: [
{
key: 'd'
hotkeys: true
description: 'Dashboard'
callback: ->
window.location.hash = '#dashboard'
}
{
key: 'o'
hotkeys: true
description: 'Overviews'
callback: ->
window.location.hash = '#ticket/view'
}
{
key: 's'
hotkeys: true
description: 'Search'
callback: ->
$('#global-search').focus()
}
{
key: 'n'
hotkeys: true
description: 'New Ticket'
callback: ->
window.location.hash = '#ticket/create'
}
{
key: 'e'
hotkeys: true
description: 'Logout'
callback: ->
window.location.hash = '#logout'
}
{
key: 'h'
hotkeys: true
description: 'List of shortcuts'
callback: ->
window.location.hash = '#keyboard_shortcuts'
}
{
key: 'x'
hotkeys: true
description: 'Close current tab'
callback: ->
$('#navigation .tasks .is-active .js-close').click()
}
{
key: 'tab'
hotkeys: true
description: 'Next in tab'
callback: ->
if $('#navigation .tasks .is-active').get(0)
if $('#navigation .tasks .is-active').next().get(0)
$('#navigation .tasks .is-active').next().find('div').first().click()
return
$('#navigation .tasks .task').first().find('div').first().click()
}
{
key: 'shift+tab'
hotkeys: true
description: 'Previous tab'
callback: ->
if $('#navigation .tasks .is-active').get(0)
if $('#navigation .tasks .is-active').prev().get(0)
$('#navigation .tasks .is-active').prev().find('div').first().click()
return
$('#navigation .tasks .task').last().find('div').first().click()
}
{
key: 'return'
hotkeys: true
description: 'Confirm/submit dialog'
callback: ->
# check of primary modal exists
dialog = $('body > div.modal')
if dialog.get(0)
dialog.find('.js-submit').click()
return
# check of local modal exists
dialog = $('.active.content > div.modal')
if dialog.get(0)
dialog.find('.js-submit').click()
return
# check ticket edit
dialog = $('.active.content .js-attributeBar .js-submit')
if dialog.get(0)
dialog.first().click()
return
dialog = $('.active.content .js-submit')
if dialog.get(0)
dialog.first().click()
return
}
]
}
{
where: 'Used in lists (views and results)'
shortcuts: [
{
key: ['', '']
description: 'Move up and down'
}
{
key: ['', '']
description: 'Move left and right'
}
{
key: 'enter'
description: 'Select item',
}
]
}
]
}
{
headline: 'Translations'
location: 'left'
content: [
{
where: 'Used anywhere (admin only)'
shortcuts: [
{
admin: true
key: 't'
hotkeys: true
description: 'Enable/disable inline translations'
}
]
}
]
}
{
headline: 'Tickets'
location: 'right'
content: [
{
where: 'Used when viewing a Ticket'
shortcuts: [
{
key: 'm'
hotkeys: true
description: 'Open note box'
callback: ->
$('.active.content .article-new .articleNewEdit-body').first().focus()
}
{
key: 'r'
hotkeys: true
description: 'Reply to last article'
callback: ->
lastArticleWithReply = $('.active.content .ticket-article .icon-reply').last()
lastArticleWithReplyAll = lastArticleWithReply.parent().find('.icon-reply-all')
if lastArticleWithReplyAll.get(0)
lastArticleWithReplyAll.click()
return
lastArticleWithReply.click()
}
#{
# key: 'm'
# hotkeys: true
# description: 'Open macro selection'
# callback: ->
# window.location.hash = '#ticket/create'
#}
{
key: 'c'
hotkeys: true
description: 'Update as closed'
callback: ->
return if !$('.active.content .edit').get(0)
$('.active.content .edit [name="state_id"]').val(4)
$('.active.content .js-attributeBar .js-submit').first().click()
}
]
}
]
}
{
headline: 'Text editing'
location: 'right'
content: [
{
where: 'Used when composing a text'
shortcuts: [
{
key: 'u'
hotkeys: true
description: 'Format as _underlined_'
}
{
key: 'b'
hotkeys: true
description: 'Format as |bold|'
}
{
key: 'i'
hotkeys: true
description: 'Format as ||italic||'
}
{
key: 't'
hotkeys: true
description: 'Format as //strikethrough//'
}
{
key: 'f'
hotkeys: true
description: 'Removes the formatting'
}
{
key: 'z'
hotkeys: true,
description: 'Inserts a horizontal rule'
}
{
key: 'l'
hotkeys: true,
description: 'Format as unordered list'
}
{
key: 'k'
hotkeys: true,
description: 'Format as ordered list'
}
{
key: '1'
hotkeys: true,
description: 'Format as h1 heading'
}
{
key: '2'
hotkeys: true,
description: 'Format as h2 heading'
}
{
key: '3'
hotkeys: true,
description: 'Format as h3 heading'
}
{
key: 'w'
hotkeys: true,
description: 'Removes any hyperlink'
}
]
}
]
}
]
)

View file

@ -38,7 +38,7 @@ class App.Browser
# define min. required browser version # define min. required browser version
map = map =
Chrome: 37 Chrome: 37
Firefox: 31 Firefox: 36
Explorer: 10 Explorer: 10
Safari: 6 Safari: 6
Opera: 22 Opera: 22

View file

@ -224,6 +224,7 @@ class _i18nSingleton extends Spine.Module
.replace(/\|\|(.+?)\|\|/gm, '<i>$1</i>') .replace(/\|\|(.+?)\|\|/gm, '<i>$1</i>')
.replace(/\|(.+?)\|/gm, '<b>$1</b>') .replace(/\|(.+?)\|/gm, '<b>$1</b>')
.replace(/_(.+?)_/gm, '<u>$1</u>') .replace(/_(.+?)_/gm, '<u>$1</u>')
.replace(/\/\/(.+?)\/\//gm, '<del>$1</del>')
.replace(/§(.+?)§/gm, '<kbd>$1</kbd>') .replace(/§(.+?)§/gm, '<kbd>$1</kbd>')
# search %s # search %s

View file

@ -6,8 +6,7 @@ class App.Run extends App.Controller
App.Event.trigger('app:init') App.Event.trigger('app:init')
# browser check # browser check
if !App.Browser.check() return if !App.Browser.check()
return
# hide splash screen # hide splash screen
$('.splash').hide() $('.splash').hide()

View file

@ -51,7 +51,6 @@
this._defaults = defaults; this._defaults = defaults;
this._name = pluginName; this._name = pluginName;
this._setTimeOutReformat = false;
// take placeholder from markup // take placeholder from markup
if ( !this.options.placeholder && this.$element.data('placeholder') ) { if ( !this.options.placeholder && this.$element.data('placeholder') ) {
@ -67,6 +66,19 @@
Plugin.prototype.init = function () { Plugin.prototype.init = function () {
var _this = this var _this = this
this.toggleBlock = function(tag) {
sel = window.getSelection()
node = $(sel.anchorNode)
console.log('toggleBlock', tag, node.parent(), node.is())
if (node.is(tag) || node.parent().is(tag) || node.parent().parent().is(tag)) {
document.execCommand('formatBlock', false, 'div')
//document.execCommand('RemoveFormat')
}
else {
document.execCommand('formatBlock', false, tag)
}
}
// handle enter // handle enter
this.$element.on('keydown', function (e) { this.$element.on('keydown', function (e) {
_this.log('keydown', e.keyCode) _this.log('keydown', e.keyCode)
@ -94,19 +106,22 @@
} }
} }
// on zammad metaKey + ctrlKey + i/b/u // on zammad altKey + ctrlKey + i/b/u
// metaKey + ctrlKey + u -> Toggles the current selection between underlined and not underlined // altKey + ctrlKey + u -> Toggles the current selection between underlined and not underlined
// metaKey + ctrlKey + b -> Toggles the current selection between bold and non-bold // altKey + ctrlKey + b -> Toggles the current selection between bold and non-bold
// metaKey + ctrlKey + i -> Toggles the current selection between italic and non-italic // altKey + ctrlKey + i -> Toggles the current selection between italic and non-italic
// metaKey + ctrlKey + r -> Removes the formatting tags from the current selection // altKey + ctrlKey + f -> Removes the formatting tags from the current selection
// metaKey + ctrlKey + h -> Inserts a Horizontal Rule // altKey + ctrlKey + z -> Inserts a Horizontal Rule
// metaKey + ctrlKey + l -> Toggles the text selection between an unordered list and a normal block // altKey + ctrlKey + l -> Toggles the text selection between an unordered list and a normal block
// metaKey + ctrlKey + k -> Toggles the text selection between an ordered list and a normal block // altKey + ctrlKey + k -> Toggles the text selection between an ordered list and a normal block
// metaKey + ctrlKey + o -> Draws a line through the middle of the current selection // altKey + ctrlKey + o -> Draws a line through the middle of the current selection
// metaKey + ctrlKey + w -> Removes any hyperlink from the current selection // altKey + ctrlKey + w -> Removes any hyperlink from the current selection
if ( !e.altKey && e.ctrlKey && e.metaKey && (_this.options.richTextFormatKey[ e.keyCode ] if ( e.altKey && e.ctrlKey && !e.metaKey && (_this.options.richTextFormatKey[ e.keyCode ]
|| e.keyCode == 82 || e.keyCode == 49
|| e.keyCode == 72 || e.keyCode == 50
|| e.keyCode == 51
|| e.keyCode == 70
|| e.keyCode == 90
|| e.keyCode == 76 || e.keyCode == 76
|| e.keyCode == 75 || e.keyCode == 75
|| e.keyCode == 79 || e.keyCode == 79
@ -121,10 +136,10 @@
if (e.keyCode == 85) { if (e.keyCode == 85) {
document.execCommand('Underline') document.execCommand('Underline')
} }
if (e.keyCode == 82) { if (e.keyCode == 70) {
document.execCommand('RemoveFormat') document.execCommand('RemoveFormat')
} }
if (e.keyCode == 72) { if (e.keyCode == 90) {
document.execCommand('insertHorizontalRule') document.execCommand('insertHorizontalRule')
} }
if (e.keyCode == 76) { if (e.keyCode == 76) {
@ -139,6 +154,15 @@
if (e.keyCode == 87) { if (e.keyCode == 87) {
document.execCommand('Unlink') document.execCommand('Unlink')
} }
if (e.keyCode == 49) {
_this.toggleBlock('h1')
}
if (e.keyCode == 50) {
_this.toggleBlock('h2')
}
if (e.keyCode == 51) {
_this.toggleBlock('h3')
}
_this.log('content editable richtext key', e.keyCode) _this.log('content editable richtext key', e.keyCode)
return true return true
} }

View file

@ -0,0 +1,204 @@
/*jslint browser: true*/
/*jslint jquery: true*/
/*
* jQuery Hotkeys Plugin
* Copyright 2010, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Based upon the plugin by Tzury Bar Yochay:
* https://github.com/tzuryby/jquery.hotkeys
*
* Original idea by:
* Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
*/
/*
* One small change is: now keys are passed by object { keys: '...' }
* Might be useful, when you want to pass some other data to your handler
*/
(function(jQuery) {
jQuery.hotkeys = {
version: "0.2.0",
specialKeys: {
8: "backspace",
9: "tab",
10: "return",
13: "return",
16: "shift",
17: "ctrl",
18: "alt",
19: "pause",
20: "capslock",
27: "esc",
32: "space",
33: "pageup",
34: "pagedown",
35: "end",
36: "home",
37: "left",
38: "up",
39: "right",
40: "down",
45: "insert",
46: "del",
59: ";",
61: "=",
96: "0",
97: "1",
98: "2",
99: "3",
100: "4",
101: "5",
102: "6",
103: "7",
104: "8",
105: "9",
106: "*",
107: "+",
109: "-",
110: ".",
111: "/",
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: "scroll",
173: "-",
186: ";",
187: "=",
188: ",",
189: "-",
190: ".",
191: "/",
192: "`",
219: "[",
220: "\\",
221: "]",
222: "'"
},
shiftNums: {
"`": "~",
"1": "!",
"2": "@",
"3": "#",
"4": "$",
"5": "%",
"6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
";": ": ",
"'": "\"",
",": "<",
".": ">",
"/": "?",
"\\": "|"
},
// excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url
textAcceptingInputTypes: [
"text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime",
"datetime-local", "search", "color", "tel"],
// default input types not to bind to unless bound directly
textInputTypes: /textarea|input|select/i,
options: {
filterInputAcceptingElements: true,
filterTextInputs: true,
filterContentEditable: true
}
};
function keyHandler(handleObj) {
if (typeof handleObj.data === "string") {
handleObj.data = {
keys: handleObj.data
};
}
// Only care when a possible input has been specified
if (!handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string") {
return;
}
var origHandler = handleObj.handler,
keys = handleObj.data.keys.toLowerCase().split(" ");
handleObj.handler = function(event) {
// Don't fire in text-accepting inputs that we didn't directly bind to
if (this !== event.target &&
(jQuery.hotkeys.options.filterInputAcceptingElements &&
jQuery.hotkeys.textInputTypes.test(event.target.nodeName) ||
(jQuery.hotkeys.options.filterContentEditable && jQuery(event.target).attr('contenteditable')) ||
(jQuery.hotkeys.options.filterTextInputs &&
jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1))) {
return;
}
var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
character = String.fromCharCode(event.which).toLowerCase(),
modif = "",
possible = {};
jQuery.each(["alt", "ctrl", "shift"], function(index, specialKey) {
if (event[specialKey + 'Key'] && special !== specialKey) {
modif += specialKey + '+';
}
});
// metaKey is triggered off ctrlKey erronously
if (event.metaKey && !event.ctrlKey && special !== "meta") {
modif += "meta+";
}
if (event.metaKey && special !== "meta" && modif.indexOf("alt+ctrl+shift+") > -1) {
modif = modif.replace("alt+ctrl+shift+", "hyper+");
}
if (special) {
possible[modif + special] = true;
}
else {
possible[modif + character] = true;
possible[modif + jQuery.hotkeys.shiftNums[character]] = true;
// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
if (modif === "shift+") {
possible[jQuery.hotkeys.shiftNums[character]] = true;
}
}
for (var i = 0, l = keys.length; i < l; i++) {
if (possible[keys[i]]) {
return origHandler.apply(this, arguments);
}
}
};
}
jQuery.each(["keydown", "keyup", "keypress"], function() {
jQuery.event.special[this] = {
add: keyHandler
};
});
})(jQuery || this.jQuery || window.jQuery);

View file

@ -59,6 +59,9 @@ var originalShow = $.fn.popover.Constructor.prototype.show;
$.fn.popover.Constructor.prototype.show = function(){ $.fn.popover.Constructor.prototype.show = function(){
originalShow.call(this); originalShow.call(this);
var maxHeight = $(this.options.viewport.selector).height() - 2 * this.options.viewport.padding; // improved error handling - no exeption if no $tip exists
if (!this.$tip) {
return
}
this.$tip.find('.popover-body').css('maxHeight', maxHeight); this.$tip.find('.popover-body').css('maxHeight', maxHeight);
} }

View file

@ -0,0 +1,38 @@
<div class="horizontal">
<div class="flex">
<% for area in @areas: %>
<% if area.location is 'left': %>
<h2><%- @T(area.headline) %></h2>
<% for item in area.content: %>
<% if item.where: %><p><i><%- @T(item.where) %></i></p><% end %>
<% for shortcut in item.shortcuts: %>
<% if shortcut.hotkeys: %><kbd>ctrl</kbd> <kbd>alt</kbd> <% end %>
<% if _.isArray(shortcut.key): %><% for key in shortcut.key: %> <kbd><%- key %></kbd><% end %>
<% else: %>
<kbd><%- shortcut.key %></kbd>
<% end %>
<%- @T(shortcut.description) %><br>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
<div class="flex">
<% for area in @areas: %>
<% if area.location is 'right': %>
<h2><%- @T(area.headline) %></h2>
<% for item in area.content: %>
<% if item.where: %><p><i><%- @T(item.where) %></i></p><% end %>
<% for shortcut in item.shortcuts: %>
<% if shortcut.hotkeys: %><kbd>ctrl</kbd> <kbd>alt</kbd> <% end %>
<% if _.isArray(shortcut.key): %><% for key in shortcut.key: %> <kbd><%- key %></kbd><% end %>
<% else: %>
<kbd><%- shortcut.key %></kbd>
<% end %>
<%- @T(shortcut.description) %><br>
<% end %>
<% end %>
<% end %>
<% end %>
</div>
</div>

View file

@ -289,6 +289,9 @@ test( "i18n", function() {
translated = App.i18n.translateContent('§%s§ %s test', 123, 'xxx'); translated = App.i18n.translateContent('§%s§ %s test', 123, 'xxx');
equal( translated, '<kbd>123</kbd> xxx test', 'de-de - §%s§ %s' ); equal( translated, '<kbd>123</kbd> xxx test', 'de-de - §%s§ %s' );
translated = App.i18n.translateContent('//%s// %s test', 123, 'xxx');
equal( translated, '<del>123</del> xxx test', 'de-de - //%s// %s' );
translated = App.i18n.translateContent('\'%s\' %s test', 123, 'xxx'); translated = App.i18n.translateContent('\'%s\' %s test', 123, 'xxx');
equal( translated, '&#39;123&#39; xxx test', 'de-de - \'%s\' %s' ); equal( translated, '&#39;123&#39; xxx test', 'de-de - \'%s\' %s' );
@ -332,6 +335,9 @@ test( "i18n", function() {
translated = App.i18n.translateContent('§%s§ %s test', 123, 'xxx'); translated = App.i18n.translateContent('§%s§ %s test', 123, 'xxx');
equal( translated, '<kbd>123</kbd> xxx test', 'en-us - §%s§ %s' ); equal( translated, '<kbd>123</kbd> xxx test', 'en-us - §%s§ %s' );
translated = App.i18n.translateContent('//%s// %s test', 123, 'xxx');
equal( translated, '<del>123</del> xxx test', 'en-us - //%s// %s' );
translated = App.i18n.translateContent('\'%s\' %s test', 123, 'xxx'); translated = App.i18n.translateContent('\'%s\' %s test', 123, 'xxx');
equal( translated, '&#39;123&#39; xxx test', 'en-us - \'%s\' %s' ); equal( translated, '&#39;123&#39; xxx test', 'en-us - \'%s\' %s' );

View file

@ -18,6 +18,7 @@ elif [ "$LEVEL" == '2' ]; then
# only ticket action # only ticket action
rm test/browser/aab_unit_test.rb rm test/browser/aab_unit_test.rb
rm test/browser/aac_basic_richtext_test.rb rm test/browser/aac_basic_richtext_test.rb
rm test/browser/aab_basic_urls_test.rb
rm test/browser/agent_organization_profile_test.rb rm test/browser/agent_organization_profile_test.rb
rm test/browser/agent_user_*.rb rm test/browser/agent_user_*.rb
rm test/browser/auth_test.rb rm test/browser/auth_test.rb
@ -40,6 +41,7 @@ elif [ "$LEVEL" == '3' ]; then
# only profile action # only profile action
rm test/browser/aab_unit_test.rb rm test/browser/aab_unit_test.rb
rm test/browser/aac_basic_richtext_test.rb rm test/browser/aac_basic_richtext_test.rb
rm test/browser/aab_basic_urls_test.rb
rm test/browser/agent_user_manage_test.rb rm test/browser/agent_user_manage_test.rb
rm test/browser/agent_ticket_*.rb rm test/browser/agent_ticket_*.rb
rm test/browser/auth_test.rb rm test/browser/auth_test.rb

View file

@ -0,0 +1,36 @@
# encoding: utf-8
require 'browser_test_helper'
class AABBasicUrlsTest < TestCase
def test_logout
@browser = browser_instance
location(
url: "#{browser_url}/#logout",
)
location_check(
url: "#{browser_url}/#login",
)
end
def test_session
@browser = browser_instance
location(
url: "#{browser_url}/#system/sessions",
)
location_check(
url: "#{browser_url}/#login",
)
end
def test_profile
@browser = browser_instance
location(
url: "#{browser_url}/#profile/linked",
)
location_check(
url: "#{browser_url}/#login",
)
end
end

View file

@ -272,7 +272,7 @@ class TestCase < Test::Unit::TestCase
instance.get(params[:url]) instance.get(params[:url])
# check if reload was successfull # check if reload was successfull
if !instance.find_elements({ css: 'body' })[0] || instance.find_elements({ css: 'body' })[0].text =~ /unavailable or too busy/i if !instance.find_elements(css: 'body')[0] || instance.find_elements(css: 'body')[0].text =~ /unavailable or too busy/i
instance.navigate.refresh instance.navigate.refresh
end end
screenshot(browser: instance, comment: 'location') screenshot(browser: instance, comment: 'location')