Bug 34323: Enhance header search icon for more options
[koha.git] / koha-tmpl / intranet-tmpl / prog / js / staff-global.js
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 = {};
4
5 function _(s) { return s; } // dummy function for gettext
6
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);
11     var idx = 0;
12     return str.replace(/%%|%s|%(\d+)\$s/g, function (m, n) {
13         if (m == "%%") { return "%"; }
14         if (m == "%s") { return col[idx++]; }
15         return col[n];
16     });
17 }
18
19 var HtmlCharsToEscape = {
20     '&': '&',
21     '<': '&lt;',
22     '>': '&gt;'
23 };
24 String.prototype.escapeHtml = function() {
25     return this.replace(/[&<>]/g, function(c) {
26         return HtmlCharsToEscape[c] || c;
27     });
28 };
29 function escape_str(s){
30     return s != null ? s.escapeHtml() : "";
31 }
32
33 /*
34  * Void method for numbers, for consistency
35  */
36 Number.prototype.escapeHtml = function() {
37     return this;
38 };
39 function escape_price(p){
40     return p != null ? p.escapeHtml().format_price() : "";
41 }
42
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);
46 };
47 $.fn.selectTabByID = function (tabID) {
48     $("a[href='" + tabID + "']", $(this) ).tab("show");
49 };
50
51 $(document).ready(function() {
52
53     //check for a hash before setting focus
54     let hash = window.location.hash;
55     if ( ! hash ) {
56         $(".tab-pane.active input:text:first").focus();
57     }
58     $("#header_search a[data-toggle='tab']").on("shown.bs.tab", function (e) {
59         $( e.target.hash ).find("input:text:first").focus();
60     });
61
62     $(".close").click(function(){ window.close(); });
63
64     $("#checkin_search form").preventDoubleFormSubmit();
65
66     if($("#header_search #checkin_search").length > 0){
67         shortcut.add('Alt+r',function (){
68             $("#header_search").selectTabByID("#checkin_search");
69             $("#ret_barcode").focus();
70         });
71     } else {
72         shortcut.add('Alt+r',function (){
73             location.href="/cgi-bin/koha/circ/returns.pl"; });
74     }
75     if($("#header_search #circ_search").length > 0){
76         shortcut.add('Alt+u',function (){
77             $("#header_search").selectTabByID("#circ_search");
78             $("#findborrower").focus();
79         });
80     } else {
81         shortcut.add('Alt+u',function(){ location.href="/cgi-bin/koha/circ/circulation.pl"; });
82     }
83     if($("#header_search #catalog_search").length > 0){
84         shortcut.add('Alt+q',function (){
85             $("#header_search").selectTabByID("#catalog_search");
86             $("#search-form").focus();
87         });
88     } else {
89         shortcut.add('Alt+q',function(){ location.href="/cgi-bin/koha/catalogue/search.pl"; });
90     }
91     if($("#header_search #renew_search").length > 0){
92         shortcut.add('Alt+w',function (){
93             $("#header_search").selectTabByID("#renew_search");
94             $("#ren_barcode").focus();
95         });
96     } else {
97         shortcut.add('Alt+w',function(){ location.href="/cgi-bin/koha/circ/renew.pl"; });
98     }
99
100     $('#header_search .form-extra-content-toggle').on('click', function () {
101         const extraContent = $(this).closest('form').find('.form-extra-content');
102         if (extraContent.is(':visible')) {
103             extraContent.hide();
104             $(this).removeClass("extra-content-open");
105         } else {
106             extraContent.show();
107             $(this).addClass("extra-content-open");
108         }
109     });
110
111     $(".focus").focus();
112     $(".validated").each(function() {
113         $(this).validate();
114     });
115     jQuery.validator.addClassRules("decimal", {
116         number: true
117     });
118
119     $("#logout").on("click",function(){
120         logOut();
121     });
122     $("#helper").on("click",function(){
123         openHelp();
124         return false;
125     });
126
127     $("body").on("keypress", ".noEnterSubmit", function(e){
128         return checkEnter(e);
129     });
130
131     $(".keep_text").on("click",function(){
132         var field_index = $(this).parent().index();
133         keep_text( field_index );
134     });
135
136     $(".toggle_element").on("click",function(e){
137         e.preventDefault();
138         $( $(this).data("element") ).toggle();
139         if (typeof Sticky !== "undefined" && typeof hcSticky === "function") {
140             Sticky.hcSticky('update');
141         }
142     });
143
144     var navmenulist = $("#navmenulist");
145     if( navmenulist.length > 0 ){
146         var path = location.pathname.substring(1);
147         var url = window.location.toString();
148         var params = '';
149         if ( url.match(/\?(.+)$/) ) {
150             params = "?" + RegExp.$1;
151         }
152         if ($("a[href$=\"/" + path + params + "\"]", navmenulist).length == 0){
153             $("a[href$=\"/" + path + "\"]", navmenulist).addClass("current");
154         } else {
155             $("a[href$=\"/" + path + params + "\"]", navmenulist).addClass("current");
156         }
157     }
158
159     $("#catalog-search-link a").on("mouseenter mouseleave", function(){
160         $("#catalog-search-dropdown a").toggleClass("catalog-search-dropdown-hover");
161     });
162
163     if ( localStorage.getItem("lastborrowernumber") ){
164         if( $("#hiddenborrowernumber").val() != localStorage.getItem("lastborrowernumber") ) {
165             $("#lastborrowerlink").show();
166             $("#lastborrowerlink").prop("title", localStorage.getItem("lastborrowername") + " (" + localStorage.getItem("lastborrowercard") + ")");
167             $("#lastborrowerlink").prop("href", "/cgi-bin/koha/circ/circulation.pl?borrowernumber=" + localStorage.getItem("lastborrowernumber"));
168             $("#lastborrower-window").css("display", "inline-flex");
169         }
170     }
171
172     if( !localStorage.getItem("lastborrowernumber") || ( $("#hiddenborrowernumber").val() != localStorage.getItem("lastborrowernumber") && localStorage.getItem("currentborrowernumber") != $("#hiddenborrowernumber").val())) {
173         if( $("#hiddenborrowernumber").val() ){
174             localStorage.setItem("lastborrowernumber", $("#hiddenborrowernumber").val() );
175             localStorage.setItem("lastborrowername", $("#hiddenborrowername").val() );
176             localStorage.setItem("lastborrowercard", $("#hiddenborrowercard").val() );
177         }
178     }
179
180     if( $("#hiddenborrowernumber").val() ){
181         localStorage.setItem("currentborrowernumber", $("#hiddenborrowernumber").val() );
182     }
183
184     $("#lastborrower-remove").click(function() {
185         removeLastBorrower();
186         $("#lastborrower-window").hide();
187     });
188
189     /* Search results browsing */
190     /* forms with action leading to search */
191     $("form[action*='search.pl']").submit(function(){
192         $('[name^="limit"]').each(function(){
193             if( $(this).val() == '' ){
194                 $(this).prop("disabled","disabled");
195             }
196         });
197         var disabledPrior = false;
198         $(".search-term-row").each(function(){
199             if( disabledPrior ){
200                 $(this).find('select[name="op"]').prop("disabled","disabled");
201                 disabledPrior = false;
202             }
203             if( $(this).find('input[name="q"]').val() == "" ){
204                 $(this).find('input').prop("disabled","disabled");
205                 $(this).find('select').prop("disabled","disabled");
206                 disabledPrior = true;
207             }
208         });
209         resetSearchContext();
210         saveOrClearSimpleSearchParams();
211     });
212     /* any link to launch a search except navigation links */
213     $("[href*='search.pl?']").not(".nav").not('.searchwithcontext').click(function(){
214         resetSearchContext();
215     });
216     /* any link to a detail page from the results page. */
217     $("#bookbag_form a[href*='detail.pl?']").click(function(){
218         resetSearchContext();
219     });
220
221 });
222
223 function removeLastBorrower(){
224     localStorage.removeItem("lastborrowernumber");
225     localStorage.removeItem("lastborrowername");
226     localStorage.removeItem("lastborrowercard");
227     localStorage.removeItem("currentborrowernumber");
228 }
229
230 // http://jennifermadden.com/javascript/stringEnterKeyDetector.html
231 function checkEnter(e){ //e is event object passed from function invocation
232     var characterCode; // literal character code will be stored in this variable
233     if(e && e.which){ //if which property of event object is supported (NN4)
234         characterCode = e.which; //character code is contained in NN4's which property
235     } else {
236         characterCode = e.keyCode; //character code is contained in IE's keyCode property
237     }
238     if( characterCode == 13 //if generated character code is equal to ascii 13 (if enter key)
239         && e.target.nodeName == "INPUT"
240         && e.target.type != "submit" // Allow enter to submit using the submit button
241     ){
242         return false;
243     } else {
244         return true;
245     }
246 }
247
248 function clearHoldFor(){
249     Cookies.remove("holdfor", { path: '/', SameSite: 'Lax' });
250 }
251
252 function logOut(){
253     if( typeof delBasket == 'function' ){
254         delBasket('main', true);
255     }
256     clearHoldFor();
257     removeLastBorrower();
258     localStorage.removeItem("sql_reports_activetab");
259     localStorage.removeItem("searches");
260     localStorage.removeItem("bibs_selected");
261     localStorage.removeItem("patron_search_selections");
262 }
263
264 function openHelp(){
265     window.open( "/cgi-bin/koha/help.pl", "_blank");
266 }
267
268 jQuery.fn.preventDoubleFormSubmit = function() {
269     jQuery(this).submit(function() {
270         $("body, form input[type='submit'], form button[type='submit'], form a").addClass('waiting');
271         if (this.beenSubmitted)
272             return false;
273         else
274             this.beenSubmitted = true;
275     });
276 };
277
278 function openWindow(link,name,width,height) {
279     name = (typeof name == "undefined")?'popup':name;
280     width = (typeof width == "undefined")?'600':width;
281     height = (typeof height == "undefined")?'400':height;
282     //IE <= 9 can't handle a "name" with whitespace
283     try {
284         window.open(link,name,'width='+width+',height='+height+',resizable=yes,toolbar=false,scrollbars=yes,top');
285     } catch(e) {
286         window.open(link,null,'width='+width+',height='+height+',resizable=yes,toolbar=false,scrollbars=yes,top');
287     }
288 }
289
290 // Use this function to remove the focus from any element for
291 // repeated scanning actions on errors so the librarian doesn't
292 // continue scanning and miss the error.
293 function removeFocus() {
294     $(':focus').blur();
295 }
296
297 function toUC(f) {
298     var x=f.value.toUpperCase();
299     f.value=x;
300     return true;
301 }
302
303 function confirmDelete(message) {
304     return (confirm(message) ? true : false);
305 }
306
307 function confirmClone(message) {
308     return (confirm(message) ? true : false);
309 }
310
311 function playSound( sound ) {
312     if ( ! ( sound.indexOf('http://') === 0 || sound.indexOf('https://') === 0  ) ) {
313         sound = AUDIO_ALERT_PATH + sound;
314     }
315     document.getElementById("audio-alert").innerHTML = '<audio src="' + sound + '" autoplay="autoplay" autobuffer="autobuffer"></audio>';
316 }
317
318 // For keeping the text when navigating the search tabs
319 function keep_text(clicked_index) {
320     var searchboxes = document.getElementsByClassName("head-searchbox");
321     var persist = searchboxes[0].value;
322
323     for (var i = 0; i < searchboxes.length - 1; i++) {
324         if (searchboxes[i].value != searchboxes[i+1].value) {
325             if (i === searchboxes.length-2) {
326                 if (searchboxes[i].value != searchboxes[0].value) {
327                     persist = searchboxes[i].value;
328                 } else if (searchboxes.length === 2) {
329                     if (clicked_index === 0) {
330                         persist = searchboxes[1].value;
331                     }
332                 } else {
333                     persist = searchboxes[i+1].value;
334                 }
335             } else if (searchboxes[i+1].value != searchboxes[i+2].value) {
336                 persist = searchboxes[i+1].value;
337             }
338         }
339     }
340
341     for (i = 0; i < searchboxes.length; i++) {
342         searchboxes[i].value = persist;
343     }
344 }
345
346 // Extends jQuery API
347 jQuery.extend({uniqueArray:function(array){
348     return $.grep(array, function(el, index) {
349         return index === $.inArray(el, array);
350     });
351 }});
352
353 function removeByValue(arr, val) {
354     for(var i=0; i<arr.length; i++) {
355         if(arr[i] == val) {
356             arr.splice(i, 1);
357             break;
358         }
359     }
360 }
361
362 function addBibToContext( bibnum ) {
363     bibnum = parseInt(bibnum, 10);
364     var bibnums = getContextBiblioNumbers();
365     bibnums.push(bibnum);
366     setContextBiblioNumbers( bibnums );
367     setContextBiblioNumbers( $.uniqueArray( bibnums ) );
368 }
369
370 function delBibToContext( bibnum ) {
371     var bibnums = getContextBiblioNumbers();
372     removeByValue( bibnums, bibnum );
373     setContextBiblioNumbers( $.uniqueArray( bibnums ) );
374 }
375
376 function setContextBiblioNumbers( bibnums ) {
377     localStorage.setItem('bibs_selected', JSON.stringify( bibnums ) );
378 }
379
380 function getContextBiblioNumbers() {
381     var r = localStorage.getItem('bibs_selected');
382     if ( r ) {
383         return JSON.parse(r);
384     }
385     r = new Array();
386     return r;
387 }
388
389 function resetSearchContext() {
390     setContextBiblioNumbers( new Array() );
391 }
392
393 function saveOrClearSimpleSearchParams() {
394     // Simple masthead search - pass value for display on details page
395     var pulldown_selection;
396     var searchbox_value;
397     if( $("#cat-search-block select.advsearch").length ){
398         pulldown_selection = $("#cat-search-block select.advsearch").val();
399     } else {
400         pulldown_selection ="";
401     }
402     if( $("#cat-search-block #search-form").length ){
403         searchbox_value = $("#cat-search-block #search-form").val();
404     } else {
405         searchbox_value ="";
406     }
407     localStorage.setItem('cat_search_pulldown_selection', pulldown_selection );
408     localStorage.setItem('searchbox_value', searchbox_value );
409 }
410
411 function patron_autocomplete(node, options) {
412     let link_to;
413     let url_params;
414     let on_select_callback;
415
416     if (options) {
417         if (options['link-to']) {
418             link_to = options['link-to'];
419         }
420         if (options['url-params']) {
421             url_params = options['url-params'];
422         }
423         if (options['on-select-callback']) {
424             on_select_callback = options['on-select-callback'];
425         }
426     }
427     return node.autocomplete({
428         source: function (request, response) {
429             let q = buildPatronSearchQuery(request.term);
430
431             let params = {
432                 '_page': 1,
433                 '_per_page': 10,
434                 'q': JSON.stringify(q),
435                 '_order_by': '+me.surname,+me.firstname',
436             };
437             $.ajax({
438                 data: params,
439                 type: 'GET',
440                 url: '/api/v1/patrons',
441                 headers: {
442                     "x-koha-embed": "library"
443                 },
444                 success: function (data) {
445                     return response(data);
446                 },
447                 error: function (e) {
448                     if (e.state() != 'rejected') {
449                         alert(__("An error occurred. Check the logs"));
450                     }
451                     return response();
452                 }
453             });
454         },
455         minLength: 3,
456         select: function (event, ui) {
457             if (ui.item.link) {
458                 window.location.href = ui.item.link;
459             } else if (on_select_callback) {
460                 return on_select_callback(event, ui);
461             }
462         },
463         focus: function (event, ui) {
464             event.preventDefault(); // Don't replace the text field
465         },
466     })
467         .data("ui-autocomplete")
468         ._renderItem = function (ul, item) {
469             if (link_to) {
470                 item.link = link_to == 'circ'
471                     ? "/cgi-bin/koha/circ/circulation.pl"
472                     : link_to == 'reserve'
473                         ? "/cgi-bin/koha/reserve/request.pl"
474                         : "/cgi-bin/koha/members/moremember.pl";
475                 item.link += (url_params ? '?' + url_params + '&' : "?") + 'borrowernumber=' + item.patron_id;
476             } else {
477                 item.link = null;
478             }
479
480             var cardnumber = "";
481             if (item.cardnumber != "") {
482                 // Display card number in parentheses if it exists
483                 cardnumber = " (" + item.cardnumber + ") ";
484             }
485             if (item.library_id == loggedInLibrary) {
486                 loggedInClass = "ac-currentlibrary";
487             } else {
488                 loggedInClass = "";
489             }
490             return $("<li></li>")
491                 .addClass(loggedInClass)
492                 .data("ui-autocomplete-item", item)
493                 .append(
494                     ""
495                     + (item.link ? "<a href=\"" + item.link + "\">" : "<a>")
496                     + (item.surname ? item.surname.escapeHtml() : "") + ", "
497                     + (item.firstname ? item.firstname.escapeHtml() : "")
498                     + cardnumber.escapeHtml()
499                     + " <small>"
500                     + (item.date_of_birth
501                         ? $date(item.date_of_birth)
502                         + "<span class=\"age_years\"> ("
503                         + $get_age(item.date_of_birth)
504                         + " "
505                         + __("years")
506                         + ")</span>,"
507                         : ""
508                     ) + " "
509                     + $format_address(item, { no_line_break: true, include_li: false }) + " "
510                     + (!singleBranchMode
511                         ?
512                         "<span class=\"ac-library\">"
513                         + item.library.name.escapeHtml()
514                         + "</span>"
515                         : "")
516                     + "</small>"
517                     + "</a>")
518                 .appendTo(ul);
519         };
520 }
521
522 /**
523  * Build patron search query
524  * - term: The full search term input by the user
525  * You can then pass a list of options:
526  * - search_type: String 'contains' or 'starts_with', defaults to DefaultPatronSearchMethod system preference
527  * - search_fields: String comma-separated list of specific fields, defaults to DefaultPatronSearchFields system preference
528  * - extended_attribute_types: JSON object containing the patron attribute types to be searched on
529  */
530 function buildPatronSearchQuery(term, options) {
531
532     let q = [];
533     let leading_wildcard;
534     let search_fields;
535     let patterns = term.split(/[\s,]+/).filter(function (s) { return s.length });
536
537     // Bail if no patterns
538     if (patterns.length == 0) {
539         return q;
540     }
541
542     // Leading wildcard: If search_type option exists, we use that
543     if (typeof options !== 'undefined' && options.search_type) {
544         leading_wildcard = options.search_type === "contains" ? '%' : '';
545     // If not, we use DefaultPatronSearchMethod system preference instead
546     } else {
547         leading_wildcard = defaultPatronSearchMethod === 'contains' ? '%' : '';
548     }
549
550     // Search fields: If search_fields option exists, we use that
551     if (typeof options !== 'undefined' && options.search_fields) {
552         search_fields = options.search_fields;
553     // If not, we use DefaultPatronSearchFields system preference instead
554     } else {
555         search_fields = defaultPatronSearchFields;
556     }
557
558     // Add each pattern for each search field
559     let pattern_subquery_and = [];
560     patterns.forEach(function (pattern, i) {
561             let pattern_subquery_or = [];
562             search_fields.split(',').forEach(function (field, i) {
563                 pattern_subquery_or.push(
564                     { ["me." + field]: { 'like': leading_wildcard + pattern + '%' } }
565                 );
566                 if (field == 'dateofbirth') {
567                     try {
568                         let d = $date_to_rfc3339(pattern);
569                         pattern_subquery_or.push({ ["me." + field]: d });
570                     } catch {
571                         // Hide the warning if the date is not correct
572                     }
573                 }
574             });
575             pattern_subquery_and.push(pattern_subquery_or);
576         });
577     q.push({ "-and": pattern_subquery_and });
578
579     // Add full search term for each search field
580     let term_subquery_or = [];
581     search_fields.split(',').forEach(function (field, i) {
582         term_subquery_or.push(
583             { ["me." + field]: { 'like': leading_wildcard + term + '%' } }
584         );
585     });
586     q.push({ "-or": term_subquery_or });
587
588     // Add each pattern for each extended patron attributes
589     if (typeof options !== 'undefined' && options.extended_attribute_types && extendedPatronAttributes) {
590         extended_attribute_subquery_and = [];
591         patterns.forEach(function (pattern, i) {
592             let extended_attribute_sub_or = [];
593             extended_attribute_sub_or.push({
594                 "extended_attributes.value": { "like": leading_wildcard + pattern + '%' },
595                 "extended_attributes.code": options.extended_attribute_types
596             });
597             extended_attribute_subquery_and.push(extended_attribute_sub_or);
598         });
599         q.push({ "-and": extended_attribute_subquery_and });
600     }
601     return q;
602 }
603
604 function selectBsTabByHash( tabs_container_id ){
605     /* Check for location.hash in the page URL */
606     /* If present the location hash will be used to activate the correct tab */
607     var hash = document.location.hash;
608     if( hash !== "" ){
609         $('#' + tabs_container_id + ' a[href="' + hash + '"]').tab('show');
610     } else {
611         $('#' + tabs_container_id + ' a:first').tab('show');
612     }
613 }