Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions R/module-expdes-server.R
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ expdesServer <- function(input, output, session, parent_session, loadpage_input,

tagList(
h4("Dose response power analysis"),
selectizeInput(ns(NAMESPACE_EXPDES$protein_select),
label = h5("Select protein template"),
choices = protein_choices,
options = list(placeholder = "Search protein..."),
multiple = FALSE),
copyable_select(
selectizeInput(ns(NAMESPACE_EXPDES$protein_select),
label = h5("Select protein template"),
choices = protein_choices,
options = list(placeholder = "Search protein..."),
multiple = FALSE),
"Copy protein name"),
sliderInput(ns(NAMESPACE_EXPDES$rep_range),
"Replicates per dose",
min = 1, max = 10, value = c(1, 5), step = 1),
Expand Down
34 changes: 22 additions & 12 deletions R/qc-server-plots.R
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ register_qc_plots <- function(input, output, session, loadpage_input, get_data,
label = h5("Quality metric"),
choices = metric_cols,
selected = metric_cols[1]),
selectizeInput(ns("qm_protein"),
label = h5("Show plot for"),
choices = c("", protein_choices))
copyable_select(
selectizeInput(ns("qm_protein"),
label = h5("Show plot for"),
choices = c("", protein_choices)),
"Copy analyte name")
)
})

Expand All @@ -70,21 +72,29 @@ register_qc_plots <- function(input, output, session, loadpage_input, get_data,
error = function(e) NULL)
if ((loadpage_input()$BIO!="PTM" && input$qc_page_plot_type == "QCPlot")) {
req(length(protein_choices) > 0)
selectizeInput(ns("which_protein_for_data_process_plots"), "Show plot for",
choices = c("", "ALL ANALYTES" = "allonly", protein_choices))
copyable_select(
selectizeInput(ns("which_protein_for_data_process_plots"), "Show plot for",
choices = c("", "ALL ANALYTES" = "allonly", protein_choices)),
"Copy analyte name")
} else if (loadpage_input()$BIO == "PTM"){
if (input$qc_page_plot_type == "QCPlot"){
selectizeInput(ns("which_protein_for_data_process_plots"), "Show plot for",
choices = c("", "ALL ANALYTES" = "allonly",
unique(get_data()$PTM$ProteinName)))
copyable_select(
selectizeInput(ns("which_protein_for_data_process_plots"), "Show plot for",
choices = c("", "ALL ANALYTES" = "allonly",
unique(get_data()$PTM$ProteinName))),
"Copy analyte name")
} else {
selectizeInput(ns("which_protein_for_data_process_plots"), "Show plot for",
choices = c("", unique(get_data()$PTM$ProteinName)))
copyable_select(
selectizeInput(ns("which_protein_for_data_process_plots"), "Show plot for",
choices = c("", unique(get_data()$PTM$ProteinName))),
"Copy analyte name")
}
} else {
req(length(protein_choices) > 0)
selectizeInput(ns("which_protein_for_data_process_plots"), "Show plot for",
choices = c("", protein_choices))
copyable_select(
selectizeInput(ns("which_protein_for_data_process_plots"), "Show plot for",
choices = c("", protein_choices)),
"Copy analyte name")
}
})

Expand Down
8 changes: 6 additions & 2 deletions R/qc-server-sidebar.R
Original file line number Diff line number Diff line change
Expand Up @@ -304,10 +304,14 @@ register_qc_sidebar_options <- function(input, output, session, loadpage_input,
}

if (input$standards == "Proteins") {
selectizeInput(ns("names"), "choose standard", unique(get_data()$ProteinName), multiple = TRUE)
copyable_select(
selectizeInput(ns("names"), "choose standard", unique(get_data()$ProteinName), multiple = TRUE),
"Copy standard name")
}
else if (input$standards == "Peptides") {
selectizeInput(ns("names"), "choose standard", unique(get_data()$PeptideSequence), multiple = TRUE)
copyable_select(
selectizeInput(ns("names"), "choose standard", unique(get_data()$PeptideSequence), multiple = TRUE),
"Copy standard name")
}

})
Expand Down
8 changes: 5 additions & 3 deletions R/statmodel-server-visualization.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ render_group_comparison_plot_inputs = function(output, session, rownames, get_da
proteins = tryCatch(unique(as.character(preprocess_data()$ProteinLevelData$Protein)),
error = function(e) NULL)
req(length(proteins) > 0)
selectInput(ns(NAMESPACE_STATMODEL$visualization_which_protein),
label = h4("which analyte to plot"),
proteins)
copyable_select(
selectInput(ns(NAMESPACE_STATMODEL$visualization_which_protein),
label = h4("which analyte to plot"),
proteins),
"Copy analyte name")
})

output[[NAMESPACE_STATMODEL$visualization_plot_options_conditional_panel]] = renderUI({
Expand Down
88 changes: 88 additions & 0 deletions R/utils-copyable-select.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# ============================================================================
# Copy-to-clipboard wrapper for protein-name dropdowns
# ============================================================================

#' HTML dependency carrying the copy-button assets.
#'
#' Delivered as a dependency rather than as `tags$link` / `tags$script` in the
#' page headers for two reasons: every call site renders inside `renderUI()`,
#' and `expdesUI()` does not load `assets/style.css` at all, so a header-tag
#' approach would silently skip the experimental design dropdown.
#'
#' @noRd
copyable_select_dependency <- function() {
htmltools::htmlDependency(
name = "msstatsshiny-copyable-select",
version = as.character(utils::packageVersion("MSstatsShiny")),
src = c(file = system.file("assets", package = "MSstatsShiny")),
script = "copy-select.js",
stylesheet = "copy-select.css"
)
}

#' Count `select` elements in a tag tree.
#'
#' Used only to enforce the one-select contract of [copyable_select()].
#'
#' @noRd
count_select_tags <- function(x) {
if (inherits(x, "shiny.tag")) {
return(as.integer(identical(x$name, "select")) + count_select_tags(x$children))
}
if (is.list(x)) {
return(sum(vapply(x, count_select_tags, integer(1))))
}
0L
}

#' Add a copy-to-clipboard button to a select input.
#'
#' Wraps an already-built [shiny::selectInput()] or [shiny::selectizeInput()]
#' so the user can copy the selected protein name out of the dropdown.
#' Selectize renders the current selection as a `div`, which browsers will not
#' let the user select as text, so a button is the only reliable affordance.
#'
#' This is a pure wrapper: the input keeps its id, choices, label and options
#' untouched. The button finds its select by DOM traversal, so the namespaced
#' `inputId` does not need to be repeated here.
#'
#' Wrap one select, not a `tagList` of several. The button has no way to know
#' which of several selects it belongs to, so that case is rejected rather
#' than silently binding to the first one.
#'
#' @param select_tag A select input tag, as returned by `selectInput()` or
#' `selectizeInput()`.
#' @param tooltip Hover text for the button, and the text announced to screen
#' readers as the button's name.
#'
#' @return A `div` containing `select_tag`, the copy button, and the JS/CSS
#' dependency.
#'
#' @noRd
copyable_select <- function(select_tag, tooltip = "Copy name") {
n_selects <- count_select_tags(select_tag)
if (n_selects != 1L) {
stop("copyable_select() needs exactly one select input, got ", n_selects,
". Wrap the individual selectInput()/selectizeInput(), not a tagList.",
call. = FALSE)
}

button <- tags$button(
type = "button",
class = "copyable-select-btn",
`aria-label` = tooltip,
icon("copy", lib = "font-awesome"),
# span, not div: a button's content model is phrasing content only.
# role/aria-live let the flashed result ("Copied") reach screen readers,
# which the static aria-label above cannot.
tags$span(tooltip,
class = "copyable-select-tip",
role = "status",
`aria-live` = "polite")
)

htmltools::attachDependencies(
div(class = "copyable-select", select_tag, button),
copyable_select_dependency()
)
}
80 changes: 80 additions & 0 deletions inst/assets/copy-select.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* Copy-to-clipboard affordance for protein-name dropdowns.
Paired with assets/copy-select.js; both are delivered as one
htmlDependency by copyable_select() in R/utils-copyable-select.R.

Deliberately self-contained: it must not lean on assets/style.css, because
expdesUI() never loads that file. Hence the tooltip rules below duplicate
the .icon-tooltip pattern rather than extending it. */

.copyable-select {
display: flex;
align-items: flex-end;
gap: 6px;
}

/* The wrapped shiny input keeps all the horizontal room it had before. */
.copyable-select > .form-group,
.copyable-select > .shiny-input-container {
flex: 1 1 auto;
min-width: 0;
margin-bottom: 15px;
}

/* flex-end aligns margin boxes, so the button carries the same bottom margin
as the form-group to sit level with the select rather than 15px below it.
Aligning on the bottom edge keeps the button level no matter how tall the
label is - call sites use h4(), h5() and bare strings. */
.copyable-select-btn {
position: relative;
flex: 0 0 auto;
margin-bottom: 15px;
height: 34px;
width: 34px;
padding: 0;
line-height: 1;
cursor: pointer;
color: #333;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
}

.copyable-select-btn:hover:not(:disabled) {
background-color: #e6e6e6;
}

.copyable-select-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}

.copyable-select-tip {
display: block;
visibility: hidden;
opacity: 0;
transition: opacity 0.3s;
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
z-index: 999;
margin-bottom: 6px;
padding: 5px 10px;
border-radius: 6px;
background-color: #333;
color: #fff;
font-size: 10px;
font-weight: normal;
text-align: center;
white-space: nowrap;
pointer-events: none;
}

/* :focus-visible so keyboard users get the same hint as hover; the flash
class covers the post-click result for both. */
.copyable-select-btn:hover:not(:disabled) .copyable-select-tip,
.copyable-select-btn:focus-visible .copyable-select-tip,
.copyable-select-btn.is-flashing .copyable-select-tip {
visibility: visible;
opacity: 1;
}
Loading
Loading