You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为集成Rhandsontable的Shiny应用实现逐次撤销按钮?

实现Rhandsontable的逐次撤销功能

需求说明

现有Shiny集成Rhandsontable的应用,已有一个一次性撤销所有更改的按钮,需要新增一个支持逐次撤销的按钮——每次点击仅恢复最近的一项更改,多次点击逐步还原全部修改。

解决方案代码

library(shiny)
library(rhandsontable)

change_hook <- "function(el,x) {
var hot = this.hot;  
// 维护变更历史栈,存储每次修改的完整信息:行、列、旧值、新值
var changeHistory = [];

var changefn = function(changes, source) { 
  if (source === 'edit' || source === 'autofill' || source === 'paste') {
    // 遍历所有变更(处理批量修改如自动填充、粘贴)
    changes.forEach(function(change) {
      var row = change[0];
      var col = change[1];
      var oldval = change[2];
      var newval = change[3];
      
      if (oldval !== newval) {
        // 记录变更到历史栈
        changeHistory.push({
          'rowid': row, 
          'colid': col, 
          'oldval': oldval, 
          'newval': newval
        });
        // 标记修改过的单元格
        var cell = hot.getCell(row, col);
        cell.style.background = 'cyan';
      }
    });
  }
  // 处理undo来源的变更(逐次撤销时触发,从历史栈移除对应记录)
  else if (source === 'undo') {
    changeHistory.pop();
  }
}

var renderfn = function(isForced) {
  // 重新高亮所有未撤销的修改单元格
  changeHistory.forEach(function(item) {
    var cell = hot.getCell(item.rowid, item.colid);
    cell.style.background = 'cyan';
  });
}

var loadfn = function(initialLoad) {
  // 一次性撤销时清空历史栈并还原单元格样式
  changeHistory.forEach(function(item) {
    var cell = hot.getCell(item.rowid, item.colid);
    cell.style.background = 'white';
  });
  changeHistory = [];
}

hot.addHook('afterChange', changefn);
hot.addHook('afterRender', renderfn);
hot.addHook('afterLoadData', loadfn);

// 暴露历史栈给Shiny,供后端调用逐次撤销逻辑
window.hotChangeHistory = changeHistory;
}  "

ui <- div(
  actionButton(inputId = "undo_all_button", label = "一次性撤销全部"),
  actionButton(inputId = "undo_step_button", label = "逐次撤销最近一项"),
  rHandsontableOutput(outputId="mtcars")
)

server <- function(input, output, session) {
  # 用于触发一次性撤销的信号
  undo_all <- reactiveVal(0)
  # 用于触发逐次撤销的信号
  undo_step <- reactiveVal(0)
  
  output$mtcars <- renderRHandsontable({
    # 处理一次性撤销
    if(undo_all() == 1){
      rht <- rhandsontable(mtcars, reset = 1, stretchH="all", height=300)
      undo_all(0)
    } 
    # 处理逐次撤销
    else if(undo_step() == 1){
      current_data <- hot_to_r(input$mtcars)
      # 调用前端的历史栈获取最近一次变更
      change <- session$sendCustomMessage(
        type = "getLatestChange",
        message = NULL,
        callback = function(msg) {
          if(!is.null(msg)){
            current_data[msg$rowid + 1, msg$colid + 1] <- msg$oldval
          }
        }
      )
      # 等待回调完成后渲染表格
      Sys.sleep(0.1)
      rht <- rhandsontable(current_data, stretchH="all", height=300)
      undo_step(0)
    }
    # 初始渲染或无撤销操作时
    else {
      rht <- rhandsontable(mtcars, stretchH="all", height=300)
    }
    htmlwidgets::onRender(rht, change_hook)
  })
  
  # 一次性撤销按钮逻辑
  observeEvent(input$undo_all_button, {
    undo_all(1)
  })
  
  # 逐次撤销按钮逻辑
  observeEvent(input$undo_step_button, {
    undo_step(1)
    # 触发前端执行undo操作,同步更新历史栈
    session$sendCustomMessage(type = "triggerUndo", message = NULL)
  })
  
  # 注册前端消息监听,返回最近一次变更
  session$onMessage("getLatestChange", function(msg, response) {
    if(length(window.hotChangeHistory) > 0){
      response(window.hotChangeHistory[window.hotChangeHistory.length - 1])
    } else {
      response(NULL)
    }
  })
  
  # 注册前端消息监听,触发hot的undo方法
  session$onMessage("triggerUndo", function(msg) {
    if(window.hotChangeHistory.length > 0){
      window.hot.undo()
    }
  })
}

shinyApp(ui, server)

关键改动说明

  1. 前端变更历史管理

    • 将原有的cellChanges数组改为changeHistory栈,存储每次修改的完整信息(行、列、旧值、新值)
    • 在afterChange钩子中处理批量修改(自动填充、粘贴),确保所有变更都被记录
    • 当触发undo操作时,自动从栈中移除对应记录
  2. 新增逐次撤销按钮

    • UI中添加undo_step_button按钮
    • 后端通过自定义消息与前端通信,获取最近一次变更并还原旧值
    • 调用Rhandsontable原生的undo()方法,确保表格状态同步更新
  3. 两种撤销逻辑分离

    • 保留原有的一次性撤销逻辑(通过reset=1重置表格到初始状态)
    • 逐次撤销逻辑基于变更历史栈,每次仅回滚最近一次操作

内容的提问来源于stack exchange,提问作者galaxy--

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 07:37:48