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.