diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index f5e1ca9860d..a022c207d08 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -152,9 +152,4 @@ class Dispatcher
new Shortcuts()
initSearch: ->
- opts = $('.search-autocomplete-opts')
- path = opts.data('autocomplete-path')
- project_id = opts.data('autocomplete-project-id')
- project_ref = opts.data('autocomplete-project-ref')
-
- new SearchAutocomplete(path, project_id, project_ref)
+ new SearchAutocomplete()
diff --git a/app/assets/javascripts/lib/category_autocomplete.js.coffee b/app/assets/javascripts/lib/category_autocomplete.js.coffee
new file mode 100644
index 00000000000..490032dc782
--- /dev/null
+++ b/app/assets/javascripts/lib/category_autocomplete.js.coffee
@@ -0,0 +1,17 @@
+$.widget( "custom.catcomplete", $.ui.autocomplete,
+ _create: ->
+ @_super();
+ @widget().menu("option", "items", "> :not(.ui-autocomplete-category)")
+
+ _renderMenu: (ul, items) ->
+ currentCategory = ''
+ $.each items, (index, item) =>
+ if item.category isnt currentCategory
+ ul.append("
#{item.category}")
+ currentCategory = item.category
+
+ li = @_renderItemData(ul, item)
+
+ if item.category?
+ li.attr('aria-label', item.category + " : " + item.label)
+ )
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
index c1801365266..df31b07910c 100644
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ b/app/assets/javascripts/search_autocomplete.js.coffee
@@ -1,11 +1,164 @@
class @SearchAutocomplete
- constructor: (search_autocomplete_path, project_id, project_ref) ->
- project_id = '' unless project_id
- project_ref = '' unless project_ref
- query = "?project_id=" + project_id + "&project_ref=" + project_ref
+ 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
- $("#search").autocomplete
- source: search_autocomplete_path + query
+ @keyCode =
+ ESCAPE: 27
+ BACKSPACE: 8
+ TAB: 9
+ ENTER: 13
+
+ @locationBadgeEl = @$('.search-location-badge')
+ @locationText = @$('.location-text')
+ @searchInput = @$('.search-input')
+ @projectInputEl = @$('#project_id')
+ @groupInputEl = @$('#group_id')
+ @searchCodeInputEl = @$('#search_code')
+ @repositoryInputEl = @$('#repository_ref')
+ @scopeInputEl = @$('#scope')
+
+ @saveOriginalState()
+ @createAutocomplete()
+ @bindEvents()
+
+ $: (selector) ->
+ @wrap.find(selector)
+
+ saveOriginalState: ->
+ @originalState = @serializeState()
+
+ restoreOriginalState: ->
+ inputs = Object.keys @originalState
+
+ for input in inputs
+ @$("##{input}").val(@originalState[input])
+
+
+ if @originalState._location is ''
+ @locationBadgeEl.html('')
+ else
+ @addLocationBadge(
+ value: @originalState._location
+ )
+
+ serializeState: ->
+ {
+ # Search Criteria
+ project_id: @projectInputEl.val()
+ group_id: @groupInputEl.val()
+ search_code: @searchCodeInputEl.val()
+ repository_ref: @repositoryInputEl.val()
+
+ # Location badge
+ _location: $.trim(@locationText.text())
+ }
+
+ createAutocomplete: ->
+ @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef
+
+ @catComplete = @searchInput.catcomplete
+ appendTo: 'form.navbar-form'
+ source: @autocompletePath + @query
minLength: 1
- select: (event, ui) ->
- location.href = ui.item.url
+ close: (e) ->
+ e.preventDefault()
+
+ select: (event, ui) =>
+ # Pressing enter choses an alternative
+ if event.keyCode is @keyCode.ENTER
+ @goToResult(ui.item)
+ else
+ # Pressing tab sets the scope
+ if event.keyCode is @keyCode.TAB and ui.item.scope?
+ @setLocationBadge(ui.item)
+ @searchInput
+ .val('') # remove selected value from input
+ .focus()
+ else
+ # If option is not a scope go to page
+ @goToResult(ui.item)
+
+ # Return false to avoid focus on the next element
+ return false
+
+
+ bindEvents: ->
+ @searchInput.on 'keydown', @onSearchKeyDown
+ @wrap.on 'click', '.remove-badge', @onRemoveLocationBadgeClick
+
+ onRemoveLocationBadgeClick: (e) =>
+ e.preventDefault()
+ @removeLocationBadge()
+ @searchInput.focus()
+
+ onSearchKeyDown: (e) =>
+ # Remove tag when pressing backspace and input search is empty
+ if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is ''
+ @removeLocationBadge()
+ @destroyAutocomplete()
+ @searchInput.focus()
+ else if e.keyCode is @keyCode.ESCAPE
+ @restoreOriginalState()
+ else
+ # Create new autocomplete instance if it's not created
+ @createAutocomplete() unless @catcomplete?
+
+ addLocationBadge: (item) ->
+ category = if item.category? then "#{item.category}: " else ''
+ value = if item.value? then item.value else ''
+
+ html = "
+ #{category}#{value}
+ x
+ "
+ @locationBadgeEl.html(html)
+
+ setLocationBadge: (item) ->
+ @addLocationBadge(item)
+
+ # Reset input states
+ @resetSearchState()
+
+ switch item.scope
+ when 'projects'
+ @projectInputEl.val(item.id)
+ # @searchCodeInputEl.val('true') # TODO: always true for projects?
+ # @repositoryInputEl.val('master') # TODO: always master?
+
+ when 'groups'
+ @groupInputEl.val(item.id)
+
+ removeLocationBadge: ->
+ @locationBadgeEl.empty()
+
+ # Reset state
+ @resetSearchState()
+
+ resetSearchState: ->
+ # Remove scope
+ @scopeInputEl.val('')
+
+ # Remove group
+ @groupInputEl.val('')
+
+ # Remove project id
+ @projectInputEl.val('')
+
+ # Remove code search
+ @searchCodeInputEl.val('')
+
+ # Remove repository ref
+ @repositoryInputEl.val('')
+
+ goToResult: (result) ->
+ location.href = result.url
+
+ destroyAutocomplete: ->
+ @catComplete.destroy() if @catcomplete?
+ @catComplete = null
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 494dad0b41e..9102fd6d501 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -23,45 +23,45 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
- { label: "Profile settings", url: profile_path },
- { label: "SSH Keys", url: profile_keys_path },
- { label: "Dashboard", url: root_path },
- { label: "Admin Section", url: admin_root_path },
+ { category: "Settings", label: "Profile settings", url: profile_path },
+ { category: "Settings", label: "SSH Keys", url: profile_keys_path },
+ { category: "Settings", label: "Dashboard", url: root_path },
+ { category: "Settings", label: "Admin Section", url: admin_root_path },
]
end
# Autocomplete results for internal help pages
def help_autocomplete
[
- { label: "help: API Help", url: help_page_path("api", "README") },
- { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") },
- { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") },
- { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") },
- { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") },
- { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") },
- { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
- { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
- { label: "help: Workflow Help", url: help_page_path("workflow", "README") },
+ { category: "Help", label: "API Help", url: help_page_path("api", "README") },
+ { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") },
+ { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") },
+ { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") },
+ { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") },
+ { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") },
+ { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
+ { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
+ { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") },
]
end
# Autocomplete results for the current project, if it's defined
def project_autocomplete
if @project && @project.repository.exists? && @project.repository.root_ref
- prefix = search_result_sanitize(@project.name_with_namespace)
+ prefix = "Project - " + search_result_sanitize(@project.name_with_namespace)
ref = @ref || @project.repository.root_ref
[
- { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
- { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) },
- { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
- { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
- { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) },
- { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
+ { category: prefix, label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) },
+ { category: prefix, label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) },
+ { category: prefix, label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) },
+ { category: prefix, label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) },
+ { category: prefix, label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) },
+ { category: prefix, label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
+ { category: prefix, label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
+ { category: prefix, label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
+ { category: prefix, label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
+ { category: prefix, label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
[]
@@ -72,7 +72,10 @@ module SearchHelper
def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.search(term).limit(limit).map do |group|
{
- label: "group: #{search_result_sanitize(group.name)}",
+ category: "Groups",
+ scope: "groups",
+ id: group.id,
+ label: "#{search_result_sanitize(group.name)}",
url: group_path(group)
}
end
@@ -83,7 +86,11 @@ module SearchHelper
current_user.authorized_projects.search_by_title(term).
sorted_by_stars.non_archived.limit(limit).map do |p|
{
- label: "project: #{search_result_sanitize(p.name_with_namespace)}",
+ category: "Projects",
+ scope: "projects",
+ id: p.id,
+ value: "#{search_result_sanitize(p.name)}",
+ label: "#{search_result_sanitize(p.name_with_namespace)}",
url: namespace_project_path(p.namespace, p)
}
end
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 54af2c3063c..c5002893831 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,10 +1,12 @@
.search
= form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f|
+ = render 'shared/location_badge'
= search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1"
= hidden_field_tag :group_id, @group.try(:id)
- - if @project && @project.persisted?
- = hidden_field_tag :project_id, @project.id
+ = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : ''
+
+ - if @project && @project.persisted?
- if current_controller?(:issues)
= hidden_field_tag :scope, 'issues'
- elsif current_controller?(:merge_requests)
@@ -21,10 +23,3 @@
= hidden_field_tag :repository_ref, @ref
= button_tag 'Go' if ENV['RAILS_ENV'] == 'test'
.search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
-
-:javascript
- $('.search-input').on('keyup', function(e) {
- if (e.keyCode == 27) {
- $('.search-input').blur();
- }
- });
diff --git a/app/views/shared/_location_badge.html.haml b/app/views/shared/_location_badge.html.haml
new file mode 100644
index 00000000000..dfe8bc010d6
--- /dev/null
+++ b/app/views/shared/_location_badge.html.haml
@@ -0,0 +1,13 @@
+- if controller.controller_path =~ /^groups/
+ - label = 'This group'
+- if controller.controller_path =~ /^projects/
+ - label = 'This project'
+
+.search-location-badge
+ - if label.present?
+ %span.label.label-primary
+ %i.location-text
+ = label
+
+ %a.remove-badge{href: '#'}
+ x