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
26 changes: 12 additions & 14 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- New feature: `matrix_multiple` question type — a matrix where each row allows multiple selections (checkboxes), complementing the single-selection `matrix` type (radio buttons). Usage is identical to `matrix` (same `row` and `option` parameters); each row becomes a `<question_id>_<row_id>` sub-question stored as its own column, with multiple selections pipe-joined (e.g., `"fast|cheap"`). Row shuffling, required-question handling, and session restoration work the same as for `matrix`.

- Enhancement: Sliders have been restyled. `slider` and `slider_numeric` questions now get a slim, theme-colored track with a clean circular handle and value bubble (previously ionRangeSlider shipped completely unstyled). The dense minor tick marks between positions are hidden; only the labeled major tick marks at the main breaks are shown.
- Enhancement: `sd_reactive()` no longer warns when its expression fails simply because the questions it references are not answered yet (e.g., arithmetic on a blank value at session start). Question references (`input$x`, `all_data$x`, `sd_value()`, `sd_values()`) are extracted from the expression; if any referenced value is still blank when an error occurs, the error is treated as the expected "not answered yet" state and `""` is stored quietly. Genuine errors (all referenced values filled in, or no detectable references) still warn. For this detection to work, call `sd_value()` *inside* the `sd_reactive()` expression rather than capturing values in local variables beforehand.
- Enhancement: `sd_reactive()` no longer warns when its expression fails simply because the questions it references are not answered yet (e.g., arithmetic on a blank value at session start). Question references (`input$x`, `all_data$x`, `sd_value()`, `sd_values()`) are extracted from the expression; if any referenced value is still blank when an error occurs, the error is treated as the expected "not answered yet" state and `""` is stored quietly. Genuine errors (all referenced values filled in, or no detectable references) still warn. For this detection to work, call `sd_value()` _inside_ the `sd_reactive()` expression rather than capturing values in local variables beforehand.

- Bug fix: Navigating back to and then forward again past an answered matrix (or `matrix_multiple`) question no longer crashes the survey. The matrix parent question has no input of its own, so its stored value can be `NA`; the restoration guard now handles `NA` safely instead of erroring.
- Bug fix: Multi-value answers (`mc_multiple`, `mc_multiple_buttons`, `daterange`, and range `slider_numeric`) are now correctly restored when a respondent navigates back to a page or refreshes. These answers are stored pipe-joined (e.g. `"red|blue"`), but the restoration logic was splitting on commas, so previously the inputs came back blank. They now split on the pipe to match how the values are stored.
Expand Down Expand Up @@ -50,7 +50,7 @@
# surveydown 1.1.0

- Bug fix (#246): Added a helper function that provides a vector of all preserved words that cannot be used as IDs and use it to prevent preserved IDs in `sd_store_value()`.
- New feature (#247): `sd_values()` and `sd_value()` as new approach of accessing question values, replacing the Shiny default `input$`. `sd_values()` and `sd_value()` are able to restore user inputs from db after refreshing the page. They also accept either quoted question IDs or unquoted, so that either `sd_value(fruit)` or `sd_value("fruit")` is fine. They also support multiple parameters, which return into a vector of values. For example, `sd_value(fruit, vegetable)` returns `c("apple", "lettuce")`. sd_value()` is the alias of `sd_values()`, so they function the same.
- New feature (#247): `sd_values()` and `sd_value()` as new approach of accessing question values, replacing the Shiny default `input$`. `sd_values()` and `sd_value()` are able to restore user inputs from db after refreshing the page. They also accept either quoted question IDs or unquoted, so that either `sd_value(fruit)` or `sd_value("fruit")` is fine. They also support multiple parameters, which return into a vector of values. For example, `sd_value(fruit, vegetable)` returns `c("apple", "lettuce")`. sd_value()`is the alias of`sd_values()`, so they function the same.
- New feature (#248): Option shuffling supported for `mc`, `mc_buttons`, `mc_multiple`, and `mc_multiple_buttons`. Subquestion shuffling supported for `matrix`. In YAML of `survey.qmd`, 2 new keys are available: `shuffled` and `all-shuffled`. `shuffled` is used to list the question IDs of these 5 question types to have their options/subquestions randomly shuffled. `all-shuffled` is by default `false` and can be set to `true` to apply shuffling to all questions.
- New feature (#248, continued): Indexed option shuffling. Use indexing to define what positions of options/subquestions you want to shuffle. For example, `question_id: 1-5` means to shuffle the first 5 options of the question, or the first 5 subquestions if it's a matrix question. Other supported syntax: `question_id: [1, 2, 4, 7]`, `question_id: [1-5, 8-10]`. No indexing means to shuffle all options/subquestions.
- Breaking change: The `required-questions` and `all-questions-required` YAML keys are renamed as `required` and `all-required`, respectively.
Expand All @@ -68,7 +68,7 @@

# surveydown 1.0.1

- Bug fix (#243): Updated all internal templates and roxygen examples to latest UI established in v1.0.0.
- Bug fix (#243): Updated all internal templates and roxygen examples to latest UI established in v1.0.0.
- Bug fix (#244): theme assignment with custom scss files in YAML now works properly, e.g. `theme: [united, custom.scss]`.
- Progress bar update: Now users can define `barcolor` under the `theme-settings` YAML section to customize the progress bar color, accepting both text color names (over 140 supported by browser CSS) and hex color codes in 3 or 6 digits, e.g. `#FF5733` or `#F53`. If not defined, it shows `~` as the value in `settings.yml`, indicating it's using the default setting which follows the theme.
- Documentation update: Removed the `inst/examples/` directory. Now all exported functions are documented with simple execution showcases, plus a designated template from one of the supported [templates](https://github.com/surveydown-dev).
Expand Down Expand Up @@ -111,7 +111,7 @@

# surveydown 0.13.1

- Updated citation to include *PLOS One* publication
- Updated citation to include _PLOS One_ publication

# surveydown 0.13.0

Expand Down Expand Up @@ -173,7 +173,7 @@
# surveydown 0.11.0

- Questions can now be defined using an external yml file, defaulting to `"questions.yml"`.
- New `yml` argument in `sd_question()`, defaults to `"questions.yml"`, to allow users which yml file to use for questions. For details, refer to the [Defining Questions](https://surveydown.org/docs/defining-questions) documentation page.
- New `yml` argument in `sd_question()`, defaults to `"questions.yml"`, to allow users which yml file to use for questions. For details, refer to the [Defining Questions](https://surveydown.org/docs/defining-questions) documentation page.
- Update to `sd_create_survey()`: now the the `template` argument is by default `"default"`.
- Update to `sd_create_survey()`: new `template = "questions_yml"` option to create a survey with the `questions_yml` template.

Expand Down Expand Up @@ -282,9 +282,9 @@

# surveydown 0.4.1

- Modified survey rendering to move all rendered files into "_survey" folder.
- Export survey question metadata to "_survey/questions.yml" file (see #132).
- Survey content is now extracted and saved to '_survey/pages.rds', '_survey/head.rds', and '_survey/questions.yml' files for faster loading.
- Modified survey rendering to move all rendered files into "\_survey" folder.
- Export survey question metadata to "\_survey/questions.yml" file (see #132).
- Survey content is now extracted and saved to '\_survey/pages.rds', '\_survey/head.rds', and '\_survey/questions.yml' files for faster loading.
- Survey will load content from stored files if no changes detected in 'survey.qmd' or 'app.R' files.

# surveydown 0.4.0
Expand Down Expand Up @@ -331,9 +331,9 @@
# surveydown 0.3.3

- Enhance: `sd_server()` now has a new parameter called `auto_scroll`. It's default to `TRUE`, which enables auto scrolling that tracks the user's input, can be turned off by changing to `FALSE`. Thanks to the contribution from [Zain Hoda](https://github.com/zainhoda1).
- Enhance: `sd_question()` now has the `"matrix"` type.
- Enhance: `sd_question()` now has the `"matrix"` type.
- Enhance: Asterisk, as an indication of required questions, is now moved to the top right corner of question containers.
- Enhance: Replaced the default shiny alert with `sweetalert`.
- Enhance: Replaced the default shiny alert with `sweetalert`.

# surveydown 0.3.2

Expand Down Expand Up @@ -419,12 +419,12 @@
- Removed `sd_admin_ui()` and `sd_add_admin()` from the package.
- Moved the surveydown.css and page_nav.js files into the R package and out of the Quarto extension.
- Added keep alive functionality to the survey.
- Bug fixes:
- Bug fixes:
- The admin page password was looking for the `SUPABASE_PASSWORD` environment variable, but it should be `SURVEYDOWN_PASSWORD`.
- The data fetching was not working to download the data as a CSV file in the admin page, now uses `sd_get_data()` and works.
- The `show_if_custom` logic was not working for multiple conditions, now it does.
- The `skip_if_custom` logic could error if a condition was `NULL` (fixed with `isTRUE()`).
- In `sd_config()`, items were being assigned with the ` <- ` operator, causing them to be deleted when the thing being assigned was `NULL`. Now changed to ` = `.
- In `sd_config()`, items were being assigned with the `<-` operator, causing them to be deleted when the thing being assigned was `NULL`. Now changed to `=`.

# surveydown 0.1.0

Expand Down Expand Up @@ -467,7 +467,6 @@
- (extension) Now the `example.qmd` survey has instructions for supabase configuration and shinyapps deployment.
- (extension) Now the `.gitignore` file has `.Renviron` included. This file will store supabase password and is essential for shinyapps deployment. Eliminating this file from pushing to GitHub will ensure that your password is only saved locally.


# surveydown 0.0.5

- `create_survey()` changed to `sd_create_survey()` for function name consistency.
Expand All @@ -483,7 +482,6 @@
- (extension) Options of `mc_button` and `mu_multiple_buttons` types of questions are now centered.
- Now the `surveydown::create_survey()` function will download the whole extension repo, containing the extension, an example survey, and an RStudio project.


# surveydown 0.0.3

- Now `show_if` can be applied to multiple options of the same question, which allows users to have more than one option in a question that triggers the revealing of a hidden question.
Expand Down
18 changes: 17 additions & 1 deletion R/config.R
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,21 @@ extract_question_structure_html <- function(html_content) {
}
}

# Extract options for ranking questions. Ranking lists are hand-built markup,
# so they carry none of the Shiny input classes the option
# extraction below keys off.
if(grepl("sd-ranking-list", type, fixed = TRUE)){
item_nodes <- rvest::html_nodes(question_node, ".sd-ranking-item")
if(length(item_nodes) > 0){
options <- rvest::html_attr(item_nodes, "data-value")
# Read the label span, not the whole <li>: the item also contains
# the drag handle, whose glyph would otherwise end up in the label
label_nodes <- rvest::html_element(item_nodes, ".sd-ranking-label")
names(options) <- rvest::html_text(label_nodes, trim = TRUE)
question_structure[[question_id]]$options <- as.list(options)
}
}

# Extract options for the question ( mc, *_multiple, *_buttons, and select)
if (length(type) > 0 && grepl("radio|checkbox|select|matrix", type)) {
if (is_matrix) {
Expand Down Expand Up @@ -1855,7 +1870,8 @@ write_question_structure_yaml <- function(question_structure, file_yaml) {
'js-range-slider sw-slider-text' = 'slider',
'js-range-slider' = 'slider_numeric',
'shiny-date-input form-group shiny-input-container' = 'date',
'shiny-date-range-input form-group shiny-input-container' = 'daterange'
'shiny-date-range-input form-group shiny-input-container' = 'daterange',
'sd-ranking-list' = 'ranking'
)

# Add index to each question (1-based, for display order tracking)
Expand Down
48 changes: 47 additions & 1 deletion R/question_types.R
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ qt_render_daterange <- function(a, ...) {
id
)

shiny::tagAppendChild(
shiny::tagAppendChild(
output,
shiny::tags$script(htmltools::HTML(js_init))
)
Expand Down Expand Up @@ -473,6 +473,38 @@ qt_render_matrix <- function(a, ...) {
})
}

qt_render_ranking <- function(a, ...){
with(a, {
# Hand-built markup rather than a Shiny input widget: the value is
# reported by ranking.js via Shiny.setInputValue() as the drag order
# changes (same approach the slider renderer uses for its label ->
# value remapping).
labels <- names(choice_html(option))
items <- lapply(seq_along(option), function(i) {
shiny::tags$li(
class = "sd-ranking-item",
`data-value` = unname(option[i]),
shiny::tags$span(
class = "sd-ranking-label",
shiny::HTML(labels[i])
),
shiny::tags$span(
class = "sd-ranking-handle",
`aria-hidden` = "true",
"\u2261"
)
)
})

shiny::div(
class = "sd-ranking-container",
shiny::tags$label(class = "control-label", label),
shiny::tags$ul(id = id, class = "sd-ranking-list", items)
)

})
}

# -- Restorers --------------------------------------------------------------

qt_restore_mc <- function(session, id, value, info) {
Expand Down Expand Up @@ -589,6 +621,15 @@ qt_restore_daterange <- function(session, id, value, info) {
}
}

qt_restore_ranking <- function(session, id, value, info){
# Stored pipe-joined; as.list() keeps it a JSON array even for one item
order <- split_stored_value(value)
session$sendCustomMessage(
"restoreRankingOrder",
list(id = id, order = as.list(order))
)
}

# -- Registry ----------------------------------------------------------------

question_type_registry <- list(
Expand Down Expand Up @@ -675,5 +716,10 @@ question_type_registry <- list(
render = qt_render_matrix,
restore = NULL,
requires_option = TRUE
),
ranking = list(
render = qt_render_ranking,
restore = qt_restore_ranking,
requires_option = TRUE
)
)
3 changes: 2 additions & 1 deletion R/ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ extract_head_content <- function(html_content) {
#' `"mc"`, `"mc_multiple"`, `"mc_buttons"`, `"mc_multiple_buttons"`,
#' `"mc_image"`, `"mc_multiple_image"`, `"text"`, `"textarea"`, `"numeric"`,
#' `"slider"`, `"slider_numeric"`, `"date"`, `"daterange"`, `"matrix"`, and
#' `"matrix_multiple"`. Defaults to `NULL`.
#' `"matrix_multiple"`, `"ranking"`. Defaults to `NULL`.
#' @param label Character string. The label for the UI element, which can be
#' formatted with markdown. Defaults to `NULL`
#' @param cols Integer. Number of columns for the `"textarea"` question type.
Expand Down Expand Up @@ -789,6 +789,7 @@ extract_head_content <- function(html_content) {
#' selection per row, radio buttons)
#' - `"matrix_multiple"`: Matrix-style question where each row allows
#' multiple selections (checkboxes)
#' - `"ranking"`: Ranking question where respondents can order options by preference
#'
#' For `"matrix"` and `"matrix_multiple"` type questions, use the `row`
#' parameter to define the rows of the matrix. Each element in the `row`
Expand Down
59 changes: 59 additions & 0 deletions inst/css/surveydown.css
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,65 @@ a:hover {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-color) 35%, transparent);
}

/* Ranking drag to reorder */
.sd-ranking-list{
list-style: none;
padding : 0;
margin: 0.5rem 0 0 0;
counter-reset: sd-rank;
}

.sd-ranking-item{
display:flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
margin-bottom: 6px;
border: 1px solid #ddd;
border-radius: 6px;
background-color: #fff;
user-select: none;
--webkit-user-select: none;
cursor: grab;
}

/* Position number via CSS counter, so a reorder renumbers for free with no JS bookkeeping. */
.sd-ranking-item::before{
counter-increment: sd-rank;
content: counter(sd-rank);
flex: 0 0 1.75em;
height: 1.75em;
line-height: 1.75em;
text-align: center;
border-radius: 50%;
background-color: var(--theme-color);
color: #fff;
font-size: 0.85em;
}

/* Only the handle disables touch scrolling (see the pointerdown guard in ranking.js),
so swiping over the body of a long list still scrolls the page on mobile.
*/
.sd-ranking-handle{
touch-action: none;
cursor: grab;
margin-left: auto;
padding: 0 4px;
color: #999;
line-height: 1;
}

.sd-ranking-item:hover{
border-color: var(--theme-color);
}

.sd-ranking-dragging{
cursor: grabbing;
opacity: 0.65;
border-color: var(--theme-color);
box-shadow: 0 2px 8px rgba(0,0,0, 0.15);
}

/* Survey mode banner (preview mode / database not connected) */
.sd-mode-banner {
position: fixed;
Expand Down
Loading