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
This commit is contained in:
Felix Niklas 2018-07-28 12:15:30 +02:00
parent dc884fc604
commit 2639df5a97
6 changed files with 152 additions and 49 deletions

View file

@ -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 = []) =>
# remove result if not result exists
if _.isEmpty(result)
@searchContainer.removeClass('open')
@globalSearch.close()
@searchResult.html('')
renderResult: (result = [], noChange) =>
if noChange
return
# show no results placeholder if no result exists
if _.isEmpty(result)
@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
href = @$('.global-search-result .nav-tab.is-hover').attr('href')
return if !href
@navigate(href)
@emptyAndClose()
@searchInput.blur()
href = @$('.global-search-result .nav-tab.is-hover').attr('href')
if href
@navigate(href)
else
@openExtendedSearch()
@emptyAndClose()
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: =>

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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%);