1
0

datatables.js 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  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. var scrollHead = $(el).find('.dataTables_scrollHead,.dataTables_scrollFoot');
  356. var cssOverflow = scrollHead.css('overflow');
  357. if (cssOverflow === 'hidden') {
  358. $x.on('show hide', function(e) {
  359. scrollHead.css('overflow', e.type === 'show' ? '' : cssOverflow);
  360. });
  361. $x.css('z-index', 25);
  362. }
  363. if (inArray(type, ['factor', 'logical'])) {
  364. $input.on({
  365. click: function() {
  366. $input.parent().hide(); $x.show().trigger('show'); filter[0].selectize.focus();
  367. },
  368. input: function() {
  369. if ($input.val() === '') filter[0].selectize.setValue([]);
  370. }
  371. });
  372. var $input2 = $x.children('select');
  373. filter = $input2.selectize({
  374. options: $input2.data('options').map(function(v, i) {
  375. return ({text: v, value: v});
  376. }),
  377. plugins: ['remove_button'],
  378. hideSelected: true,
  379. onChange: function(value) {
  380. if (value === null) value = []; // compatibility with jQuery 3.0
  381. $input.val(value.length ? JSON.stringify(value) : '');
  382. if (value.length) $input.trigger('input');
  383. $input.attr('title', $input.val());
  384. if (server) {
  385. table.column(i).search(value.length ? encode_plus(JSON.stringify(value)) : '').draw();
  386. return;
  387. }
  388. // turn off filter if nothing selected
  389. $td.data('filter', value.length > 0);
  390. table.draw(); // redraw table, and filters will be applied
  391. }
  392. });
  393. if (searchCol) filter[0].selectize.setValue(JSON.parse(searchCol));
  394. filter[0].selectize.on('blur', function() {
  395. $x.hide().trigger('hide'); $input.parent().show(); $input.trigger('blur');
  396. });
  397. filter.next('div').css('margin-bottom', 'auto');
  398. } else if (type === 'character') {
  399. var fun = function() {
  400. searchColumn(i, $input.val()).draw();
  401. };
  402. if (server) {
  403. fun = $.fn.dataTable.util.throttle(fun, options.searchDelay);
  404. }
  405. $input.on('input', fun);
  406. } else if (inArray(type, ['number', 'integer', 'date', 'time'])) {
  407. var $x0 = $x;
  408. $x = $x0.children('div').first();
  409. $x0.css({
  410. 'background-color': '#fff',
  411. 'border': '1px #ddd solid',
  412. 'border-radius': '4px',
  413. 'padding': '20px 20px 10px 20px'
  414. });
  415. var $spans = $x0.children('span').css({
  416. 'margin-top': '10px',
  417. 'white-space': 'nowrap'
  418. });
  419. var $span1 = $spans.first(), $span2 = $spans.last();
  420. var r1 = +$x.data('min'), r2 = +$x.data('max');
  421. // when the numbers are too small or have many decimal places, the
  422. // slider may have numeric precision problems (#150)
  423. var scale = Math.pow(10, Math.max(0, +$x.data('scale') || 0));
  424. r1 = Math.round(r1 * scale); r2 = Math.round(r2 * scale);
  425. var scaleBack = function(x, scale) {
  426. if (scale === 1) return x;
  427. var d = Math.round(Math.log(scale) / Math.log(10));
  428. // to avoid problems like 3.423/100 -> 0.034230000000000003
  429. return (x / scale).toFixed(d);
  430. };
  431. $input.on({
  432. focus: function() {
  433. $x0.show().trigger('show');
  434. // first, make sure the slider div leaves at least 20px between
  435. // the two (slider value) span's
  436. $x0.width(Math.max(160, $span1.outerWidth() + $span2.outerWidth() + 20));
  437. // then, if the input is really wide, make the slider the same
  438. // width as the input
  439. if ($x0.outerWidth() < $input.outerWidth()) {
  440. $x0.outerWidth($input.outerWidth());
  441. }
  442. // make sure the slider div does not reach beyond the right margin
  443. if ($(window).width() < $x0.offset().left + $x0.width()) {
  444. $x0.offset({
  445. 'left': $input.offset().left + $input.outerWidth() - $x0.outerWidth()
  446. });
  447. }
  448. },
  449. blur: function() {
  450. $x0.hide().trigger('hide');
  451. },
  452. input: function() {
  453. if ($input.val() === '') filter.val([r1, r2]);
  454. },
  455. change: function() {
  456. var v = $input.val().replace(/\s/g, '');
  457. if (v === '') return;
  458. v = v.split('...');
  459. if (v.length !== 2) {
  460. $input.parent().addClass('has-error');
  461. return;
  462. }
  463. if (v[0] === '') v[0] = r1;
  464. if (v[1] === '') v[1] = r2;
  465. $input.parent().removeClass('has-error');
  466. // treat date as UTC time at midnight
  467. var strTime = function(x) {
  468. var s = type === 'date' ? 'T00:00:00Z' : '';
  469. var t = new Date(x + s).getTime();
  470. // add 10 minutes to date since it does not hurt the date, and
  471. // it helps avoid the tricky floating point arithmetic problems,
  472. // e.g. sometimes the date may be a few milliseconds earlier
  473. // than the midnight due to precision problems in noUiSlider
  474. return type === 'date' ? t + 3600000 : t;
  475. };
  476. if (inArray(type, ['date', 'time'])) {
  477. v[0] = strTime(v[0]);
  478. v[1] = strTime(v[1]);
  479. }
  480. if (v[0] != r1) v[0] *= scale;
  481. if (v[1] != r2) v[1] *= scale;
  482. filter.val(v);
  483. }
  484. });
  485. var formatDate = function(d, isoFmt) {
  486. d = scaleBack(d, scale);
  487. if (type === 'number') return d;
  488. if (type === 'integer') return parseInt(d);
  489. var x = new Date(+d);
  490. var fmt = ('filterDateFmt' in data) ? data.filterDateFmt[i] : undefined;
  491. if (fmt !== undefined && isoFmt === false) return x[fmt.method].apply(x, fmt.params);
  492. if (type === 'date') {
  493. var pad0 = function(x) {
  494. return ('0' + x).substr(-2, 2);
  495. };
  496. return x.getUTCFullYear() + '-' + pad0(1 + x.getUTCMonth())
  497. + '-' + pad0(x.getUTCDate());
  498. } else {
  499. return x.toISOString();
  500. }
  501. };
  502. var opts = type === 'date' ? { step: 60 * 60 * 1000 } :
  503. type === 'integer' ? { step: 1 } : {};
  504. filter = $x.noUiSlider($.extend({
  505. start: [r1, r2],
  506. range: {min: r1, max: r2},
  507. connect: true
  508. }, opts));
  509. if (scale > 1) (function() {
  510. var t1 = r1, t2 = r2;
  511. var val = filter.val();
  512. while (val[0] > r1 || val[1] < r2) {
  513. if (val[0] > r1) {
  514. t1 -= val[0] - r1;
  515. }
  516. if (val[1] < r2) {
  517. t2 += r2 - val[1];
  518. }
  519. filter = $x.noUiSlider($.extend({
  520. start: [t1, t2],
  521. range: {min: t1, max: t2},
  522. connect: true
  523. }, opts), true);
  524. val = filter.val();
  525. }
  526. r1 = t1; r2 = t2;
  527. })();
  528. var updateSliderText = function(v1, v2) {
  529. $span1.text(formatDate(v1, false)); $span2.text(formatDate(v2, false));
  530. };
  531. updateSliderText(r1, r2);
  532. var updateSlider = function(e) {
  533. var val = filter.val();
  534. // turn off filter if in full range
  535. $td.data('filter', val[0] > r1 || val[1] < r2);
  536. var v1 = formatDate(val[0]), v2 = formatDate(val[1]), ival;
  537. if ($td.data('filter')) {
  538. ival = v1 + ' ... ' + v2;
  539. $input.attr('title', ival).val(ival).trigger('input');
  540. } else {
  541. $input.attr('title', '').val('');
  542. }
  543. updateSliderText(val[0], val[1]);
  544. if (e.type === 'slide') return; // no searching when sliding only
  545. if (server) {
  546. table.column(i).search($td.data('filter') ? ival : '').draw();
  547. return;
  548. }
  549. table.draw();
  550. };
  551. filter.on({
  552. set: updateSlider,
  553. slide: updateSlider
  554. });
  555. }
  556. // server-side processing will be handled by R (or whatever server
  557. // language you use); the following code is only needed for client-side
  558. // processing
  559. if (server) {
  560. // if a search string has been pre-set, search now
  561. if (searchCol) searchColumn(i, searchCol).draw();
  562. return;
  563. }
  564. var customFilter = function(settings, data, dataIndex) {
  565. // there is no way to attach a search function to a specific table,
  566. // and we need to make sure a global search function is not applied to
  567. // all tables (i.e. a range filter in a previous table should not be
  568. // applied to the current table); we use the settings object to
  569. // determine if we want to perform searching on the current table,
  570. // since settings.sTableId will be different to different tables
  571. if (table.settings()[0] !== settings) return true;
  572. // no filter on this column or no need to filter this column
  573. if (typeof filter === 'undefined' || !$td.data('filter')) return true;
  574. var r = filter.val(), v, r0, r1;
  575. var i_data = function(i) {
  576. if (!colReorderEnabled()) return i;
  577. var order = table.colReorder.order(), k;
  578. for (k = 0; k < order.length; ++k) if (order[k] === i) return k;
  579. return i; // in theory it will never be here...
  580. }
  581. v = data[i_data(i)];
  582. if (type === 'number' || type === 'integer') {
  583. v = parseFloat(v);
  584. // how to handle NaN? currently exclude these rows
  585. if (isNaN(v)) return(false);
  586. r0 = parseFloat(scaleBack(r[0], scale))
  587. r1 = parseFloat(scaleBack(r[1], scale));
  588. if (v >= r0 && v <= r1) return true;
  589. } else if (type === 'date' || type === 'time') {
  590. v = new Date(v);
  591. r0 = new Date(r[0] / scale); r1 = new Date(r[1] / scale);
  592. if (v >= r0 && v <= r1) return true;
  593. } else if (type === 'factor') {
  594. if (r.length === 0 || inArray(v, r)) return true;
  595. } else if (type === 'logical') {
  596. if (r.length === 0) return true;
  597. if (inArray(v === '' ? 'na' : v, r)) return true;
  598. }
  599. return false;
  600. };
  601. $.fn.dataTable.ext.search.push(customFilter);
  602. // search for the preset search strings if it is non-empty
  603. if (searchCol) {
  604. if (inArray(type, ['factor', 'logical'])) {
  605. filter[0].selectize.setValue(JSON.parse(searchCol));
  606. } else if (type === 'character') {
  607. $input.trigger('input');
  608. } else if (inArray(type, ['number', 'integer', 'date', 'time'])) {
  609. $input.trigger('change');
  610. }
  611. }
  612. });
  613. }
  614. // highlight search keywords
  615. var highlight = function() {
  616. var body = $(table.table().body());
  617. // removing the old highlighting first
  618. body.unhighlight();
  619. // don't highlight the "not found" row, so we get the rows using the api
  620. if (table.rows({ filter: 'applied' }).data().length === 0) return;
  621. // highlight gloal search keywords
  622. body.highlight($.trim(table.search()).split(/\s+/));
  623. // then highlight keywords from individual column filters
  624. if (filterRow) filterRow.each(function(i, td) {
  625. var $td = $(td), type = $td.data('type');
  626. if (type !== 'character') return;
  627. var $input = $td.children('div').first().children('input');
  628. var column = table.column(i).nodes().to$(),
  629. val = $.trim($input.val());
  630. if (type !== 'character' || val === '') return;
  631. column.highlight(val.split(/\s+/));
  632. });
  633. };
  634. if (options.searchHighlight) {
  635. table
  636. .on('draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth', highlight)
  637. .on('destroy', function() {
  638. // remove event handler
  639. table.off('draw.dt.dth column-visibility.dt.dth column-reorder.dt.dth');
  640. });
  641. // initial highlight for state saved conditions and initial states
  642. highlight();
  643. }
  644. // run the callback function on the table instance
  645. if (typeof data.callback === 'function') data.callback(table);
  646. // double click to edit the cell
  647. if (data.editable) table.on('dblclick.dt', 'tbody td', function() {
  648. var $input = $('<input type="text">');
  649. var $this = $(this), value = table.cell(this).data(), html = $this.html();
  650. var changed = false;
  651. $input.val(value);
  652. $this.empty().append($input);
  653. $input.css('width', '100%').focus().on('change', function() {
  654. changed = true;
  655. var valueNew = $input.val();
  656. if (valueNew != value) {
  657. table.cell($this).data(valueNew);
  658. if (HTMLWidgets.shinyMode) changeInput('cell_edit', cellInfo($this));
  659. // for server-side processing, users have to call replaceData() to update the table
  660. if (!server) table.draw(false);
  661. } else {
  662. $this.html(html);
  663. }
  664. $input.remove();
  665. }).on('blur', function() {
  666. if (!changed) $input.trigger('change');
  667. });
  668. });
  669. // interaction with shiny
  670. if (!HTMLWidgets.shinyMode && !crosstalkOptions.group) return;
  671. var methods = {};
  672. var shinyData = {};
  673. methods.updateCaption = function(caption) {
  674. if (!caption) return;
  675. $table.children('caption').replaceWith(caption);
  676. }
  677. // register clear functions to remove input values when the table is removed
  678. instance.clearInputs = {};
  679. var changeInput = function(id, value, type, noCrosstalk) {
  680. var event = id;
  681. id = el.id + '_' + id;
  682. if (type) id = id + ':' + type;
  683. // do not update if the new value is the same as old value
  684. if (shinyData.hasOwnProperty(id) && shinyData[id] === JSON.stringify(value))
  685. return;
  686. shinyData[id] = JSON.stringify(value);
  687. if (HTMLWidgets.shinyMode) {
  688. Shiny.onInputChange(id, value);
  689. if (!instance.clearInputs[id]) instance.clearInputs[id] = function() {
  690. Shiny.onInputChange(id, null);
  691. }
  692. }
  693. // HACK
  694. if (event === "rows_selected" && !noCrosstalk) {
  695. if (crosstalkOptions.group) {
  696. var keys = crosstalkOptions.key;
  697. var selectedKeys = null;
  698. if (value) {
  699. selectedKeys = [];
  700. for (var i = 0; i < value.length; i++) {
  701. // The value array's contents use 1-based row numbers, so we must
  702. // convert to 0-based before indexing into the keys array.
  703. selectedKeys.push(keys[value[i] - 1]);
  704. }
  705. }
  706. instance.ctselectHandle.set(selectedKeys);
  707. }
  708. }
  709. };
  710. var addOne = function(x) {
  711. return x.map(function(i) { return 1 + i; });
  712. };
  713. var unique = function(x) {
  714. var ux = [];
  715. $.each(x, function(i, el){
  716. if ($.inArray(el, ux) === -1) ux.push(el);
  717. });
  718. return ux;
  719. }
  720. // change the row index of a cell
  721. var tweakCellIndex = function(cell) {
  722. var info = cell.index();
  723. if (server) {
  724. info.row = DT_rows_current[info.row];
  725. } else {
  726. info.row += 1;
  727. }
  728. return {row: info.row, col: info.column};
  729. }
  730. var selMode = data.selection.mode, selTarget = data.selection.target;
  731. if (inArray(selMode, ['single', 'multiple'])) {
  732. var selClass = data.style === 'bootstrap' ? 'active' : 'selected';
  733. var selected = data.selection.selected, selected1, selected2;
  734. // selected1: row indices; selected2: column indices
  735. if (selected === null) {
  736. selected1 = selected2 = [];
  737. } else if (selTarget === 'row') {
  738. selected1 = $.makeArray(selected);
  739. } else if (selTarget === 'column') {
  740. selected2 = $.makeArray(selected);
  741. } else if (selTarget === 'row+column') {
  742. selected1 = $.makeArray(selected.rows);
  743. selected2 = $.makeArray(selected.cols);
  744. }
  745. // After users reorder the rows or filter the table, we cannot use the table index
  746. // directly. Instead, we need this function to find out the rows between the two clicks.
  747. // If user filter the table again between the start click and the end click, the behavior
  748. // would be undefined, but it should not be a problem.
  749. var shiftSelRowsIndex = function(start, end) {
  750. var indexes = server ? DT_rows_all : table.rows({ search: 'applied' }).indexes().toArray();
  751. start = indexes.indexOf(start); end = indexes.indexOf(end);
  752. // if start is larger than end, we need to swap
  753. if (start > end) {
  754. var tmp = end; end = start; start = tmp;
  755. }
  756. return indexes.slice(start, end + 1);
  757. }
  758. var serverRowIndex = function(clientRowIndex) {
  759. return server ? DT_rows_current[clientRowIndex] : clientRowIndex + 1;
  760. }
  761. // row, column, or cell selection
  762. var lastClickedRow;
  763. if (inArray(selTarget, ['row', 'row+column'])) {
  764. var selectedRows = function() {
  765. var rows = table.rows('.' + selClass);
  766. var idx = rows.indexes().toArray();
  767. if (!server) return addOne(idx);
  768. idx = idx.map(function(i) {
  769. return DT_rows_current[i];
  770. });
  771. selected1 = selMode === 'multiple' ? unique(selected1.concat(idx)) : idx;
  772. return selected1;
  773. }
  774. table.on('mousedown.dt', 'tbody tr', function(e) {
  775. var $this = $(this), thisRow = table.row(this);
  776. if (selMode === 'multiple') {
  777. if (e.shiftKey && lastClickedRow !== undefined) {
  778. // select or de-select depends on the last clicked row's status
  779. var flagSel = !$this.hasClass(selClass);
  780. var crtClickedRow = serverRowIndex(thisRow.index());
  781. if (server) {
  782. var rowsIndex = shiftSelRowsIndex(lastClickedRow, crtClickedRow);
  783. // update current page's selClass
  784. rowsIndex.map(function(i) {
  785. var rowIndex = DT_rows_current.indexOf(i);
  786. if (rowIndex >= 0) {
  787. var row = table.row(rowIndex).nodes().to$();
  788. var flagRowSel = !row.hasClass(selClass);
  789. if (flagSel === flagRowSel) row.toggleClass(selClass);
  790. }
  791. });
  792. // update selected1
  793. if (flagSel) {
  794. selected1 = unique(selected1.concat(rowsIndex));
  795. } else {
  796. selected1 = selected1.filter(function(index) {
  797. return !inArray(index, rowsIndex);
  798. });
  799. }
  800. } else {
  801. // js starts from 0
  802. shiftSelRowsIndex(lastClickedRow - 1, crtClickedRow - 1).map(function(value) {
  803. var row = table.row(value).nodes().to$();
  804. var flagRowSel = !row.hasClass(selClass);
  805. if (flagSel === flagRowSel) row.toggleClass(selClass);
  806. });
  807. }
  808. e.preventDefault();
  809. } else {
  810. $this.toggleClass(selClass);
  811. }
  812. } else {
  813. if ($this.hasClass(selClass)) {
  814. $this.removeClass(selClass);
  815. } else {
  816. table.$('tr.' + selClass).removeClass(selClass);
  817. $this.addClass(selClass);
  818. }
  819. }
  820. if (server && !$this.hasClass(selClass)) {
  821. var id = DT_rows_current[thisRow.index()];
  822. // remove id from selected1 since its class .selected has been removed
  823. if (inArray(id, selected1)) selected1.splice($.inArray(id, selected1), 1);
  824. }
  825. changeInput('rows_selected', selectedRows());
  826. changeInput('row_last_clicked', serverRowIndex(thisRow.index()));
  827. lastClickedRow = serverRowIndex(thisRow.index());
  828. });
  829. changeInput('rows_selected', selected1);
  830. var selectRows = function() {
  831. table.$('tr.' + selClass).removeClass(selClass);
  832. if (selected1.length === 0) return;
  833. if (server) {
  834. table.rows({page: 'current'}).every(function() {
  835. if (inArray(DT_rows_current[this.index()], selected1)) {
  836. $(this.node()).addClass(selClass);
  837. }
  838. });
  839. } else {
  840. var selected0 = selected1.map(function(i) { return i - 1; });
  841. $(table.rows(selected0).nodes()).addClass(selClass);
  842. }
  843. }
  844. selectRows(); // in case users have specified pre-selected rows
  845. // restore selected rows after the table is redrawn (e.g. sort/search/page);
  846. // client-side tables will preserve the selections automatically; for
  847. // server-side tables, we have to *real* row indices are in `selected1`
  848. if (server) table.on('draw.dt', selectRows);
  849. methods.selectRows = function(selected) {
  850. selected1 = $.makeArray(selected);
  851. selectRows();
  852. changeInput('rows_selected', selected1);
  853. }
  854. }
  855. if (inArray(selTarget, ['column', 'row+column'])) {
  856. if (selTarget === 'row+column') {
  857. $(table.columns().footer()).css('cursor', 'pointer');
  858. }
  859. table.on('click.dt', selTarget === 'column' ? 'tbody td' : 'tfoot tr th', function() {
  860. var colIdx = selTarget === 'column' ? table.cell(this).index().column :
  861. $.inArray(this, table.columns().footer()),
  862. thisCol = $(table.column(colIdx).nodes());
  863. if (colIdx === -1) return;
  864. if (thisCol.hasClass(selClass)) {
  865. thisCol.removeClass(selClass);
  866. selected2.splice($.inArray(colIdx, selected2), 1);
  867. } else {
  868. if (selMode === 'single') $(table.cells().nodes()).removeClass(selClass);
  869. thisCol.addClass(selClass);
  870. selected2 = selMode === 'single' ? [colIdx] : unique(selected2.concat([colIdx]));
  871. }
  872. changeInput('columns_selected', selected2);
  873. });
  874. changeInput('columns_selected', selected2);
  875. var selectCols = function() {
  876. table.columns().nodes().flatten().to$().removeClass(selClass);
  877. if (selected2.length > 0)
  878. table.columns(selected2).nodes().flatten().to$().addClass(selClass);
  879. }
  880. selectCols(); // in case users have specified pre-selected columns
  881. if (server) table.on('draw.dt', selectCols);
  882. methods.selectColumns = function(selected) {
  883. selected2 = $.makeArray(selected);
  884. selectCols();
  885. changeInput('columns_selected', selected2);
  886. }
  887. }
  888. if (selTarget === 'cell') {
  889. var selected3;
  890. if (selected === null) {
  891. selected3 = [];
  892. } else {
  893. selected3 = selected;
  894. }
  895. var findIndex = function(ij) {
  896. for (var i = 0; i < selected3.length; i++) {
  897. if (ij[0] === selected3[i][0] && ij[1] === selected3[i][1]) return i;
  898. }
  899. return -1;
  900. }
  901. table.on('click.dt', 'tbody td', function() {
  902. var $this = $(this), info = tweakCellIndex(table.cell(this));
  903. if ($this.hasClass(selClass)) {
  904. $this.removeClass(selClass);
  905. selected3.splice(findIndex([info.row, info.col]), 1);
  906. } else {
  907. if (selMode === 'single') $(table.cells().nodes()).removeClass(selClass);
  908. $this.addClass(selClass);
  909. selected3 = selMode === 'single' ? [[info.row, info.col]] :
  910. unique(selected3.concat([[info.row, info.col]]));
  911. }
  912. changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix');
  913. });
  914. changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix');
  915. var selectCells = function() {
  916. table.$('td.' + selClass).removeClass(selClass);
  917. if (selected3.length === 0) return;
  918. if (server) {
  919. table.cells({page: 'current'}).every(function() {
  920. var info = tweakCellIndex(this);
  921. if (findIndex([info.row, info.col], selected3) > -1)
  922. $(this.node()).addClass(selClass);
  923. });
  924. } else {
  925. selected3.map(function(ij) {
  926. $(table.cell(ij[0] - 1, ij[1]).node()).addClass(selClass);
  927. });
  928. }
  929. };
  930. selectCells(); // in case users have specified pre-selected columns
  931. if (server) table.on('draw.dt', selectCells);
  932. methods.selectCells = function(selected) {
  933. selected3 = selected ? selected : [];
  934. selectCells();
  935. changeInput('cells_selected', transposeArray2D(selected3), 'shiny.matrix');
  936. }
  937. }
  938. }
  939. // expose some table info to Shiny
  940. var updateTableInfo = function(e, settings) {
  941. // TODO: is anyone interested in the page info?
  942. // changeInput('page_info', table.page.info());
  943. var updateRowInfo = function(id, modifier) {
  944. var idx;
  945. if (server) {
  946. idx = modifier.page === 'current' ? DT_rows_current : DT_rows_all;
  947. } else {
  948. var rows = table.rows($.extend({
  949. search: 'applied',
  950. page: 'all'
  951. }, modifier));
  952. idx = addOne(rows.indexes().toArray());
  953. }
  954. changeInput('rows' + '_' + id, idx);
  955. };
  956. updateRowInfo('current', {page: 'current'});
  957. updateRowInfo('all', {});
  958. }
  959. table.on('draw.dt', updateTableInfo);
  960. updateTableInfo();
  961. // state info
  962. table.on('draw.dt column-visibility.dt', function() {
  963. changeInput('state', table.state());
  964. });
  965. changeInput('state', table.state());
  966. // search info
  967. var updateSearchInfo = function() {
  968. changeInput('search', table.search());
  969. if (filterRow) changeInput('search_columns', filterRow.toArray().map(function(td) {
  970. return $(td).find('input').first().val();
  971. }));
  972. }
  973. table.on('draw.dt', updateSearchInfo);
  974. updateSearchInfo();
  975. var cellInfo = function(thiz) {
  976. var info = tweakCellIndex(table.cell(thiz));
  977. info.value = table.cell(thiz).data();
  978. return info;
  979. }
  980. // the current cell clicked on
  981. table.on('click.dt', 'tbody td', function() {
  982. changeInput('cell_clicked', cellInfo(this));
  983. })
  984. changeInput('cell_clicked', {});
  985. // do not trigger table selection when clicking on links unless they have classes
  986. table.on('click.dt', 'tbody td a', function(e) {
  987. if (this.className === '') e.stopPropagation();
  988. });
  989. methods.addRow = function(data, rowname) {
  990. var data0 = table.row(0).data(), n = data0.length, d = n - data.length;
  991. if (d === 1) {
  992. data = rowname.concat(data)
  993. } else if (d !== 0) {
  994. console.log(data);
  995. console.log(data0);
  996. throw 'New data must be of the same length as current data (' + n + ')';
  997. };
  998. table.row.add(data).draw();
  999. }
  1000. methods.updateSearch = function(keywords) {
  1001. if (keywords.global !== null)
  1002. $(table.table().container()).find('input[type=search]').first()
  1003. .val(keywords.global).trigger('input');
  1004. var columns = keywords.columns;
  1005. if (!filterRow || columns === null) return;
  1006. filterRow.toArray().map(function(td, i) {
  1007. var v = typeof columns === 'string' ? columns : columns[i];
  1008. if (typeof v === 'undefined') {
  1009. console.log('The search keyword for column ' + i + ' is undefined')
  1010. return;
  1011. }
  1012. $(td).find('input').first().val(v);
  1013. searchColumn(i, v);
  1014. });
  1015. table.draw();
  1016. }
  1017. methods.hideCols = function(hide, reset) {
  1018. if (reset) table.columns().visible(true, false);
  1019. table.columns(hide).visible(false);
  1020. }
  1021. methods.showCols = function(show, reset) {
  1022. if (reset) table.columns().visible(false, false);
  1023. table.columns(show).visible(true);
  1024. }
  1025. methods.colReorder = function(order, origOrder) {
  1026. table.colReorder.order(order, origOrder);
  1027. }
  1028. methods.selectPage = function(page) {
  1029. if (table.page.info().pages < page || page < 1) {
  1030. throw 'Selected page is out of range';
  1031. };
  1032. table.page(page - 1).draw(false);
  1033. }
  1034. methods.reloadData = function(resetPaging, clearSelection) {
  1035. // empty selections first if necessary
  1036. if (methods.selectRows && inArray('row', clearSelection)) methods.selectRows([]);
  1037. if (methods.selectColumns && inArray('column', clearSelection)) methods.selectColumns([]);
  1038. if (methods.selectCells && inArray('cell', clearSelection)) methods.selectCells([]);
  1039. table.ajax.reload(null, resetPaging);
  1040. }
  1041. table.shinyMethods = methods;
  1042. },
  1043. resize: function(el, width, height, instance) {
  1044. if (instance.data) this.renderValue(el, instance.data, instance);
  1045. // dynamically adjust height if fillContainer = TRUE
  1046. if (instance.fillContainer)
  1047. this.fillAvailableHeight(el, height);
  1048. this.adjustWidth(el);
  1049. },
  1050. // dynamically set the scroll body to fill available height
  1051. // (used with fillContainer = TRUE)
  1052. fillAvailableHeight: function(el, availableHeight) {
  1053. // see how much of the table is occupied by header/footer elements
  1054. // and use that to compute a target scroll body height
  1055. var dtWrapper = $(el).find('div.dataTables_wrapper');
  1056. var dtScrollBody = $(el).find($('div.dataTables_scrollBody'));
  1057. var framingHeight = dtWrapper.innerHeight() - dtScrollBody.innerHeight();
  1058. var scrollBodyHeight = availableHeight - framingHeight;
  1059. // set the height
  1060. dtScrollBody.height(scrollBodyHeight + 'px');
  1061. },
  1062. // adjust the width of columns; remove the hard-coded widths on table and the
  1063. // scroll header when scrollX/Y are enabled
  1064. adjustWidth: function(el) {
  1065. var $el = $(el), table = $el.data('datatable');
  1066. if (table) table.columns.adjust();
  1067. $el.find('.dataTables_scrollHeadInner').css('width', '')
  1068. .children('table').css('margin-left', '');
  1069. }
  1070. });
  1071. if (!HTMLWidgets.shinyMode) return;
  1072. Shiny.addCustomMessageHandler('datatable-calls', function(data) {
  1073. var id = data.id;
  1074. var el = document.getElementById(id);
  1075. var table = el ? $(el).data('datatable') : null;
  1076. if (!table) {
  1077. console.log("Couldn't find table with id " + id);
  1078. return;
  1079. }
  1080. var methods = table.shinyMethods, call = data.call;
  1081. if (methods[call.method]) {
  1082. methods[call.method].apply(table, call.args);
  1083. } else {
  1084. console.log("Unknown method " + call.method);
  1085. }
  1086. });
  1087. })();