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