1 /* global shortcut delBasket Sticky AUDIO_ALERT_PATH Cookies */
2 /* exported addBibToContext delBibToContext escape_str escape_price openWindow _ removeFocus toUC confirmDelete confirmClone playSound */
3 if ( KOHA === undefined ) var KOHA = {};
5 function _(s) { return s; } // dummy function for gettext
7 // http://stackoverflow.com/questions/1038746/equivalent-of-string-format-in-jquery/5341855#5341855
8 String.prototype.format = function() { return formatstr(this, arguments); };
9 function formatstr(str, col) {
10 col = typeof col === 'object' ? col : Array.prototype.slice.call(arguments, 1);
12 return str.replace(/%%|%s|%(\d+)\$s/g, function (m, n) {
13 if (m == "%%") { return "%"; }
14 if (m == "%s") { return col[idx++]; }
19 var HtmlCharsToEscape = {
24 String.prototype.escapeHtml = function() {
25 return this.replace(/[&<>]/g, function(c) {
26 return HtmlCharsToEscape[c] || c;
29 function escape_str(s){
30 return s != null ? s.escapeHtml() : "";
34 * Void method for numbers, for consistency
36 Number.prototype.escapeHtml = function() {
39 function escape_price(p){
40 return p != null ? p.escapeHtml().format_price() : "";
43 // http://stackoverflow.com/questions/14859281/select-tab-by-name-in-jquery-ui-1-10-0/16550804#16550804
44 $.fn.tabIndex = function () {
45 return $(this).parent().children('div').index(this);
47 $.fn.selectTabByID = function (tabID) {
48 $("a[href='" + tabID + "']", $(this) ).tab("show");
51 $(document).ready(function() {
53 //check for a hash before setting focus
54 let hash = window.location.hash;
56 $(".tab-pane.active input:text:first").focus();
58 $("#header_search a[data-toggle='tab']").on("shown.bs.tab", function (e) {
59 $( e.target.hash ).find("input:text:first").focus();
62 $(".close, .close_window").on("click", function(e){
67 $("#checkin_search form").preventDoubleFormSubmit();
69 if($("#header_search #checkin_search").length > 0){
70 shortcut.add('Alt+r',function (){
71 $("#header_search").selectTabByID("#checkin_search");
72 $("#ret_barcode").focus();
75 shortcut.add('Alt+r',function (){
76 location.href="/cgi-bin/koha/circ/returns.pl"; });
78 if($("#header_search #circ_search").length > 0){
79 shortcut.add('Alt+u',function (){
80 $("#header_search").selectTabByID("#circ_search");
81 $("#findborrower").focus();
84 shortcut.add('Alt+u',function(){ location.href="/cgi-bin/koha/circ/circulation.pl"; });
86 if($("#header_search #catalog_search").length > 0){
87 shortcut.add('Alt+q',function (){
88 $("#header_search").selectTabByID("#catalog_search");
89 $("#search-form").focus();
92 shortcut.add('Alt+q',function(){ location.href="/cgi-bin/koha/catalogue/search.pl"; });
94 if($("#header_search #renew_search").length > 0){
95 shortcut.add('Alt+w',function (){
96 $("#header_search").selectTabByID("#renew_search");
97 $("#ren_barcode").focus();
100 shortcut.add('Alt+w',function(){ location.href="/cgi-bin/koha/circ/renew.pl"; });
103 $('#header_search .form-extra-content-toggle').on('click', function () {
104 const extraContent = $(this).closest('form').find('.form-extra-content');
105 if (extraContent.is(':visible')) {
107 $(this).removeClass("extra-content-open");
110 $(this).addClass("extra-content-open");
115 $(".validated").each(function() {
118 jQuery.validator.addClassRules("decimal", {
122 $("#logout").on("click",function(){
125 $("#helper").on("click",function(){
130 $("body").on("keypress", ".noEnterSubmit", function(e){
131 return checkEnter(e);
134 $(".keep_text").on("click",function(){
135 var field_index = $(this).parent().index();
136 keep_text( field_index );
139 $(".toggle_element").on("click",function(e){
141 $( $(this).data("element") ).toggle();
142 if (typeof Sticky !== "undefined" && typeof hcSticky === "function") {
143 Sticky.hcSticky('update');
147 var navmenulist = $("#navmenulist");
148 if( navmenulist.length > 0 ){
149 var path = location.pathname.substring(1);
150 var url = window.location.toString();
152 if ( url.match(/\?(.+)$/) ) {
153 params = "?" + RegExp.$1;
155 if ($("a[href$=\"/" + path + params + "\"]", navmenulist).length == 0){
156 $("a[href$=\"/" + path + "\"]", navmenulist).addClass("current");
158 $("a[href$=\"/" + path + params + "\"]", navmenulist).addClass("current");
162 $("#catalog-search-link a").on("mouseenter mouseleave", function(){
163 $("#catalog-search-dropdown a").toggleClass("catalog-search-dropdown-hover");
166 if ( localStorage.getItem("previousPatrons") || $("#hiddenborrowernumber").val() ){
167 var previous_patrons = [];
168 if ( localStorage.getItem("previousPatrons") ) {
169 previous_patrons = JSON.parse(localStorage.getItem("previousPatrons"));
172 if ( $("#hiddenborrowernumber").val() ) {
173 // Remove this patron from the list if they are already there
174 previous_patrons = previous_patrons.filter(function (p) {
175 return p["borrowernumber"] != $("#hiddenborrowernumber").val();
178 const previous_patron = {
179 "borrowernumber": $("#hiddenborrowernumber").val(),
180 "name": $("#hiddenborrowername").val(),
181 "card": $("#hiddenborrowercard").val()
184 previous_patrons.unshift( previous_patron );
185 // Limit to number of patrons specified in showLastPatronCount
186 if ( previous_patrons.length > showLastPatronCount ) previous_patrons.pop();
187 localStorage.setItem("previousPatrons", JSON.stringify(previous_patrons));
190 if ( previous_patrons.length ) {
191 let p = previous_patrons[0];
192 $("#lastborrowerlink").show();
193 $("#lastborrowerlink").prop("title", `${p["name"]} (${p["card"]})`);
194 $("#lastborrowerlink").prop("href", `/cgi-bin/koha/circ/circulation.pl?borrowernumber=${p["borrowernumber"]}`);
195 $("#lastborrower-window").css("display", "inline-flex");
197 previous_patrons.reverse();
198 for ( i in previous_patrons ) {
199 p = previous_patrons[i];
200 const el = `<li><a href="/cgi-bin/koha/circ/circulation.pl?borrowernumber=${p["borrowernumber"]}">${p["name"]} (${p["card"]})</a></li>`;
201 $("#lastBorrowerList").prepend(el);
206 if( $("#hiddenborrowernumber").val() ){
207 localStorage.setItem("currentborrowernumber", $("#hiddenborrowernumber").val() );
210 $("#lastborrower-remove").click(function() {
211 removeLastBorrower();
212 $("#lastborrower-window").hide();
215 /* Search results browsing */
216 /* forms with action leading to search */
217 $("form[action*='search.pl']").submit(function(){
218 $('[name^="limit"]').each(function(){
219 if( $(this).val() == '' ){
220 $(this).prop("disabled","disabled");
223 var disabledPrior = false;
224 $(".search-term-row").each(function(){
226 $(this).find('select[name="op"]').prop("disabled","disabled");
227 disabledPrior = false;
229 if( $(this).find('input[name="q"]').val() == "" ){
230 $(this).find('input').prop("disabled","disabled");
231 $(this).find('select').prop("disabled","disabled");
232 disabledPrior = true;
235 resetSearchContext();
236 saveOrClearSimpleSearchParams();
238 /* any link to launch a search except navigation links */
239 $("[href*='search.pl?']").not(".nav").not('.searchwithcontext').click(function(){
240 resetSearchContext();
242 /* any link to a detail page from the results page. */
243 $("#bookbag_form a[href*='detail.pl?']").click(function(){
244 resetSearchContext();
249 function removeLastBorrower(){
250 localStorage.removeItem("previousPatrons");
253 // http://jennifermadden.com/javascript/stringEnterKeyDetector.html
254 function checkEnter(e){ //e is event object passed from function invocation
255 var characterCode; // literal character code will be stored in this variable
256 if(e && e.which){ //if which property of event object is supported (NN4)
257 characterCode = e.which; //character code is contained in NN4's which property
259 characterCode = e.keyCode; //character code is contained in IE's keyCode property
261 if( characterCode == 13 //if generated character code is equal to ascii 13 (if enter key)
262 && e.target.nodeName == "INPUT"
263 && e.target.type != "submit" // Allow enter to submit using the submit button
271 function clearHoldFor(){
272 Cookies.remove("holdfor", { path: '/', SameSite: 'Lax' });
276 if( typeof delBasket == 'function' ){
277 delBasket('main', true);
280 removeLastBorrower();
281 localStorage.removeItem("sql_reports_activetab");
282 localStorage.removeItem("searches");
283 localStorage.removeItem("bibs_selected");
284 localStorage.removeItem("patron_search_selections");
288 window.open( "/cgi-bin/koha/help.pl", "_blank");
291 jQuery.fn.preventDoubleFormSubmit = function() {
292 jQuery(this).submit(function() {
293 $("body, form input[type='submit'], form button[type='submit'], form a").addClass('waiting');
294 if (this.beenSubmitted)
297 this.beenSubmitted = true;
301 function openWindow(link,name,width,height) {
302 name = (typeof name == "undefined")?'popup':name;
303 width = (typeof width == "undefined")?'600':width;
304 height = (typeof height == "undefined")?'400':height;
305 //IE <= 9 can't handle a "name" with whitespace
307 window.open(link,name,'width='+width+',height='+height+',resizable=yes,toolbar=false,scrollbars=yes,top');
309 window.open(link,null,'width='+width+',height='+height+',resizable=yes,toolbar=false,scrollbars=yes,top');
313 // Use this function to remove the focus from any element for
314 // repeated scanning actions on errors so the librarian doesn't
315 // continue scanning and miss the error.
316 function removeFocus() {
321 var x=f.value.toUpperCase();
326 function confirmDelete(message) {
327 return (confirm(message) ? true : false);
330 function confirmClone(message) {
331 return (confirm(message) ? true : false);
334 function playSound( sound ) {
335 if ( ! ( sound.indexOf('http://') === 0 || sound.indexOf('https://') === 0 ) ) {
336 sound = AUDIO_ALERT_PATH + sound;
338 document.getElementById("audio-alert").innerHTML = '<audio src="' + sound + '" autoplay="autoplay" autobuffer="autobuffer"></audio>';
341 // For keeping the text when navigating the search tabs
342 function keep_text(clicked_index) {
343 var searchboxes = document.getElementsByClassName("head-searchbox");
344 var persist = searchboxes[0].value;
346 for (var i = 0; i < searchboxes.length - 1; i++) {
347 if (searchboxes[i].value != searchboxes[i+1].value) {
348 if (i === searchboxes.length-2) {
349 if (searchboxes[i].value != searchboxes[0].value) {
350 persist = searchboxes[i].value;
351 } else if (searchboxes.length === 2) {
352 if (clicked_index === 0) {
353 persist = searchboxes[1].value;
356 persist = searchboxes[i+1].value;
358 } else if (searchboxes[i+1].value != searchboxes[i+2].value) {
359 persist = searchboxes[i+1].value;
364 for (i = 0; i < searchboxes.length; i++) {
365 searchboxes[i].value = persist;
369 // Extends jQuery API
370 jQuery.extend({uniqueArray:function(array){
371 return $.grep(array, function(el, index) {
372 return index === $.inArray(el, array);
376 function removeByValue(arr, val) {
377 for(var i=0; i<arr.length; i++) {
385 function addBibToContext( bibnum ) {
386 bibnum = parseInt(bibnum, 10);
387 var bibnums = getContextBiblioNumbers();
388 bibnums.push(bibnum);
389 setContextBiblioNumbers( bibnums );
390 setContextBiblioNumbers( $.uniqueArray( bibnums ) );
393 function delBibToContext( bibnum ) {
394 var bibnums = getContextBiblioNumbers();
395 removeByValue( bibnums, bibnum );
396 setContextBiblioNumbers( $.uniqueArray( bibnums ) );
399 function setContextBiblioNumbers( bibnums ) {
400 localStorage.setItem('bibs_selected', JSON.stringify( bibnums ) );
403 function getContextBiblioNumbers() {
404 var r = localStorage.getItem('bibs_selected');
406 return JSON.parse(r);
412 function resetSearchContext() {
413 setContextBiblioNumbers( new Array() );
416 function saveOrClearSimpleSearchParams() {
417 // Simple masthead search - pass value for display on details page
418 var pulldown_selection;
420 if( $("#cat-search-block select.advsearch").length ){
421 pulldown_selection = $("#cat-search-block select.advsearch").val();
423 pulldown_selection ="";
425 if( $("#cat-search-block #search-form").length ){
426 searchbox_value = $("#cat-search-block #search-form").val();
430 localStorage.setItem('cat_search_pulldown_selection', pulldown_selection );
431 localStorage.setItem('searchbox_value', searchbox_value );
434 function patron_autocomplete(node, options) {
437 let on_select_callback;
440 if (options['link-to']) {
441 link_to = options['link-to'];
443 if (options['url-params']) {
444 url_params = options['url-params'];
446 if (options['on-select-callback']) {
447 on_select_callback = options['on-select-callback'];
450 return node.autocomplete({
451 source: function (request, response) {
452 let q = buildPatronSearchQuery(request.term);
457 'q': JSON.stringify(q),
458 '_order_by': '+me.surname,+me.firstname',
463 url: '/api/v1/patrons',
465 "x-koha-embed": "library"
467 success: function (data) {
468 return response(data);
470 error: function (e) {
471 if (e.state() != 'rejected') {
472 alert(__("An error occurred. Check the logs"));
479 select: function (event, ui) {
481 window.location.href = ui.item.link;
482 } else if (on_select_callback) {
483 return on_select_callback(event, ui);
486 focus: function (event, ui) {
487 event.preventDefault(); // Don't replace the text field
490 .data("ui-autocomplete")
491 ._renderItem = function (ul, item) {
493 item.link = link_to == 'circ'
494 ? "/cgi-bin/koha/circ/circulation.pl"
495 : link_to == 'reserve'
496 ? "/cgi-bin/koha/reserve/request.pl"
497 : "/cgi-bin/koha/members/moremember.pl";
498 item.link += (url_params ? '?' + url_params + '&' : "?") + 'borrowernumber=' + item.patron_id;
504 if (item.cardnumber != "") {
505 // Display card number in parentheses if it exists
506 cardnumber = " (" + item.cardnumber + ") ";
508 if (item.library_id == loggedInLibrary) {
509 loggedInClass = "ac-currentlibrary";
513 return $("<li></li>")
514 .addClass(loggedInClass)
515 .data("ui-autocomplete-item", item)
518 + (item.link ? "<a href=\"" + item.link + "\">" : "<a>")
519 + (item.surname ? item.surname.escapeHtml() : "") + ", "
520 + (item.firstname ? item.firstname.escapeHtml() : "")
521 + " " + (item.middle_name ? item.middle_name.escapeHtml() : "")
522 + cardnumber.escapeHtml()
524 + (item.date_of_birth
525 ? $date(item.date_of_birth)
526 + "<span class=\"age_years\"> ("
527 + $get_age(item.date_of_birth)
533 + $format_address(item, { no_line_break: true, include_li: false }) + " "
536 "<span class=\"ac-library\">"
537 + item.library.name.escapeHtml()
546 function expandPatronSearchFields(search_fields) {
547 switch(search_fields) {
549 return defaultPatronSearchFields;
552 return 'streetnumber|streettype|address|address2|city|state|zipcode|country';
555 return 'email|emailpro|B_email';
558 return 'phone|phonepro|B_phone|altcontactphone|mobile';
561 return search_fields;
566 * Build patron search query
567 * - term: The full search term input by the user
568 * You can then pass a list of options:
569 * - search_type: String 'contains' or 'starts_with', defaults to DefaultPatronSearchMethod system preference
570 * - search_fields: String comma-separated list of specific fields, defaults to DefaultPatronSearchFields system preference
571 * - extended_attribute_types: JSON object containing the patron attribute types to be searched on
573 function buildPatronSearchQuery(term, options) {
576 let leading_wildcard;
578 let patterns = term.split(/[\s,]+/).filter(function (s) { return s.length });
580 // Bail if no patterns
581 if (patterns.length == 0) {
585 // Leading wildcard: If search_type option exists, we use that
586 if (typeof options !== 'undefined' && options.search_type) {
587 leading_wildcard = options.search_type === "contains" ? '%' : '';
588 // If not, we use DefaultPatronSearchMethod system preference instead
590 leading_wildcard = defaultPatronSearchMethod === 'contains' ? '%' : '';
593 // Search fields: If search_fields option exists, we use that
594 if (typeof options !== 'undefined' && options.search_fields) {
595 search_fields = expandPatronSearchFields(options.search_fields);
596 // If not, we use DefaultPatronSearchFields system preference instead
598 search_fields = defaultPatronSearchFields;
601 // Add each pattern for each search field
602 let pattern_subquery_and = [];
603 patterns.forEach(function (pattern, i) {
604 let pattern_subquery_or = [];
605 search_fields.split('\|').forEach(function (field, i) {
606 pattern_subquery_or.push(
607 { ["me." + field]: { 'like': leading_wildcard + pattern + '%' } }
609 if (field == 'dateofbirth') {
611 let d = $date_to_rfc3339(pattern);
612 pattern_subquery_or.push({ ["me." + field]: d });
614 // Hide the warning if the date is not correct
618 pattern_subquery_and.push(pattern_subquery_or);
620 q.push({ "-and": pattern_subquery_and });
622 // Add full search term for each search field
623 let term_subquery_or = [];
624 search_fields.split('\|').forEach(function (field, i) {
625 term_subquery_or.push(
626 { ["me." + field]: { 'like': leading_wildcard + term + '%' } }
629 q.push({ "-or": term_subquery_or });
631 // Add each pattern for each extended patron attributes
632 if (typeof options !== 'undefined' && options.search_fields == 'standard' && options.extended_attribute_types && extendedPatronAttributes) {
633 extended_attribute_subquery_and = [];
634 patterns.forEach(function (pattern, i) {
635 let extended_attribute_sub_or = [];
636 extended_attribute_sub_or.push({
637 "extended_attributes.value": { "like": leading_wildcard + pattern + '%' },
638 "extended_attributes.code": options.extended_attribute_types
640 extended_attribute_subquery_and.push(extended_attribute_sub_or);
642 q.push({ "-and": extended_attribute_subquery_and });
647 function selectBsTabByHash( tabs_container_id ){
648 /* Check for location.hash in the page URL */
649 /* If present the location hash will be used to activate the correct tab */
650 var hash = document.location.hash;
652 $('#' + tabs_container_id + ' a[href="' + hash + '"]').tab('show');
654 $('#' + tabs_container_id + ' a:first').tab('show');