Adding tinyMCE editor in Shiny modal - r

I'd like to create an HTML editor in a Shiny app, using the shinyMCE package.
This works well in the example below.
library(shiny)
library(shinyMCE)
library(shinyjs)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(
useShinyjs(),
header = dashboardHeader(disable = T),
sidebar = dashboardSidebar(disable = T),
body = dashboardBody(
tags$script(src = "http://cdn.tinymce.com/4/tinymce.min.js",
referrerpolicy = "origin"),
tinyMCE("editor", "The content"),
actionButton("ok", "OK")
))
server <- function(input, output, session)
{
observeEvent(
input$ok,
{
print(input$editor)
}
)
observeEvent(
input$open,
{
showModal(myModal())
})
}
shinyApp(ui, server = server)
Indeed, if you press OK, the content of the editor is printed in the R console.
Now, I'd like to put the editor in a modal. If I do the following the editor appears, but if I press OK the content doesn't get updated. That is, the R console always shows "the content", independently of what is written in the textarea.
library(shiny)
library(shinyMCE)
library(shinyjs)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(
useShinyjs(),
header = dashboardHeader(disable = T),
sidebar = dashboardSidebar(disable = T),
body = dashboardBody(
tags$script(src = "http://cdn.tinymce.com/4/tinymce.min.js",
referrerpolicy = "origin"),
flowLayout (
actionButton("open", "Open")
)))
myModal <- function()
{
modalDialog(size = "l",
title = "A modal dialog",
tinyMCE("tinyTxt", "the content"),
actionButton("ok", "OK"),
easyClose = T)
}
server <- function(input, output, session)
{
observeEvent(
input$ok,
{
print(input$tinyTxt)
}
)
observeEvent(
input$open,
{
showModal(myModal())
})
}
shinyApp(ui, server = server)
In the JS console I get
Uncaught TypeError: Cannot read property 'getContent' of null
at exports.InputBinding.getValue (<anonymous>:9:41)
at c (init_shiny.js:117)
at init_shiny.js:163
at eN.<anonymous> (<anonymous>:16:18)
at mp.c.fire (tinymce.min.js:2)
at eN.fire (tinymce.min.js:2)
at eN.<anonymous> (tinymce.min.js:2)
at mp.c.fire (tinymce.min.js:2)
at eN.fire (tinymce.min.js:2)
at Rp (tinymce.min.js:2)
Any idea of how to get around the problem?
EDIT: One further observation. In the first (working) example tinyMCE.editors contains one instance of an editor, while in the second it is empty (although the editor does display!).

I managed to solve this, by manually creating the TinyMCE editor (which solves the issue of the editor not appearing in tinymce.editors) and then use some custom JS to retrieve the value.
This seems a bit hacky to me, but it works...
Here's an example
library(shiny)
library(shinyjs)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(
useShinyjs(),
header = dashboardHeader(disable = T),
sidebar = dashboardSidebar(disable = T),
body = dashboardBody(
singleton(tags$head(tags$script(src = "http://cdn.tinymce.com/4/tinymce.min.js",
referrerpolicy = "origin"))),
# Register a custom message handler that gets the content of the editor
# and forces update of the textarea
singleton(tags$head(tags$script("Shiny.addCustomMessageHandler('getTxt',
function(message) {
var content = tinyMCE.get('tinyTxt').getContent();
Shiny.onInputChange('tinyTxt', content);
})"))),
flowLayout (
actionButton("open", "Open"),
htmlOutput("content")
)))
myModal <- function()
{
modalDialog(size = "l",
title = "A modal dialog",
textAreaInput("tinyTxt", "the content"),
actionButton("ok", "OK"),
easyClose = T)
}
server <- function(input, output, session)
{
observeEvent(
input$ok,
{
# Retrieve the content of the editor
session$sendCustomMessage("getTxt", "")
removeModal()
})
output$content <- renderText(
input$tinyTxt
)
observeEvent(
input$open,
{
showModal(myModal())
# Create the tinyMCE editor
runjs("var ed = new tinymce.Editor('tinyTxt', {
selector: 'tinyTxt',
theme: 'modern'},
tinymce.EditorManager);
ed.render();")
})
}
shinyApp(ui, server = server)

For anyone looking at this in 2022, this is an updated solution working with version 6 of TinyMCE. You need to get an API key which is available on registration at https://www.tiny.cloud/
I also added a print to the R console with a delay() as the returned input was blank otherwise.
There are a few additional options compared to the original solution. I kept them there to show how it's done. Hopefully this is useful to someone!
library(shinyjs)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(
useShinyjs(),
header = dashboardHeader(disable = T),
sidebar = dashboardSidebar(disable = T),
body = dashboardBody(
tags$head(tags$script(src = "https://cdn.tiny.cloud/1/--API-KEY-HERE--/tinymce/6/tinymce.min.js",
referrerpolicy = "origin")),
# Register a custom message handler that gets the content of the editor
# and forces update of the textarea
tags$head(tags$script("Shiny.addCustomMessageHandler('getTxt',
function(message) {
var content = tinymce.activeEditor.getContent();;
Shiny.onInputChange('tinyTxt', content);
})")),
flowLayout (
actionButton("open", "Open"),
htmlOutput("content")
)))
myModal <- function()
{
modalDialog(size = "l",
title = "A modal dialog",
textAreaInput("tinyTxt", "the content"),
actionButton("ok", "OK"),
easyClose = T)
}
server <- function(input, output, session)
{
observeEvent(
input$open,
{
showModal(myModal())
# Create the tinyMCE editor
runjs("var ed = new tinymce.Editor('tinyTxt', {
menubar: false,
branding: false,
plugins: 'lists, table, link',
contextmenu: 'lists, link, table',
toolbar1: 'bold italic forecolor backcolor | formatselect fontselect fontsizeselect | alignleft aligncenter alignright alignjustify',
toolbar2: 'undo redo removeformat bullist numlist table blockquote code superscript subscript strikethrough link'},
tinymce.EditorManager);
ed.render();")
})
observeEvent(
input$ok,
{
# Retrieve the content of the editor
session$sendCustomMessage("getTxt", "")
output$content <- renderText(
input$tinyTxt
)
delay(500, print(input$tinyTxt))
removeModal()
})
}
shinyApp(ui, server = server)

Related

Shiny remove extra row added by external link in navbar tab panel

I'm trying to add an external link to the tab panel title in a shiny navbar. The link itself works fine, but it moves the tab with the link into a separate row.
How can I include a link and maintain the placement of the tab in the same row as any other tabs that don't contain links?
Here is my minimalistic code:
library(shiny)
ui <- navbarPage(
title = "",
id = "navbar",
header = "",
tabsetPanel(id="tabs",
tabPanel(
title = "Tab1", value = "tab1"
),
tabPanel(
title = a("Tab2",
href = "https://www.google.com/"),
value = "tab2"
)
)
)
server <- function(input, output, session) {
}
shinyApp(ui, server)
I have tried using the HTML function to see if that for some reason gives a different result, but as expected it didn't:
tabPanel(
title = HTML("<a href = 'https://www.google.com/'>tab2</a>"),
value = "tab2"
)
I would really appreciate your advice!
If you also happen to have any idea on how to remove the title row from the navbarPage, that would also be much appreciated.
If you look at the HTML for your tabs, you can see that the tabs themselves already have a <a href ...> tag. So what you're doing is adding another one below the existing one.
A work-around is to do something like
Observe when Tab2 is pressed
Navigate to the URL
library(shiny)
ui <- navbarPage(
title = "",
id = "navbar",
header = "",
tabsetPanel(
id = "tabs",
tabPanel(title = "Tab1"),
tabPanel(title = "Tab2")
)
)
server <- function(input, output, session) {
observeEvent(input$tabs, {
print(input$tabs)
if( input$tabs == "Tab2" ) {
browseURL("https://www.google.com/")
}
})
}
shinyApp(ui, server)
One way to do this, is to use a javascript function to do the linking for us. Then we do not need to include <a href> inside the tab which is already a link!.
We can easily set up a Js function with {shinyjs} and extendShinyjs(). Then we call it in an observeEvent() when the tab is clicked.
library(shiny)
library(shinyjs)
ui <- navbarPage(
# use shinyjs
useShinyjs(),
# write JS function to open window
shinyjs::extendShinyjs(text = "shinyjs.myfun = function() { window.open('https://www.google.com/', '_self'); }",
functions = c("myfun")),
title = "",
id = "navbar",
header = "",
tabsetPanel(id="tabs",
tabPanel(
title = "Tab1", value = "tab1"
),
tabPanel(
title = "Tab2",
value = "tab2"
)
)
)
server <- function(input, output, session) {
# use observeEvent to check if user clicks tab no 2
observeEvent(input$tabs,
{
if (input$tabs == "tab2") {
shinyjs::js$myfun()
}
})
}
shinyApp(ui, server)

Arrow of menuItem on top of text when creating from list in server - how to remove it? (shinyDashboard)

I am creating a shiny dashboard app where part of the sidebar is generated automatically when adding some inputs. When the menuItems are created inside a list in a server function, the default arrow that should be next to the text is actually on top. I have tried some CSS to remove it but I don't know much and of course it doesn't work.
/* Hide icons in sub-menu items */
.sidebar .sidebar-menu .treeview-menu>li>a>.fa {
display: none;
}
This is how it looks:
This is the code used:
ui.R :
shinyUI(dashboardPage(
dashboardSidebar(
width = 400,
sidebarMenu(
menuItem('Annotations', icon = icon('pencil-alt'),
uiOutput('annot')
)
)#end of menu
)#end of sidebar
)) #end of shinyUI
server.R :
##Create inputs for annotations
output$annot <- renderUI({
nclones <- 3 ##this changes (input) but for the example it is enough
cloneLabs <- c('A','B','C') ##this changes (input) but for the example it is enough
lapply(1:(nclones), function(i) {
list(menuItem(paste('Clone',cloneLabs[i]),
textInput(paste0('annot',i),
paste('Annotations',cloneLabs[i])),
switchInput(paste0("col",i), "Text color", labelWidth = "80px",
onLabel = 'White', offLabel = 'Black',
onStatus = 'default'
) #end of menuItem
) #end of list
}) #end of lapply
}) # end of renderUI
Please check shinydashboard's capabilities on dynamic content.
The problem with renderUI in this case is, that it creates a div tag. However, for the menuItems we need to create a li tag. This is what renderMenu does.
library(shiny)
library(shinyWidgets)
library(shinydashboard)
ui <- dashboardPage(dashboardHeader(),
dashboardSidebar(width = 400,
sidebarMenu(
menuItem('Annotations', icon = icon('pencil-alt'),
menuItemOutput('annot'))
)),
dashboardBody())
server <- function(input, output, session) {
##Create inputs for annotations
output$annot <- renderMenu({
nclones <- 3 ##this changes (input) but for the example it is enough
cloneLabs <- c('A', 'B', 'C') ##this changes (input) but for the example it is enough
sidebarMenu(
lapply(1:(nclones), function(i) {
menuItem(
paste('Clone', cloneLabs[i]),
textInput(paste0('annot', i),
paste('Annotations', cloneLabs[i])),
switchInput(
paste0("col", i),
"Text color",
labelWidth = "80px",
onLabel = 'White',
offLabel = 'Black',
onStatus = 'default'
)
) # end of menuItem
}) # end of lapply
) # end of sidebarMenu
}) # end of renderMenu
}
shinyApp(ui, server)
This solution has a different UI effect, but follows the same principle as #ismirsehregal--get rid of the div automatically generated by renderUI(). It uses insertUI() instead.
library(shiny)
library(shinydashboard)
library(shinyWidgets)
ui <- shinyUI(dashboardPage(
dashboardHeader(),
dashboardSidebar(
width = 400,
sidebarMenu(
menuItem('Annotations', icon = icon('pencil-alt'),
span(id ="annot"))
)#end of menu
),#end of sidebar
dashboardBody()
)) #end of shinyUI
server <- function(input, output) {
##Create inputs for annotations
make_menu <- function() {
nclones <- 3 ##this changes (input) but for the example it is enough
cloneLabs <- c('A','B','C') ##this changes (input) but for the example it is enough
menu_bundle <- lapply(1:(nclones), function(i) {
menuItem(paste('Clone',cloneLabs[i]),
textInput(paste0('annot',i),
paste('Annotations',cloneLabs[i])),
switchInput(paste0("col",i), "Text color", labelWidth = "80px",
onLabel = 'White', offLabel = 'Black',
onStatus = 'default'
) #end of menuItem
) #end of list
}) #end of lapply
insertUI(
selector = "#annot",
where = "afterEnd",
ui = menu_bundle
)
} # end of renderUI
make_menu()
}
shinyApp(ui, server)

Show only one notification in Shiny

I would like to control the phone number by showing a notification :
If the user type a wrong number (like "aaaa")
If the user type a long number (greater than 10 digits)
I used the function showNotification from shiny with closeButton = FALSE and duration = NULL.
When the user type a wrong number, the notification popped up but when he type a long number the the notification also popped up but the previous one does not disappear
I would like to show only one notification (wrong number or long number) but not the both at the same time. How can we do that ? Here's my apps :
library(shiny)
library(shinydashboard)
library(shinydashboardPlus)
############# UI ############
body <- dashboardBody(
tabItems(
tabItem(tabName = "tab1",
fluidRow(
tags$h1('my title'),
textInput("phone_number", "enter your phone number", value = ""),
actionButton("button", "go")
)
)
)
)
ui <- dashboardPage(
title = "Example",
options = list(sidebarExpandOnHover = TRUE),
header = dashboardHeader(disable = FALSE),
sidebar = dashboardSidebar(
minified = TRUE, collapsed = TRUE,
sidebarMenu(id="menu",
menuItem("first tab", tabName = "mytab", icon = icon("fas fa-acorn"),
menuSubItem('menu1',
tabName = 'tab1',
icon = icon('fas fa-hand-point-right'))
)
)
),
body
)
############# SERVER ############
server <- function(input, output) {
observeEvent(
input$button,
{
if(is.na(as.numeric(input$phone_number))) {
showNotification(type = "error",
duration = NULL,
closeButton = FALSE,
"wrong number")
} else if(nchar(input$phone_number)<10) {
showNotification(type = "error",
duration = NULL,
closeButton = FALSE,
"too long (10 digits required)")
}
}
)
}
############# RUN THE APP ############
shinyApp(ui = ui, server = server)
Some help would be appreciated
I would not use a notification here, because they will always be displayed for a fixed time duration and at a different position of the window, which might confuse the user. I would just render the message in using a textOutput:
library(shiny)
library(shinyjs)
ui <- fluidPage(
useShinyjs(),
textInput("phone_number", "phone number"),
textOutput("phone_notification")
)
server <- function(input, output, session) {
output$phone_notification <- renderText({
if(input$phone_number == "") {
"" # empty is ok
}
else if(is.na(as.numeric(input$phone_number))) {
"wrong number"
} else if (nchar(input$phone_number) > 10) {
"too long"
} else {
"" # correct number
}
})
}
shinyApp(ui, server)
You can also style the text e.g. to make it red:
ui <- fluidPage(
useShinyjs(),
textInput("phone_number", "phone number"),
tagAppendAttributes(
textOutput("phone_notification"),
style = "color:red"
)
)
I needed the solution OP asked. I found the following code snippet to work in my case:
removeNotification(id = "onlyOnce", session = getDefaultReactiveDomain())
showNotification("Show me once", id = "onlyOnce")
See also: https://search.r-project.org/CRAN/refmans/shiny/html/showNotification.html

R Shiny stop code execution until modal is closed

I want to execute rest of shiny app code only when modal dialog box is closed. How can I achieve this?
Here simple code:
# ui.R
actionButton("loadData", label = "Button", icon = icon("mail-forward"))
# server.R
observeEvent(input$loadData, {
showModal(modalDialog(
title = modal.title,
textInput("newName", "Enter file name:", value = ""),
easyClose = TRUE,
footer = list(
actionButton("confirmName", "OK"),
modalButton("Cancel"))
))
# ...code to be executed after modal is closed...
})
Create an event handler that executes code when the OK action button has been clicked, and also closes the modal using removeModal.
library(shiny)
ui <- fluidPage(
actionButton("loadData", label = "Button", icon = icon("mail-forward")),
verbatimTextOutput("filename")
)
server <- function(input, output, session) {
observeEvent(input$loadData, {
showModal(modalDialog(
title = "title",
textInput("newName", "Enter file name:", value = ""),
easyClose = TRUE,
footer = list(
actionButton("confirmName", "OK"),
modalButton("Cancel"))
))
})
output$filename <- eventReactive(input$confirmName, {
message("Closing modal")
removeModal()
input$newName
})
}
shinyApp(ui, server)
There's an example of this in the docs: https://shiny.rstudio.com/reference/shiny/latest/modalDialog.html

R Shinydashboard dynamic menuItem

I'm trying to generate multiple menuItems dynamically, may be simple, but I'm not getting the right idea.
library(shiny)
library(shinydashboard)
port_tables<-c("tab1","tab2","tab3","tab4") # These are from a DB connection in the original code
function(input, output) {
output$smenu1 <- renderMenu({
sidebarMenu( id = "tabs",
h4("Tables",style="color:yellow;margin-left:20px;"),
paste("menuItem(\"",port_tables,"\",tabName=\"",port_tables,"\",icon=icon('th'))",collapse=",")
)
})
)
The menuItems from the paste function doesn't resolve( I get the result of paste function on the sidebar). I tried eval, eval(parse(paste(...))), both didn't work - what am I missing?
I couldn't quite make out what you're asking for, but here's an example of something with a dynamic menu.
library(shiny)
library(shinydashboard)
ui <- dashboardPage(
dashboardHeader(title = "Dynamic Menu"),
dashboardSidebar(
sidebarMenuOutput(outputId = "dy_menu")
),
dashboardBody(
tabItems(
tabItem(tabName = "main",
textInput(inputId = "new_menu_name",
label = "New Menu Name"),
actionButton(inputId = "add",
label = "Add Menu")
)
)
)
)
server <- function(input, output, session){
output$dy_menu <- renderMenu({
menu_list <- list(
menuItem("Add Menu Items", tabName = "main", selected = TRUE),
menu_vals$menu_list)
sidebarMenu(.list = menu_list)
})
menu_vals = reactiveValues(menu_list = NULL)
observeEvent(eventExpr = input$add,
handlerExpr = {
menu_vals$menu_list[[length(menu_vals$menu_list) + 1]] <- menuItem(input$new_menu_name,
tabName = input$new_menu_name)
})
}
shinyApp(ui, server)
I changed the code as follows and it worked :
library(shiny)
library(shinydashboard)
port_tables<-c("tab1","tab2","tab3","tab4") # These are from a DB connection in the original code
text1<-paste("menuItem(\"",port_tables,"\",tabName=\"",port_tables,"\",icon=icon('th'))")
text2<-paste("sidebarMenu(id = 'tabs',textInput('port', 'Enter port:'),h4('Tables',style='color:yellow;margin-left:20px;'),",paste(text1,collapse=","),paste(")"))
function(input, output) {
output$smenu1 <- renderMenu({
eval(parse(text=text2))
})
)
So, the key is put the whole content of sidebarMenu in a text field and evaluate it

Resources