├── .Rbuildignore
├── .gitattributes
├── .github
├── .gitignore
└── workflows
│ ├── R-CMD-check.yaml
│ └── routine.yaml
├── .gitignore
├── .vscode
└── tasks.json
├── CRAN-SUBMISSION
├── DESCRIPTION
├── LICENSE
├── LICENSE.md
├── NAMESPACE
├── NEWS.md
├── R
├── assertions.R
├── bucket_list.R
├── dependencies.R
├── incrementor.R
├── js.R
├── label_ids.R
├── methods.R
├── question_rank.R
├── rank_list.R
├── sortable-package.R
├── sortable_js.R
├── sortable_options.R
└── zzz.R
├── README.Rmd
├── README.md
├── _pkgdown.yml
├── codecov.yml
├── cran-comments.md
├── inst
├── WORDLIST
├── examples
│ ├── example_bucket_list.R
│ ├── example_question_rank.R
│ ├── example_rank_list.R
│ ├── example_rank_list_multidrag.R
│ ├── example_rank_list_swap.R
│ ├── example_sortable_js.R
│ └── example_sortable_js_capture.R
├── htmlwidgets
│ ├── lib
│ │ └── sortable
│ │ │ └── sortable.js
│ ├── plugins
│ │ └── sortable-rstudio
│ │ │ ├── .gitignore
│ │ │ ├── _colors.scss
│ │ │ ├── bucket_list.css
│ │ │ ├── bucket_list.scss
│ │ │ ├── rank_list.css
│ │ │ ├── rank_list.scss
│ │ │ └── rank_list_binding.js
│ ├── sortable.js
│ └── sortable.yaml
├── shiny
│ ├── bucket_list
│ │ └── app.R
│ ├── clone_remove
│ │ └── app.R
│ ├── custom_css
│ │ └── app.R
│ ├── drag_vars_to_plot
│ │ └── app.R
│ ├── group_list
│ │ └── app.R
│ ├── horizontal
│ │ └── app.R
│ ├── rank_list
│ │ └── app.R
│ ├── shiny_tabset
│ │ └── app.R
│ ├── update_bucket_list
│ │ └── app.R
│ ├── update_rank_list_method
│ │ └── app.R
│ └── update_rank_list_ui
│ │ └── app.R
└── tutorials
│ └── question_rank
│ └── question_rank.Rmd
├── logo.svg
├── man-roxygen
└── options.R
├── man
├── add_rank_list.Rd
├── bucket_list.Rd
├── chain_js_events.Rd
├── figures
│ ├── README-unnamed-chunk-4-1.png
│ ├── README-unnamed-chunk-5-1.png
│ ├── bucket_list_shiny.gif
│ ├── diagrammer.gif
│ ├── lifecycle-archived.svg
│ ├── lifecycle-defunct.svg
│ ├── lifecycle-deprecated.svg
│ ├── lifecycle-experimental.svg
│ ├── lifecycle-maturing.svg
│ ├── lifecycle-questioning.svg
│ ├── lifecycle-retired.svg
│ ├── lifecycle-soft-deprecated.svg
│ ├── lifecycle-stable.svg
│ ├── logo.png
│ ├── logo.svg
│ ├── rank_list_shiny.gif
│ ├── simple_sortable_shiny.gif
│ └── sortable-logo.png
├── is_sortable_options.Rd
├── question_rank.Rd
├── rank_list.Rd
├── render_sortable.Rd
├── sortable.Rd
├── sortable_js.Rd
├── sortable_js_capture_input.Rd
├── sortable_options.Rd
├── sortable_output.Rd
├── update_bucket_list.Rd
└── update_rank_list.Rd
├── pkgdown
└── favicon
│ ├── apple-touch-icon-120x120.png
│ ├── apple-touch-icon-152x152.png
│ ├── apple-touch-icon-180x180.png
│ ├── apple-touch-icon-60x60.png
│ ├── apple-touch-icon-76x76.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ └── favicon.ico
├── scripts
├── build_docs.R
├── compile_css.R
├── deploy_apps.R
├── download_sortablejs.R
├── load_all_shim.R
└── readme.md
├── sortable.Rproj
├── tests
├── spelling.R
├── testthat.R
└── testthat
│ ├── test-bucket_list.R
│ ├── test-creation.R
│ ├── test-htmltools.R
│ ├── test-js.R
│ ├── test-label_ids.R
│ ├── test-learnr-question_rank.R
│ └── test-rank_list.R
└── vignettes
├── .gitignore
├── built_in.Rmd
├── cloning.Rmd
├── figures
├── clone_delete.gif
├── drag_vars_to_plot.gif
├── simple_rank_list.gif
└── sortable_tabs.gif
├── novel_solutions.Rmd
├── understanding_sortable_js.Rmd
├── updating_rank_list.Rmd
└── using_custom_css.Rmd
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^.*\.Rproj$
2 | ^\.Rproj\.user$
3 | ^LICENSE\.md$
4 |
5 | ^Readme\.md$
6 | ^README\.Rmd$
7 | ^README\.html$
8 |
9 | ^\.travis\.yml$
10 | ^\.lintr$
11 |
12 | ^doc$
13 | ^Meta$
14 | ^scripts/
15 | ^man-roxygen/
16 | ^docs$
17 |
18 | ^_pkgdown\.yml$
19 | ^pkgdown$
20 |
21 | ^codecov\.yml$
22 | ^logo.svg$
23 |
24 | ^inst/tutorials/.*/rsconnect/*
25 | ^inst/shiny/.*/rsconnect/*
26 | ^inst/tutorials/.*\\.html$
27 |
28 | ^.*\.scss$
29 | ^cran-comments\.md$
30 | ^CRAN-RELEASE$
31 | ^\.github$
32 | ^CRAN-SUBMISSION$
33 |
34 | revdep/*
35 | ^.sass_cache_keys$
36 | inst/htmlwidgets/plugins/sortable-rstudio/.sass_cache_keys
37 | sortable/man-roxygen
38 | sortable/scripts
39 | ^revdep$
40 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.html linguist-documentation=true
2 | docs/* linguist-documentation=true
3 | inst/htmlwidgets/lib/sortable/* linguist-vendored
4 |
--------------------------------------------------------------------------------
/.github/.gitignore:
--------------------------------------------------------------------------------
1 | *.html
2 |
--------------------------------------------------------------------------------
/.github/workflows/R-CMD-check.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/rstudio/shiny-workflows
2 | #
3 | # NOTE: This Shiny team GHA workflow is overkill for most R packages.
4 | # For most R packages it is better to use https://github.com/r-lib/actions
5 | on:
6 | push:
7 | branches: [main, rc-**]
8 | pull_request:
9 | branches: [main]
10 | # schedule:
11 | # - cron: '0 8 * * 1' # every monday
12 |
13 | name: Package checks
14 |
15 | jobs:
16 | website:
17 | uses: rstudio/shiny-workflows/.github/workflows/website.yaml@v1
18 | R-CMD-check:
19 | uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1
20 |
21 | deploy-tutorials:
22 | name: Deploy tutorials
23 | if: github.repository == 'rstudio/sortable' && github.event_name == 'push'
24 | needs: R-CMD-check
25 | runs-on: ubuntu-latest
26 | env:
27 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
28 | SHINYAPPS_NAME: ${{ secrets.SHINYAPPS_NAME }}
29 | SHINYAPPS_TOKEN: ${{ secrets.SHINYAPPS_TOKEN }}
30 | SHINYAPPS_SECRET: ${{ secrets.SHINYAPPS_SECRET }}
31 | steps:
32 | - uses: actions/checkout@v2
33 |
34 | - uses: rstudio/shiny-workflows/setup-r-package@v1
35 | with:
36 | extra-packages: |
37 | remotes
38 | rsconnect
39 | glue
40 | withr
41 |
42 | - name: Install package from GitHub
43 | shell: Rscript {0}
44 | run: |
45 | remotes::install_github("rstudio/sortable")
46 |
47 | - name: Deploy tutorials
48 | continue-on-error: true
49 | shell: Rscript {0}
50 | run: source("scripts/deploy_apps.R")
51 |
52 | - name: Returns a success even if the deployment step fails.
53 | run: exit 0
54 |
--------------------------------------------------------------------------------
/.github/workflows/routine.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/rstudio/shiny-workflows
2 | #
3 | # NOTE: This Shiny team GHA workflow is overkill for most R packages.
4 | # For most R packages it is better to use https://github.com/r-lib/actions
5 | on:
6 | pull_request:
7 | branches: [main]
8 |
9 | name: Routine package checks
10 |
11 | jobs:
12 | routine:
13 | uses: rstudio/shiny-workflows/.github/workflows/routine.yaml@v1
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .Rproj.user
2 | .Rhistory
3 | .RData
4 | docs/
5 | Meta
6 | README.html
7 | doc
8 | inst/doc
9 | inst/tutorials/*.html
10 | vignettes/*.html
11 | vignettes/*.R
12 |
13 | inst/tutorials/*/rsconnect/*
14 | inst/shiny/*/rsconnect/*
15 | inst/tutorials/*/*.html
16 | /doc/
17 | /Meta/
18 |
19 | revdep/*
20 |
21 | /.quarto/
22 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "rPackageTask",
6 | "problemMatcher": [],
7 | "label": "R: Check R package",
8 | "group": {
9 | "kind": "build",
10 | "isDefault": true
11 | }
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/CRAN-SUBMISSION:
--------------------------------------------------------------------------------
1 | Version: 0.5.0
2 | Date: 2023-03-26 12:26:41 UTC
3 | SHA: 6ce650d0ea9930bc00b300efd44d70a21ed7e775
4 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Type: Package
2 | Package: sortable
3 | Title: Drag-and-Drop in 'shiny' Apps with 'SortableJS'
4 | Version: 0.5.0.9000
5 | Authors@R:
6 | c(person(given = "Andrie",
7 | family = "de Vries",
8 | role = c("cre", "aut"),
9 | email = "apdevries@gmail.com"),
10 | person(given = "Barret",
11 | family = "Schloerke",
12 | role = "aut",
13 | email = "barret@rstudio.com"),
14 | person(given = "Kenton",
15 | family = "Russell",
16 | role = c("aut", "ccp"),
17 | email = "kent.russell@timelyportfolio.com",
18 | comment = "Original author"),
19 | person("RStudio", role = c("cph", "fnd")),
20 | person(given = "Lebedev",
21 | family = "Konstantin",
22 | role = "cph",
23 | comment = "'SortableJS', https://sortablejs.github.io/Sortable/"))
24 | Description: Enables drag-and-drop behaviour in Shiny apps, by exposing the
25 | functionality of the 'SortableJS'
` tag, so not strictly speaking a header.) Note
44 | #' that you must explicitly provide `header` argument, especially in the case
45 | #' where you want the header to be empty - to do this use `header = NULL` or
46 | #' `header = NA`.
47 | #'
48 | #' @param ... One or more specifications for a rank list, and must be defined by
49 | #' [add_rank_list].
50 | #'
51 | #' @param class A css class applied to the bucket list and rank lists. This can
52 | #' be used to define custom styling.
53 | #'
54 | #' @param group_name Passed to `SortableJS` as the group name. Also the input
55 | #' value set in Shiny. (`input[[group_name]]`). Items can be dragged between
56 | #' bucket lists which share the same group name.
57 | #'
58 | #' @param group_put_max Not yet implemented
59 | #'
60 | #' @param orientation Either `horizontal` or `vertical`, and specifies the
61 | #' layout of the components on the page.
62 | #'
63 | #' @param css_id This is the css id to use, and must be unique in your shiny
64 | #' app. This defaults to the value of `group_id`, and will be appended to the
65 | #' value "bucket-list-container", to ensure the CSS id is unique for the
66 | #' container as well as the embedded rank lists.
67 | #'
68 | #' @return A list with class `bucket_list`
69 | #' @seealso [rank_list], [update_rank_list]
70 | #' @export
71 | #'
72 | #' @example inst/examples/example_bucket_list.R
73 | #' @examples
74 | #' ## Example of a shiny app
75 | #' if (interactive()) {
76 | #' app <- system.file(
77 | #' "shiny/bucket_list/app.R",
78 | #' package = "sortable"
79 | #' )
80 | #' shiny::runApp(app)
81 | #' }
82 | bucket_list <- function(
83 | header = NULL,
84 | ...,
85 | group_name,
86 | css_id = group_name,
87 | group_put_max = rep(Inf, length(labels)),
88 | options = sortable_options(),
89 | class = "default-sortable",
90 | orientation = c("horizontal", "vertical")
91 | ) {
92 |
93 | assert_that(is_header(header))
94 | if (isTRUE(is.na(header))) header <- NULL
95 |
96 | assert_that(is_sortable_options(options))
97 | if (missing(group_name) || is.null(group_name)) {
98 | group_name <- increment_bucket_group()
99 | }
100 |
101 | orientation <- match.arg(orientation)
102 |
103 | class <- paste(class, collapse = " ")
104 |
105 | # capture the dots
106 | rlang::check_dots_unnamed()
107 | dot_vals <- rlang::list2(...)
108 |
109 | # Remove any NULL elements
110 | dot_vals <- dot_vals[!vapply(dot_vals, is.null, FUN.VALUE = logical(1))]
111 |
112 | # modify the dots by adding the group_name to the sortable options
113 | dots <- lapply(seq_along(dot_vals), function(i) {
114 | dot <- dot_vals[[i]]
115 | assert_that(is_add_rank_list(dot))
116 |
117 | if (is.null(dot$css_id)) {
118 | dot$css_id <- increment_rank_list()
119 | }
120 | modifyList(
121 | dot,
122 | val = list(
123 | options = sortable_options(group = group_name),
124 | class = paste(class, paste0("column_", i))
125 | )
126 | )
127 | })
128 |
129 | css_ids <- vapply(dots, function(dot) dot$css_id, character(1))
130 | input_ids <- vapply(dots, function(dot) dot$input_id, character(1))
131 |
132 | set_bucket <- sortable_js_capture_bucket_input(group_name, input_ids, css_ids)
133 | display_empty_class <- sortable_js_set_empty_class(css_ids)
134 |
135 | dots <- lapply(dots, function(dot) {
136 | dot$options$onLoad <- chain_js_events(set_bucket, dot$options$onLoad) # nolint
137 | dot$options$onSort <- chain_js_events(set_bucket, dot$options$onSort) # nolint
138 | dot$options$onMove <- chain_js_events(dot$options$onMove, display_empty_class) # nolint
139 | dot
140 | })
141 |
142 | # construct list rank_list objects
143 | sortables <- lapply(dots, function(dot) do.call(rank_list, dot))
144 |
145 | title_tag <-
146 | if (!is.null(header)) {
147 | tags$p(class = "bucket-list-header", header)
148 | } else {
149 | NULL
150 | }
151 |
152 | z <- tagList(
153 | tags$div(
154 | class = paste("bucket-list-container", class),
155 | id = as_bucket_list_id(css_id),
156 | title_tag,
157 | tags$div(
158 | class = paste(class, "bucket-list",
159 | paste0("bucket-list-", orientation)
160 | ),
161 | sortables
162 | )
163 | ),
164 | bucket_list_dependencies()
165 | )
166 |
167 | as_bucket_list(z)
168 | }
169 |
--------------------------------------------------------------------------------
/R/dependencies.R:
--------------------------------------------------------------------------------
1 | css_dependency <- function(name, files, scripts = NULL) {
2 | list(
3 | htmltools::htmlDependency(
4 | name,
5 | version = utils::packageVersion("sortable"),
6 | src = "htmlwidgets/plugins/sortable-rstudio",
7 | package = "sortable",
8 | stylesheet = files,
9 | script = scripts
10 | )
11 | )
12 | }
13 |
14 | rank_list_dependencies <- function() {
15 | css_dependency(
16 | "sortable-rank-list",
17 | files = "rank_list.css",
18 | scripts = "rank_list_binding.js"
19 | )
20 | }
21 |
22 | bucket_list_dependencies <- function() {
23 | append(
24 | rank_list_dependencies(),
25 | css_dependency("sortable-bucket-list", "bucket_list.css")
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/R/incrementor.R:
--------------------------------------------------------------------------------
1 | incrementor <- function(prefix = "increment_"){
2 | i <- 0
3 | function(){
4 | i <<- i + 1
5 | paste0(prefix, i)
6 | }
7 | }
8 |
9 | increment_rank_list <- incrementor("rank_list_id_")
10 |
11 | increment_bucket_list <- incrementor("bucket_list_id_")
12 |
13 | increment_bucket_group <- incrementor("bucket_group_")
14 |
15 | increment_rank_list_input_id <- incrementor("rank_list_shiny_")
16 |
--------------------------------------------------------------------------------
/R/js.R:
--------------------------------------------------------------------------------
1 | get_child_id_or_text_js_fn <- function() {
2 | paste0(collapse = "\n",
3 | "function(child) {",
4 | " return ",
5 | # use child element attribute 'data-rank-id'
6 | " $(child).attr('data-rank-id') || ",
7 | # otherwise return the inner text of the element
8 | # use inner text vs `.text()` to avoid extra white space
9 | " $.trim(child.innerText);",
10 | "}"
11 | )
12 | }
13 |
14 |
15 | #' Construct JavaScript method to capture Shiny inputs on change.
16 | #'
17 | #' This captures the state of a `sortable` list. It will look for a `data-rank-id`
18 | #' attribute of the first child for each element. If no? attribute exists for
19 | #' that particular item's first child, the inner text will be used as an
20 | #' identifier.
21 | #'
22 | #' This method is used with the `onSort` option of `sortable_js`. See
23 | #' [sortable_options()].
24 | #'
25 | #' @param input_id Shiny input name to set
26 | #'
27 | #' @seealso [sortable_js] and [rank_list].
28 | #'
29 | #' @rdname sortable_js_capture_input
30 | #'
31 | #' @return A character vector with class `JS_EVAL`. See [htmlwidgets::JS()].
32 | #' @family JavaScript functions
33 | #' @export
34 | #' @example inst/examples/example_sortable_js_capture.R
35 | #' @examples
36 | #'
37 | #' ## ------------------------------------
38 | #' # For an example, see the Shiny app at
39 | #' system.file("shiny/drag_vars_to_plot/app.R", package = "sortable")
40 | sortable_js_capture_input <- function(input_id) {
41 | # can call jquery as shiny will always have jquery
42 | inner_text <- paste0(
43 | "$.map(",
44 | " this.el.children, ",
45 | get_child_id_or_text_js_fn(),
46 | ")",
47 | collapse = "\n"
48 | )
49 | js_text <- "function(evt) {
50 | if (typeof Shiny !== \"undefined\") {
51 | Shiny.initializedPromise.then(() => {
52 | Shiny.setInputValue(\"%s:sortablejs.rank_list\", %s)
53 | });
54 | }
55 | }"
56 |
57 | js <- sprintf(js_text, input_id, inner_text)
58 |
59 | htmlwidgets::JS(js)
60 | }
61 |
62 |
63 | #' @rdname sortable_js_capture_input
64 | #' @param input_ids Set of Shiny input ids to set corresponding to the provided
65 | #' `css_ids`
66 | #' @param css_ids Set of SortableJS `css_id` values to help retrieve all to
67 | #' set as an object
68 | #'
69 | #' @export
70 | sortable_js_capture_bucket_input <- function(input_id, input_ids, css_ids) {
71 | assert_that(length(input_ids) > 0)
72 | assert_that(length(input_ids) == length(css_ids))
73 |
74 | # can use jquery as shiny will have jquery
75 | js_text <- "function(evt) {
76 | if (typeof Shiny == \"undefined\") {
77 | return;
78 | }
79 |
80 | var child_id_or_text_fn = %s;
81 |
82 | var ret = {}, i;
83 | var css_ids = %s;
84 | var input_ids = %s;
85 |
86 | $.map(css_ids, function(css_id, i) {
87 | var input_id = input_ids[i];
88 | var item = $('#' + css_id).get(0);
89 | if (item && item.children) {
90 | ret[input_id] = $.map(item.children, child_id_or_text_fn);
91 | } else {
92 | ret[input_id] = undefined;
93 | }
94 | });
95 | Shiny.setInputValue(\"%s:sortablejs.bucket_list\", ret)
96 | }"
97 |
98 | js <- sprintf(
99 | js_text,
100 | get_child_id_or_text_js_fn(),
101 | to_json_array(css_ids),
102 | to_json_array(input_ids),
103 | input_id
104 | )
105 |
106 | htmlwidgets::JS(js)
107 | }
108 |
109 |
110 |
111 | # add empty class to all css_ids on execution
112 | # need to setTimeout as dom effects have not executed
113 | # need to pass in all ids, as moving an element from
114 | # group A to B back to A without dropping will remove empty class from B,
115 | # but never add it back.
116 | sortable_js_set_empty_class <- function(css_ids) {
117 | js_text <-
118 | "function(evt) {
119 | var css_ids = %s;
120 | setTimeout(function() {
121 | css_ids.map(function(id) {
122 | var el = window.document.getElementById(id);
123 | if (el) {
124 | Sortable.utils.toggleClass(el, 'rank-list-empty', el.children.length == 0);
125 | }
126 | })
127 | }, 0);
128 | }"
129 |
130 | js <- sprintf(
131 | js_text,
132 | to_json_array(css_ids)
133 | )
134 |
135 | htmlwidgets::JS(js)
136 | }
137 |
138 | to_json_array <- function(x) {
139 | as.character(
140 | jsonlite::toJSON(
141 | as.list(x),
142 | auto_unbox = TRUE
143 | )
144 | )
145 | }
146 |
147 |
148 | #' Chain multiple JavaScript events
149 | #'
150 | #' SortableJS does not have an event based system. To be able to call multiple
151 | #' JavaScript events under the same event execution, they need to be executed
152 | #' one after another.
153 | #'
154 | #' @param ... JavaScript functions defined by [htmlwidgets::JS]
155 | #' @return A single JavaScript function that will call all methods provided with
156 | #' the event
157 | #' @export
158 | #' @family JavaScript functions
159 | chain_js_events <- function(...) {
160 |
161 | fns <- rlang::list2(...)
162 | fns <- fns[!vapply(fns, is.null, logical(1))]
163 | fns <- lapply(fns, as.character)
164 |
165 | if (length(fns) == 1) {
166 | return(htmlwidgets::JS(fns[[1]]))
167 | }
168 |
169 | js_text <- collapse(
170 | # do not provide arguments to avoid confusion
171 | "function() {",
172 | # call fns with all arguments supplied to outer func
173 | # some event methods have more than one argument (most have 1).
174 | collapse(
175 | " try {",
176 | rep(" (%s).apply(this, arguments);", length(fns)),
177 | " } catch(e) {",
178 | " if (window.console && window.console.error) window.console.error(e);",
179 | " }",
180 | collapse = "\n\n"
181 | ),
182 | "}"
183 | )
184 |
185 | js <- do.call(sprintf, c(js_text, fns))
186 |
187 | htmlwidgets::JS(js)
188 | }
189 |
190 |
191 | collapse <- function(..., sep = "\n", collapse = "\n") {
192 | paste(..., sep = sep, collapse = collapse)
193 | }
194 |
--------------------------------------------------------------------------------
/R/label_ids.R:
--------------------------------------------------------------------------------
1 |
2 |
3 | label_ids <- function(labels) {
4 |
5 | nms <- names(labels)
6 | if (is.null(nms)) {
7 | return(rep("", length(labels)))
8 | }
9 |
10 | nms[is.na(nms)] <- ""
11 | nms
12 | }
13 |
--------------------------------------------------------------------------------
/R/methods.R:
--------------------------------------------------------------------------------
1 |
2 | as_rank_list <- function(x){
3 | class(x) <- c("rank_list", class(x))
4 | x
5 | }
6 |
7 |
8 | as_bucket_list <- function(x){
9 | class(x) <- c("bucket_list", class(x))
10 | x
11 | }
12 |
13 |
14 | #' @export
15 | print.rank_list <- function(x, ...){
16 | htmltools::html_print(x)
17 | }
18 |
19 | #' @export
20 | print.bucket_list <- function(x, ...){
21 | htmltools::html_print(x)
22 | }
23 |
24 |
25 | # The names must be suffix values to allow for modules which use prefix values
26 | # https://github.com/rstudio/sortable/issues/100
27 | as_rank_list_id <- function(id) {
28 | paste0("rank-list-", id)
29 | }
30 | # TODO: in future, change the order of paste, to enable shiny modules
31 | # paste0(id, "-rank-list")
32 |
33 | as_bucket_list_id <- function(id) {
34 | paste0("bucket-list-", id)
35 | }
36 | # TODO: in future, change the order of paste, to enable shiny modules
37 | # paste0(id, "-bucket-list")
38 |
--------------------------------------------------------------------------------
/R/question_rank.R:
--------------------------------------------------------------------------------
1 | #' @importFrom learnr question_ui_initialize
2 | #' @importFrom learnr question_ui_completed
3 | #' @importFrom learnr question_ui_try_again
4 | #' @importFrom learnr question_is_valid
5 | #' @importFrom learnr question_is_correct
6 | #' @importFrom learnr mark_as
7 | #' @importFrom learnr disable_all_tags
8 | NULL
9 |
10 |
11 | #' Ranking question for learnr tutorials.
12 | #'
13 | #' Add interactive ranking tasks to your `learnr` tutorials. The student can
14 | #' drag-and-drop the answer options into the desired order.
15 | #'
16 | #' Each set of answer options must contain the same set of answer options. When
17 | #' the question is completed, the first correct answer will be displayed.
18 | #'
19 | #' Note that, by default, the answer order is randomized.
20 | #'
21 | #' @param ... parameters passed onto \code{\link[learnr:quiz]{learnr::question()}}.
22 | #'
23 | #' @template options
24 | #'
25 | #' @inheritParams learnr::question
26 | #'
27 | #' @return A custom `learnr` question, with `type = sortable_rank`.
28 | #' See \code{\link[learnr:quiz]{learnr::question()}}.
29 | #'
30 | #' @export
31 | #' @examples
32 | #' ## Example of rank problem inside a learnr tutorial
33 | #' if (interactive()) {
34 | #' learnr::run_tutorial("question_rank", package = "sortable")
35 | #' }
36 | question_rank <- function(
37 | text,
38 | ...,
39 | correct = "Correct!",
40 | incorrect = "Incorrect",
41 | loading = c("**Loading:** ", text, "
"),
42 | submit_button = "Submit Answer",
43 | try_again_button = "Try Again",
44 | allow_retry = FALSE,
45 | random_answer_order = TRUE,
46 | options = sortable_options()
47 | ) {
48 | learnr::question(
49 | text = text,
50 | ...,
51 | type = "sortable_rank",
52 | correct = correct,
53 | incorrect = incorrect,
54 | loading = loading,
55 | submit_button = submit_button,
56 | try_again_button = try_again_button,
57 | allow_retry = allow_retry,
58 | random_answer_order = random_answer_order,
59 | options = options
60 | )
61 | }
62 |
63 |
64 | #' @export
65 | #' @seealso [question_rank]
66 | question_ui_initialize.sortable_rank <- function(question, value, ...) {
67 |
68 | # quickly validate the all possible answers are possible
69 | answer <- question$answers[[1]]
70 | possible_answer_vals <- sort(answer$option)
71 | for (answer in question$answers) {
72 | if (!identical(
73 | possible_answer_vals,
74 | sort(answer$option)
75 | )) {
76 | stop(
77 | "All question_rank answers MUST have the same set of answers. (Order does not matter.) ",
78 | "\nBad set: ", paste0(answer$option, collapse = ", "),
79 | call. = FALSE
80 | )
81 | }
82 | }
83 |
84 | # if no label order has been provided
85 | if (!is.null(value)) {
86 | labels <- value
87 | } else {
88 | labels <- question$answers[[1]]$option
89 |
90 | # if the question is to be displayed in random order, shuffle the options
91 | if (
92 | isTRUE(question$random_answer_order) # and we should randomize the order
93 | ) {
94 | labels <- sample(labels, length(labels))
95 | }
96 | }
97 |
98 |
99 | # return the rank_list htmlwidget
100 | rank_list(
101 | text = question$question,
102 | input_id = question$ids$answer,
103 | labels = labels,
104 | options = question$options
105 | )
106 | }
107 |
108 | #' @export
109 | #' @seealso [question_rank]
110 | question_ui_completed.sortable_rank <- function(question, value, ...) {
111 | # TODO display correct values with X or √ compared to best match
112 | # TODO DON'T display correct values (listen to an option?)
113 | disable_all_tags(
114 | rank_list(
115 | text = question$question,
116 | input_id = question$ids$answer,
117 | labels = value,
118 | options = modifyList(
119 | question$options,
120 | sortable_options(disabled = TRUE)
121 | )
122 | )
123 | )
124 | }
125 |
126 | #' @export
127 | #' @seealso [question_rank]
128 | question_ui_try_again.sortable_rank <- function(question, value, ...) {
129 | disable_all_tags(
130 | rank_list(
131 | text = question$question,
132 | input_id = question$ids$answer,
133 | labels = value,
134 | options = modifyList(
135 | question$options,
136 | sortable_options(disabled = TRUE)
137 | )
138 | )
139 | )
140 | }
141 |
142 |
143 | #' @export
144 | #' @seealso [question_rank]
145 | question_is_correct.sortable_rank <- function(question, value, ...) {
146 | # for each possible answer, check if it matches
147 | for (answer in question$answers) {
148 | if (identical(answer$option, value)) {
149 | # if it matches, return the correct-ness and its message
150 | return(mark_as(answer$correct, answer$message))
151 | }
152 | }
153 | # no match found. not correct
154 | mark_as(FALSE, NULL)
155 | }
156 |
--------------------------------------------------------------------------------
/R/rank_list.R:
--------------------------------------------------------------------------------
1 | # Create label tags for rank_list
2 | as_label_tags <- function(labels) {
3 | mapply(
4 | USE.NAMES = FALSE,
5 | SIMPLIFY = FALSE,
6 | labels,
7 | label_ids(labels),
8 | FUN = function(label, label_id) {
9 | if (identical(label_id, "")) {
10 | label_id <- NULL
11 | }
12 | tags$div(
13 | class = "rank-list-item",
14 | "data-rank-id" = label_id,
15 | label
16 | )
17 | }
18 | )
19 | }
20 |
21 |
22 | #' Create a ranking item list.
23 | #'
24 | #' @description Creates a ranking item list using the `SortableJS` framework,
25 | #' and generates an `htmlwidgets` element. The elements of this list can be
26 | #' dragged and dropped in any order.
27 | #'
28 | #' You can embed a ranking question inside a `learnr` tutorial, using
29 | #' [question_rank()].
30 | #'
31 | #' To embed a `rank_list` inside a shiny app, see the Details section.
32 | #'
33 | #' @details
34 | #'
35 | #' You can embed a `rank_list` inside a Shiny app, to capture the preferred
36 | #' ranking order of your user.
37 | #'
38 | #' The widget automatically updates a Shiny output, with the matching
39 | #' `input_id`.
40 | #'
41 | #'
42 | #' @param input_id output variable to read the plot/image from.
43 | #'
44 | #' @param labels A character vector with the text to display inside the widget.
45 | #' This can also be a list of html tag elements. The text content of each
46 | #' label or label name will be used to set the shiny `input_id` value.
47 | #' To create an empty `rank_list`, use `labels = list()`.
48 | #'
49 | #' @param text Text to appear at top of list.
50 | #'
51 | #' @param css_id This is the css id to use, and must be unique in your shiny
52 | #' app. This defaults to the value of `input_id`, and will be appended to the
53 | #' value "rank-list-container", to ensure the CSS id is unique for the
54 | #' container as well as the labels.
55 | #' If NULL, the function generates an id of the form
56 | #' `rank_list_id_1`, and will automatically increment for every `rank_list`.
57 | #'
58 | #' @param class A css class applied to the rank list. This can be used to
59 | #' define custom styling.
60 | #'
61 | #' @param orientation Set this to "horizontal" to get horizontal orientation of
62 | #' the items.
63 | #'
64 | #' @template options
65 | #'
66 | #' @seealso [update_rank_list], [sortable_js], [bucket_list] and [question_rank]
67 | #'
68 | #' @export
69 | #' @importFrom utils modifyList
70 | #' @importFrom htmltools tagList tags
71 | #' @example inst/examples/example_rank_list.R
72 | #' @example inst/examples/example_rank_list_multidrag.R
73 | #' @example inst/examples/example_rank_list_swap.R
74 | #' @examples
75 | #' ## Example of a shiny app
76 | #' if (interactive()) {
77 | #' app <- system.file("shiny/rank_list/app.R", package = "sortable")
78 | #' shiny::runApp(app)
79 | #' }
80 | #'
81 | rank_list <- function(
82 | text = "",
83 | labels,
84 | input_id,
85 | css_id = input_id,
86 | options = sortable_options(),
87 | orientation = c("vertical", "horizontal"),
88 | class = "default-sortable"
89 | ) {
90 | if (is.null(css_id)) {
91 | css_id <- increment_rank_list()
92 | }
93 | orientation = match.arg(orientation)
94 | if (orientation == "horizontal") class = paste0(class, " horizontal")
95 | assert_that(is_sortable_options(options))
96 | assert_that(is_input_id(input_id))
97 |
98 | options$onSort <- chain_js_events( # nolint
99 | options$onSort, # nolint
100 | sortable_js_capture_input(input_id)
101 | )
102 | options$onLoad <- chain_js_events( # nolint
103 | options$onLoad, # nolint
104 | sortable_js_capture_input(input_id),
105 | sortable_js_set_empty_class(css_id)
106 | )
107 |
108 | title_tag <- if (!is.null(text) && nchar(text) > 0) {
109 | tags$p(class = "rank-list-title", text)
110 | } else {
111 | NULL
112 | }
113 |
114 | label_tags <- as_label_tags(labels)
115 |
116 | rank_list_tags <- tagList(
117 | tags$div(
118 | class = paste("rank-list-container", paste(class, collapse = " ")),
119 | id = as_rank_list_id(css_id),
120 | title_tag,
121 | tags$div(
122 | class = "rank-list",
123 | id = css_id,
124 | label_tags
125 | )
126 | ),
127 | sortable_js(
128 | css_id = css_id,
129 | options = options
130 | ),
131 | rank_list_dependencies()
132 | )
133 |
134 | as_rank_list(rank_list_tags)
135 |
136 | }
137 |
138 |
139 | dropNulls <- function(x) {
140 | x[!vapply(x, is.null, FUN.VALUE = logical(1))]
141 | }
142 |
143 |
144 | #' Change the text or labels of a rank list.
145 | #'
146 | #'
147 | #' @inheritParams rank_list
148 | #' @param session The `session` object passed to function given to
149 | #' `shinyServer`.
150 | #' @seealso [rank_list]
151 | #' @export
152 | #' @examples
153 | #' ## Example of a shiny app that updates a bucket list and rank list
154 | #' if (interactive()) {
155 | #' app <- system.file(
156 | #' "shiny/update_rank_list/app.R",
157 | #' package = "sortable"
158 | #' )
159 | #' shiny::runApp(app)
160 | #' }
161 | update_rank_list <- function(css_id, text = NULL, labels = NULL,
162 | session = shiny::getDefaultReactiveDomain()) {
163 | inputId <- as_rank_list_id(css_id)
164 | if ( !is.null(labels) && length(labels) > 0) {
165 | labels <- as.character(tagList(as_label_tags(labels)))
166 | }
167 | message <- dropNulls(list(id = inputId, text = text, labels = labels))
168 | session$sendInputMessage(inputId, message)
169 |
170 | }
171 |
172 |
173 | #' Change the value of a bucket list.
174 | #'
175 | #' You can only update the `header` of the `bucket_list`.
176 | #' To update any of the labels or rank list text, use `update_rank_list()`
177 | #' instead.
178 | #'
179 | #' @inheritParams bucket_list
180 | #' @param session The `session` object passed to function given to
181 | #' `shinyServer`.
182 | #' @seealso [bucket_list], [update_rank_list]
183 | #' @export
184 | #' @examples
185 | #' ## Example of a shiny app that updates a bucket list and rank list
186 | #' if (interactive()) {
187 | #' app <- system.file(
188 | #' "shiny/update/app.R",
189 | #' package = "sortable"
190 | #' )
191 | #' shiny::runApp(app)
192 | #' }
193 | update_bucket_list <- function(css_id, header = NULL,
194 | session = shiny::getDefaultReactiveDomain()) {
195 | inputId <- paste0("bucket-list-", css_id)
196 | message <- dropNulls(list(id = inputId, header = header))
197 | session$sendInputMessage(inputId, message)
198 | }
199 |
--------------------------------------------------------------------------------
/R/sortable-package.R:
--------------------------------------------------------------------------------
1 | #' @section A new html widget:
2 | #'
3 | #' * [sortable_js()] is a low-level function that adds the `SortableJS` to your widgets.
4 | #'
5 | #' @section Important functions:
6 | #'
7 | #' The important functions in this package are:
8 | #'
9 | #' * [rank_list()] creates a drag-and-drop, rank list
10 | #'
11 | #' * [bucket_list()] lets you add multiple `rank_list` objects in columns
12 | #'
13 | #' @section Custom question types for `learnr`:
14 | #'
15 | #' You can also use new question types in your `learnr` tutorials:
16 | #'
17 | #' * [question_rank()]
18 | #'
19 | #'
20 | #' @importFrom assertthat assert_that is.string
21 | #' @name sortable
22 | #' @aliases sortable-package sortable
23 | #' @keywords internal
24 | "_PACKAGE"
25 |
26 |
27 | # The following block is used by usethis to automatically manage
28 | # roxygen namespace tags. Modify with care!
29 | ## usethis namespace: start
30 | ## usethis namespace: end
31 | NULL
32 |
--------------------------------------------------------------------------------
/R/sortable_js.R:
--------------------------------------------------------------------------------
1 | #' Creates an htmlwidget with embedded 'SortableJS' library.
2 | #'
3 | #' Creates an `htmlwidget` that provides
4 | #' [SortableJS](https://github.com/SortableJS/Sortable) to use for
5 | #' drag-and-drop interactivity in Shiny apps and R Markdown.
6 | #'
7 | #' @param css_id `String` css_id id on which to apply `SortableJS`. Note,
8 | #' `sortable_js` works with any html element, not just `ul/li`.
9 | #' @template options
10 | #' @inheritParams htmlwidgets::createWidget
11 | #'
12 | #' @importFrom htmlwidgets shinyWidgetOutput
13 | #' @seealso [sortable_options()]
14 | #'
15 | #' @export
16 | #' @example inst/examples/example_sortable_js.R
17 | sortable_js <- function(
18 | css_id,
19 | options = sortable_options(),
20 | width = 0,
21 | height = 0,
22 | elementId = NULL, # nolint
23 | preRenderHook = NULL # nolint
24 | ) {
25 |
26 | assert_that(is_sortable_options(options))
27 |
28 | # forward options using x
29 | x <- list(
30 | css_id = css_id,
31 | options = modifyList(
32 | # set default options to be overwritten by supplied options
33 | default_sortable_options(),
34 | options
35 | )
36 | )
37 |
38 | # create widget
39 | htmlwidgets::createWidget(
40 | name = "sortable",
41 | x,
42 | width = width,
43 | height = height,
44 | package = "sortable",
45 | elementId = elementId, # nolint
46 | preRenderHook = preRenderHook # nolint
47 | )
48 | }
49 |
50 | #' Widget output function for use in Shiny.
51 | #'
52 | #' @inheritParams sortable_js
53 | #' @param input_id output variable to use for the sortable object
54 | #'
55 | #' @export
56 | sortable_output <- function(input_id, width = "0px", height = "0px") {
57 | htmlwidgets::shinyWidgetOutput(input_id, "sortable", width, height, package = "sortable")
58 | }
59 |
60 | #' Widget render function for use in Shiny.
61 | #'
62 | #' @param expr An expression
63 | #' @param env The environment in which to evaluate `expr`.
64 | #' @param quoted Is `expr` a quoted expression (with `quote()`)? This is useful
65 | #' if you want to save an expression in a variable.
66 | #'
67 | #' @export
68 | render_sortable <- function(expr, env = parent.frame(), quoted = FALSE) {
69 | if (!quoted) {
70 | expr <- substitute(expr)
71 | } # force quoted
72 | htmlwidgets::shinyRenderWidget(expr, sortable_output, env, quoted = TRUE)
73 | }
74 |
--------------------------------------------------------------------------------
/R/sortable_options.R:
--------------------------------------------------------------------------------
1 | #' Check if object is sortable options.
2 | #'
3 | #' @param x Object to test
4 | #' @return Logical vector. TRUE if the object inherits from `sortable_options`
5 | #' @export
6 | #' @examples
7 | #' is_sortable_options("foo") # returns FALSE
8 | is_sortable_options <- function(x) {
9 | inherits(x, "sortable_options")
10 | }
11 |
12 |
13 | #' Define options to pass to a sortable object.
14 | #'
15 | #' Use this function to define the options for [sortable_js] and [rank_list],
16 | #' which will pass these in turn to the `SortableJS` JavaScript library.
17 | #'
18 | #' Many of the `SortableJS` options will accept a JavaScript function. You can
19 | #' do this using the `htmlwidgets::JS` function.
20 | #'
21 | #' @param ... other arguments passed onto `SortableJS`
22 | #'
23 | #' @param swap If `TRUE`, modifies the behaviour of `sortable` to allow for items to
24 | #' be swapped with each other rather than sorted. Once dragging starts, the
25 | #' user can drag over other items and there will be no change in the elements.
26 | #' However, the item that the user drops on will be swapped with the
27 | #' originally dragged item.
28 | #' See also https://github.com/SortableJS/Sortable/tree/master/plugins/Swap
29 | #'
30 | #' @param multiDrag If `TRUE`, allows the selection of multiple items within a
31 | #' `sortable` at once, and drag them as one item. Once placed, the items will
32 | #' unfold into their original order, but all beside each other at the new
33 | #' position.
34 | #' See also https://github.com/SortableJS/Sortable/wiki/Dragging-Multiple-Items-in-Sortable
35 | #'
36 | #'
37 | #' @param group To drag elements from one list into another, both lists must
38 | #' have the same group value. See
39 | #' [Sortable#group-option](https://github.com/sortablejs/Sortable/#group-option)
40 | #' for more details. \[`"name"`\]
41 | #'
42 | #' @param sort Boolean that allows sorting inside a list. \[`TRUE`\]
43 | #'
44 | #' @param delay Time in milliseconds to define when the sorting should start.
45 | #' \[`0`\]
46 | #
47 | #' @param disabled Boolean that disables the `sortable` if set to true. \[`FALSE`\]
48 | #
49 | #' @param animation Millisecond duration of the animation of items when sorting
50 | #' \[`0` (no animation)\]
51 | #'
52 | #' @param handle CSS selector used for the drag handle selector within list
53 | #' items. \[`".my-handle"`\]
54 | #'
55 | #' @param filter CSS selector or JS function used for elements that cannot be
56 | #' dragged. \[`".ignore-elements"`\]
57 | #'
58 | #' @param draggable CSS selector of which items inside the element should be
59 | #' draggable. \[`".item"`\]
60 | #'
61 | #' @param swapThreshold Percentage of the target that the swap zone will take
62 | #' up, as a number between `0` and `1`. \[`1`\]
63 | #'
64 | #' @param invertSwap Set to \code{TRUE} to set the swap zone to the sides of the
65 | #' target, for the effect of sorting "in between" items. \[`FALSE`\]
66 | #'
67 | #' @param direction Direction of `sortable` \[`"horizontal"`\]
68 | #'
69 | #' @param scrollSensitivity Number of pixels the mouse needs to be to an edge to
70 | #' start scrolling. \[`30`\]
71 | #'
72 | #' @param scrollSpeed Number of pixels for the speed of scrolling. \[`10`\]
73 | #
74 | #' @param onStart,onEnd JS function called when an element dragging starts or ends
75 | #'
76 | #' @param onAdd JS function called when an element is dropped into the list from
77 | #' another list
78 | #'
79 | #' @param onUpdate JS function called when the sorting is changed within a list
80 | #'
81 | #' @param onSort JS function called by any change to the list (add / update /
82 | #' remove)
83 | #'
84 | #' @param onRemove JS function called when an element is removed from the list
85 | #' into another list
86 | #'
87 | #' @param onFilter JS function called when an attempt is made to drag a filtered
88 | #' element
89 | #'
90 | #' @param onMove JS function called when an item is moved in a list or between
91 | #' lists
92 | #'
93 | #' @param onLoad JS function dispatched on the "next tick" after SortableJS has
94 | #' initialized
95 | #
96 | # @param delayOnTouchOnly Boolean that will only delay if user is using touch
97 | # (mobile display). \[`FALSE`\]
98 | #
99 | # @param touchStartThreshold Number of pixels a point should move before
100 | # cancelling a delayed drag event. \[`0`\]
101 | #
102 | # @param store Saving and restoring of the sort. See
103 | # [Sortable#store](https://github.com/sortablejs/Sortable/#store)
104 | #
105 | # @param easing Easing for animation. \[`NULL`\] See
106 | # [https://easings.net/](https://easings.net/) for examples.
107 | #
108 | # @param preventOnFilter Boolean that determines if `event.preventDefault()` is
109 | # called when `filter` is triggered. \[`TRUE`\]
110 | #
111 | # @param dataIdAttr no documentation
112 | #
113 | # @param ghostClass CSS class name for the drop placeholder.
114 | # \[`"sortable-ghost"`\]
115 | #
116 | # @param chosenClass CSS class name for the chosen item \[`"sortable-chosen"`\]
117 | #
118 | # @param dragClass CSS class name for the dragging item \[`"sortable-drag"`\]
119 | #
120 | # @param invertedSwapThreshold Percentage of the target that the inverted swap
121 | # zone will take up, as a number between `0` and `1`. \[`swapThreshold`\]
122 | #
123 | # @param forceFallback : false, // ignore the HTML5 DnD behaviour and force the
124 | # fallback to kick in
125 | #
126 | # @param fallbackClass : "sortable-fallback", // Class name for the cloned DOM
127 | # Element when using forceFallback
128 | #
129 | # @param fallbackOnBody : false, // Appends the cloned DOM Element into the
130 | # Document's Body
131 | #
132 | # @param fallbackTolerance : 0, // Specify in pixels how far the mouse should
133 | # move before it's considered as a drag.
134 | #
135 | # @param dragoverBubble If set to true, the dragover event will bubble to parent
136 | # sortables. \[`false`\]
137 | #
138 | # @param removeCloneOnHide: true, // Remove the clone element when it is not
139 | # showing, rather than just hiding it
140 | #
141 | # @param emptyInsertThreshold: Number of pixels a mouse must be from empty
142 | # `sortable` to insert drag element into it. \[`5`\]
143 | #
144 | # @param setData undocumented on website
145 | # https://github.com/SortableJS/Sortable/tree/master/plugins/AutoScroll
146 | #
147 | # @param onChoose,onUnchoose JS function called when an element is chosen or
148 | # unchosen
149 | #
150 | # @param onClone JS function that is called when creating a clone of an element
151 | #
152 | # @param onChange JS function that is called when a dragging element changes
153 | # position
154 | #'
155 | #'
156 | #' @references [https://github.com/sortablejs/Sortable/](https://github.com/sortablejs/Sortable/)
157 | #'
158 | #' @seealso [sortable_js]
159 | #'
160 | #' @return A list with class `sortable_options`
161 | #' @examples
162 | #' sortable_options(sort = FALSE)
163 | #' @export
164 | sortable_options <- function(
165 | ...,
166 | # nolint start
167 | swap = NULL,
168 | multiDrag = NULL,
169 | group = NULL,
170 | sort = NULL,
171 | delay = NULL,
172 | disabled = NULL,
173 | animation = NULL,
174 | handle = NULL,
175 | filter = NULL,
176 | draggable = NULL,
177 | swapThreshold = NULL,
178 | invertSwap = NULL,
179 | direction = NULL,
180 | scrollSensitivity = NULL,
181 | scrollSpeed = NULL,
182 | onStart = NULL,
183 | onEnd = NULL,
184 | onAdd = NULL,
185 | onUpdate = NULL,
186 | onSort = NULL,
187 | onRemove = NULL,
188 | onFilter = NULL,
189 | onMove = NULL,
190 | onLoad = NULL
191 | # nolint end
192 | ) {
193 | extra_args <- rlang::list2(...)
194 |
195 | # get all names and values
196 | args <- names(formals(sortable_options))
197 | arg_vals <- mget(args[-1], environment()) # remove first element (...)
198 |
199 | # remove null values
200 | is_null <- vapply(arg_vals, is.null, logical(1))
201 | arg_vals <- arg_vals[!is_null]
202 |
203 | # merge all args
204 | ret <- append(arg_vals, extra_args)
205 |
206 | class(ret) <- "sortable_options"
207 | ret
208 | }
209 |
210 |
211 | default_sortable_options <- function() {
212 | sortable_options(
213 | animation = 150,
214 | emptyInsertThreshold = 50 / 4
215 | )
216 | }
217 |
--------------------------------------------------------------------------------
/R/zzz.R:
--------------------------------------------------------------------------------
1 | # nocov start
2 |
3 | .onLoad <- function(...) {
4 |
5 | as_character_vector <- function(x) {
6 | # works for both x = NULL and x = list()
7 | if (length(x) == 0) {
8 | return(character(0))
9 | }
10 | unlist(x)
11 | }
12 |
13 | # Register a handler for a bucket_list to unlist each set of values.
14 | # should return a list of character vectors or NULL
15 | shiny::registerInputHandler(
16 | force = TRUE,
17 | "sortablejs.rank_list",
18 | function(val, shinysession, name) {
19 | ret <- as_character_vector(val)
20 | ret
21 | }
22 | )
23 |
24 | # Register a handler for a bucket_list to unlist each set of values.
25 | # should return a list of character vectors or NULL
26 | shiny::registerInputHandler(
27 | force = TRUE,
28 | "sortablejs.bucket_list",
29 | function(val, shinysession, name) {
30 | ret <- lapply(val, function(x) {
31 | as_character_vector(x)
32 | })
33 | ret
34 | }
35 | )
36 |
37 | }
38 |
39 | # nocov end
40 |
--------------------------------------------------------------------------------
/README.Rmd:
--------------------------------------------------------------------------------
1 | ---
2 | output: github_document
3 | format: gfm
4 | default-image-extension: ""
5 | ---
6 |
7 |
8 |
9 | ```{r, include = FALSE}
10 | knitr::opts_chunk$set(
11 | collapse = TRUE,
12 | comment = "#>",
13 | fig.path = "man/figures/README-",
14 | out.width = "100%"
15 | )
16 | ```
17 |
18 | # sortable
19 |
20 |
21 | [](https://CRAN.R-project.org/package=sortable)
22 | [](https://www.r-pkg.org/pkg/sortable)
23 | [](https://github.com/rstudio/sortable/actions)
24 | [](https://codecov.io/gh/rstudio/sortable?branch=main)
25 | [](https://lifecycle.r-lib.org/articles/stages.html#stable)
26 |
27 |
28 | The `sortable` package enables drag-and-drop behaviour in your Shiny apps. It does this by exposing the functionality of the [SortableJS](https://sortablejs.github.io/Sortable/) JavaScript library as an [htmlwidget](http://www.htmlwidgets.org) in R, so you can use this in Shiny apps and widgets, `learnr` tutorials as well as R Markdown. In addition, provides a custom `learnr` question type - `question_rank()` that allows ranking questions with drag-and-drop.
29 |
30 |
31 |
32 | ## Installation
33 |
34 | You can install the released version of sortable from [CRAN](https://CRAN.R-project.org) with:
35 |
36 | ```r
37 | install.packages("sortable")
38 | ```
39 |
40 | And the development version from [GitHub](https://github.com/rstudio/sortable) with:
41 |
42 | ```r
43 | # install.packages("remotes")
44 | remotes::install_github("rstudio/sortable")
45 | ```
46 |
47 | ## Examples
48 |
49 | ### Rank list
50 |
51 | You can create a drag-and-drop input object in Shiny, using the `rank_list()` function.
52 |
53 |
} tag, so not strictly speaking a header.) Note
21 | that you must explicitly provide \code{header} argument, especially in the case
22 | where you want the header to be empty - to do this use \code{header = NULL} or
23 | \code{header = NA}.}
24 |
25 | \item{...}{One or more specifications for a rank list, and must be defined by
26 | \link{add_rank_list}.}
27 |
28 | \item{group_name}{Passed to \code{SortableJS} as the group name. Also the input
29 | value set in Shiny. (\code{input[[group_name]]}). Items can be dragged between
30 | bucket lists which share the same group name.}
31 |
32 | \item{css_id}{This is the css id to use, and must be unique in your shiny
33 | app. This defaults to the value of \code{group_id}, and will be appended to the
34 | value "bucket-list-container", to ensure the CSS id is unique for the
35 | container as well as the embedded rank lists.}
36 |
37 | \item{group_put_max}{Not yet implemented}
38 |
39 | \item{options}{Options to be supplied to \link{sortable_js} object. See \link{sortable_options} for more details}
40 |
41 | \item{class}{A css class applied to the bucket list and rank lists. This can
42 | be used to define custom styling.}
43 |
44 | \item{orientation}{Either \code{horizontal} or \code{vertical}, and specifies the
45 | layout of the components on the page.}
46 | }
47 | \value{
48 | A list with class \code{bucket_list}
49 | }
50 | \description{
51 | A bucket list can contain more than one \link{rank_list} and allows drag-and-drop
52 | of items between the different lists.
53 | }
54 | \examples{
55 | ## -- example-bucket-list ---------------------------------------------
56 |
57 | ## bucket list
58 |
59 | if(interactive()) {
60 | bucket_list(
61 | header = "This is a bucket list. You can drag items between the lists.",
62 | add_rank_list(
63 | text = "Drag from here",
64 | labels = c("a", "bb", "ccc")
65 | ),
66 | add_rank_list(
67 | text = "to here",
68 | labels = NULL
69 | )
70 | )
71 | }
72 |
73 | ## bucket list with three columns
74 |
75 | if(interactive()) {
76 | bucket_list(
77 | header = c("Sort these items into Letters and Numbers"),
78 | add_rank_list(
79 | text = "Drag from here",
80 | labels = sample(c(1:3, letters[1:2]))
81 | ),
82 | add_rank_list(
83 | text = "Letters"
84 | ),
85 | add_rank_list(
86 | text = "Numbers"
87 | )
88 | )
89 | }
90 |
91 | ## drag items between bucket lists
92 |
93 | if(interactive()) {
94 |
95 | ui <- shiny::fluidPage(
96 | shiny::column(4, bucket_list(NULL,
97 | group_name = "foo",
98 | add_rank_list(
99 | text = "Drag from here...",
100 | labels = sample(c(1:3, letters[1:2]))
101 | )
102 | )),
103 | shiny::column(4, "Some empty space"),
104 | shiny::column(4, bucket_list(NULL,
105 | group_name = "foo",
106 | add_rank_list(
107 | text = "...To here"
108 | )
109 | ))
110 | )
111 |
112 | server <- function(input, output, session) {}
113 |
114 | shiny::shinyApp(ui, server)
115 |
116 |
117 | }
118 |
119 | ## Example of a shiny app
120 | if (interactive()) {
121 | app <- system.file(
122 | "shiny/bucket_list/app.R",
123 | package = "sortable"
124 | )
125 | shiny::runApp(app)
126 | }
127 | }
128 | \seealso{
129 | \link{rank_list}, \link{update_rank_list}
130 | }
131 |
--------------------------------------------------------------------------------
/man/chain_js_events.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/js.R
3 | \name{chain_js_events}
4 | \alias{chain_js_events}
5 | \title{Chain multiple JavaScript events}
6 | \usage{
7 | chain_js_events(...)
8 | }
9 | \arguments{
10 | \item{...}{JavaScript functions defined by \link[htmlwidgets:JS]{htmlwidgets::JS}}
11 | }
12 | \value{
13 | A single JavaScript function that will call all methods provided with
14 | the event
15 | }
16 | \description{
17 | SortableJS does not have an event based system. To be able to call multiple
18 | JavaScript events under the same event execution, they need to be executed
19 | one after another.
20 | }
21 | \seealso{
22 | Other JavaScript functions:
23 | \code{\link{sortable_js_capture_input}()}
24 | }
25 | \concept{JavaScript functions}
26 |
--------------------------------------------------------------------------------
/man/figures/README-unnamed-chunk-4-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/man/figures/README-unnamed-chunk-4-1.png
--------------------------------------------------------------------------------
/man/figures/README-unnamed-chunk-5-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/man/figures/README-unnamed-chunk-5-1.png
--------------------------------------------------------------------------------
/man/figures/bucket_list_shiny.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/man/figures/bucket_list_shiny.gif
--------------------------------------------------------------------------------
/man/figures/diagrammer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/man/figures/diagrammer.gif
--------------------------------------------------------------------------------
/man/figures/lifecycle-archived.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-defunct.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-deprecated.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-experimental.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-maturing.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-questioning.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-retired.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-soft-deprecated.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/lifecycle-stable.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/man/figures/logo.png
--------------------------------------------------------------------------------
/man/figures/rank_list_shiny.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/man/figures/rank_list_shiny.gif
--------------------------------------------------------------------------------
/man/figures/simple_sortable_shiny.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/man/figures/simple_sortable_shiny.gif
--------------------------------------------------------------------------------
/man/figures/sortable-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/man/figures/sortable-logo.png
--------------------------------------------------------------------------------
/man/is_sortable_options.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/sortable_options.R
3 | \name{is_sortable_options}
4 | \alias{is_sortable_options}
5 | \title{Check if object is sortable options.}
6 | \usage{
7 | is_sortable_options(x)
8 | }
9 | \arguments{
10 | \item{x}{Object to test}
11 | }
12 | \value{
13 | Logical vector. TRUE if the object inherits from \code{sortable_options}
14 | }
15 | \description{
16 | Check if object is sortable options.
17 | }
18 | \examples{
19 | is_sortable_options("foo") # returns FALSE
20 | }
21 |
--------------------------------------------------------------------------------
/man/question_rank.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/question_rank.R
3 | \name{question_rank}
4 | \alias{question_rank}
5 | \title{Ranking question for learnr tutorials.}
6 | \usage{
7 | question_rank(
8 | text,
9 | ...,
10 | correct = "Correct!",
11 | incorrect = "Incorrect",
12 | loading = c("**Loading:** ", text, "
"),
13 | submit_button = "Submit Answer",
14 | try_again_button = "Try Again",
15 | allow_retry = FALSE,
16 | random_answer_order = TRUE,
17 | options = sortable_options()
18 | )
19 | }
20 | \arguments{
21 | \item{text}{Question or option text}
22 |
23 | \item{...}{parameters passed onto \code{\link[learnr:quiz]{learnr::question()}}.}
24 |
25 | \item{correct}{For \code{question}, text to print for a correct answer (defaults
26 | to "Correct!"). For \code{answer}, a boolean indicating whether this answer is
27 | correct.}
28 |
29 | \item{incorrect}{Text to print for an incorrect answer (defaults to
30 | "Incorrect") when \code{allow_retry} is \code{FALSE}.}
31 |
32 | \item{loading}{Loading text to display as a placeholder while the question is
33 | loaded. If not provided, generic "Loading..." or placeholder elements will
34 | be displayed.}
35 |
36 | \item{submit_button}{Label for the submit button. Defaults to \code{"Submit Answer"}}
37 |
38 | \item{try_again_button}{Label for the try again button. Defaults to \code{"Submit Answer"}}
39 |
40 | \item{allow_retry}{Allow retry for incorrect answers. Defaults to \code{FALSE}.}
41 |
42 | \item{random_answer_order}{Display answers in a random order.}
43 |
44 | \item{options}{Options to be supplied to \link{sortable_js} object. See \link{sortable_options} for more details}
45 | }
46 | \value{
47 | A custom \code{learnr} question, with \code{type = sortable_rank}.
48 | See \code{\link[learnr:quiz]{learnr::question()}}.
49 | }
50 | \description{
51 | Add interactive ranking tasks to your \code{learnr} tutorials. The student can
52 | drag-and-drop the answer options into the desired order.
53 | }
54 | \details{
55 | Each set of answer options must contain the same set of answer options. When
56 | the question is completed, the first correct answer will be displayed.
57 |
58 | Note that, by default, the answer order is randomized.
59 | }
60 | \examples{
61 | ## Example of rank problem inside a learnr tutorial
62 | if (interactive()) {
63 | learnr::run_tutorial("question_rank", package = "sortable")
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/man/rank_list.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/rank_list.R
3 | \name{rank_list}
4 | \alias{rank_list}
5 | \title{Create a ranking item list.}
6 | \usage{
7 | rank_list(
8 | text = "",
9 | labels,
10 | input_id,
11 | css_id = input_id,
12 | options = sortable_options(),
13 | orientation = c("vertical", "horizontal"),
14 | class = "default-sortable"
15 | )
16 | }
17 | \arguments{
18 | \item{text}{Text to appear at top of list.}
19 |
20 | \item{labels}{A character vector with the text to display inside the widget.
21 | This can also be a list of html tag elements. The text content of each
22 | label or label name will be used to set the shiny \code{input_id} value.
23 | To create an empty \code{rank_list}, use \code{labels = list()}.}
24 |
25 | \item{input_id}{output variable to read the plot/image from.}
26 |
27 | \item{css_id}{This is the css id to use, and must be unique in your shiny
28 | app. This defaults to the value of \code{input_id}, and will be appended to the
29 | value "rank-list-container", to ensure the CSS id is unique for the
30 | container as well as the labels.
31 | If NULL, the function generates an id of the form
32 | \code{rank_list_id_1}, and will automatically increment for every \code{rank_list}.}
33 |
34 | \item{options}{Options to be supplied to \link{sortable_js} object. See \link{sortable_options} for more details}
35 |
36 | \item{orientation}{Set this to "horizontal" to get horizontal orientation of
37 | the items.}
38 |
39 | \item{class}{A css class applied to the rank list. This can be used to
40 | define custom styling.}
41 | }
42 | \description{
43 | Creates a ranking item list using the \code{SortableJS} framework,
44 | and generates an \code{htmlwidgets} element. The elements of this list can be
45 | dragged and dropped in any order.
46 |
47 | You can embed a ranking question inside a \code{learnr} tutorial, using
48 | \code{\link[=question_rank]{question_rank()}}.
49 |
50 | To embed a \code{rank_list} inside a shiny app, see the Details section.
51 | }
52 | \details{
53 | You can embed a \code{rank_list} inside a Shiny app, to capture the preferred
54 | ranking order of your user.
55 |
56 | The widget automatically updates a Shiny output, with the matching
57 | \code{input_id}.
58 | }
59 | \examples{
60 | ## - example-rank-list ------------------------------------------------
61 |
62 | if (interactive()) {
63 | rank_list(
64 | text = "You can drag, drop and re-order these items:",
65 | labels = c("one", "two", "three", "four", "five"),
66 | input_id = "example_2"
67 | )
68 | }
69 | ## - example-rank-list-multidrag ------------------------------------------
70 |
71 | if (interactive()) {
72 | rank_list(
73 | text = "You can select multiple items and drag as a group:",
74 | labels = c("one", "two", "three", "four", "five"),
75 | input_id = "example_2",
76 | options = sortable_options(
77 | multiDrag = TRUE
78 | )
79 | )
80 | }
81 | ## - example-rank-list-swap -----------------------------------------------
82 |
83 | if (interactive()) {
84 | rank_list(
85 | text = "You can re-order these items, and notice the swapping behaviour:",
86 | labels = c("one", "two", "three", "four", "five"),
87 | input_id = "example_2",
88 | options = sortable_options(
89 | swap = TRUE
90 | )
91 | )
92 | }
93 | ## Example of a shiny app
94 | if (interactive()) {
95 | app <- system.file("shiny/rank_list/app.R", package = "sortable")
96 | shiny::runApp(app)
97 | }
98 |
99 | }
100 | \seealso{
101 | \link{update_rank_list}, \link{sortable_js}, \link{bucket_list} and \link{question_rank}
102 | }
103 |
--------------------------------------------------------------------------------
/man/render_sortable.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/sortable_js.R
3 | \name{render_sortable}
4 | \alias{render_sortable}
5 | \title{Widget render function for use in Shiny.}
6 | \usage{
7 | render_sortable(expr, env = parent.frame(), quoted = FALSE)
8 | }
9 | \arguments{
10 | \item{expr}{An expression}
11 |
12 | \item{env}{The environment in which to evaluate \code{expr}.}
13 |
14 | \item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This is useful
15 | if you want to save an expression in a variable.}
16 | }
17 | \description{
18 | Widget render function for use in Shiny.
19 | }
20 |
--------------------------------------------------------------------------------
/man/sortable.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/sortable-package.R
3 | \docType{package}
4 | \name{sortable}
5 | \alias{sortable}
6 | \alias{sortable-package}
7 | \title{sortable: Drag-and-Drop in 'shiny' Apps with 'SortableJS'}
8 | \description{
9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}}
10 |
11 | Enables drag-and-drop behaviour in Shiny apps, by exposing the functionality of the 'SortableJS' \url{https://sortablejs.github.io/Sortable/} JavaScript library as an 'htmlwidget'. You can use this in Shiny apps and widgets, 'learnr' tutorials as well as R Markdown. In addition, provides a custom 'learnr' question type - 'question_rank()' - that allows ranking questions with drag-and-drop.
12 | }
13 | \section{A new html widget}{
14 |
15 | \itemize{
16 | \item \code{\link[=sortable_js]{sortable_js()}} is a low-level function that adds the \code{SortableJS} to your widgets.
17 | }
18 | }
19 |
20 | \section{Important functions}{
21 |
22 |
23 | The important functions in this package are:
24 | \itemize{
25 | \item \code{\link[=rank_list]{rank_list()}} creates a drag-and-drop, rank list
26 | \item \code{\link[=bucket_list]{bucket_list()}} lets you add multiple \code{rank_list} objects in columns
27 | }
28 | }
29 |
30 | \section{Custom question types for \code{learnr}}{
31 |
32 |
33 | You can also use new question types in your \code{learnr} tutorials:
34 | \itemize{
35 | \item \code{\link[=question_rank]{question_rank()}}
36 | }
37 | }
38 |
39 | \seealso{
40 | Useful links:
41 | \itemize{
42 | \item \url{https://rstudio.github.io/sortable/}
43 | \item Report bugs at \url{https://github.com/rstudio/sortable/issues}
44 | }
45 |
46 | }
47 | \author{
48 | \strong{Maintainer}: Andrie de Vries \email{apdevries@gmail.com}
49 |
50 | Authors:
51 | \itemize{
52 | \item Barret Schloerke \email{barret@rstudio.com}
53 | \item Kenton Russell \email{kent.russell@timelyportfolio.com} (Original author) [conceptor]
54 | }
55 |
56 | Other contributors:
57 | \itemize{
58 | \item RStudio [copyright holder, funder]
59 | \item Lebedev Konstantin ('SortableJS', https://sortablejs.github.io/Sortable/) [copyright holder]
60 | }
61 |
62 | }
63 | \keyword{internal}
64 |
--------------------------------------------------------------------------------
/man/sortable_js.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/sortable_js.R
3 | \name{sortable_js}
4 | \alias{sortable_js}
5 | \title{Creates an htmlwidget with embedded 'SortableJS' library.}
6 | \usage{
7 | sortable_js(
8 | css_id,
9 | options = sortable_options(),
10 | width = 0,
11 | height = 0,
12 | elementId = NULL,
13 | preRenderHook = NULL
14 | )
15 | }
16 | \arguments{
17 | \item{css_id}{\code{String} css_id id on which to apply \code{SortableJS}. Note,
18 | \code{sortable_js} works with any html element, not just \code{ul/li}.}
19 |
20 | \item{options}{Options to be supplied to \link{sortable_js} object. See \link{sortable_options} for more details}
21 |
22 | \item{width}{Fixed width for widget (in css units). The default is
23 | \code{NULL}, which results in intelligent automatic sizing based on the
24 | widget's container.}
25 |
26 | \item{height}{Fixed height for widget (in css units). The default is
27 | \code{NULL}, which results in intelligent automatic sizing based on the
28 | widget's container.}
29 |
30 | \item{elementId}{Use an explicit element ID for the widget (rather than an
31 | automatically generated one). Useful if you have other JavaScript that
32 | needs to explicitly discover and interact with a specific widget instance.}
33 |
34 | \item{preRenderHook}{A function to be run on the widget, just prior to
35 | rendering. It accepts the entire widget object as input, and should return
36 | a modified widget object.}
37 | }
38 | \description{
39 | Creates an \code{htmlwidget} that provides
40 | \href{https://github.com/SortableJS/Sortable}{SortableJS} to use for
41 | drag-and-drop interactivity in Shiny apps and R Markdown.
42 | }
43 | \examples{
44 | ## -- example-sortable-js -------------------------------------------------
45 | # Simple example of sortable_js.
46 | # Important: set the tags CSS `id` equal to the sortable_js `css_id`
47 |
48 | if (interactive()) {
49 | if (require(htmltools)) {
50 | html_print(
51 | tagList(
52 | tags$p("You can drag and reorder the items in this list:"),
53 | tags$ul(
54 | id = "example_1",
55 | tags$li("Move"),
56 | tags$li("Or drag"),
57 | tags$li("Each of the items"),
58 | tags$li("To different positions")
59 | ),
60 | sortable_js(css_id = "example_1")
61 | )
62 | )
63 | }
64 | }
65 | }
66 | \seealso{
67 | \code{\link[=sortable_options]{sortable_options()}}
68 | }
69 |
--------------------------------------------------------------------------------
/man/sortable_js_capture_input.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/js.R
3 | \name{sortable_js_capture_input}
4 | \alias{sortable_js_capture_input}
5 | \alias{sortable_js_capture_bucket_input}
6 | \title{Construct JavaScript method to capture Shiny inputs on change.}
7 | \usage{
8 | sortable_js_capture_input(input_id)
9 |
10 | sortable_js_capture_bucket_input(input_id, input_ids, css_ids)
11 | }
12 | \arguments{
13 | \item{input_id}{Shiny input name to set}
14 |
15 | \item{input_ids}{Set of Shiny input ids to set corresponding to the provided
16 | \code{css_ids}}
17 |
18 | \item{css_ids}{Set of SortableJS \code{css_id} values to help retrieve all to
19 | set as an object}
20 | }
21 | \value{
22 | A character vector with class \code{JS_EVAL}. See \code{\link[htmlwidgets:JS]{htmlwidgets::JS()}}.
23 | }
24 | \description{
25 | This captures the state of a \code{sortable} list. It will look for a \code{data-rank-id}
26 | attribute of the first child for each element. If no? attribute exists for
27 | that particular item's first child, the inner text will be used as an
28 | identifier.
29 | }
30 | \details{
31 | This method is used with the \code{onSort} option of \code{sortable_js}. See
32 | \code{\link[=sortable_options]{sortable_options()}}.
33 | }
34 | \examples{
35 | ## -- example-sortable-js-capture -----------------------------------------
36 | # Simple example of sortable_js_capture.
37 | # Important: set the tags CSS `id` equal to the sortable_js `css_id`
38 |
39 | if(interactive()) {
40 | library(shiny)
41 | library(sortable)
42 |
43 | ui <- fluidPage(
44 | div(
45 | id = "sortable",
46 | div(id = 1, `data-rank-id` = "HELLO", class = "well", "Hello"),
47 | div(id = 2, `data-rank-id` = "WORLD", class = "well", "world")
48 | ),
49 | verbatimTextOutput("chosen"),
50 | sortable_js(
51 | css_id = "sortable",
52 | options = sortable_options(
53 | onSort = sortable_js_capture_input(input_id = "selected")
54 | )
55 | )
56 | )
57 |
58 | server <- function(input, output){
59 | output$chosen <- renderPrint(input$selected)
60 | }
61 |
62 | shinyApp(ui, server)
63 | }
64 |
65 |
66 |
67 | ## ------------------------------------
68 | # For an example, see the Shiny app at
69 | system.file("shiny/drag_vars_to_plot/app.R", package = "sortable")
70 | }
71 | \seealso{
72 | \link{sortable_js} and \link{rank_list}.
73 |
74 | Other JavaScript functions:
75 | \code{\link{chain_js_events}()}
76 | }
77 | \concept{JavaScript functions}
78 |
--------------------------------------------------------------------------------
/man/sortable_options.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/sortable_options.R
3 | \name{sortable_options}
4 | \alias{sortable_options}
5 | \title{Define options to pass to a sortable object.}
6 | \usage{
7 | sortable_options(
8 | ...,
9 | swap = NULL,
10 | multiDrag = NULL,
11 | group = NULL,
12 | sort = NULL,
13 | delay = NULL,
14 | disabled = NULL,
15 | animation = NULL,
16 | handle = NULL,
17 | filter = NULL,
18 | draggable = NULL,
19 | swapThreshold = NULL,
20 | invertSwap = NULL,
21 | direction = NULL,
22 | scrollSensitivity = NULL,
23 | scrollSpeed = NULL,
24 | onStart = NULL,
25 | onEnd = NULL,
26 | onAdd = NULL,
27 | onUpdate = NULL,
28 | onSort = NULL,
29 | onRemove = NULL,
30 | onFilter = NULL,
31 | onMove = NULL,
32 | onLoad = NULL
33 | )
34 | }
35 | \arguments{
36 | \item{...}{other arguments passed onto \code{SortableJS}}
37 |
38 | \item{swap}{If \code{TRUE}, modifies the behaviour of \code{sortable} to allow for items to
39 | be swapped with each other rather than sorted. Once dragging starts, the
40 | user can drag over other items and there will be no change in the elements.
41 | However, the item that the user drops on will be swapped with the
42 | originally dragged item.
43 | See also https://github.com/SortableJS/Sortable/tree/master/plugins/Swap}
44 |
45 | \item{multiDrag}{If \code{TRUE}, allows the selection of multiple items within a
46 | \code{sortable} at once, and drag them as one item. Once placed, the items will
47 | unfold into their original order, but all beside each other at the new
48 | position.
49 | See also https://github.com/SortableJS/Sortable/wiki/Dragging-Multiple-Items-in-Sortable}
50 |
51 | \item{group}{To drag elements from one list into another, both lists must
52 | have the same group value. See
53 | \href{https://github.com/sortablejs/Sortable/#group-option}{Sortable#group-option}
54 | for more details. [\code{"name"}]}
55 |
56 | \item{sort}{Boolean that allows sorting inside a list. [\code{TRUE}]}
57 |
58 | \item{delay}{Time in milliseconds to define when the sorting should start.
59 | [\code{0}]}
60 |
61 | \item{disabled}{Boolean that disables the \code{sortable} if set to true. [\code{FALSE}]}
62 |
63 | \item{animation}{Millisecond duration of the animation of items when sorting
64 | [\code{0} (no animation)]}
65 |
66 | \item{handle}{CSS selector used for the drag handle selector within list
67 | items. [\code{".my-handle"}]}
68 |
69 | \item{filter}{CSS selector or JS function used for elements that cannot be
70 | dragged. [\code{".ignore-elements"}]}
71 |
72 | \item{draggable}{CSS selector of which items inside the element should be
73 | draggable. [\code{".item"}]}
74 |
75 | \item{swapThreshold}{Percentage of the target that the swap zone will take
76 | up, as a number between \code{0} and \code{1}. [\code{1}]}
77 |
78 | \item{invertSwap}{Set to \code{TRUE} to set the swap zone to the sides of the
79 | target, for the effect of sorting "in between" items. [\code{FALSE}]}
80 |
81 | \item{direction}{Direction of \code{sortable} [\code{"horizontal"}]}
82 |
83 | \item{scrollSensitivity}{Number of pixels the mouse needs to be to an edge to
84 | start scrolling. [\code{30}]}
85 |
86 | \item{scrollSpeed}{Number of pixels for the speed of scrolling. [\code{10}]}
87 |
88 | \item{onStart, onEnd}{JS function called when an element dragging starts or ends}
89 |
90 | \item{onAdd}{JS function called when an element is dropped into the list from
91 | another list}
92 |
93 | \item{onUpdate}{JS function called when the sorting is changed within a list}
94 |
95 | \item{onSort}{JS function called by any change to the list (add / update /
96 | remove)}
97 |
98 | \item{onRemove}{JS function called when an element is removed from the list
99 | into another list}
100 |
101 | \item{onFilter}{JS function called when an attempt is made to drag a filtered
102 | element}
103 |
104 | \item{onMove}{JS function called when an item is moved in a list or between
105 | lists}
106 |
107 | \item{onLoad}{JS function dispatched on the "next tick" after SortableJS has
108 | initialized}
109 | }
110 | \value{
111 | A list with class \code{sortable_options}
112 | }
113 | \description{
114 | Use this function to define the options for \link{sortable_js} and \link{rank_list},
115 | which will pass these in turn to the \code{SortableJS} JavaScript library.
116 | }
117 | \details{
118 | Many of the \code{SortableJS} options will accept a JavaScript function. You can
119 | do this using the \code{htmlwidgets::JS} function.
120 | }
121 | \examples{
122 | sortable_options(sort = FALSE)
123 | }
124 | \references{
125 | \url{https://github.com/sortablejs/Sortable/}
126 | }
127 | \seealso{
128 | \link{sortable_js}
129 | }
130 |
--------------------------------------------------------------------------------
/man/sortable_output.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/sortable_js.R
3 | \name{sortable_output}
4 | \alias{sortable_output}
5 | \title{Widget output function for use in Shiny.}
6 | \usage{
7 | sortable_output(input_id, width = "0px", height = "0px")
8 | }
9 | \arguments{
10 | \item{input_id}{output variable to use for the sortable object}
11 |
12 | \item{width}{Fixed width for widget (in css units). The default is
13 | \code{NULL}, which results in intelligent automatic sizing based on the
14 | widget's container.}
15 |
16 | \item{height}{Fixed height for widget (in css units). The default is
17 | \code{NULL}, which results in intelligent automatic sizing based on the
18 | widget's container.}
19 | }
20 | \description{
21 | Widget output function for use in Shiny.
22 | }
23 |
--------------------------------------------------------------------------------
/man/update_bucket_list.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/rank_list.R
3 | \name{update_bucket_list}
4 | \alias{update_bucket_list}
5 | \title{Change the value of a bucket list.}
6 | \usage{
7 | update_bucket_list(
8 | css_id,
9 | header = NULL,
10 | session = shiny::getDefaultReactiveDomain()
11 | )
12 | }
13 | \arguments{
14 | \item{css_id}{This is the css id to use, and must be unique in your shiny
15 | app. This defaults to the value of \code{group_id}, and will be appended to the
16 | value "bucket-list-container", to ensure the CSS id is unique for the
17 | container as well as the embedded rank lists.}
18 |
19 | \item{header}{Text that appears at the top of the bucket list. (This is
20 | encoded as an HTML \verb{
} tag, so not strictly speaking a header.) Note 21 | that you must explicitly provide \code{header} argument, especially in the case 22 | where you want the header to be empty - to do this use \code{header = NULL} or 23 | \code{header = NA}.} 24 | 25 | \item{session}{The \code{session} object passed to function given to 26 | \code{shinyServer}.} 27 | } 28 | \description{ 29 | You can only update the \code{header} of the \code{bucket_list}. 30 | To update any of the labels or rank list text, use \code{update_rank_list()} 31 | instead. 32 | } 33 | \examples{ 34 | ## Example of a shiny app that updates a bucket list and rank list 35 | if (interactive()) { 36 | app <- system.file( 37 | "shiny/update/app.R", 38 | package = "sortable" 39 | ) 40 | shiny::runApp(app) 41 | } 42 | } 43 | \seealso{ 44 | \link{bucket_list}, \link{update_rank_list} 45 | } 46 | -------------------------------------------------------------------------------- /man/update_rank_list.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rank_list.R 3 | \name{update_rank_list} 4 | \alias{update_rank_list} 5 | \title{Change the text or labels of a rank list.} 6 | \usage{ 7 | update_rank_list( 8 | css_id, 9 | text = NULL, 10 | labels = NULL, 11 | session = shiny::getDefaultReactiveDomain() 12 | ) 13 | } 14 | \arguments{ 15 | \item{css_id}{This is the css id to use, and must be unique in your shiny 16 | app. This defaults to the value of \code{input_id}, and will be appended to the 17 | value "rank-list-container", to ensure the CSS id is unique for the 18 | container as well as the labels. 19 | If NULL, the function generates an id of the form 20 | \code{rank_list_id_1}, and will automatically increment for every \code{rank_list}.} 21 | 22 | \item{text}{Text to appear at top of list.} 23 | 24 | \item{labels}{A character vector with the text to display inside the widget. 25 | This can also be a list of html tag elements. The text content of each 26 | label or label name will be used to set the shiny \code{input_id} value. 27 | To create an empty \code{rank_list}, use \code{labels = list()}.} 28 | 29 | \item{session}{The \code{session} object passed to function given to 30 | \code{shinyServer}.} 31 | } 32 | \description{ 33 | Change the text or labels of a rank list. 34 | } 35 | \examples{ 36 | ## Example of a shiny app that updates a bucket list and rank list 37 | if (interactive()) { 38 | app <- system.file( 39 | "shiny/update_rank_list/app.R", 40 | package = "sortable" 41 | ) 42 | shiny::runApp(app) 43 | } 44 | } 45 | \seealso{ 46 | \link{rank_list} 47 | } 48 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/sortable/2b938859b4c44a2bd4cf99628023590e8cdd4910/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /scripts/build_docs.R: -------------------------------------------------------------------------------- 1 | # compile readme 2 | rmarkdown::render("README.Rmd", rmarkdown::github_document(html_preview = FALSE)) 3 | 4 | # build website locally 5 | devtools::install() # files are retrieved from system.file location 6 | pkgdown::build_site() 7 | -------------------------------------------------------------------------------- /scripts/compile_css.R: -------------------------------------------------------------------------------- 1 | 2 | local({ 3 | if (!require(sass)) { 4 | install.packages("sass") 5 | } 6 | library(sass) 7 | 8 | 9 | scss_files <- dir( 10 | file.path("inst", "htmlwidgets", "plugins", "sortable-rstudio"), 11 | pattern = "\\.scss", 12 | full.names = TRUE 13 | ) 14 | 15 | for (scss_file in scss_files) { 16 | if (grepl("^_", basename(scss_file))) { 17 | message("Skipping : ", basename(scss_file)) 18 | NULL 19 | } else { 20 | message("Translating: ", basename(scss_file)) 21 | sass::sass( 22 | input = sass::sass_file(scss_file), 23 | output = sub("\\.scss", ".css", scss_file) 24 | ) 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /scripts/deploy_apps.R: -------------------------------------------------------------------------------- 1 | # install rsconnect and glue 2 | if (!requireNamespace("remotes")) install.packages("remotes") 3 | if (!requireNamespace("rsconnect")) remotes::install_cran("rsconnect") 4 | if (!requireNamespace("glue")) remotes::install_cran("glue") 5 | 6 | # Set the account info for deployment. 7 | rsconnect::setAccountInfo( 8 | name = Sys.getenv("SHINYAPPS_NAME"), 9 | token = Sys.getenv("SHINYAPPS_TOKEN"), 10 | secret = Sys.getenv("SHINYAPPS_SECRET") 11 | ) 12 | 13 | deploy_app <- function( 14 | app_dir, 15 | name = glue::glue("sortable_{basename(app_dir)}_app"), 16 | ... 17 | ) { 18 | server <- "shinyapps.io" 19 | account <- "andrie-de-vries" 20 | 21 | all_apps <- rsconnect::applications(account = account, server = server) 22 | last_deployed <- all_apps[all_apps[["name"]] == name, "updated_time"] 23 | last_updated <- max(file.mtime(list.files(app_dir, recursive = TRUE, full.names = TRUE))) 24 | 25 | last_deployed <- as.POSIXct(last_deployed, format = "%FT%T") 26 | 27 | if (!length(last_deployed) || last_updated > last_deployed) { 28 | message("\n") 29 | message("Deploying: ", name) 30 | message("\n") 31 | rsconnect::deployApp( 32 | appDir = app_dir, 33 | appName = name, 34 | server = server, 35 | account = account, 36 | forceUpdate = TRUE, 37 | ... 38 | ) 39 | } else { 40 | message("Nothing to do for ", name) 41 | } 42 | } 43 | 44 | deploy_tutorial <- function( 45 | app_dir, 46 | doc = dir(app_dir, pattern = "\\.Rmd$")[1], 47 | name = glue::glue("sortable_tutorial_{basename(app_dir)}") 48 | ) { 49 | deploy_app( 50 | app_dir = app_dir, 51 | name = name, 52 | appPrimaryDoc = dir(app_dir, pattern = "\\.Rmd$")[1] 53 | ) 54 | } 55 | 56 | 57 | deploy_folder <- function(path, fn) { 58 | invisible(lapply( 59 | dir(path, full.names = TRUE), 60 | function(path) { 61 | if (dir.exists(path)) { 62 | fn(path) 63 | } 64 | } 65 | )) 66 | } 67 | 68 | 69 | 70 | deploy_folder(system.file("shiny", package = "sortable"), deploy_app) 71 | deploy_folder(system.file("shiny", package = "sortable"), deploy_tutorial) 72 | 73 | message("done") 74 | -------------------------------------------------------------------------------- /scripts/download_sortablejs.R: -------------------------------------------------------------------------------- 1 | 2 | get_new_sortable_version <- function() { 3 | x <- readLines("https://unpkg.com/sortablejs", n = 1) 4 | # sub("^.* Sortable (.*?) - MIT.*$", "\\1", x) 5 | sub(".*(\\d+\\.\\d+\\.\\d+).*", "\\1", x) 6 | } 7 | get_new_sortable_version() 8 | 9 | sortable_version <- "1.15.3" 10 | yaml_version <- sortable_version 11 | 12 | 13 | writeLines( 14 | readLines(paste0("https://unpkg.com/sortablejs@", sortable_version), warn = FALSE), 15 | file.path("inst", "htmlwidgets", "lib", "sortable", "sortable.js") 16 | ) 17 | 18 | sortable_yaml_file <- file.path("inst", "htmlwidgets", "sortable.yaml") 19 | config <- yaml::read_yaml(sortable_yaml_file) 20 | config$dependencies[[1]]$version <- yaml_version 21 | yaml::write_yaml(config, sortable_yaml_file) 22 | 23 | 24 | -------------------------------------------------------------------------------- /scripts/load_all_shim.R: -------------------------------------------------------------------------------- 1 | # Makes it easy to test the package in development by shimming `htmlwidgets` 2 | # and `htmltools`, before `load_all()`. 3 | # 4 | # Solution modified from Winston Chang 5 | # (https://gist.github.com/wch/c942335660dc6c96322f) 6 | 7 | local({ 8 | 9 | shim_system_file <- function(package) { 10 | imports <- parent.env(asNamespace(package)) 11 | pkgload:::unlock_environment(imports) 12 | imports$system.file <- pkgload:::shim_system.file 13 | } 14 | 15 | desc <- here::here("DESCRIPTION") 16 | if (file.exists(desc)) { 17 | 18 | pkgs <- desc::desc_get_deps()$package 19 | 20 | if ("htmlwidgets" %in% pkgs) { 21 | message("shimming htmlwidgets") 22 | shim_system_file("htmlwidgets") 23 | 24 | } 25 | if ("htmltools" %in% pkgs) { 26 | message("shimming htmltools") 27 | shim_system_file("htmltools") 28 | } 29 | } 30 | 31 | # devtools::load_all() 32 | 33 | }) 34 | 35 | -------------------------------------------------------------------------------- /scripts/readme.md: -------------------------------------------------------------------------------- 1 | ## Developer scripts 2 | 3 | All script should be run from the root directory, such as `source("scripts/compile_css.R")`. 4 | 5 | 6 | * `load_all_shim.R` 7 | * Adds a shim to source local htmlwidget files and calls `devtools::load_all()` 8 | 9 | * `compile_css.R` 10 | * Compiles all Sass code into css 11 | 12 | * `deploy_apps.R` 13 | * Deploys all shiny application examples to shinyapps.io 14 | * Deploys all learnr tutorials to shinyapps.io 15 | 16 | * `deploy_apps.R` 17 | * Calls `deploy_apps.R` 18 | * Called within Travis-CI. 19 | 20 | * `download_sortablejs.R` 21 | * Downloads a `sortable.js` file and updates the appropriate versions 22 | 23 | * `build_docs.R` 24 | * Build the README.Rmd 25 | * Installs the pkg to avoid docs issues 26 | * Build the pkgdocs locally (which is `.gitignore`'d) 27 | -------------------------------------------------------------------------------- /sortable.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: XeLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageCheckArgs: --as-cran 22 | PackageRoxygenize: rd,collate,namespace,vignette 23 | -------------------------------------------------------------------------------- /tests/spelling.R: -------------------------------------------------------------------------------- 1 | if(requireNamespace('spelling', quietly = TRUE)) 2 | spelling::spell_check_test(vignettes = TRUE, error = FALSE, 3 | skip_on_cran = TRUE) 4 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(sortable) 3 | 4 | test_check("sortable") 5 | -------------------------------------------------------------------------------- /tests/testthat/test-bucket_list.R: -------------------------------------------------------------------------------- 1 | test_that("Can use add_rank_list", { 2 | z <- add_rank_list(text = "missing", labels = NULL, input_id = NULL) 3 | expect_s3_class(z, "add_rank_list") 4 | }) 5 | 6 | 7 | test_that("Can create bucket_list", { 8 | 9 | z <- bucket_list( 10 | header = "This is a bucket list. You can drag items between the lists.", 11 | add_rank_list( 12 | text = "Drag from here", 13 | labels = c("a", "bb", "ccc") 14 | ), 15 | add_rank_list( 16 | text = "to here", 17 | labels = NULL, 18 | input_id = "input_to" 19 | ) 20 | ) 21 | expect_s3_class(z, "bucket_list") 22 | 23 | 24 | expect_error( 25 | bucket_list( 26 | # header = "This is a bucket list. You can drag items between the lists.", 27 | add_rank_list( 28 | text = "Drag from here", 29 | labels = c("a", "bb", "ccc") 30 | ), 31 | add_rank_list( 32 | text = "to here", 33 | labels = NULL 34 | ) 35 | ), 36 | "must be NULL or a string" 37 | ) 38 | 39 | z <- bucket_list( 40 | header = NA, 41 | add_rank_list( 42 | text = "Drag from here", 43 | labels = c("a", "bb", "ccc") 44 | ), 45 | add_rank_list( 46 | text = "to here", 47 | labels = NULL 48 | ) 49 | ) 50 | expect_s3_class(z, "bucket_list") 51 | 52 | }) 53 | 54 | -------------------------------------------------------------------------------- /tests/testthat/test-creation.R: -------------------------------------------------------------------------------- 1 | test_that( "sortable_js makes a htmlwidget ", { 2 | expect_s3_class( sortable_js( "" ), "htmlwidget" ) 3 | expect_s3_class( sortable_js( "" ), "sortable" ) 4 | }) 5 | 6 | test_that( "sortable_js height and width", { 7 | # by default sortable_js should be 0 height and width 8 | # since intended to be used to provide dependencies 9 | # and pass config options 10 | expect_equal( sortable_js( "" )$width, 0 ) 11 | expect_equal( sortable_js( "" )$height, 0 ) 12 | # however, someone might want to override height/width 13 | expect_equal( sortable_js( "", width = 100 )$width, 100 ) 14 | expect_equal( sortable_js( "", height = 100 )$height, 100 ) 15 | }) 16 | 17 | test_that( "css_id and options passed as expected", { 18 | expect_identical( sortable_js( "an_id" )$x$css_id, "an_id" ) 19 | expect_identical( 20 | sortable_js( 21 | "an_id", 22 | options = sortable_options( 23 | group = "name", 24 | sort = FALSE, 25 | disabled = FALSE 26 | ) 27 | )$x, 28 | list( 29 | css_id = "an_id", 30 | options = modifyList( 31 | default_sortable_options(), 32 | sortable_options( 33 | group = "name", 34 | sort = FALSE, 35 | disabled = FALSE 36 | ) 37 | ) 38 | ) 39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/testthat/test-htmltools.R: -------------------------------------------------------------------------------- 1 | library(htmltools) 2 | test_that( "works with tags ", { 3 | z <- html_print( 4 | viewer = NULL, 5 | tagList( 6 | tags$h1( "Check to make sure items move"), 7 | HTML(" 8 |
You can drag and drop these items in any order (try it!):
63 |You can drag and drop these items in any order (try it!):
79 |