1280 linhas
49KB

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