PageSourceSearch

https://dwayzer.netlify.app/site_libs/datatables-binding-0.17/datatables.js

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

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.