Browse Source

Added keyboard support to online notifications. Added browser tests. Improved search keyboard support.

Martin Edenhofer 9 years ago
parent
commit
532d562845

+ 5 - 0
LICENSE-3RD-PARTY.txt

@@ -82,6 +82,11 @@ Copyright: @leChanteaux <santiago at mural.ly>
            Mural.ly Dev Team <dev at mural.ly>
 License: dfyw
 -----------------------------------------------------------------------------
+jquery.visible.js
+Source: https://github.com/customd/jquery-visible
+Copyright: 2012, Digital Fusion
+License: MIT license
+-----------------------------------------------------------------------------
 marked.js
 Source: https://github.com/chjj/marked
 Copyright: 2011-2014, Christopher Jeffrey

+ 36 - 38
app/assets/javascripts/app/controllers/_application_controller.coffee

@@ -128,32 +128,32 @@ class App.Controller extends Spine.Controller
     return if !$('#navigation').is(':visible')
     $('#navigation').addClass('hide')
 
-  scrollTo: ( x = 0, y = 0, delay = 0 ) ->
+  scrollTo: (x = 0, y = 0, delay = 0) ->
     a = ->
-      window.scrollTo( x, y )
+      window.scrollTo(x, y)
 
-    @delay( a, delay )
+    @delay(a, delay)
 
   shake: (element) ->
 
     # this part is from wordpress 3, thanks to open source
     shakeMe = (element, position, positionEnd) ->
       positionStart = position.shift()
-      element.css( 'left', positionStart + 'px' )
+      element.css('left', positionStart + 'px')
       if position.length > 0
-        setTimeout( ->
-          shakeMe( element, position, positionEnd )
+        setTimeout(->
+          shakeMe(element, position, positionEnd)
         , positionEnd)
       else
         try
-          element.css( 'position', 'static' )
+          element.css('position', 'static')
         catch e
           console.log 'error', e
 
     position = [ 15, 30, 15, 0, -15, -30, -15, 0 ]
-    position = position.concat( position.concat( position ) )
-    element.css( 'position', 'relative' )
-    shakeMe( element, position, 20 )
+    position = position.concat(position.concat(position))
+    element.css('position', 'relative')
+    shakeMe(element, position, 20)
 
   isRole: (name) ->
     roles = @Session.get('roles')
@@ -183,8 +183,8 @@ class App.Controller extends Spine.Controller
     App.Utils.humanFileSize(size)
 
   # human readable time
-  humanTime: ( time, escalation, long = true ) ->
-    App.PrettyDate.humanTime( time, escalation, long )
+  humanTime: (time, escalation, long = true) ->
+    App.PrettyDate.humanTime(time, escalation, long)
 
   userInfo: (data) ->
     el = data.el || $('[data-id="customer_info"]')
@@ -206,7 +206,7 @@ class App.Controller extends Spine.Controller
     if !checkOnly
       location = window.location.hash
       if location isnt '#login' && location isnt '#logout' && location isnt '#keyboard_shortcuts'
-        @Config.set( 'requested_url', location)
+        @Config.set('requested_url', location)
 
     return false if checkOnly
 
@@ -262,16 +262,16 @@ class App.Controller extends Spine.Controller
       placement:  position
       title: ->
         ticket_id = $(@).data('id')
-        ticket    = App.Ticket.fullLocal( ticket_id )
-        App.Utils.htmlEscape( ticket.title )
+        ticket    = App.Ticket.fullLocal(ticket_id)
+        App.Utils.htmlEscape(ticket.title)
       content: ->
         ticket_id = $(@).data('id')
-        ticket    = App.Ticket.fullLocal( ticket_id )
+        ticket    = App.Ticket.fullLocal(ticket_id)
         html = App.view('popover/ticket')(
           ticket: ticket
         )
         html = $(html)
-        html.find('.humanTimeFromNow').each( ->
+        html.find('.humanTimeFromNow').each(->
           item = $(@)
           ui.frontendTimeUpdateItem(item)
         )
@@ -306,11 +306,11 @@ class App.Controller extends Spine.Controller
       placement:  "auto #{position}"
       title: ->
         user_id = $(@).data('id')
-        user    = App.User.fullLocal( user_id )
-        App.Utils.htmlEscape( user.displayName() )
+        user    = App.User.fullLocal(user_id)
+        App.Utils.htmlEscape(user.displayName())
       content: ->
         user_id = $(@).data('id')
-        user    = App.User.fullLocal( user_id )
+        user    = App.User.fullLocal(user_id)
 
         # get display data
         userData = []
@@ -318,7 +318,7 @@ class App.Controller extends Spine.Controller
 
           # check if value for _id exists
           name    = attributeName
-          nameNew = name.substr( 0, name.length - 3 )
+          nameNew = name.substr(0, name.length - 3)
           if nameNew of user
             name = nameNew
 
@@ -364,11 +364,11 @@ class App.Controller extends Spine.Controller
       placement:  "auto #{position}"
       title: ->
         organization_id = $(@).data('id')
-        organization    = App.Organization.fullLocal( organization_id )
-        App.Utils.htmlEscape( organization.name )
+        organization    = App.Organization.fullLocal(organization_id)
+        App.Utils.htmlEscape(organization.name)
       content: ->
         organization_id = $(@).data('id')
-        organization    = App.Organization.fullLocal( organization_id )
+        organization    = App.Organization.fullLocal(organization_id)
 
         # get display data
         organizationData = []
@@ -376,7 +376,7 @@ class App.Controller extends Spine.Controller
 
           # check if value for _id exists
           name    = attributeName
-          nameNew = name.substr( 0, name.length - 3 )
+          nameNew = name.substr(0, name.length - 3)
           if nameNew of organization
             name = nameNew
 
@@ -424,13 +424,13 @@ class App.Controller extends Spine.Controller
           tickets = []
           if ticket_list[type]
             for ticket_id in ticket_list[type]
-              tickets.push App.Ticket.fullLocal( ticket_id )
+              tickets.push App.Ticket.fullLocal(ticket_id)
 
           # insert data
           html = App.view('popover/user_ticket_list')(
             tickets: tickets
           )
-          html = $( html )
+          html = $(html )
           html.find('.humanTimeFromNow').each( ->
             item = $(@)
             ui.frontendTimeUpdateItem(item)
@@ -440,15 +440,14 @@ class App.Controller extends Spine.Controller
 
     fetch = (params) =>
       @ajax(
-        type:  'GET',
-        url:   @Config.get('api_path') + '/ticket_customer',
-        data:  {
-          customer_id: params.user_id,
-        }
-        processData: true,
+        type:  'GET'
+        url:   @Config.get('api_path') + '/ticket_customer'
+        data:
+          customer_id: params.user_id
+        processData: true
         success: (data, status, xhr) ->
-          App.Collection.loadAssets( data.assets )
-          show( params, { open: data.ticket_ids_open, closed: data.ticket_ids_closed } )
+          App.Collection.loadAssets(data.assets)
+          show(params, { open: data.ticket_ids_open, closed: data.ticket_ids_closed })
       )
 
     # get data
@@ -514,10 +513,10 @@ class App.Controller extends Spine.Controller
               item.default = params[item.name]
               #if !item.default
               #  delete item['default']
-              newElement = ui.formGenItem( item, classname, form )
+              newElement = ui.formGenItem(item, classname, form)
 
           # replace new option list
-          form.find('[name="' + fieldNameToChange + '"]').closest('.form-group').replaceWith( newElement )
+          form.find('[name="' + fieldNameToChange + '"]').closest('.form-group').replaceWith(newElement)
 
   stopPropagation: (e) ->
     e.stopPropagation()
@@ -712,7 +711,6 @@ class App.ControllerModal extends App.Controller
       'hide.bs.modal':   @onClose
       'hidden.bs.modal': =>
         @onClosed()
-        # remove modal from dom
         $('.modal').remove()
 
   close: (e) =>

+ 17 - 9
app/assets/javascripts/app/controllers/navigation.coffee

@@ -195,7 +195,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
     @$('form.search').on('submit', (e) ->
       e.preventDefault()
     )
-    @$('#global-search').on('keydown', @navigate)
+    @$('#global-search').on('keydown', @listNavigate)
 
     # bind to empty search
     @$('.empty-search').on('click', =>
@@ -206,7 +206,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
       el: @el
     )
 
-  navigate: (e) =>
+  listNavigate: (e) =>
     if e.keyCode is 27 # close on esc
       @emptyAndClose()
       return
@@ -217,7 +217,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
       @nudge(e, 1)
       return
     else if e.keyCode is 13 # enter
-      href = @$('#global-search-result .nav-tab.is-active').attr('href')
+      href = @$('#global-search-result .nav-tab.is-hover').attr('href')
       @locationExecute(href)
       @emptyAndClose()
       return
@@ -229,21 +229,29 @@ class App.Navigation extends App.ControllerWidgetPermanent
 
     # get current
     navigationResult = @$('#global-search-result')
-    current = navigationResult.find('.nav-tab.is-active')
+    current = navigationResult.find('.nav-tab.is-hover')
     if !current.get(0)
-      navigationResult.find('.nav-tab').first().addClass('is-active')
+      navigationResult.find('.nav-tab').first().addClass('is-hover')
       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')
+        current.removeClass('is-hover').popover('hide')
+        next.addClass('is-hover').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')
+        current.removeClass('is-hover').popover('hide')
+        prev.addClass('is-hover').popover('show')
+
+    if next
+      element = next.get(0)
+    if prev
+      element = prev.get(0)
+    return if !element
+    return if $(element).visible(true)
+    element.scrollIntoView()
 
   emptyAndClose: =>
     @$('#global-search').val('').blur()

+ 13 - 0
app/assets/javascripts/app/controllers/widget/keyboard_shortcuts.coffee

@@ -59,6 +59,7 @@ App.Config.set(
               description: 'Dashboard'
               callback: (e) ->
                 e.preventDefault()
+                $('#global-search').blur()
                 App.Event.trigger('keyboard_shortcuts_close')
                 window.location.hash = '#dashboard'
             }
@@ -68,6 +69,7 @@ App.Config.set(
               description: 'Overviews'
               callback: (e) ->
                 e.preventDefault()
+                $('#global-search').blur()
                 App.Event.trigger('keyboard_shortcuts_close')
                 window.location.hash = '#ticket/view'
             }
@@ -80,12 +82,23 @@ App.Config.set(
                 App.Event.trigger('keyboard_shortcuts_close')
                 $('#global-search').focus()
             }
+            {
+              key: 'y'
+              hotkeys: true
+              description: 'Notifications'
+              callback: (e) ->
+                e.preventDefault()
+                $('#global-search').blur()
+                App.Event.trigger('keyboard_shortcuts_close')
+                $('#navigation .js-toggleNotifications').click()
+            }
             {
               key: 'n'
               hotkeys: true
               description: 'New Ticket'
               callback: (e) ->
                 e.preventDefault()
+                $('#global-search').blur()
                 App.Event.trigger('keyboard_shortcuts_close')
                 window.location.hash = '#ticket/create'
             }

+ 45 - 0
app/assets/javascripts/app/controllers/widget/online_notification.coffee

@@ -48,6 +48,7 @@ class App.OnlineNotificationWidget extends App.Controller
   release: ->
     @removeContainer()
     $(window).off 'click.notifications'
+    $(window).off 'keydown.notifications'
     App.OnlineNotification.unsubscribe(@subscribeId)
 
   access: ->
@@ -56,6 +57,48 @@ class App.OnlineNotificationWidget extends App.Controller
     return true if @isRole('Admin')
     return false
 
+  listNavigate: (e) =>
+
+    if e.keyCode is 27 # close on esc
+      @hidePopover()
+      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
+      $('.js-notificationsContainer .popover-content .activity-entry.is-hover .js-locationVerify').click()
+
+  nudge: (e, position) ->
+
+    # get current
+    navigation = $('.js-notificationsContainer .popover-content')
+    current = navigation.find('.activity-entry.is-hover')
+    if !current.get(0)
+      navigation.find('.activity-entry').first().addClass('is-hover')
+      return
+
+    if position is 1
+      next = current.next('.activity-entry')
+      if next.get(0)
+        current.removeClass('is-hover')
+        next.addClass('is-hover')
+    else
+      prev = current.prev('.activity-entry')
+      if prev.get(0)
+        current.removeClass('is-hover')
+        prev.addClass('is-hover')
+
+    if next
+      element = next.get(0)
+    if prev
+      element = prev.get(0)
+    return if !element
+    return if $(element).visible(true)
+    element.scrollIntoView()
+
   counterUpdate: (count) =>
     if !count
       @el.find('.js-counter').text('')
@@ -100,9 +143,11 @@ class App.OnlineNotificationWidget extends App.Controller
 
     notificationsContainer.on 'click', @stopPropagation
     $(window).on 'click.notifications', @hidePopover
+    $(window).on 'keydown.notifications', @listNavigate
 
   onHide: ->
     $(window).off 'click.notifications'
+    $(window).off 'keydown.notifications'
 
   hidePopover: =>
     @toggle.popover('hide')

+ 68 - 0
app/assets/javascripts/app/lib/base/jquery.visible.js

@@ -0,0 +1,68 @@
+(function($){
+
+    /**
+     * Copyright 2012, Digital Fusion
+     * Licensed under the MIT license.
+     * http://teamdf.com/jquery-plugins/license/
+     *
+     * @author Sam Sehnert
+     * @desc A small plugin that checks whether elements are within
+     *       the user visible viewport of a web browser.
+     *       only accounts for vertical position, not horizontal.
+     */
+    var $w = $(window);
+    $.fn.visible = function(partial,hidden,direction){
+
+        if (this.length < 1)
+            return;
+
+        var $t        = this.length > 1 ? this.eq(0) : this,
+            t         = $t.get(0),
+            vpWidth   = $w.width(),
+            vpHeight  = $w.height(),
+            direction = (direction) ? direction : 'both',
+            clientSize = hidden === true ? t.offsetWidth * t.offsetHeight : true;
+
+        if (typeof t.getBoundingClientRect === 'function'){
+
+            // Use this native browser method, if available.
+            var rec = t.getBoundingClientRect(),
+                tViz = rec.top    >= 0 && rec.top    <  vpHeight,
+                bViz = rec.bottom >  0 && rec.bottom <= vpHeight,
+                lViz = rec.left   >= 0 && rec.left   <  vpWidth,
+                rViz = rec.right  >  0 && rec.right  <= vpWidth,
+                vVisible   = partial ? tViz || bViz : tViz && bViz,
+                hVisible   = partial ? lViz || rViz : lViz && rViz;
+
+            if(direction === 'both')
+                return clientSize && vVisible && hVisible;
+            else if(direction === 'vertical')
+                return clientSize && vVisible;
+            else if(direction === 'horizontal')
+                return clientSize && hVisible;
+        } else {
+
+            var viewTop         = $w.scrollTop(),
+                viewBottom      = viewTop + vpHeight,
+                viewLeft        = $w.scrollLeft(),
+                viewRight       = viewLeft + vpWidth,
+                offset          = $t.offset(),
+                _top            = offset.top,
+                _bottom         = _top + $t.height(),
+                _left           = offset.left,
+                _right          = _left + $t.width(),
+                compareTop      = partial === true ? _bottom : _top,
+                compareBottom   = partial === true ? _top : _bottom,
+                compareLeft     = partial === true ? _right : _left,
+                compareRight    = partial === true ? _left : _right;
+
+            if(direction === 'both')
+                return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)) && ((compareRight <= viewRight) && (compareLeft >= viewLeft));
+            else if(direction === 'vertical')
+                return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop));
+            else if(direction === 'horizontal')
+                return !!clientSize && ((compareRight <= viewRight) && (compareLeft >= viewLeft));
+        }
+    };
+
+})(jQuery);

+ 6 - 1
app/assets/stylesheets/zammad.scss

@@ -2904,7 +2904,8 @@ footer {
   }
 
   .nav-tab.is-active,
-  .nav-tab.nav-tab--search:hover {
+  .nav-tab.nav-tab--search:hover,
+  .nav-tab.nav-tab--search.is-hover {
     background: #389ed9;
     color: white;
 
@@ -3893,6 +3894,10 @@ footer {
       opacity: 0.5;
     }
 
+    &.is-hover {
+      background-color: #f8f9fa;
+    }
+
     &.activity-entry--removeable {
       padding-right: 0;
     }

+ 32 - 0
test/browser/keyboard_shortcuts_test.rb

@@ -154,6 +154,38 @@ class KeyboardShortcutsTest < TestCase
       timeout: 6,
     )
 
+    # open online notification
+    @browser_agent = browser_instance
+    login(
+      browser:  @browser_agent,
+      username: 'agent1@example.com',
+      password: 'test',
+      url: browser_url,
+    )
+    ticket2 = ticket_create(
+      browser:  @browser_agent,
+      data: {
+        customer: 'nico',
+        group: 'Users',
+        title: 'Test Ticket for Shortcuts II - ABC123',
+        body: 'Test Ticket Body for Shortcuts II - ABC123',
+      },
+    )
+    sleep 5
+    shortcut(key: 'y')
+    watch_for(
+      css:     '.js-notificationsContainer',
+      value:   'Test Ticket for Shortcuts II',
+      timeout: 10,
+    )
+    window_keys(value: :arrow_down)
+    window_keys(value: :enter)
+    watch_for(
+      css:     '.active.content',
+      value:   ticket2[:number],
+      timeout: 2,
+    )
+
     shortcut(key: 'e')
     watch_for(
       css:     'body',

+ 22 - 22
test/browser/prefereces_test.rb

@@ -9,8 +9,8 @@ class PreferencesTest < TestCase
       password: 'test',
       url: browser_url,
     )
-    click( css: 'a[href="#current_user"]' )
-    click( css: 'a[href="#profile"]' )
+    click(css: 'a[href="#current_user"]')
+    click(css: 'a[href="#profile"]')
     match(
       css: '.content .NavBarProfile',
       value: 'Password',
@@ -36,8 +36,8 @@ class PreferencesTest < TestCase
       password: 'test',
       url: browser_url,
     )
-    click( css: 'a[href="#current_user"]' )
-    click( css: 'a[href="#profile"]' )
+    click(css: 'a[href="#current_user"]')
+    click(css: 'a[href="#profile"]')
     match(
       css: '.content .NavBarProfile',
       value: 'Password',
@@ -96,14 +96,14 @@ class PreferencesTest < TestCase
       value: 'Zammad Foundation',
     )
 
-    click( css: 'a[href="#current_user"]' )
-    click( css: 'a[href="#profile"]' )
-    click( css: 'a[href="#profile/language"]' )
+    click(css: 'a[href="#current_user"]')
+    click(css: 'a[href="#profile"]')
+    click(css: 'a[href="#profile/language"]')
     select(
       css: '.language_item [name="locale"]',
       value: 'Deutsch',
     )
-    click( css: '.content button[type="submit"]' )
+    click(css: '.content button[type="submit"]')
     watch_for(
       css: 'body',
       value: 'Sprache',
@@ -116,14 +116,14 @@ class PreferencesTest < TestCase
     )
 
     # check language in dashboard
-    click( css: '.js-menu a[href="#dashboard"]' )
+    click(css: '.js-menu a[href="#dashboard"]')
     watch_for(
       css: '.content.active',
       value: 'Meine Statistik'
     )
 
     # check language in overview
-    click( css: '.js-menu a[href="#ticket/view"]' )
+    click(css: '.js-menu a[href="#ticket/view"]')
     watch_for(
       css: '.content.active',
       value: 'Meine'
@@ -209,14 +209,14 @@ class PreferencesTest < TestCase
       value: 'notiz'
     )
 
-    click( css: 'a[href="#current_user"]' )
-    click( css: 'a[href="#profile"]' )
-    click( css: 'a[href="#profile/language"]' )
+    click(css: 'a[href="#current_user"]')
+    click(css: 'a[href="#profile"]')
+    click(css: 'a[href="#profile/language"]')
     select(
       css: '.language_item [name="locale"]',
       value: 'English (United States)',
     )
-    click( css: '.content button[type="submit"]' )
+    click(css: '.content button[type="submit"]')
     sleep 2
     watch_for(
       css: 'body',
@@ -230,14 +230,14 @@ class PreferencesTest < TestCase
     )
 
     # check language in dashboard
-    click( css: '.js-menu a[href="#dashboard"]' )
+    click(css: '.js-menu a[href="#dashboard"]')
     watch_for(
       css: '.content.active',
       value: 'My Stats'
     )
 
     # check language in overview
-    click( css: '.js-menu a[href="#ticket/view"]' )
+    click(css: '.js-menu a[href="#ticket/view"]')
     watch_for(
       css: '.content.active',
       value: 'My'
@@ -324,15 +324,15 @@ class PreferencesTest < TestCase
     )
 
     # switch to de again
-    click( css: 'a[href="#current_user"]' )
-    click( css: 'a[href="#profile"]' )
-    click( css: 'a[href="#profile/language"]' )
+    click(css: 'a[href="#current_user"]')
+    click(css: 'a[href="#profile"]')
+    click(css: 'a[href="#profile/language"]')
     sleep 4
     select(
       css: '.language_item [name="locale"]',
       value: 'Deutsch',
     )
-    click( css: '.content button[type="submit"]' )
+    click(css: '.content button[type="submit"]')
     sleep 4
     watch_for(
       css: 'body',
@@ -356,14 +356,14 @@ class PreferencesTest < TestCase
     )
 
     # check language in dashboard
-    click( css: '.js-menu a[href="#dashboard"]' )
+    click(css: '.js-menu a[href="#dashboard"]')
     watch_for(
       css: '.content.active',
       value: 'Meine Statistik'
     )
 
     # check language in overview
-    click( css: '.js-menu a[href="#ticket/view"]' )
+    click(css: '.js-menu a[href="#ticket/view"]')
     watch_for(
       css: '.content.active',
       value: 'Meine'