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

Shiny golem模块化场景下rintrojs引导弹窗显示在左上角问题

rintrojs 模块化开发下引导弹窗固定在左上角的解决方法

问题背景

使用rintrojs包实现应用分步引导功能,应用基于golem框架采用模块化架构开发,每个标签页对应独立模块。触发引导流程时,所有引导对话框都显示在屏幕左上角,非模块化场景下的解决方案在当前架构下不生效。
问题效果参考:问题截图
可复现问题的示例代码如下:

library(shiny)
library(rintrojs)
library(shinyWidgets)

# UI Module 1
mod_module1_ui <- function(id){
  ns <- NS(id)
  tagList(
    rintrojs::introjsUI(),
    column(
      width = 12,
      actionButton(
        inputId = ns("bt"),
        label = "Display Button"
      )
    ),
    div(
      sidebarPanel(
        style = "height: 100px;",
        width = 12,
        shiny::column(
          width = 3,
          rintrojs::introBox(
            shiny::numericInput(
              inputId = ns("numeric"),
              label = "Numeric Input",
              value = 45
            ),
            data.step = 1,
            data.intro = div(
              h5("Description goes here")
            )
          ),
        ),
        shiny::column(
          width = 3,
          rintrojs::introBox(
            shinyWidgets::pickerInput(
              inputId = ns("picker"),
              label = "Picker Input",
              choices = c(1, 2, 3, 4, 5)
            ),
            data.step = 2,
            data.intro = div(
              h5("Description goes here")
            )
          ),
        ),
      ),
    ),
  )
}

# SERVER Module 1
mod_module1_server <- function(id){
  moduleServer( id, function(input, output, session){
    ns <- session$ns
    observeEvent(input$bt, rintrojs::introjs(session))

  })
}


# UI Module 2
mod_module2_ui <- function(id){
  ns <- NS(id)
  tagList(
    rintrojs::introjsUI(),
    column(
      width = 12,
      actionButton(
        inputId = ns("bt"),
        label = "Display Button"
      )
    ),
    div(
      sidebarPanel(
        style = "height: 100px;",
        width = 12,
        shiny::column(
          width = 3,
          rintrojs::introBox(
            shiny::numericInput(
              inputId = ns("numeric"),
              label = "Numeric Input",
              value = 45
            ),
            data.step = 1,
            data.intro = div(
              h5("Description goes here")
            )
          ),
        ),
        shiny::column(
          width = 3,
          rintrojs::introBox(
            shinyWidgets::pickerInput(
              inputId = ns("picker"),
              label = "Picker Input",
              choices = c(1, 2, 3, 4, 5)
            ),
            data.step = 2,
            data.intro = div(
              h5("Description goes here")
            )
          ),
        ),
      ),
    ),
  )
}

# SERVER Module 2
mod_module2_server <- function(id){
  moduleServer( id, function(input, output, session){
    ns <- session$ns
    observeEvent(input$bt, rintrojs::introjs(session))
  })
}
 
# APP UI 
app_ui <- function(request) {
  tagList(
    shiny::navbarPage(
      title = ("Example"),
      fluid = TRUE,

      # 1 - Tab 1 ----
      tabPanel(
        title = "tab1",
        shinydashboard::dashboardHeader(
          title = span(
            h1("Title tab 1")
          )
        ),
        shinydashboard::dashboardBody(
          mod_module1_ui("module1_1")
        ),
      ),
      # 2 - Tab 2 ----
      shiny::tabPanel(
        title = "tab2",
        shinydashboard::dashboardHeader(
          title = h1("Title tab 2")
        ),
        shinydashboard::dashboardBody(
          mod_module2_ui("module2_1")
        ),
      ),
    )
  )
}

# APP SERVER
app_server <- function(input, output, session) {
  mod_module1_server("module1_1")
  mod_module2_server("module2_1")
}

shinyApp(app_ui, app_server)

故障原因

  • 重复加载introjs依赖:rintrojs::introjsUI()的作用是向页面注入intro.js所需的CSS和JS资源,全局只需要调用一次即可。示例中两个子模块都调用了该函数,导致JS事件重复绑定,元素定位逻辑冲突。
  • 命名空间下元素查找失效:Shiny模块化开发时,所有模块内的元素ID都会自动加上模块命名空间前缀,旧版本rintrojs默认全局查找带data-step属性的元素时,无法正确匹配模块内带命名空间的元素,定位失败后弹窗默认显示在页面左上角。

修复方案

按照以下两点修改即可解决问题:

  1. 删除所有子模块UI中的rintrojs::introjsUI()调用,仅在全局app_ui的最外层调用一次,加载全局依赖。
  2. (兼容旧版本rintrojs可选配置)给每个模块的内容用div包裹作为根容器并设置唯一ID,调用introjs时显式指定从当前模块的根容器内查找引导元素,避免全局匹配失效。

核心修改代码

首先修改全局UI,在顶层引入introjs依赖:

app_ui <- function(request) {
  tagList(
    # 全局只加载一次introjs依赖
    rintrojs::introjsUI(),
    shiny::navbarPage(
      title = ("Example"),
      fluid = TRUE,
      # 原有tabPanel代码保持不变
      tabPanel(
        title = "tab1",
        shinydashboard::dashboardHeader(
          title = span(h1("Title tab 1"))
        ),
        shinydashboard::dashboardBody(
          mod_module1_ui("module1_1")
        ),
      ),
      tabPanel(
        title = "tab2",
        shinydashboard::dashboardHeader(
          title = h1("Title tab 2")
        ),
        shinydashboard::dashboardBody(
          mod_module2_ui("module2_1")
        ),
      ),
    )
  )
}

删除两个子模块UI里的rintrojs::introjsUI()行即可正常运行。如果仍存在定位问题,可按如下方式给模块加根容器限定查找范围,以模块1为例:

mod_module1_ui <- function(id){
  ns <- NS(id)
  # 用div包裹模块全部内容,作为引导查找的根容器
  div(
    id = ns("module_root"),
    column(
      width = 12,
      actionButton(
        inputId = ns("bt"),
        label = "Display Button"
      )
    ),
    div(
      sidebarPanel(
        style = "height: 100px;",
        width = 12,
        shiny::column(
          width = 3,
          rintrojs::introBox(
            shiny::numericInput(
              inputId = ns("numeric"),
              label = "Numeric Input",
              value = 45
            ),
            data.step = 1,
            data.intro = div(h5("Description goes here"))
          ),
        ),
        shiny::column(
          width = 3,
          rintrojs::introBox(
            shinyWidgets::pickerInput(
              inputId = ns("picker"),
              label = "Picker Input",
              choices = c(1, 2, 3, 4, 5)
            ),
            data.step = 2,
            data.intro = div(h5("Description goes here"))
          ),
        ),
      ),
    )
  )
}

mod_module1_server <- function(id){
  moduleServer( id, function(input, output, session){
    ns <- session$ns
    observeEvent(input$bt, {
      # 指定引导元素查找范围为当前模块根容器
      rintrojs::introjs(
        session, 
        options = list(scope = I(paste0("#", ns("module_root"))))
      )
    })
  })
}

模块2按照相同逻辑修改即可。

内容的提问来源于stack exchange,提问作者Guilherme Fonseca

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 02:24:15