My goal is to add a search box on top of the custom input control in shiny. I would like when a user searches Hampshire for example, the selection to pick New Hampshire which is not currently possible as it searches just by the first letter.
server.R
shinyServer(function(input, output, session) {
output$main <- renderUI({
source("chooser.R")
chooserInput("mychooser","Available frobs","Selected frobs",
row.names(USArrests),c(),size=20,multiple=TRUE)})
})
ui.R
source("chooser.R")
shinyUI(fluidPage(
uiOutput("main")
))
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",
div(class="chooser-container chooser-left-container",
tags$select(class="left", size=size, multiple=multiple, leftChoices)
),
div(class="chooser-container chooser-center-container",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
),
div(class="chooser-container chooser-right-container",
tags$select(class="right", size=size, multiple=multiple, rightChoices)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
chooser-binding.js (in www folder)
(function() {
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();
Cool widget (or whatever the terminology is). This question has actually been answered here so make sure to vote on the persons answer if it helps you.
Here's a super simple implementations of it (could be better):
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",style="",
div(
div(style="min-width:100px;",
tags$input(type="text",class="chooser-input-search",style="width:100px;")
)
),
div(style="display:table",
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-left-container",
style="width:100%;",
tags$select(class="left", size=size, multiple=multiple, leftChoices,style="width:100%;min-width:100px")
)
),
div(style="min-width:50px; display:table-cell;vertical-align: middle;",
div(class="chooser-container chooser-center-container",
style="padding:10px;",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
)
),
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-right-container", style="width:100%;",
tags$select(class="right", size=size, multiple=multiple, rightChoices,style="width:100%;")
)
)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
chooser-bindings.js
(function() {
var options = [];
jQuery.fn.filterByText = function(textbox, selectSingleMatch) {
return this.each(function() {
var select = this;
options = [];
$(select).find('option').each(function() {
options.push({value: $(this).val(), text: $(this).text()});
});
$(select).data('options', options);
$(textbox).bind('change keyup', function() {
options = $(select).empty().scrollTop(0).data('options');
var search = $.trim($(this).val());
var regex = new RegExp(search,'gi');
$.each(options, function(i) {
var option = options[i];
if(option.text.match(regex) !== null) {
$(select).append(
$('<option>').text(option.text).val(option.value)
);
}
});
if (selectSingleMatch === true &&
$(select).children().length === 1) {
$(select).children().get(0).selected = true;
}
});
});
};
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(".chooser").change(function(){
});
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
$(function() {
$('.left').filterByText($('.chooser-input-search'), true);
});
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();
As you can see this is pretty much a shameful copy and paste.
Related
I would like to have a box in my shiny app, which user can browse to a folder structure and select a file to download.
I have tried the shinyFiles but the file selection is a pop-up window and I could just download a single file :
library(shiny)
library(shinyFiles)
ui <- fluidPage(
shinyFilesButton('files', label='File select', title='Please select a file', multiple=T) ,
verbatimTextOutput('rawInputValue'),
verbatimTextOutput('filepaths') ,
downloadButton("downloadFiles", "Download Files")
)
server <- function(input, output) {
roots = c(wd = 'H:/')
shinyFileChoose(input, 'files',
roots = roots,
filetypes=c('', 'txt' , 'gz' , 'md5' , 'pdf' , 'fasta' , 'fastq' , 'aln'))
output$rawInputValue <- renderPrint({str(input$files)})
output$filepaths <- renderPrint({parseFilePaths(roots, input$files)})
output$downloadFiles <- downloadHandler(
filename = function() {
as.character(parseFilePaths(roots, input$files)$name)
},
content = function(file) {
fullName <- as.character(parseFilePaths(roots, input$files)$datapath)
file.copy(fullName, file)
}
)
}
shinyApp(ui = ui , server = server)
What I would like is to have the file selection option like
within the ui, not as new (pop-up) window !
Here is a first version of the app I talked about in my comment. Its advantage is that the contents of a folder are loaded only when the user selects this folder, and only the first descendants are loaded, no recursion.
App folder structure:
C:\PATH\TO\MYAPP
| global.R
| server.R
| ui.R
|
\---www
navigator.css
navigator.js
File global.R:
library(shiny)
library(jsTreeR)
library(htmlwidgets)
library(magrittr)
library(shinyFiles)
roots <- c(wd = "C:/SL/MyPackages/", getVolumes()())
File server.R:
shinyServer(function(input, output, session){
shinyDirChoose(
input, "rootfolder", roots = roots,
allowDirCreate = FALSE, defaultRoot = "wd"
)
RootFolder <- eventReactive(input[["rootfolder"]], {
parseDirPath(roots, input[["rootfolder"]])
})
output[["choice"]] <- reactive({
isTruthy(RootFolder())
})
outputOptions(output, "choice", suspendWhenHidden = FALSE)
output[["navigator"]] <- renderJstree({
req(isTruthy(RootFolder()))
jstree(
nodes = list(
list(
text = RootFolder(),
type = "folder",
children = FALSE,
li_attr = list(
class = "jstree-x"
)
)
),
types = list(
folder = list(
icon = "fa fa-folder gold"
),
file = list(
icon = "far fa-file red"
)
),
checkCallback = TRUE,
theme = "default",
checkboxes = TRUE,
search = TRUE,
selectLeavesOnly = TRUE
) %>% onRender("function(el, x){tree = $(el).jstree(true);}")
})
observeEvent(input[["path"]], {
lf <- list.files(input[["path"]], full.names = TRUE)
fi <- file.info(lf, extra_cols = FALSE)
x <- list(
elem = as.list(basename(lf)),
folder = as.list(fi[["isdir"]])
)
session$sendCustomMessage("getChildren", x)
})
Paths <- reactive({
vapply(
input[["navigator_selected_paths"]], `[[`,
character(1L), "path"
)
})
output[["selections"]] <- renderPrint({
cat(Paths(), sep = "\n")
})
output[["dwnld"]] <- downloadHandler(
filename = "myfiles.zip",
content = function(file){
zip(file, files = Paths())
}
)
})
File ui.R:
shinyUI(fluidPage(
tags$head(
tags$link(rel = "stylesheet", href = "navigator.css"),
tags$script(src = "navigator.js")
),
br(),
conditionalPanel(
condition = "!output.choice",
fluidRow(
column(
width = 12,
shinyDirButton(
"rootfolder",
label = "Browse to choose a root folder",
title = "Choose a folder",
buttonType = "primary",
class = "btn-block"
)
)
)
),
conditionalPanel(
condition = "output.choice",
style = "display: none;",
fluidRow(
column(
width = 6,
jstreeOutput("navigator")
),
column(
width = 6,
tags$fieldset(
tags$legend(
tags$h1("Selections:", style = "float: left;"),
downloadButton(
"dwnld",
class = "btn-primary btn-lg",
icon = icon("save"),
style = "float: right;"
)
),
verbatimTextOutput("selections")
)
)
)
)
))
File navigator.css:
.jstree-default .jstree-x.jstree-closed > .jstree-icon.jstree-ocl,
.jstree-default .jstree-x.jstree-leaf > .jstree-icon.jstree-ocl {
background-position: -100px -4px;
}
.red {
color: red;
}
.gold {
color: gold;
}
.jstree-proton {
font-weight: bold;
}
.jstree-anchor {
font-size: medium;
}
File navigator.js:
var tree;
$(document).ready(function () {
var Children = null;
Shiny.addCustomMessageHandler("getChildren", function (x) {
Children = x;
});
$("#navigator").on("click", "li.jstree-x > i", function (e) {
var $li = $(this).parent();
if (!$li.hasClass("jstree-x")) {
alert("that should not happen...");
return;
}
var id = $li.attr("id");
var node = tree.get_node(id);
if (tree.is_leaf(node) && node.original.type === "folder") {
var path = tree.get_path(node, "/");
Shiny.setInputValue("path", path);
var interval = setInterval(function () {
if (Children !== null) {
clearInterval(interval);
for (var i = 0; i < Children.elem.length; i++) {
var isdir = Children.folder[i];
var newnode = tree.create_node(id, {
text: Children.elem[i],
type: isdir ? "folder" : "file",
children: false,
li_attr: isdir ? { class: "jstree-x" } : null
});
}
Children = null;
setTimeout(function () {
tree.open_node(id);
}, 10);
}
}, 100);
}
});
});
(I am the author of jsTreeR and I think I will do a Shiny module for this folder navigator and include it in the package.)
EDIT
I improved the app and it uses the proton theme now, which looks more pretty to me:
To use this app, you first need the updated version of the package:
remotes::install_github("stla/jsTreeR")
There are some changes in three files:
server.R:
shinyServer(function(input, output, session){
shinyDirChoose(
input, "rootfolder", roots = roots,
allowDirCreate = FALSE, defaultRoot = "wd"
)
RootFolder <- eventReactive(input[["rootfolder"]], {
parseDirPath(roots, input[["rootfolder"]])
})
output[["choice"]] <- reactive({
isTruthy(RootFolder())
})
outputOptions(output, "choice", suspendWhenHidden = FALSE)
output[["navigator"]] <- renderJstree({
req(isTruthy(RootFolder()))
jstree(
nodes = list(
list(
text = RootFolder(),
type = "folder",
children = FALSE,
li_attr = list(
class = "jstree-x"
)
)
),
types = list(
folder = list(
icon = "fa fa-folder gold"
),
file = list(
icon = "far fa-file red"
)
),
checkCallback = TRUE,
theme = "proton",
checkboxes = TRUE,
search = TRUE,
selectLeavesOnly = TRUE
)
})
observeEvent(input[["path"]], {
lf <- list.files(input[["path"]], full.names = TRUE)
fi <- file.info(lf, extra_cols = FALSE)
x <- list(
elem = as.list(basename(lf)),
folder = as.list(fi[["isdir"]])
)
session$sendCustomMessage("getChildren", x)
})
Paths <- reactive({
vapply(
input[["navigator_selected_paths"]], `[[`,
character(1L), "path"
)
})
output[["selections"]] <- renderPrint({
cat(Paths(), sep = "\n")
})
output[["dwnld"]] <- downloadHandler(
filename = "myfiles.zip",
content = function(file){
zip(file, files = Paths())
}
)
})
navigator.css:
.jstree-proton {
font-weight: bold;
}
.jstree-anchor {
font-size: medium;
}
.jstree-proton .jstree-x.jstree-closed > .jstree-icon.jstree-ocl,
.jstree-proton .jstree-x.jstree-leaf > .jstree-icon.jstree-ocl {
background-position: -101px -5px;
}
.jstree-proton .jstree-checkbox.jstree-checkbox-disabled {
background-position: -37px -69px;
}
.red {
color: red;
}
.gold {
color: gold;
}
navigator.js:
$(document).ready(function () {
var tree;
var Children = null;
Shiny.addCustomMessageHandler("getChildren", function (x) {
Children = x;
});
$navigator = $("#navigator");
$navigator.one("ready.jstree", function (e, data) {
tree = data.instance;
tree.disable_checkbox("j1_1");
tree.disable_node("j1_1");
});
$navigator.on("after_open.jstree", function (e, data) {
tree.enable_checkbox(data.node);
tree.enable_node(data.node);
});
$navigator.on("after_close.jstree", function (e, data) {
tree.disable_checkbox(data.node);
tree.disable_node(data.node);
});
$navigator.on("click", "li.jstree-x > i", function (e) {
var $li = $(this).parent();
if (!$li.hasClass("jstree-x")) {
alert("that should not happen...");
return;
}
var id = $li.attr("id");
var node = tree.get_node(id);
if (tree.is_leaf(node) && node.original.type === "folder") {
var path = tree.get_path(node, "/");
Shiny.setInputValue("path", path);
var interval = setInterval(function () {
if (Children !== null) {
clearInterval(interval);
for (var i = 0; i < Children.elem.length; i++) {
var isdir = Children.folder[i];
var newnode = tree.create_node(id, {
text: Children.elem[i],
type: isdir ? "folder" : "file",
children: false,
li_attr: isdir ? { class: "jstree-x" } : null
});
if (isdir) {
tree.disable_checkbox(newnode);
tree.disable_node(newnode);
}
}
Children = null;
setTimeout(function () {
tree.open_node(id);
}, 10);
}
}, 100);
}
});
});
EDIT 2
The new version of the package provides a Shiny module allowing to conveniently renders such a 'tree navigator' (or even several ones). This is the example given in the package:
library(shiny)
library(jsTreeR)
css <- HTML("
.flexcol {
display: flex;
flex-direction: column;
width: 100%;
margin: 0;
}
.stretch {
flex-grow: 1;
height: 1px;
}
.bottomright {
position: fixed;
bottom: 0;
right: 15px;
min-width: calc(50% - 15px);
}
")
ui <- fixedPage(
tags$head(
tags$style(css)
),
class = "flexcol",
br(),
fixedRow(
column(
width = 6,
treeNavigatorUI("explorer")
),
column(
width = 6,
tags$div(class = "stretch"),
tags$fieldset(
class = "bottomright",
tags$legend(
tags$h1("Selections:", style = "float: left;"),
downloadButton(
"dwnld",
class = "btn-primary btn-lg",
style = "float: right;",
icon = icon("save")
)
),
verbatimTextOutput("selections")
)
)
)
)
server <- function(input, output, session){
Paths <- treeNavigatorServer(
"explorer", rootFolder = getwd(),
search = list( # (search in the visited folders only)
show_only_matches = TRUE,
case_sensitive = TRUE,
search_leaves_only = TRUE
)
)
output[["selections"]] <- renderPrint({
cat(Paths(), sep = "\n")
})
}
shinyApp(ui, server)
I am creating a shiny app, which will show modal only when a user visits the app for the first time. After the first time, modal will not be shown. However, this does not work and modal is shown in every visit.
First I set a session ID as cookies and then tried to put modal inside if statement so that if the session ID is present and equal to what I set, it will not be executed. Although the statement in if condition is FALSE, the modal is still shown. In addition, if I write FALSE inside if condition manually, it works and doesn't show the modal. I used textOutput to show if the condition is TRUE or FALSE and it shows the opposite of how if statement is behaving.
I have also tried to set only input$jscookie != sessionid as condition, but it gives Error in if: argument is of length zero error although it prints FALSE when I use same statement in textOutput({input$jscookie != sessionid}).
Below is a reproducible example showing what I'm experiencing. Thank you in advance.
library(shiny)
library(shinyjs)
if (!dir.exists('www/')) {
dir.create('www')
}
download.file(
url = 'https://github.com/js-cookie/js-cookie/releases/download/v2.2.1/js.cookie-2.2.1.min.js',
destfile = 'www/js.cookie.js'
)
sessionid <- "OQGYIrpOvV3KnOpBSPgOhqGxz2dE5A9IpKhP6Dy2kd7xIQhLjwYzskn9mIhRAVHo29"
addResourcePath("js", "www")
jsCode <- '
shinyjs.getcookie = function(params) {
var cookie = Cookies.get("id");
if (typeof cookie !== "undefined") {
Shiny.onInputChange("jscookie", cookie);
} else {
var cookie = "";
Shiny.onInputChange("jscookie", cookie);
}
}
shinyjs.setcookie = function(params) {
Cookies.set("id", escape(params), { expires: 0.5 });
Shiny.onInputChange("jscookie", params);
}
shinyjs.rmcookie = function(params) {
Cookies.remove("id");
Shiny.onInputChange("jscookie", "");
}
'
ui <- fluidPage(
tags$head(
tags$script(src = "js/js.cookie.js")
),
useShinyjs(),
extendShinyjs(text = jsCode),
textOutput('id'),
verbatimTextOutput('sometext')
)
server <- function(input, output) {
observeEvent("", {
js$getcookie()
if (is.null(input$jscookie) || input$jscookie != sessionid) {
showModal(modalDialog(
"Click close button",
easyClose = TRUE,
footer = tagList(
actionButton(inputId = "close", label = "Close", icon = icon("close"))
)
))
}
})
observeEvent(input$close,{
removeModal()
js$setcookie(sessionid)
})
sometext<-reactive({
is.null(input$jscookie) || input$jscookie != sessionid
})
output$id<-renderText({
paste('session ID:', input$jscookie)
})
output$sometext<-renderText({
sometext()
})
}
shinyApp(ui = ui, server = server)
Ok, I've solved it.
Using input$jscookie as statement in observeEvent and putting js$getcookie() outside of observeEvent solved the problem.
library(shiny)
library(shinyjs)
if (!dir.exists('www/')) {
dir.create('www')
}
download.file(
url = 'https://github.com/js-cookie/js-cookie/releases/download/v2.2.1/js.cookie-2.2.1.min.js',
destfile = 'www/js.cookie.js'
)
sessionid <- "OQGYIrpOvV3KnOpBSPgOhqGxz2dE5A9IpKhP6Dy2kd7xIQhLjwYzskn9mIhRAVHo291"
addResourcePath("js", "www")
jsCode <- '
shinyjs.getcookie = function(params) {
var cookie = Cookies.get("id");
if (typeof cookie !== "undefined") {
Shiny.onInputChange("jscookie", cookie);
} else {
var cookie = "";
Shiny.onInputChange("jscookie", cookie);
}
}
shinyjs.setcookie = function(params) {
Cookies.set("id", escape(params), { expires: 0.5 });
Shiny.onInputChange("jscookie", params);
}
shinyjs.rmcookie = function(params) {
Cookies.remove("id");
Shiny.onInputChange("jscookie", "");
}
'
ui <- fluidPage(
tags$head(
tags$script(src = "js/js.cookie.js")
),
useShinyjs(),
extendShinyjs(text = jsCode),
textOutput('id'),
verbatimTextOutput('sometext')
)
server <- function(input, output) {
js$getcookie()
observeEvent(input$jscookie, {
if (is.null(input$jscookie) || input$jscookie != sessionid) {
showModal(modalDialog(
"Click close button",
easyClose = TRUE,
footer = tagList(
actionButton(inputId = "close", label = "Close", icon = icon("close"))
)
))
}
})
observeEvent(input$close,{
removeModal()
js$setcookie(sessionid)
})
sometext<-reactive({
is.null(input$jscookie) || input$jscookie != sessionid
})
output$id<-renderText({
paste('session ID:', input$jscookie)
})
output$sometext<-renderText({
sometext()
})
}
shinyApp(ui = ui, server = server)
Does anyone know if it is possible to create a "shinyWidget :: searchInput" designed with a grid that would allow entering only 9 digits in the field?
https://pasteboard.co/J3mduIS.png
Here is a shiny implementation of bootstrap-pincode-input.
Download the files bootstrap-pincode-input.js and bootstrap-pincode-input.css from here. Put them in the www subfolder of the folder containing the shiny app.
Also add the JS file below to the www subfolder, call it pincodeBinding.js.
var pincodeBinding = new Shiny.InputBinding();
$.extend(pincodeBinding, {
find: function (scope) {
return $(scope).find(".pincode");
},
getValue: function (el) {
return $(el).val();
},
setValue: function(el, value) {
$(el).val(value);
},
subscribe: function (el, callback) {
$(el).on("change.pincodeBinding", function (e) {
callback();
});
},
unsubscribe: function (el) {
$(el).off(".pincodeBinding");
},
initialize: function(el) {
var $el = $(el);
$el.pincodeInput({
inputs: $el.data("ndigits"),
hidedigits: $el.data("hide"),
complete: function(value, e, errorElement){
Shiny.setInputValue($el.attr("id"), value);
}
});
}
});
Shiny.inputBindings.register(pincodeBinding);
Now, the shiny app:
library(shiny)
pincodeInput <- function(inputId, width = "30%", height = "100px",
label = NULL, ndigits = 4, hideDigits = FALSE){
tags$div(style = sprintf("width: %s; height: %s;", width, height),
shiny:::shinyInputLabel(inputId, label),
tags$input(id = inputId, class = "pincode", type = "text",
`data-ndigits` = ndigits,
`data-hide` = ifelse(hideDigits, "true", "false")
)
)
}
ui <- fluidPage(
tags$head(
tags$link(rel = "stylesheet", href = "bootstrap-pincode-input.css"),
tags$script(src = "bootstrap-pincode-input.js"),
tags$script(src = "pincodeBinding.js")
),
br(),
pincodeInput("pincode", label = "Enter pincode"),
br(),
h3("You entered:"),
verbatimTextOutput("pincodeValue")
)
server <- function(input, output, session){
output[["pincodeValue"]] <- renderPrint({
input[["pincode"]]
})
}
shinyApp(ui, server)
Note that the pincode input only accepts digits, not alphabetical characters. I don't know whether this is what you want?
EDIT: clearable pincode input
Add this CSS file in the www folder, name it pincode-input.css:
.clearable {
padding: 1px 6px 1px 1px;
display: inline-flex;
}
.clearable span {
cursor: pointer;
color: blue;
font-weight: bold;
visibility: hidden;
margin-left: 5px;
}
Replace pincodeBinding.js with this file:
var pincodeBinding = new Shiny.InputBinding();
$.extend(pincodeBinding, {
find: function (scope) {
return $(scope).find(".pincode");
},
getValue: function (el) {
return $(el).val();
},
setValue: function(el, value) {
$(el).val(value);
},
subscribe: function (el, callback) {
$(el).on("change.pincodeBinding", function (e) {
callback();
});
},
unsubscribe: function (el) {
$(el).off(".pincodeBinding");
},
initialize: function(el) {
var $el = $(el);
var clearBtn = el.nextElementSibling;
$el.pincodeInput({
inputs: $el.data("ndigits"),
hidedigits: $el.data("hide"),
complete: function(value, e, errorElement){
Shiny.setInputValue($el.attr("id"), value);
},
change: function(){
clearBtn.style.visibility = ($el.val().length) ? "visible" : "hidden";
}
});
clearBtn.onclick = function() {
this.style.visibility = "hidden";
$el.pincodeInput().data("plugin_pincodeInput").clear();
Shiny.setInputValue($el.attr("id"), "");
};
}
});
Shiny.inputBindings.register(pincodeBinding);
The app:
library(shiny)
pincodeInput <- function(inputId, width = "30%", height = "100px",
label = NULL, ndigits = 4, hideDigits = FALSE){
tags$div(style = sprintf("width: %s; height: %s;", width, height),
shiny:::shinyInputLabel(inputId, label),
tags$span(
class = "clearable",
tags$input(id = inputId, class = "pincode", type = "text",
`data-ndigits` = ndigits,
`data-hide` = ifelse(hideDigits, "true", "false")
),
tags$span(title = "Clear", HTML("×"))
)
)
}
ui <- fluidPage(
tags$head(
tags$link(rel = "stylesheet", href = "bootstrap-pincode-input.css"),
tags$link(rel = "stylesheet", href = "pincode-input.css"),
tags$script(src = "bootstrap-pincode-input.js"),
tags$script(src = "pincodeBinding.js")
),
br(),
pincodeInput("pincode", label = "Enter pincode"),
br(),
h3("You entered:"),
verbatimTextOutput("pincodeValue")
)
server <- function(input, output, session){
output[["pincodeValue"]] <- renderPrint({
input[["pincode"]]
})
}
shinyApp(ui, server)
I am still learning about Shiny and developing my own application.
The following widget allows to select columns however the user wants (with live example):
https://shiny.rstudio.com/gallery/custom-input-control.html
UI.R
source("chooser.R")
fluidPage(
chooserInput("mychooser", "Available frobs", "Selected frobs",
row.names(USArrests), c(), size = 10, multiple = TRUE
),
verbatimTextOutput("selection")
)
server.R
function(input, output, session) {
output$selection <- renderPrint(
input$mychooser
)
}
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",
div(class="chooser-container chooser-left-container",
tags$select(class="left", size=size, multiple=multiple, leftChoices)
),
div(class="chooser-container chooser-center-container",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
),
div(class="chooser-container chooser-right-container",
tags$select(class="right", size=size, multiple=multiple, rightChoices)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
www/chooser-binding.js
(function() {
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();
Once the columns have been scrambled, we can't go back to the original order except reloading the application. I'd like to have an actionButton that resets all the columns of mychooser to their default values.
What I tried so far
UI.R
source("chooser.R")
fluidPage(
chooserInput("mychooser", "Available frobs", "Selected frobs",
row.names(USArrests), c(), size = 10, multiple = TRUE
),
actionButton(inputId = "resetcols", label = "Reset"),
verbatimTextOutput("selection")
)
server.R
function(input, output, session) {
colvalues <- row.names(USArrests)
output$selection <- renderPrint(
input$mychooser
)
eventReactive(input$resetcols, {
output$mychooser <- row.names(USArrests)
})
}
However, there's nothing happened and I don't know what can I do.
Thanks in advance
Here is the way.
Add this JS code in chooser-binding.js, e.g. after binding.setValue:
binding.receiveMessage = function (el, data) {
$(".chooser select.left").empty();
$(".chooser select.right").empty();
if(data.left !== null){
for(var i = 0; i < data.left.length; ++i){
$(".chooser select.left")
.append($("<option>" + data.left[i] + "</option>"));
}
}
if(data.right !== null){
for(var i = 0; i < data.right.length; ++i){
$(".chooser select.right")
.append($("<option>" + data.right[i] + "</option>"));
}
}
var chooser = $(el);
updateChooser(chooser);
chooser.trigger("change");
};
Define the updater for chooserInput:
updateChooserInput <- function(session, inputId, left, right){
session$sendInputMessage(inputId, list(right = right, left = left))
}
Now, here is an example:
# ui ####
ui <- fluidPage(
br(),
chooserInput("mychooser", "Available frobs", "Selected frobs",
row.names(USArrests), c(), size = 10, multiple = TRUE
),
verbatimTextOutput("selection"),
br(),
actionButton("update", "Update"),
actionButton("reset", "Reset")
)
# server ####
server <- function(input, output, session) {
output$selection <- renderPrint(
input$mychooser
)
observeEvent(input$update, {
updateChooserInput(session, "mychooser",
left = c("aaa", "bbb", "ccc"),
right = c("xxx", "yyy", "zzz"))
})
observeEvent(input$reset, {
updateChooserInput(session, "mychooser",
left = row.names(USArrests),
right = c())
})
}
shinyApp(ui, server)
I have a search box that searches states and my selections are able to transferred to another bucket. My goal is for the user to be able to press enter upon searching the state they are interested in and push their result to the selection bucket. For example, user puts New Hampshire in search box and presses enter -- New Hampshire disappears from the list of selections and is transferred to the selection bucket. Currently the user would have to double click on New Hampshire after searching to push the selection to the other box. Furthermore, New Hampshire doesn't disappear from the list of selections if it is pushed to the selected bucket.
server.R
shinyServer(function(input, output, session) {
output$main <- renderUI({
source("chooser.R")
chooserInput("mychooser","Available frobs","Selected frobs",
row.names(USArrests),c(),size=20,multiple=TRUE)})
})
ui.R
source("chooser.R")
shinyUI(fluidPage(
uiOutput("main")
))
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",style="",
div(
div(style="min-width:100px;",
tags$input(type="text",class="chooser-input-search",style="width:100px;")
)
),
div(style="display:table",
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-left-container",
style="width:100%;",
tags$select(class="left", size=size, multiple=multiple, leftChoices,style="width:100%;min-width:100px")
)
),
div(style="min-width:50px; display:table-cell;vertical-align: middle;",
div(class="chooser-container chooser-center-container",
style="padding:10px;",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
)
),
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-right-container", style="width:100%;",
tags$select(class="right", size=size, multiple=multiple, rightChoices,style="width:100%;")
)
)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
chooser-bindings.js (in www folder)
(function() {
var options = [];
jQuery.fn.filterByText = function(textbox, selectSingleMatch) {
return this.each(function() {
var select = this;
options = [];
$(select).find('option').each(function() {
options.push({value: $(this).val(), text: $(this).text()});
});
$(select).data('options', options);
$(textbox).bind('change keyup', function() {
options = $(select).empty().scrollTop(0).data('options');
var search = $.trim($(this).val());
var regex = new RegExp(search,'gi');
$.each(options, function(i) {
var option = options[i];
if(option.text.match(regex) !== null) {
$(select).append(
$('<option>').text(option.text).val(option.value)
);
}
});
if (selectSingleMatch === true &&
$(select).children().length === 1) {
$(select).children().get(0).selected = true;
}
});
});
};
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(".chooser").change(function(){
});
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
$(function() {
$('.left').filterByText($('.chooser-input-search'), true);
});
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();
As you can see this is pretty much a shameful copy and paste.
I think this works somewhat
chooser.R
chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {
leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)
if (multiple)
multiple <- "multiple"
else
multiple <- NULL
tagList(
singleton(tags$head(
tags$script(src="chooser-binding.js"),
tags$style(type="text/css",
HTML(".chooser-container { display: inline-block; }")
)
)),
div(id=inputId, class="chooser",style="",
div(
div(style="min-width:100px;",
tags$input(type="text",class="chooser-input-search",style="width:100px;")
)
),
div(style="display:table",
div(style="min-width:100px; display:table-cell;",
div(class="chooser-container chooser-left-container",
style="width:100%;",
tags$select(class="left", size=size, multiple=multiple, leftChoices,style="width:100%;min-width:100px")
)
),
div(style="min-width:50px; display:table-cell;vertical-align: middle;",
div(class="chooser-container chooser-center-container",
style="padding:10px;",
icon("arrow-circle-o-right", "right-arrow fa-3x"),
tags$br(),
icon("arrow-circle-o-left", "left-arrow fa-3x")
)
),
div(style=
"min-width:100px; display:table-cell;",
div(class="chooser-container chooser-right-container", style="width:100%;",
tags$select(class="right", size=size, multiple=multiple, rightChoices,style="width:100%;")
)
)
)
)
)
}
registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)
chooser-bindings.js
(function() {
var options = [];
jQuery.fn.filterByText = function(textbox, selectSingleMatch) {
return this.each(function() {
var select = this;
options = [];
$(select).find('option').each(function() {
options.push({value: $(this).val(), text: $(this).text()});
});
$(select).data('options', options);
$(textbox).bind('change keyup', function() {
options = $(select).empty().scrollTop(0).data('options');
var search = $.trim($(this).val());
var regex = new RegExp(search,'gi');
$.each(options, function(i) {
var option = options[i];
if(option.text.match(regex) !== null) {
$(select).append(
$('<option>').text(option.text).val(option.value)
);
}
});
if (selectSingleMatch === true &&
$(select).children().length === 1) {
$(select).children().get(0).selected = true;
}
});
});
};
function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");
var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;
leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}
function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
// Push back options to left select array
if(source == '.right'){
$.each(selected,function(i){
var sel = selected[i];
options.push({value: $(sel).val(), text: $(sel).text()});
});
}
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}
$(".chooser").change(function(){
});
$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});
$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});
$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});
var binding = new Shiny.InputBinding();
binding.find = function(scope) {
return $(scope).find(".chooser");
};
binding.initialize = function(el) {
updateChooser(el);
/*
Create separate bindings for each chooser widget
*/
$('.chooser').each(function(){
var chooser = $(this);
var left_sel = $(this).find('.left');
var right_sel = $(this).find('.right');
var search_b = $(this).find('.chooser-input-search');
// Search function
$(function() {
$(left_sel).filterByText(search_b, true);
});
//Enter binding
// If element in focus
$('.chooser-input-search').focus(function() {
$(this).keypress(function(e){
// If enter pressed
if(e.which == 13) {
if( $(search_b).val().length > 2){
// Save for debuging
var sel_options = [];
$(left_sel).find('option').each(function() {
var curr_val = $(this).val();
var curr_txt = $(this).text();
// Push to debug array
sel_options.push({value: curr_val, text: curr_txt});
// Append to tight selection
$(right_sel).append(
$('<option>').text(curr_val).val(curr_txt)
);
// Remove option
$(this).remove();
}); // end each
// Remove options from gloabl options array
$.each(options, function(i) {
var option = options[i];
$.each(sel_options,function(j){
var sel_option = sel_options[j];
if (option.value==sel_option.value && option.text==sel_option.text){
options.splice(i, 1);
}
});
});
}
}
});
});
}); // End enter keybinding
};
binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) { return e.value; }))
}
};
binding.setValue = function(el, value) {
// TODO: implement
};
binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};
binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};
binding.getType = function() {
return "shinyjsexamples.chooser";
};
Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");
})();