gitlab-org--gitlab-foss/app/assets/javascripts/search_autocomplete.js.coffee

298 lines
7.8 KiB
CoffeeScript
Raw Normal View History

2014-10-20 16:48:07 -04:00
class @SearchAutocomplete
KEYCODE =
ESCAPE: 27
BACKSPACE: 8
ENTER: 13
constructor: (opts = {}) ->
{
@wrap = $('.search')
@optsEl = @wrap.find('.search-autocomplete-opts')
@autocompletePath = @optsEl.data('autocomplete-path')
@projectId = @optsEl.data('autocomplete-project-id') || ''
@projectRef = @optsEl.data('autocomplete-project-ref') || ''
} = opts
# Dropdown Element
@dropdown = @wrap.find('.dropdown')
@dropdownContent = @dropdown.find('.dropdown-content')
@locationBadgeEl = @getElement('.location-badge')
2016-03-18 18:35:26 -04:00
@scopeInputEl = @getElement('#scope')
@searchInput = @getElement('.search-input')
@projectInputEl = @getElement('#search_project_id')
@groupInputEl = @getElement('#group_id')
@searchCodeInputEl = @getElement('#search_code')
@repositoryInputEl = @getElement('#repository_ref')
2016-03-21 17:00:53 -04:00
@clearInput = @getElement('.js-clear-input')
@saveOriginalState()
# Only when user is logged in
@createAutocomplete() if gon.current_user_id
@searchInput.addClass('disabled')
@saveTextLength()
@bindEvents()
2016-03-18 18:35:26 -04:00
# Finds an element inside wrapper element
getElement: (selector) ->
@wrap.find(selector)
saveOriginalState: ->
@originalState = @serializeState()
saveTextLength: ->
@lastTextLength = @searchInput.val().length
createAutocomplete: ->
@searchInput.glDropdown
filterInputBlur: false
filterable: true
filterRemote: true
highlight: true
2016-03-23 23:07:07 -04:00
enterCallback: false
filterInput: 'input#search'
search:
fields: ['text']
data: @getData.bind(@)
selectable: true
clicked: @onClick.bind(@)
getData: (term, callback) ->
_this = @
# Do not trigger request if input is empty
return if @searchInput.val() is ''
# Prevent multiple ajax calls
return if @loadingSuggestions
@loadingSuggestions = true
jqXHR = $.get(@autocompletePath, {
project_id: @projectId
project_ref: @projectRef
term: term
}, (response) ->
# Hide dropdown menu if no suggestions returns
if !response.length
_this.disableAutocomplete()
return
data = []
# List results
2016-03-24 14:32:05 -04:00
firstCategory = true
for suggestion in response
# Add group header before list each group
if lastCategory isnt suggestion.category
2016-03-24 18:13:37 -04:00
data.push 'separator' if !firstCategory
2016-03-24 14:32:05 -04:00
firstCategory = false if firstCategory
data.push
header: suggestion.category
lastCategory = suggestion.category
data.push
id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}"
category: suggestion.category
text: suggestion.label
url: suggestion.url
2016-03-27 14:27:49 -04:00
# Add option to proceed with the search
if data.length
data.push('separator')
data.push
text: "Result name contains \"#{term}\""
url: "/search?\
search=#{term}\
&project_id=#{_this.projectInputEl.val()}\
&group_id=#{_this.groupInputEl.val()}"
callback(data)
).always ->
_this.loadingSuggestions = false
serializeState: ->
{
# Search Criteria
search_project_id: @projectInputEl.val()
group_id: @groupInputEl.val()
search_code: @searchCodeInputEl.val()
repository_ref: @repositoryInputEl.val()
2016-03-21 15:01:20 -04:00
scope: @scopeInputEl.val()
# Location badge
_location: @locationBadgeEl.text()
}
bindEvents: ->
2016-04-05 20:19:00 -04:00
$(document).on 'click', @onDocumentClick
@searchInput.on 'keydown', @onSearchInputKeyDown
@searchInput.on 'keyup', @onSearchInputKeyUp
@searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus
@clearInput.on 'click', @onClearInputClick
@locationBadgeEl.on 'click', =>
@searchInput.focus()
2016-04-05 20:19:00 -04:00
onDocumentClick: (e) =>
# If clicking outside the search box
# And search input is not focused
# And we are not clicking inside a suggestion
if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length
2016-04-05 20:19:00 -04:00
@onSearchInputBlur()
enableAutocomplete: ->
# No need to enable anything if user is not logged in
return if !gon.current_user_id
unless @dropdown.hasClass('open')
_this = @
@loadingSuggestions = false
@dropdown
.addClass('open')
.trigger('shown.bs.dropdown')
@searchInput.removeClass('disabled')
onSearchInputKeyDown: =>
# Saves last length of the entered text
@saveTextLength()
onSearchInputKeyUp: (e) =>
switch e.keyCode
when KEYCODE.BACKSPACE
# when trying to remove the location badge
if @lastTextLength is 0 and @badgePresent()
@removeLocationBadge()
# When removing the last character and no badge is present
2016-03-26 17:52:16 -04:00
if @lastTextLength is 1
@disableAutocomplete()
2016-03-26 17:52:16 -04:00
# When removing any character from existin value
if @lastTextLength > 1
@enableAutocomplete()
when KEYCODE.ESCAPE
2016-03-26 17:52:16 -04:00
@restoreOriginalState()
2016-03-08 21:26:24 -05:00
else
# Handle the case when deleting the input value other than backspace
# e.g. Pressing ctrl + backspace or ctrl + x
if @searchInput.val() is ''
@disableAutocomplete()
else
# We should display the menu only when input is not empty
@enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
@wrap.toggleClass 'has-value', !!e.target.value
# Avoid falsy value to be returned
return
2016-03-23 17:36:44 -04:00
onSearchInputClick: (e) =>
# Prevents closing the dropdown menu
e.stopImmediatePropagation()
onSearchInputFocus: =>
2016-04-05 20:19:00 -04:00
@isFocused = true
@wrap.addClass('search-active')
onClearInputClick: (e) =>
2016-03-21 17:00:53 -04:00
e.preventDefault()
@searchInput.val('').focus()
2016-03-21 17:00:53 -04:00
onSearchInputBlur: (e) =>
2016-04-05 20:19:00 -04:00
@isFocused = false
@wrap.removeClass('search-active')
2016-03-21 17:00:53 -04:00
2016-04-05 20:19:00 -04:00
# If input is blank then restore state
if @searchInput.val() is ''
@restoreOriginalState()
addLocationBadge: (item) ->
category = if item.category? then "#{item.category}: " else ''
value = if item.value? then item.value else ''
badgeText = "#{category}#{value}"
@locationBadgeEl.text(badgeText).show()
2016-03-21 17:00:53 -04:00
@wrap.addClass('has-location-badge')
restoreOriginalState: ->
inputs = Object.keys @originalState
for input in inputs
2016-03-18 18:35:26 -04:00
@getElement("##{input}").val(@originalState[input])
if @originalState._location is ''
@locationBadgeEl.hide()
else
@addLocationBadge(
value: @originalState._location
)
@dropdown.removeClass 'open'
badgePresent: ->
@locationBadgeEl.length
resetSearchState: ->
2016-03-21 15:01:20 -04:00
inputs = Object.keys @originalState
2016-03-21 15:01:20 -04:00
for input in inputs
2016-03-21 15:01:20 -04:00
# _location isnt a input
break if input is '_location'
2016-03-21 15:01:20 -04:00
@getElement("##{input}").val('')
removeLocationBadge: ->
@locationBadgeEl.hide()
# Reset state
@resetSearchState()
2016-03-21 17:00:53 -04:00
@wrap.removeClass('has-location-badge')
disableAutocomplete: ->
2016-03-23 17:13:46 -04:00
@searchInput.addClass('disabled')
@dropdown.removeClass('open')
@restoreMenu()
restoreMenu: ->
html = "<ul>
<li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
</ul>"
@dropdownContent.html(html)
onClick: (item, $el, e) ->
if location.pathname.indexOf(item.url) isnt -1
e.preventDefault()
if not @badgePresent
if item.category is 'Projects'
@projectInputEl.val(item.id)
@addLocationBadge(
value: 'This project'
)
if item.category is 'Groups'
@groupInputEl.val(item.id)
@addLocationBadge(
value: 'This group'
)
$el.removeClass('is-active')
@disableAutocomplete()
@searchInput.val('').focus()