Skip to content

Commit

Permalink
Use auto-complete-element for exhibit search
Browse files Browse the repository at this point in the history
  • Loading branch information
taylor-steve committed Nov 25, 2024
1 parent 0ee6298 commit 377e3f9
Show file tree
Hide file tree
Showing 18 changed files with 179 additions and 132 deletions.
84 changes: 16 additions & 68 deletions app/assets/javascripts/exhibit_search_typeahead.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,88 +8,36 @@
ExhibitSearchTypeahead = {
form: null,
typeaheadElement: null,
typeaheadRemoteUrl: null,
typeaheadOptions: { hint: true, highlight: true, minLength: 1 },
typeaheadElementInput: null,

init: function (el) {
var _this = this;
var container = el.parents('.exhibit-search-typeahead');
_this.typeaheadElement = el;
_this.form = el.parents('form');
const separator = el.data().typeaheadRemoteUrl.includes('?') ? '&' : '?';
_this.typeaheadRemoteUrl = el.data().typeaheadRemoteUrl + `${separator}q=%QUERY`;
_this.typeaheadElement = el.closest('auto-complete');
_this.typeaheadElementInput = el;
_this.form = el.closest('form');
_this.preventFormSubmitOnTypeahead();

// Cleanup typeahead if it alread exists (e.g. back-button cache)
if (el.parent().hasClass('twitter-typeahead')) {
el.typeahead('destroy');
_this.typeaheadElement.addEventListener('auto-complete-change', function(e) {
const slug = e.relatedTarget.value;

el.attr('disabled', false);
el.attr('style', '');
el.removeClass('tt-hint');
if (!slug) return;

container.html(el);
}

el.typeahead(_this.typeaheadOptions, _this.typeaheadSources());

container.find('.tt-dropdown-menu').attr('aria-live', 'assertive');

el.bind('typeahead:selected', function(e, suggestion) {
window.location = '/' + suggestion.slug;
e.relatedTarget.value = _this.getTitleFromSlug(slug);
window.location = '/' + slug;
});
},

preventFormSubmitOnTypeahead: function() {
var _this = this;

_this.form.on('submit', function(e) {
if(_this.typeaheadElement.is(':focus')) {
this.form.addEventListener('submit', (e) => {
if (document.activeElement === this.typeaheadElementInput) {
e.preventDefault();
}
});
},

typeaheadSources: function() {
var bloodhound = this.bloodhoundEngine();
bloodhound.initialize();
return {
name: 'exhibit',
displayKey: 'title',
source: bloodhound.ttAdapter(),
templates: {
empty: [
'<div class="no-items">',
'No matches found',
'</div>'
].join('\n'),
suggestion: function(suggestion) {
return `<div class="exhibit-result">${suggestion.title} <span class="subtitle">${suggestion.subtitle ?? "" }</span></div>`;
}
}
};
},

bloodhoundEngine: function() {
return new Bloodhound({
limit: 5,
remote: {
url: this.typeaheadRemoteUrl,
filter: function (documents) {
return $.map(documents, function (document) {
return {
title: document.title,
subtitle: document.subtitle,
slug: document.slug
};
});
}
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d);
}
});
getTitleFromSlug: function(slug) {
const option = this.typeaheadElement.querySelector(`[data-autocomplete-value="${slug}"][role="option"]`);
return option.dataset.autocompleteTitle;
}
};

Expand All @@ -99,7 +47,7 @@
Blacklight.onLoad(function () {
'use strict';

$('[data-behavior="exhibit-search-typeahead"]').each(function (i, element) {
ExhibitSearchTypeahead.init($(element)); // eslint-disable-line no-undef
document.querySelectorAll('[data-behavior="exhibit-search-typeahead"]').forEach((element) => {
ExhibitSearchTypeahead.init(element);
});
});
64 changes: 16 additions & 48 deletions app/assets/javascripts/index_status_typeahead.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,44 @@

IndexStatusTypeahead = {
itemStatusRemoteUrl: null,
typeaheadRemoteUrl: null,
typeaheadOptions: { hint: true, highlight: true, minLength: 1 },

indexStatusTable: function() {
return $('[data-behavior="index-status-typeahead-table"]');
},

init: function (el) {
var _this = this;
_this.itemStatusRemoteUrl = el.data().typeaheadRemoteUrl;
const separator = el.data().typeaheadRemoteUrl.includes('?') ? '&' : '?';
_this.typeaheadRemoteUrl = el.data().typeaheadRemoteUrl + `${separator}q=%QUERY`;
el.typeahead(_this.typeaheadOptions, _this.typeaheadSources());
const completer = el.closest('auto-complete');
_this.itemStatusRemoteUrl = completer.dataset.typeaheadRemoteUrl;

el.bind('typeahead:selected', function(e, suggestion) {
_this.addIndexStatusRow(suggestion);
completer.addEventListener('submit', function(e) {
e.preventDefault();
});
},

typeaheadSources: function() {
var bloodhound = this.bloodhoundEngine();
bloodhound.initialize();
return {
name: 'druid',
displayKey: 'druid',
source: bloodhound.ttAdapter(),
templates: {
empty: [
'<div class="no-items">',
'No matches found',
'</div>'
].join('\n')
completer.addEventListener('auto-complete-change', function(e) {
const option = completer.querySelector(`[data-autocomplete-value="${e.relatedTarget.value}"][role="option"]`);
if (option) {
_this.addIndexStatusRow(option.dataset.autocompleteValue);
}
};
});
},

addIndexStatusRow: function(suggestion) {
if(this.indexStatusRow(suggestion.druid).length > 0) {
addIndexStatusRow: function(druid) {
if(this.indexStatusRow(druid).length > 0) {
return; // Return if there is already an index status row present
}

this.indexStatusTable().show(); // Ensure the table is shown
this.indexStatusTable().find('tbody').append(
[
'<tr data-index-status-id="' + suggestion.druid + '">',
'<td>' + suggestion.druid + '</td>',
'<tr data-index-status-id="' + druid + '">',
'<td>' + druid + '</td>',
'<td data-behavior="index-item-status"></td>',
'</tr>'
].join('\n')
);

this.updateItemIndexStatus(suggestion.druid);
this.updateItemIndexStatus(druid);
},

// Getter for an index status row given a druid
Expand Down Expand Up @@ -86,24 +72,6 @@
druids: function() {
return $('[data-index-status-content]').data('index-status-content');
},

bloodhoundEngine: function() {
return new Bloodhound({
limit: 10,
remote: {
url: this.typeaheadRemoteUrl,
filter: function (druids) {
return $.map(druids, function (druid) {
return { druid: druid };
});
}
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d);
}
});
}
};

global.IndexStatusTypeahead = IndexStatusTypeahead;
Expand All @@ -112,7 +80,7 @@
Blacklight.onLoad(function () {
'use strict';

$('[data-behavior="index-status-typeahead"]').each(function (i, element) {
IndexStatusTypeahead.init($(element)); // eslint-disable-line no-undef
document.querySelectorAll('[data-behavior="index-status-typeahead"]').forEach((element) => {
IndexStatusTypeahead.init(element); // eslint-disable-line no-undef
});
});
6 changes: 6 additions & 0 deletions app/assets/stylesheets/modules/admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
}
}

#external_resources_form {
.accordion > .card {
overflow: visible;
}
}

.form-group.sdr-form {
margin-left: 10px;
}
Expand Down
6 changes: 6 additions & 0 deletions app/assets/stylesheets/modules/exhibit_search_typeahead.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.exhibit-search-typeahead {
z-index: 2;

.tt-input {
// Override the transparent background added inline
// by twitter-typeahead
Expand Down Expand Up @@ -27,4 +29,8 @@
padding: 0 15px 8px;
text-transform: none;
}

[role="option"][aria-disabled="true"]:hover {
background-color: inherit;
}
}
6 changes: 5 additions & 1 deletion app/components/site_search_form_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
</div>

<div class="exhibit-search-typeahead site-search-type" data-behavior="site-search-type" id="exhibit-search" style="display: none;">
<input type="text" class="form-control" role="combobox" aria-labelledby="site-search-type" data-behavior="exhibit-search-typeahead" data-typeahead-remote-url="<%= exhibit_finder_index_path %>" />
<auto-complete src="<%= exhibit_autocomplete_index_path %>" for="exhibit-search-popup">
<input type="text" class="form-control tt-input" role="combobox" aria-labelledby="site-search-type" name="exhibit-search" data-behavior="exhibit-search-typeahead" />
<ul id="exhibit-search-popup" class="tt-dropdown-menu"></ul>
<div id="exhibit-search-popup-feedback" class="sr-only visually-hidden"></div>
</auto-complete>
</div>

<div id="item-search" class="site-search-type" data-behavior="site-search-type">
Expand Down
11 changes: 11 additions & 0 deletions app/controllers/exhibit_autocomplete_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

##
# Provide the GitHub auto-complete-element response for exhibit autocomplete
class ExhibitAutocompleteController < ApplicationController
# /exhibit_autocomplete
def index
@exhibits = ExhibitFinder.search(params[:q]).as_json
render layout: false
end
end
5 changes: 5 additions & 0 deletions app/controllers/index_statuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def index
render json: filtered_solr_document_ids
end

def autocomplete
@suggestions = filtered_solr_document_ids
render layout: false
end

private

def build_resource
Expand Down
6 changes: 6 additions & 0 deletions app/helpers/search_across_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ def render_exhibit_title_facet(value)
exhibit_metadata.slice(*value).values.map { |x| x['title'] || x['slug'] }.join(', ')
end

def highlight_autocomplete_suggestion(value)
return value if params[:q].blank? || value.blank?

sanitize(value.gsub(/(#{Regexp.escape(params[:q])})/i, '<strong>\1</strong>'))
end

private

def exhibit_slugs
Expand Down
11 changes: 10 additions & 1 deletion app/views/dor_harvester/_many_index_statuses.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
<p><%= t('admin.items.many-object-druids-help_html', count: harvester.solr_document_sidecars.count) %></p>

<%= bootstrap_form_for('object', layout: :horizontal, label_col: 'col-md-2', control_col: 'col-md-6') do |f| %>
<%= f.text_field :druid, data: { behavior: 'index-status-typeahead' , typeahead_remote_url: exhibit_dor_harvester_index_statuses_path(current_exhibit)}%>
<div class="form-group row">
<%= f.label :druid, for: 'object_druid', class: 'col-form-label col-md-2'%>
<div class="col-md-6">
<auto-complete src="<%= autocomplete_exhibit_dor_harvester_index_statuses_path(current_exhibit) %>" for="exhibit-status-popup" data-typeahead-remote-url="<%= exhibit_dor_harvester_index_statuses_path(current_exhibit) %>">
<%= f.text_field :druid, wrapper: false, class: 'form-control tt-input', role: 'combobox', 'aria-labelledby': 'site-search-type', name: 'exhibit-status', data: { behavior: 'index-status-typeahead' } %>
<ul id="exhibit-status-popup" class="tt-dropdown-menu"></ul>
<div id="exhibit-status-popup-feedback" class="sr-only visually-hidden"></div>
</auto-complete>
</div>
</div>
<% end %>

<table style="display: none;" class="table table-striped" data-behavior="index-status-typeahead-table">
Expand Down
12 changes: 12 additions & 0 deletions app/views/exhibit_autocomplete/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<% if @exhibits.empty? %>
<li class="exhibit-result no-items" role="option" aria-disabled="true">No matches found</li>
<% else %>
<% @exhibits.each do |exhibit| %>
<li class="exhibit-result tt-suggestion" role="option" data-autocomplete-value="<%= exhibit['slug'] %>" data-autocomplete-title="<%= exhibit['title'] %>">
<%= highlight_autocomplete_suggestion(exhibit['title']) %>
<% if exhibit['subtitle'].present? %>
<span class="subtitle"><%= highlight_autocomplete_suggestion(exhibit['subtitle']) %></span>
<% end %>
</li>
<% end %>
<% end %>
9 changes: 9 additions & 0 deletions app/views/index_statuses/autocomplete.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<% if @suggestions.empty? %>
<li class="suggestion-result no-items" role="option" aria-disabled="true">No matches found</li>
<% else %>
<% @suggestions.each do |suggestion| %>
<li class="suggestion-result tt-suggestion" data-autocomplete-value="<%= suggestion %>" role="option">
<%= highlight_autocomplete_suggestion(suggestion) %>
</li>
<% end %>
<% end %>
1 change: 1 addition & 0 deletions app/views/layouts/spotlight/base.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<link href="<%= spotlight.opensearch_exhibit_catalog_url(current_exhibit, format: 'xml') %>" title="<%= application_name %>" type="application/opensearchdescription+xml" rel="search"/>
<% end %>
<%= favicon_link_tag 'favicon.ico' %>
<script src="https://cdn.skypack.dev/@github/auto-complete-element" type="module"></script>
<% if current_exhibit %>
<%= exhibit_stylesheet_link_tag "application" %>
<% else %>
Expand Down
7 changes: 6 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
end

resources :exhibit_finder, only: %i[show index]
resources :exhibit_autocomplete, only: [:index]

scope '(:locale)', locale: Regexp.union(Spotlight::Engine.config.i18n_locales.keys.map(&:to_s)), defaults: { locale: nil } do
mount Blacklight::Oembed::Engine, at: 'oembed'
Expand Down Expand Up @@ -58,7 +59,11 @@

resources :exhibits, path: '/', only: [] do
resource :dor_harvester, controller: :"dor_harvester", only: [:create, :update] do
resources :index_statuses, only: [:index, :show]
resources :index_statuses, only: [:index, :show] do
collection do
get :autocomplete
end
end
end
resource :bibliography_resources, only: [:create, :update]
resource :viewers, only: [:create, :edit, :update]
Expand Down
4 changes: 2 additions & 2 deletions spec/features/adding_items_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@

expect(page).to have_content("There are #{number_of_resources} object druids indexed in this exhibit")

fill_in_typeahead_field attribute: "data-behavior='index-status-typeahead'", with: 'abc1'
fill_in_typeahead_field type: 'index-status', with: 'abc1'

expect(page).to have_css('tr[data-index-status-id="abc1"] td', text: 'abc1', visible: :visible)
expect(page).to have_css('td[data-behavior="index-item-status"]', text: 'Published')
Expand All @@ -79,7 +79,7 @@
within '#status-accordion' do
click_button 'Object druids'

fill_in_typeahead_field attribute: "data-behavior='index-status-typeahead'", with: 'xyz'
fill_in_typeahead_field type: 'index-status', with: 'xyz'

expect(page).to have_css('tr.danger[data-index-status-id="xyz"] td', text: 'xyz', visible: :visible)
expect(page).to have_css('td[data-behavior="index-item-status"]', text: 'There was a problem indexing')
Expand Down
Loading

0 comments on commit 377e3f9

Please sign in to comment.