Added keyboard shortcut support.
This commit is contained in:
parent
3d372543b2
commit
ffe011553d
25 changed files with 784 additions and 119 deletions
|
@ -128,6 +128,11 @@ Source: https://jqueryui.com
|
|||
Copyright: 2014 jQuery Foundation
|
||||
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
|
||||
Source: http://underscorejs.org
|
||||
Copyright: 2009-2015 Jeremy Ashkenas, DocumentCloud
|
||||
|
|
|
@ -458,6 +458,9 @@ class App.Controller extends Spine.Controller
|
|||
if @userTicketPopupsList
|
||||
@userTicketPopupsList.popover('destroy')
|
||||
|
||||
anyPopoversDestroy: ->
|
||||
$('.popover').remove()
|
||||
|
||||
recentView: (object, o_id) =>
|
||||
params =
|
||||
object: object
|
||||
|
|
|
@ -351,6 +351,9 @@ class App.ControllerNavSidbar extends App.ControllerContent
|
|||
constructor: (params) ->
|
||||
super
|
||||
|
||||
if @authenticateRequired
|
||||
return if !@authenticate()
|
||||
|
||||
@params = params
|
||||
|
||||
# get accessable groups
|
||||
|
|
|
@ -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')
|
|
@ -2223,4 +2223,4 @@ class ChatToTicketRef extends App.ControllerContent
|
|||
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' )
|
|
@ -1,4 +1,5 @@
|
|||
class IndexRouter extends App.ControllerNavSidbar
|
||||
authenticateRequired: true
|
||||
configKey: 'NavBarAdmin'
|
||||
|
||||
App.Config.set('manage', IndexRouter, 'Routes')
|
||||
|
|
|
@ -138,7 +138,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
|
|||
el = @$('#global-search-result')
|
||||
|
||||
# remove result if not result exists
|
||||
if _.isEmpty( result )
|
||||
if _.isEmpty(result)
|
||||
@$('.search').removeClass('open')
|
||||
el.html('')
|
||||
return
|
||||
|
@ -179,11 +179,98 @@ class App.Navigation extends App.ControllerWidgetPermanent
|
|||
# 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
|
||||
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(
|
||||
id: 'search'
|
||||
|
@ -193,13 +280,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
|
|||
query: @query
|
||||
processData: true,
|
||||
success: (data, status, xhr) =>
|
||||
|
||||
# load assets
|
||||
App.Collection.loadAssets( data.assets )
|
||||
|
||||
# cache search result
|
||||
@searchResultCache[@query] = data.result
|
||||
|
||||
App.Collection.loadAssets(data.assets)
|
||||
result = {}
|
||||
for item in data.result
|
||||
if App[item.type] && App[item.type].find
|
||||
|
@ -214,84 +295,26 @@ class App.Navigation extends App.ControllerWidgetPermanent
|
|||
else
|
||||
@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)
|
||||
|
||||
@$('#global-search-result').on('click', 'a', ->
|
||||
close()
|
||||
@$('#global-search-result').on('click', 'a', =>
|
||||
@andClose()
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
@delay(search, 200, 'search')
|
||||
|
||||
getItems: (data) ->
|
||||
navbar = _.values(data.navbar)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
class Index extends App.ControllerNavSidbar
|
||||
authenticateRequired: true
|
||||
configKey: 'NavBarProfile'
|
||||
|
||||
App.Config.set( 'profile', Index, 'Routes' )
|
||||
|
|
|
@ -133,6 +133,9 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
|
||||
# preselect article type
|
||||
@setArticleType(data.type.name)
|
||||
|
||||
# set focus into field
|
||||
@textarea.focus()
|
||||
)
|
||||
|
||||
# reset new article screen
|
||||
|
|
|
@ -85,7 +85,7 @@ class Index extends App.ControllerContent
|
|||
release: =>
|
||||
rerender = ->
|
||||
App.Event.trigger('ui:rerender')
|
||||
if @translationList.changes()
|
||||
if @translationList && @translationList.changes()
|
||||
App.Delay.set(rerender, 400)
|
||||
|
||||
showAction: =>
|
||||
|
|
|
@ -17,4 +17,4 @@ class Widget
|
|||
"""
|
||||
console.log(banner)
|
||||
|
||||
App.Config.set( 'dev_banner', Widget, 'Widgets' )
|
||||
App.Config.set('dev_banner', Widget, 'Widgets')
|
||||
|
|
|
@ -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' )
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
)
|
|
@ -38,7 +38,7 @@ class App.Browser
|
|||
# define min. required browser version
|
||||
map =
|
||||
Chrome: 37
|
||||
Firefox: 31
|
||||
Firefox: 36
|
||||
Explorer: 10
|
||||
Safari: 6
|
||||
Opera: 22
|
||||
|
|
|
@ -224,6 +224,7 @@ class _i18nSingleton extends Spine.Module
|
|||
.replace(/\|\|(.+?)\|\|/gm, '<i>$1</i>')
|
||||
.replace(/\|(.+?)\|/gm, '<b>$1</b>')
|
||||
.replace(/_(.+?)_/gm, '<u>$1</u>')
|
||||
.replace(/\/\/(.+?)\/\//gm, '<del>$1</del>')
|
||||
.replace(/§(.+?)§/gm, '<kbd>$1</kbd>')
|
||||
|
||||
# search %s
|
||||
|
|
|
@ -6,8 +6,7 @@ class App.Run extends App.Controller
|
|||
App.Event.trigger('app:init')
|
||||
|
||||
# browser check
|
||||
if !App.Browser.check()
|
||||
return
|
||||
return if !App.Browser.check()
|
||||
|
||||
# hide splash screen
|
||||
$('.splash').hide()
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
|
||||
this._defaults = defaults;
|
||||
this._name = pluginName;
|
||||
this._setTimeOutReformat = false;
|
||||
|
||||
// take placeholder from markup
|
||||
if ( !this.options.placeholder && this.$element.data('placeholder') ) {
|
||||
|
@ -67,6 +66,19 @@
|
|||
Plugin.prototype.init = function () {
|
||||
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
|
||||
this.$element.on('keydown', function (e) {
|
||||
_this.log('keydown', e.keyCode)
|
||||
|
@ -94,19 +106,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
// on zammad metaKey + ctrlKey + i/b/u
|
||||
// metaKey + ctrlKey + u -> Toggles the current selection between underlined and not underlined
|
||||
// metaKey + ctrlKey + b -> Toggles the current selection between bold and non-bold
|
||||
// metaKey + ctrlKey + i -> Toggles the current selection between italic and non-italic
|
||||
// metaKey + ctrlKey + r -> Removes the formatting tags from the current selection
|
||||
// metaKey + ctrlKey + h -> Inserts a Horizontal Rule
|
||||
// metaKey + 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
|
||||
// metaKey + ctrlKey + o -> Draws a line through the middle of the current selection
|
||||
// metaKey + ctrlKey + w -> Removes any hyperlink from the current selection
|
||||
if ( !e.altKey && e.ctrlKey && e.metaKey && (_this.options.richTextFormatKey[ e.keyCode ]
|
||||
|| e.keyCode == 82
|
||||
|| e.keyCode == 72
|
||||
// on zammad altKey + ctrlKey + i/b/u
|
||||
// altKey + ctrlKey + u -> Toggles the current selection between underlined and not underlined
|
||||
// altKey + ctrlKey + b -> Toggles the current selection between bold and non-bold
|
||||
// altKey + ctrlKey + i -> Toggles the current selection between italic and non-italic
|
||||
// altKey + ctrlKey + f -> Removes the formatting tags from the current selection
|
||||
// altKey + ctrlKey + z -> Inserts a Horizontal Rule
|
||||
// altKey + ctrlKey + 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
|
||||
// altKey + ctrlKey + o -> Draws a line through the middle of 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 ]
|
||||
|| e.keyCode == 49
|
||||
|| e.keyCode == 50
|
||||
|| e.keyCode == 51
|
||||
|| e.keyCode == 70
|
||||
|| e.keyCode == 90
|
||||
|| e.keyCode == 76
|
||||
|| e.keyCode == 75
|
||||
|| e.keyCode == 79
|
||||
|
@ -121,10 +136,10 @@
|
|||
if (e.keyCode == 85) {
|
||||
document.execCommand('Underline')
|
||||
}
|
||||
if (e.keyCode == 82) {
|
||||
if (e.keyCode == 70) {
|
||||
document.execCommand('RemoveFormat')
|
||||
}
|
||||
if (e.keyCode == 72) {
|
||||
if (e.keyCode == 90) {
|
||||
document.execCommand('insertHorizontalRule')
|
||||
}
|
||||
if (e.keyCode == 76) {
|
||||
|
@ -139,6 +154,15 @@
|
|||
if (e.keyCode == 87) {
|
||||
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)
|
||||
return true
|
||||
}
|
||||
|
|
204
app/assets/javascripts/app/lib/base/jquery.hotkeys.js
Normal file
204
app/assets/javascripts/app/lib/base/jquery.hotkeys.js
Normal 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);
|
|
@ -59,6 +59,9 @@ var originalShow = $.fn.popover.Constructor.prototype.show;
|
|||
$.fn.popover.Constructor.prototype.show = function(){
|
||||
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);
|
||||
}
|
38
app/assets/javascripts/app/views/keyboard_shortcuts.jst.eco
Normal file
38
app/assets/javascripts/app/views/keyboard_shortcuts.jst.eco
Normal 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>
|
|
@ -289,6 +289,9 @@ test( "i18n", function() {
|
|||
translated = App.i18n.translateContent('§%s§ %s test', 123, 'xxx');
|
||||
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');
|
||||
equal( translated, ''123' xxx test', 'de-de - \'%s\' %s' );
|
||||
|
||||
|
@ -332,6 +335,9 @@ test( "i18n", function() {
|
|||
translated = App.i18n.translateContent('§%s§ %s test', 123, 'xxx');
|
||||
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');
|
||||
equal( translated, ''123' xxx test', 'en-us - \'%s\' %s' );
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ elif [ "$LEVEL" == '2' ]; then
|
|||
# only ticket action
|
||||
rm test/browser/aab_unit_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_user_*.rb
|
||||
rm test/browser/auth_test.rb
|
||||
|
@ -40,6 +41,7 @@ elif [ "$LEVEL" == '3' ]; then
|
|||
# only profile action
|
||||
rm test/browser/aab_unit_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_ticket_*.rb
|
||||
rm test/browser/auth_test.rb
|
||||
|
|
36
test/browser/aab_basic_urls_test.rb
Normal file
36
test/browser/aab_basic_urls_test.rb
Normal 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
|
|
@ -272,7 +272,7 @@ class TestCase < Test::Unit::TestCase
|
|||
instance.get(params[:url])
|
||||
|
||||
# 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
|
||||
end
|
||||
screenshot(browser: instance, comment: 'location')
|
||||
|
|
Loading…
Reference in a new issue