Bug 30708: Rebase - Use name instead of url for router-links
[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, .close_window").on("click", function(e){
63         e.preventDefault();
64         window.close();
65     });
66
67     $("#checkin_search form").preventDoubleFormSubmit();
68
69     if($("#header_search #checkin_search").length > 0){
70         shortcut.add('Alt+r',function (){
71             $("#header_search").selectTabByID("#checkin_search");
72             $("#ret_barcode").focus();
73         });
74     } else {
75         shortcut.add('Alt+r',function (){
76             location.href="/cgi-bin/koha/circ/returns.pl"; });
77     }
78     if($("#header_search #circ_search").length > 0){
79         shortcut.add('Alt+u',function (){
80             $("#header_search").selectTabByID("#circ_search");
81             $("#findborrower").focus();
82         });
83     } else {
84         shortcut.add('Alt+u',function(){ location.href="/cgi-bin/koha/circ/circulation.pl"; });
85     }
86     if($("#header_search #catalog_search").length > 0){
87         shortcut.add('Alt+q',function (){
88             $("#header_search").selectTabByID("#catalog_search");
89             $("#search-form").focus();
90         });
91     } else {
92         shortcut.add('Alt+q',function(){ location.href="/cgi-bin/koha/catalogue/search.pl"; });
93     }
94     if($("#header_search #renew_search").length > 0){
95         shortcut.add('Alt+w',function (){
96             $("#header_search").selectTabByID("#renew_search");
97             $("#ren_barcode").focus();
98         });
99     } else {
100         shortcut.add('Alt+w',function(){ location.href="/cgi-bin/koha/circ/renew.pl"; });
101     }
102
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')) {
106             extraContent.hide();
107             $(this).removeClass("extra-content-open");
108         } else {
109             extraContent.show();
110             $(this).addClass("extra-content-open");
111         }
112     });
113
114     $(".focus").focus();
115     $(".validated").each(function() {
116         $(this).validate();
117     });
118     jQuery.validator.addClassRules("decimal", {
119         number: true
120     });
121
122     $("#logout").on("click",function(){
123         logOut();
124     });
125     $("#helper").on("click",function(){
126         openHelp();
127         return false;
128     });
129
130     $("body").on("keypress", ".noEnterSubmit", function(e){
131         return checkEnter(e);
132     });
133
134     $(".keep_text").on("click",function(){
135         var field_index = $(this).parent().index();
136         keep_text( field_index );
137     });
138
139     $(".toggle_element").on("click",function(e){
140         e.preventDefault();
141         $( $(this).data("element") ).toggle();
142         if (typeof Sticky !== "undefined" && typeof hcSticky === "function") {
143             Sticky.hcSticky('update');
144         }
145     });
146
147     var navmenulist = $("#navmenulist");
148     if( navmenulist.length > 0 ){
149         var path = location.pathname.substring(1);
150         var url = window.location.toString();
151         var params = '';
152         if ( url.match(/\?(.+)$/) ) {
153             params = "?" + RegExp.$1;
154         }
155         if ($("a[href$=\"/" + path + params + "\"]", navmenulist).length == 0){
156             $("a[href$=\"/" + path + "\"]", navmenulist).addClass("current");
157         } else {
158             $("a[href$=\"/" + path + params + "\"]", navmenulist).addClass("current");
159         }
160     }
161
162     $("#catalog-search-link a").on("mouseenter mouseleave", function(){
163         $("#catalog-search-dropdown a").toggleClass("catalog-search-dropdown-hover");
164     });
165
166     if ( localStorage.getItem("previousPatrons") || $("#hiddenborrowernumber").val() ){
167         var previous_patrons = [];
168         if ( localStorage.getItem("previousPatrons") ) {
169             previous_patrons = JSON.parse(localStorage.getItem("previousPatrons"));
170         }
171
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();
176             });
177
178             const previous_patron = {
179                 "borrowernumber": $("#hiddenborrowernumber").val(),
180                 "name": $("#hiddenborrowername").val(),
181                 "card": $("#hiddenborrowercard").val()
182             };
183
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));
188         }
189
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");
196
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);
202             }
203         }
204     }
205
206     if( $("#hiddenborrowernumber").val() ){
207         localStorage.setItem("currentborrowernumber", $("#hiddenborrowernumber").val() );
208     }
209
210     $("#lastborrower-remove").click(function() {
211         removeLastBorrower();
212         $("#lastborrower-window").hide();
213     });
214
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");
221             }
222         });
223         var disabledPrior = false;
224         $(".search-term-row").each(function(){
225             if( disabledPrior ){
226                 $(this).find('select[name="op"]').prop("disabled","disabled");
227                 disabledPrior = false;
228             }
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;
233             }
234         });
235         resetSearchContext();
236         saveOrClearSimpleSearchParams();
237     });
238     /* any link to launch a search except navigation links */
239     $("[href*='search.pl?']").not(".nav").not('.searchwithcontext').click(function(){
240         resetSearchContext();
241     });
242     /* any link to a detail page from the results page. */
243     $("#bookbag_form a[href*='detail.pl?']").click(function(){
244         resetSearchContext();
245     });
246
247 });
248
249 function removeLastBorrower(){
250     localStorage.removeItem("previousPatrons");
251 }
252
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
258     } else {
259         characterCode = e.keyCode; //character code is contained in IE's keyCode property
260     }
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
264     ){
265         return false;
266     } else {
267         return true;
268     }
269 }
270
271 function clearHoldFor(){
272     Cookies.remove("holdfor", { path: '/', SameSite: 'Lax' });
273 }
274
275 function logOut(){
276     if( typeof delBasket == 'function' ){
277         delBasket('main', true);
278     }
279     clearHoldFor();
280     removeLastBorrower();
281     localStorage.removeItem("sql_reports_activetab");
282     localStorage.removeItem("searches");
283     localStorage.removeItem("bibs_selected");
284     localStorage.removeItem("patron_search_selections");
285 }
286
287 function openHelp(){
288     window.open( "/cgi-bin/koha/help.pl", "_blank");
289 }
290
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)
295             return false;
296         else
297             this.beenSubmitted = true;
298     });
299 };
300
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
306     try {
307         window.open(link,name,'width='+width+',height='+height+',resizable=yes,toolbar=false,scrollbars=yes,top');
308     } catch(e) {
309         window.open(link,null,'width='+width+',height='+height+',resizable=yes,toolbar=false,scrollbars=yes,top');
310     }
311 }
312
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() {
317     $(':focus').blur();
318 }
319
320 function toUC(f) {
321     var x=f.value.toUpperCase();
322     f.value=x;
323     return true;
324 }
325
326 function confirmDelete(message) {
327     return (confirm(message) ? true : false);
328 }
329
330 function confirmClone(message) {
331     return (confirm(message) ? true : false);
332 }
333
334 function playSound( sound ) {
335     if ( ! ( sound.indexOf('http://') === 0 || sound.indexOf('https://') === 0  ) ) {
336         sound = AUDIO_ALERT_PATH + sound;
337     }
338     document.getElementById("audio-alert").innerHTML = '<audio src="' + sound + '" autoplay="autoplay" autobuffer="autobuffer"></audio>';
339 }
340
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;
345
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;
354                     }
355                 } else {
356                     persist = searchboxes[i+1].value;
357                 }
358             } else if (searchboxes[i+1].value != searchboxes[i+2].value) {
359                 persist = searchboxes[i+1].value;
360             }
361         }
362     }
363
364     for (i = 0; i < searchboxes.length; i++) {
365         searchboxes[i].value = persist;
366     }
367 }
368
369 // Extends jQuery API
370 jQuery.extend({uniqueArray:function(array){
371     return $.grep(array, function(el, index) {
372         return index === $.inArray(el, array);
373     });
374 }});
375
376 function removeByValue(arr, val) {
377     for(var i=0; i<arr.length; i++) {
378         if(arr[i] == val) {
379             arr.splice(i, 1);
380             break;
381         }
382     }
383 }
384
385 function addBibToContext( bibnum ) {
386     bibnum = parseInt(bibnum, 10);
387     var bibnums = getContextBiblioNumbers();
388     bibnums.push(bibnum);
389     setContextBiblioNumbers( bibnums );
390     setContextBiblioNumbers( $.uniqueArray( bibnums ) );
391 }
392
393 function delBibToContext( bibnum ) {
394     var bibnums = getContextBiblioNumbers();
395     removeByValue( bibnums, bibnum );
396     setContextBiblioNumbers( $.uniqueArray( bibnums ) );
397 }
398
399 function setContextBiblioNumbers( bibnums ) {
400     localStorage.setItem('bibs_selected', JSON.stringify( bibnums ) );
401 }
402
403 function getContextBiblioNumbers() {
404     var r = localStorage.getItem('bibs_selected');
405     if ( r ) {
406         return JSON.parse(r);
407     }
408     r = new Array();
409     return r;
410 }
411
412 function resetSearchContext() {
413     setContextBiblioNumbers( new Array() );
414 }
415
416 function saveOrClearSimpleSearchParams() {
417     // Simple masthead search - pass value for display on details page
418     var pulldown_selection;
419     var searchbox_value;
420     if( $("#cat-search-block select.advsearch").length ){
421         pulldown_selection = $("#cat-search-block select.advsearch").val();
422     } else {
423         pulldown_selection ="";
424     }
425     if( $("#cat-search-block #search-form").length ){
426         searchbox_value = $("#cat-search-block #search-form").val();
427     } else {
428         searchbox_value ="";
429     }
430     localStorage.setItem('cat_search_pulldown_selection', pulldown_selection );
431     localStorage.setItem('searchbox_value', searchbox_value );
432 }
433
434 function patron_autocomplete(node, options) {
435     let link_to;
436     let url_params;
437     let on_select_callback;
438
439     if (options) {
440         if (options['link-to']) {
441             link_to = options['link-to'];
442         }
443         if (options['url-params']) {
444             url_params = options['url-params'];
445         }
446         if (options['on-select-callback']) {
447             on_select_callback = options['on-select-callback'];
448         }
449     }
450     return node.autocomplete({
451         source: function (request, response) {
452             let q = buildPatronSearchQuery(request.term);
453
454             let params = {
455                 '_page': 1,
456                 '_per_page': 10,
457                 'q': JSON.stringify(q),
458                 '_order_by': '+me.surname,+me.firstname',
459             };
460             $.ajax({
461                 data: params,
462                 type: 'GET',
463                 url: '/api/v1/patrons',
464                 headers: {
465                     "x-koha-embed": "library"
466                 },
467                 success: function (data) {
468                     return response(data);
469                 },
470                 error: function (e) {
471                     if (e.state() != 'rejected') {
472                         alert(__("An error occurred. Check the logs"));
473                     }
474                     return response();
475                 }
476             });
477         },
478         minLength: 3,
479         select: function (event, ui) {
480             if (ui.item.link) {
481                 window.location.href = ui.item.link;
482             } else if (on_select_callback) {
483                 return on_select_callback(event, ui);
484             }
485         },
486         focus: function (event, ui) {
487             event.preventDefault(); // Don't replace the text field
488         },
489     })
490         .data("ui-autocomplete")
491         ._renderItem = function (ul, item) {
492             if (link_to) {
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;
499             } else {
500                 item.link = null;
501             }
502
503             var cardnumber = "";
504             if (item.cardnumber != "") {
505                 // Display card number in parentheses if it exists
506                 cardnumber = " (" + item.cardnumber + ") ";
507             }
508             if (item.library_id == loggedInLibrary) {
509                 loggedInClass = "ac-currentlibrary";
510             } else {
511                 loggedInClass = "";
512             }
513             return $("<li></li>")
514                 .addClass(loggedInClass)
515                 .data("ui-autocomplete-item", item)
516                 .append(
517                     ""
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()
523                     + " <small>"
524                     + (item.date_of_birth
525                         ? $date(item.date_of_birth)
526                         + "<span class=\"age_years\"> ("
527                         + $get_age(item.date_of_birth)
528                         + " "
529                         + __("years")
530                         + ")</span>,"
531                         : ""
532                     ) + " "
533                     + $format_address(item, { no_line_break: true, include_li: false }) + " "
534                     + (!singleBranchMode
535                         ?
536                         "<span class=\"ac-library\">"
537                         + item.library.name.escapeHtml()
538                         + "</span>"
539                         : "")
540                     + "</small>"
541                     + "</a>")
542                 .appendTo(ul);
543         };
544 }
545
546 function expandPatronSearchFields(search_fields) {
547     switch(search_fields) {
548         case 'standard':
549             return defaultPatronSearchFields;
550             break;
551         case 'full_address':
552             return 'streetnumber|streettype|address|address2|city|state|zipcode|country';
553             break;
554         case 'all_emails':
555             return 'email|emailpro|B_email';
556             break;
557         case 'all_phones':
558             return 'phone|phonepro|B_phone|altcontactphone|mobile';
559             break;
560         default:
561             return search_fields;
562     }
563 }
564
565 /**
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
572  */
573 function buildPatronSearchQuery(term, options) {
574
575     let q = [];
576     let leading_wildcard;
577     let search_fields;
578     let patterns = term.split(/[\s,]+/).filter(function (s) { return s.length });
579
580     // Bail if no patterns
581     if (patterns.length == 0) {
582         return q;
583     }
584
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
589     } else {
590         leading_wildcard = defaultPatronSearchMethod === 'contains' ? '%' : '';
591     }
592
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
597     } else {
598         search_fields = defaultPatronSearchFields;
599     }
600
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 + '%' } }
608                 );
609                 if (field == 'dateofbirth') {
610                     try {
611                         let d = $date_to_rfc3339(pattern);
612                         pattern_subquery_or.push({ ["me." + field]: d });
613                     } catch {
614                         // Hide the warning if the date is not correct
615                     }
616                 }
617             });
618             pattern_subquery_and.push(pattern_subquery_or);
619         });
620     q.push({ "-and": pattern_subquery_and });
621
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 + '%' } }
627         );
628     });
629     q.push({ "-or": term_subquery_or });
630
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
639             });
640             extended_attribute_subquery_and.push(extended_attribute_sub_or);
641         });
642         q.push({ "-and": extended_attribute_subquery_and });
643     }
644     return q;
645 }
646
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;
651     if( hash !== "" ){
652         $('#' + tabs_container_id + ' a[href="' + hash + '"]').tab('show');
653     } else {
654         $('#' + tabs_container_id + ' a:first').tab('show');
655     }
656 }