Bugzilla – Attachment 181444 Details for
Bug 39739
OPAC's datatables.js out of sync with staff's
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 39739: Sync OPAC's datatables.js with the staff interface's version
Bug-39739-Sync-OPACs-datatablesjs-with-the-staff-i.patch (text/plain), 50.22 KB, created by
Tomás Cohen Arazi (tcohen)
on 2025-04-24 13:22:26 UTC
(
hide
)
Description:
Bug 39739: Sync OPAC's datatables.js with the staff interface's version
Filename:
MIME Type:
Creator:
Tomás Cohen Arazi (tcohen)
Created:
2025-04-24 13:22:26 UTC
Size:
50.22 KB
patch
obsolete
>From 3010d37ba92a9421a866bd0dfa5af34fdec4b90d Mon Sep 17 00:00:00 2001 >From: Tomas Cohen Arazi <tomascohen@theke.io> >Date: Thu, 24 Apr 2025 09:58:25 -0300 >Subject: [PATCH] Bug 39739: Sync OPAC's datatables.js with the staff > interface's version > >This patch overwrites the OPAC version of `datatables,js` with the one >for the staff interface. > >A block code adding links to the columns configuration page is removed >as it doesn't apply to the OPAC and produces errors in the console for >missing variable definitions that would be inherited from the caller >context (looking for `CAN_user_parameters_manage_column_config`). > >To test: >1. Apply this patch >2. Find templates using `kohaTable`: > $ cd koha-tmpl/opac-tmpl > $ git grep kohaTable >3. Play in the UI with those pages >=> SUCCESS: Tables work, nothing's broken >4. Sign off :-D > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >--- > .../opac-tmpl/bootstrap/js/datatables.js | 1213 ++++++++++++++++- > 1 file changed, 1151 insertions(+), 62 deletions(-) > >diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/datatables.js b/koha-tmpl/opac-tmpl/bootstrap/js/datatables.js >index 0fc33651d15..b34dd3cb431 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/js/datatables.js >+++ b/koha-tmpl/opac-tmpl/bootstrap/js/datatables.js >@@ -1,4 +1,3 @@ >-/* global __ */ > // These default options are for translation but can be used > // for any other datatables settings > // To use it, write: >@@ -37,9 +36,36 @@ var dataTablesDefaults = { > excel: __("Excel"), > }, > }, >- dom: "t", >- buttons: ["clearFilter", "copy", "csv", "print"], >- paging: false, >+ dom: '<"dt-info"i><"top pager"<"table_entries"lp><"table_controls"fB>>tr<"bottom pager"ip>', >+ buttons: [ >+ { >+ fade: 100, >+ className: "dt_button_clear_filter", >+ titleAttr: __("Clear filter"), >+ enabled: false, >+ text: >+ '<i class="fa fa-lg fa-times"></i> <span class="dt-button-text">' + >+ __("Clear filter") + >+ "</span>", >+ available: function (dt) { >+ // The "clear filter" button is made available if this test returns true >+ if (dt.settings()[0].aanFeatures.f) { >+ // aanFeatures.f is null if there is no search form >+ return true; >+ } >+ }, >+ action: function (e, dt, node) { >+ dt.search("").draw("page"); >+ node.addClass("disabled"); >+ }, >+ }, >+ ], >+ lengthMenu: [ >+ [10, 20, 50, 100, -1], >+ [10, 20, 50, 100, __("All")], >+ ], >+ pageLength: 20, >+ fixedHeader: true, > initComplete: function (settings) { > var tableId = settings.nTable.id; > state = settings.oLoadedState; >@@ -61,12 +87,175 @@ function toggledClearFilter(searchText, tableId) { > } > } > >+// Sorting on html contains >+// <a href="foo.pl">bar</a> sort on 'bar' >+function dt_overwrite_html_sorting_localeCompare() { >+ jQuery.fn.dataTableExt.oSort["html-asc"] = function (a, b) { >+ a = a.replace(/<.*?>/g, "").replace(/\s+/g, " "); >+ b = b.replace(/<.*?>/g, "").replace(/\s+/g, " "); >+ if (typeof (a.localeCompare == "function")) { >+ return a.localeCompare(b); >+ } else { >+ return a > b ? 1 : a < b ? -1 : 0; >+ } >+ }; >+ >+ jQuery.fn.dataTableExt.oSort["html-desc"] = function (a, b) { >+ a = a.replace(/<.*?>/g, "").replace(/\s+/g, " "); >+ b = b.replace(/<.*?>/g, "").replace(/\s+/g, " "); >+ if (typeof (b.localeCompare == "function")) { >+ return b.localeCompare(a); >+ } else { >+ return b > a ? 1 : b < a ? -1 : 0; >+ } >+ }; >+ >+ jQuery.fn.dataTableExt.oSort["num-html-asc"] = function (a, b) { >+ var x = a.replace(/<.*?>/g, ""); >+ var y = b.replace(/<.*?>/g, ""); >+ x = parseFloat(x); >+ y = parseFloat(y); >+ return x < y ? -1 : x > y ? 1 : 0; >+ }; >+ >+ jQuery.fn.dataTableExt.oSort["num-html-desc"] = function (a, b) { >+ var x = a.replace(/<.*?>/g, ""); >+ var y = b.replace(/<.*?>/g, ""); >+ x = parseFloat(x); >+ y = parseFloat(y); >+ return x < y ? 1 : x > y ? -1 : 0; >+ }; >+} >+ >+$.fn.dataTableExt.oSort["num-html-asc"] = function (a, b) { >+ var x = a.replace(/<.*?>/g, ""); >+ var y = b.replace(/<.*?>/g, ""); >+ x = parseFloat(x); >+ y = parseFloat(y); >+ return x < y ? -1 : x > y ? 1 : 0; >+}; >+ >+$.fn.dataTableExt.oSort["num-html-desc"] = function (a, b) { >+ var x = a.replace(/<.*?>/g, ""); >+ var y = b.replace(/<.*?>/g, ""); >+ x = parseFloat(x); >+ y = parseFloat(y); >+ return x < y ? 1 : x > y ? -1 : 0; >+}; >+ >+(function () { >+ /* >+ * Natural Sort algorithm for Javascript - Version 0.7 - Released under MIT license >+ * Author: Jim Palmer (based on chunking idea from Dave Koelle) >+ * Contributors: Mike Grier (mgrier.com), Clint Priest, Kyle Adams, guillermo >+ * See: http://js-naturalsort.googlecode.com/svn/trunk/naturalSort.js >+ */ >+ function naturalSort(a, b) { >+ var re = /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, >+ sre = /(^[ ]*|[ ]*$)/g, >+ dre = >+ /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, >+ hre = /^0x[0-9a-f]+$/i, >+ ore = /^0/, >+ // convert all to strings and trim() >+ x = a.toString().replace(sre, "") || "", >+ y = b.toString().replace(sre, "") || "", >+ // chunk/tokenize >+ xN = x >+ .replace(re, "\0$1\0") >+ .replace(/\0$/, "") >+ .replace(/^\0/, "") >+ .split("\0"), >+ yN = y >+ .replace(re, "\0$1\0") >+ .replace(/\0$/, "") >+ .replace(/^\0/, "") >+ .split("\0"), >+ // numeric, hex or date detection >+ xD = >+ parseInt(x.match(hre), 10) || >+ (xN.length != 1 && x.match(dre) && Date.parse(x)), >+ yD = >+ parseInt(y.match(hre), 10) || >+ (xD && y.match(dre) && Date.parse(y)) || >+ null; >+ // first try and sort Hex codes or Dates >+ if (yD) >+ if (xD < yD) return -1; >+ else if (xD > yD) return 1; >+ // natural sorting through split numeric strings and default strings >+ for ( >+ var cLoc = 0, numS = Math.max(xN.length, yN.length); >+ cLoc < numS; >+ cLoc++ >+ ) { >+ // find floats not starting with '0', string or 0 if not defined (Clint Priest) >+ var oFxNcL = >+ (!(xN[cLoc] || "").match(ore) && parseFloat(xN[cLoc])) || >+ xN[cLoc] || >+ 0; >+ var oFyNcL = >+ (!(yN[cLoc] || "").match(ore) && parseFloat(yN[cLoc])) || >+ yN[cLoc] || >+ 0; >+ // handle numeric vs string comparison - number < string - (Kyle Adams) >+ if (isNaN(oFxNcL) !== isNaN(oFyNcL)) return isNaN(oFxNcL) ? 1 : -1; >+ // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' >+ else if (typeof oFxNcL !== typeof oFyNcL) { >+ oFxNcL += ""; >+ oFyNcL += ""; >+ } >+ if (oFxNcL < oFyNcL) return -1; >+ if (oFxNcL > oFyNcL) return 1; >+ } >+ return 0; >+ } >+ >+ jQuery.extend(jQuery.fn.dataTableExt.oSort, { >+ "natural-asc": function (a, b) { >+ return naturalSort(a, b); >+ }, >+ >+ "natural-desc": function (a, b) { >+ return naturalSort(a, b) * -1; >+ }, >+ }); >+})(); >+ >+/* Plugin to allow sorting on data stored in a span's title attribute >+ * >+ * Ex: <td><span title="[% ISO_date %]">[% formatted_date %]</span></td> >+ * >+ * In DataTables config: >+ * "data": [ >+ * { "type": "title-string" }, >+ * ] >+ * http://datatables.net/plug-ins/sorting#hidden_title_string >+ */ >+jQuery.extend(jQuery.fn.dataTableExt.oSort, { >+ "title-string-pre": function (a) { >+ var m = a.match(/title="(.*?)"/); >+ if (null !== m && m.length) { >+ return m[1].toLowerCase(); >+ } >+ return ""; >+ }, >+ >+ "title-string-asc": function (a, b) { >+ return a < b ? -1 : a > b ? 1 : 0; >+ }, >+ >+ "title-string-desc": function (a, b) { >+ return a < b ? 1 : a > b ? -1 : 0; >+ }, >+}); >+ > (function () { > /* Plugin to allow text sorting to ignore articles > * > * In DataTables config: >- * "aoColumns": [ >- * { "sType": "anti-the" }, >+ * "data": [ >+ * { "type": "anti-the" }, > * ] > * Based on the plugin found here: > * http://datatables.net/plug-ins/sorting#anti_the >@@ -79,7 +268,7 @@ function toggledClearFilter(searchText, tableId) { > if (config_exclude_articles_from_sort) { > var articles = config_exclude_articles_from_sort.split(" "); > var rpattern = ""; >- for (var i = 0; i < articles.length; i++) { >+ for (i = 0; i < articles.length; i++) { > rpattern += "^" + articles[i] + " "; > if (i < articles.length - 1) { > rpattern += "|"; >@@ -106,12 +295,521 @@ function toggledClearFilter(searchText, tableId) { > }); > })(); > >+// Remove string between NSB NSB characters >+$.fn.dataTableExt.oSort["nsb-nse-asc"] = function (a, b) { >+ var pattern = new RegExp("\x88.*\x89"); >+ a = a.replace(pattern, ""); >+ b = b.replace(pattern, ""); >+ return a > b ? 1 : a < b ? -1 : 0; >+}; >+$.fn.dataTableExt.oSort["nsb-nse-desc"] = function (a, b) { >+ var pattern = new RegExp("\x88.*\x89"); >+ a = a.replace(pattern, ""); >+ b = b.replace(pattern, ""); >+ return b > a ? 1 : b < a ? -1 : 0; >+}; >+ >+/* Define two custom functions (asc and desc) for basket callnumber sorting */ >+jQuery.fn.dataTableExt.oSort["callnumbers-asc"] = function (x, y) { >+ var x_array = x.split("<div>"); >+ var y_array = y.split("<div>"); >+ >+ /* Pop the first elements, they are empty strings */ >+ x_array.shift(); >+ y_array.shift(); >+ >+ x_array = jQuery.map(x_array, function (a) { >+ return parse_callnumber(a); >+ }); >+ y_array = jQuery.map(y_array, function (a) { >+ return parse_callnumber(a); >+ }); >+ >+ x_array.sort(); >+ y_array.sort(); >+ >+ x = x_array.shift(); >+ y = y_array.shift(); >+ >+ if (!x) { >+ x = ""; >+ } >+ if (!y) { >+ y = ""; >+ } >+ >+ return x < y ? -1 : x > y ? 1 : 0; >+}; >+ >+jQuery.fn.dataTableExt.oSort["callnumbers-desc"] = function (x, y) { >+ var x_array = x.split("<div>"); >+ var y_array = y.split("<div>"); >+ >+ /* Pop the first elements, they are empty strings */ >+ x_array.shift(); >+ y_array.shift(); >+ >+ x_array = jQuery.map(x_array, function (a) { >+ return parse_callnumber(a); >+ }); >+ y_array = jQuery.map(y_array, function (a) { >+ return parse_callnumber(a); >+ }); >+ >+ x_array.sort(); >+ y_array.sort(); >+ >+ x = x_array.pop(); >+ y = y_array.pop(); >+ >+ if (!x) { >+ x = ""; >+ } >+ if (!y) { >+ y = ""; >+ } >+ >+ return x < y ? 1 : x > y ? -1 : 0; >+}; >+ >+function parse_callnumber(html) { >+ var array = html.split('<span class="callnumber">'); >+ if (array[1]) { >+ array = array[1].split("</span>"); >+ return array[0]; >+ } else { >+ return ""; >+ } >+} >+ >+// see http://www.datatables.net/examples/advanced_init/footer_callback.html >+function footer_column_sum(api, column_numbers) { >+ // Remove the formatting to get integer data for summation >+ var intVal = function (i) { >+ if (typeof i === "number") { >+ if (isNaN(i)) return 0; >+ return i; >+ } else if (typeof i === "string") { >+ var value = i.replace(/[a-zA-Z ,.]/g, "") * 1; >+ if (isNaN(value)) return 0; >+ return value; >+ } >+ return 0; >+ }; >+ >+ for (var indice = 0; indice < column_numbers.length; indice++) { >+ var column_number = column_numbers[indice]; >+ >+ var total = 0; >+ var cells = api >+ .column(column_number, { page: "current" }) >+ .nodes() >+ .to$() >+ .find("span.total_amount"); >+ var budgets_totaled = []; >+ $(cells).each(function () { >+ budgets_totaled.push($(this).data("self_id")); >+ }); >+ $(cells).each(function () { >+ if ( >+ $(this).data("parent_id") && >+ $.inArray($(this).data("parent_id"), budgets_totaled) > -1 >+ ) { >+ return; >+ } else { >+ total += intVal($(this).html()); >+ } >+ }); >+ total /= 100; // Hard-coded decimal precision >+ >+ // Update footer >+ $(api.column(column_number).footer()).html(total.format_price()); >+ } >+} >+ >+function filterDataTable(table, column, term) { >+ if (column) { >+ table.column(column).search(term).draw("page"); >+ } else { >+ table.search(term).draw("page"); >+ } >+} >+ >+jQuery.fn.dataTable.ext.errMode = function (settings, note, message) { >+ if (settings && settings.jqXHR) { >+ console.log( >+ "Got %s (%s)".format( >+ settings.jqXHR.status, >+ settings.jqXHR.statusText >+ ) >+ ); >+ alert( >+ __( >+ "Something went wrong when loading the table.\n%s: %s. \n%s" >+ ).format( >+ settings.jqXHR.status, >+ settings.jqXHR.statusText, >+ settings.jqXHR.responseJSON && >+ settings.jqXHR.responseJSON.errors >+ ? settings.jqXHR.responseJSON.errors >+ .map(m => m.message) >+ .join("\n") >+ : "" >+ ) >+ ); >+ } else { >+ alert(__("Something went wrong when loading the table.")); >+ } >+ console.log(message); >+}; >+ >+function _dt_default_ajax(params) { >+ let default_filters = params.default_filters; >+ let options = params.options; >+ >+ if ( >+ !options.criteria || >+ ["contains", "starts_with", "ends_with", "exact"].indexOf( >+ options.criteria.toLowerCase() >+ ) === -1 >+ ) >+ options.criteria = "contains"; >+ options.criteria = options.criteria.toLowerCase(); >+ >+ return { >+ type: "GET", >+ cache: true, >+ dataSrc: "data", >+ beforeSend: function (xhr, settings) { >+ this._xhr = xhr; >+ if (options.embed) { >+ xhr.setRequestHeader( >+ "x-koha-embed", >+ Array.isArray(options.embed) >+ ? options.embed.join(",") >+ : options.embed >+ ); >+ } >+ }, >+ dataFilter: function (data, type) { >+ var json = { data: JSON.parse(data) }; >+ if ((total = this._xhr.getResponseHeader("x-total-count"))) { >+ json.recordsTotal = total; >+ json.recordsFiltered = total; >+ } >+ if ((total = this._xhr.getResponseHeader("x-base-total-count"))) { >+ json.recordsTotal = total; >+ } >+ if ((draw = this._xhr.getResponseHeader("x-koha-request-id"))) { >+ json.draw = draw; >+ } >+ >+ return JSON.stringify(json); >+ }, >+ data: function (data, settings) { >+ var length = data.length; >+ var start = data.start; >+ >+ let api = new $.fn.dataTable.Api(settings); >+ const global_search = api.search(); >+ >+ var dataSet = { >+ _page: Math.floor(start / length) + 1, >+ _per_page: length, >+ }; >+ >+ function build_query(col, value) { >+ var parts = []; >+ var attributes = col.data.split(":"); >+ for (var i = 0; i < attributes.length; i++) { >+ var part = {}; >+ var attr = attributes[i]; >+ let default_build = true; >+ let criteria = options.criteria; >+ if (value.match(/^\^(.*)\$$/)) { >+ value = value.replace(/^\^/, "").replace(/\$$/, ""); >+ criteria = "exact"; >+ } else { >+ // escape SQL LIKE special characters % >+ value = value.replace(/(\%|\\)/g, "\\$1"); >+ } >+ >+ let built_value; >+ if (col.type == "date") { >+ let rfc3339 = $date_to_rfc3339(value); >+ if (rfc3339 != "Invalid Date") { >+ built_value = rfc3339; >+ } >+ } >+ >+ if (col.datatype !== undefined) { >+ default_build = false; >+ let coded_datatype = >+ col.datatype.match(/^coded_value:(.*)/); >+ if (col.datatype == "related-object") { >+ let query_term = value; >+ >+ if (criteria != "exact") { >+ query_term = { >+ like: >+ (["contains", "ends_with"].indexOf( >+ criteria >+ ) !== -1 >+ ? "%" >+ : "") + >+ value + >+ (["contains", "starts_with"].indexOf( >+ criteria >+ ) !== -1 >+ ? "%" >+ : ""), >+ }; >+ } >+ >+ part = { >+ [col.related + "." + col.relatedKey]: >+ col.relatedValue, >+ [col.related + "." + col.relatedSearchOn]: >+ query_term, >+ }; >+ } else if ( >+ coded_datatype && >+ coded_datatype.length > 1 >+ ) { >+ if (global_search.length || value.length) { >+ coded_datatype = coded_datatype[1]; >+ const search_value = value.length >+ ? value >+ : global_search; >+ const regex = new RegExp( >+ `^${search_value}`, >+ "i" >+ ); >+ if ( >+ coded_values && >+ coded_values.hasOwnProperty(coded_datatype) >+ ) { >+ let codes = [ >+ ...coded_values[ >+ coded_datatype >+ ].entries(), >+ ] >+ .filter(([label]) => regex.test(label)) >+ .map(([, code]) => code); >+ >+ if (codes.length) { >+ part[ >+ !attr.includes(".") >+ ? "me." + attr >+ : attr >+ ] = codes; >+ } else { >+ // Coded value not found using the description, fallback to code >+ default_build = true; >+ } >+ } else { >+ console.log( >+ "coded datatype %s not supported yet".format( >+ coded_datatype >+ ) >+ ); >+ } >+ } else { >+ default_build = true; >+ } >+ } else { >+ console.log( >+ "datatype %s not supported yet".format( >+ col.datatype >+ ) >+ ); >+ } >+ } >+ if (default_build) { >+ let value_part; >+ if (criteria === "exact") { >+ value_part = built_value >+ ? [value, built_value] >+ : value; >+ } else { >+ let like = { >+ like: >+ (["contains", "ends_with"].indexOf( >+ criteria >+ ) !== -1 >+ ? "%" >+ : "") + >+ value + >+ (["contains", "starts_with"].indexOf( >+ criteria >+ ) !== -1 >+ ? "%" >+ : ""), >+ }; >+ value_part = built_value >+ ? [like, built_value] >+ : like; >+ } >+ >+ part[!attr.includes(".") ? "me." + attr : attr] = >+ value_part; >+ } >+ >+ if (Object.keys(part).length) parts.push(part); >+ } >+ return parts; >+ } >+ >+ var filter = data.search.value; >+ // Build query for each column filter >+ var and_query_parameters = settings.aoColumns >+ .filter(function (col) { >+ return ( >+ col.bSearchable && >+ typeof col.data == "string" && >+ data.columns[col.idx].search.value != "" >+ ); >+ }) >+ .map(function (col) { >+ var value = data.columns[col.idx].search.value; >+ return build_query(col, value); >+ }) >+ .map(function r(e) { >+ return $.isArray(e) ? $.map(e, r) : e; >+ }); >+ >+ // Build query for the global search filter >+ var or_query_parameters = settings.aoColumns >+ .filter(function (col) { >+ return col.bSearchable && filter != ""; >+ }) >+ .map(function (col) { >+ var value = filter; >+ return build_query(col, value); >+ }) >+ .map(function r(e) { >+ return $.isArray(e) ? $.map(e, r) : e; >+ }); >+ >+ if (default_filters) { >+ let additional_filters = {}; >+ for (f in default_filters) { >+ let k; >+ let v; >+ if (typeof default_filters[f] === "function") { >+ let val = default_filters[f](); >+ if (val != undefined && val != "") { >+ k = f; >+ v = val; >+ } >+ } else { >+ k = f; >+ v = default_filters[f]; >+ } >+ >+ // Pass to -or if you want a separate OR clause >+ // It's not the usual DBIC notation! >+ if (f == "-or") { >+ if (v) or_query_parameters.push(v); >+ } else if (f == "-and") { >+ if (v) and_query_parameters.push(v); >+ } else if (v) { >+ additional_filters[k] = v; >+ } >+ } >+ if (Object.keys(additional_filters).length) { >+ and_query_parameters.push(additional_filters); >+ } >+ } >+ query_parameters = and_query_parameters; >+ if (or_query_parameters.length) { >+ query_parameters.push(or_query_parameters); >+ } >+ >+ if (query_parameters.length) { >+ query_parameters = JSON.stringify( >+ query_parameters.length === 1 >+ ? query_parameters[0] >+ : { "-and": query_parameters } >+ ); >+ dataSet.q = query_parameters; >+ } >+ >+ dataSet._match = options.criteria; >+ >+ if (data["draw"] !== undefined) { >+ settings.ajax.headers = { "x-koha-request-id": data.draw }; >+ } >+ >+ if (options.columns) { >+ var order = data.order; >+ var orderArray = new Array(); >+ order.forEach(function (e, i) { >+ var order_col = e.column; >+ var order_by = options.columns[order_col].data; >+ order_by = order_by.split(":"); >+ var order_dir = e.dir == "asc" ? "+" : "-"; >+ Array.prototype.push.apply( >+ orderArray, >+ order_by.map( >+ x => order_dir + (!x.includes(".") ? "me." + x : x) >+ ) >+ ); >+ }); >+ dataSet._order_by = orderArray >+ .filter((v, i, a) => a.indexOf(v) === i) >+ .join(","); >+ } >+ >+ return dataSet; >+ }, >+ }; >+} >+ >+function build_url_with_state(dt, table_settings) { >+ let table_key = "DataTables_%s_%s_%s".format( >+ table_settings.module, >+ table_settings.page, >+ table_settings.table >+ ); >+ >+ let state = JSON.stringify(dt.state()); >+ delete state.time; >+ let searchParams = new URLSearchParams(window.location.search); >+ searchParams.set(table_key + "_state", btoa(state)); >+ >+ return ( >+ window.location.origin + >+ window.location.pathname + >+ "?" + >+ searchParams.toString() + >+ window.location.hash >+ ); >+} >+ > function _dt_buttons(params) { > let settings = params.settings || {}; > let table_settings = params.table_settings; > > var exportColumns = ":visible:not(.no-export)"; > >+ const export_format_spreadsheet = { >+ body: function (data, row, column, node) { >+ var newnode = $(node); >+ >+ if (newnode.find(".no-export").length > 0) { >+ newnode = newnode.clone(); >+ newnode.find(".no-export").remove(); >+ } >+ let trimmed_str = newnode.text().replace(/\n/g, " ").trim(); >+ const unsafeCharacters = /^[=+\-@\t\r]/; >+ if (unsafeCharacters.test(trimmed_str)) { >+ trimmed_str = "'" + trimmed_str; >+ } >+ return trimmed_str; >+ }, >+ }; >+ > var export_format = { > body: function (data, row, column, node) { > var newnode = $(node); >@@ -130,14 +828,14 @@ function _dt_buttons(params) { > extend: "excelHtml5", > exportOptions: { > columns: exportColumns, >- format: export_format, >+ format: export_format_spreadsheet, > }, > }, > { > extend: "csvHtml5", > exportOptions: { > columns: exportColumns, >- format: export_format, >+ format: export_format_spreadsheet, > }, > }, > { >@@ -156,46 +854,21 @@ function _dt_buttons(params) { > }, > ]; > >- let buttons = [ >- { >- fade: 100, >- className: "dt_button_clear_filter", >- titleAttr: __("Clear filter"), >- enabled: false, >- text: >- '<i class="fa fa-lg fa-times" aria-hidden="true"></i> <span class="dt-button-text">' + >- __("Clear filter") + >- "</span>", >- action: function (e, dt, node, config) { >- dt.search("").draw("page"); >- node.addClass("disabled"); >- }, >+ let buttons = []; >+ buttons.push({ >+ fade: 100, >+ className: "dt_button_clear_filter", >+ titleAttr: __("Clear filter"), >+ enabled: false, >+ text: >+ '<i class="fa fa-lg fa-remove"></i> <span class="dt-button-text">' + >+ __("Clear filter") + >+ "</span>", >+ action: function (e, dt, node, config) { >+ dt.search("").draw("page"); >+ node.addClass("disabled"); > }, >- { >- extend: "csvHtml5", >- text: __("CSV"), >- exportOptions: { >- columns: exportColumns, >- format: export_format, >- }, >- }, >- { >- extend: "copyHtml5", >- text: __("Copy"), >- exportOptions: { >- columns: exportColumns, >- format: export_format, >- }, >- }, >- { >- extend: "print", >- text: __("Print"), >- exportOptions: { >- columns: exportColumns, >- format: export_format, >- }, >- }, >- ]; >+ }); > > // Retrieving bKohaColumnsUseNames from the options passed to the constructor, not DT's settings > // But ideally should be retrieved using table.data() >@@ -223,7 +896,7 @@ function _dt_buttons(params) { > className: "columns_controls", > titleAttr: __("Columns settings"), > text: >- '<i class="fa fa-lg fa-gear" aria-hidden="true"></i> <span class="dt-button-text">' + >+ '<i class="fa fa-lg fa-gear"></i> <span class="dt-button-text">' + > __("Columns") + > "</span>", > exportOptions: { >@@ -232,6 +905,45 @@ function _dt_buttons(params) { > }); > } > >+ buttons.push({ >+ extend: "collection", >+ autoClose: true, >+ fade: 100, >+ className: "export_controls", >+ titleAttr: __("Export or print"), >+ text: >+ '<i class="fa fa-lg fa-download"></i> <span class="dt-button-text">' + >+ __("Export") + >+ "</span>", >+ buttons: export_buttons, >+ }); >+ >+ if (table_settings) { >+ const writeToClipboard = async (text, node) => { >+ await navigator.clipboard.writeText(text); >+ $(node) >+ .tooltip({ trigger: "manual", title: __("Copied!") }) >+ .tooltip("show"); >+ }; >+ buttons.push({ >+ autoClose: true, >+ fade: 100, >+ className: "copyConditions_controls", >+ titleAttr: __("Copy shareable link"), >+ text: >+ '<i class="fa fa-lg fa-copy"></i> <span class="dt-button-text">' + >+ __("Copy shareable link") + >+ "</span>", >+ action: function (e, dt, node, config) { >+ const url = build_url_with_state(dt, table_settings); >+ >+ if (navigator.clipboard && navigator.clipboard.writeText) { >+ writeToClipboard(url, node); >+ } >+ }, >+ }); >+ } >+ > return buttons; > } > >@@ -266,6 +978,329 @@ function _dt_visibility(table_settings, table_dt) { > return hidden_ids; > } > >+function _dt_on_visibility(add_filters, table_node, table_dt) { >+ // FIXME REPLACE ME >+ if (typeof columnsInit == "function") { >+ // This function can be created separately and used to trigger >+ // an event after the DataTable has loaded AND column visibility >+ // has been updated according to the table's configuration >+ columnsInit(); >+ } >+} >+ >+function _dt_add_filters(table_node, table_dt, filters_options = {}) { >+ if (!$(table_node).length) return; >+ >+ $(table_node).find("thead tr:eq(1)").remove(); // Remove if one exists already >+ $(table_node) >+ .find("thead tr") >+ .clone() >+ .appendTo($(table_node).find("thead")); >+ >+ let visibility = table_dt.columns().visible(); >+ let columns = table_dt.settings()[0].aoColumns; >+ table_dt.columns().every(function () { >+ var column = this; >+ let i = column.index(); >+ var visible_i = table_dt.column.index("fromData", i); >+ let th = $(table_node).find( >+ "thead tr:eq(1) th:eq(%s)".format(visible_i) >+ ); >+ var is_searchable = columns[i].bSearchable; >+ $(th) >+ .removeClass("sorting") >+ .removeClass("sorting_asc") >+ .removeClass("sorting_desc"); >+ if (is_searchable) { >+ let input_type = "input"; >+ let existing_search = column.search(); >+ if ($(th).data("filter") || filters_options.hasOwnProperty(i)) { >+ input_type = "select"; >+ let filter_type = $(th).data("filter"); >+ let select = $( >+ '<select class="dt-select-filter"><option value=""></option></select' >+ ); >+ >+ // FIXME eval here is bad and dangerous, how do we workaround that? >+ if (!filters_options.hasOwnProperty(i)) { >+ filters_options[i] = eval(filter_type); >+ } else if (typeof filters_options[i] === "function") { >+ filters_options[i] = filters_options[i](table_dt); >+ } >+ $(filters_options[i]).each(function () { >+ let o = $( >+ '<option value="%s">%s</option>'.format( >+ this._id, >+ this._str >+ ) >+ ); >+ // Compare with lc, or selfreg won't match ^SELFREG$ for instance, see bug 32517 >+ // This is only for category, we might want to apply it only in this case. >+ existing_search = existing_search.toLowerCase(); >+ if ( >+ existing_search === this._id || >+ (existing_search && >+ this._id.toLowerCase().match(existing_search)) >+ ) { >+ o.prop("selected", "selected"); >+ } >+ o.appendTo(select); >+ }); >+ $(th).html(select); >+ } else { >+ var title = $(th).text(); >+ if (existing_search) { >+ $(th).html( >+ '<input type="text" value="%s" style="width: 100%" />'.format( >+ existing_search >+ ) >+ ); >+ } else { >+ var search_title = __("%s search").format(title); >+ $(th).html( >+ '<input type="text" placeholder="%s" style="width: 100%" />'.format( >+ search_title >+ ) >+ ); >+ } >+ } >+ } else { >+ $(th).html(""); >+ } >+ }); >+ _dt_add_delay_filters(table_dt, table_node); >+ table_dt.fixedHeader.adjust(); >+} >+ >+function _dt_add_delay(table_dt, table_node) { >+ let delay_ms = 500; >+ >+ let search = DataTable.util.debounce(function (val) { >+ table_dt.search(val); >+ table_dt.draw(); >+ }, delay_ms); >+ >+ $("#" + table_node.attr("id") + "_wrapper") >+ .find(".dt-input") >+ .unbind() >+ .bind("keyup", search(this.value)); >+} >+ >+function _dt_add_delay_filters(table_dt, table_node) { >+ let delay_ms = 500; >+ >+ let col_input_search = DataTable.util.debounce(function (i, val) { >+ table_dt.column(i).search(val).draw(); >+ }, delay_ms); >+ let col_select_search = DataTable.util.debounce(function (i, val) { >+ table_dt.column(i).search(val, true, false).draw(); >+ }, delay_ms); >+ >+ $(table_node) >+ .find("thead tr:eq(1) th") >+ .each(function (visible_i) { >+ var i = table_dt.column.index("fromVisible", visible_i); >+ $(this) >+ .find("input") >+ .unbind() >+ .bind("keyup change", function (e) { >+ if (e.keyCode === undefined) return; >+ col_input_search(i, this.value); >+ }); >+ >+ $(this) >+ .find("select") >+ .unbind() >+ .bind("keyup change", function () { >+ let value = this.value.length ? "^" + this.value + "$" : ""; >+ col_select_search(i, this.value); >+ }); >+ }); >+} >+ >+function _dt_save_restore_state(table_settings, external_filter_nodes = {}) { >+ let table_key = "DataTables_%s_%s_%s".format( >+ table_settings.module, >+ table_settings.page, >+ table_settings.table >+ ); >+ >+ let default_save_state = table_settings.default_save_state; >+ let default_save_state_search = table_settings.default_save_state_search; >+ >+ let stateSaveCallback = function (settings, data) { >+ localStorage.setItem(table_key, JSON.stringify(data)); >+ }; >+ >+ function set_default(table_settings, table_dt) { >+ let columns = new Array(table_dt.columns()[0].length).fill({ >+ visible: true, >+ }); >+ let hidden_ids = _dt_visibility(table_settings, table_dt); >+ hidden_ids.forEach((id, i) => { >+ columns[id] = { visible: false }; >+ }); >+ // State is not loaded if time is not passed >+ return { columns, time: new Date() }; >+ } >+ let stateLoadCallback = function (settings) { >+ // Load state from URL >+ const url = new URL(window.location.href); >+ let state_from_url = url.searchParams.get(table_key + "_state"); >+ if (state_from_url) { >+ $("#" + settings.nTable.id).data("loaded_from_state", true); >+ return JSON.parse(atob(state_from_url)); >+ } >+ >+ if (!default_save_state) return set_default(table_settings, this.api()); >+ >+ let state = localStorage.getItem(table_key); >+ if (!state) return set_default(table_settings, this.api()); >+ >+ state = JSON.parse(state); >+ >+ if (default_save_state_search) { >+ $("#" + settings.nTable.id).data("loaded_from_state", true); >+ } else { >+ delete state.search; >+ state.columns.forEach(c => delete c.search); >+ } >+ return state; >+ }; >+ >+ let stateSaveParams = function (settings, data) { >+ // FIXME Selector in on the whole DOM, we don't know where the filters are >+ // If others exist on the same page this will lead to unexpected behaviours >+ // Should be safe so far as: patron search use the same code for the different searches >+ // but only the main one has the table settings (and so the state saved) >+ data.external_filters = Object.keys(external_filter_nodes).reduce( >+ (r, k) => { >+ let node = $(external_filter_nodes[k]); >+ let tag_name = node.prop("tagName"); >+ if (tag_name == "INPUT" && node.prop("type") == "checkbox") { >+ r[k] = $(external_filter_nodes[k]).prop("checked"); >+ } else if (tag_name == "INPUT" || tag_name == "SELECT") { >+ r[k] = $(external_filter_nodes[k]).val(); >+ } else { >+ console.log( >+ "Tag '%s' not supported yet for DT state".format( >+ tag_name >+ ) >+ ); >+ } >+ return r; >+ }, >+ {} >+ ); >+ }; >+ let stateLoadParams = function (settings, data) { >+ if (!$("#" + settings.nTable.id).data("loaded_from_state")) return; >+ >+ if (data.external_filters) { >+ Object.keys(external_filter_nodes).forEach((k, i) => { >+ if (data.external_filters.hasOwnProperty(k)) { >+ let node = $(external_filter_nodes[k]); >+ let tag_name = node.prop("tagName"); >+ let value = data.external_filters[k]; >+ if ( >+ tag_name == "INPUT" && >+ node.prop("type") == "checkbox" >+ ) { >+ node.prop("checked", value); >+ } else if ( >+ tag_name == "INPUT" && >+ node.hasClass("flatpickr") >+ ) { >+ const fp = document.querySelector( >+ external_filter_nodes[k] >+ )._flatpickr; >+ fp.setDate(value); >+ } else if (tag_name == "INPUT" || tag_name == "SELECT") { >+ node.val(value); >+ } else { >+ console.log( >+ "Tag '%s' not supported yet for DT state".format( >+ tag_name >+ ) >+ ); >+ } >+ } >+ }); >+ } >+ }; >+ >+ return { >+ stateSave: true, >+ stateSaveCallback, >+ stateLoadCallback, >+ stateSaveParams, >+ stateLoadParams, >+ }; >+} >+ >+function update_search_description( >+ table_node, >+ table_dt, >+ additional_search_descriptions >+) { >+ let description_node = additional_search_descriptions.node; >+ let description_fns = >+ additional_search_descriptions.additional_search_descriptions; >+ let description = []; >+ let global_search = table_dt.search(); >+ if (global_search.length) { >+ description.push(__("Anywhere: %s").format(global_search)); >+ } >+ let additional_searches = []; >+ for (f in description_fns) { >+ let d = description_fns[f](); >+ if (d.length) { >+ additional_searches.push(d); >+ } >+ } >+ if (additional_searches.length) { >+ description.push(additional_searches.join(", ")); >+ } >+ let column_searches = []; >+ table_dt >+ .columns() >+ .search() >+ .each((search, i) => { >+ if (search.length) { >+ // Retrieve the value from the dropdown list if there is one >+ var visible_i = table_dt.column.index("fromData", i); >+ let option = $(table_node).find( >+ "thead tr:eq(1) th:eq(%s) select option:selected".format( >+ visible_i >+ ) >+ ); >+ if (option.length) { >+ search = $(option).text(); >+ } >+ >+ column_searches.push( >+ "%s=%s".format( >+ table_dt.column(i).header().innerHTML, >+ search >+ ) >+ ); >+ } >+ }); >+ if (column_searches.length) { >+ description.push(column_searches.join(", ")); >+ } >+ >+ if (description.length) { >+ let display = description.length ? "block" : "none"; >+ let description_str = $(description_node) >+ .data("prefix") >+ .format(description.join("<br/>")); >+ $(description_node).html(description_str).show(); >+ } else { >+ $(description_node).html("").hide(); >+ } >+} >+ > (function ($) { > /** > * Create a new dataTables instance that uses the Koha RESTful API's as a data source >@@ -276,11 +1311,33 @@ function _dt_visibility(table_settings, table_dt) { > * Supports `contains`, `starts_with`, `ends_with` and `exact` match > * @param {string} [options.columns.*.type Data type the field is stored in so we may impose some additional > * manipulation to search strings. Supported types are currenlty 'date' >+ * @param {string} [options.columns.*.datatype Data type the field is stored in so we may impose some additional >+ * manipulation logic to search. Supported types are currently 'related-object', >+ * for implimenting joins in api search queries, and 'coded_value:TABLE' to allow >+ * for clientside translations of description to code to reduce join requirements. >+ * See bug 39011 for an example implimentation. > * @param {Object} table_settings The arrayref as returned by TableSettings.GetTableSettings function > * available from the columns_settings template toolkit include >+ * @param {Boolean} add_filters Add a filters row as the top row of the table >+ * @param {Object} default_filters Add a set of default search filters to apply at table initialisation >+ * @param {Object} filters_options Build selects for the filters row, and pass their value. >+ * eg. { 1 => vendors } will build a select with the vendor list in the second column >+ * @param {Object} show_search_descriptions Whether or not display the search descriptions when the table is drawn >+ * Expect a node in which to place the descriptions. It must have a data-prefix attribute to build the description properly. >+ * @param {Object} aditional_search_descriptions Pass additional descriptions >+ * eg. { surname => () => { 'Surname with '.format($("#surname_form").val()) } } > * @return {Object} The dataTables instance > */ >- $.fn.kohaTable = function (options = {}, table_settings = undefined) { >+ $.fn.kohaTable = function ( >+ options = {}, >+ table_settings, >+ add_filters, >+ default_filters, >+ filters_options, >+ external_filter_nodes, >+ show_search_descriptions, >+ additional_search_descriptions >+ ) { > // Early return if the node does not exist > if (!this.length) return; > >@@ -293,6 +1350,17 @@ function _dt_visibility(table_settings, table_dt) { > dataTablesDefaults.initComplete(settings, json); > }; > } >+ >+ if (options.ajax && !options.bKohaAjaxSVC) { >+ options.ajax = Object.assign( >+ {}, >+ options.ajax, >+ _dt_default_ajax({ default_filters, options }) >+ ); >+ options.serverSide = true; >+ options.processing = true; >+ options.pagingType = "full_numbers"; >+ } > } > > var settings = $.extend( >@@ -300,6 +1368,7 @@ function _dt_visibility(table_settings, table_dt) { > {}, > dataTablesDefaults, > { >+ paging: true, > searching: true, > language: { > emptyTable: options.emptyTable >@@ -312,7 +1381,17 @@ function _dt_visibility(table_settings, table_dt) { > > settings["buttons"] = _dt_buttons({ settings, table_settings }); > >+ if (add_filters) { >+ settings["orderCellsTop"] = true; >+ } >+ > if (table_settings) { >+ let state_settings = _dt_save_restore_state( >+ table_settings, >+ external_filter_nodes >+ ); >+ settings = { ...settings, ...state_settings }; >+ > if ( > table_settings.hasOwnProperty("default_display_length") && > table_settings["default_display_length"] != null >@@ -343,9 +1422,19 @@ function _dt_visibility(table_settings, table_dt) { > } > > $(this).data("bKohaColumnsUseNames", settings.bKohaColumnsUseNames); >+ $(this).data("bKohaAjaxSVC", settings.bKohaAjaxSVC); // Must not be used for new tables! > var table = $(this).dataTable(settings); > > var table_dt = table.DataTable(); >+ if (add_filters) { >+ _dt_add_filters(this, table_dt, filters_options); >+ } >+ >+ table_dt.on("column-visibility.dt", function () { >+ if (add_filters) { >+ _dt_add_filters(this, table_dt, filters_options); >+ } >+ }); > > table_dt.on("search.dt", function (e, settings) { > // When the DataTables search function is triggered, >@@ -354,18 +1443,18 @@ function _dt_visibility(table_settings, table_dt) { > toggledClearFilter(table_dt.search(), settings.nTable.id); > }); > >- let hidden_ids = _dt_visibility(table_settings, table_dt); >- table_dt >- .on("column-visibility.dt", function () { >- if (typeof columnsInit == "function") { >- // This function can be created separately and used to trigger >- // an event after the DataTable has loaded AND column visibility >- // has been updated according to the table's configuration >- columnsInit(); >- } >- }) >- .columns(hidden_ids) >- .visible(false); >+ if (show_search_descriptions !== undefined) { >+ table_dt.on("draw", function (e, settings) { >+ update_search_description(table, table_dt, { >+ node: show_search_descriptions, >+ additional_search_descriptions, >+ }); >+ }); >+ update_search_description(table, table_dt, { >+ node: show_search_descriptions, >+ additional_search_descriptions, >+ }); >+ } > > return table; > }; >-- >2.49.0
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 39739
:
181444
|
181506