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