Last active
November 12, 2021 00:20
-
-
Save dralletje/cfd63cdfff590260be3159ab9e130cd8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### A Pluto.jl notebook ### | |
# v0.17.1 | |
using Markdown | |
using InteractiveUtils | |
# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error). | |
macro bind(def, element) | |
quote | |
local el = $(esc(element)) | |
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing | |
el | |
end | |
end | |
# ╔═╡ 94e75fdf-de64-4150-b154-909b0b8c48b1 | |
md""" | |
# Javascript (and State) in Pluto | |
This package gives the ability to use ObservableHQ style javascript cells. | |
Importing this in a notebook requires JSON3 to be installed: | |
```julia | |
import JSON3 | |
module PlutoJavascript require("PlutoJavascript.jl") end | |
import .PlutoJavascript: @javascript, State | |
``` | |
""" | |
# ╔═╡ 9e5078da-50dd-4fe8-a639-b8170605f64e | |
md"### Javascript basics" | |
# ╔═╡ 4ffd15b8-55fc-4840-ac6d-427c08b6810d | |
@bind slider_number html"<input type=range />" | |
# ╔═╡ 26239200-e1dc-40f5-94d2-608108229768 | |
md"### Observable Import-ish" | |
# ╔═╡ dff95b2e-06af-4904-9984-d389d7b0e0ce | |
md"#### Keep the same element for animations and transitions!" | |
# ╔═╡ 28636b0c-fe8a-487a-8667-3415dfe2342e | |
@bind is_blue html"<input type=checkbox>" | |
# ╔═╡ 7017c598-641a-4958-84be-09cd6de58d2b | |
html"""<style> | |
.pluto-observablehq-like[pluto-js-renderer="app"] { | |
transition: opacity .4s; | |
transition-delay: 0.2s; | |
} | |
pluto-cell.queued .pluto-observablehq-like[pluto-js-renderer="app"] > *, | |
pluto-cell.running .pluto-observablehq-like[pluto-js-renderer="app"] > *, | |
.pluto-observablehq-like[pluto-js-renderer="app"][pluto-js-is-loading] > * { | |
opacity: 0.2; | |
cursor: wait; | |
} | |
""" | |
# ╔═╡ 1358d748-fa55-428f-8dd3-b401cca323ef | |
md"### Toggle between value, element and error" | |
# ╔═╡ ec8f2ae7-f5d4-40c8-af46-d102a56bace0 | |
@bind app_state html"""<select> | |
<option value="element">Element</option> | |
<option value="value">Value</option> | |
<option value="error">Error</option> | |
</select>""" | |
# ╔═╡ 25d7af20-3b31-4c7e-89d7-8470a55ae759 | |
md"#### Fetch and html example" | |
# ╔═╡ 5360f727-4eb9-4f4f-9694-4148743c6755 | |
md"#### Error flows through javascript" | |
# ╔═╡ fe0d0426-cdaa-43dd-be06-0d5667f1af14 | |
md"### Component example (with state!)" | |
# ╔═╡ 8ed3f97d-7360-468f-a457-362b38ca505c | |
columns = [ | |
"Column A", | |
"Column B", | |
"Column C", | |
] | |
# ╔═╡ 4989de20-2e6b-4d17-8f20-0a4c9b704c3b | |
md""" | |
### Vanilla javascript version of the state listeners | |
I decided to still use Observable style javascript instead of <script> tags, | |
because screw <script> tags (and it does automatic show(::MIME"application/javascript) stuff for the bond :D). Easy enough to change back though. | |
""" | |
# ╔═╡ 43926f68-7a28-48c8-8cdf-6fcac6e94b63 | |
md"## Appendix" | |
# ╔═╡ 4d15edb4-f5a2-4766-85ce-96a1519f4a83 | |
import JSON3 | |
# ╔═╡ 969af4f8-22cf-4c48-b282-d7cad9627ed9 | |
import UUIDs | |
# ╔═╡ 66bb4876-9db3-4061-aa4c-251e67cfb6c8 | |
md"### Javascript" | |
# ╔═╡ 11de0697-362e-41e9-a443-83cb6bc56b47 | |
"JavascriptRef" | |
struct JavascriptRef | |
id::String | |
code::String | |
end | |
# ╔═╡ 56eb5717-cc0f-4ba8-970a-878ce0f0310b | |
struct Javascript | |
code::String | |
end | |
# ╔═╡ 511ad5a6-f913-43de-b73c-d0b91d70aa85 | |
function Base.show(io::IO, ::MIME"application/javascript", js::Javascript) | |
print(io, js.code) | |
end | |
# ╔═╡ dfc22d15-4dc2-49a5-8153-7b09fe9117a2 | |
embed(js::JavascriptRef) = Javascript(js.code) | |
# ╔═╡ f3e4cce5-ead1-4644-908a-ef5cf2ee3986 | |
"Show JavascriptRef element" | |
function Base.show(io::IO, ::MIME"text/html", ref::JavascriptRef) | |
print(io, """ | |
<script id="inspector-patronus"> | |
// Do pluto_value_registry voodoo | |
let ref_id = $(JSON3.write(ref.id)) | |
window.pluto_value_registry = window.pluto_value_registry || {} | |
// I'd throw an error here directly, but I want to show it in a nice | |
// observable inspector, so I keep my mouth shut until just before | |
// I'm actually going to execute the script, and then I'm not going to 😈 | |
let is_already_on_the_dom = window.pluto_value_registry[ref_id]?.on_the_dom === true | |
if (window.pluto_value_registry[ref_id] == null) { | |
let defered = {} | |
let promise = new Promise((resolve, reject) => { | |
defered.resolve = resolve | |
defered.reject = reject | |
}) | |
defered.promise = promise | |
window.pluto_value_registry[ref_id] = defered | |
} | |
window.pluto_value_registry[ref_id].on_the_dom = true | |
let ref_promise = window.pluto_value_registry[ref_id] | |
invalidation.then(() => { | |
delete window.pluto_value_registry[ref_id] | |
}) | |
let widget_container = this | |
if (widget_container == null) { | |
widget_container = html`<span | |
class="pluto-observablehq-like" | |
pluto-js-renderer="none" | |
style="display: contents" | |
></span>` | |
Object.defineProperties(widget_container, { | |
value: { | |
set: function(value) { | |
if (this.firstElementChild != null) { | |
this.firstElementChild.value = value | |
} | |
}, | |
get: function() { | |
return this.firstElementChild?.value | |
}, | |
}, | |
}) | |
} | |
widget_container.setAttribute("pluto-js-is-loading", "") | |
// So I can check for invalidation after await'ing the value | |
let is_invalidated = false | |
invalidation.then(() => { | |
is_invalidated = true | |
}) | |
// Straight out of Pluto source | |
let execute_dynamic_function = async ({ environment, code }) => { | |
// single line so that we don't affect line numbers in the stack trace | |
const wrapped_code = `"use strict"; return (async () => {\${code}})()` | |
let { ["this"]: this_value, ...args } = environment | |
let arg_names = Object.keys(args) | |
let arg_values = Object.values(args) | |
const result = await Function(...arg_names, wrapped_code).bind(this_value)(...arg_values) | |
return result | |
} | |
// "inspector" or "app" | |
let renderer = widget_container.getAttribute("pluto-js-renderer") | |
let previous_value = widget_container.previous_value | |
let spawn_inspector = async () => { | |
if (renderer === "inspector" && widget_container.observablehq_inspector_instance != null) { | |
return widget_container.observablehq_inspector_instance | |
} | |
let inspector_container = DOM.element('span') | |
let shadow = inspector_container.attachShadow({mode: 'open'}); | |
shadow.innerHTML = ` | |
<link rel="stylesheet" href="https://unpkg.com/@observablehq/[email protected]/src/style.css"> | |
<style> | |
/* Inspector CSS uses :root, but apparently that isn't good enough | |
for shadow dom 🤷♂️ */ | |
:host { | |
--syntax_normal: #1b1e23; | |
--syntax_comment: #a9b0bc; | |
--syntax_number: #20a5ba; | |
--syntax_keyword: #c30771; | |
--syntax_atom: #10a778; | |
--syntax_string: #008ec4; | |
--syntax_error: #ffbedc; | |
--syntax_unknown_variable: #838383; | |
--syntax_known_variable: #005f87; | |
--syntax_matchbracket: #20bbfc; | |
--syntax_key: #6636b4; | |
--mono_fonts: 82%/1.5 Menlo, Consolas, monospace; | |
} | |
/* For some, weird reason, this rule isn't | |
in the open source version */ | |
.observablehq--caret { | |
margin-right: 4px; | |
vertical-align: baseline; | |
} | |
/* Makes the whole inspector flow like text */ | |
.observablehq--inspect { | |
display: inline; | |
} | |
#inspect { | |
font-family: var(--mono_fonts); | |
} | |
/* Add a gimmicky javascript logo */ | |
.observablehq--inspect.observablehq--collapsed > a::before, | |
.observablehq--inspect:not(.observablehq--collapsed)::before, | |
.observablehq--running::before { | |
all: initial; | |
content: 'JS'; | |
color: #323330; | |
background-color: #F0DB4F; | |
display: inline-block; | |
padding-left: 4px; | |
padding-right: 4px; | |
padding-top: 3px; | |
padding-bottom: 2px; | |
margin-right: 5px; | |
font-size: 14px; | |
font-family: 'Roboto Mono'; | |
font-weight: bold; | |
margin-bottom: 3px; | |
} | |
</style> | |
<span id="inspect"> | |
</span> | |
` | |
let {Inspector} = await import("https://unpkg.com/@observablehq/[email protected]/src/index.js?module") | |
let inspector = new Inspector(shadow.querySelector('#inspect')) | |
widget_container.setAttribute("pluto-js-renderer", "inspector") | |
widget_container.replaceChildren(inspector_container) | |
widget_container.observablehq_inspector_instance = inspector | |
return inspector | |
} | |
try { | |
if (is_already_on_the_dom) { | |
throw new Error("It's not yet possible to render a JavascriptRef to the same page at multiple places at the same time.") | |
} | |
let value = await execute_dynamic_function({ | |
environment: { | |
"this": widget_container.previous_value, | |
invalidation: invalidation, | |
getPublishedObject: getPublishedObject, | |
DOM: DOM, | |
Files: Files, | |
Generators: Generators, | |
Promises: Promises, | |
now: now, | |
svg: svg, | |
html: html, | |
require: require, | |
}, | |
code: $(JSON3.write(ref.code)), | |
}) | |
if (is_invalidated) return | |
ref_promise.resolve(value) | |
widget_container.previous_value = value | |
// Don't show HTMLElement inside inspector, because it will | |
// be rendered inside the shadow dom, and I don't like that (yet?) | |
if (value instanceof HTMLElement) { | |
widget_container.setAttribute("pluto-js-renderer", "app") | |
if (value !== previous_value) { | |
widget_container.replaceChildren(value) | |
// Not sure if this should be in or outside of this `if` | |
value.addEventListener("input", () => { | |
widget_container.dispatchEvent(new Event("input")) | |
}) | |
widget_container.dispatchEvent( | |
new Event("input") | |
) | |
} | |
} else { | |
let inspector = await spawn_inspector() | |
inspector.fulfilled(value) | |
} | |
} catch (error) { | |
if (is_invalidated) return | |
// Hide "uncaught promise" warning in the browser | |
// ref_promise.promise.catch(() => {}) | |
ref_promise.reject(error) | |
widget_container.previous_value = null | |
let inspector = await spawn_inspector() | |
inspector.rejected(error) | |
} finally { | |
widget_container.removeAttribute("pluto-js-is-loading") | |
} | |
return widget_container; | |
</script> | |
""") | |
end | |
# ╔═╡ 3da48b70-6f5a-440b-84c7-167aae2b7ac9 | |
"Render javascript for state" | |
function Base.show(io::IO, ::MIME"application/javascript", ref::JavascriptRef) | |
print(io, """(await (() => { | |
window.pluto_value_registry = window.pluto_value_registry || {} | |
let ref_id = $(JSON3.write(ref.id)) | |
if (window.pluto_value_registry[ref_id] == null) { | |
// No promise/defered, so we put out own there | |
let defered = {} | |
let promise = new Promise((resolve, reject) => { | |
defered.resolve = resolve | |
defered.reject = reject | |
}) | |
defered.promise = promise | |
window.pluto_value_registry[ref_id] = defered | |
} | |
return window.pluto_value_registry[ref_id].promise | |
})()) | |
""") | |
end | |
# ╔═╡ 3dc4c309-e8b4-4265-b325-7fa3ebc2ac40 | |
"Same as javascript but no mime" | |
function Base.show(io::IO, ref::JavascriptRef) | |
show(io, MIME("application/javascript"), ref) | |
end | |
# ╔═╡ eeae3d5a-adcd-4641-adea-2c39b92ea26f | |
function expression_to_string(expr) | |
repr(expr) | |
end | |
# ╔═╡ e4d22957-7020-42bd-8a1f-162c3757502f | |
function expression_to_string(expr::Symbol) | |
string(expr) | |
end | |
# ╔═╡ d7c12348-0aa2-44c4-81c3-87ebcd1779ee | |
function expression_to_string(with_linenums::Expr) | |
expr = Base.remove_linenums!(deepcopy(with_linenums)) | |
if Meta.isexpr(expr, :block, 1) | |
expression_to_string(expr.args[1]) | |
else | |
result = repr(expr) | |
if startswith(result, ":") | |
result[begin+1:end] | |
else | |
result | |
end | |
end | |
end | |
# ╔═╡ d02097f4-8fb9-452e-8862-dcb4dc20b090 | |
""" | |
wrap_javascript_in_pluto_shell(code::String) | |
Wrap javascript code in <script> tags, add the code to share the value between | |
cells in javascript, and render with Observable inspector. | |
""" | |
function wrap_javascript_in_pluto_shell(code::AbstractString) | |
uuid = string(UUIDs.uuid4()) | |
JavascriptRef(uuid, code) | |
end | |
# ╔═╡ ad490316-621c-44e4-8644-7caec7d72f28 | |
macro javascript(cell::String) | |
wrap_javascript_in_pluto_shell; # For Pluto dependency graph | |
trimmed = strip(cell) | |
if startswith(trimmed, "{") && endswith(trimmed, "}") | |
wrap_javascript_in_pluto_shell(trimmed[begin+1:end-1]) | |
else | |
wrap_javascript_in_pluto_shell("return $(cell)") | |
end | |
end | |
# ╔═╡ 10255d34-fcf2-4fb6-9e10-e32eb9df7dd6 | |
macro javascript(expr::Expr) | |
wrap_javascript_in_pluto_shell; # For Pluto dependency graph | |
if !Meta.isexpr(expr, :string) throw("Mehh") end | |
expression_parts = expr.args | |
is_expression = true | |
_begin = firstindex(expression_parts) | |
if ( | |
expression_parts[_begin] isa String && | |
startswith(strip(expression_parts[_begin]), "{") && | |
expression_parts[end] isa String && | |
endswith(strip(expression_parts[end]), "}") | |
) | |
expression_parts[_begin] = expression_parts[_begin][_begin+1:end] | |
expression_parts[end] = expression_parts[end][_begin:end-1] | |
is_expression = false | |
end | |
expression_parts_with_staticness = map(expr.args) do arg | |
is_static = arg isa String | |
:(( | |
is_static=$(is_static), | |
value=$(esc(arg)), | |
name=$(is_static ? nothing : expression_to_string(arg)) | |
)) | |
end | |
quote | |
expression_parts = [$(expression_parts_with_staticness...)] | |
is_expression = $(is_expression) | |
string_parts = [] | |
variables_to_load = Dict{String,String}() | |
for (is_static, part, name) in expression_parts | |
if is_static | |
push!(string_parts, part) | |
else | |
name_safe = JSON3.write(name) | |
push!(string_parts, "deps[$(name_safe)]") | |
if showable(MIME("application/javascript"), part) | |
variables_to_load[name_safe] = repr(MIME("application/javascript"), part) | |
elseif showable(MIME("application/json"), part) | |
# TODO Make JSON not load as javascript, but with JSON.parse | |
variables_to_load[name_safe] = repr(MIME("application/json"), part) | |
else | |
# TODO Make JSON not load as javascript, but with JSON.parse | |
# variables_to_load[name_safe] = JSON3.write(part) | |
variables_to_load[name_safe] = PlutoRunner.publish_to_js(part) | |
end | |
end | |
end | |
preload = map(collect(variables_to_load)) do variable_to_load | |
(name, load_str) = variable_to_load | |
""" | |
try { | |
deps[$name] = $load_str | |
} catch (error) { | |
throw new ReferenceError(`\${$name} is not defined`) | |
} | |
""" | |
end | |
preload_block = """ | |
let deps = {} | |
$(join(preload, "\n")) | |
""" | |
if is_expression | |
wrap_javascript_in_pluto_shell(""" | |
$(preload_block) | |
return ($(string_parts...)) | |
""") | |
else | |
wrap_javascript_in_pluto_shell(""" | |
$(preload_block) | |
$(string_parts...) | |
""") | |
end | |
end | |
end | |
# ╔═╡ ca0ebf01-e620-4029-a583-376859de7384 | |
js_string = @javascript "`hey`" | |
# ╔═╡ 3ea4d132-2d69-497e-a1d9-71bdb1ca9ac5 | |
js_number = @javascript "10" | |
# ╔═╡ 1b3d8ce9-0b9d-4f02-8e2c-2c5a4d560a3b | |
js_function = @javascript "(a, b, c) => {}" | |
# ╔═╡ 0b6faa4e-84a1-49bc-b3ce-c99e87bc4ada | |
typeof(js_function) | |
# ╔═╡ 7b23a041-bbe7-4e47-a264-0db20366422a | |
js_array = @javascript "[1, 2, 3, 4]" | |
# ╔═╡ 4fee059b-b327-463b-ba4b-1c495a899ae4 | |
js_dict_with_references = @javascript """({ | |
number: $js_number, | |
nested: { object: "with", some: "key" }, | |
})""" | |
# ╔═╡ d9420313-31d0-4785-a9f8-32e71b0edcdb | |
move_slider_to_see_changes = @javascript "({ number: $slider_number })" | |
# ╔═╡ f4af25a0-8003-4b0a-9ea0-a37a4017d8f6 | |
ObservableCell(url, cell; kwargs...) = @javascript """{ | |
const div = html`<div></div>` | |
const { Runtime, Inspector } = await import("https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js") | |
const { default: define } = await import($url) | |
const runtime = new Runtime() | |
const main = runtime.module(define, name => { | |
if (name === $cell) return new Inspector(div); | |
}); | |
console.log('main:', main) | |
let entries = $(Dict(kwargs...)) | |
console.log('entries:', entries) | |
for (let [key, value] of Object.entries(entries)) { | |
main.redefine(key, value) | |
} | |
return div | |
}""" | |
# ╔═╡ 9b682047-6561-447d-8a3e-6b63bb6bd69a | |
ObservableCell( | |
"https://api.observablehq.com/d/[email protected]?v=3", | |
"chart", | |
data=[1,2,6,9,1,10,11,5] | |
) | |
# ╔═╡ 0ee7c824-4230-4f2e-b51d-97e2fd990efa | |
@javascript """{ | |
let element = this || html` | |
<div style=" | |
width: 100px; | |
height: 100px; | |
transition: all 1s; | |
"> | |
` | |
await new Promise((resolve) => setTimeout(resolve, 1000)) | |
console.log(':::') | |
element.style.backgroundColor = $(is_blue) ? "blue" : "cyan" | |
return element | |
}""" | |
# ╔═╡ 90f81a53-aff3-464d-b30a-1b641eedbba9 | |
@javascript """{ | |
await new Promise((resolve) => setTimeout(resolve, 2000)) | |
if ($(app_state) == "element") { | |
return html`<div style=" | |
padding: 3px 16px; | |
background-color: #eee; | |
border-radius: 5px; | |
">Here is an HTML element for you</div>` | |
} else if ($(app_state) == "value") { | |
return "Here is a value for ya!" | |
} else if ($(app_state) == "error") { | |
throw new Error("Here is an error for you :O") | |
} | |
}""" | |
# ╔═╡ b7b8d9f4-48e7-4444-8388-8c670fb37458 | |
js_async = @javascript """{ | |
await new Promise((resolve) => setTimeout(resolve, 5000)) | |
let response = await fetch(`https://jsonplaceholder.typicode.com/todos/1`) | |
return await response.json() | |
}""" | |
# ╔═╡ a60606d9-6bf6-41db-a716-723f1e9e5c48 | |
derived_from_async = @javascript """ | |
html` | |
<article> | |
<b>\${$js_async.title}</b> | |
<p>\${$js_async.completed ? "Complete" : "Not Complete"}</p> | |
</article> | |
` | |
""" | |
# ╔═╡ 47f01cfa-9ace-4762-a27d-9b724aa63f70 | |
js_syntax_error = @javascript "{" | |
# ╔═╡ e0218257-3f8b-4bf4-88bb-8550045dfcb5 | |
derived_from_syntax_error = @javascript "$(js_syntax_error)" | |
# ╔═╡ ef202964-5ed9-49ff-a749-c3aa91799f3e | |
md"### React dependencies and utils" | |
# ╔═╡ c1ab37f5-5bcd-4f86-b986-900ebde9e4b1 | |
react_require_prod = @javascript """require.alias({ | |
react: "react@17/umd/react.development.js", | |
"react-dom": "react-dom@17/umd/react-dom.development.js" | |
})""" | |
# ╔═╡ cb636a28-9cac-43b4-af0f-3fada70f1fea | |
React = @javascript "$react_require_prod('react')" | |
# ╔═╡ 6620a98d-0e99-4a68-9295-61c4cbc4a55d | |
useStateElement = @javascript """(element) => { | |
let [value, set_value] = $(React).useState(element.value) | |
$(React).useEffect(() => { | |
let handle = () => { | |
set_value(element.value) | |
} | |
element.addEventListener('change', handle) | |
return () => element.removeEventListener('change', handle) | |
}, [element, set_value]) | |
return [value, (new_value) => { | |
set_value(new_value) | |
element.value = new_value | |
element.dispatchEvent(new Event('input')) | |
}] | |
}""" | |
# ╔═╡ ded9dcbb-c4e1-4ac3-9c96-c3d19360251d | |
ReactDOM = @javascript "$react_require_prod('react-dom')" | |
# ╔═╡ 74940fe8-30a4-4cca-9922-b51bc622b141 | |
jsx = @javascript """ | |
(await require("htm@3/dist/htm.umd.js")).bind($React.createElement) | |
""" | |
# ╔═╡ 81b419fc-7a25-42b2-a65f-043f61b5cbc5 | |
BioequivalenceConfig = @javascript """({ element, columns }) => { | |
let [value, set_value] = $useStateElement(element) | |
return $jsx`<div style=\${{ display: 'flex', flexDirection: 'column' }}> | |
<div> | |
<b>BE Enabled</b> | |
<input | |
type="checkbox" | |
checked=\${value.be_enabled} | |
onChange=\${(e) => { | |
set_value({ | |
...value, | |
be_enabled: e.target.checked, | |
}) | |
}} | |
/> | |
</div> | |
<select | |
placeholder="Period Columns" | |
type="text" | |
value=\${value.period_column} | |
onChange=\${(e) => { | |
set_value({ | |
...value, | |
period_column: e.target.value, | |
}) | |
}} | |
> | |
<option value="">Select Column</option> | |
\${columns.map((column) => $jsx` | |
<option key=\${column} value=\${column}>\${column}</options> | |
`)} | |
</select> | |
</div>` | |
}""" | |
# ╔═╡ 6d2b689d-8d56-4886-99fe-a95372ed0192 | |
render = @javascript """(container, element) => { | |
if (element == null) { | |
element = container; | |
container = null; | |
} | |
// Used to check specifically for `container == null`, but turns out | |
// that doesn't work nicely always with Observable. | |
if (!container) { | |
container = html`<div class="react-root" style="display: contents"></div>`; | |
// TODO Do some stuff to make this work as a input? | |
// Maybe even await the Suspense somehow? Would be cool. | |
} | |
element = typeof element === 'function' ? $jsx`<\${element} />` : element; | |
$ReactDOM.render( | |
$jsx` | |
<\${$React.Suspense} fallback=\${$jsx`<div />`}> | |
\${element} | |
</\${$React.Suspense}> | |
`, | |
container | |
); | |
return container; | |
}""" | |
# ╔═╡ 3353d69f-45ff-4d46-88c2-924e3ee6b5d7 | |
function skip_as_script(m::Module) | |
if isdefined(m, :PlutoForceDisplay) | |
return m.PlutoForceDisplay | |
else | |
isdefined(m, :PlutoRunner) && parentmodule(m) == Main | |
end | |
end | |
# ╔═╡ c5b75129-fd6c-44f3-aeee-fa366e0839dc | |
macro skip_as_script(ex) skip_as_script(__module__) ? esc(ex) : nothing end | |
# ╔═╡ 186079ef-487e-4065-a546-e775c7ee45ba | |
md"### State" | |
# ╔═╡ 812212dd-71e9-4789-9f99-de8d7107626d | |
begin | |
""" | |
@bind name State(key, initial_value) | |
""" | |
struct State | |
name::String | |
initial_value::Any | |
javascript_ref::JavascriptRef | |
function State(name::String, initial_value::Any) | |
javascript_ref = @javascript """{ | |
// Possibly use `this`? | |
let element = html`<dral-state-component> | |
<code | |
style="pointer-events: none; user-select: none;" | |
class="language-julia" | |
>State("\${$(name)}")</code> | |
</dral-state-component>` | |
element.setAttribute("name", $(name)) | |
element._value = $(initial_value) | |
Object.defineProperties(element, { | |
value: { | |
set: function(value) { | |
this._value = value | |
this.dispatchEvent(new Event("change")) | |
}, | |
get: function() { | |
return this._value | |
} | |
}, | |
}) | |
return element | |
}""" | |
new(name, initial_value, javascript_ref) | |
end | |
end | |
Base.get(state::State) = state.initial_value | |
function Base.show(io::IO, mime::MIME"text/html", state::State) | |
show(io, mime, state.javascript_ref) | |
end | |
function Base.show(io::IO, mime::MIME"application/javascript", state::State) | |
show(io, mime, state.javascript_ref) | |
end | |
function Base.showable(::MIME"application/javascript", bond::Main.PlutoRunner.Bond) | |
return bond.element isa State | |
end | |
function Base.show(io::IO, mime::MIME"application/javascript", bond::Main.PlutoRunner.Bond) | |
# Add a generic type to PlutoRunner.Bond? | |
Base.showable(mime, bond) || throw("Right now, only State() bonds can do JS") | |
show(io, mime, bond.element) | |
end | |
end | |
# ╔═╡ 2cd025ca-c646-4ad2-88ed-7bb3a8eb4c5f | |
export @javascript, State | |
# ╔═╡ 65be5d23-b64d-4f3f-85b5-44d588dbb435 | |
bond = @bind state State("my_number", Dict( | |
:be_enabled => true, | |
:period_column => "", | |
:sequence_column => "", | |
)) | |
# ╔═╡ 929f8963-aefa-4830-a3cf-cd3deae1dc8c | |
state | |
# ╔═╡ 0adb6d8c-d400-4fa0-93f0-e8dad097c4f0 | |
@javascript """{ | |
return $render($jsx` | |
<\${$BioequivalenceConfig} | |
element=\${$bond} | |
columns=\${$columns} | |
/> | |
`) | |
}""" | |
# ╔═╡ 4c0303b3-dc4f-4514-9c6e-bc70ae368f8b | |
@javascript """{ | |
let input = html`<input | |
type="text" | |
style="width: 100%; font-size: inherit; font-family: inherit;" | |
/>` | |
let bond = $bond | |
// Show and keep in sync with the bond balue | |
input.value = JSON.stringify(bond.value) | |
bond.addEventListener('change', () => { | |
input.value = JSON.stringify(bond.value) | |
}) | |
// Set state when input is mutated | |
input.addEventListener('input', () => { | |
try { | |
let valid_json = JSON.parse(input.value) | |
bond.value = valid_json | |
bond.dispatchEvent(new Event('input')) | |
} catch (error) {} | |
}) | |
return input | |
}""" | |
# ╔═╡ 8afd3e48-c655-4b02-9c92-92eed0efe9a2 | |
@bind x State("hey", "") | |
# ╔═╡ 00000000-0000-0000-0000-000000000001 | |
PLUTO_PROJECT_TOML_CONTENTS = """ | |
[deps] | |
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" | |
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" | |
[compat] | |
JSON3 = "~1.9.0" | |
""" | |
# ╔═╡ 00000000-0000-0000-0000-000000000002 | |
PLUTO_MANIFEST_TOML_CONTENTS = """ | |
# This file is machine-generated - editing it directly is not advised | |
[[Dates]] | |
deps = ["Printf"] | |
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" | |
[[JSON3]] | |
deps = ["Dates", "Mmap", "Parsers", "StructTypes", "UUIDs"] | |
git-tree-sha1 = "e13a411f59a3f9ad25378b25a40ea08c2ded02e7" | |
uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" | |
version = "1.9.0" | |
[[Mmap]] | |
uuid = "a63ad114-7e13-5084-954f-fe012c677804" | |
[[Parsers]] | |
deps = ["Dates"] | |
git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc" | |
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" | |
version = "1.1.0" | |
[[Printf]] | |
deps = ["Unicode"] | |
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" | |
[[Random]] | |
deps = ["Serialization"] | |
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" | |
[[SHA]] | |
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" | |
[[Serialization]] | |
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" | |
[[StructTypes]] | |
deps = ["Dates", "UUIDs"] | |
git-tree-sha1 = "e36adc471280e8b346ea24c5c87ba0571204be7a" | |
uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" | |
version = "1.7.2" | |
[[UUIDs]] | |
deps = ["Random", "SHA"] | |
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" | |
[[Unicode]] | |
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" | |
""" | |
# ╔═╡ Cell order: | |
# ╟─94e75fdf-de64-4150-b154-909b0b8c48b1 | |
# ╠═2cd025ca-c646-4ad2-88ed-7bb3a8eb4c5f | |
# ╟─9e5078da-50dd-4fe8-a639-b8170605f64e | |
# ╠═ca0ebf01-e620-4029-a583-376859de7384 | |
# ╠═3ea4d132-2d69-497e-a1d9-71bdb1ca9ac5 | |
# ╠═1b3d8ce9-0b9d-4f02-8e2c-2c5a4d560a3b | |
# ╠═0b6faa4e-84a1-49bc-b3ce-c99e87bc4ada | |
# ╠═7b23a041-bbe7-4e47-a264-0db20366422a | |
# ╠═4fee059b-b327-463b-ba4b-1c495a899ae4 | |
# ╠═4ffd15b8-55fc-4840-ac6d-427c08b6810d | |
# ╠═d9420313-31d0-4785-a9f8-32e71b0edcdb | |
# ╟─26239200-e1dc-40f5-94d2-608108229768 | |
# ╟─f4af25a0-8003-4b0a-9ea0-a37a4017d8f6 | |
# ╠═9b682047-6561-447d-8a3e-6b63bb6bd69a | |
# ╟─dff95b2e-06af-4904-9984-d389d7b0e0ce | |
# ╠═28636b0c-fe8a-487a-8667-3415dfe2342e | |
# ╠═7017c598-641a-4958-84be-09cd6de58d2b | |
# ╠═0ee7c824-4230-4f2e-b51d-97e2fd990efa | |
# ╟─1358d748-fa55-428f-8dd3-b401cca323ef | |
# ╠═ec8f2ae7-f5d4-40c8-af46-d102a56bace0 | |
# ╠═90f81a53-aff3-464d-b30a-1b641eedbba9 | |
# ╟─25d7af20-3b31-4c7e-89d7-8470a55ae759 | |
# ╠═b7b8d9f4-48e7-4444-8388-8c670fb37458 | |
# ╠═a60606d9-6bf6-41db-a716-723f1e9e5c48 | |
# ╟─5360f727-4eb9-4f4f-9694-4148743c6755 | |
# ╠═47f01cfa-9ace-4762-a27d-9b724aa63f70 | |
# ╠═e0218257-3f8b-4bf4-88bb-8550045dfcb5 | |
# ╟─fe0d0426-cdaa-43dd-be06-0d5667f1af14 | |
# ╠═6620a98d-0e99-4a68-9295-61c4cbc4a55d | |
# ╟─8ed3f97d-7360-468f-a457-362b38ca505c | |
# ╠═81b419fc-7a25-42b2-a65f-043f61b5cbc5 | |
# ╠═65be5d23-b64d-4f3f-85b5-44d588dbb435 | |
# ╠═929f8963-aefa-4830-a3cf-cd3deae1dc8c | |
# ╠═0adb6d8c-d400-4fa0-93f0-e8dad097c4f0 | |
# ╟─4989de20-2e6b-4d17-8f20-0a4c9b704c3b | |
# ╠═4c0303b3-dc4f-4514-9c6e-bc70ae368f8b | |
# ╟─43926f68-7a28-48c8-8cdf-6fcac6e94b63 | |
# ╠═4d15edb4-f5a2-4766-85ce-96a1519f4a83 | |
# ╠═969af4f8-22cf-4c48-b282-d7cad9627ed9 | |
# ╟─66bb4876-9db3-4061-aa4c-251e67cfb6c8 | |
# ╠═11de0697-362e-41e9-a443-83cb6bc56b47 | |
# ╠═56eb5717-cc0f-4ba8-970a-878ce0f0310b | |
# ╠═511ad5a6-f913-43de-b73c-d0b91d70aa85 | |
# ╠═dfc22d15-4dc2-49a5-8153-7b09fe9117a2 | |
# ╟─f3e4cce5-ead1-4644-908a-ef5cf2ee3986 | |
# ╟─3da48b70-6f5a-440b-84c7-167aae2b7ac9 | |
# ╟─3dc4c309-e8b4-4265-b325-7fa3ebc2ac40 | |
# ╟─eeae3d5a-adcd-4641-adea-2c39b92ea26f | |
# ╟─e4d22957-7020-42bd-8a1f-162c3757502f | |
# ╟─d7c12348-0aa2-44c4-81c3-87ebcd1779ee | |
# ╠═ad490316-621c-44e4-8644-7caec7d72f28 | |
# ╠═10255d34-fcf2-4fb6-9e10-e32eb9df7dd6 | |
# ╟─d02097f4-8fb9-452e-8862-dcb4dc20b090 | |
# ╟─ef202964-5ed9-49ff-a749-c3aa91799f3e | |
# ╟─c1ab37f5-5bcd-4f86-b986-900ebde9e4b1 | |
# ╟─cb636a28-9cac-43b4-af0f-3fada70f1fea | |
# ╟─ded9dcbb-c4e1-4ac3-9c96-c3d19360251d | |
# ╟─74940fe8-30a4-4cca-9922-b51bc622b141 | |
# ╟─6d2b689d-8d56-4886-99fe-a95372ed0192 | |
# ╟─3353d69f-45ff-4d46-88c2-924e3ee6b5d7 | |
# ╟─c5b75129-fd6c-44f3-aeee-fa366e0839dc | |
# ╟─186079ef-487e-4065-a546-e775c7ee45ba | |
# ╟─8afd3e48-c655-4b02-9c92-92eed0efe9a2 | |
# ╟─812212dd-71e9-4789-9f99-de8d7107626d | |
# ╟─00000000-0000-0000-0000-000000000001 | |
# ╟─00000000-0000-0000-0000-000000000002 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment