View | Details | Raw Unified | Return to bug 39739
Collapse All | Expand All

(-)a/koha-tmpl/opac-tmpl/bootstrap/js/datatables.js (-63 / +1151 lines)
Lines 1-4 Link Here
1
/* global __ */
2
// These default options are for translation but can be used
1
// These default options are for translation but can be used
3
// for any other datatables settings
2
// for any other datatables settings
4
// To use it, write:
3
// To use it, write:
Lines 37-45 var dataTablesDefaults = { Link Here
37
            excel: __("Excel"),
36
            excel: __("Excel"),
38
        },
37
        },
39
    },
38
    },
40
    dom: "t",
39
    dom: '<"dt-info"i><"top pager"<"table_entries"lp><"table_controls"fB>>tr<"bottom pager"ip>',
41
    buttons: ["clearFilter", "copy", "csv", "print"],
40
    buttons: [
42
    paging: false,
41
        {
42
            fade: 100,
43
            className: "dt_button_clear_filter",
44
            titleAttr: __("Clear filter"),
45
            enabled: false,
46
            text:
47
                '<i class="fa fa-lg fa-times"></i> <span class="dt-button-text">' +
48
                __("Clear filter") +
49
                "</span>",
50
            available: function (dt) {
51
                // The "clear filter" button is made available if this test returns true
52
                if (dt.settings()[0].aanFeatures.f) {
53
                    // aanFeatures.f is null if there is no search form
54
                    return true;
55
                }
56
            },
57
            action: function (e, dt, node) {
58
                dt.search("").draw("page");
59
                node.addClass("disabled");
60
            },
61
        },
62
    ],
63
    lengthMenu: [
64
        [10, 20, 50, 100, -1],
65
        [10, 20, 50, 100, __("All")],
66
    ],
67
    pageLength: 20,
68
    fixedHeader: true,
43
    initComplete: function (settings) {
69
    initComplete: function (settings) {
44
        var tableId = settings.nTable.id;
70
        var tableId = settings.nTable.id;
45
        state = settings.oLoadedState;
71
        state = settings.oLoadedState;
Lines 61-72 function toggledClearFilter(searchText, tableId) { Link Here
61
    }
87
    }
62
}
88
}
63
89
90
// Sorting on html contains
91
// <a href="foo.pl">bar</a> sort on 'bar'
92
function dt_overwrite_html_sorting_localeCompare() {
93
    jQuery.fn.dataTableExt.oSort["html-asc"] = function (a, b) {
94
        a = a.replace(/<.*?>/g, "").replace(/\s+/g, " ");
95
        b = b.replace(/<.*?>/g, "").replace(/\s+/g, " ");
96
        if (typeof (a.localeCompare == "function")) {
97
            return a.localeCompare(b);
98
        } else {
99
            return a > b ? 1 : a < b ? -1 : 0;
100
        }
101
    };
102
103
    jQuery.fn.dataTableExt.oSort["html-desc"] = function (a, b) {
104
        a = a.replace(/<.*?>/g, "").replace(/\s+/g, " ");
105
        b = b.replace(/<.*?>/g, "").replace(/\s+/g, " ");
106
        if (typeof (b.localeCompare == "function")) {
107
            return b.localeCompare(a);
108
        } else {
109
            return b > a ? 1 : b < a ? -1 : 0;
110
        }
111
    };
112
113
    jQuery.fn.dataTableExt.oSort["num-html-asc"] = function (a, b) {
114
        var x = a.replace(/<.*?>/g, "");
115
        var y = b.replace(/<.*?>/g, "");
116
        x = parseFloat(x);
117
        y = parseFloat(y);
118
        return x < y ? -1 : x > y ? 1 : 0;
119
    };
120
121
    jQuery.fn.dataTableExt.oSort["num-html-desc"] = function (a, b) {
122
        var x = a.replace(/<.*?>/g, "");
123
        var y = b.replace(/<.*?>/g, "");
124
        x = parseFloat(x);
125
        y = parseFloat(y);
126
        return x < y ? 1 : x > y ? -1 : 0;
127
    };
128
}
129
130
$.fn.dataTableExt.oSort["num-html-asc"] = function (a, b) {
131
    var x = a.replace(/<.*?>/g, "");
132
    var y = b.replace(/<.*?>/g, "");
133
    x = parseFloat(x);
134
    y = parseFloat(y);
135
    return x < y ? -1 : x > y ? 1 : 0;
136
};
137
138
$.fn.dataTableExt.oSort["num-html-desc"] = function (a, b) {
139
    var x = a.replace(/<.*?>/g, "");
140
    var y = b.replace(/<.*?>/g, "");
141
    x = parseFloat(x);
142
    y = parseFloat(y);
143
    return x < y ? 1 : x > y ? -1 : 0;
144
};
145
146
(function () {
147
    /*
148
     * Natural Sort algorithm for Javascript - Version 0.7 - Released under MIT license
149
     * Author: Jim Palmer (based on chunking idea from Dave Koelle)
150
     * Contributors: Mike Grier (mgrier.com), Clint Priest, Kyle Adams, guillermo
151
     * See: http://js-naturalsort.googlecode.com/svn/trunk/naturalSort.js
152
     */
153
    function naturalSort(a, b) {
154
        var re = /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi,
155
            sre = /(^[ ]*|[ ]*$)/g,
156
            dre =
157
                /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,
158
            hre = /^0x[0-9a-f]+$/i,
159
            ore = /^0/,
160
            // convert all to strings and trim()
161
            x = a.toString().replace(sre, "") || "",
162
            y = b.toString().replace(sre, "") || "",
163
            // chunk/tokenize
164
            xN = x
165
                .replace(re, "\0$1\0")
166
                .replace(/\0$/, "")
167
                .replace(/^\0/, "")
168
                .split("\0"),
169
            yN = y
170
                .replace(re, "\0$1\0")
171
                .replace(/\0$/, "")
172
                .replace(/^\0/, "")
173
                .split("\0"),
174
            // numeric, hex or date detection
175
            xD =
176
                parseInt(x.match(hre), 10) ||
177
                (xN.length != 1 && x.match(dre) && Date.parse(x)),
178
            yD =
179
                parseInt(y.match(hre), 10) ||
180
                (xD && y.match(dre) && Date.parse(y)) ||
181
                null;
182
        // first try and sort Hex codes or Dates
183
        if (yD)
184
            if (xD < yD) return -1;
185
            else if (xD > yD) return 1;
186
        // natural sorting through split numeric strings and default strings
187
        for (
188
            var cLoc = 0, numS = Math.max(xN.length, yN.length);
189
            cLoc < numS;
190
            cLoc++
191
        ) {
192
            // find floats not starting with '0', string or 0 if not defined (Clint Priest)
193
            var oFxNcL =
194
                (!(xN[cLoc] || "").match(ore) && parseFloat(xN[cLoc])) ||
195
                xN[cLoc] ||
196
                0;
197
            var oFyNcL =
198
                (!(yN[cLoc] || "").match(ore) && parseFloat(yN[cLoc])) ||
199
                yN[cLoc] ||
200
                0;
201
            // handle numeric vs string comparison - number < string - (Kyle Adams)
202
            if (isNaN(oFxNcL) !== isNaN(oFyNcL)) return isNaN(oFxNcL) ? 1 : -1;
203
            // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
204
            else if (typeof oFxNcL !== typeof oFyNcL) {
205
                oFxNcL += "";
206
                oFyNcL += "";
207
            }
208
            if (oFxNcL < oFyNcL) return -1;
209
            if (oFxNcL > oFyNcL) return 1;
210
        }
211
        return 0;
212
    }
213
214
    jQuery.extend(jQuery.fn.dataTableExt.oSort, {
215
        "natural-asc": function (a, b) {
216
            return naturalSort(a, b);
217
        },
218
219
        "natural-desc": function (a, b) {
220
            return naturalSort(a, b) * -1;
221
        },
222
    });
223
})();
224
225
/* Plugin to allow sorting on data stored in a span's title attribute
226
 *
227
 * Ex: <td><span title="[% ISO_date %]">[% formatted_date %]</span></td>
228
 *
229
 * In DataTables config:
230
 *     "data": [
231
 *        { "type": "title-string" },
232
 *      ]
233
 * http://datatables.net/plug-ins/sorting#hidden_title_string
234
 */
235
jQuery.extend(jQuery.fn.dataTableExt.oSort, {
236
    "title-string-pre": function (a) {
237
        var m = a.match(/title="(.*?)"/);
238
        if (null !== m && m.length) {
239
            return m[1].toLowerCase();
240
        }
241
        return "";
242
    },
243
244
    "title-string-asc": function (a, b) {
245
        return a < b ? -1 : a > b ? 1 : 0;
246
    },
247
248
    "title-string-desc": function (a, b) {
249
        return a < b ? 1 : a > b ? -1 : 0;
250
    },
251
});
252
64
(function () {
253
(function () {
65
    /* Plugin to allow text sorting to ignore articles
254
    /* Plugin to allow text sorting to ignore articles
66
     *
255
     *
67
     * In DataTables config:
256
     * In DataTables config:
68
     *     "aoColumns": [
257
     *     "data": [
69
     *        { "sType": "anti-the" },
258
     *        { "type": "anti-the" },
70
     *      ]
259
     *      ]
71
     * Based on the plugin found here:
260
     * Based on the plugin found here:
72
     * http://datatables.net/plug-ins/sorting#anti_the
261
     * http://datatables.net/plug-ins/sorting#anti_the
Lines 79-85 function toggledClearFilter(searchText, tableId) { Link Here
79
    if (config_exclude_articles_from_sort) {
268
    if (config_exclude_articles_from_sort) {
80
        var articles = config_exclude_articles_from_sort.split(" ");
269
        var articles = config_exclude_articles_from_sort.split(" ");
81
        var rpattern = "";
270
        var rpattern = "";
82
        for (var i = 0; i < articles.length; i++) {
271
        for (i = 0; i < articles.length; i++) {
83
            rpattern += "^" + articles[i] + " ";
272
            rpattern += "^" + articles[i] + " ";
84
            if (i < articles.length - 1) {
273
            if (i < articles.length - 1) {
85
                rpattern += "|";
274
                rpattern += "|";
Lines 106-117 function toggledClearFilter(searchText, tableId) { Link Here
106
    });
295
    });
107
})();
296
})();
108
297
298
// Remove string between NSB NSB characters
299
$.fn.dataTableExt.oSort["nsb-nse-asc"] = function (a, b) {
300
    var pattern = new RegExp("\x88.*\x89");
301
    a = a.replace(pattern, "");
302
    b = b.replace(pattern, "");
303
    return a > b ? 1 : a < b ? -1 : 0;
304
};
305
$.fn.dataTableExt.oSort["nsb-nse-desc"] = function (a, b) {
306
    var pattern = new RegExp("\x88.*\x89");
307
    a = a.replace(pattern, "");
308
    b = b.replace(pattern, "");
309
    return b > a ? 1 : b < a ? -1 : 0;
310
};
311
312
/* Define two custom functions (asc and desc) for basket callnumber sorting */
313
jQuery.fn.dataTableExt.oSort["callnumbers-asc"] = function (x, y) {
314
    var x_array = x.split("<div>");
315
    var y_array = y.split("<div>");
316
317
    /* Pop the first elements, they are empty strings */
318
    x_array.shift();
319
    y_array.shift();
320
321
    x_array = jQuery.map(x_array, function (a) {
322
        return parse_callnumber(a);
323
    });
324
    y_array = jQuery.map(y_array, function (a) {
325
        return parse_callnumber(a);
326
    });
327
328
    x_array.sort();
329
    y_array.sort();
330
331
    x = x_array.shift();
332
    y = y_array.shift();
333
334
    if (!x) {
335
        x = "";
336
    }
337
    if (!y) {
338
        y = "";
339
    }
340
341
    return x < y ? -1 : x > y ? 1 : 0;
342
};
343
344
jQuery.fn.dataTableExt.oSort["callnumbers-desc"] = function (x, y) {
345
    var x_array = x.split("<div>");
346
    var y_array = y.split("<div>");
347
348
    /* Pop the first elements, they are empty strings */
349
    x_array.shift();
350
    y_array.shift();
351
352
    x_array = jQuery.map(x_array, function (a) {
353
        return parse_callnumber(a);
354
    });
355
    y_array = jQuery.map(y_array, function (a) {
356
        return parse_callnumber(a);
357
    });
358
359
    x_array.sort();
360
    y_array.sort();
361
362
    x = x_array.pop();
363
    y = y_array.pop();
364
365
    if (!x) {
366
        x = "";
367
    }
368
    if (!y) {
369
        y = "";
370
    }
371
372
    return x < y ? 1 : x > y ? -1 : 0;
373
};
374
375
function parse_callnumber(html) {
376
    var array = html.split('<span class="callnumber">');
377
    if (array[1]) {
378
        array = array[1].split("</span>");
379
        return array[0];
380
    } else {
381
        return "";
382
    }
383
}
384
385
// see http://www.datatables.net/examples/advanced_init/footer_callback.html
386
function footer_column_sum(api, column_numbers) {
387
    // Remove the formatting to get integer data for summation
388
    var intVal = function (i) {
389
        if (typeof i === "number") {
390
            if (isNaN(i)) return 0;
391
            return i;
392
        } else if (typeof i === "string") {
393
            var value = i.replace(/[a-zA-Z ,.]/g, "") * 1;
394
            if (isNaN(value)) return 0;
395
            return value;
396
        }
397
        return 0;
398
    };
399
400
    for (var indice = 0; indice < column_numbers.length; indice++) {
401
        var column_number = column_numbers[indice];
402
403
        var total = 0;
404
        var cells = api
405
            .column(column_number, { page: "current" })
406
            .nodes()
407
            .to$()
408
            .find("span.total_amount");
409
        var budgets_totaled = [];
410
        $(cells).each(function () {
411
            budgets_totaled.push($(this).data("self_id"));
412
        });
413
        $(cells).each(function () {
414
            if (
415
                $(this).data("parent_id") &&
416
                $.inArray($(this).data("parent_id"), budgets_totaled) > -1
417
            ) {
418
                return;
419
            } else {
420
                total += intVal($(this).html());
421
            }
422
        });
423
        total /= 100; // Hard-coded decimal precision
424
425
        // Update footer
426
        $(api.column(column_number).footer()).html(total.format_price());
427
    }
428
}
429
430
function filterDataTable(table, column, term) {
431
    if (column) {
432
        table.column(column).search(term).draw("page");
433
    } else {
434
        table.search(term).draw("page");
435
    }
436
}
437
438
jQuery.fn.dataTable.ext.errMode = function (settings, note, message) {
439
    if (settings && settings.jqXHR) {
440
        console.log(
441
            "Got %s (%s)".format(
442
                settings.jqXHR.status,
443
                settings.jqXHR.statusText
444
            )
445
        );
446
        alert(
447
            __(
448
                "Something went wrong when loading the table.\n%s: %s. \n%s"
449
            ).format(
450
                settings.jqXHR.status,
451
                settings.jqXHR.statusText,
452
                settings.jqXHR.responseJSON &&
453
                    settings.jqXHR.responseJSON.errors
454
                    ? settings.jqXHR.responseJSON.errors
455
                          .map(m => m.message)
456
                          .join("\n")
457
                    : ""
458
            )
459
        );
460
    } else {
461
        alert(__("Something went wrong when loading the table."));
462
    }
463
    console.log(message);
464
};
465
466
function _dt_default_ajax(params) {
467
    let default_filters = params.default_filters;
468
    let options = params.options;
469
470
    if (
471
        !options.criteria ||
472
        ["contains", "starts_with", "ends_with", "exact"].indexOf(
473
            options.criteria.toLowerCase()
474
        ) === -1
475
    )
476
        options.criteria = "contains";
477
    options.criteria = options.criteria.toLowerCase();
478
479
    return {
480
        type: "GET",
481
        cache: true,
482
        dataSrc: "data",
483
        beforeSend: function (xhr, settings) {
484
            this._xhr = xhr;
485
            if (options.embed) {
486
                xhr.setRequestHeader(
487
                    "x-koha-embed",
488
                    Array.isArray(options.embed)
489
                        ? options.embed.join(",")
490
                        : options.embed
491
                );
492
            }
493
        },
494
        dataFilter: function (data, type) {
495
            var json = { data: JSON.parse(data) };
496
            if ((total = this._xhr.getResponseHeader("x-total-count"))) {
497
                json.recordsTotal = total;
498
                json.recordsFiltered = total;
499
            }
500
            if ((total = this._xhr.getResponseHeader("x-base-total-count"))) {
501
                json.recordsTotal = total;
502
            }
503
            if ((draw = this._xhr.getResponseHeader("x-koha-request-id"))) {
504
                json.draw = draw;
505
            }
506
507
            return JSON.stringify(json);
508
        },
509
        data: function (data, settings) {
510
            var length = data.length;
511
            var start = data.start;
512
513
            let api = new $.fn.dataTable.Api(settings);
514
            const global_search = api.search();
515
516
            var dataSet = {
517
                _page: Math.floor(start / length) + 1,
518
                _per_page: length,
519
            };
520
521
            function build_query(col, value) {
522
                var parts = [];
523
                var attributes = col.data.split(":");
524
                for (var i = 0; i < attributes.length; i++) {
525
                    var part = {};
526
                    var attr = attributes[i];
527
                    let default_build = true;
528
                    let criteria = options.criteria;
529
                    if (value.match(/^\^(.*)\$$/)) {
530
                        value = value.replace(/^\^/, "").replace(/\$$/, "");
531
                        criteria = "exact";
532
                    } else {
533
                        // escape SQL LIKE special characters %
534
                        value = value.replace(/(\%|\\)/g, "\\$1");
535
                    }
536
537
                    let built_value;
538
                    if (col.type == "date") {
539
                        let rfc3339 = $date_to_rfc3339(value);
540
                        if (rfc3339 != "Invalid Date") {
541
                            built_value = rfc3339;
542
                        }
543
                    }
544
545
                    if (col.datatype !== undefined) {
546
                        default_build = false;
547
                        let coded_datatype =
548
                            col.datatype.match(/^coded_value:(.*)/);
549
                        if (col.datatype == "related-object") {
550
                            let query_term = value;
551
552
                            if (criteria != "exact") {
553
                                query_term = {
554
                                    like:
555
                                        (["contains", "ends_with"].indexOf(
556
                                            criteria
557
                                        ) !== -1
558
                                            ? "%"
559
                                            : "") +
560
                                        value +
561
                                        (["contains", "starts_with"].indexOf(
562
                                            criteria
563
                                        ) !== -1
564
                                            ? "%"
565
                                            : ""),
566
                                };
567
                            }
568
569
                            part = {
570
                                [col.related + "." + col.relatedKey]:
571
                                    col.relatedValue,
572
                                [col.related + "." + col.relatedSearchOn]:
573
                                    query_term,
574
                            };
575
                        } else if (
576
                            coded_datatype &&
577
                            coded_datatype.length > 1
578
                        ) {
579
                            if (global_search.length || value.length) {
580
                                coded_datatype = coded_datatype[1];
581
                                const search_value = value.length
582
                                    ? value
583
                                    : global_search;
584
                                const regex = new RegExp(
585
                                    `^${search_value}`,
586
                                    "i"
587
                                );
588
                                if (
589
                                    coded_values &&
590
                                    coded_values.hasOwnProperty(coded_datatype)
591
                                ) {
592
                                    let codes = [
593
                                        ...coded_values[
594
                                            coded_datatype
595
                                        ].entries(),
596
                                    ]
597
                                        .filter(([label]) => regex.test(label))
598
                                        .map(([, code]) => code);
599
600
                                    if (codes.length) {
601
                                        part[
602
                                            !attr.includes(".")
603
                                                ? "me." + attr
604
                                                : attr
605
                                        ] = codes;
606
                                    } else {
607
                                        // Coded value not found using the description, fallback to code
608
                                        default_build = true;
609
                                    }
610
                                } else {
611
                                    console.log(
612
                                        "coded datatype %s not supported yet".format(
613
                                            coded_datatype
614
                                        )
615
                                    );
616
                                }
617
                            } else {
618
                                default_build = true;
619
                            }
620
                        } else {
621
                            console.log(
622
                                "datatype %s not supported yet".format(
623
                                    col.datatype
624
                                )
625
                            );
626
                        }
627
                    }
628
                    if (default_build) {
629
                        let value_part;
630
                        if (criteria === "exact") {
631
                            value_part = built_value
632
                                ? [value, built_value]
633
                                : value;
634
                        } else {
635
                            let like = {
636
                                like:
637
                                    (["contains", "ends_with"].indexOf(
638
                                        criteria
639
                                    ) !== -1
640
                                        ? "%"
641
                                        : "") +
642
                                    value +
643
                                    (["contains", "starts_with"].indexOf(
644
                                        criteria
645
                                    ) !== -1
646
                                        ? "%"
647
                                        : ""),
648
                            };
649
                            value_part = built_value
650
                                ? [like, built_value]
651
                                : like;
652
                        }
653
654
                        part[!attr.includes(".") ? "me." + attr : attr] =
655
                            value_part;
656
                    }
657
658
                    if (Object.keys(part).length) parts.push(part);
659
                }
660
                return parts;
661
            }
662
663
            var filter = data.search.value;
664
            // Build query for each column filter
665
            var and_query_parameters = settings.aoColumns
666
                .filter(function (col) {
667
                    return (
668
                        col.bSearchable &&
669
                        typeof col.data == "string" &&
670
                        data.columns[col.idx].search.value != ""
671
                    );
672
                })
673
                .map(function (col) {
674
                    var value = data.columns[col.idx].search.value;
675
                    return build_query(col, value);
676
                })
677
                .map(function r(e) {
678
                    return $.isArray(e) ? $.map(e, r) : e;
679
                });
680
681
            // Build query for the global search filter
682
            var or_query_parameters = settings.aoColumns
683
                .filter(function (col) {
684
                    return col.bSearchable && filter != "";
685
                })
686
                .map(function (col) {
687
                    var value = filter;
688
                    return build_query(col, value);
689
                })
690
                .map(function r(e) {
691
                    return $.isArray(e) ? $.map(e, r) : e;
692
                });
693
694
            if (default_filters) {
695
                let additional_filters = {};
696
                for (f in default_filters) {
697
                    let k;
698
                    let v;
699
                    if (typeof default_filters[f] === "function") {
700
                        let val = default_filters[f]();
701
                        if (val != undefined && val != "") {
702
                            k = f;
703
                            v = val;
704
                        }
705
                    } else {
706
                        k = f;
707
                        v = default_filters[f];
708
                    }
709
710
                    // Pass to -or if you want a separate OR clause
711
                    // It's not the usual DBIC notation!
712
                    if (f == "-or") {
713
                        if (v) or_query_parameters.push(v);
714
                    } else if (f == "-and") {
715
                        if (v) and_query_parameters.push(v);
716
                    } else if (v) {
717
                        additional_filters[k] = v;
718
                    }
719
                }
720
                if (Object.keys(additional_filters).length) {
721
                    and_query_parameters.push(additional_filters);
722
                }
723
            }
724
            query_parameters = and_query_parameters;
725
            if (or_query_parameters.length) {
726
                query_parameters.push(or_query_parameters);
727
            }
728
729
            if (query_parameters.length) {
730
                query_parameters = JSON.stringify(
731
                    query_parameters.length === 1
732
                        ? query_parameters[0]
733
                        : { "-and": query_parameters }
734
                );
735
                dataSet.q = query_parameters;
736
            }
737
738
            dataSet._match = options.criteria;
739
740
            if (data["draw"] !== undefined) {
741
                settings.ajax.headers = { "x-koha-request-id": data.draw };
742
            }
743
744
            if (options.columns) {
745
                var order = data.order;
746
                var orderArray = new Array();
747
                order.forEach(function (e, i) {
748
                    var order_col = e.column;
749
                    var order_by = options.columns[order_col].data;
750
                    order_by = order_by.split(":");
751
                    var order_dir = e.dir == "asc" ? "+" : "-";
752
                    Array.prototype.push.apply(
753
                        orderArray,
754
                        order_by.map(
755
                            x => order_dir + (!x.includes(".") ? "me." + x : x)
756
                        )
757
                    );
758
                });
759
                dataSet._order_by = orderArray
760
                    .filter((v, i, a) => a.indexOf(v) === i)
761
                    .join(",");
762
            }
763
764
            return dataSet;
765
        },
766
    };
767
}
768
769
function build_url_with_state(dt, table_settings) {
770
    let table_key = "DataTables_%s_%s_%s".format(
771
        table_settings.module,
772
        table_settings.page,
773
        table_settings.table
774
    );
775
776
    let state = JSON.stringify(dt.state());
777
    delete state.time;
778
    let searchParams = new URLSearchParams(window.location.search);
779
    searchParams.set(table_key + "_state", btoa(state));
780
781
    return (
782
        window.location.origin +
783
        window.location.pathname +
784
        "?" +
785
        searchParams.toString() +
786
        window.location.hash
787
    );
788
}
789
109
function _dt_buttons(params) {
790
function _dt_buttons(params) {
110
    let settings = params.settings || {};
791
    let settings = params.settings || {};
111
    let table_settings = params.table_settings;
792
    let table_settings = params.table_settings;
112
793
113
    var exportColumns = ":visible:not(.no-export)";
794
    var exportColumns = ":visible:not(.no-export)";
114
795
796
    const export_format_spreadsheet = {
797
        body: function (data, row, column, node) {
798
            var newnode = $(node);
799
800
            if (newnode.find(".no-export").length > 0) {
801
                newnode = newnode.clone();
802
                newnode.find(".no-export").remove();
803
            }
804
            let trimmed_str = newnode.text().replace(/\n/g, " ").trim();
805
            const unsafeCharacters = /^[=+\-@\t\r]/;
806
            if (unsafeCharacters.test(trimmed_str)) {
807
                trimmed_str = "'" + trimmed_str;
808
            }
809
            return trimmed_str;
810
        },
811
    };
812
115
    var export_format = {
813
    var export_format = {
116
        body: function (data, row, column, node) {
814
        body: function (data, row, column, node) {
117
            var newnode = $(node);
815
            var newnode = $(node);
Lines 130-143 function _dt_buttons(params) { Link Here
130
            extend: "excelHtml5",
828
            extend: "excelHtml5",
131
            exportOptions: {
829
            exportOptions: {
132
                columns: exportColumns,
830
                columns: exportColumns,
133
                format: export_format,
831
                format: export_format_spreadsheet,
134
            },
832
            },
135
        },
833
        },
136
        {
834
        {
137
            extend: "csvHtml5",
835
            extend: "csvHtml5",
138
            exportOptions: {
836
            exportOptions: {
139
                columns: exportColumns,
837
                columns: exportColumns,
140
                format: export_format,
838
                format: export_format_spreadsheet,
141
            },
839
            },
142
        },
840
        },
143
        {
841
        {
Lines 156-201 function _dt_buttons(params) { Link Here
156
        },
854
        },
157
    ];
855
    ];
158
856
159
    let buttons = [
857
    let buttons = [];
160
        {
858
    buttons.push({
161
            fade: 100,
859
        fade: 100,
162
            className: "dt_button_clear_filter",
860
        className: "dt_button_clear_filter",
163
            titleAttr: __("Clear filter"),
861
        titleAttr: __("Clear filter"),
164
            enabled: false,
862
        enabled: false,
165
            text:
863
        text:
166
                '<i class="fa fa-lg fa-times" aria-hidden="true"></i> <span class="dt-button-text">' +
864
            '<i class="fa fa-lg fa-remove"></i> <span class="dt-button-text">' +
167
                __("Clear filter") +
865
            __("Clear filter") +
168
                "</span>",
866
            "</span>",
169
            action: function (e, dt, node, config) {
867
        action: function (e, dt, node, config) {
170
                dt.search("").draw("page");
868
            dt.search("").draw("page");
171
                node.addClass("disabled");
869
            node.addClass("disabled");
172
            },
173
        },
870
        },
174
        {
871
    });
175
            extend: "csvHtml5",
176
            text: __("CSV"),
177
            exportOptions: {
178
                columns: exportColumns,
179
                format: export_format,
180
            },
181
        },
182
        {
183
            extend: "copyHtml5",
184
            text: __("Copy"),
185
            exportOptions: {
186
                columns: exportColumns,
187
                format: export_format,
188
            },
189
        },
190
        {
191
            extend: "print",
192
            text: __("Print"),
193
            exportOptions: {
194
                columns: exportColumns,
195
                format: export_format,
196
            },
197
        },
198
    ];
199
872
200
    // Retrieving bKohaColumnsUseNames from the options passed to the constructor, not DT's settings
873
    // Retrieving bKohaColumnsUseNames from the options passed to the constructor, not DT's settings
201
    // But ideally should be retrieved using table.data()
874
    // But ideally should be retrieved using table.data()
Lines 223-229 function _dt_buttons(params) { Link Here
223
            className: "columns_controls",
896
            className: "columns_controls",
224
            titleAttr: __("Columns settings"),
897
            titleAttr: __("Columns settings"),
225
            text:
898
            text:
226
                '<i class="fa fa-lg fa-gear" aria-hidden="true"></i> <span class="dt-button-text">' +
899
                '<i class="fa fa-lg fa-gear"></i> <span class="dt-button-text">' +
227
                __("Columns") +
900
                __("Columns") +
228
                "</span>",
901
                "</span>",
229
            exportOptions: {
902
            exportOptions: {
Lines 232-237 function _dt_buttons(params) { Link Here
232
        });
905
        });
233
    }
906
    }
234
907
908
    buttons.push({
909
        extend: "collection",
910
        autoClose: true,
911
        fade: 100,
912
        className: "export_controls",
913
        titleAttr: __("Export or print"),
914
        text:
915
            '<i class="fa fa-lg fa-download"></i> <span class="dt-button-text">' +
916
            __("Export") +
917
            "</span>",
918
        buttons: export_buttons,
919
    });
920
921
    if (table_settings) {
922
        const writeToClipboard = async (text, node) => {
923
            await navigator.clipboard.writeText(text);
924
            $(node)
925
                .tooltip({ trigger: "manual", title: __("Copied!") })
926
                .tooltip("show");
927
        };
928
        buttons.push({
929
            autoClose: true,
930
            fade: 100,
931
            className: "copyConditions_controls",
932
            titleAttr: __("Copy shareable link"),
933
            text:
934
                '<i class="fa fa-lg fa-copy"></i> <span class="dt-button-text">' +
935
                __("Copy shareable link") +
936
                "</span>",
937
            action: function (e, dt, node, config) {
938
                const url = build_url_with_state(dt, table_settings);
939
940
                if (navigator.clipboard && navigator.clipboard.writeText) {
941
                    writeToClipboard(url, node);
942
                }
943
            },
944
        });
945
    }
946
235
    return buttons;
947
    return buttons;
236
}
948
}
237
949
Lines 266-271 function _dt_visibility(table_settings, table_dt) { Link Here
266
    return hidden_ids;
978
    return hidden_ids;
267
}
979
}
268
980
981
function _dt_on_visibility(add_filters, table_node, table_dt) {
982
    // FIXME REPLACE ME
983
    if (typeof columnsInit == "function") {
984
        // This function can be created separately and used to trigger
985
        // an event after the DataTable has loaded AND column visibility
986
        // has been updated according to the table's configuration
987
        columnsInit();
988
    }
989
}
990
991
function _dt_add_filters(table_node, table_dt, filters_options = {}) {
992
    if (!$(table_node).length) return;
993
994
    $(table_node).find("thead tr:eq(1)").remove(); // Remove if one exists already
995
    $(table_node)
996
        .find("thead tr")
997
        .clone()
998
        .appendTo($(table_node).find("thead"));
999
1000
    let visibility = table_dt.columns().visible();
1001
    let columns = table_dt.settings()[0].aoColumns;
1002
    table_dt.columns().every(function () {
1003
        var column = this;
1004
        let i = column.index();
1005
        var visible_i = table_dt.column.index("fromData", i);
1006
        let th = $(table_node).find(
1007
            "thead tr:eq(1) th:eq(%s)".format(visible_i)
1008
        );
1009
        var is_searchable = columns[i].bSearchable;
1010
        $(th)
1011
            .removeClass("sorting")
1012
            .removeClass("sorting_asc")
1013
            .removeClass("sorting_desc");
1014
        if (is_searchable) {
1015
            let input_type = "input";
1016
            let existing_search = column.search();
1017
            if ($(th).data("filter") || filters_options.hasOwnProperty(i)) {
1018
                input_type = "select";
1019
                let filter_type = $(th).data("filter");
1020
                let select = $(
1021
                    '<select class="dt-select-filter"><option value=""></option></select'
1022
                );
1023
1024
                // FIXME eval here is bad and dangerous, how do we workaround that?
1025
                if (!filters_options.hasOwnProperty(i)) {
1026
                    filters_options[i] = eval(filter_type);
1027
                } else if (typeof filters_options[i] === "function") {
1028
                    filters_options[i] = filters_options[i](table_dt);
1029
                }
1030
                $(filters_options[i]).each(function () {
1031
                    let o = $(
1032
                        '<option value="%s">%s</option>'.format(
1033
                            this._id,
1034
                            this._str
1035
                        )
1036
                    );
1037
                    // Compare with lc, or selfreg won't match ^SELFREG$ for instance, see bug 32517
1038
                    // This is only for category, we might want to apply it only in this case.
1039
                    existing_search = existing_search.toLowerCase();
1040
                    if (
1041
                        existing_search === this._id ||
1042
                        (existing_search &&
1043
                            this._id.toLowerCase().match(existing_search))
1044
                    ) {
1045
                        o.prop("selected", "selected");
1046
                    }
1047
                    o.appendTo(select);
1048
                });
1049
                $(th).html(select);
1050
            } else {
1051
                var title = $(th).text();
1052
                if (existing_search) {
1053
                    $(th).html(
1054
                        '<input type="text" value="%s" style="width: 100%" />'.format(
1055
                            existing_search
1056
                        )
1057
                    );
1058
                } else {
1059
                    var search_title = __("%s search").format(title);
1060
                    $(th).html(
1061
                        '<input type="text" placeholder="%s" style="width: 100%" />'.format(
1062
                            search_title
1063
                        )
1064
                    );
1065
                }
1066
            }
1067
        } else {
1068
            $(th).html("");
1069
        }
1070
    });
1071
    _dt_add_delay_filters(table_dt, table_node);
1072
    table_dt.fixedHeader.adjust();
1073
}
1074
1075
function _dt_add_delay(table_dt, table_node) {
1076
    let delay_ms = 500;
1077
1078
    let search = DataTable.util.debounce(function (val) {
1079
        table_dt.search(val);
1080
        table_dt.draw();
1081
    }, delay_ms);
1082
1083
    $("#" + table_node.attr("id") + "_wrapper")
1084
        .find(".dt-input")
1085
        .unbind()
1086
        .bind("keyup", search(this.value));
1087
}
1088
1089
function _dt_add_delay_filters(table_dt, table_node) {
1090
    let delay_ms = 500;
1091
1092
    let col_input_search = DataTable.util.debounce(function (i, val) {
1093
        table_dt.column(i).search(val).draw();
1094
    }, delay_ms);
1095
    let col_select_search = DataTable.util.debounce(function (i, val) {
1096
        table_dt.column(i).search(val, true, false).draw();
1097
    }, delay_ms);
1098
1099
    $(table_node)
1100
        .find("thead tr:eq(1) th")
1101
        .each(function (visible_i) {
1102
            var i = table_dt.column.index("fromVisible", visible_i);
1103
            $(this)
1104
                .find("input")
1105
                .unbind()
1106
                .bind("keyup change", function (e) {
1107
                    if (e.keyCode === undefined) return;
1108
                    col_input_search(i, this.value);
1109
                });
1110
1111
            $(this)
1112
                .find("select")
1113
                .unbind()
1114
                .bind("keyup change", function () {
1115
                    let value = this.value.length ? "^" + this.value + "$" : "";
1116
                    col_select_search(i, this.value);
1117
                });
1118
        });
1119
}
1120
1121
function _dt_save_restore_state(table_settings, external_filter_nodes = {}) {
1122
    let table_key = "DataTables_%s_%s_%s".format(
1123
        table_settings.module,
1124
        table_settings.page,
1125
        table_settings.table
1126
    );
1127
1128
    let default_save_state = table_settings.default_save_state;
1129
    let default_save_state_search = table_settings.default_save_state_search;
1130
1131
    let stateSaveCallback = function (settings, data) {
1132
        localStorage.setItem(table_key, JSON.stringify(data));
1133
    };
1134
1135
    function set_default(table_settings, table_dt) {
1136
        let columns = new Array(table_dt.columns()[0].length).fill({
1137
            visible: true,
1138
        });
1139
        let hidden_ids = _dt_visibility(table_settings, table_dt);
1140
        hidden_ids.forEach((id, i) => {
1141
            columns[id] = { visible: false };
1142
        });
1143
        // State is not loaded if time is not passed
1144
        return { columns, time: new Date() };
1145
    }
1146
    let stateLoadCallback = function (settings) {
1147
        // Load state from URL
1148
        const url = new URL(window.location.href);
1149
        let state_from_url = url.searchParams.get(table_key + "_state");
1150
        if (state_from_url) {
1151
            $("#" + settings.nTable.id).data("loaded_from_state", true);
1152
            return JSON.parse(atob(state_from_url));
1153
        }
1154
1155
        if (!default_save_state) return set_default(table_settings, this.api());
1156
1157
        let state = localStorage.getItem(table_key);
1158
        if (!state) return set_default(table_settings, this.api());
1159
1160
        state = JSON.parse(state);
1161
1162
        if (default_save_state_search) {
1163
            $("#" + settings.nTable.id).data("loaded_from_state", true);
1164
        } else {
1165
            delete state.search;
1166
            state.columns.forEach(c => delete c.search);
1167
        }
1168
        return state;
1169
    };
1170
1171
    let stateSaveParams = function (settings, data) {
1172
        // FIXME Selector in on the whole DOM, we don't know where the filters are
1173
        // If others exist on the same page this will lead to unexpected behaviours
1174
        // Should be safe so far as: patron search use the same code for the different searches
1175
        // but only the main one has the table settings (and so the state saved)
1176
        data.external_filters = Object.keys(external_filter_nodes).reduce(
1177
            (r, k) => {
1178
                let node = $(external_filter_nodes[k]);
1179
                let tag_name = node.prop("tagName");
1180
                if (tag_name == "INPUT" && node.prop("type") == "checkbox") {
1181
                    r[k] = $(external_filter_nodes[k]).prop("checked");
1182
                } else if (tag_name == "INPUT" || tag_name == "SELECT") {
1183
                    r[k] = $(external_filter_nodes[k]).val();
1184
                } else {
1185
                    console.log(
1186
                        "Tag '%s' not supported yet for DT state".format(
1187
                            tag_name
1188
                        )
1189
                    );
1190
                }
1191
                return r;
1192
            },
1193
            {}
1194
        );
1195
    };
1196
    let stateLoadParams = function (settings, data) {
1197
        if (!$("#" + settings.nTable.id).data("loaded_from_state")) return;
1198
1199
        if (data.external_filters) {
1200
            Object.keys(external_filter_nodes).forEach((k, i) => {
1201
                if (data.external_filters.hasOwnProperty(k)) {
1202
                    let node = $(external_filter_nodes[k]);
1203
                    let tag_name = node.prop("tagName");
1204
                    let value = data.external_filters[k];
1205
                    if (
1206
                        tag_name == "INPUT" &&
1207
                        node.prop("type") == "checkbox"
1208
                    ) {
1209
                        node.prop("checked", value);
1210
                    } else if (
1211
                        tag_name == "INPUT" &&
1212
                        node.hasClass("flatpickr")
1213
                    ) {
1214
                        const fp = document.querySelector(
1215
                            external_filter_nodes[k]
1216
                        )._flatpickr;
1217
                        fp.setDate(value);
1218
                    } else if (tag_name == "INPUT" || tag_name == "SELECT") {
1219
                        node.val(value);
1220
                    } else {
1221
                        console.log(
1222
                            "Tag '%s' not supported yet for DT state".format(
1223
                                tag_name
1224
                            )
1225
                        );
1226
                    }
1227
                }
1228
            });
1229
        }
1230
    };
1231
1232
    return {
1233
        stateSave: true,
1234
        stateSaveCallback,
1235
        stateLoadCallback,
1236
        stateSaveParams,
1237
        stateLoadParams,
1238
    };
1239
}
1240
1241
function update_search_description(
1242
    table_node,
1243
    table_dt,
1244
    additional_search_descriptions
1245
) {
1246
    let description_node = additional_search_descriptions.node;
1247
    let description_fns =
1248
        additional_search_descriptions.additional_search_descriptions;
1249
    let description = [];
1250
    let global_search = table_dt.search();
1251
    if (global_search.length) {
1252
        description.push(__("Anywhere: %s").format(global_search));
1253
    }
1254
    let additional_searches = [];
1255
    for (f in description_fns) {
1256
        let d = description_fns[f]();
1257
        if (d.length) {
1258
            additional_searches.push(d);
1259
        }
1260
    }
1261
    if (additional_searches.length) {
1262
        description.push(additional_searches.join(", "));
1263
    }
1264
    let column_searches = [];
1265
    table_dt
1266
        .columns()
1267
        .search()
1268
        .each((search, i) => {
1269
            if (search.length) {
1270
                // Retrieve the value from the dropdown list if there is one
1271
                var visible_i = table_dt.column.index("fromData", i);
1272
                let option = $(table_node).find(
1273
                    "thead tr:eq(1) th:eq(%s) select option:selected".format(
1274
                        visible_i
1275
                    )
1276
                );
1277
                if (option.length) {
1278
                    search = $(option).text();
1279
                }
1280
1281
                column_searches.push(
1282
                    "%s=%s".format(
1283
                        table_dt.column(i).header().innerHTML,
1284
                        search
1285
                    )
1286
                );
1287
            }
1288
        });
1289
    if (column_searches.length) {
1290
        description.push(column_searches.join(", "));
1291
    }
1292
1293
    if (description.length) {
1294
        let display = description.length ? "block" : "none";
1295
        let description_str = $(description_node)
1296
            .data("prefix")
1297
            .format(description.join("<br/>"));
1298
        $(description_node).html(description_str).show();
1299
    } else {
1300
        $(description_node).html("").hide();
1301
    }
1302
}
1303
269
(function ($) {
1304
(function ($) {
270
    /**
1305
    /**
271
     * Create a new dataTables instance that uses the Koha RESTful API's as a data source
1306
     * Create a new dataTables instance that uses the Koha RESTful API's as a data source
Lines 276-286 function _dt_visibility(table_settings, table_dt) { Link Here
276
     *                                                Supports `contains`, `starts_with`, `ends_with` and `exact` match
1311
     *                                                Supports `contains`, `starts_with`, `ends_with` and `exact` match
277
     * @param  {string}  [options.columns.*.type      Data type the field is stored in so we may impose some additional
1312
     * @param  {string}  [options.columns.*.type      Data type the field is stored in so we may impose some additional
278
     *                                                manipulation to search strings. Supported types are currenlty 'date'
1313
     *                                                manipulation to search strings. Supported types are currenlty 'date'
1314
     * @param  {string}  [options.columns.*.datatype  Data type the field is stored in so we may impose some additional
1315
     *                                                manipulation logic to search. Supported types are currently 'related-object',
1316
     *                                                for implimenting joins in api search queries, and 'coded_value:TABLE' to allow
1317
     *                                                for clientside translations of description to code to reduce join requirements.
1318
     *                                                See bug 39011 for an example implimentation.
279
     * @param  {Object}  table_settings               The arrayref as returned by TableSettings.GetTableSettings function
1319
     * @param  {Object}  table_settings               The arrayref as returned by TableSettings.GetTableSettings function
280
     *                                                available from the columns_settings template toolkit include
1320
     *                                                available from the columns_settings template toolkit include
1321
     * @param  {Boolean} add_filters                  Add a filters row as the top row of the table
1322
     * @param  {Object}  default_filters              Add a set of default search filters to apply at table initialisation
1323
     * @param  {Object}  filters_options              Build selects for the filters row, and pass their value.
1324
     *                                                eg. { 1 => vendors } will build a select with the vendor list in the second column
1325
     * @param  {Object}  show_search_descriptions     Whether or not display the search descriptions when the table is drawn
1326
     *                                                Expect a node in which to place the descriptions. It must have a data-prefix attribute to build the description properly.
1327
     * @param  {Object}  aditional_search_descriptions Pass additional descriptions
1328
     *                                                 eg. { surname => () => { 'Surname with '.format($("#surname_form").val()) } }
281
     * @return {Object}                               The dataTables instance
1329
     * @return {Object}                               The dataTables instance
282
     */
1330
     */
283
    $.fn.kohaTable = function (options = {}, table_settings = undefined) {
1331
    $.fn.kohaTable = function (
1332
        options = {},
1333
        table_settings,
1334
        add_filters,
1335
        default_filters,
1336
        filters_options,
1337
        external_filter_nodes,
1338
        show_search_descriptions,
1339
        additional_search_descriptions
1340
    ) {
284
        // Early return if the node does not exist
1341
        // Early return if the node does not exist
285
        if (!this.length) return;
1342
        if (!this.length) return;
286
1343
Lines 293-298 function _dt_visibility(table_settings, table_dt) { Link Here
293
                    dataTablesDefaults.initComplete(settings, json);
1350
                    dataTablesDefaults.initComplete(settings, json);
294
                };
1351
                };
295
            }
1352
            }
1353
1354
            if (options.ajax && !options.bKohaAjaxSVC) {
1355
                options.ajax = Object.assign(
1356
                    {},
1357
                    options.ajax,
1358
                    _dt_default_ajax({ default_filters, options })
1359
                );
1360
                options.serverSide = true;
1361
                options.processing = true;
1362
                options.pagingType = "full_numbers";
1363
            }
296
        }
1364
        }
297
1365
298
        var settings = $.extend(
1366
        var settings = $.extend(
Lines 300-305 function _dt_visibility(table_settings, table_dt) { Link Here
300
            {},
1368
            {},
301
            dataTablesDefaults,
1369
            dataTablesDefaults,
302
            {
1370
            {
1371
                paging: true,
303
                searching: true,
1372
                searching: true,
304
                language: {
1373
                language: {
305
                    emptyTable: options.emptyTable
1374
                    emptyTable: options.emptyTable
Lines 312-318 function _dt_visibility(table_settings, table_dt) { Link Here
312
1381
313
        settings["buttons"] = _dt_buttons({ settings, table_settings });
1382
        settings["buttons"] = _dt_buttons({ settings, table_settings });
314
1383
1384
        if (add_filters) {
1385
            settings["orderCellsTop"] = true;
1386
        }
1387
315
        if (table_settings) {
1388
        if (table_settings) {
1389
            let state_settings = _dt_save_restore_state(
1390
                table_settings,
1391
                external_filter_nodes
1392
            );
1393
            settings = { ...settings, ...state_settings };
1394
316
            if (
1395
            if (
317
                table_settings.hasOwnProperty("default_display_length") &&
1396
                table_settings.hasOwnProperty("default_display_length") &&
318
                table_settings["default_display_length"] != null
1397
                table_settings["default_display_length"] != null
Lines 343-351 function _dt_visibility(table_settings, table_dt) { Link Here
343
        }
1422
        }
344
1423
345
        $(this).data("bKohaColumnsUseNames", settings.bKohaColumnsUseNames);
1424
        $(this).data("bKohaColumnsUseNames", settings.bKohaColumnsUseNames);
1425
        $(this).data("bKohaAjaxSVC", settings.bKohaAjaxSVC); // Must not be used for new tables!
346
        var table = $(this).dataTable(settings);
1426
        var table = $(this).dataTable(settings);
347
1427
348
        var table_dt = table.DataTable();
1428
        var table_dt = table.DataTable();
1429
        if (add_filters) {
1430
            _dt_add_filters(this, table_dt, filters_options);
1431
        }
1432
1433
        table_dt.on("column-visibility.dt", function () {
1434
            if (add_filters) {
1435
                _dt_add_filters(this, table_dt, filters_options);
1436
            }
1437
        });
349
1438
350
        table_dt.on("search.dt", function (e, settings) {
1439
        table_dt.on("search.dt", function (e, settings) {
351
            // When the DataTables search function is triggered,
1440
            // When the DataTables search function is triggered,
Lines 354-371 function _dt_visibility(table_settings, table_dt) { Link Here
354
            toggledClearFilter(table_dt.search(), settings.nTable.id);
1443
            toggledClearFilter(table_dt.search(), settings.nTable.id);
355
        });
1444
        });
356
1445
357
        let hidden_ids = _dt_visibility(table_settings, table_dt);
1446
        if (show_search_descriptions !== undefined) {
358
        table_dt
1447
            table_dt.on("draw", function (e, settings) {
359
            .on("column-visibility.dt", function () {
1448
                update_search_description(table, table_dt, {
360
                if (typeof columnsInit == "function") {
1449
                    node: show_search_descriptions,
361
                    // This function can be created separately and used to trigger
1450
                    additional_search_descriptions,
362
                    // an event after the DataTable has loaded AND column visibility
1451
                });
363
                    // has been updated according to the table's configuration
1452
            });
364
                    columnsInit();
1453
            update_search_description(table, table_dt, {
365
                }
1454
                node: show_search_descriptions,
366
            })
1455
                additional_search_descriptions,
367
            .columns(hidden_ids)
1456
            });
368
            .visible(false);
1457
        }
369
1458
370
        return table;
1459
        return table;
371
    };
1460
    };
372
- 

Return to bug 39739