diff --git a/.Rbuildignore b/.Rbuildignore index aa1e0a8..3419c2a 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -6,3 +6,4 @@ ^\.travis\.yml$ ^cran-comments\.md$ ^docs$ +^\.github$ diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 0000000..2d19fc7 --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..a3ac618 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,49 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +name: R-CMD-check + +jobs: + R-CMD-check: + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + strategy: + fail-fast: false + matrix: + config: + - {os: macos-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes + + steps: + - uses: actions/checkout@v3 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck + needs: check + + - uses: r-lib/actions/check-r-package@v2 + with: + upload-snapshots: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8d139ac..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -# R for travis: see documentation at https://docs.travis-ci.com/user/languages/r - -language: R -sudo: false -cache: packages diff --git a/DESCRIPTION b/DESCRIPTION index cf38464..7c61efa 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: listviewer Type: Package Title: 'htmlwidget' for Interactive Views of R Lists -Version: 3.0.0 -Date: 2019-11-02 +Version: 4.0.0 +Date: 2023-09-30 Authors@R: c( person("Jos", "de Jong", role = c("aut", "cph"), comment = "jsoneditor.js library in htmlwidgets/jsoneditor, http://github.com/josdejong/jsoneditor/" ), person("Mac"," Gainer", role = c("aut", "cph"), comment = "react-json-view library in htmlwidgets/react-json, https://github.com/mac-s-g/react-json-view"), @@ -27,4 +27,4 @@ Suggests: rstudioapi Enhances: reactR -RoxygenNote: 6.1.1 +RoxygenNote: 7.2.3 diff --git a/NEWS.md b/NEWS.md index bbac629..bce9bbc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# listviewer 4.0.0 + +* update `jsoneditor` to completely rebuilt [0.18.7](https://github.com/josdejong/svelte-jsoneditor/releases/tag/v0.18.7) + # listviewer 3.0.0 *API Changes* diff --git a/R/jsonedit.R b/R/jsonedit.R index a79150c..3fcc735 100644 --- a/R/jsonedit.R +++ b/R/jsonedit.R @@ -10,7 +10,7 @@ #' \code{listdata} could be a \code{String} of valid \code{JSON}. This might be helpful #' when dealing with an API response. #' @param mode \code{string} for the initial view from \code{modes}. \code{'tree'} is the default. -#' @param modes \code{string} \code{c('code', 'form', 'text', 'tree', 'view')} will be the default, since +#' @param modes \code{string} \code{c('tree', 'text', 'table')} will be the default, since #' these are all the modes currently supported by \code{jsoneditor}. #' @param ... \code{list} of other options for \code{jsoneditor}. This is a temporary way #' of trying other options in \code{jsoneditor}. In the future, this will be eliminated @@ -38,7 +38,7 @@ #' #' # jsonedit also works with a JSON string #' jsonedit( -#' '{"array" : [1,2,3] , "boolean" : true, "null" : null, number = 123}' +#' '{"array" : [1,2,3] , "boolean" : true, "null" : null, "number": 123}' #' ) #' #' # also works with most data.frames @@ -53,7 +53,7 @@ jsonedit <- function( listdata = NULL , mode = 'tree' - , modes = c('code', 'form', 'text', 'tree', 'view') + , modes = c('text', 'tree', 'table') , ... , width = NULL , height = NULL diff --git a/R/jsonedit_gadget.R b/R/jsonedit_gadget.R index 5637ff9..34f9ac6 100644 --- a/R/jsonedit_gadget.R +++ b/R/jsonedit_gadget.R @@ -23,19 +23,18 @@ jsonedit_gadget <- function(..., height = NULL, width = NULL) { } stopifnot(requireNamespace("miniUI"), requireNamespace("shiny")) ui <- miniUI::miniPage( - miniUI::miniContentPanel(jsonedit(...), height=NULL, width=NULL), + miniUI::miniContentPanel( + jsonedit( + ..., + onChange = htmlwidgets::JS('function(after, before, patch) { + Shiny.onInputChange("jsoneditordata", after.json); + }') + ), + height=NULL, + width=NULL + ), - miniUI::gadgetTitleBar("Edit Data", right = miniUI::miniTitleBarButton("done", "Done", primary = TRUE)), - - htmltools::tags$script(' -document.getElementById("done").onclick = function() { - var listdata = JSON.parse( - HTMLWidgets.find(".jsonedit").editor.getText() - ); - Shiny.onInputChange("jsoneditordata", listdata); -}; -' - ) + miniUI::gadgetTitleBar("Edit Data", right = miniUI::miniTitleBarButton("done", "Done", primary = TRUE)) ) server <- function(input, output, session) { diff --git a/README.md b/README.md index 7ca2b0d..d48e408 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -[![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/listviewer)](https://cran.r-project.org/package=listviewer) [![Travis-CI Build Status](https://travis-ci.org/timelyportfolio/listviewer.svg?branch=master)](https://travis-ci.org/timelyportfolio/listviewer) +[![CRAN_Status_Badge](http://www.r-pkg.org/badges/version/listviewer)](https://cran.r-project.org/package=listviewer) + +[![R-CMD-check](https://github.com/timelyportfolio/listviewer/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/timelyportfolio/listviewer/actions/workflows/R-CMD-check.yaml) # listviewer A package of R htmlwidgets to interactively view *and maybe modify* `lists`. As of now, `listviewer` provides an interface to [`jsoneditor`](https://github.com/josdejong/jsoneditor) and [`react-json-view`](https://github.com/mac-s-g/react-json-view). `listviewer` is designed to support multiple interfaces. @@ -124,9 +126,9 @@ ui <- shinyUI( server <- function(input,output){ output$jsed <- renderJsonedit({ jsonedit( - as.list( .GlobalEnv ) - ,"change" = htmlwidgets::JS('function(){ - console.log( event.currentTarget.parentNode.editor.get() ) + jsonlite::toJSON(mtcars, auto_unbox = TRUE, data.frame = "rows") + ,"onChange" = htmlwidgets::JS('function(after, before, patch){ + console.log( after.json ) }') ) @@ -152,7 +154,7 @@ ui <- shinyUI( server <- function(input,output){ output$rjed <- renderReactjson({ - reactjson( as.list( .GlobalEnv ) ) + reactjson( jsonlite::toJSON(mtcars, auto_unbox = TRUE, data.frame = "rows") ) }) observeEvent(input$rjed_edit, { diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..49a3e67 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,103 @@ + + + + + + + +Page not found (404) • listviewer + + + + + + + + + + + +
+
+ + + + +
+
+ + +Content not found. Please use links in the navbar. + +
+ + + +
+ + + + +
+ + + + + + + + diff --git a/docs/CONDUCT.html b/docs/CONDUCT.html index 7160ecd..afd1fd6 100644 --- a/docs/CONDUCT.html +++ b/docs/CONDUCT.html @@ -1,55 +1,12 @@ - - - - - - - -Contributor Code of Conduct • listviewer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Contributor Code of Conduct • listviewer + + - - - -
-
- -
-
+
+ +
-
- + +
+ + + - - + diff --git a/docs/LICENSE-text.html b/docs/LICENSE-text.html index 26c376d..9c03795 100644 --- a/docs/LICENSE-text.html +++ b/docs/LICENSE-text.html @@ -1,55 +1,12 @@ - - - - - - - -License • listviewer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -License • listviewer + + - - - -
-
- -
-
+
+ +
-
- + +
+ + + - - + diff --git a/docs/authors.html b/docs/authors.html index a1f7490..ffea285 100644 --- a/docs/authors.html +++ b/docs/authors.html @@ -1,55 +1,12 @@ - - - - - - - -Authors • listviewer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Authors and Citation • listviewer + + - - - - -
-
-
-
+
-
-
- + +
+ + + - - + diff --git a/docs/bootstrap-toc.css b/docs/bootstrap-toc.css new file mode 100644 index 0000000..5a85941 --- /dev/null +++ b/docs/bootstrap-toc.css @@ -0,0 +1,60 @@ +/*! + * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) + * Copyright 2015 Aidan Feldman + * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ + +/* modified from https://github.com/twbs/bootstrap/blob/94b4076dd2efba9af71f0b18d4ee4b163aa9e0dd/docs/assets/css/src/docs.css#L548-L601 */ + +/* All levels of nav */ +nav[data-toggle='toc'] .nav > li > a { + display: block; + padding: 4px 20px; + font-size: 13px; + font-weight: 500; + color: #767676; +} +nav[data-toggle='toc'] .nav > li > a:hover, +nav[data-toggle='toc'] .nav > li > a:focus { + padding-left: 19px; + color: #563d7c; + text-decoration: none; + background-color: transparent; + border-left: 1px solid #563d7c; +} +nav[data-toggle='toc'] .nav > .active > a, +nav[data-toggle='toc'] .nav > .active:hover > a, +nav[data-toggle='toc'] .nav > .active:focus > a { + padding-left: 18px; + font-weight: bold; + color: #563d7c; + background-color: transparent; + border-left: 2px solid #563d7c; +} + +/* Nav: second level (shown on .active) */ +nav[data-toggle='toc'] .nav .nav { + display: none; /* Hide by default, but at >768px, show it */ + padding-bottom: 10px; +} +nav[data-toggle='toc'] .nav .nav > li > a { + padding-top: 1px; + padding-bottom: 1px; + padding-left: 30px; + font-size: 12px; + font-weight: normal; +} +nav[data-toggle='toc'] .nav .nav > li > a:hover, +nav[data-toggle='toc'] .nav .nav > li > a:focus { + padding-left: 29px; +} +nav[data-toggle='toc'] .nav .nav > .active > a, +nav[data-toggle='toc'] .nav .nav > .active:hover > a, +nav[data-toggle='toc'] .nav .nav > .active:focus > a { + padding-left: 28px; + font-weight: 500; +} + +/* from https://github.com/twbs/bootstrap/blob/e38f066d8c203c3e032da0ff23cd2d6098ee2dd6/docs/assets/css/src/docs.css#L631-L634 */ +nav[data-toggle='toc'] .nav > .active > ul { + display: block; +} diff --git a/docs/bootstrap-toc.js b/docs/bootstrap-toc.js new file mode 100644 index 0000000..1cdd573 --- /dev/null +++ b/docs/bootstrap-toc.js @@ -0,0 +1,159 @@ +/*! + * Bootstrap Table of Contents v0.4.1 (http://afeld.github.io/bootstrap-toc/) + * Copyright 2015 Aidan Feldman + * Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */ +(function() { + 'use strict'; + + window.Toc = { + helpers: { + // return all matching elements in the set, or their descendants + findOrFilter: function($el, selector) { + // http://danielnouri.org/notes/2011/03/14/a-jquery-find-that-also-finds-the-root-element/ + // http://stackoverflow.com/a/12731439/358804 + var $descendants = $el.find(selector); + return $el.filter(selector).add($descendants).filter(':not([data-toc-skip])'); + }, + + generateUniqueIdBase: function(el) { + var text = $(el).text(); + var anchor = text.trim().toLowerCase().replace(/[^A-Za-z0-9]+/g, '-'); + return anchor || el.tagName.toLowerCase(); + }, + + generateUniqueId: function(el) { + var anchorBase = this.generateUniqueIdBase(el); + for (var i = 0; ; i++) { + var anchor = anchorBase; + if (i > 0) { + // add suffix + anchor += '-' + i; + } + // check if ID already exists + if (!document.getElementById(anchor)) { + return anchor; + } + } + }, + + generateAnchor: function(el) { + if (el.id) { + return el.id; + } else { + var anchor = this.generateUniqueId(el); + el.id = anchor; + return anchor; + } + }, + + createNavList: function() { + return $(''); + }, + + createChildNavList: function($parent) { + var $childList = this.createNavList(); + $parent.append($childList); + return $childList; + }, + + generateNavEl: function(anchor, text) { + var $a = $(''); + $a.attr('href', '#' + anchor); + $a.text(text); + var $li = $('
  • '); + $li.append($a); + return $li; + }, + + generateNavItem: function(headingEl) { + var anchor = this.generateAnchor(headingEl); + var $heading = $(headingEl); + var text = $heading.data('toc-text') || $heading.text(); + return this.generateNavEl(anchor, text); + }, + + // Find the first heading level (`

    `, then `

    `, etc.) that has more than one element. Defaults to 1 (for `

    `). + getTopLevel: function($scope) { + for (var i = 1; i <= 6; i++) { + var $headings = this.findOrFilter($scope, 'h' + i); + if ($headings.length > 1) { + return i; + } + } + + return 1; + }, + + // returns the elements for the top level, and the next below it + getHeadings: function($scope, topLevel) { + var topSelector = 'h' + topLevel; + + var secondaryLevel = topLevel + 1; + var secondarySelector = 'h' + secondaryLevel; + + return this.findOrFilter($scope, topSelector + ',' + secondarySelector); + }, + + getNavLevel: function(el) { + return parseInt(el.tagName.charAt(1), 10); + }, + + populateNav: function($topContext, topLevel, $headings) { + var $context = $topContext; + var $prevNav; + + var helpers = this; + $headings.each(function(i, el) { + var $newNav = helpers.generateNavItem(el); + var navLevel = helpers.getNavLevel(el); + + // determine the proper $context + if (navLevel === topLevel) { + // use top level + $context = $topContext; + } else if ($prevNav && $context === $topContext) { + // create a new level of the tree and switch to it + $context = helpers.createChildNavList($prevNav); + } // else use the current $context + + $context.append($newNav); + + $prevNav = $newNav; + }); + }, + + parseOps: function(arg) { + var opts; + if (arg.jquery) { + opts = { + $nav: arg + }; + } else { + opts = arg; + } + opts.$scope = opts.$scope || $(document.body); + return opts; + } + }, + + // accepts a jQuery object, or an options object + init: function(opts) { + opts = this.helpers.parseOps(opts); + + // ensure that the data attribute is in place for styling + opts.$nav.attr('data-toggle', 'toc'); + + var $topContext = this.helpers.createChildNavList(opts.$nav); + var topLevel = this.helpers.getTopLevel(opts.$scope); + var $headings = this.helpers.getHeadings(opts.$scope, topLevel); + this.helpers.populateNav($topContext, topLevel, $headings); + } + }; + + $(function() { + $('nav[data-toggle="toc"]').each(function(i, el) { + var $nav = $(el); + Toc.init($nav); + }); + }); +})(); diff --git a/docs/index.html b/docs/index.html index 47a66ef..eec1531 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,23 +5,26 @@ -'htmlwidget' for Interactive Views of R Lists • listviewer - - - - +htmlwidget for Interactive Views of R Lists • listviewer + + + + + + - + lists. The function reactjson() requires a package + reactR that can be installed from CRAN or <https://github.com/timelyportfolio/reactR>."> - + + +
    -
    - -

    A package of R htmlwidgets to interactively view and maybe modify lists. As of now, listviewer provides an interface to jsoneditor and react-json-view. listviewer is designed to support multiple interfaces.

    -
    -

    -install

    +

    R-CMD-check

    +
    + +

    A package of R htmlwidgets to interactively view and maybe modify lists. As of now, listviewer provides an interface to jsoneditor and react-json-view. listviewer is designed to support multiple interfaces.

    +
    +

    install +

    CRAN

    -
    install.packages("listviewer")
    +
    +install.packages("listviewer")

    Development Version

    -
    devtools::install_github("timelyportfolio/listviewer")
    +
    +devtools::install_github("timelyportfolio/listviewer")
    -
    -

    -jsoneditor

    -

    jsoneditor is a really well designed JSON interactive editor by Jos de Jong. Since most R data can be represented in JSON, we can use this great JavaScript library in R.

    - - - - -

    See the above interactive view of par for yourself.

    +
    +

    jsoneditor +

    +

    jsoneditor is a really well designed JSON interactive editor by Jos de Jong. Since most R data can be represented in JSON, we can use this great JavaScript library in R.

    +
    +# using the data from the jsoneditor simple example
    +#  in R list form
    +
    +library(listviewer)
    +
    +jsonedit(
    +  list(
    +    array = c(1,2,3)
    +    ,boolean = TRUE
    +    ,null = NULL
    +    ,number = 123
    +    ,object = list( a="b", c="d" )
    +    ,string = "Hello World"
    +  )
    +)
    +
    +# also works with data.frames
    +jsonedit( mtcars )
    +
    +# helpful interactive view of par
    +jsonedit( par() )
    +
    +# meta view of the above
    +jsonedit(jsonedit(par()))
    +

    See the above interactive view of par for yourself.

    I got this idea courtesy of @jasonpbecker on Twitter. htmlwidgets dependencies are defined by YAML. Let’s see the dependencies for jsonedit.

    -
    jsonedit(
    -  yaml.load_file(system.file("htmlwidgets/jsonedit.yaml",package="listviewer"))
    -)
    +
    +jsonedit(
    +  yaml.load_file(system.file("htmlwidgets/jsonedit.yaml",package="listviewer"))
    +)

    How about topojson?

    - +
    +### experiment with topojson
    +library(httr)
    +library(pipeR)
    +library(listviewer)
    +
    +# topojson for Afghanistan
    +url_path = "https://gist.githubusercontent.com/markmarkoh/8856417/raw/6178d18115d9f273656d294a867c3f83b739a951/customAfghanMap.topo.json"
    +
    +url_path %>>% 
    +  GET %>>%
    +  content( as = "text") %>>%
    +  jsonedit
    -
    -

    -reactjson

    -

    react-json-view is another very nice JSON interactive editor. We even get copy/paste! All of the above examples should also work with reactjson.

    - +
    +

    reactjson +

    +

    react-json-view is another very nice JSON interactive editor. We even get copy/paste! All of the above examples should also work with reactjson.

    +
    +# using the data from the jsoneditor simple example
    +#  in R list form
    +
    +library(listviewer)
    +
    +reactjson(
    +  list(
    +    array = c(1,2,3)
    +    ,boolean = TRUE
    +    ,null = NULL
    +    ,number = 123
    +    ,object = list( a="b", c="d" )
    +    ,string = "Hello World"
    +  )
    +)
    -
    -

    -Shiny example

    +
    +

    Shiny example +

    listviewer works with Shiny but the implementation is crude and likely to change for jsonedit while reactjson integration is much better. If you really want to use jsonedit with Shiny, I would recommend debouncing the change callback. Here are examples with each.

    - - +
    +library(shiny)
    +library(listviewer)
    +
    +# put some data in environment so it will show up
    +data(mtcars)
    +
    +ui <- shinyUI(
    +  fluidPage(
    +    jsoneditOutput( "jsed" )
    +  )
    +)
    +
    +server <- function(input,output){
    +  output$jsed <- renderJsonedit({
    +    jsonedit(
    +      jsonlite::toJSON(mtcars, auto_unbox = TRUE, data.frame = "rows")
    +      ,"onChange" = htmlwidgets::JS('function(after, before, patch){
    +        console.log( after.json )
    +      }')
    +    )
    +    
    +  })
    +}
    +
    +runApp( list( ui = ui, server = server ) )
    +
    +library(shiny)
    +library(listviewer)
    +
    +# put some data in environment so it will show up
    +data(mtcars)
    +
    +ui <- shinyUI(
    +  fluidPage(
    +    reactjsonOutput( "rjed" )
    +  )
    +)
    +
    +server <- function(input,output){
    +  output$rjed <- renderReactjson({
    +    reactjson( jsonlite::toJSON(mtcars, auto_unbox = TRUE, data.frame = "rows") )
    +  })
    +  
    +  observeEvent(input$rjed_edit, {
    +    str(input$rjed_edit, max.level=2)
    +  })
    +}
    +
    +runApp( list( ui = ui, server = server ) )
    -
    -

    -code of conduct

    -

    Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

    +
    +

    code of conduct +

    +

    Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

    -