PageSourceSearch

https://many-analysts.netlify.app/index_files/datatables-binding/datatables.js

js many-analysts.netlify.app collected 2026-10-03 11:02:33 UTC 57,061 bytes, 1,421 lines download raw bytes

1(function() {
2
3// some helper functions: using a global object DTWidget so that it can be used
4// in JS() code, e.g. datatable(options = list(foo = JS('code'))); unlike R's
5// dynamic scoping, when 'code' is eval()'ed, JavaScript does not know objects
6// from the "parent frame", e.g. JS('DTWidget') will not work unless it was made
7// a global object
8var DTWidget = {};
9
10// 123456666.7890 -> 123,456,666.7890
11var markInterval = function(d, digits, interval, mark, decMark, precision) {
12  x = precision ? d.toPrecision(digits) : d.toFixed(digits);
13  if (!/^-?[\d.]+$/.test(x)) return x;
14  var xv = x.split('.');
15  if (xv.length > 2) return x;  // should have at most one decimal point
16  xv[0] = xv[0].replace(new RegExp('\\B(?=(\\d{' + interval + '})+(?!\\d))', 'g'), mark);
17  return xv.join(decMark);
18};
19
20DTWidget.formatCurrency = function(data, currency, digits, interval, mark, decMark, before) {
21  var d = parseFloat(data);
22  if (isNaN(d)) return '';
23  var res = markInterval(d, digits, interval, mark, decMark);
24  res = before ? (/^-/.test(res) ? '-' + currency + res.replace(/^-/, '') : currency + res) :
25    res + currency;
26  return res;
27};
28
29DTWidget.formatString = function(data, prefix, suffix) {
30  var d = data;
31  if (d === null) return '';
32  return prefix + d + suffix;
33};
34
35DTWidget.formatPercentage = function(data, digits, interval, mark, decMark) {
36  var d = parseFloat(data);
37  if (isNaN(d)) return '';
38  return markInterval(d * 100, digits, interval, mark, decMark) + '%';
39};
40
41DTWidget.formatRound = function(data, digits, interval, mark, decMark) {
42  var d = parseFloat(data);
43  if (isNaN(d)) return '';
44  return markInterval(d, digits, interval, mark, decMark);
45};
46
47DTWidget.formatSignif = function(data, digits, interval, mark, decMark) {
48  var d = parseFloat(data);
49  if (isNaN(d)) return '';
50  return markInterval(d, digits, interval, mark, decMark, true);
51};
52
53DTWidget.formatDate = function(data, method, params) {
54  var d = data;
55  if (d === null) return '';
56  // (new Date('2015-10-28')).toDateString() may return 2015-10-27 because the
57  // actual time created could be like 'Tue Oct 27 2015 19:00:00 GMT-0500 (CDT)',
58  // i.e. the date-only string is treated as UTC time instead of local time
59  if ((method === 'toDateString' || method === 'toLocaleDateString') && /^\d{4,}\D\d{2}\D\d{2}$/.test(d)) {
60    d = d.split(/\D/);
61    d = new Date(d[0], d[1] - 1, d[2]);
62  } else {
63    d = new Date(d);
64  }
65  return d[method].apply(d, params);
66};
67
68window.DTWidget = DTWidget;
69
70var transposeArray2D = function(a) {
71  return a.length === 0 ? a : HTMLWidgets.transposeArray2D(a);
72};
73
74var crosstalkPluginsInstalled = false;
75
76function maybeInstallCrosstalkPlugins() {
77  if (crosstalkPluginsInstalled)
78    return;
79  crosstalkPluginsInstalled = true;
80
81  $.fn.dataTable.ext.afnFiltering.push(
82    function(oSettings, aData, iDataIndex) {
83      var ctfilter = oSettings.nTable.ctfilter;
84      if (ctfilter && !ctfilter[iDataIndex])
85        return false;
86
87      var ctselect = oSettings.nTable.ctselect;
88      if (ctselect && !ctselect[iDataIndex])
89        return false;
90
91      return true;
92    }
93  );
94}
95
96HTMLWidgets.widget({
97  name: "datatables",
98  type: "output",
99  renderOnNullValue: true,
100  initialize: function(el, width, height) {
101    $(el).html(' ');
102    return {
103      data: null,
104      ctfilterHandle: new crosstalk.FilterHandle(),
105      ctfilterSubscription: null,
106      ctselectHandle: new crosstalk.SelectionHandle(),
107      ctselectSubscription: null
108    };
109  },
110  renderValue: function(el, data, instance) {
111    if (el.offsetWidth === 0 || el.offsetHeight === 0) {
112      instance.data = data;
113      return;
114    }
115    instance.data = null;
116    var $el = $(el);
117    $el.empty();
118
119    if (data === null) {
120      $el.append(' ');
121      // clear previous Shiny inputs (if any)
122      for (var i in instance.clearInputs) instance.clearInputs[i]();
123      instance.clearInputs = {};
124      return;
125    }
126
127    var crosstalkOptions = data.crosstalkOptions;
128    if (!crosstalkOptions) crosstalkOptions = {
129      'key': null, 'group': null
130    };
131    if (crosstalkOptions.group) {
132      maybeInstallCrosstalkPlugins();
133      instance.ctfilterHandle.setGroup(crosstalkOptions.group);
134      instance.ctselectHandle.setGroup(crosstalkOptions.group);
135    }
136
137    // if we are in the viewer then we always want to fillContainer and
138    // and autoHideNavigation (unless the user has explicitly set these)
139    if (window.HTMLWidgets.viewerMode) {
140      if (!data.hasOwnProperty("fillContainer"))
141        data.fillContainer = true;
142      if (!data.hasOwnProperty("autoHideNavigation"))
143        data.autoHideNavigation = true;
144    }
145
146    // propagate fillContainer to instance (so we have it in resize)
147    instance.fillContainer = data.fillContainer;
148
149    var cells = data.data;
150
151    if (cells instanceof Array) cells = transposeArray2D(cells);
152
153    $el.append(data.container);
154    var $table = $el.find('table');
155    if (data.class) $table.addClass(data.class);
156    if (data.caption) $table.prepend(data.caption);
157
158    if (!data.selection) data.selection = {
159      mode: 'none', selected: null, target: 'row', selectable: null
160    };
161    if (HTMLWidgets.shinyMode && data.selection.mode !== 'none' &&
162        data.selection.target === 'row+column') {
163      if ($table.children('tfoot').length === 0) {
164        $table.append($('<tfoot>'));
165        $table.find('thead tr').clone().appendTo($table.find('tfoot'));
166      }
167    }
168
169    // column filters
170    var filterRow;
171    switch (data.filter) {
172      case 'top':
173        $table.children('thead').append(data.filterHTML);
174        filterRow = $table.find('thead tr:last td');
175        break;
176      case 'bottom':
177        if ($table.children('tfoot').length === 0) {
178          $table.append($('<tfoot>'));
179        }
180        $table.children('tfoot').prepend(data.filterHTML);
181        filterRow = $table.find('tfoot tr:first td');
182        break;
183    }
184
185    var options = { searchDelay: 1000 };
186    if (cells !== null) $.extend(options, {
187      data: cells
188    });
189
190    // options for fillContainer
191    var bootstrapActive = typeof($.fn.popover) != 'undefined';
192    if (instance.fillContainer) {
193
194      // force scrollX/scrollY and turn off autoWidth
195      options.scrollX = true;
196      options.scrollY = "100px"; // can be any value, we'll adjust below
197
198      // if we aren't paginating then move around the info/filter controls
199      // to save space at the bottom and rephrase the info callback
200      if (data.options.paging === false) {
201
202        // we know how to do this cleanly for bootstrap, not so much
203        // for other themes/layouts
204        if (bootstrapActive) {
205          options.dom = "<'row'<'col-sm-4'i><'col-sm-8'f>>" +
206                        "<'row'<'col-sm-12'tr>>";
207        }
208
209        options.fnInfoCallback = function(oSettings, iStart, iEnd,
210                                           iMax, iTotal, sPre) {
211          return Number(iTotal).toLocaleString() + " records";
212        };
213      }
214    }
215
216    // auto hide navigation if requested
217    // Note, this only works on client-side processing mode as on server-side,
218    // cells (data.data) is null; In addition, we require the pageLength option
219    // being provided explicitly to enable this. Despite we may be able to deduce
220    // the default value of pageLength, it may complicate things so we'd rather
221    // put this responsiblity to users and warn them on the R side.
222    if (data.autoHideNavigation === true && data.options.paging !== false) {
223      // strip all nav if length >= cells
224      if ((cells instanceof Array) && data.options.pageLength >= cells.length)
225        options.dom = bootstrapActive ? "<'row'<'col-sm-12'tr>>" : "t";
226      // alternatively lean things out for flexdashboard mobile portrait
227      else if (bootstrapActive && window.FlexDashboard && window.FlexDashboard.isMobilePhone())
228        options.dom = "<'row'<'col-sm-12'f>>" +
229                      "<'row'<'col-sm-12'tr>>"  +
230                      "<'row'<'col-sm-12'p>>";
231    }
232
233    $.extend(true, options, data.options || {});
234
235    var searchCols = options.searchCols;
236    if (searchCols) {
237      searchCols = searchCols.map(function(x) {
238        return x === null ? '' : x.search;
239      });
240      // FIXME: this means I don't respect the escapeRegex setting
241      delete options.searchCols;
242    }
243
244    // server-side processing?
245    var server = options.serverSide === true;
246
247    // use the dataSrc function to pre-process JSON data returned from R
248    var DT_rows_all = [], DT_rows_current = [];
249    if (server && HTMLWidgets.shinyMode && typeof options.ajax === 'object' &&
250        /^session\/[\da-z]+\/dataobj/.test(options.ajax.url) && !options.ajax.dataSrc) {
251      options.ajax.dataSrc = function(json) {
252        DT_rows_all = $.makeArray(json.DT_rows_all);
253        DT_rows_current = $.makeArray(json.DT_rows_current);
254        var data = json.data;
255        if (!colReorderEnabled()) return data;
256        var table = $table.DataTable(), order = table.colReorder.order(), flag = true, i, j, row;
257        for (i = 0; i < order.length; ++i) if (order[i] !== i) flag = false;
258        if (flag) return data;
259        for (i = 0; i < data.length; ++i) {
260          row = data[i].slice();
261          for (j = 0; j < order.length; ++j) data[i][j] = row[order[j]];
262        }
263        return data;
264      };
265    }
266
267    var thiz = this;
268    if (instance.fillContainer) $table.on('init.dt', function(e) {
269      thiz.fillAvailableHeight(el, $(el).innerHeight());
270    });
271    // If the page contains serveral datatables and one of which enables colReorder,
272    // the table.colReorder.order() function will exist but throws error when called.
273    // So it seems like the only way to know if colReorder is enabled or not is to
274    // check the options.
275    var colReorderEnabled = function() { return "colReorder" in options; };
276    var table = $table.DataTable(options);
277    $el.data('datatable', table);
278
279    // Unregister previous Crosstalk event subscriptions, if they exist
280    if (instance.ctfilterSubscription) {
281      instance.ctfilterHandle.off("change", instance.ctfilterSubscription);
282      instance.ctfilterSubscription = null;
283    }
284    if (instance.ctselectSubscription) {
285      instance.ctselectHandle.off("change", instance.ctselectSubscription);
286      instance.ctselectSubscription = null;
287    }
288
289    if (!crosstalkOptions.group) {
290      $table[0].ctfilter = null;
291      $table[0].ctselect = null;
292    } else {
293      var key = crosstalkOptions.key;
294      function keysToMatches(keys) {
295        if (!keys) {
296          return null;
297        } else {
298          var selectedKeys = {};
299          for (var i = 0; i < keys.length; i++) {
300            selectedKeys[keys[i]] = true;
301          }
302          var matches = {};
303          for (var j = 0; j < key.length; j++) {
304            if (selectedKeys[key[j]])
305              matches[j] = true;
306          }
307          return matches;
308        }
309      }
310
311      function applyCrosstalkFilter(e) {
312        $table[0].ctfilter = keysToMatches(e.value);
313        table.draw();
314      }
315      instance.ctfilterSubscription = instance.ctfilterHandle.on("change", applyCrosstalkFilter);
316      applyCrosstalkFilter({value: instance.ctfilterHandle.filteredKeys});
317
318      function applyCrosstalkSelection(e) {
319        if (e.sender !== instance.ctselectHandle) {
320          table
321            .rows('.' + selClass, {search: 'applied'})
322            .nodes()
323            .to$()
324            .removeClass(selClass);
325          if (selectedRows)
326            changeInput('rows_selected', selectedRows(), void 0, true);
327        }
328
329        if (e.sender !== instance.ctselectHandle && e.value && e.value.length) {
330          var matches = keysToMatches(e.value);
331
332          // persistent selection with plotly (& leaflet)
333          var ctOpts = crosstalk.var("plotlyCrosstalkOpts").get() || {};
334          if (ctOpts.persistent === true) {
335            var matches = $.extend(matches, $table[0].ctselect);
336          }
337
338          $table[0].ctselect = matches;
339          table.draw();
340        } else {
341          if ($table[0].ctselect) {
342            $table[0].ctselect = null;
343            table.draw();
344          }
345        }
346      }
347      instance.ctselectSubscription = instance.ctselectHandle.on("change", applyCrosstalkSelection);
348      // TODO: This next line doesn't seem to work when renderDataTable is used
349      applyCrosstalkSelection({value: instance.ctselectHandle.value});
350    }
351
352    var inArray = function(val, array) {
353      return $.inArray(val, $.makeArray(array)) > -1;
354    };
355
356    // encode + to %2B when searching in the table on server side, because
357    // shiny::parseQueryString() treats + as spaces, and DataTables does not
358    // encode + to %2B (or % to %25) when sending the request
359    var encode_plus = function(x) {
360      return server ? x.replace(/%/g, '%25').replace(/\+/g, '%2B') : x;
361    };
362
363    // search the i-th column
364    var searchColumn = function(i, value) {
365      var regex = false, ci = true;
366      if (options.search) {
367        regex = options.search.regex,
368        ci = options.search.caseInsensitive !== false;
369      }
370      return table.column(i).search(encode_plus(value), regex, !regex, ci);
371    };
372
373    if (data.filter !== 'none') {
374
375      filterRow.each(function(i, td) {
376
377        var $td = $(td), type = $td.data('type'), filter;
378        var $input = $td.children('div').first().children('input');
379        $input.prop('disabled', !table.settings()[0].aoColumns[i].bSearchable || type === 'disabled');
380        $input.on('input blur', function() {
381          $input.next('span').toggle(Boolean($input.val()));
382        });
383        // Bootstrap sets pointer-events to none and we won't be able to click
384        // the clear button
385        $input.next('span').css('pointer-events', 'auto').hide().click(function() {
386          $(this).hide().prev('input').val('').trigger('input').focus();
387        });
388        var searchCol;  // search string for this column
389        if (searchCols && searchCols[i]) {
390          searchCol = searchCols[i];
391          $input.val(searchCol).trigger('input');
392        }
393        var $x = $td.children('div').last();
394
395        // remove the overflow: hidden attribute of the scrollHead
396        // (otherwise the scrolling table body obscures the filters)
397        // The workaround and the discussion from
398        // https://github.com/rstudio/DT/issues/554#issuecomment-518007347
399        // Otherwise the filter selection will not be anchored to the values
400        // when the columns number is many and scrollX is enabled.
401        var scrollHead = $(el).find('.dataTables_scrollHead,.dataTables_scrollFoot');
402        var cssOverflowHead = scrollHead.css('overflow');
403        var scrollBody = $(el).find('.dataTables_scrollBody');
404        var cssOverflowBody = scrollBody.css('overflow');
405        var scrollTable = $(el).find('.dataTables_scroll');
406        var cssOverflowTable = scrollTable.css('overflow');
407        if (cssOverflowHead === 'hidden') {
408          $x.on('show hide', function(e) {
409            if (e.type === 'show') {
410              scrollHead.css('overflow', 'visible');
411              scrollBody.css('overflow', 'visible');
412              scrollTable.css('overflow-x', 'scroll');
413            } else {
414              scrollHead.css('overflow', cssOverflowHead);
415              scrollBody.css('overflow', cssOverflowBody);
416              scrollTable.css('overflow-x', cssOverflowTable);
417            }
418          });
419          $x.css('z-index', 25);
420        }
421
422        if (inArray(type, ['factor', 'logical'])) {
423          $input.on({
424            click: function() {
425              $input.parent().hide(); $x.show().trigger('show'); filter[0].selectize.focus();
426            },
427            input: function() {
428              if ($input.val() === '') filter[0].selectize.setValue([]);
429            }
430          });
431          var $input2 = $x.children('select');
432          filter = $input2.selectize({
433            options: $input2.data('options').map(function(v, i) {
434              return ({text: v, value: v});
435            }),
436            plugins: ['remove_button'],
437            hideSelected: true,
438            onChange: function(value) {
439              if (value === null) value = []; // compatibility with jQuery 3.0
440              $input.val(value.length ? JSON.stringify(value) : '');
441              if (value.length) $input.trigger('input');
442              $input.attr('title', $input.val());
443              if (server) {
444                table.column(i).search(value.length ? encode_plus(JSON.stringify(value)) : '').draw();
445                return;
446              }
447              // turn off filter if nothing selected
448              $td.data('filter', value.length > 0);
449              table.draw();  // redraw table, and filters will be applied
450            }
451          });
452          if (searchCol) filter[0].selectize.setValue(JSON.parse(searchCol));
453          filter[0].selectize.on('blur', function() {
454            $x.hide().trigger('hide'); $input.parent().show(); $input.trigger('blur');
455          });
456          filter.next('div').css('margin-bottom', 'auto');
457        } else if (type === 'character') {
458          var fun = function() {
459            searchColumn(i, $input.val()).draw();
460          };
461          if (server) {
462            fun = $.fn.dataTable.util.throttle(fun, options.searchDelay);
463          }
464          $input.on('input', fun);
465        } else if (inArray(type, ['number', 'integer', 'date', 'time'])) {
466          var $x0 = $x;
467          $x = $x0.children('div').first();
468          $x0.css({
469            'background-color': '#fff',
470            'border': '1px #ddd solid',
471            'border-radius': '4px',
472            'padding': data.vertical ? '35px 20px': '20px 20px 10px 20px'
473          });
474          var $spans = $x0.children('span').css({
475            'margin-top': data.vertical ? '0' : '10px',
476            'white-space': 'nowrap'
477          });
478          var $span1 = $spans.first(), $span2 = $spans.last();
479          var r1 = +$x.data('min'), r2 = +$x.data('max');
480          // when the numbers are too small or have many decimal places, the
481          // slider may have numeric precision problems (#150)
482          var scale = Math.pow(10, Math.max(0, +$x.data('scale') || 0));
483          r1 = Math.round(r1 * scale); r2 = Math.round(r2 * scale);
484          var scaleBack = function(x, scale) {
485            if (scale === 1) return x;
486            var d = Math.round(Math.log(scale) / Math.log(10));
487            // to avoid problems like 3.423/100 -> 0.034230000000000003
488            return (x / scale).toFixed(d);
489          };
490          var slider_min = function() {
491            return filter.noUiSlider('options').range.min;
492          };
493          var slider_max = function() {
494            return filter.noUiSlider('options').range.max;
495          };
496          $input.on({
497            focus: function() {
498              $x0.show().trigger('show');
499              // first, make sure the slider div leaves at least 20px between
500              // the two (slider value) span's
501              $x0.width(Math.max(160, $span1.outerWidth() + $span2.outerWidth() + 20));
502              // then, if the input is really wide or slider is vertical,
503              // make the slider the same width as the input
504              if ($x0.outerWidth() < $input.outerWidth() || data.vertical) {
505                $x0.outerWidth($input.outerWidth());
506              }
507              // make sure the slider div does not reach beyond the right margin
508              if ($(window).width() < $x0.offset().left + $x0.width()) {
509                $x0.offset({
510                  'left': $input.offset().left + $input.outerWidth() - $x0.outerWidth()
511                });
512              }
513            },
514            blur: function() {
515              $x0.hide().trigger('hide');
516            },
517            input: function() {
518              if ($input.val() === '') filter.val([slider_min(), slider_max()]);
519            },
520            change: function() {
521              var v = $input.val().replace(/\s/g, '');
522              if (v === '') return;
523              v = v.split('...');
524              if (v.length !== 2) {
525                $input.parent().addClass('has-error');
526                return;
527              }
528              if (v[0] === '') v[0] = slider_min();
529              if (v[1] === '') v[1] = slider_max();
530              $input.parent().removeClass('has-error');
531              // treat date as UTC time at midnight
532              var strTime = function(x) {
533                var s = type === 'date' ? 'T00:00:00Z' : '';
534                var t = new Date(x + s).getTime();
535                // add 10 minutes to date since it does not hurt the date, and
536                // it helps avoid the tricky floating point arithmetic problems,
537                // e.g. sometimes the date may be a few milliseconds earlier
538                // than the midnight due to precision problems in noUiSlider
539                return type === 'date' ? t + 3600000 : t;
540              };
541              if (inArray(type, ['date', 'time'])) {
542                v[0] = strTime(v[0]);
543                v[1] = strTime(v[1]);
544              }
545              if (v[0] != slider_min()) v[0] *= scale;
546              if (v[1] != slider_max()) v[1] *= scale;
547              filter.val(v);
548            }
549          });
550          var formatDate = function(d, isoFmt) {
551            d = scaleBack(d, scale);
552            if (type === 'number') return d;
553            if (type === 'integer') return parseInt(d);
554            var x = new Date(+d);
555            var fmt = ('filterDateFmt' in data) ? data.filterDateFmt[i] : undefined;
556            if (fmt !== undefined && isoFmt === false) return x[fmt.method].apply(x, fmt.params);
557            if (type === 'date') {
558              var pad0 = function(x) {
559                return ('0' + x).substr(-2, 2);
560              };
561              return x.getUTCFullYear() + '-' + pad0(1 + x.getUTCMonth())
562                      + '-' + pad0(x.getUTCDate());
563            } else {
564              return x.toISOString();
565            }
566          };
567          var opts = type === 'date' ? { step: 60 * 60 * 1000 } :
568                     type === 'integer' ? { step: 1 } : {};
569
570          opts.orientation = data.vertical ? 'vertical': 'horizontal';
571          opts.direction = data.vertical ? 'rtl': 'ltr';
572
573          filter = $x.noUiSlider($.extend({
574            start: [r1, r2],
575            range: {min: r1, max: r2},
576            connect: true
577          }, opts));
578          if (scale > 1) (function() {
579            var t1 = r1, t2 = r2;
580            var val = filter.val();
581            while (val[0] > r1 || val[1] < r2) {
582              if (val[0] > r1) {
583                t1 -= val[0] - r1;
584              }
585              if (val[1] < r2) {
586                t2 += r2 - val[1];
587              }
588              filter = $x.noUiSlider($.extend({
589                start: [t1, t2],
590                range: {min: t1, max: t2},
591                connect: true
592              }, opts), true);
593              val = filter.val();
594            }
595            r1  = t1; r2 = t2;
596          })();
597          var updateSliderText = function(v1, v2) {
598            $span1.text(formatDate(v1, false)); $span2.text(formatDate(v2, false));
599          };
600          updateSliderText(r1, r2);
601          var updateSlider = function(e) {
602            var val = filter.val();
603            // turn off filter if in full range
604            $td.data('filter', val[0] > slider_min() || val[1] < slider_max());
605            var v1 = formatDate(val[0]), v2 = formatDate(val[1]), ival;
606            if ($td.data('filter')) {
607              ival = v1 + ' ... ' + v2;
608              $input.attr('title', ival).val(ival).trigger('input');
609            } else {
610              $input.attr('title', '').val('');
611            }
612            updateSliderText(val[0], val[1]);
613            if (e.type === 'slide') return;  // no searching when sliding only
614            if (server) {
615              table.column(i).search($td.data('filter') ? ival : '').draw();
616              return;
617            }
618            table.draw();
619          };
620          filter.on({
621            set: updateSlider,
622            slide: updateSlider
623          });
624        }
625
626        // server-side processing will be handled by R (or whatever server
627        // language you use); the following code is only needed for client-side
628        // processing
629        if (server) {
630          // if a search string has been pre-set, search now
631          if (searchCol) searchColumn(i, searchCol).draw();
632          return;
633        }
634
635        var customFilter = function(settings, data, dataIndex) {
636          // there is no way to attach a search function to a specific table,
637          // and we need to make sure a global search function is not applied to
638          // all tables (i.e. a range filter in a previous table should not be
639          // applied to the current table); we use the settings object to
640          // determine if we want to perform searching on the current table,
641          // since settings.sTableId will be different to different tables
642          if (table.settings()[0] !== settings) return true;
643          // no filter on this column or no need to filter this column
644          if (typeof filter === 'undefined' || !$td.data('filter')) return true;
645
646          var r = filter.val(), v, r0, r1;
647          var i_data = function(i) {
648            if (!colReorderEnabled()) return i;
649            var order = table.colReorder.order(), k;
650            for (k = 0; k < order.length; ++k) if (order[k] === i) return k;
651            return i; // in theory it will never be here...
652          }
653          v = data[i_data(i)];
654          if (type === 'number' || type === 'integer') {
655            v = parseFloat(v);
656            // how to handle NaN? currently exclude these rows
657            if (isNaN(v)) return(false);
658            r0 = parseFloat(scaleBack(r[0], scale))
659            r1 = parseFloat(scaleBack(r[1], scale));
660            if (v >= r0 && v <= r1) return true;
661          } else if (type === 'date' || type === 'time') {
662            v = new Date(v);
663            r0 = new Date(r[0] / scale); r1 = new Date(r[1] / scale);
664            if (v >= r0 && v <= r1) return true;
665          } else if (type === 'factor') {
666            if (r.length === 0 || inArray(v, r)) return true;
667          } else if (type === 'logical') {
668            if (r.length === 0) return true;
669            if (inArray(v === '' ? 'na' : v, r)) return true;
670          }
671          return false;
672        };
673
674        $.fn.dataTable.ext.search.push(customFilter);
675
676        // search for the preset search strings if it is non-empty
677        if (searchCol) {
678          if (inArray(type, ['factor', 'logical'])) {
679            filter[0].selectize.setValue(JSON.parse(searchCol));
680          } else if (type === 'character') {
681            $input.trigger('input');
682          } else if (inArray(type, ['number', 'integer', 'date', 'time'])) {
683            $input.trigger('change');
684          }
685        }
686
687      });
688
689    }
690
691    // highlight search keywords
692    var highlight = function() {
693      var body = $(table.table().body());
694      // removing the old highlighting first
695      body.unhighlight();
696
697      // don't highlight the "not found" row, so we get the rows using the api
698      if (table.rows({ filter: 'applied' }).data().length === 0) return;
699      // highlight global search keywords
700      body.highlight($.trim(table.search()).split(/\s+/));
701      // then highlight keywords from individual column filters
702      if (filterRow) filterRow.each(function(i, td) {
703        var $td = $(td), type = $td.data('type');
704        if (type !== 'character') return;
705        var $input = $td.children('div').first().children('input');
706        var column = table.column(i).nodes().to$(),
707            val = $.trim($input.val());
708        if (type !== 'character' || val === '') return;
709        column.highlight(val.split(/\s+/));
710      });
711    };
712
713    if (options.searchHighlight) {
714      table
715      .on('draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth', highlight)
716      .on('destroy', function() {
717        // remove event handler
718        table.off('draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth');
719      });
720
721      // Set the option for escaping regex characters in our search string.  This will be used
722      // for all future matching.
723      jQuery.fn.highlight.options.escapeRegex = (!options.search || !options.search.regex);
724
725      // initial highlight for state saved conditions and initial states
726      highlight();
727    }
728
729    // run the callback function on the table instance
730    if (typeof data.callback === 'function') data.callback(table);
731
732    // double click to edit the cell, row, column, or all cells
733    if (data.editable) table.on('dblclick.dt', 'tbody td', function(e) {
734      // only bring up the editor when the cell itself is dbclicked, and ignore
735      // other dbclick events bubbled up (e.g. from the <input>)
736      if (e.target !== this) return;
737      var target = [], immediate = false;
738      switch (data.editable.target) {
739        case 'cell':
740          target = [this];
741          immediate = true;  // edit will take effect immediately
742          break;
743        case 'row':
744          target = table.cells(table.cell(this).index().row, '*').nodes();
745          break;
746        case 'column':
747          target = table.cells('*', table.cell(this).index().column).nodes();
748          break;
749        case 'all':
750          target = table.cells().nodes();
751          break;
752        default:
753          throw 'The editable parameter must be "cell", "row", "column", or "all"';
754      }
755      var disableCols = data.editable.disable ? data.editable.disable.columns : null;
756      var numericCols = data.editable.numeric;
757      var areaCols = data.editable.area;
758      for (var i = 0; i < target.length; i++) {
759        (function(cell, current) {
760          var $cell = $(cell), html = $cell.html();
761          var _cell = table.cell(cell), value = _cell.data(), index = _cell.index().column;
762          var $input;
763          if (inArray(index, numericCols)) {
764            $input = $('<input type="number">');
765          } else if (inArray(index, areaCols)) {
766            $input = $('<textarea></textarea>');
767          } else {
768            $input = $('<input type="text">');
769          }
770          if (!immediate) {
771            $cell.data('input', $input).data('html', html);
772            $input.attr('title', 'Hit Ctrl+Enter to finish editing, or Esc to cancel');
773          }
774          $input.val(value);
775          if (inArray(index, disableCols)) {
776            $input.attr('readonly', '').css('filter', 'invert(25%)');
777          }
778          $cell.empty().append($input);
779          if (cell === current) $input.focus();
780          $input.css('width', '100%');
781
782          if (immediate) $input.on('blur', function(e) {
783            var valueNew = $input.val();
784            if (valueNew != value) {
785              _cell.data(valueNew);
786              if (HTMLWidgets.shinyMode) {
787                changeInput('cell_edit', [cellInfo(cell)], 'DT.cellInfo', null, {priority: 'event'});
788              }
789              // for server-side processing, users have to call replaceData() to update the table
790              if (!server) table.draw(false);
791            } else {
792              $cell.html(html);
793            }
794          }).on('keyup', function(e) {
795            // hit Escape to cancel editing
796            if (e.keyCode === 27) $input.trigger('blur');
797          });
798
799          // bulk edit (row, column, or all)
800          if (!immediate) $input.on('keyup', function(e) {
801            var removeInput = function($cell, restore) {
802              $cell.data('input').remove();
803              if (restore) $cell.html($cell.data('html'));
804            }
805            if (e.keyCode === 27) {
806              for (var i = 0; i < target.length; i++) {
807                removeInput($(target[i]), true);
808              }
809            } else if (e.keyCode === 13 && e.ctrlKey) {
810              // Ctrl + Enter
811              var cell, $cell, _cell, cellData = [];
812              for (var i = 0; i < target.length; i++) {
813                cell = target[i]; $cell = $(cell); _cell = table.cell(cell);
814                _cell.data($cell.data('input').val());
815                HTMLWidgets.shinyMode && cellData.push(cellInfo(cell));
816                removeInput($cell, false);
817              }
818              if (HTMLWidgets.shinyMode) {
819                changeInput('cell_edit', cellData, 'DT.cellInfo', null, {priority: "event"});
820              }
821              if (!server) table.draw(false);
822            }
823          });
824        })(target[i], this);
825      }
826    });
827
828    // interaction with shiny
829    if (!HTMLWidgets.shinyMode && !crosstalkOptions.group) return;
830
831    var methods = {};
832    var shinyData = {};
833
834    methods.updateCaption = function(caption) {
835      if (!caption) return;
836      $table.children('caption').replaceWith(caption);
837    }
838
839    // register clear functions to remove input values when the table is removed
840    instance.clearInputs = {};
841
842    var changeInput = function(id, value, type, noCrosstalk, opts) {
843      var event = id;
844      id = el.id + '_' + id;
845      if (type) id = id + ':' + type;
846      // do not update if the new value is the same as old value
847      if (event !== 'cell_edit' && !/_clicked$/.test(event) && shinyData.hasOwnProperty(id) && shinyData[id] === JSON.stringify(value))
848        return;
849      shinyData[id] = JSON.stringify(value);
850      if (HTMLWidgets.shinyMode && Shiny.setInputValue) {
851        Shiny.setInputValue(id, value, opts);
852        if (!instance.clearInputs[id]) instance.clearInputs[id] = function() {
853          Shiny.setInputValue(id, null);
854        }
855      }
856
857      // HACK
858      if (event === "rows_selected" && !noCrosstalk) {
859        if (crosstalkOptions.group) {
860          var keys = crosstalkOptions.key;
861          var selectedKeys = null;
862          if (value) {
863            selectedKeys = [];
864            for (var i = 0; i < value.length; i++) {
865              // The value array's contents use 1-based row numbers, so we must
866              // convert to 0-based before indexing into the keys array.
867              selectedKeys.push(keys[value[i] - 1]);
868            }
869          }
870          instance.ctselectHandle.set(selectedKeys);
871        }
872      }
873    };
874
875    var addOne = function(x) {
876      return x.map(function(i) { return 1 + i; });
877    };
878
879    var unique = function(x) {
880      var ux = [];
881      $.each(x, function(i, el){
882        if ($.inArray(el, ux) === -1) ux.push(el);
883      });
884      return ux;
885    }
886
887    // change the row index of a cell
888    var tweakCellIndex = function(cell) {
889      var info = cell.index();
890      // some cell may not be valid. e.g, #759
891      // when using the RowGroup extension, datatables will
892      // generate the row label and the cells are not part of
893      // the data thus contain no row/col info
894      if (info === undefined)
895        return {row: null, col: null};
896      if (server) {
897        info.row = DT_rows_current[info.row];
898      } else {
899        info.row += 1;
900      }
901      return {row: info.row, col: info.column};
902    }
903
904    var cleanSelectedValues = function() {
905      changeInput('rows_selected', []);
906      changeInput('columns_selected', []);
907      changeInput('cells_selected', transposeArray2D([]), 'shiny.matrix');
908    }
909    // #828 we should clean the selection on the server-side when the table reloads
910    cleanSelectedValues();
911
912    // a flag to indicates if select extension is initialized or not
913    var flagSelectExt = table.settings()[0]._select !== undefined;
914    // the Select extension should only be used in the client mode and
915    // when the selection.mode is set to none
916    if (data.selection.mode === 'none' && !server && flagSelectExt) {
917      var updateRowsSelected = function() {
918        var rows = table.rows({selected: true});
919        var selected = [];
920        $.each(rows.indexes().toArray(), function(i, v) {
921          selected.push(v + 1);
922        });
923        changeInput('rows_selected', selected);
924      }
925      var updateColsSelected = function() {
926        var columns = table.columns({selected: true});
927        changeInput('columns_selected', columns.indexes().toArray());
928      }
929      var updateCellsSelected = function() {
930        var cells = table.cells({selected: true});
931        var selected = [];
932        cells.every(function() {
933          var row = this.index().row;
934          var col = this.index().column;
935          selected = selected.concat([[row + 1, col]]);
936        });
937        changeInput('cells_selected', transposeArray2D(selected), 'shiny.matrix');
938      }
939      table.on('select deselect', function(e, dt, type, indexes) {
940        updateRowsSelected();
941        updateColsSelected();
942        updateCellsSelected();
943      })
944    }
945
946    var selMode = data.selection.mode, selTarget = data.selection.target;
947    var selDisable = data.selection.selectable === false;
948    if (inArray(selMode, ['single', 'multiple'])) {
949      var selClass = inArray(data.style, ['bootstrap', 'bootstrap4']) ? 'active' : 'selected';
950      // selected1: row indices; selected2: column indices
951      var initSel = function(x) {
952        if (x === null || typeof x === 'boolean' || selTarget === 'cell') {
953          return {rows: [], cols: []};
954        } else if (selTarget === 'row') {
955          return {rows: $.makeArray(x), cols: []};
956        } else if (selTarget === 'column') {
957          return {rows: [], cols: $.makeArray(x)};
958        } else if (selTarget === 'row+column') {
959          return {rows: $.makeArray(x.rows), cols: $.makeArray(x.cols)};
960        }
961      }
962      var selected = data.selection.selected;
963      var selected1 = initSel(selected).rows, selected2 = initSel(selected).cols;
964      // selectable should contain either all positive or all non-positive values, not both
965      // positive values indicate "selectable" while non-positive values means "nonselectable"
966      // the assertion is performed on R side. (only column indicides could be zero which indicates
967      // the row name)
968      var selectable = data.selection.selectable;
969      var selectable1 = initSel(selectable).rows, selectable2 = initSel(selectable).cols;
970
971      // After users reorder the rows or filter the table, we cannot use the table index
972      // directly. Instead, we need this function to find out the rows between the two clicks.
973      // If user filter the table again between the start click and the end click, the behavior
974      // would be undefined, but it should not be a problem.
975      var shiftSelRowsIndex = function(start, end) {
976        var indexes = server ? DT_rows_all : table.rows({ search: 'applied' }).indexes().toArray();
977        start = indexes.indexOf(start); end = indexes.indexOf(end);
978        // if start is larger than end, we need to swap
979        if (start > end) {
980          var tmp = end; end = start; start = tmp;
981        }
982        return indexes.slice(start, end + 1);
983      }
984
985      var serverRowIndex = function(clientRowIndex) {
986        return server ? DT_rows_current[clientRowIndex] : clientRowIndex + 1;
987      }
988
989      // row, column, or cell selection
990      var lastClickedRow;
991      if (inArray(selTarget, ['row', 'row+column'])) {
992        // Get the current selected rows. It will also
993        // update the selected1's value based on the current row selection state
994        // Note we can't put this function inside selectRows() directly,
995        // the reason is method.selectRows() will override selected1's value but this
996        // function will add rows to selected1 (keep the existing selection), which is
997        // inconsistent with column and cell selection.
998        var selectedRows = function() {
999          var rows = table.rows('.' + selClass);
1000          var idx = rows.indexes().toArray();
1001          if (!server) {
1002            selected1 = addOne(idx);
1003            return selected1;
1004          }
1005          idx = idx.map(function(i) {
1006            return DT_rows_current[i];
1007          });
1008          selected1 = selMode === 'multiple' ? unique(selected1.concat(idx)) : idx;
1009          return selected1;
1010        }
1011        // Change selected1's value based on selectable1, then refresh the row state
1012        var onlyKeepSelectableRows = function() {
1013          if (selDisable) { // users can't select; useful when only want backend select
1014            selected1 = [];
1015            return;
1016          }
1017          if (selectable1.length === 0) return;
1018          var nonselectable = selectable1[0] <= 0;
1019          if (nonselectable) {
1020            // should make selectable1 positive
1021            selected1 = $(selected1).not(selectable1.map(function(i) { return -i; })).get();
1022          } else {
1023            selected1 = $(selected1).filter(selectable1).get();
1024          }
1025        }
1026        // Change selected1's value based on selectable1, then
1027        // refresh the row selection state according to values in selected1
1028        var selectRows = function(ignoreSelectable) {
1029          if (!ignoreSelectable) onlyKeepSelectableRows();
1030          table.$('tr.' + selClass).removeClass(selClass);
1031          if (selected1.length === 0) return;
1032          if (server) {
1033            table.rows({page: 'current'}).every(function() {
1034              if (inArray(DT_rows_current[this.index()], selected1)) {
1035                $(this.node()).addClass(selClass);
1036              }
1037            });
1038          } else {
1039            var selected0 = selected1.map(function(i) { return i - 1; });
1040            $(table.rows(selected0).nodes()).addClass(selClass);
1041          }
1042        }
1043        table.on('mousedown.dt', 'tbody tr', function(e) {
1044          var $this = $(this), thisRow = table.row(this);
1045          if (selMode === 'multiple') {
1046            if (e.shiftKey && lastClickedRow !== undefined) {
1047              // select or de-select depends on the last clicked row's status
1048              var flagSel = !$this.hasClass(selClass);
1049              var crtClickedRow = serverRowIndex(thisRow.index());
1050              if (server) {
1051                var rowsIndex = shiftSelRowsIndex(lastClickedRow, crtClickedRow);
1052                // update current page's selClass
1053                rowsIndex.map(function(i) {
1054                  var rowIndex = DT_rows_current.indexOf(i);
1055                  if (rowIndex >= 0) {
1056                    var row = table.row(rowIndex).nodes().to$();
1057                    var flagRowSel = !row.hasClass(selClass);
1058                    if (flagSel === flagRowSel) row.toggleClass(selClass);
1059                  }
1060                });
1061                // update selected1
1062                if (flagSel) {
1063                  selected1 = unique(selected1.concat(rowsIndex));
1064                } else {
1065                  selected1 = selected1.filter(function(index) {
1066                    return !inArray(index, rowsIndex);
1067                  });
1068                }
1069              } else {
1070                // js starts from 0
1071                shiftSelRowsIndex(lastClickedRow - 1, crtClickedRow - 1).map(function(value) {
1072                  var row = table.row(value).nodes().to$();
1073                  var flagRowSel = !row.hasClass(selClass);
1074                  if (flagSel === flagRowSel) row.toggleClass(selClass);
1075                });
1076              }
1077              e.preventDefault();
1078            } else {
1079              $this.toggleClass(selClass);
1080            }
1081          } else {
1082            if ($this.hasClass(selClass)) {
1083              $this.removeClass(selClass);
1084            } else {
1085              table.$('tr.' + selClass).removeClass(selClass);
1086              $this.addClass(selClass);
1087            }
1088          }
1089          if (server && !$this.hasClass(selClass)) {
1090            var id = DT_rows_current[thisRow.index()];
1091            // remove id from selected1 since its class .selected has been removed
1092            if (inArray(id, selected1)) selected1.splice($.inArray(id, selected1), 1);
1093          }
1094          selectedRows(); // update selected1's value based on selClass
1095          selectRows(false); // only keep the selectable rows
1096          changeInput('rows_selected', selected1);
1097          changeInput('row_last_clicked', serverRowIndex(thisRow.index()), null, null, {priority: 'event'});
1098          lastClickedRow = serverRowIndex(thisRow.index());
1099        });
1100        selectRows(false);  // in case users have specified pre-selected rows
1101        // restore selected rows after the table is redrawn (e.g. sort/search/page);
1102        // client-side tables will preserve the selections automatically; for
1103        // server-side tables, we have to *real* row indices are in `selected1`
1104        changeInput('rows_selected', selected1);
1105        if (server) table.on('draw.dt', function(e) { selectRows(false); });
1106        methods.selectRows = function(selected, ignoreSelectable) {
1107          selected1 = $.makeArray(selected);
1108          selectRows(ignoreSelectable);
1109          changeInput('rows_selected', selected1);
1110        }
1111      }
1112
1113      if (inArray(selTarget, ['column', 'row+column'])) {
1114        if (selTarget === 'row+column') {
1115          $(table.columns().footer()).css('cursor', 'pointer');
1116        }
1117        // update selected2's value based on selectable2
1118        var onlyKeepSelectableCols = function() {
1119          if (selDisable) { // users can't select; useful when only want backend select
1120            selected2 = [];
1121            return;
1122          }
1123          if (selectable2.length === 0) return;
1124          var nonselectable = selectable2[0] <= 0;
1125          if (nonselectable) {
1126            // need to make selectable2 positive
1127            selected2 = $(selected2).not(selectable2.map(function(i) { return -i; })).get();
1128          } else {
1129            selected2 = $(selected2).filter(selectable2).get();
1130          }
1131        }
1132        // update selected2 and then
1133        // refresh the col selection state according to values in selected2
1134        var selectCols = function(ignoreSelectable) {
1135          if (!ignoreSelectable) onlyKeepSelectableCols();
1136          // if selected2 is not a valide index (e.g., larger than the column number)
1137          // table.columns(selected2) will fail and result in a blank table
1138          // this is different from the table.rows(), where the out-of-range indexes
1139          // doesn't affect at all
1140          selected2 = $(selected2).filter(table.columns().indexes()).get();
1141          table.columns().nodes().flatten().to$().removeClass(selClass);
1142          if (selected2.length > 0)
1143            table.columns(selected2).nodes().flatten().to$().addClass(selClass);
1144        }
1145        var callback = function() {
1146          var colIdx = selTarget === 'column' ? table.cell(this).index().column :
1147              $.inArray(this, table.columns().footer()),
1148              thisCol = $(table.column(colIdx).nodes());
1149          if (colIdx === -1) return;
1150          if (thisCol.hasClass(selClass)) {
1151            thisCol.removeClass(selClass);
1152            selected2.splice($.inArray(colIdx, selected2), 1);
1153          } else {
1154            if (selMode === 'single') $(table.cells().nodes()).removeClass(selClass);
1155            thisCol.addClass(selClass);
1156            selected2 = selMode === 'single' ? [colIdx] : unique(selected2.concat([colIdx]));
1157          }
1158          selectCols(false); // update selected2 based on selectable
1159          changeInput('columns_selected', selected2);
1160        }
1161        if (selTarget === 'column') {
1162          $(table.table().body()).on('click.dt', 'td', callback);
1163        } else {
1164          $(table.table().footer()).on('click.dt', 'tr th', callback);
1165        }
1166        selectCols(false);  // in case users have specified pre-selected columns
1167        changeInput('columns_selected', selected2);
1168        if (server) table.on('draw.dt', function(e) { selectCols(false); });
1169        methods.selectColumns = function(selected, ignoreSelectable) {
1170          selected2 = $.makeArray(selected);
1171          selectCols(ignoreSelectable);
1172          changeInput('columns_selected', selected2);
1173        }
1174      }
1175
1176      if (selTarget === 'cell') {
1177        var selected3 = [], selectable3 = [];
1178        if (selected !== null) selected3 = selected;
1179        if (selectable !== null && typeof selectable !== 'boolean') selectable3 = selectable;
1180        var findIndex = function(ij, sel) {
1181          for (var i = 0; i < sel.length; i++) {
1182            if (ij[0] === sel[i][0] && ij[1] === sel[i][1]) return i;
1183          }
1184          return -1;
1185        }
1186         // Change selected3's value based on selectable3, then refresh the cell state
1187        var onlyKeepSelectableCells = function() {
1188          if (selDisable) { // users can't select; useful when only want backend select
1189            selected3 = [];
1190            return;
1191          }
1192          if (selectable3.length === 0) return;
1193          var nonselectable = selectable3[0][0] <= 0;
1194          var out = [];
1195          if (nonselectable) {
1196            selected3.map(function(ij) {
1197              // should make selectable3 positive
1198              if (findIndex([-ij[0], -ij[1]], selectable3) === -1) { out.push(ij); }
1199            });
1200          } else {
1201            selected3.map(function(ij) {
1202              if (findIndex(ij, selectable3) > -1) { out.push(ij); }
1203            });
1204          }
1205          selected3 = out;
1206        }
1207        // Change selected3's value based on selectable3, then
1208        // refresh the cell selection state according to values in selected3
1209        var selectCells = function(ignoreSelectable) {
1210          if (!ignoreSelectable) onlyKeepSelectableCells();
1211          table.$('td.' + selClass).removeClass(selClass);
1212          if (selected3.length === 0) return;
1213          if (server) {
1214            table.cells({page: 'current'}).every(function() {
1215              var info = tweakCellIndex(this);
1216              if (findIndex([info.row, info.col], selected3) > -1)
1217                $(this.node()).addClass(selClass);
1218            });
1219          } else {
1220            selected3.map(function(ij) {
1221              $(table.cell(ij[0] - 1, ij[1]).node()).addClass(selClass);
1222            });
1223          }
1224        };
1225        table.on('click.dt', 'tbody td', function() {
1226          var $this = $(this), info = tweakCellIndex(table.cell(this));
1227          if ($this.hasClass(selClass)) {
1228            $this.removeClass(selClass);
1229            selected3.splice(findIndex([info.row, info.col], selected3), 1);
1230          } else {
1231            if (selMode === 'single') $(table.cells().nodes()).removeClass(selClass);
1232            $this.addClass(selClass);
1233            selected3 = selMode === 'single' ? [[info.row, info.col]] :
1234              unique(selected3.concat([[info.row, info.col]]));
1235          }
1236          selectCells(false); // must call this to update selected3 based on selectable3
1237          changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix');
1238        });
1239        selectCells(false);  // in case users have specified pre-selected columns
1240        changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix');
1241
1242        if (server) table.on('draw.dt', function(e) { selectCells(false); });
1243        methods.selectCells = function(selected, ignoreSelectable) {
1244          selected3 = selected ? selected : [];
1245          selectCells(ignoreSelectable);
1246          changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix');
1247        }
1248      }
1249    }
1250
1251    // expose some table info to Shiny
1252    var updateTableInfo = function(e, settings) {
1253      // TODO: is anyone interested in the page info?
1254      // changeInput('page_info', table.page.info());
1255      var updateRowInfo = function(id, modifier) {
1256        var idx;
1257        if (server) {
1258          idx = modifier.page === 'current' ? DT_rows_current : DT_rows_all;
1259        } else {
1260          var rows = table.rows($.extend({
1261            search: 'applied',
1262            page: 'all'
1263          }, modifier));
1264          idx = addOne(rows.indexes().toArray());
1265        }
1266        changeInput('rows' + '_' + id, idx);
1267      };
1268      updateRowInfo('current', {page: 'current'});
1269      updateRowInfo('all', {});
1270    }
1271    table.on('draw.dt', updateTableInfo);
1272    updateTableInfo();
1273
1274    // state info
1275    table.on('draw.dt column-visibility.dt', function() {
1276      changeInput('state', table.state());
1277    });
1278    changeInput('state', table.state());
1279
1280    // search info
1281    var updateSearchInfo = function() {
1282      changeInput('search', table.search());
1283      if (filterRow) changeInput('search_columns', filterRow.toArray().map(function(td) {
1284        return $(td).find('input').first().val();
1285      }));
1286    }
1287    table.on('draw.dt', updateSearchInfo);
1288    updateSearchInfo();
1289
1290    var cellInfo = function(thiz) {
1291      var info = tweakCellIndex(table.cell(thiz));
1292      info.value = table.cell(thiz).data();
1293      return info;
1294    }
1295    // the current cell clicked on
1296    table.on('click.dt', 'tbody td', function() {
1297      changeInput('cell_clicked', cellInfo(this), null, null, {priority: 'event'});
1298    })
1299    changeInput('cell_clicked', {});
1300
1301    // do not trigger table selection when clicking on links unless they have classes
1302    table.on('click.dt', 'tbody td a', function(e) {
1303      if (this.className === '') e.stopPropagation();
1304    });
1305
1306    methods.addRow = function(data, rowname, resetPaging) {
1307      var n = table.columns().indexes().length, d = n - data.length;
1308      if (d === 1) {
1309        data = rowname.concat(data)
1310      } else if (d !== 0) {
1311        console.log(data);
1312        console.log(table.columns().indexes());
1313        throw 'New data must be of the same length as current data (' + n + ')';
1314      };
1315      table.row.add(data).draw(resetPaging);
1316    }
1317
1318    methods.updateSearch = function(keywords) {
1319      if (keywords.global !== null)
1320        $(table.table().container()).find('input[type=search]').first()
1321             .val(keywords.global).trigger('input');
1322      var columns = keywords.columns;
1323      if (!filterRow || columns === null) return;
1324      filterRow.toArray().map(function(td, i) {
1325        var v = typeof columns === 'string' ? columns : columns[i];
1326        if (typeof v === 'undefined') {
1327          console.log('The search keyword for column ' + i + ' is undefined')
1328          return;
1329        }
1330        $(td).find('input').first().val(v);
1331        searchColumn(i, v);
1332      });
1333      table.draw();
1334    }
1335
1336    methods.hideCols = function(hide, reset) {
1337      if (reset) table.columns().visible(true, false);
1338      table.columns(hide).visible(false);
1339    }
1340
1341    methods.showCols = function(show, reset) {
1342      if (reset) table.columns().visible(false, false);
1343      table.columns(show).visible(true);
1344    }
1345
1346    methods.colReorder = function(order, origOrder) {
1347      table.colReorder.order(order, origOrder);
1348    }
1349
1350    methods.selectPage = function(page) {
1351      if (table.page.info().pages < page || page < 1) {
1352        throw 'Selected page is out of range';
1353      };
1354      table.page(page - 1).draw(false);
1355    }
1356
1357    methods.reloadData = function(resetPaging, clearSelection) {
1358      // empty selections first if necessary
1359      if (methods.selectRows && inArray('row', clearSelection)) methods.selectRows([]);
1360      if (methods.selectColumns && inArray('column', clearSelection)) methods.selectColumns([]);
1361      if (methods.selectCells && inArray('cell', clearSelection)) methods.selectCells([]);
1362      table.ajax.reload(null, resetPaging);
1363    }
1364
1365    table.shinyMethods = methods;
1366  },
1367  resize: function(el, width, height, instance) {
1368    if (instance.data) this.renderValue(el, instance.data, instance);
1369
1370    // dynamically adjust height if fillContainer = TRUE
1371    if (instance.fillContainer)
1372      this.fillAvailableHeight(el, height);
1373
1374    this.adjustWidth(el);
1375  },
1376
1377  // dynamically set the scroll body to fill available height
1378  // (used with fillContainer = TRUE)
1379  fillAvailableHeight: function(el, availableHeight) {
1380
1381    // see how much of the table is occupied by header/footer elements
1382    // and use that to compute a target scroll body height
1383    var dtWrapper = $(el).find('div.dataTables_wrapper');
1384    var dtScrollBody = $(el).find($('div.dataTables_scrollBody'));
1385    var framingHeight = dtWrapper.innerHeight() - dtScrollBody.innerHeight();
1386    var scrollBodyHeight = availableHeight - framingHeight;
1387
1388    // set the height
1389    dtScrollBody.height(scrollBodyHeight + 'px');
1390  },
1391
1392  // adjust the width of columns; remove the hard-coded widths on table and the
1393  // scroll header when scrollX/Y are enabled
1394  adjustWidth: function(el) {
1395    var $el = $(el), table = $el.data('datatable');
1396    if (table) table.columns.adjust();
1397    $el.find('.dataTables_scrollHeadInner').css('width', '')
1398        .children('table').css('margin-left', '');
1399  }
1400});
1401
1402  if (!HTMLWidgets.shinyMode) return;
1403
1404  Shiny.addCustomMessageHandler('datatable-calls', function(data) {
1405    var id = data.id;
1406    var el = document.getElementById(id);
1407    var table = el ? $(el).data('datatable') : null;
1408    if (!table) {
1409      console.log("Couldn't find table with id " + id);
1410      return;
1411    }
1412
1413    var methods = table.shinyMethods, call = data.call;
1414    if (methods[call.method]) {
1415      methods[call.method].apply(table, call.args);
1416    } else {
1417      console.log("Unknown method " + call.method);
1418    }
1419  });
1420
1421})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.