2015-04-26 12:48:37 +05:30
|
|
|
|
class @SearchAutocomplete
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
2016-06-16 23:09:34 +05:30
|
|
|
|
@locationBadgeEl = @getElement('.location-badge')
|
2016-06-02 11:05:42 +05:30
|
|
|
|
@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(@)
|
2016-08-24 12:49:21 +05:30
|
|
|
|
id: (data) -> _.escape(data.text)
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
|
|
|
|
getData: (term, callback) ->
|
|
|
|
|
_this = @
|
|
|
|
|
|
2016-06-22 15:30:34 +05:30
|
|
|
|
unless term
|
|
|
|
|
if contents = @getCategoryContents()
|
|
|
|
|
@searchInput.data('glDropdown').filter.options.callback contents
|
|
|
|
|
@enableAutocomplete()
|
|
|
|
|
|
|
|
|
|
return
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
2016-06-22 15:30:34 +05:30
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2016-06-02 11:05:42 +05:30
|
|
|
|
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
|
2016-06-16 23:09:34 +05:30
|
|
|
|
_location: @locationBadgeEl.text()
|
2016-06-02 11:05:42 +05:30
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bindEvents: ->
|
|
|
|
|
@searchInput.on 'keydown', @onSearchInputKeyDown
|
|
|
|
|
@searchInput.on 'keyup', @onSearchInputKeyUp
|
|
|
|
|
@searchInput.on 'click', @onSearchInputClick
|
|
|
|
|
@searchInput.on 'focus', @onSearchInputFocus
|
2016-08-24 12:49:21 +05:30
|
|
|
|
@searchInput.on 'blur', @onSearchInputBlur
|
2016-06-02 11:05:42 +05:30
|
|
|
|
@clearInput.on 'click', @onClearInputClick
|
2016-06-16 23:09:34 +05:30
|
|
|
|
@locationBadgeEl.on 'click', =>
|
|
|
|
|
@searchInput.focus()
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
|
|
|
|
enableAutocomplete: ->
|
|
|
|
|
# No need to enable anything if user is not logged in
|
|
|
|
|
return if !gon.current_user_id
|
|
|
|
|
|
2016-06-16 23:09:34 +05:30
|
|
|
|
unless @dropdown.hasClass('open')
|
|
|
|
|
_this = @
|
|
|
|
|
@loadingSuggestions = false
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
2016-06-16 23:09:34 +05:30
|
|
|
|
@dropdown
|
|
|
|
|
.addClass('open')
|
|
|
|
|
.trigger('shown.bs.dropdown')
|
|
|
|
|
@searchInput.removeClass('disabled')
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
|
|
|
|
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
|
2016-06-16 23:09:34 +05:30
|
|
|
|
@enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
|
|
|
|
@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')
|
|
|
|
|
|
2016-06-22 15:30:34 +05:30
|
|
|
|
@getData() if @getValue() is ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getValue: -> return @searchInput.val()
|
|
|
|
|
|
|
|
|
|
|
2016-06-02 11:05:42 +05:30
|
|
|
|
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 ''
|
|
|
|
|
|
2016-06-16 23:09:34 +05:30
|
|
|
|
badgeText = "#{category}#{value}"
|
|
|
|
|
@locationBadgeEl.text(badgeText).show()
|
2016-06-02 11:05:42 +05:30
|
|
|
|
@wrap.addClass('has-location-badge')
|
|
|
|
|
|
2016-06-22 15:30:34 +05:30
|
|
|
|
|
|
|
|
|
hasLocationBadge: -> return @wrap.is '.has-location-badge'
|
|
|
|
|
|
|
|
|
|
|
2016-06-02 11:05:42 +05:30
|
|
|
|
restoreOriginalState: ->
|
|
|
|
|
inputs = Object.keys @originalState
|
|
|
|
|
|
|
|
|
|
for input in inputs
|
|
|
|
|
@getElement("##{input}").val(@originalState[input])
|
|
|
|
|
|
|
|
|
|
if @originalState._location is ''
|
2016-06-16 23:09:34 +05:30
|
|
|
|
@locationBadgeEl.hide()
|
2016-06-02 11:05:42 +05:30
|
|
|
|
else
|
|
|
|
|
@addLocationBadge(
|
|
|
|
|
value: @originalState._location
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
badgePresent: ->
|
2016-06-16 23:09:34 +05:30
|
|
|
|
@locationBadgeEl.length
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
|
|
|
|
resetSearchState: ->
|
|
|
|
|
inputs = Object.keys @originalState
|
|
|
|
|
|
|
|
|
|
for input in inputs
|
|
|
|
|
|
|
|
|
|
# _location isnt a input
|
|
|
|
|
break if input is '_location'
|
|
|
|
|
|
|
|
|
|
@getElement("##{input}").val('')
|
|
|
|
|
|
2016-06-22 15:30:34 +05:30
|
|
|
|
|
2016-06-02 11:05:42 +05:30
|
|
|
|
removeLocationBadge: ->
|
|
|
|
|
|
2016-06-22 15:30:34 +05:30
|
|
|
|
@locationBadgeEl.hide()
|
2016-06-02 11:05:42 +05:30
|
|
|
|
@resetSearchState()
|
|
|
|
|
@wrap.removeClass('has-location-badge')
|
2016-06-22 15:30:34 +05:30
|
|
|
|
@disableAutocomplete()
|
|
|
|
|
|
2016-06-02 11:05:42 +05:30
|
|
|
|
|
|
|
|
|
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()
|