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