343 lines
9 KiB
CoffeeScript
343 lines
9 KiB
CoffeeScript
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')
|
||
@scopeInputEl = @getElement('#scope')
|
||
@searchInput = @getElement('.search-input')
|
||
@projectInputEl = @getElement('#search_project_id')
|
||
@groupInputEl = @getElement('#group_id')
|
||
@searchCodeInputEl = @getElement('#search_code')
|
||
@repositoryInputEl = @getElement('#repository_ref')
|
||
@clearInput = @getElement('.js-clear-input')
|
||
|
||
@saveOriginalState()
|
||
|
||
# Only when user is logged in
|
||
@createAutocomplete() if gon.current_user_id
|
||
|
||
@searchInput.addClass('disabled')
|
||
|
||
@saveTextLength()
|
||
|
||
@bindEvents()
|
||
|
||
# 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
|
||
enterCallback: false
|
||
filterInput: 'input#search'
|
||
search:
|
||
fields: ['text']
|
||
data: @getData.bind(@)
|
||
selectable: true
|
||
clicked: @onClick.bind(@)
|
||
|
||
getData: (term, callback) ->
|
||
_this = @
|
||
|
||
unless term
|
||
if contents = @getCategoryContents()
|
||
@searchInput.data('glDropdown').filter.options.callback contents
|
||
@enableAutocomplete()
|
||
|
||
return
|
||
|
||
# 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
|
||
firstCategory = true
|
||
for suggestion in response
|
||
|
||
# Add group header before list each group
|
||
if lastCategory isnt suggestion.category
|
||
data.push 'separator' if !firstCategory
|
||
|
||
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
|
||
|
||
# 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
|
||
|
||
|
||
getCategoryContents: ->
|
||
|
||
userId = gon.current_user_id
|
||
{ utils, projectOptions, groupOptions, dashboardOptions } = gl
|
||
|
||
if utils.isInGroupsPage() and groupOptions
|
||
options = groupOptions[utils.getGroupSlug()]
|
||
|
||
else if utils.isInProjectPage() and projectOptions
|
||
options = projectOptions[utils.getProjectSlug()]
|
||
|
||
else if dashboardOptions
|
||
options = dashboardOptions
|
||
|
||
{ issuesPath, mrPath, name } = options
|
||
|
||
items = [
|
||
{ header: "#{name}" }
|
||
{ text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" }
|
||
{ text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" }
|
||
'separator'
|
||
{ text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" }
|
||
{ text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" }
|
||
]
|
||
|
||
items.splice 0, 1 unless name
|
||
|
||
return items
|
||
|
||
|
||
serializeState: ->
|
||
{
|
||
# Search Criteria
|
||
search_project_id: @projectInputEl.val()
|
||
group_id: @groupInputEl.val()
|
||
search_code: @searchCodeInputEl.val()
|
||
repository_ref: @repositoryInputEl.val()
|
||
scope: @scopeInputEl.val()
|
||
|
||
# Location badge
|
||
_location: @locationBadgeEl.text()
|
||
}
|
||
|
||
bindEvents: ->
|
||
$(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()
|
||
|
||
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
|
||
@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
|
||
if @lastTextLength is 1
|
||
@disableAutocomplete()
|
||
|
||
# When removing any character from existin value
|
||
if @lastTextLength > 1
|
||
@enableAutocomplete()
|
||
|
||
when KEYCODE.ESCAPE
|
||
@restoreOriginalState()
|
||
|
||
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
|
||
|
||
onSearchInputClick: (e) =>
|
||
# Prevents closing the dropdown menu
|
||
e.stopImmediatePropagation()
|
||
|
||
onSearchInputFocus: =>
|
||
@isFocused = true
|
||
@wrap.addClass('search-active')
|
||
|
||
@getData() if @getValue() is ''
|
||
|
||
|
||
getValue: -> return @searchInput.val()
|
||
|
||
|
||
onClearInputClick: (e) =>
|
||
e.preventDefault()
|
||
@searchInput.val('').focus()
|
||
|
||
onSearchInputBlur: (e) =>
|
||
@isFocused = false
|
||
@wrap.removeClass('search-active')
|
||
|
||
# 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()
|
||
@wrap.addClass('has-location-badge')
|
||
|
||
|
||
hasLocationBadge: -> return @wrap.is '.has-location-badge'
|
||
|
||
|
||
restoreOriginalState: ->
|
||
inputs = Object.keys @originalState
|
||
|
||
for input in inputs
|
||
@getElement("##{input}").val(@originalState[input])
|
||
|
||
if @originalState._location is ''
|
||
@locationBadgeEl.hide()
|
||
else
|
||
@addLocationBadge(
|
||
value: @originalState._location
|
||
)
|
||
|
||
@dropdown.removeClass 'open'
|
||
|
||
badgePresent: ->
|
||
@locationBadgeEl.length
|
||
|
||
resetSearchState: ->
|
||
inputs = Object.keys @originalState
|
||
|
||
for input in inputs
|
||
|
||
# _location isnt a input
|
||
break if input is '_location'
|
||
|
||
@getElement("##{input}").val('')
|
||
|
||
|
||
removeLocationBadge: ->
|
||
|
||
@locationBadgeEl.hide()
|
||
@resetSearchState()
|
||
@wrap.removeClass('has-location-badge')
|
||
@disableAutocomplete()
|
||
|
||
|
||
disableAutocomplete: ->
|
||
@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()
|