diff --git a/LICENSE-3RD-PARTY.txt b/LICENSE-3RD-PARTY.txt
index 4028ed602..98274dce3 100644
--- a/LICENSE-3RD-PARTY.txt
+++ b/LICENSE-3RD-PARTY.txt
@@ -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
diff --git a/app/assets/javascripts/app/controllers/_application_controller.coffee b/app/assets/javascripts/app/controllers/_application_controller.coffee
index bf526ae87..941784e3f 100644
--- a/app/assets/javascripts/app/controllers/_application_controller.coffee
+++ b/app/assets/javascripts/app/controllers/_application_controller.coffee
@@ -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
diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee
index b6c4b06e7..4c7428251 100644
--- a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee
+++ b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee
@@ -351,6 +351,9 @@ class App.ControllerNavSidbar extends App.ControllerContent
constructor: (params) ->
super
+ if @authenticateRequired
+ return if !@authenticate()
+
@params = params
# get accessable groups
diff --git a/app/assets/javascripts/app/controllers/keyboard_shortcurs.coffee b/app/assets/javascripts/app/controllers/keyboard_shortcurs.coffee
new file mode 100644
index 000000000..ec5f5fd03
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/keyboard_shortcurs.coffee
@@ -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')
diff --git a/app/assets/javascripts/app/controllers/layout_ref.coffee b/app/assets/javascripts/app/controllers/layout_ref.coffee
index 0d905ff37..6c46eac3b 100644
--- a/app/assets/javascripts/app/controllers/layout_ref.coffee
+++ b/app/assets/javascripts/app/controllers/layout_ref.coffee
@@ -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' )
\ No newline at end of file
+App.Config.set( 'LayoutRef', { prio: 1600, parent: '#current_user', name: 'Layout Reference', translate: true, target: '#layout_ref', role: [ 'Admin' ] }, 'NavBarRight' )
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/manage.coffee b/app/assets/javascripts/app/controllers/manage.coffee
index e42758b76..f59d7bb72 100644
--- a/app/assets/javascripts/app/controllers/manage.coffee
+++ b/app/assets/javascripts/app/controllers/manage.coffee
@@ -1,4 +1,5 @@
class IndexRouter extends App.ControllerNavSidbar
+ authenticateRequired: true
configKey: 'NavBarAdmin'
App.Config.set('manage', IndexRouter, 'Routes')
diff --git a/app/assets/javascripts/app/controllers/navigation.coffee b/app/assets/javascripts/app/controllers/navigation.coffee
index 20dd58956..38ef4c35b 100644
--- a/app/assets/javascripts/app/controllers/navigation.coffee
+++ b/app/assets/javascripts/app/controllers/navigation.coffee
@@ -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)
diff --git a/app/assets/javascripts/app/controllers/profile.coffee b/app/assets/javascripts/app/controllers/profile.coffee
index e1d8503fa..a4b4c8938 100644
--- a/app/assets/javascripts/app/controllers/profile.coffee
+++ b/app/assets/javascripts/app/controllers/profile.coffee
@@ -1,4 +1,5 @@
class Index extends App.ControllerNavSidbar
+ authenticateRequired: true
configKey: 'NavBarProfile'
App.Config.set( 'profile', Index, 'Routes' )
diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee
index 1eb9ebb75..be08b66fa 100644
--- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee
+++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee
@@ -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
diff --git a/app/assets/javascripts/app/controllers/translation.coffee b/app/assets/javascripts/app/controllers/translation.coffee
index 1d7ec1afb..d9589d0c6 100644
--- a/app/assets/javascripts/app/controllers/translation.coffee
+++ b/app/assets/javascripts/app/controllers/translation.coffee
@@ -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: =>
diff --git a/app/assets/javascripts/app/controllers/widget/dev_banner.coffee b/app/assets/javascripts/app/controllers/widget/dev_banner.coffee
index cfc3819f6..a8f7f5c9e 100644
--- a/app/assets/javascripts/app/controllers/widget/dev_banner.coffee
+++ b/app/assets/javascripts/app/controllers/widget/dev_banner.coffee
@@ -17,4 +17,4 @@ class Widget
"""
console.log(banner)
-App.Config.set( 'dev_banner', Widget, 'Widgets' )
+App.Config.set('dev_banner', Widget, 'Widgets')
diff --git a/app/assets/javascripts/app/controllers/widget/ff_lt_35.coffee b/app/assets/javascripts/app/controllers/widget/ff_lt_35.coffee
deleted file mode 100644
index cffa499e6..000000000
--- a/app/assets/javascripts/app/controllers/widget/ff_lt_35.coffee
+++ /dev/null
@@ -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' )
\ No newline at end of file
diff --git a/app/assets/javascripts/app/controllers/widget/keyboard_shortcuts.coffee b/app/assets/javascripts/app/controllers/widget/keyboard_shortcuts.coffee
new file mode 100644
index 000000000..2f7569bbc
--- /dev/null
+++ b/app/assets/javascripts/app/controllers/widget/keyboard_shortcuts.coffee
@@ -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'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+)
diff --git a/app/assets/javascripts/app/controllers/widget/maintenance.coffee b/app/assets/javascripts/app/controllers/widget/maintenance.coffee
index 5c0d74f9b..eea18cf04 100644
--- a/app/assets/javascripts/app/controllers/widget/maintenance.coffee
+++ b/app/assets/javascripts/app/controllers/widget/maintenance.coffee
@@ -32,4 +32,4 @@ class Widget extends App.Controller
forceReload: message.reload
)
-App.Config.set('maintenance', Widget, 'Widgets')
\ No newline at end of file
+App.Config.set('maintenance', Widget, 'Widgets')
diff --git a/app/assets/javascripts/app/lib/app_post/browser.coffee.coffee b/app/assets/javascripts/app/lib/app_post/browser.coffee.coffee
index 881bf20a3..35fb62680 100644
--- a/app/assets/javascripts/app/lib/app_post/browser.coffee.coffee
+++ b/app/assets/javascripts/app/lib/app_post/browser.coffee.coffee
@@ -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
diff --git a/app/assets/javascripts/app/lib/app_post/i18n.coffee b/app/assets/javascripts/app/lib/app_post/i18n.coffee
index aa3ed464f..776bae199 100644
--- a/app/assets/javascripts/app/lib/app_post/i18n.coffee
+++ b/app/assets/javascripts/app/lib/app_post/i18n.coffee
@@ -224,6 +224,7 @@ class _i18nSingleton extends Spine.Module
.replace(/\|\|(.+?)\|\|/gm, '$1')
.replace(/\|(.+?)\|/gm, '$1')
.replace(/_(.+?)_/gm, '$1')
+ .replace(/\/\/(.+?)\/\//gm, '$1')
.replace(/§(.+?)§/gm, '$1')
# search %s
diff --git a/app/assets/javascripts/app/lib/app_post/interface_handle.coffee b/app/assets/javascripts/app/lib/app_post/interface_handle.coffee
index 0679e76ed..1836d605f 100644
--- a/app/assets/javascripts/app/lib/app_post/interface_handle.coffee
+++ b/app/assets/javascripts/app/lib/app_post/interface_handle.coffee
@@ -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()
diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
index ed1ecad53..337b4651b 100644
--- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
+++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js
@@ -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
}
diff --git a/app/assets/javascripts/app/lib/base/jquery.hotkeys.js b/app/assets/javascripts/app/lib/base/jquery.hotkeys.js
new file mode 100644
index 000000000..e7701f39c
--- /dev/null
+++ b/app/assets/javascripts/app/lib/base/jquery.hotkeys.js
@@ -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);
diff --git a/app/assets/javascripts/app/lib/bootstrap/popover-enhance.js b/app/assets/javascripts/app/lib/bootstrap/popover-enhance.js
index 3662cfb87..22cfe7584 100644
--- a/app/assets/javascripts/app/lib/bootstrap/popover-enhance.js
+++ b/app/assets/javascripts/app/lib/bootstrap/popover-enhance.js
@@ -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);
}
\ No newline at end of file
diff --git a/app/assets/javascripts/app/views/keyboard_shortcuts.jst.eco b/app/assets/javascripts/app/views/keyboard_shortcuts.jst.eco
new file mode 100644
index 000000000..9fa3d9c27
--- /dev/null
+++ b/app/assets/javascripts/app/views/keyboard_shortcuts.jst.eco
@@ -0,0 +1,38 @@
+
<%- @T(item.where) %>
<% end %> + <% for shortcut in item.shortcuts: %> + <% if shortcut.hotkeys: %>ctrl alt <% end %> + <% if _.isArray(shortcut.key): %><% for key in shortcut.key: %> <%- key %><% end %> + <% else: %> + <%- shortcut.key %> + <% end %> + <%- @T(shortcut.description) %><%- @T(item.where) %>
<% end %> + <% for shortcut in item.shortcuts: %> + <% if shortcut.hotkeys: %>ctrl alt <% end %> + <% if _.isArray(shortcut.key): %><% for key in shortcut.key: %> <%- key %><% end %> + <% else: %> + <%- shortcut.key %> + <% end %> + <%- @T(shortcut.description) %>