Skip to content

Commit 2035623

Browse files
committed
Initial draft
1 parent 352cc25 commit 2035623

File tree

15 files changed

+910
-1
lines changed

15 files changed

+910
-1
lines changed

.github/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.html

.github/workflows/R-CMD-check.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2+
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
8+
name: R-CMD-check.yaml
9+
10+
permissions: read-all
11+
12+
jobs:
13+
R-CMD-check:
14+
runs-on: ubuntu-latest
15+
env:
16+
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
17+
R_KEEP_PKG_SOURCE: yes
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- uses: r-lib/actions/setup-r@v2
22+
with:
23+
use-public-rspm: true
24+
25+
- uses: r-lib/actions/setup-r-dependencies@v2
26+
with:
27+
extra-packages: any::rcmdcheck
28+
needs: check
29+
30+
- uses: r-lib/actions/check-r-package@v2
31+
with:
32+
upload-snapshots: true
33+
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'

.github/workflows/pkgdown.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2+
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
release:
8+
types: [published]
9+
workflow_dispatch:
10+
11+
name: pkgdown.yaml
12+
13+
permissions: read-all
14+
15+
jobs:
16+
pkgdown:
17+
runs-on: ubuntu-latest
18+
# Only restrict concurrency for non-PR jobs
19+
concurrency:
20+
group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }}
21+
env:
22+
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
23+
permissions:
24+
contents: write
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- uses: r-lib/actions/setup-pandoc@v2
29+
30+
- uses: r-lib/actions/setup-r@v2
31+
with:
32+
use-public-rspm: true
33+
34+
- uses: r-lib/actions/setup-r-dependencies@v2
35+
with:
36+
extra-packages: any::pkgdown, local::.
37+
needs: website
38+
39+
- name: Build site
40+
run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
41+
shell: Rscript {0}
42+
43+
- name: Deploy to GitHub pages 🚀
44+
if: github.event_name != 'pull_request'
45+
uses: JamesIves/github-pages-deploy-action@v4.5.0
46+
with:
47+
clean: false
48+
branch: gh-pages
49+
folder: docs

NAMESPACE

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Generated by roxygen2: do not edit by hand
2+
3+
export(inlineNumericInput)
4+
export(inlineOutput)
5+
export(renderInline)
6+
import(shiny)

R/inline-interactive-widget.R

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
#' Create an inline numeric input field
2+
#'
3+
#' Creates a numeric input field that can be embedded inline within text. The field
4+
#' supports mouse drag and scroll wheel for value changes.
5+
#'
6+
#' @param inputId The input identifier used to access the value in server logic
7+
#' @param value Initial value
8+
#' @param min Minimum value allowed (NA for no minimum)
9+
#' @param max Maximum value allowed (NA for no maximum)
10+
#' @param step Step size for increments/decrements
11+
#' @param sensitivity Drag sensitivity multiplier
12+
#'
13+
#' @return
14+
#' A Shiny tag list containing the input element and its dependencies
15+
#'
16+
#' @examples
17+
#' library(shiny)
18+
#'
19+
#' ui <- fluidPage(
20+
#' p("Let x = ", inlineNumericInput("x"))
21+
#' )
22+
#'
23+
#' server <- function(input, output) {
24+
#' # Empty
25+
#' }
26+
#'
27+
#' if (interactive()) {
28+
#' shinyApp(ui, server)
29+
#' }
30+
#' @export
31+
inlineNumericInput <- function(inputId,
32+
value = 0,
33+
min = NA,
34+
max = NA,
35+
step = 0.1,
36+
sensitivity = 0.1) {
37+
38+
# Determine if we should use integer formatting
39+
is_integer_input <- is.numeric(step) && step == floor(step) &&
40+
is.numeric(value) && value == floor(value)
41+
42+
# Format value based on whether it's integer or decimal
43+
formatted_value <- if (is_integer_input) {
44+
format(round(value))
45+
} else {
46+
format(value, nsmall = 1)
47+
}
48+
49+
# Create container with dependencies and input
50+
shiny::tagList(
51+
.ensure_inline_dependencies(sensitivity),
52+
shiny::tags$input(
53+
type = "number",
54+
id = inputId,
55+
class = "inline-interactive-number",
56+
value = formatted_value,
57+
min = if (!is.na(min)) min,
58+
max = if (!is.na(max)) max,
59+
step = step
60+
)
61+
)
62+
}
63+
64+
#' Create an inline output span
65+
#'
66+
#' Creates an output element that can be embedded inline within text.
67+
#'
68+
#' @param outputId The output identifier used to update the value from server logic
69+
#'
70+
#' @return
71+
#' A Shiny UI output element configured for inline display
72+
#'
73+
#' @export
74+
#' @examples
75+
#' library(shiny)
76+
#'
77+
#' ui <- fluidPage(
78+
#' p("The result is", inlineOutput("result"))
79+
#' )
80+
#'
81+
#' server <- function(input, output) {
82+
#' output$result <- renderInline({
83+
#' # Will be formatted to one decimal place
84+
#' 42.123
85+
#' })
86+
#' }
87+
#'
88+
#' if (interactive()) {
89+
#' shinyApp(ui, server)
90+
#' }
91+
inlineOutput <- function(outputId) {
92+
shiny::uiOutput(outputId, inline = TRUE)
93+
}
94+
95+
#' Create an inline render function
96+
#'
97+
#' Creates a render function for inline outputs that automatically formats numeric
98+
#' values appropriately (integers without decimals, other numbers to one decimal place).
99+
#'
100+
#' @param expr Expression to evaluate
101+
#' @param env Environment to evaluate in
102+
#' @param quoted Whether the expression is quoted
103+
#'
104+
#' @return
105+
#' A Shiny render function that creates inline output elements
106+
#' @export
107+
#' @examples
108+
#' library(shiny)
109+
#'
110+
#' ui <- fluidPage(
111+
#' p("The result is", inlineOutput("result"))
112+
#' )
113+
#'
114+
#' server <- function(input, output) {
115+
#' output$result <- renderInline({
116+
#' # Will be formatted to one decimal place
117+
#' 42.123
118+
#' })
119+
#' }
120+
#'
121+
#' if (interactive()) {
122+
#' shinyApp(ui, server)
123+
#' }
124+
renderInline <- function(expr, env = parent.frame(), quoted = FALSE) {
125+
if (!quoted) {
126+
expr <- substitute(expr)
127+
}
128+
129+
func <- shiny::exprToFunction(expr, env)
130+
131+
shiny::renderUI({
132+
value <- func()
133+
if (is.null(value)) return(NULL)
134+
135+
formatted_value <- if (is.numeric(value)) {
136+
if (value == floor(value)) {
137+
# Integer values
138+
format(round(value))
139+
} else {
140+
# Decimal values
141+
format(round(value, 1), nsmall = 1)
142+
}
143+
} else {
144+
as.character(value)
145+
}
146+
147+
shiny::tags$span(
148+
class = "inline-interactive-output",
149+
formatted_value
150+
)
151+
})
152+
}
153+
154+
# Internal function to add required dependencies
155+
.ensure_inline_dependencies <- function(sensitivity = 0.1) {
156+
shiny::singleton(
157+
shiny::tags$head(
158+
# CSS styling remains the same
159+
shiny::tags$style("
160+
input[type='number'].inline-interactive-number {
161+
background: #f0f0f0;
162+
padding: 2px 5px;
163+
border-radius: 3px;
164+
color: #2196F3;
165+
font-weight: bold;
166+
cursor: ew-resize;
167+
display: inline-block;
168+
border: none;
169+
width: 4em;
170+
text-align: right;
171+
-moz-appearance: textfield;
172+
-webkit-appearance: none;
173+
font-size: inherit;
174+
font-family: inherit;
175+
}
176+
177+
input[type='number'].inline-interactive-number::-webkit-inner-spin-button,
178+
input[type='number'].inline-interactive-number::-webkit-outer-spin-button {
179+
-webkit-appearance: none;
180+
margin: 0;
181+
}
182+
183+
.inline-interactive-number:hover {
184+
background: #e0e0e0;
185+
}
186+
187+
.inline-interactive-number.dragging {
188+
background: #d0d0d0;
189+
}
190+
191+
.inline-interactive-output {
192+
background: #f8f8f8;
193+
padding: 2px 5px;
194+
border-radius: 3px;
195+
color: #666;
196+
display: inline-block;
197+
min-width: 2em;
198+
text-align: right;
199+
}
200+
"),
201+
202+
# Modified JavaScript to handle integer formatting
203+
shiny::tags$script(sprintf("$(document).ready(function() {
204+
var isDragging = false;
205+
var startY;
206+
var startValue;
207+
var sensitivity = %f;
208+
var $activeInput = null;
209+
210+
$('.inline-interactive-number').on('mousedown touchstart', function(e) {
211+
isDragging = true;
212+
$activeInput = $(this);
213+
startY = e.type === 'mousedown' ? e.pageY : e.touches[0].pageY;
214+
startValue = parseFloat($activeInput.val()) || 0;
215+
$('body').css('cursor', 'ew-resize');
216+
$activeInput.addClass('dragging');
217+
e.preventDefault();
218+
return false;
219+
});
220+
221+
$(document).on('mousemove touchmove', function(e) {
222+
if (!isDragging || !$activeInput) return;
223+
224+
var currentY = e.type === 'mousemove' ? e.pageY : e.touches[0].pageY;
225+
var deltaY = startY - currentY;
226+
var newValue = startValue + (deltaY * sensitivity);
227+
228+
var min = parseFloat($activeInput.attr('min'));
229+
var max = parseFloat($activeInput.attr('max'));
230+
var step = parseFloat($activeInput.attr('step')) || 1;
231+
232+
if (!isNaN(min)) newValue = Math.max(min, newValue);
233+
if (!isNaN(max)) newValue = Math.min(max, newValue);
234+
newValue = Math.round(newValue / step) * step;
235+
236+
// Format based on step size
237+
var formattedValue = step === 1
238+
? Math.round(newValue).toString()
239+
: newValue.toFixed(1);
240+
241+
$activeInput.val(formattedValue);
242+
Shiny.setInputValue($activeInput.attr('id'), newValue);
243+
244+
e.preventDefault();
245+
return false;
246+
});
247+
248+
$(document).on('mouseup touchend', function(e) {
249+
if (isDragging) {
250+
isDragging = false;
251+
$activeInput.removeClass('dragging');
252+
$activeInput = null;
253+
$('body').css('cursor', 'default');
254+
e.preventDefault();
255+
}
256+
});
257+
258+
$('.inline-interactive-number').on('wheel', function(e) {
259+
var $input = $(this);
260+
var step = parseFloat($input.attr('step')) || 1;
261+
var delta = e.originalEvent.deltaY < 0 ? step : -step;
262+
var currentValue = parseFloat($input.val()) || 0;
263+
var newValue = currentValue + delta;
264+
265+
var min = parseFloat($input.attr('min'));
266+
var max = parseFloat($input.attr('max'));
267+
if (!isNaN(min)) newValue = Math.max(min, newValue);
268+
if (!isNaN(max)) newValue = Math.min(max, newValue);
269+
270+
// Format based on step size
271+
var formattedValue = step === 1
272+
? Math.round(newValue).toString()
273+
: newValue.toFixed(1);
274+
275+
$input.val(formattedValue);
276+
Shiny.setInputValue($input.attr('id'), newValue);
277+
278+
e.preventDefault();
279+
return false;
280+
});
281+
282+
// Handle direct input changes
283+
$('.inline-interactive-number').on('input change', function(e) {
284+
var $input = $(this);
285+
var value = parseFloat($input.val()) || 0;
286+
var step = parseFloat($input.attr('step')) || 1;
287+
288+
// Format based on step size
289+
var formattedValue = step === 1
290+
? Math.round(value).toString()
291+
: value.toFixed(1);
292+
293+
$input.val(formattedValue);
294+
Shiny.setInputValue($input.attr('id'), value);
295+
});
296+
});
297+
", sensitivity))
298+
)
299+
)
300+
}

0 commit comments

Comments
 (0)