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