PageSourceSearch

https://wormbuilder.org/crispr/datatables-binding-0.13/datatables.js

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

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.