-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathbasic_widgets.jl
More file actions
363 lines (299 loc) · 11.4 KB
/
basic_widgets.jl
File metadata and controls
363 lines (299 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# Editable #
#=
Create an element inspired by (and almost equivalent to) the Scrubbable from
PlutoUI but with the possibility of changing the value by clicking on the number
and editing the value
=#
## Struct ##
Base.@kwdef struct Editable{T <: Number}
default::T
format::Union{AbstractString,Nothing}=nothing
prefix::AbstractString=""
suffix::AbstractString=""
end
### Real Version ###
"""
Editable(x::Real; [prefix, suffix, format])
Create a Pluto widget similar to [`Scrubbable`](@ref) from PlutoUI but that can contain an arbitrary (Real) number provided as input.
The displayed HTML will create a span with a blue background which contains the number and is preceded by an optional text `prefix` and an optional text `suffix`. If `format` is specified, it will be used to format the shown number using the [d3-format](https://github.com/d3/d3-format#locale_format) specification.
The widget will trigger a bond update only upon pressing Enter or moving the focus out of the widget itself.

# Keyword Arguments
- `prefix::AbstractString`: A string that will be inserted in the displayed HTML before the number. Clicking on the suffix will select the full text defining the number
- `suffix::AbstractString`: A string that will be inserted in the displayed HTML after the number. Clicking on the suffix will select the full text defining the number
- `format::AbstractString`: A string specifing the format to use for displaying the number in HTML. Uses the [d3-format](https://github.com/d3/d3-format#locale_format) specification
"""
Editable(x::Real; kwargs...) = Editable(; default=x, kwargs...)
### Bool Version ###
"""
Editable(x::Bool[, true_string="true", false_string="false")
Create a Pluto widget that contain a Boolean value.
The displayed HTML will create a span with a green background that displays the custom string `true_string` when true and the `false_string` when false. If not provided, the second argument `true_string` defaults to "true" and the third one the "false".
The widget will trigger a bond update when clicking on it.

"""
Editable(x::Bool,truestr::AbstractString="true",falsestr::AbstractString="false"; kwargs...) = Editable(; default=x, kwargs...,prefix=truestr,suffix=falsestr)
### AbstractPlutoDingetjes methods ###
Base.get(s::Editable) = s.default
Bonds.initial_value(s::Editable) = s.default
Bonds.possible_values(s::Editable) = Bonds.InfinitePossibilities()
Bonds.possible_values(s::Editable{Bool}) = (true, false)
Bonds.validate_value(s::Editable, from_browser::Union{Real, Bool}) = true
Bonds.validate_value(s::Editable, from_browser) = false
### Show - Bool ###
# In case of Bool type, the prefix and suffix are used as strings to display for the 'true' and 'false' flags respectively
function Base.show(io::IO, m::MIME"text/html", s::Editable{Bool})
show(io,m,@htl """
<script>
const d3format = await import("https://cdn.jsdelivr.net/npm/d3-format@2/+esm")
const el = html`
<span class="bool_Editable" style="
cursor: pointer;
touch-action: none;
padding: 0em .2em;
border-radius: .3em;
font-weight: bold;">$(s.default)</span>
`
const formatter = x => x ? $((s.prefix)) : $((s.suffix))
let localVal = $(s.default)
el.innerText = formatter($(s.default))
Object.defineProperty(el,"value",{
get: () => Boolean(localVal),
set: x => {
localVal = Boolean(x)
el.innerText = formatter(x)
}
})
el.addEventListener('click',(e) => {
el.value = el.value ? false : true
el.dispatchEvent(new CustomEvent("input"))
})
el.onselectstart = () => false
return el
</script>
<style>
@media (prefers-color-scheme: light) {
span.bool_Editable {
background: hsl(133, 47%, 73%);
}
}
@media (prefers-color-scheme: dark) {
span.bool_Editable {
background: hsl(133, 47%, 40%);
}
}
</style>
""")
end
### Show - Generic ###
function Base.show(io::IO, m::MIME"text/html", s::Editable)
format = if s.format === nothing
# TODO: auto format
if eltype(s.default) <: Integer
""
else
".4~g"
end
else
String(s.format)
end
show(io,m,@htl """
<script>
const d3format = await import("https://cdn.jsdelivr.net/npm/d3-format@2/+esm")
const elp = html`
<span class="number_Editable" style="
touch-action: none;
padding: 0em .2em;
border-radius: .3em;
font-weight: bold;">$(HypertextLiteral.JavaScript(s.prefix))<span contentEditable=true>$(s.default)</span>$(HypertextLiteral.JavaScript(s.suffix))</span>
`
const formatter = s => d3format.format($(format))(s)
const el = elp.querySelector("span")
let localVal = parseFloat($(s.default))
el.innerText = formatter($(s.default))
Object.defineProperty(elp,"value",{
get: () => localVal,
set: x => {
localVal = parseFloat(x)
el.innerText = formatter(x)
}
})
const dispatchEvent = (e) => {
if (el.innerText === "") {
elp.value = $(s.default)
} else {
/*
The replace is needed because d3-format outputs '-' as in U+2212 (math symbol) but fails to parse negative number correctly if they have that sign as negative sign. So we just replace it with the dash U+002D sign
*/
elp.value = el.innerText.replace('−', '-')
}
elp.dispatchEvent(new CustomEvent("input"))
}
// Function to blur the element when pressing enter instead of adding a newline
const onEnter = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
el.blur()
}
}
el.addEventListener('input',(e) => {
console.log(e)
e.preventDefault()
e.stopImmediatePropagation()
})
function selectText(el){
var sel, range;
if (window.getSelection && document.createRange) { //Browser compatibility
sel = window.getSelection();
if(sel.toString() == ''){ //no text selection
window.setTimeout(function(){
range = document.createRange(); //range object
range.selectNodeContents(el); //sets Range
sel.removeAllRanges(); //remove all ranges from selection
sel.addRange(range);//add Range to a Selection.
},1);
}
}else if (document.selection) { //older ie
sel = document.selection.createRange();
if(sel.text == ''){ //no text selection
range = document.body.createTextRange();//Creates TextRange object
range.moveToElementText(el);//sets Range
range.select(); //make selection.
}
}
}
el.addEventListener('focusout',dispatchEvent)
el.addEventListener('keydown',onEnter)
el.addEventListener('click',(e) => e.stopImmediatePropagation()) // modify text
elp.addEventListener('click',(e) => selectText(el)) // Select all text
return elp
</script>
<style>
@media (prefers-color-scheme: light) {
span.number_Editable {
background: hsl(204, 95%, 84%);
}
}
@media (prefers-color-scheme: dark) {
span.number_Editable {
background: hsl(204, 95%, 40%);
}
}
</style>
""")
end
# StringOnEnter #
#=
Create an element inspired by TextField from PlutoUI but with the possibility of
updating the bond value only when `Enter` is pressed ot the focus is moved away
from the field itself.
=#
## Struct ##
"""
StringOnEnter(default::AbstractString)
Creates a Pluto widget that allows to provide a string as output when used with `@bind`.
Unlike the custom "TextField" from PlutoUI this only triggers a bond update upon pressing Enter or moving the focus out of the widget (similar to [`Editable`](@ref))
When rendered in HTML, the widget text will be shown with a dark yellow background.

"""
struct StringOnEnter
default::String
end
## StringOnEnter - Show ##
Base.show(io::IO, mime::MIME"text/html", mt::StringOnEnter) = show(io, mime, @htl """
<span><span
class="text_StringOnEnter" style="
padding: 0em .2em;
border-radius: .3em;
font-weight: bold;"
contentEditable=true>$(mt.default)</span></span>
<script>
const elp = currentScript.previousElementSibling
const el = elp.querySelector('span')
Object.defineProperty(elp,"value",{
get: () => el.innerText,
set: x => {
el.innerText = x
}
})
const dispatchEvent = (e) => {
if (el.innerText === "") {
elp.value = $(mt.default)
} else {
elp.value = el.innerText
}
elp.dispatchEvent(new CustomEvent("input"))
}
el.addEventListener('input',(e) => {
console.log(e)
e.preventDefault()
e.stopImmediatePropagation()
})
const onEnter = (e) => {
if (e.keyCode === 13) {
e.preventDefault();
el.blur()
}
}
elp.addEventListener('focusout',dispatchEvent)
elp.addEventListener('keydown',onEnter)
</script>
<style>
@media (prefers-color-scheme: light) {
span.text_StringOnEnter {
background: hsl(48, 90%, 61%);
}
}
@media (prefers-color-scheme: dark) {
span.text_StringOnEnter {
background: hsl(48, 57%, 37%);
}
}
</style>
""")
## AbstractPlutoDingetjes Methods ##
Base.get(t::StringOnEnter) = t.default
Bonds.initial_value(t::StringOnEnter) = t.default
Bonds.possible_values(t::StringOnEnter) = Bonds.InfinitePossibilities()
function Bonds.validate_value(t::StringOnEnter, val)
val isa AbstractString
end
# DateTimePicker #
#=
Create an element to utilize the HTML5 date-time input type.
=#
## Struct ##
"""
DateTimePicker(default::DateTime=now(), min::Union{DateTime,Nothing}=nothing, max::Union{DateTime,Nothing}=nothing, step::Dates.Period=Dates.Minute(1))
Creates a Pluto widget that allows to provide a DateTime as output when used with `@bind`.
Due to the implementation of the HTML5 date-time input type, only date-times with a precision up to the minute are supported.
Options:
- `default::DateTime`: The default value to show when the widget is created. Defaults to the current date-time floored to the minute.
- `min::Union{DateTime,Nothing}`: The minimum date-time that can be selected. Defaults to `nothing` (no minimum)
- `max::Union{DateTime,Nothing}`: The maximum date-time that can be selected. Defaults to `nothing` (no maximum)
- `step::Dates.Period`: The step size, defaults to `Dates.Minute(1)`. This defines the increments in which the date-time can be changed.
When rendered in HTML, the widget will use the native date-time picker of the browser.
"""
Base.@kwdef struct DateTimePicker
default::DateTime=floor(now(), Dates.Minute)
min::Union{DateTime,Nothing}=nothing
max::Union{DateTime,Nothing}=nothing
step::Dates.Period=Dates.Minute(1)
function DateTimePicker(default, min, max, step)
flrd_default = floor(default, Dates.Minute)
flrd_min = isnothing(min) ? nothing : floor(min, Dates.Minute)
flrd_max = isnothing(max) ? nothing : floor(max, Dates.Minute)
new(flrd_default, flrd_min, flrd_max, step)
end
end
Base.show(io::IO, mime::MIME"text/html", dtp::DateTimePicker) = show(io, mime, @htl """
<input $((type="datetime-local", value=dtp.default, min=dtp.min, max=dtp.max, step=Dates.seconds(dtp.step)))></input>
""")
Base.get(dtp::DateTimePicker) = dtp.default
Bonds.initial_value(dtp::DateTimePicker) = dtp.default
function Bonds.transform_value(dtp::DateTimePicker, val)
something(tryparse(DateTime, val, dateformat"YYYY-mm-ddTHH:MM"), DateTime(1970,1,1))
end
function Bonds.validate_value(dtp::DateTimePicker, val)
!isnothing(tryparse(DateTime, val, dateformat"YYYY-mm-ddTHH:MM"))
end