From ffe011553d8c6554484e47cb25a2e2cf077ff036 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 25 Feb 2016 06:44:17 +0100 Subject: [PATCH] Added keyboard shortcut support. --- LICENSE-3RD-PARTY.txt | 5 + .../_application_controller.coffee | 3 + .../_application_controller_generic.coffee | 3 + .../app/controllers/keyboard_shortcurs.coffee | 30 ++ .../app/controllers/layout_ref.coffee | 2 +- .../javascripts/app/controllers/manage.coffee | 1 + .../app/controllers/navigation.coffee | 191 +++++++----- .../app/controllers/profile.coffee | 1 + .../ticket_zoom/article_new.coffee | 3 + .../app/controllers/translation.coffee | 2 +- .../app/controllers/widget/dev_banner.coffee | 2 +- .../app/controllers/widget/ff_lt_35.coffee | 10 - .../widget/keyboard_shortcuts.coffee | 293 ++++++++++++++++++ .../app/controllers/widget/maintenance.coffee | 2 +- .../app/lib/app_post/browser.coffee.coffee | 2 +- .../javascripts/app/lib/app_post/i18n.coffee | 1 + .../app/lib/app_post/interface_handle.coffee | 3 +- .../app/lib/base/jquery.contenteditable.js | 56 +++- .../app/lib/base/jquery.hotkeys.js | 204 ++++++++++++ .../app/lib/bootstrap/popover-enhance.js | 5 +- .../app/views/keyboard_shortcuts.jst.eco | 38 +++ public/assets/tests/core.js | 6 + script/build/test_slice_tests.sh | 2 + test/browser/aab_basic_urls_test.rb | 36 +++ test/browser_test_helper.rb | 2 +- 25 files changed, 784 insertions(+), 119 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/keyboard_shortcurs.coffee delete mode 100644 app/assets/javascripts/app/controllers/widget/ff_lt_35.coffee create mode 100644 app/assets/javascripts/app/controllers/widget/keyboard_shortcuts.coffee create mode 100644 app/assets/javascripts/app/lib/base/jquery.hotkeys.js create mode 100644 app/assets/javascripts/app/views/keyboard_shortcuts.jst.eco create mode 100644 test/browser/aab_basic_urls_test.rb 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 @@ +
+
+<% for area in @areas: %> + <% if area.location is 'left': %> +

<%- @T(area.headline) %>

+ <% for item in area.content: %> + <% if item.where: %>

<%- @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) %>
+ <% end %> + <% end %> + <% end %> +<% end %> +
+
+<% for area in @areas: %> + <% if area.location is 'right': %> +

<%- @T(area.headline) %>

+ <% for item in area.content: %> + <% if item.where: %>

<%- @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) %>
+ <% end %> + <% end %> + <% end %> +<% end %> +
+
diff --git a/public/assets/tests/core.js b/public/assets/tests/core.js index 606e71bb1..0749e3a48 100644 --- a/public/assets/tests/core.js +++ b/public/assets/tests/core.js @@ -289,6 +289,9 @@ test( "i18n", function() { translated = App.i18n.translateContent('§%s§ %s test', 123, 'xxx'); equal( translated, '123 xxx test', 'de-de - §%s§ %s' ); + translated = App.i18n.translateContent('//%s// %s test', 123, 'xxx'); + equal( translated, '123 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, '123 xxx test', 'en-us - §%s§ %s' ); + translated = App.i18n.translateContent('//%s// %s test', 123, 'xxx'); + equal( translated, '123 xxx test', 'en-us - //%s// %s' ); + translated = App.i18n.translateContent('\'%s\' %s test', 123, 'xxx'); equal( translated, ''123' xxx test', 'en-us - \'%s\' %s' ); diff --git a/script/build/test_slice_tests.sh b/script/build/test_slice_tests.sh index 2b7c0e69b..1ebddeffb 100755 --- a/script/build/test_slice_tests.sh +++ b/script/build/test_slice_tests.sh @@ -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 diff --git a/test/browser/aab_basic_urls_test.rb b/test/browser/aab_basic_urls_test.rb new file mode 100644 index 000000000..7fc833feb --- /dev/null +++ b/test/browser/aab_basic_urls_test.rb @@ -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 diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index 7e43895b1..af9d85dfa 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -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')