Bug 27200: (follow-up) Browse search interface update

This patch updates the browse search interface to confirm to new
interface patterns in the Bootstrap 4 framework, especially in the form
markup.

The results list has been changed to a Bootstrap "Collapse" component
configured as an accordion
(https://getbootstrap.com/docs/4.5/components/collapse/#accordion-example).

Instead of loading the bibliographic record results below the list of
terms returned, the bibliographic results are now displayed in the
"panel" expanded below the selected term. Subtitle has been added to the
information displayed about the bibliographic record.

To test you must be using ElasticSearch and the OpacBrowseSearch
preference must be enabled. Apply the patch and rebuild the OPAC CSS
(https://wiki.koha-community.org/wiki/Working_with_SCSS_in_the_OPAC_and_staff_client).

- Open the "Browse search" page in the OPAC.
- Test various searches: Author, Subject, and Title.
- When results are found, the should be displayed as a Bootstrap-styled
  accordion widget. Clicking any individual result should expand the
  panel containing the corresponding records.
- Clicking the record link should open the bibliographic detail page in
  a new window.
- When no results are found, a Bootstrap-style "alert" box should
  appear.

Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
This commit is contained in:
Owen Leonard 2020-12-16 18:38:11 +00:00 committed by Jonathan Druart
parent fc2c86ba78
commit e12e0ef016
5 changed files with 193 additions and 212 deletions

View File

@ -44,7 +44,7 @@ $sci-heading-color: #727272;
@import "../../../../../node_modules/bootstrap/scss/breadcrumb";
@import "../../../../../node_modules/bootstrap/scss/button-group";
@import "../../../../../node_modules/bootstrap/scss/buttons";
// @import "../../../../../node_modules/bootstrap/scss/card";
@import "../../../../../node_modules/bootstrap/scss/card";
// @import "../../../../../node_modules/bootstrap/scss/carousel";
@import "../../../../../node_modules/bootstrap/scss/close";
// @import "../../../../../node_modules/bootstrap/scss/code";

View File

@ -2513,94 +2513,35 @@ $star-selected: #EDB867;
}
/*opac browse search*/
#browse-resultswrapper {
margin-top: 15px;
}
#browse-searchfuzziness {
padding: 15px 0;
}
#browse-searchresults, #browse-selectionsearch {
border: 1px solid #E3E3E3;
border-radius: 4px;
padding: 0;
margin-bottom: 2em;
}
#browse-selectionsearch p.subjects {
font-size: 0.9em;
margin-bottom: 0;
}
#browse-selectionsearch h4 {
margin: 0;
}
#browse-suggestionserror {
margin-top: 1rem;
}
#browse-search {
form {
label {
display: inline-block;
margin-right:5px;
}
[type=submit] {
margin-top: 10px;
}
}
#browse-resultswrapper {
margin-top: 4em;
@media (min-width: 768px) and (max-width: 984px) {
margin-top: 2em;
}
@media (max-width: 767px) {
margin-top: 1em;
}
}
#browse-searchresults, #browse-selectionsearch {
border: 1px solid #E3E3E3;
border-radius: 4px;
padding: 0;
overflow-y: auto;
max-height: 31em;
margin-bottom: 2em;
}
#browse-searchresults {
max-height: 31em;
list-style: none;
padding: 10px;
a {
display: block;
margin-bottom: 5px;
&.selected {
background-color:#EEE;
}
}
li:last-child a {
margin-bottom: 0;
}
@media (max-width: 767px) {
max-height: 13em;
}
}
#browse-selection {
margin-top: -40px;
padding-top: 0;
@media (max-width: 767px) {
margin-top: 0;
}
}
#browse-selectionsearch ol {
list-style: none;
margin: 0;
li {
padding: 1em;
&:nth-child(odd) {
background-color: #F4F4F4;
}
}
}
#browse-selectionsearch p.subjects {
font-size: 0.9em;
margin-bottom: 0;
}
#browse-selectionsearch h4 {
margin: 0;
}
.error, .no-results {
background-color: #EEE;
border: 1px solid #E8E8E8;
text-align: left;
padding: 0.5em;
border-radius: 3px;
}
.loading {
text-align: center;
@ -2611,6 +2552,15 @@ $star-selected: #EDB867;
}
}
}
#card_template {
display: none;
}
.result-title {
margin-bottom: .4rem;
margin-left: 1rem;
}
/*end browse search*/
/* Skip to content link. CSS adapted from https://webaim.org/ */
@ -2638,4 +2588,29 @@ $star-selected: #EDB867;
}
}
.accordion {
.card-header {
padding: 0;
a {
display: block;
padding: .75rem 1.25rem;
&[aria-expanded="true"] { font-weight: bold; }
&[aria-expanded="false"] { font-weight: normal; }
&:hover {
background-color: rgba(255, 255, 204, 0.8);
text-decoration: none;
}
}
}
.collapse {
&.show {
border-top: 1px solid #888;
}
}
}
@import "responsive";

View File

@ -41,44 +41,70 @@
<h1>Browse search</h1>
<form>
<label for="browse-searchterm">Search for:</label>
<input type="search" id="browse-searchterm" name="searchterm" value="">
<label for="browse-searchfield" class="hide-text">Search type:</label>
<select id="browse-searchfield" name="searchfield">
<option value="author">Author</option>
<option value="subject">Subject</option>
<option value="title">Title</option>
</select>
<div id="browse-searchfuzziness">
<label for="exact" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="exact" value="0">Exact</label>
<label for="fuzzy" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="fuzzy" value="1" checked="checked"> Fuzzy</label>
<label for="reallyfuzzy" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="reallyfuzzy" value="2"> Really fuzzy</label>
<div class="form-row">
<div class="col">
<label for="browse-searchterm">Search for:</label>
<input type="search" id="browse-searchterm" class="form-control" name="searchterm" value="" />
</div> <!-- /.col-12.col-sm-9 -->
<div class="col-auto">
<label for="browse-searchfield">Search type:</label>
<select id="browse-searchfield" name="searchfield" class="form-control">
<option value="author">Author</option>
<option value="subject">Subject</option>
<option value="title">Title</option>
</select>
</div> <!-- /.col-auto -->
</div> <!-- /.form-row -->
<div class="form-row">
<div class="col">
<div id="browse-searchfuzziness">
<div class="form-check form-check-inline">
<label for="exact" class="form-check-label">
<input class="form-check-input" type="radio" name="browse-searchfuzziness" id="exact" value="0" />Exact
</label>
</div> <!-- /.form-check.form-check-inline -->
<div class="form-check form-check-inline">
<label for="fuzzy" class="form-check-label">
<input class="form-check-input" type="radio" name="browse-searchfuzziness" id="fuzzy" value="1" checked="checked" /> Fuzzy
</label>
</div> <!-- /.form-check.form-check-inline -->
<div class="form-check form-check-inline">
<label for="reallyfuzzy" class="form-check-label">
<input class="form-check-input" type="radio" name="browse-searchfuzziness" id="reallyfuzzy" value="2" /> Really fuzzy
</label>
</div> <!-- /.form-check.form-check-inline -->
</div> <!-- /#browse-searchfuzziness -->
</div> <!-- /.col -->
</div> <!-- /.form-row -->
<div class="form-row">
<div class="col">
<button class="btn btn-primary" type="submit" accesskey="s">Search</button>
</div>
</div>
<button class="btn btn-success" type="submit" accesskey="s">Search</button>
</form>
<p id="browse-suggestionserror" class="error hidden">
An error occurred, please try again.</p>
<div id="browse-suggestionserror" class="alert alert-warning d-none" role="alert">
An error occurred, please try again.
</div>
<div id="browse-resultswrapper" class="d-none">
<ul id="browse-searchresults" start="-1" aria-live="polite">
<li class="loading hidden"><img src="[% interface | html %]/[% theme |html %]/images/loading.gif" alt=""> Loading</li>
<h2>Results</h2>
<li class="no-results hidden">Sorry, there are no results, try a different search term.</li>
</ul>
<div class="loading d-none"><img src="[% interface | html %]/[% theme |html %]/images/loading.gif" alt=""> Loading</div>
<div class="alert alert-warning no-results d-none" role="alert">Sorry, there are no results. Try a different search term.</div>
<h3 id="browse-selection"></h3>
<div id="browse-selectionsearch" class="d-none">
<div class="loading hidden">
<img src="[% interface | html %]/[% theme | html %]/images/loading.gif" alt=""> Loading
</div>
<div class="no-results hidden">No results</div>
<ol aria-live="polite"></ol>
</div>
<div class="accordion" id="browse-searchresults">
<div id="card_template" class="card">
<div class="card-header" id="heading">
<a class="expand-result" href="#" data-toggle="collapse" aria-expanded="false" aria-controls="collapse">
</a>
</div> <!-- /#heading.card-header -->
<div id="collapse" class="collapse" aria-labelledby="heading" data-parent="#browse-searchresults">
<div class="card-body">
</div>
</div> <!-- /#collapse.collapse -->
</div> <!-- /#card_template.card -->
</div> <!-- /#browse-searchresults.accordion -->
</div><!-- / #browse-resultswrapper -->
</div><!-- /#browse-search -->
[% ELSE %]

View File

@ -16,8 +16,7 @@ $(document).ready(function(){
var userInput = $('#browse-searchterm').val().trim();
var userField = $('#browse-searchfield').val();
var userFuzziness = $('input[name=browse-searchfuzziness]:checked', '#browse-searchfuzziness').val();
var leftPaneResults = $('#browse-searchresults li').not('.loading, .no-results');
var rightPaneResults = $('#browse-selectionsearch ol li');
var card_template = $("#card_template");
event.preventDefault();
@ -25,49 +24,50 @@ $(document).ready(function(){
return;
}
// remove any error states and show the results area (except right pane)
/* return the browsing results to empty state in case previous results have been loaded */
$("#browse-searchresults").empty().append( card_template );
// remove any error states and show the results area
$('#browse-suggestionserror').addClass('d-none');
$('#browse-searchresults .no-results').addClass('d-none');
$('.no-results').addClass('d-none');
$('#browse-resultswrapper').removeClass('d-none');
$('#browse-selection').addClass('d-none').text("");
$('#browse-selectionsearch').addClass('d-none');
/* Reset results browser to default state */
// clear any results from left and right panes
leftPaneResults.remove();
rightPaneResults.remove();
// show the spinner in the left pane
$('#browse-searchresults .loading').removeClass('d-none');
// show the spinner
$('.loading').removeClass('d-none');
xhrGetSuggestions = $.get(window.location.pathname, {api: "GetSuggestions", field: userField, prefix: userInput, fuzziness: userFuzziness})
.always(function() {
// hide spinner
$('#browse-searchresults .loading').addClass('d-none');
$('.loading').addClass('d-none');
})
.done(function(data) {
var fragment = document.createDocumentFragment();
if (data.length === 0) {
$('#browse-searchresults .no-results').removeClass('d-none');
$('.no-results').removeClass('d-none');
return;
}
// scroll to top of container again
$("#browse-searchresults").overflowScrollReset();
// store the type of search that was performed as an attrib
$('#browse-searchresults').data('field', userField);
$.each(data, function(index, object) {
// use a document fragment so we don't need to nest the elems
// or append during each iteration (which would be slow)
var elem = document.createElement("li");
var link = document.createElement("a");
link.textContent = object.text;
link.setAttribute("href", "#");
elem.appendChild(link);
fragment.appendChild(elem);
var card = card_template.clone().removeAttr("id");
// change card-header id
card
.find(".card-header")
.attr("id", "heading" + index)
.find("a").attr("data-target", "#collapse" + index)
.attr("aria-controls", "collapse" + index)
.text(object.text);
card
.find(".collapse")
.attr("id", "collapse" + index)
.attr("aria-labelledby", "heading" + index);
$(fragment).append(card);
});
$('#browse-searchresults').append(fragment.cloneNode(true));
@ -81,92 +81,71 @@ $(document).ready(function(){
});
});
$('#browse-searchresults').on("click", 'a', function(event) {
$('#browse-searchresults').on("click", 'a.expand-result', function(event) {
// if there is an in progress request, abort it so we
// don't end up with a race condition
if(xhrGetResults && xhrGetResults.readyState != 4){
xhrGetResults.abort();
}
var term = $(this).text();
var link = $(this);
var target = link.data("target");
var term = link.text();
var field = $('#browse-searchresults').data('field');
var rightPaneResults = $('#browse-selectionsearch ol li');
event.preventDefault();
// clear any current selected classes and add a new one
$(this).parent().siblings().children().removeClass('selected');
$(this).addClass('selected');
/* Don't load data via AJAX if it has already been loaded */
if ($(target).find(".result-title").length == 0) {
// do the query for the term
xhrGetResults = $.get(window.location.pathname, {api: "GetResults", field: field, term: term})
.done(function(data) {
var fragment = document.createDocumentFragment();
// copy in the clicked text
$('#browse-selection').removeClass('d-none').text(term);
if (data.length === 0) {
$('#browse-selectionsearch .no-results').removeClass('d-none');
return;
}
// show the right hand pane if it is not shown already
$('#browse-selectionsearch').removeClass('d-none');
// hide the no results element
$('#browse-selectionsearch .no-results').addClass('d-none');
$.each(data, function(index, object) {
// use a document fragment so we don't need to nest the elems
// or append during each iteration (which would be slow)
var elem = document.createElement("div");
elem.className = "result-title";
// clear results
rightPaneResults.remove();
var destination = window.location.pathname;
destination = destination.replace("browse", "detail");
destination = destination + "?biblionumber=" + object.id;
// turn the spinner on
$('#browse-selectionsearch .loading').removeClass('d-none');
var link = document.createElement("a");
link.setAttribute("href", destination);
link.setAttribute("target", "_blank");
link.textContent = object.title;
if( object.subtitle ){
link.textContent += " " + object.subtitle;
}
elem.appendChild(link);
// do the query for the term
xhrGetResults = $.get(window.location.pathname, {api: "GetResults", field: field, term: term})
.always(function() {
// hide spinner
$('#browse-selectionsearch .loading').addClass('d-none');
})
.done(function(data) {
var fragment = document.createDocumentFragment();
if( object.author ){
var author = document.createElement("span");
author.className = "author";
author.textContent = " " + object.author;
elem.appendChild(author);
}
fragment.appendChild(elem);
});
if (data.length === 0) {
$('#browse-selectionsearch .no-results').removeClass('d-none');
return;
}
// scroll to top of container again
$("#browse-selectionsearch").overflowScrollReset();
$.each(data, function(index, object) {
// use a document fragment so we don't need to nest the elems
// or append during each iteration (which would be slow)
var elem = document.createElement("li");
var title = document.createElement("h4");
var link = document.createElement("a");
var author = document.createElement("p");
var destination = window.location.pathname;
destination = destination.replace("browse", "detail");
destination = destination + "?biblionumber=" + object.id;
author.className = "author";
link.setAttribute("href", destination);
link.setAttribute("target", "_blank");
link.textContent = object.title;
title.appendChild(link);
author.textContent = object.author;
elem.appendChild(title);
elem.appendChild(author);
fragment.appendChild(elem);
$( target ).find(".card-body").append(fragment.cloneNode(true));
})
.fail(function(jqXHR) {
//if 500 or 404 (abort is okay though)
if (jqXHR.statusText !== "abort") {
$('#browse-resultswrapper').addClass('d-none');
$('#browse-suggestionserror').removeClass('d-none');
}
});
$('#browse-selectionsearch ol').append(fragment.cloneNode(true));
})
.fail(function(jqXHR) {
//if 500 or 404 (abort is okay though)
if (jqXHR.statusText !== "abort") {
$('#browse-resultswrapper').addClass('d-none');
$('#browse-suggestionserror').removeClass('d-none');
}
});
}
});
});

View File

@ -104,6 +104,7 @@ sub _filter_for_output {
{
id => $biblionumber,
title => $biblio->title,
subtitle => $biblio->subtitle,
author => $biblio->author,
};
};