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