Skip to content

Commit

Permalink
Initial draft
Browse files Browse the repository at this point in the history
  • Loading branch information
coatless committed Dec 26, 2024
1 parent 352cc25 commit 2035623
Show file tree
Hide file tree
Showing 15 changed files with 910 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.html
33 changes: 33 additions & 0 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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:

name: R-CMD-check.yaml

permissions: read-all

jobs:
R-CMD-check:
runs-on: ubuntu-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- uses: actions/checkout@v4

- uses: r-lib/actions/setup-r@v2
with:
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
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
49 changes: 49 additions & 0 deletions .github/workflows/pkgdown.yaml
Original file line number Diff line number Diff line change
@@ -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:
release:
types: [published]
workflow_dispatch:

name: pkgdown.yaml

permissions: read-all

jobs:
pkgdown:
runs-on: ubuntu-latest
# Only restrict concurrency for non-PR jobs
concurrency:
group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }}
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- uses: r-lib/actions/setup-pandoc@v2

- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true

- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::pkgdown, local::.
needs: website

- name: Build site
run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
shell: Rscript {0}

- name: Deploy to GitHub pages 🚀
if: github.event_name != 'pull_request'
uses: JamesIves/github-pages-deploy-action@v4.5.0
with:
clean: false
branch: gh-pages
folder: docs
6 changes: 6 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Generated by roxygen2: do not edit by hand

export(inlineNumericInput)
export(inlineOutput)
export(renderInline)
import(shiny)
300 changes: 300 additions & 0 deletions R/inline-interactive-widget.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
#' Create an inline numeric input field
#'
#' Creates a numeric input field that can be embedded inline within text. The field
#' supports mouse drag and scroll wheel for value changes.
#'
#' @param inputId The input identifier used to access the value in server logic
#' @param value Initial value
#' @param min Minimum value allowed (NA for no minimum)
#' @param max Maximum value allowed (NA for no maximum)
#' @param step Step size for increments/decrements
#' @param sensitivity Drag sensitivity multiplier
#'
#' @return
#' A Shiny tag list containing the input element and its dependencies
#'
#' @examples
#' library(shiny)
#'
#' ui <- fluidPage(
#' p("Let x = ", inlineNumericInput("x"))
#' )
#'
#' server <- function(input, output) {
#' # Empty
#' }
#'
#' if (interactive()) {
#' shinyApp(ui, server)
#' }
#' @export
inlineNumericInput <- function(inputId,
value = 0,
min = NA,
max = NA,
step = 0.1,
sensitivity = 0.1) {

# Determine if we should use integer formatting
is_integer_input <- is.numeric(step) && step == floor(step) &&
is.numeric(value) && value == floor(value)

# Format value based on whether it's integer or decimal
formatted_value <- if (is_integer_input) {
format(round(value))
} else {
format(value, nsmall = 1)
}

# Create container with dependencies and input
shiny::tagList(
.ensure_inline_dependencies(sensitivity),
shiny::tags$input(
type = "number",
id = inputId,
class = "inline-interactive-number",
value = formatted_value,
min = if (!is.na(min)) min,
max = if (!is.na(max)) max,
step = step
)
)
}

#' Create an inline output span
#'
#' Creates an output element that can be embedded inline within text.
#'
#' @param outputId The output identifier used to update the value from server logic
#'
#' @return
#' A Shiny UI output element configured for inline display
#'
#' @export
#' @examples
#' library(shiny)
#'
#' ui <- fluidPage(
#' p("The result is", inlineOutput("result"))
#' )
#'
#' server <- function(input, output) {
#' output$result <- renderInline({
#' # Will be formatted to one decimal place
#' 42.123
#' })
#' }
#'
#' if (interactive()) {
#' shinyApp(ui, server)
#' }
inlineOutput <- function(outputId) {
shiny::uiOutput(outputId, inline = TRUE)
}

#' Create an inline render function
#'
#' Creates a render function for inline outputs that automatically formats numeric
#' values appropriately (integers without decimals, other numbers to one decimal place).
#'
#' @param expr Expression to evaluate
#' @param env Environment to evaluate in
#' @param quoted Whether the expression is quoted
#'
#' @return
#' A Shiny render function that creates inline output elements
#' @export
#' @examples
#' library(shiny)
#'
#' ui <- fluidPage(
#' p("The result is", inlineOutput("result"))
#' )
#'
#' server <- function(input, output) {
#' output$result <- renderInline({
#' # Will be formatted to one decimal place
#' 42.123
#' })
#' }
#'
#' if (interactive()) {
#' shinyApp(ui, server)
#' }
renderInline <- function(expr, env = parent.frame(), quoted = FALSE) {
if (!quoted) {
expr <- substitute(expr)
}

func <- shiny::exprToFunction(expr, env)

shiny::renderUI({
value <- func()
if (is.null(value)) return(NULL)

formatted_value <- if (is.numeric(value)) {
if (value == floor(value)) {
# Integer values
format(round(value))
} else {
# Decimal values
format(round(value, 1), nsmall = 1)
}
} else {
as.character(value)
}

shiny::tags$span(
class = "inline-interactive-output",
formatted_value
)
})
}

# Internal function to add required dependencies
.ensure_inline_dependencies <- function(sensitivity = 0.1) {
shiny::singleton(
shiny::tags$head(
# CSS styling remains the same
shiny::tags$style("
input[type='number'].inline-interactive-number {
background: #f0f0f0;
padding: 2px 5px;
border-radius: 3px;
color: #2196F3;
font-weight: bold;
cursor: ew-resize;
display: inline-block;
border: none;
width: 4em;
text-align: right;
-moz-appearance: textfield;
-webkit-appearance: none;
font-size: inherit;
font-family: inherit;
}
input[type='number'].inline-interactive-number::-webkit-inner-spin-button,
input[type='number'].inline-interactive-number::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.inline-interactive-number:hover {
background: #e0e0e0;
}
.inline-interactive-number.dragging {
background: #d0d0d0;
}
.inline-interactive-output {
background: #f8f8f8;
padding: 2px 5px;
border-radius: 3px;
color: #666;
display: inline-block;
min-width: 2em;
text-align: right;
}
"),

# Modified JavaScript to handle integer formatting
shiny::tags$script(sprintf("$(document).ready(function() {
var isDragging = false;
var startY;
var startValue;
var sensitivity = %f;
var $activeInput = null;
$('.inline-interactive-number').on('mousedown touchstart', function(e) {
isDragging = true;
$activeInput = $(this);
startY = e.type === 'mousedown' ? e.pageY : e.touches[0].pageY;
startValue = parseFloat($activeInput.val()) || 0;
$('body').css('cursor', 'ew-resize');
$activeInput.addClass('dragging');
e.preventDefault();
return false;
});
$(document).on('mousemove touchmove', function(e) {
if (!isDragging || !$activeInput) return;
var currentY = e.type === 'mousemove' ? e.pageY : e.touches[0].pageY;
var deltaY = startY - currentY;
var newValue = startValue + (deltaY * sensitivity);
var min = parseFloat($activeInput.attr('min'));
var max = parseFloat($activeInput.attr('max'));
var step = parseFloat($activeInput.attr('step')) || 1;
if (!isNaN(min)) newValue = Math.max(min, newValue);
if (!isNaN(max)) newValue = Math.min(max, newValue);
newValue = Math.round(newValue / step) * step;
// Format based on step size
var formattedValue = step === 1
? Math.round(newValue).toString()
: newValue.toFixed(1);
$activeInput.val(formattedValue);
Shiny.setInputValue($activeInput.attr('id'), newValue);
e.preventDefault();
return false;
});
$(document).on('mouseup touchend', function(e) {
if (isDragging) {
isDragging = false;
$activeInput.removeClass('dragging');
$activeInput = null;
$('body').css('cursor', 'default');
e.preventDefault();
}
});
$('.inline-interactive-number').on('wheel', function(e) {
var $input = $(this);
var step = parseFloat($input.attr('step')) || 1;
var delta = e.originalEvent.deltaY < 0 ? step : -step;
var currentValue = parseFloat($input.val()) || 0;
var newValue = currentValue + delta;
var min = parseFloat($input.attr('min'));
var max = parseFloat($input.attr('max'));
if (!isNaN(min)) newValue = Math.max(min, newValue);
if (!isNaN(max)) newValue = Math.min(max, newValue);
// Format based on step size
var formattedValue = step === 1
? Math.round(newValue).toString()
: newValue.toFixed(1);
$input.val(formattedValue);
Shiny.setInputValue($input.attr('id'), newValue);
e.preventDefault();
return false;
});
// Handle direct input changes
$('.inline-interactive-number').on('input change', function(e) {
var $input = $(this);
var value = parseFloat($input.val()) || 0;
var step = parseFloat($input.attr('step')) || 1;
// Format based on step size
var formattedValue = step === 1
? Math.round(value).toString()
: value.toFixed(1);
$input.val(formattedValue);
Shiny.setInputValue($input.attr('id'), value);
});
});
", sensitivity))
)
)
}
Loading

0 comments on commit 2035623

Please sign in to comment.