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

R Shiny中如何用jsTreeR为拖拽树节点添加次级顺序字母编号

jsTreeR拖拽节点字母编号实现

需求说明

使用jsTreeR开发R Shiny应用时,需要为拖拽到目标容器的节点添加字母形式的次级编号,编号对应该类元素被拖入目标节点的累计次数,编号规则为第1次拖入加后缀a、第2次加b,超过26次自动生成aa/ab类双字母后缀,删除节点后剩余同类型节点编号自动重排保证连续。

拖拽效果示意图

完整可运行代码

library(jsTreeR)
library(shiny)

nodes <- list(
  list(
    text = "Menu",
    state = list(opened = TRUE),
    children = list(
      list(
        text = "Mickey",
        type = "moveable",
        state = list(disabled = TRUE)
      ),
      list(
        text = "Mouse",
        type = "moveable",
        state = list(disabled = TRUE)
      )
    )
  ),
  list(
    text = ">>> Drag here <<<",
    type = "target",
    state = list(opened = TRUE)
  )
)

checkCallback <- JS(
  "function(operation, node, parent, position, more) { ",
  "  if(operation === 'copy_node') {",
  "    if(parent.id === '#' || node.parent !== 'j1_1' || parent.type !== 'target') {",
  "      return false;",
  "    }",
  "  }",
  "  if(operation === 'delete_node') {",
  "    Shiny.setInputValue('deletion', position + 1);",
  "  }",
  "  return true;",
  "}"
)

dnd <- list(
  always_copy = TRUE,
  is_draggable = JS(
    "function(node) {",
    "  return node[0].type === 'moveable';",
    "}"
  )
)

mytree <- jstree(
  nodes, dragAndDrop = TRUE, dnd = dnd, checkCallback = checkCallback,
  types = list(moveable = list(), target = list())
)

# 核心自定义JS事件逻辑
script <- '
$(document).ready(function(){
  // 数字转字母序号工具:1→a, 2→b...26→z,27→aa
  function numToLetter(num) {
    let s = "", t;
    while(num > 0) {
      t = (num - 1) % 26;
      s = String.fromCharCode(97 + t) + s;
      num = (num - t - 1)/26;
    }
    return s;
  }

  // 监听节点复制事件,添加字母后缀
  $("#mytree").on("copy_node.jstree", function(e, data){
    const instance = data.new_instance;
    const node = data.node;
    const parentNode = instance.get_node(data.parent);
    // 统计目标容器内同原始名称的节点数量
    const sameNameCount = parentNode.children.filter(childId => {
      return instance.get_node(childId).original.text === node.original.text;
    }).length;
    const newText = node.original.text + numToLetter(sameNameCount);
    instance.rename_node(node, newText);
    Shiny.setInputValue("choice", newText);
  });

  // 监听节点删除事件,自动重排剩余节点编号避免断层
  $("#mytree").on("delete_node.jstree", function(e, data){
    const instance = data.instance;
    const parentNode = instance.get_node(data.parent);
    if(!parentNode || parentNode.type !== "target") return;
    // 按原始节点名称分组重编号
    const nameGroups = {};
    parentNode.children.forEach(childId => {
      const child = instance.get_node(childId);
      const originName = child.original.text;
      if(!nameGroups[originName]) nameGroups[originName] = [];
      nameGroups[originName].push(child);
    });
    Object.keys(nameGroups).forEach(name => {
      nameGroups[name].forEach((child, idx) => {
        instance.rename_node(child, name + numToLetter(idx+1));
      });
    });
  });
});
'

ui <- fluidPage(
  tags$head(tags$script(HTML(script))),
  fluidRow(
    column(width = 4,jstreeOutput("mytree")),
    column(width = 8,verbatimTextOutput("choices"))
  )
)

server <- function(input, output, session){
  output[["mytree"]] <- renderJstree(mytree)
  Choices <- reactiveVal(data.frame(choice = character(0)))
  observeEvent(input[["choice"]],{
    Choices(rbind(Choices(),data.frame(choice = input[["choice"]])))
  })
  observeEvent(input[["deletion"]], {
    Choices(Choices()[-input[["deletion"]], , drop = FALSE])
  })
  output[["choices"]] <- renderPrint({Choices()})
}

shinyApp(ui, server)

实现要点

  • 数字转字母逻辑兼容26个以上节点的编号场景,自动生成多字母后缀
  • 编号按节点原始名称独立计数,不同名称节点的编号互不干扰
  • 新增删除事件监听,节点删除后同组剩余节点自动重新编号,不会出现序号跳变
  • 编号直接通过jsTree原生rename_node方法修改,不影响树的拖拽、删除等原生交互

内容的提问来源于stack exchange,提问作者Curious Jorge - user9788072

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 13:33:16