Working version of autocomplete with categorized results

This commit is contained in:
Alfredo Sumaran 2016-03-08 02:56:43 -05:00
parent bc590ce63b
commit 4b3d344688
6 changed files with 229 additions and 49 deletions

View File

@ -151,9 +151,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()

View File

@ -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("<li class='ui-autocomplete-category'>#{item.category}</li>")
currentCategory = item.category
li = @_renderItemData(ul, item)
if item.category?
li.attr('aria-label', item.category + " : " + item.label)
)

View File

@ -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 = "<span class='label label-primary'>
<i class='location-text'>#{category}#{value}</i>
<a class='remove-badge' href='#'>x</a>
</span>"
@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

View File

@ -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

View File

@ -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();
}
});

View File

@ -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