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