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