Make cards draggable and resizable
I'm aiming to enhance the layout of widgets on data pages.
Currently, the widgets are stacked vertically and lack the ability to resize or reposition.
My goal is to make these widgets resizable and movable.
Below is the existing code that I've been working with:
library(shiny)
# Function to create a card element with title, content, and style options.
make_card <- function(title = character(), content = character(), size = 12, style = "") {
div(
class = paste("card ms-depth-8 ms-sm", size, "ms-xl", size),
style = style,
div(
class = "card-content",
title, br(), br(),
content
)
)
}
ui <- shiny.fluent::fluentPage(
# Uses shinyjs for JavaScript interactivity.
shinyjs::useShinyjs(),
# Includes external styles and scripts, such as Font Awesome and jQuery UI.
tags$head(
tags$link(rel = "stylesheet", href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"),
tags$script(src = "https://code.jquery.com/ui/1.12.1/jquery-ui.js"),
# Custom CSS styles defined for layout and appearance of elements.
tags$script(HTML("
function initializeSortableAndResizable() {
$('.connectedSortable').sortable({
connectWith: '.connectedSortable',
items: function() {
return $(this).children('.card').filter(function() {
return !$(this).parent().hasClass('full');
});
},
placeholder: 'sortable-placeholder',
start: function(e, ui){
ui.placeholder.height(ui.helper.outerHeight());
ui.placeholder.width(ui.helper.outerWidth());
ui.helper.css('opacity', '0.5');
$('.connectedSortable').each(function() {
if ($(this).find('.card').length >= 1) {
$(this).addClass('full');
}
});
},
stop: function(e, ui){
ui.item.css('opacity', '');
$('.connectedSortable').each(function() {
if ($(this).find('.card').length < 1) {
// Marquer comme vide et activer le tri
$(this).removeClass('full');
}
});
},
receive: function(event, ui) {
if ($(this).children('.card').length > 1) {
$(ui.sender).sortable('cancel');
}
}
});
$('.connectedSortable div').each(function() {
var handle = $('<div class=\"resize-handle\"><i class=\"fas fa-expand-arrows-alt\"></i></div>');
$(this).append(handle);
$(this).resizable({
handles: {
'se': handle
}
});
});
};
function adjustCardWidth() {
$('.card').each(function() {
var parentWidth = $(this).closest('.flex-container').width();
$(this).css('width', parentWidth * 0.96);
});
}
$(document).on('shiny:connected', function(event) {
initializeSortableAndResizable();
});
"))
),
tags$style("
.flex-container {
display: flex;
max-width:100%;
}
.connectedSortable {
flex: 1;
padding: 10px;
min-height:10px;
border: dashed grey 0.5px;
box-sizing: border-box;
}
.card {
background: #fff;
border-radius: 2px;
padding: 10px;
box-sizing: border-box;
position:relative;
}
.resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: se-resize;
color: grey;
opacity: 0.3;
}
.ui-sortable-placeholder {
border: 2px dashed #666;
margin: 5px;
background-color: #f0f0f0;
}
.full .ui-sortable-placeholder {
display: none;
}
"),
# Layout for a Stack element with a Button and a Toggle, followed by a br tag.
shiny.fluent::Stack(horizontal = TRUE, tokens = list(childrenGap = 10),
shiny.fluent::DefaultButton.shinyInput("load", "Load"),
shiny.fluent::Toggle.shinyInput("toggle_border", "Afficher la grille", value = TRUE)
), br(),
# Output for dynamic UI elements.
uiOutput("ui")
)
server <- function(input, output) {
i <- 0
# Render a dynamic UI with multiple flex containers and sortable divs.
observeEvent(input$load, {
output$ui <- renderUI(tagList(
div(
class = "flex-container",
div(
class = "connectedSortable",
),
div(
class = "connectedSortable",
make_card("Div2", "Content", style = "width:500px")
),
div(
class = "connectedSortable"
)
),
div(
class = "flex-container",
div(
class = "connectedSortable"
),
div(
class = "connectedSortable"
),
div(
class = "connectedSortable"
)
),
div(
class = "flex-container",
div(
class = "connectedSortable",
make_card("Div4", "Content")
),
div(
class = "connectedSortable"
),
div(
class = "connectedSortable"
)
),
div(
class = "flex-container",
div(
class = "connectedSortable",
make_card("Div3", "Content")
),
div(
class = "connectedSortable",
),
div(
class = "connectedSortable"
)
)
))
# Initialize JavaScript functions after rendering the UI.
shinyjs::delay(100, shinyjs::runjs("initializeSortableAndResizable();"))
# Adjust card width on the first load.
if (i == 0) shinyjs::delay(100, shinyjs::runjs("adjustCardWidth()"))
i <<- 1
})
observeEvent(input$toggle_border, {
if (input$toggle_border) {
shinyjs::runjs('$(".connectedSortable").css("border", "dashed grey 0.5px");')
} else {
shinyjs::runjs('$(".connectedSortable").css("border", "none");')
}
})
}
shinyApp(ui = ui, server = server)
Here's the current result.
Based on this code, I'd like to implement the following enhancements:
- Introduce a button to create a new widget.
- Incorporate a 'delete' button on each widget.
- Ensure that each widget can be freely positioned relative to others. It's possible that using
display:flex
might not be the ideal approach. - The combined dimensions of all flex-containers should not exceed the boundaries of the page.
- Establish a SQLite database to remember the position and size of each widget. This way, upon reloading the app, each widget returns to its last saved state and size.
Many thanks in advance to anyone who can offer assistance with this code!