From f11d6968e08455832bb27310a845d3dd2020489a Mon Sep 17 00:00:00 2001
From: Jen Zajac <jen@catalyst.net.nz>
Date: Fri, 14 Aug 2015 19:36:15 +1200
Subject: [PATCH] Bug 14567: frontend template and JS for browse interface

---
 .../opac-tmpl/bootstrap/en/modules/opac-browse.tt  |  83 ++++++++++
 koha-tmpl/opac-tmpl/bootstrap/js/browse.js         | 176 +++++++++++++++++++++
 koha-tmpl/opac-tmpl/bootstrap/less/opac.less       | 101 ++++++++++++
 opac/opac-browse.pl                                |  13 +-
 4 files changed, 372 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

diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt
new file mode 100644
index 0000000..5bda6a1
--- /dev/null
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt
@@ -0,0 +1,83 @@
+[% USE Koha %]
+[% INCLUDE 'doc-head-open.inc' %][% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo;  Browse our catalog
+[% INCLUDE 'doc-head-close.inc' %]
+[% BLOCK cssinclude %][% END %]
+</head>
+[% INCLUDE 'bodytag.inc' bodyid='opac-browser' %]
+[% INCLUDE 'masthead.inc' %]
+
+    <div class="main">
+        <ul class="breadcrumb">
+            <li><a href="/cgi-bin/koha/opac-main.pl">Home</a> <span class="divider">&rsaquo;</span></li>
+            <li><a href="#">Browse search</a></li>
+        </ul>
+
+        <div class="container-fluid">
+            <div class="row-fluid">
+                [% IF ( OpacNav || OpacNavBottom ) %]
+                    <div class="span2">
+                        <div id="navigation">
+                            [% INCLUDE 'navigation.inc' %]
+                        </div>
+                    </div>
+                [% END %]
+
+                [% IF ( OpacNav ) %]<div class="span10">[% ELSE %]<div class="span12">[% END %]
+
+                <div id="browse-search">
+                    <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>
+
+                        <button class="btn btn-success" type="submit"  accesskey="s">Search</button>
+                    </form>
+
+                    <p id="browse-suggestionserror" class="error hidden">An error occured, please try again.</p>
+
+                    <div id="browse-resultswrapper" class="hidden">
+
+                      <ol id="browse-searchresults" class="span3" start="-1" aria-live="polite">
+                          <li class="loading hidden"><img src="[% interface %]/[% theme %]/images/loading.gif" alt="" /> Loading</li>
+                          <li class="no-results hidden">Sorry, there are no results, try a different search term.</li>
+                      </ol>
+
+                      <h3 id="browse-selection" class="span9"></h3>
+                      <div id="browse-selectionsearch" class="span9 hidden">
+
+                          <div class="loading hidden">
+                            <img src="[% interface %]/[% theme %]/images/loading.gif" alt="" /> Loading
+                          </div>
+
+                          <div class="no-results hidden">
+                            No results
+                          </div>
+
+                          <ol aria-live="polite">
+                          </ol>
+                        </div>
+                      </div><!-- / .results-wrapper -->
+
+                    </div> <!-- / .userbrowser -->
+                </div> <!-- / .span10 -->
+            </div> <!-- / .row-fluid -->
+        </div> <!-- / .container-fluid -->
+    </div> <!-- / .main -->
+
+[% INCLUDE 'opac-bottom.inc' %]
+[% BLOCK jsinclude %]<script type="text/javascript" src="[% interface %]/[% theme %]/js/browse.js"></script>[% END %]
diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/browse.js b/koha-tmpl/opac-tmpl/bootstrap/js/browse.js
new file mode 100644
index 0000000..d8828af
--- /dev/null
+++ b/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');
+                }
+            });
+
+    });
+
+});
diff --git a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less b/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
index 7c79c7d..14635da 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
+++ b/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
@@ -2473,4 +2473,105 @@ a.reviewlink:visited {
     padding-left:20px;
 }
 
+/*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*/
+
 @import "responsive.less";
diff --git a/opac/opac-browse.pl b/opac/opac-browse.pl
index 4fc07c7..a6577eb 100755
--- a/opac/opac-browse.pl
+++ b/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;
@@ -43,7 +44,17 @@ my $api = $query->param('api');
 if ( !$api ) {
 
     # No parameters, so we just render the template.
-
+    my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
+        {
+            template_name   => "opac-browse.tt",
+            query           => $query,
+            type            => "opac",
+            authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ),
+            debug           => 1, # TODO remove
+        }
+    );
+    $template->param();
+    output_html_with_http_headers $query, $cookie, $template->output;
 }
 elsif ( $api eq 'GetSuggestions' ) {
     my $fuzzie = $query->param('fuzziness');
-- 
2.1.4