Browse Source

Search: loading indicator, no result screen, extended search on enter

- open extended search on enter, solves #560
- show searching indicator, requested in https://community.zammad.org/t/delay-after-querying-a-search/405
- show no result screen, solves #788
- fix bug: when on ‘#search’, ‘show search results’ in the search sidebar was highlighted as active too because it links to #search
- clear search when entering a search result
Felix Niklas 6 years ago
parent
commit
2639df5a97

+ 33 - 30
app/assets/javascripts/app/controllers/navigation.coffee

@@ -15,7 +15,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
     'focus #global-search': 'searchFocus'
     'blur #global-search': 'searchBlur'
     'keyup #global-search': 'listNavigate'
-    'click .js-global-search-result': 'andClose'
+    'click .js-global-search-result': 'emptyAndClose'
     'click .js-details-link': 'openExtendedSearch'
     'change .js-menu .js-switch input': 'switch'
 
@@ -156,31 +156,31 @@ class App.Navigation extends App.ControllerWidgetPermanent
         type:      'personal'
       )
 
-  renderResult: (result = []) =>
+  renderResult: (result = [], noChange) =>
+    if noChange
+      return
 
-    # remove result if not result exists
+    # show no results placeholder if no result exists
     if _.isEmpty(result)
-      @searchContainer.removeClass('open')
-      @globalSearch.close()
-      @searchResult.html('')
+      @searchContainer.addClass('no-match')
+      @searchResult.html(App.view('navigation/no_result')())
       return
 
+    @searchContainer.removeClass('no-match')
+
     # build markup
     html = App.view('navigation/result')(
       result: result
     )
     @searchResult.html(html)
 
-    # show result list
-    @searchContainer.addClass('open')
-
     # start ticket popups
     @ticketPopups()
 
     # start user popups
     @userPopups()
 
-    # start oorganization popups
+    # start organization popups
     @organizationPopups()
 
   render: ->
@@ -205,26 +205,22 @@ class App.Navigation extends App.ControllerWidgetPermanent
 
   searchFocus: (e) =>
     @query = '' # reset query cache
-    @searchContainer.addClass('focused')
     @anyPopoversDestroy()
-    @search()
+    @searchContainer.addClass('focused')
 
   searchBlur: (e) =>
-
     # delay to be able to click x
     update = =>
       query = @searchInput.val().trim()
       if !query
         @emptyAndClose()
         return
-      @searchContainer.removeClass('focused')
 
     @delay(update, 100, 'removeFocused')
 
   listNavigate: (e) =>
     if e.keyCode is 27 # close on esc
       @emptyAndClose()
-      @searchInput.blur()
       return
     else if e.keyCode is 38 # up
       @nudge(e, -1)
@@ -233,14 +229,13 @@ class App.Navigation extends App.ControllerWidgetPermanent
       @nudge(e, 1)
       return
     else if e.keyCode is 13 # enter
-      if @$('.global-search-menu .js-details-link.is-hover').get(0)
-        @openExtendedSearch()
-        return
+      @searchInput.blur()
       href = @$('.global-search-result .nav-tab.is-hover').attr('href')
-      return if !href
-      @navigate(href)
+      if href
+        @navigate(href)
+      else
+        @openExtendedSearch()
       @emptyAndClose()
-      @searchInput.blur()
       return
 
     # on other keys, show result
@@ -284,25 +279,33 @@ class App.Navigation extends App.ControllerWidgetPermanent
       @scrollToIfNeeded(prev, false)
 
   emptyAndClose: =>
+    @query = ''
     @searchInput.val('')
-    @searchContainer.removeClass('filled').removeClass('open').removeClass('focused')
+    @searchContainer.removeClass('focused filled open no-match')
     @globalSearch.close()
-
-    # remove not needed popovers
     @delay(@anyPopoversDestroy, 100, 'removePopovers')
 
   andClose: =>
+    @query = ''
     @searchInput.blur()
-    @searchContainer.removeClass('open')
+    @searchContainer.removeClass('open no-match')
     @globalSearch.close()
     @delay(@anyPopoversDestroy, 100, 'removePopovers')
 
   search: =>
     query = @searchInput.val().trim()
-    return if !query
-    return if query is @query
+    @searchContainer.toggleClass('filled', !!query)
+    # if we started a new search and already typed something in
+    if @query == '' and query != ''
+      @searchContainer.addClass('open no-match')
+      @searchResult.html(App.view('navigation/search_placeholder')())
+
     @query = query
-    @searchContainer.toggleClass('filled', !!@query)
+
+    if @query == ''
+      @searchContainer.removeClass('open')
+      return
+
     @globalSearch.search(query: @query)
 
   filterNavbar: (values, user, parent = null) ->
@@ -402,11 +405,11 @@ class App.Navigation extends App.ControllerWidgetPermanent
       url = params.url
       type = params.type
     if type is 'menu'
-      @$('.js-menu .is-active, .js-details-link.is-active').removeClass('is-active')
+      @$('.js-menu .is-active').removeClass('is-active')
     else
       @$('.is-active').removeClass('is-active')
     return if !url || url is '#'
-    @$("[href=\"#{url}\"]").addClass('is-active')
+    @$(".js-menu [href=\"#{url}\"], .tasks [href=\"#{url}\"]").addClass('is-active')
 
   recentViewNavbarItemsRebuild: =>
 

+ 2 - 1
app/assets/javascripts/app/controllers/search.coffee

@@ -125,7 +125,8 @@ class App.Search extends App.Controller
 
     @globalSearch.search(query: @query)
 
-  renderResult: (result = []) =>
+  renderResult: (result = [], noChange) =>
+    return if noChange
     @result = result
     for tab in @tabs
       count = 0

+ 4 - 2
app/assets/javascripts/app/lib/app_post/global_search.coffee

@@ -45,10 +45,12 @@ class App.GlobalSearch extends App.Controller
   renderTry: (result, query) =>
 
     # if result hasn't changed, do not rerender
-    diff = false
     if @lastQuery is query && @searchResultCache[query]
       diff = difference(@searchResultCache[query].result, result)
-    return if diff isnt false && _.isEmpty(diff)
+      if _.isEmpty(diff)
+        @render(result, true)
+        return
+
     @lastQuery = query
 
     # cache search result

+ 4 - 0
app/assets/javascripts/app/views/navigation/no_result.jst.eco

@@ -0,0 +1,4 @@
+<li class="global-search-detail-no-result alert alert--warning horizontal" role="alert">
+  <%- @Icon('mood-sad') %>
+  <span><%= @T('There is no match for your search.') %></span>
+</li>

+ 24 - 0
app/assets/javascripts/app/views/navigation/search_placeholder.jst.eco

@@ -0,0 +1,24 @@
+<li class="global-search-detail-placeholder">
+  <a class="nav-tab nav-tab--search">
+    <div class="nav-tab-icon">
+    	<%- @Icon('task-state') %>
+    </div>
+    <span class="nav-tab-name"></span>
+  </a>
+</li>
+<li class="global-search-detail-placeholder">
+  <a class="nav-tab nav-tab--search">
+    <div class="nav-tab-icon">
+    	<%- @Icon('task-state') %>
+    </div>
+    <span class="nav-tab-name"></span>
+  </a>
+</li>
+<li class="global-search-detail-placeholder">
+  <a class="nav-tab nav-tab--search">
+    <div class="nav-tab-icon">
+    	<%- @Icon('task-state') %>
+    </div>
+    <span class="nav-tab-name"></span>
+  </a>
+</li>

+ 82 - 13
app/assets/stylesheets/zammad.scss

@@ -1962,7 +1962,6 @@ input.has-error {
     border-radius: 19px;
     padding: 0 17px 0 42px;
     @include rtl(padding, 0 42px 0 17px);
-    will-change: transform;
 
     &.is-empty + .empty-search {
       visibility: hidden;
@@ -3279,10 +3278,6 @@ footer {
   .nav-tab.nav-tab--search.is-hover {
     background: #389ed9;
     color: white;
-
-    .nav-tab-icon .icon {
-      fill: white;
-    }
   }
 
   .nav-tab.ui-sortable-helper {
@@ -3306,7 +3301,7 @@ footer {
   .nav-tab-icon .icon {
     max-width: 18px;
     max-height: 18px;
-    fill: #808080;
+    fill: currentColor;
   }
 
   .nav-tab-icon .icon-diagonal-cross {
@@ -3432,7 +3427,8 @@ footer {
     flex: 1;
     border-radius: 15px;
     position: relative;
-    transition: 240ms;
+    transition: margin-right 120ms;
+    will-change: margin-right;
   }
 
   .empty-search {
@@ -3442,16 +3438,23 @@ footer {
     height: 30px;
     width: 40px;
     z-index: 1;
-    visibility: hidden;
     display: flex;
     align-items: center;
     justify-content: center;
     @extend %clickable;
-  }
+    visibility: hidden;
+    cursor: pointer;
+    
+    &:hover {
+      .icon {
+        opacity: 1;
+      }
+    }
 
-  .search .empty-search .icon-diagonal-cross {
-    fill: white;
-    opacity: 0.5;
+    .icon {
+      fill: white;
+      opacity: 0.5;
+    }
   }
 
     .filled.search .empty-search {
@@ -3489,7 +3492,8 @@ footer {
   }
 
   .search.focused .search-holder {
-    @include bidi-style(margin-right, -46px, margin-left, 0);
+    transition: margin-right 240ms;
+    @include bidi-style(margin-right, -59px, margin-left, 0);
   }
 
   .search.focused .logo {
@@ -3555,6 +3559,10 @@ footer {
     padding: 9px 15px 8px 0;
     margin-bottom: 7px;
     height: auto !important;
+    
+    .no-match & {
+      display: none;
+    }
 
     .nav-tab-icon {
       width: 18px;
@@ -3582,6 +3590,62 @@ footer {
     list-style: none;
   }
 
+  .global-search-detail-no-result {
+    margin: 0 10px;
+    
+    .icon {
+      width: 30px;
+      height: 29px;
+    }
+  }
+
+  .global-search-detail-placeholder {
+    pointer-events: none;
+    color: inherit;
+    
+    .nav-tab {
+      animation: fade-in 4s forwards;
+    }
+    
+    .nav-tab-name {
+      background: linear-gradient(to right, hsl(0,0%,50%) 50%, hsl(0,0%,70%));
+      background-size: 200% 100%;
+      height: 10px;
+      width: 77%;
+      animation: placeholder-background 1.4s infinite;
+    }
+
+    &:nth-child(2) {
+      .nav-tab-name {
+        width: 54%;
+      }
+    }
+
+    &:nth-child(3) {
+      .nav-tab-name {
+        width: 68%;
+      }
+    }
+  }
+
+  @keyframes fade-in {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+
+  @keyframes placeholder-background {
+    from {
+      background-position: 0% 0%;
+    }
+    to {
+      background-position: -200% 0%;
+    }
+  }
+
   .user-menu {
     padding: 0;
     margin: 0;
@@ -5843,6 +5907,11 @@ footer {
   border-radius: 3px;
   color: white;
   border: none;
+  
+  .icon {
+    margin-right: 10px;
+    fill: currentColor;
+  }
 
   &.alert--info {
     background: hsl(203,65%,55%);