From 119043652381100135ca98d3b855688181b21438 Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Mon, 31 Jul 2023 22:29:26 -0300 Subject: [PATCH 1/9] First approach on IntersectionObserver --- inst/emptystate2.js | 129 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 inst/emptystate2.js diff --git a/inst/emptystate2.js b/inst/emptystate2.js new file mode 100644 index 0000000..8686219 --- /dev/null +++ b/inst/emptystate2.js @@ -0,0 +1,129 @@ +const intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + const emptyStateContainer = entry.target.querySelector(".empty-state-container"); + const parentElement = emptyStateContainer.parentElement; + const isVisible = entry.isIntersecting; + + if (isVisible) { + emptyStateContainer.style.height = parentElement.offsetHeight + "px"; + emptyStateContainer.style.width = parentElement.offsetWidth + "px"; + emptyStateContainer.style.left = parentElement.offsetLeft + "px"; + emptyStateContainer.style.top = parentElement.offsetTop + "px"; + } + }); +}, { + root: null, + rootMargin: '0px', + threshold: 1.0 // Fully visible +}); + +function createEmptyStateContentElement(htmlContent) { + const emptyStateContentElement = document.createElement("div"); + emptyStateContentElement.innerHTML = htmlContent; + + emptyStateContentElement.classList.add("empty-state-content"); + + return emptyStateContentElement; +} + +function createEmptyStateContainer(elementToReplace) { + const emptyStateContainer = document.createElement("div"); + + emptyStateContainer.style.position = "absolute"; + + emptyStateContainer.style.height = elementToReplace.offsetHeight + "px"; + emptyStateContainer.style.width = elementToReplace.offsetWidth + "px"; + emptyStateContainer.style.left = elementToReplace.offsetLeft + "px"; + emptyStateContainer.style.top = elementToReplace.offsetTop + "px"; + + emptyStateContainer.classList.add("empty-state-container"); + + // Observe the emptyStateContainer with the IntersectionObserver + intersectionObserver.observe(elementToReplace); + + return emptyStateContainer; +} + +function findElementById(elementId) { + const element = document.querySelector(`#${elementId}`); + + if (element === null) { + throw `Unable to find element #${elementId}`; + } + + return element; +} + +function showEmptyState(elementId, htmlContent, color) { + const emptyStateContent = htmlContent; + + const white = "#FFFFFF" + const backgroundColor = (color === null) ? white : color; + + const elementToReplace = document.getElementById(elementId); + let emptyStateContainer = elementToReplace.querySelector(".empty-state-container"); + + if (!emptyStateContainer) { + // If the element doesn't have an empty state container, create one + emptyStateContainer = createEmptyStateContainer(elementToReplace); + elementToReplace.parentElement.insertBefore(emptyStateContainer, elementToReplace.nextSibling); + } + + const emptyStateContentElement = createEmptyStateContentElement(emptyStateContent); + emptyStateContainer.innerHTML = ''; // Clear existing content + emptyStateContainer.appendChild(emptyStateContentElement); + + emptyStateContainer.style.backgroundColor = backgroundColor; +} + +function hideEmptyState(message) { + const elementId = message.id; + const elementWithEmptyState = findElementById(elementId); + const emptyStateContainer = elementWithEmptyState.querySelector(".empty-state-container"); + + if (emptyStateContainer) { + emptyStateContainer.remove(); + } +} + +function repositionEmptyState() { + const emptyStateContainers = document.querySelectorAll(".empty-state-container"); + + emptyStateContainers.forEach(emptyStateContainer => { + const parentElement = emptyStateContainer.parentElement; + emptyStateContainer.style.height = parentElement.offsetHeight + "px"; + emptyStateContainer.style.width = parentElement.offsetWidth + "px"; + emptyStateContainer.style.left = parentElement.offsetLeft + "px"; + emptyStateContainer.style.top = parentElement.offsetTop + "px"; + }); +} + +function observeDOMChanges() { + const targetNode = document.getElementById("container-wrapper"); + + const mutationObserver = new MutationObserver(mutationsList => { + repositionEmptyState(); + }); + + const config = { attributes: true, childList: true, subtree: true }; + + mutationObserver.observe(targetNode, config); +} + +$(function() { + debugger; + Shiny.addCustomMessageHandler("showEmptyState", showEmptyState) + Shiny.addCustomMessageHandler("hideEmptyState", hideEmptyState) + + window.addEventListener("resize", function() { + document.querySelectorAll(".empty-state-container").forEach(emptyStateContainer => { + const parentElement= emptyStateContainer.parentElement; + emptyStateContainer.style.height = parentElement.offsetHeight + "px"; + emptyStateContainer.style.width = parentElement.offsetWidth + "px"; + emptyStateContainer.style.left = parentElement.offsetLeft + "px"; + emptyStateContainer.style.top = parentElement.offsetTop + "px"; + }) + }) + + observeDOMChanges(); +}) From 687d8083dbe42c0cd49c40cf2f3ddc6d49e22b5d Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Tue, 1 Aug 2023 12:17:07 -0300 Subject: [PATCH 2/9] Update with manual function --- examples/slide_tag/slide_tag.R | 44 +++++++++++ inst/emptystate.js | 12 +++ inst/emptystate2.js | 129 --------------------------------- 3 files changed, 56 insertions(+), 129 deletions(-) create mode 100644 examples/slide_tag/slide_tag.R delete mode 100644 inst/emptystate2.js diff --git a/examples/slide_tag/slide_tag.R b/examples/slide_tag/slide_tag.R new file mode 100644 index 0000000..997b56a --- /dev/null +++ b/examples/slide_tag/slide_tag.R @@ -0,0 +1,44 @@ +library(shiny) +library(shiny.emptystate) +library(bslib) + +ui <- page( + theme = bs_theme(version = 5), + use_empty_state(), + tags$button( + "Toggle panel", + class = "btn btn-primary", + onClick = "$('#container1').toggle(anim = 'slide', function(){shiny_emptystate_updatePosition('container2')});" + ), + div( + style = "width: 300px", + class = "d-flex flex-column gap-5", + div( + id = "container1", + div( + h1("Card 1"), + "Card content" + ) + ), + div( + id = "container2", + div( + h1("Card 2"), + "Card content" + ) + ) + ) +) + +server <- function(input, output, session) { + empty_state_content <- div( + "This is example empty state content" + ) + empty_state_manager <- EmptyStateManager$new( + id = "container2", + html_content = empty_state_content + ) + empty_state_manager$show() +} + +shinyApp(ui, server) diff --git a/inst/emptystate.js b/inst/emptystate.js index 92a36d6..aceb406 100644 --- a/inst/emptystate.js +++ b/inst/emptystate.js @@ -75,6 +75,18 @@ function hideEmptyState(message) { resizeObserver.unobserve(elementToReplace); } +function shiny_emptystate_updatePosition(id){ + const elementWithEmptyState = findElementById(id); + const emptyStateContainer = elementWithEmptyState.querySelector(".empty-state-container"); + + const parentElement = emptyStateContainer.parentElement; + emptyStateContainer.style.height = parentElement.offsetHeight + "px"; + emptyStateContainer.style.width = parentElement.offsetWidth + "px"; + emptyStateContainer.style.left = parentElement.offsetLeft + "px"; + emptyStateContainer.style.top = parentElement.offsetTop + "px"; +} + + $(function() { Shiny.addCustomMessageHandler("showEmptyState", showEmptyState) Shiny.addCustomMessageHandler("hideEmptyState", hideEmptyState) diff --git a/inst/emptystate2.js b/inst/emptystate2.js deleted file mode 100644 index 8686219..0000000 --- a/inst/emptystate2.js +++ /dev/null @@ -1,129 +0,0 @@ -const intersectionObserver = new IntersectionObserver((entries) => { - entries.forEach(entry => { - const emptyStateContainer = entry.target.querySelector(".empty-state-container"); - const parentElement = emptyStateContainer.parentElement; - const isVisible = entry.isIntersecting; - - if (isVisible) { - emptyStateContainer.style.height = parentElement.offsetHeight + "px"; - emptyStateContainer.style.width = parentElement.offsetWidth + "px"; - emptyStateContainer.style.left = parentElement.offsetLeft + "px"; - emptyStateContainer.style.top = parentElement.offsetTop + "px"; - } - }); -}, { - root: null, - rootMargin: '0px', - threshold: 1.0 // Fully visible -}); - -function createEmptyStateContentElement(htmlContent) { - const emptyStateContentElement = document.createElement("div"); - emptyStateContentElement.innerHTML = htmlContent; - - emptyStateContentElement.classList.add("empty-state-content"); - - return emptyStateContentElement; -} - -function createEmptyStateContainer(elementToReplace) { - const emptyStateContainer = document.createElement("div"); - - emptyStateContainer.style.position = "absolute"; - - emptyStateContainer.style.height = elementToReplace.offsetHeight + "px"; - emptyStateContainer.style.width = elementToReplace.offsetWidth + "px"; - emptyStateContainer.style.left = elementToReplace.offsetLeft + "px"; - emptyStateContainer.style.top = elementToReplace.offsetTop + "px"; - - emptyStateContainer.classList.add("empty-state-container"); - - // Observe the emptyStateContainer with the IntersectionObserver - intersectionObserver.observe(elementToReplace); - - return emptyStateContainer; -} - -function findElementById(elementId) { - const element = document.querySelector(`#${elementId}`); - - if (element === null) { - throw `Unable to find element #${elementId}`; - } - - return element; -} - -function showEmptyState(elementId, htmlContent, color) { - const emptyStateContent = htmlContent; - - const white = "#FFFFFF" - const backgroundColor = (color === null) ? white : color; - - const elementToReplace = document.getElementById(elementId); - let emptyStateContainer = elementToReplace.querySelector(".empty-state-container"); - - if (!emptyStateContainer) { - // If the element doesn't have an empty state container, create one - emptyStateContainer = createEmptyStateContainer(elementToReplace); - elementToReplace.parentElement.insertBefore(emptyStateContainer, elementToReplace.nextSibling); - } - - const emptyStateContentElement = createEmptyStateContentElement(emptyStateContent); - emptyStateContainer.innerHTML = ''; // Clear existing content - emptyStateContainer.appendChild(emptyStateContentElement); - - emptyStateContainer.style.backgroundColor = backgroundColor; -} - -function hideEmptyState(message) { - const elementId = message.id; - const elementWithEmptyState = findElementById(elementId); - const emptyStateContainer = elementWithEmptyState.querySelector(".empty-state-container"); - - if (emptyStateContainer) { - emptyStateContainer.remove(); - } -} - -function repositionEmptyState() { - const emptyStateContainers = document.querySelectorAll(".empty-state-container"); - - emptyStateContainers.forEach(emptyStateContainer => { - const parentElement = emptyStateContainer.parentElement; - emptyStateContainer.style.height = parentElement.offsetHeight + "px"; - emptyStateContainer.style.width = parentElement.offsetWidth + "px"; - emptyStateContainer.style.left = parentElement.offsetLeft + "px"; - emptyStateContainer.style.top = parentElement.offsetTop + "px"; - }); -} - -function observeDOMChanges() { - const targetNode = document.getElementById("container-wrapper"); - - const mutationObserver = new MutationObserver(mutationsList => { - repositionEmptyState(); - }); - - const config = { attributes: true, childList: true, subtree: true }; - - mutationObserver.observe(targetNode, config); -} - -$(function() { - debugger; - Shiny.addCustomMessageHandler("showEmptyState", showEmptyState) - Shiny.addCustomMessageHandler("hideEmptyState", hideEmptyState) - - window.addEventListener("resize", function() { - document.querySelectorAll(".empty-state-container").forEach(emptyStateContainer => { - const parentElement= emptyStateContainer.parentElement; - emptyStateContainer.style.height = parentElement.offsetHeight + "px"; - emptyStateContainer.style.width = parentElement.offsetWidth + "px"; - emptyStateContainer.style.left = parentElement.offsetLeft + "px"; - emptyStateContainer.style.top = parentElement.offsetTop + "px"; - }) - }) - - observeDOMChanges(); -}) From 9a43a607dbf4d91d487e16514e27ebc5a602e637 Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Tue, 1 Aug 2023 12:53:28 -0300 Subject: [PATCH 3/9] update example structure --- examples/slide_tag/{slide_tag.R => app.R} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/slide_tag/{slide_tag.R => app.R} (100%) diff --git a/examples/slide_tag/slide_tag.R b/examples/slide_tag/app.R similarity index 100% rename from examples/slide_tag/slide_tag.R rename to examples/slide_tag/app.R From 2aa29ccb00a2f642a8f2fb82086264e3c5ee39bf Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Wed, 2 Aug 2023 20:32:29 -0300 Subject: [PATCH 4/9] Add unit test with shinytest2 --- examples/slide_tag/app.R | 44 ------------------------------- tests/testthat/helper.R | 43 ++++++++++++++++++++++++++++++ tests/testthat/test-empty_state.R | 8 ++++++ 3 files changed, 51 insertions(+), 44 deletions(-) delete mode 100644 examples/slide_tag/app.R diff --git a/examples/slide_tag/app.R b/examples/slide_tag/app.R deleted file mode 100644 index 997b56a..0000000 --- a/examples/slide_tag/app.R +++ /dev/null @@ -1,44 +0,0 @@ -library(shiny) -library(shiny.emptystate) -library(bslib) - -ui <- page( - theme = bs_theme(version = 5), - use_empty_state(), - tags$button( - "Toggle panel", - class = "btn btn-primary", - onClick = "$('#container1').toggle(anim = 'slide', function(){shiny_emptystate_updatePosition('container2')});" - ), - div( - style = "width: 300px", - class = "d-flex flex-column gap-5", - div( - id = "container1", - div( - h1("Card 1"), - "Card content" - ) - ), - div( - id = "container2", - div( - h1("Card 2"), - "Card content" - ) - ) - ) -) - -server <- function(input, output, session) { - empty_state_content <- div( - "This is example empty state content" - ) - empty_state_manager <- EmptyStateManager$new( - id = "container2", - html_content = empty_state_content - ) - empty_state_manager$show() -} - -shinyApp(ui, server) diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index 3fc3db1..6ecc128 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -26,3 +26,46 @@ test_app <- function() { } ) } + +test_slider_app <- function() { + shiny::shinyApp( + ui = bslib::page( + theme = bslib::bs_theme(version = 5), + use_empty_state(), + shiny::actionButton( + "toggle_pannel", + "Toggle panel", + class = "btn btn-primary", + onClick = "$('#container1').toggle(anim = 'slide', function(){shiny_emptystate_updatePosition('container2')});" + ), + shiny::div( + style = "width: 300px", + class = "d-flex flex-column gap-5", + shiny::div( + id = "container1", + shiny::div( + shiny::h1("Card 1"), + "Card content" + ) + ), + shiny::div( + id = "container2", + shiny::div( + shiny::h1("Card 2"), + "Card content" + ) + ) + ) + ), + server = function(input, output) { + empty_state_content <- shiny::div( + "This is example empty state content" + ) + empty_state_manager <- EmptyStateManager$new( + id = "container2", + html_content = empty_state_content + ) + empty_state_manager$show() + } + ) +} diff --git a/tests/testthat/test-empty_state.R b/tests/testthat/test-empty_state.R index c6b6156..d77ff25 100644 --- a/tests/testthat/test-empty_state.R +++ b/tests/testthat/test-empty_state.R @@ -56,6 +56,14 @@ describe("EmptyStateManager", { expect_null(app$get_html(selector = ".empty-state-content")) app$stop() }) + + it("checks the empty state component follows slider from id", { + app <- shinytest2::AppDriver$new(test_slider_app(), name = "slide_tag") + app$expect_values() + app$click("toggle_pannel") + app$expect_values() + app$stop() + }) }) describe("use_empty_state()", { From d5ddb831a0d3b6459d5275cacd3038b55cf4fa1a Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Wed, 2 Aug 2023 20:41:19 -0300 Subject: [PATCH 5/9] Fix lint --- tests/testthat/helper.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index 6ecc128..40c63f5 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -36,7 +36,8 @@ test_slider_app <- function() { "toggle_pannel", "Toggle panel", class = "btn btn-primary", - onClick = "$('#container1').toggle(anim = 'slide', function(){shiny_emptystate_updatePosition('container2')});" + onClick = "$('#container1').toggle(anim = 'slide', + function(){shiny_emptystate_updatePosition('container2')});" ), shiny::div( style = "width: 300px", From 2aea97f139073928f4f3b8c563153898360fa658 Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Wed, 2 Aug 2023 20:47:28 -0300 Subject: [PATCH 6/9] remove bslib from test helper --- tests/testthat/helper.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index 40c63f5..27cce3b 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -29,8 +29,7 @@ test_app <- function() { test_slider_app <- function() { shiny::shinyApp( - ui = bslib::page( - theme = bslib::bs_theme(version = 5), + ui = shiny::fillPage( use_empty_state(), shiny::actionButton( "toggle_pannel", From b54ace3a488dbbf0262d595c9988c3694ee251f0 Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Thu, 28 Sep 2023 16:42:32 -0300 Subject: [PATCH 7/9] Add documentation for toggle empty state --- pkgdown/_pkgdown.yml | 39 +++++++++----- tests/testthat/helper.R | 2 +- vignettes/files/dynamic_emptystate.gif | Bin 0 -> 34932 bytes vignettes/use-toggle-animations.rmd | 69 +++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 vignettes/files/dynamic_emptystate.gif create mode 100644 vignettes/use-toggle-animations.rmd diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 6798028..84d0edb 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -31,25 +31,38 @@ url: https://appsilon.github.io/shiny.emptystate/ navbar: bg: primary - left: - - icon: fa-home - href: index.html + structure: + left: [home, tutorials, reference, changelog] + right: [search, github, twitter, mastodon] + components: + home: + icon: fa-home text: "Start" - - icon: fa-university - href: articles/use-undraw-drawkit-image.html - text: "Tutorial" - - icon: fa-file-code-o + href: index.html + tutorials: + text: Tutorials + icon: fa-university + menu: + - text: How to Use unDraw/Drawkit Illustrations + href: articles/use-undraw-drawkit-image.html + - text: How to use triggered animations + href: articles/use-toggle-animations.html + reference: + icon: fa-file-code-o text: "Reference" href: reference/index.html - - icon: fa-newspaper-o - text: Changelog + changelog: + icon: fa-newspaper-o + text: "Changelog" href: news/index.html - right: - - icon: fa-github fa-lg + github: + icon: fa-github fa-lg href: https://github.com/Appsilon/shiny.emptystate - - icon: fa-twitter fa-lg + twitter: + icon: fa-twitter fa-lg href: https://twitter.com/Appsilon - - icon: fab fa-mastodon fa-lg + mastodon: + icon: fab fa-mastodon fa-lg href: https://fosstodon.org/@appsilon home: diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index 27cce3b..67ab194 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -35,7 +35,7 @@ test_slider_app <- function() { "toggle_pannel", "Toggle panel", class = "btn btn-primary", - onClick = "$('#container1').toggle(anim = 'slide', + onClick = "$('#container1').toggle(0, function(){shiny_emptystate_updatePosition('container2')});" ), shiny::div( diff --git a/vignettes/files/dynamic_emptystate.gif b/vignettes/files/dynamic_emptystate.gif new file mode 100644 index 0000000000000000000000000000000000000000..2927ffe0747d290e32c479f30bce27d2b308c297 GIT binary patch literal 34932 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 + %\VignetteIndexEntry{How to use triggered animations} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +# Introduction + +`shiny.emptystate` is capable of being used for dynamic positioning trigger animations. This is specially interesting when we have UIs that change position and we want to keep the `emptystate` in the appropriate tag + +**Note:** This solution is only available for instant positioning animation. + +# Animation example + +The example below showcases the example of using a dynamic trigger slider that changes. + +The function `shiny_emptystate_updatePosition` updates the position of the #container1 after being re-positioned + +``` r +library(shiny) +library(shiny.emptystate) + +ui <- shiny::fillPage( + use_empty_state(), + shiny::actionButton( + "toggle_pannel", + "Toggle panel", + class = "btn btn-primary", + onClick = "$('#container1').toggle(0, + function(){shiny_emptystate_updatePosition('container2')});" + ), + shiny::div( + style = "width: 300px", + class = "d-flex flex-column gap-5", + shiny::div( + id = "container1", + shiny::div( + shiny::h1("Card 1"), + "Card content" + ) + ), + shiny::div( + id = "container2", + shiny::div( + shiny::h1("Card 2"), + "Card content" + ) + ) + ) +) + +server = function(input, output) { + empty_state_content <- shiny::div( + "This is example empty state content" + ) + empty_state_manager <- EmptyStateManager$new( + id = "container2", + html_content = empty_state_content + ) + empty_state_manager$show() +} + +shinyApp(ui, server) +``` + +![](images/dynamic_emptystate.gif) From b8d758435fa9a62a33e3023835604212f488e4e3 Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Thu, 28 Sep 2023 17:11:54 -0300 Subject: [PATCH 8/9] Update reference of gif --- vignettes/use-toggle-animations.rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/use-toggle-animations.rmd b/vignettes/use-toggle-animations.rmd index e5b7f37..71c9a82 100644 --- a/vignettes/use-toggle-animations.rmd +++ b/vignettes/use-toggle-animations.rmd @@ -66,4 +66,4 @@ server = function(input, output) { shinyApp(ui, server) ``` -![](images/dynamic_emptystate.gif) +![](files/dynamic_emptystate.gif) From cd2f3e36c34d551f338580b2c250135b089848a8 Mon Sep 17 00:00:00 2001 From: Eduardodudu Date: Mon, 2 Oct 2023 12:11:21 -0300 Subject: [PATCH 9/9] Update unit test --- tests/testthat/test-empty_state.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-empty_state.R b/tests/testthat/test-empty_state.R index 1c8746d..b204f02 100644 --- a/tests/testthat/test-empty_state.R +++ b/tests/testthat/test-empty_state.R @@ -62,9 +62,10 @@ describe("EmptyStateManager", { it("checks the empty state component follows slider from id", { app <- shinytest2::AppDriver$new(test_slider_app(), name = "slide_tag") - app$expect_values() + position_1 <- app$get_html(selector = "#container2") app$click("toggle_pannel") - app$expect_values() + position_2 <- app$get_html(selector = "#container2") + expect_false(position_1 == position_2) app$stop() }) })