Skip to content

Commit

Permalink
an enhanced data editor: now you can edit a whole row, column, or all…
Browse files Browse the repository at this point in the history
… cells (close #493)
  • Loading branch information
yihui committed Apr 4, 2019
1 parent 1613e50 commit 3bf27fe
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 25 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## NEW FEATURES

- The table editor has been enhanced: now the `editable` argument of `datatable()` can take four possible values, `cell` (or `TRUE` for backward compatibility), `row`, `column`, or `all`, which means you can edit a single cell a time, or a whole row or column, or all cells in the table. To trigger the editor, doubleclick on any cell. To submit the edit, hit `Ctrl + Enter` when multiple cells are being edited. See https://github.com/rstudio/DT/tree/master/inst/examples/DT-edit for comprehensive examples (thanks, @LukasK13 #509 and @mgirlich #493).

- Added a `funcFilter` argument to `DT::renderDT()` (thanks, @galachad, #638).

## BUG FIXES
Expand Down
12 changes: 9 additions & 3 deletions R/datatables.R
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,16 @@
#' @param plugins a character vector of the names of DataTables plug-ins
#' (\url{https://rstudio.github.io/DT/plugins.html}). Note that only those
#' plugins supported by the \code{DT} package can be used here.
#' @param editable \code{TRUE} to enable table editor.
#' @param editable \code{FALSE} to disable the table editor, or \code{TRUE} (or
#' \code{"cell"}) to enable editing a single cell. Alternatively, you can set
#' it to \code{"row"} to be able to edit a row, or \code{"column"} to edit a
#' column, or \code{"all"} to edit all cells on the current page of the table.
#' In all modes, start editing by doubleclicking on a cell.
#' @note You are recommended to escape the table content for security reasons
#' (e.g. XSS attacks) when using this function in Shiny or any other dynamic
#' web applications.
#' @references See \url{https://rstudio.github.io/DT} for the full documentation.
#' @references See \url{https://rstudio.github.io/DT} for the full
#' documentation.
#' @importFrom htmltools tags htmlDependency
#' @export
#' @example inst/examples/datatable.R
Expand Down Expand Up @@ -202,7 +207,8 @@ datatable = function(

params$caption = captionString(caption)

if (editable) params$editable = editable
if (isTRUE(editable)) editable = 'cell'
if (is.character(editable)) params$editable = editable

if (!identical(class(callback), class(JS(''))))
stop("The 'callback' argument only accept a value returned from JS()")
Expand Down
6 changes: 6 additions & 0 deletions R/shiny.R
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ renderDataTable = function(
})
}

shiny::registerInputHandler('DT.cellInfo', function(val, ...) {
opts = options(stringsAsFactors = FALSE); on.exit(options(opts), add = TRUE)
val = lapply(val, as.data.frame)
do.call(rbind, val)
}, TRUE)

func
}

Expand Down
104 changes: 82 additions & 22 deletions inst/htmlwidgets/datatables.js
Original file line number Diff line number Diff line change
Expand Up @@ -709,28 +709,88 @@ HTMLWidgets.widget({
// run the callback function on the table instance
if (typeof data.callback === 'function') data.callback(table);

// double click to edit the cell
if (data.editable) table.on('dblclick.dt', 'tbody td', function() {
var $input = $('<input type="text">');
var $this = $(this), value = table.cell(this).data(), html = $this.html();
var changed = false;
$input.val(value);
$this.empty().append($input);
$input.css('width', '100%').focus().on('change', function() {
changed = true;
var valueNew = $input.val();
if (valueNew != value) {
table.cell($this).data(valueNew);
if (HTMLWidgets.shinyMode) changeInput('cell_edit', cellInfo($this), null, null, {priority: "event"});
// for server-side processing, users have to call replaceData() to update the table
if (!server) table.draw(false);
} else {
$this.html(html);
}
$input.remove();
}).on('blur', function() {
if (!changed) $input.trigger('change');
});
// double click to edit the cell, row, column, or all cells
if (data.editable) table.on('dblclick.dt', 'tbody td', function(e) {
// only bring up the editor when the cell itself is dbclicked, and ignore
// other dbclick events bubbled up (e.g. from the <input>)
if (e.target !== this) return;
var target = [], immediate = false;
switch (data.editable) {
case 'cell':
target = [this];
immediate = true; // edit will take effect immediately
break;
case 'row':
target = table.cells(table.cell(this).index().row, '*').nodes();
break;
case 'column':
target = table.cells('*', table.cell(this).index().column).nodes();
break;
case 'all':
target = table.cells().nodes();
break;
default:
throw 'The editable parameter must be "cell", "row", "column", or "all"';
}
for (var i = 0; i < target.length; i++) {
(function(cell, current) {
var $cell = $(cell), html = $cell.html();
var _cell = table.cell(cell), value = _cell.data();
var $input = $('<input type="text">'), changed = false;
if (!immediate) $cell.data('input', $input).data('html', html);
$input.val(value);
$cell.empty().append($input);
if (cell === current) $input.focus();
$input.css('width', '100%');

if (immediate) $input.on('change', function() {
changed = true;
var valueNew = $input.val();
if (valueNew != value) {
_cell.data(valueNew);
if (HTMLWidgets.shinyMode) {
changeInput('cell_edit', [cellInfo(cell)], 'DT.cellInfo', null, {priority: "event"});
}
// for server-side processing, users have to call replaceData() to update the table
if (!server) table.draw(false);
} else {
$cell.html(html);
}
$input.remove();
}).on('blur', function() {
if (!changed) $input.trigger('change');
}).on('keyup', function(e) {
// hit Escape to cancel editing
if (e.keyCode === 27) $input.trigger('blur');
});

// bulk edit (row, column, or all)
if (!immediate) $input.on('keyup', function(e) {
var removeInput = function($cell, restore) {
$cell.data('input').remove();
if (restore) $cell.html($cell.data('html'));
}
if (e.keyCode === 27) {
for (var i = 0; i < target.length; i++) {
removeInput($(target[i]), true);
}
} else if (e.keyCode === 13 && e.ctrlKey) {
// Ctrl + Enter
var cell, $cell, _cell, cellData = [];
for (var i = 0; i < target.length; i++) {
cell = target[i]; $cell = $(cell); _cell = table.cell(cell);
_cell.data($cell.data('input').val());
cellData.push(cellInfo(cell));
removeInput($cell, false);
}
if (HTMLWidgets.shinyMode) {
changeInput('cell_edit', cellData, 'DT.cellInfo', null, {priority: "event"});
}
if (!server) table.draw(false);
}
});
})(target[i], this);
}
});

// interaction with shiny
Expand Down

0 comments on commit 3bf27fe

Please sign in to comment.