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