@@ -, +, @@ --- .../opac-tmpl/bootstrap/en/modules/opac-browse.tt | 106 +++++++++++++ koha-tmpl/opac-tmpl/bootstrap/js/browse.js | 176 +++++++++++++++++++++ koha-tmpl/opac-tmpl/bootstrap/less/opac.less | 101 ++++++++++++ opac/opac-browse.pl | 12 +- 4 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt create mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/browse.js --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt +++ a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt @@ -0,0 +1,106 @@ + + + + + + + + + + + [% USE Koha %] [% INCLUDE 'doc-head-open.inc' %][% IF ( + LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha + online[% END %] catalog › Browse our catalog [% INCLUDE + 'doc-head-close.inc' %] [% BLOCK cssinclude %][% END %] [% + INCLUDE 'bodytag.inc' bodyid='opac-browser' %] [% INCLUDE + 'masthead.inc' %] + +
+ + +
+
+ [% IF ( OpacNav || OpacNavBottom ) %] + +
+ +
[% END %] [% IF ( OpacNav ) %] + +
+ [% ELSE %] + +
+ [% END %] + + +
+
+
+
+ [% INCLUDE 'opac-bottom.inc' %] [% BLOCK jsinclude + %][% END %] +
+ + --- a/koha-tmpl/opac-tmpl/bootstrap/js/browse.js +++ a/koha-tmpl/opac-tmpl/bootstrap/js/browse.js @@ -0,0 +1,176 @@ +jQuery.fn.overflowScrollReset = function() { + $(this).scrollTop($(this).scrollTop() - $(this).offset().top); + return this; +}; + +$(document).ready(function(){ + var xhrGetSuggestions, xhrGetResults; + + $('#browse-search form').submit(function(event) { + // if there is an in progress request, abort it so we + // don't end up with a race condition + if(xhrGetSuggestions && xhrGetSuggestions.readyState != 4){ + xhrGetSuggestions.abort(); + } + + 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'); + + event.preventDefault(); + + if(!userInput) { + return; + } + + // remove any error states and show the results area (except right pane) + $('#browse-suggestionserror').addClass('hidden'); + $('#browse-searchresults .no-results').addClass('hidden'); + $('#browse-resultswrapper').removeClass('hidden'); + $('#browse-selection').addClass('hidden').text(""); + $('#browse-selectionsearch').addClass('hidden'); + + // clear any results from left and right panes + leftPaneResults.remove(); + rightPaneResults.remove(); + + // show the spinner in the left pane + $('#browse-searchresults .loading').removeClass('hidden'); + + xhrGetSuggestions = $.get(window.location.pathname, {api: "GetSuggestions", field: userField, prefix: userInput, fuzziness: userFuzziness}) + .always(function() { + // hide spinner + $('#browse-searchresults .loading').addClass('hidden'); + }) + .done(function(data) { + var fragment = document.createDocumentFragment(); + + if (data.length === 0) { + $('#browse-searchresults .no-results').removeClass('hidden'); + + 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); + }); + + $('#browse-searchresults').append(fragment.cloneNode(true)); + }) + .fail(function(jqXHR) { + //if 500 or 404 (abort is okay though) + if (jqXHR.statusText !== "abort") { + $('#browse-resultswrapper').addClass('hidden'); + $('#browse-suggestionserror').removeClass('hidden'); + } + }); + }); + + $('#browse-searchresults').on("click", 'a', 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 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'); + + // copy in the clicked text + $('#browse-selection').removeClass('hidden').text(term); + + // show the right hand pane if it is not shown already + $('#browse-selectionsearch').removeClass('hidden'); + + // hide the no results element + $('#browse-selectionsearch .no-results').addClass('hidden'); + + // clear results + rightPaneResults.remove(); + + // turn the spinner on + $('#browse-selectionsearch .loading').removeClass('hidden'); + + // 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('hidden'); + }) + .done(function(data) { + var fragment = document.createDocumentFragment(); + + if (data.length === 0) { + $('#browse-selectionsearch .no-results').removeClass('hidden'); + + 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 subjects = document.createElement("p"); + var destination = window.location.pathname; + + destination = destination.replace("browse", "detail"); + destination = destination + "?biblionumber=" + object.id; + + author.className = "author"; + subjects.className = "subjects"; + + link.setAttribute("href", destination); + link.setAttribute("target", "_blank"); + link.textContent = object.title; + title.appendChild(link); + + author.textContent = object.authors.join(', '); + subjects.textContent = object.subjects.join(' | '); + + elem.appendChild(title); + elem.appendChild(author); + elem.appendChild(subjects); + fragment.appendChild(elem); + }); + + $('#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('hidden'); + $('#browse-suggestionserror').removeClass('hidden'); + } + }); + + }); + +}); --- a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less +++ a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less @@ -304,6 +304,107 @@ td { } } +/*opac browse search*/ +#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-all(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-all(3px); + } + .loading { + text-align: center; + + img { + margin:0.5em 0; + position: relative; + left: -5px; + } + } +} +/*end browse search*/ + /* Override Bootstrap alert */ .alert { background: #fffbe5; /* Old browsers */ --- a/opac/opac-browse.pl +++ a/opac/opac-browse.pl @@ -24,6 +24,7 @@ use CGI qw( -utf8 :standard ); use C4::Auth; use C4::Context; +use C4::Output; use Koha::ElasticSearch; use Koha::SearchEngine::Elasticsearch::Browse; @@ -41,8 +42,17 @@ binmode STDOUT, ':utf8'; my $api = $query->param('api'); if ( !$api ) { + my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "opac-browse.tt", + query => $query, + type => "opac", + authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ), + } + ); + $template->param(); + output_html_with_http_headers $query, $cookie, $template->output; - # No parameters, so we just render the template. } elsif ( $api eq 'GetSuggestions' ) { --