2025-07-30 23:02:11 -07:00
|
|
|
import gleam/dict.{type Dict}
|
2025-07-22 00:21:54 -07:00
|
|
|
import gleam/dynamic.{type Dynamic}
|
2025-07-25 15:01:31 -07:00
|
|
|
import gleam/dynamic/decode
|
2025-07-30 23:02:11 -07:00
|
|
|
import gleam/http/response.{type Response}
|
2025-07-25 15:01:31 -07:00
|
|
|
import gleam/io
|
2025-07-30 23:02:11 -07:00
|
|
|
import gleam/json
|
2025-07-25 15:01:31 -07:00
|
|
|
import gleam/list
|
2025-07-30 23:02:11 -07:00
|
|
|
import gleam/option.{None, Some}
|
|
|
|
|
import gleam/pair
|
|
|
|
|
import gleam/regexp
|
2025-07-25 15:01:31 -07:00
|
|
|
import gleam/result
|
2025-07-30 23:02:11 -07:00
|
|
|
import gleam/set.{type Set}
|
2025-07-22 00:21:54 -07:00
|
|
|
import lustre.{type App}
|
|
|
|
|
import lustre/component
|
|
|
|
|
import lustre/effect.{type Effect}
|
|
|
|
|
import lustre/element.{type Element}
|
|
|
|
|
import lustre/element/html
|
2025-07-25 15:01:31 -07:00
|
|
|
import lustre/event
|
|
|
|
|
import plinth/browser/document
|
|
|
|
|
import plinth/browser/event as plinth_event
|
2025-07-30 23:02:11 -07:00
|
|
|
import rsvp
|
2025-07-22 00:21:54 -07:00
|
|
|
|
|
|
|
|
import context
|
2025-07-30 23:02:11 -07:00
|
|
|
import encodable.{type Encodable}
|
|
|
|
|
import field.{type Field}
|
|
|
|
|
import field_type.{type FieldType}
|
2025-07-22 00:21:54 -07:00
|
|
|
|
|
|
|
|
pub const name: String = "viewer-controller"
|
|
|
|
|
|
|
|
|
|
pub fn component() -> App(Nil, Model, Msg) {
|
|
|
|
|
lustre.component(init, update, view, [
|
|
|
|
|
component.on_attribute_change("root-path", fn(value) {
|
|
|
|
|
ParentChangedRootPath(value) |> Ok
|
|
|
|
|
}),
|
2025-07-30 23:02:11 -07:00
|
|
|
component.on_attribute_change("pkeys", fn(value) {
|
|
|
|
|
value
|
|
|
|
|
|> json.parse(
|
|
|
|
|
decode.list(decode.dict(decode.string, encodable.decoder())),
|
|
|
|
|
)
|
|
|
|
|
|> result.map(fn(lst) {
|
|
|
|
|
list.zip(list.range(0, list.length(lst)), lst)
|
|
|
|
|
|> dict.from_list()
|
|
|
|
|
|> ParentChangedPkeys()
|
|
|
|
|
})
|
|
|
|
|
|> result.replace_error(Nil)
|
2025-07-25 15:01:31 -07:00
|
|
|
}),
|
2025-07-30 23:02:11 -07:00
|
|
|
component.on_attribute_change("fields", fn(value) {
|
|
|
|
|
value
|
|
|
|
|
|> json.parse(decode.list(field.decoder()))
|
|
|
|
|
|> result.map(fn(lst) {
|
|
|
|
|
list.zip(list.range(0, list.length(lst)), lst)
|
|
|
|
|
|> dict.from_list()
|
|
|
|
|
|> ParentChangedFields()
|
|
|
|
|
})
|
|
|
|
|
|> result.replace_error(Nil)
|
2025-07-25 15:01:31 -07:00
|
|
|
}),
|
|
|
|
|
component.on_attribute_change("root-path", fn(value) {
|
|
|
|
|
ParentChangedRootPath(value) |> Ok
|
|
|
|
|
}),
|
2025-07-22 00:21:54 -07:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 23:02:11 -07:00
|
|
|
// -------- Model -------- //
|
|
|
|
|
|
2025-07-22 00:21:54 -07:00
|
|
|
pub type Model {
|
2025-07-25 15:01:31 -07:00
|
|
|
Model(
|
|
|
|
|
root_path: String,
|
|
|
|
|
root_path_consumers: List(Dynamic),
|
2025-07-30 23:02:11 -07:00
|
|
|
selections: List(#(Int, Int)),
|
2025-07-25 15:01:31 -07:00
|
|
|
editing: Bool,
|
2025-07-30 23:02:11 -07:00
|
|
|
pkeys: Dict(Int, Dict(String, Encodable)),
|
|
|
|
|
fields: Dict(Int, Field),
|
|
|
|
|
// Whether OS, ctrl, or alt keys are being held. If so, simultaneously
|
|
|
|
|
// pressing a typing key should not trigger an overwrite.
|
|
|
|
|
modifiers_held: Set(String),
|
2025-07-25 15:01:31 -07:00
|
|
|
)
|
2025-07-22 00:21:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn init(_) -> #(Model, Effect(Msg)) {
|
2025-07-25 15:01:31 -07:00
|
|
|
#(
|
|
|
|
|
Model(
|
|
|
|
|
root_path: "",
|
|
|
|
|
root_path_consumers: [],
|
2025-07-30 23:02:11 -07:00
|
|
|
selections: [],
|
2025-07-25 15:01:31 -07:00
|
|
|
editing: False,
|
2025-07-30 23:02:11 -07:00
|
|
|
pkeys: dict.new(),
|
|
|
|
|
fields: dict.new(),
|
|
|
|
|
modifiers_held: set.new(),
|
2025-07-25 15:01:31 -07:00
|
|
|
),
|
|
|
|
|
effect.before_paint(fn(dispatch, _) -> Nil {
|
|
|
|
|
document.add_event_listener("keydown", fn(ev) -> Nil {
|
|
|
|
|
let key = plinth_event.key(ev)
|
|
|
|
|
case key {
|
|
|
|
|
"ArrowLeft" | "ArrowRight" | "ArrowUp" | "ArrowDown" ->
|
|
|
|
|
dispatch(UserPressedDirectionalKey(key))
|
2025-07-30 23:02:11 -07:00
|
|
|
"Enter" -> dispatch(UserPressedEnterKey)
|
|
|
|
|
"Meta" | "Alt" | "Control" -> dispatch(UserPressedModifierKey(key))
|
|
|
|
|
_ ->
|
|
|
|
|
case is_typing_key(key) {
|
|
|
|
|
True -> dispatch(UserPressedTypingKey(key))
|
|
|
|
|
False -> Nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
document.add_event_listener("keyup", fn(ev) -> Nil {
|
|
|
|
|
let key = plinth_event.key(ev)
|
|
|
|
|
case key {
|
|
|
|
|
"Meta" | "Alt" | "Control" -> dispatch(UserReleasedModifierKey(key))
|
2025-07-25 15:01:31 -07:00
|
|
|
_ -> Nil
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
)
|
2025-07-22 00:21:54 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-30 23:02:11 -07:00
|
|
|
fn is_typing_key(key: String) -> Bool {
|
|
|
|
|
let assert Ok(re) =
|
|
|
|
|
regexp.from_string("^[a-zA-Z0-9!@#$%^&*._/?<>{}[\\]'\"~`-]$")
|
|
|
|
|
regexp.check(key, with: re)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------- Update -------- //
|
|
|
|
|
|
2025-07-22 00:21:54 -07:00
|
|
|
pub type Msg {
|
|
|
|
|
ParentChangedRootPath(String)
|
2025-07-30 23:02:11 -07:00
|
|
|
ParentChangedPkeys(Dict(Int, Dict(String, Encodable)))
|
|
|
|
|
ParentChangedFields(Dict(Int, Field))
|
|
|
|
|
ChildEmittedEditStartEvent
|
|
|
|
|
ChildEmittedEditUpdateEvent(original: Encodable, edited: Encodable)
|
|
|
|
|
ChildEmittedEditEndEvent(original: Encodable, edited: Encodable)
|
|
|
|
|
ChildRequestedRootPath(callback: Dynamic, subscribe: Bool)
|
|
|
|
|
ServerUpdateValuePost(Result(Response(String), rsvp.Error))
|
2025-07-25 15:01:31 -07:00
|
|
|
UserClickedCell(Int, Int)
|
2025-07-30 23:02:11 -07:00
|
|
|
UserDoubleClickedCell(Int, Int)
|
2025-07-25 15:01:31 -07:00
|
|
|
UserPressedDirectionalKey(String)
|
2025-07-30 23:02:11 -07:00
|
|
|
UserPressedEnterKey
|
|
|
|
|
UserPressedTypingKey(String)
|
|
|
|
|
UserPressedModifierKey(String)
|
|
|
|
|
UserReleasedModifierKey(String)
|
2025-07-22 00:21:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
|
|
|
|
case msg {
|
|
|
|
|
ParentChangedRootPath(root_path) -> #(
|
|
|
|
|
Model(..model, root_path:),
|
|
|
|
|
context.update_consumers(
|
|
|
|
|
model.root_path_consumers,
|
|
|
|
|
dynamic.string(root_path),
|
|
|
|
|
),
|
|
|
|
|
)
|
2025-07-30 23:02:11 -07:00
|
|
|
ParentChangedPkeys(pkeys) -> #(Model(..model, pkeys:), effect.none())
|
|
|
|
|
ParentChangedFields(fields) -> #(Model(..model, fields:), effect.none())
|
|
|
|
|
ChildEmittedEditStartEvent -> #(
|
|
|
|
|
Model(..model, editing: True),
|
2025-07-25 15:01:31 -07:00
|
|
|
effect.none(),
|
|
|
|
|
)
|
2025-07-30 23:02:11 -07:00
|
|
|
ChildEmittedEditUpdateEvent(_original, edited) -> {
|
|
|
|
|
case model.selections {
|
|
|
|
|
[coords] -> #(model, set_cell_value(coords, edited))
|
|
|
|
|
_ -> todo
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-10 14:32:15 -07:00
|
|
|
ChildEmittedEditEndEvent(_original, edited) ->
|
|
|
|
|
case model.selections {
|
|
|
|
|
[#(selected_row, _)] ->
|
|
|
|
|
case selected_row == dict.size(model.pkeys) {
|
|
|
|
|
// Cell under edit is in "insert" row
|
|
|
|
|
True -> #(Model(..model, editing: False), effect.none())
|
|
|
|
|
False -> #(model, commit_change(model:, value: edited))
|
|
|
|
|
}
|
|
|
|
|
_ -> todo
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 00:21:54 -07:00
|
|
|
ChildRequestedRootPath(callback, subscribe) -> #(
|
|
|
|
|
case subscribe {
|
|
|
|
|
True ->
|
|
|
|
|
Model(..model, root_path_consumers: [
|
|
|
|
|
callback,
|
|
|
|
|
..model.root_path_consumers
|
|
|
|
|
])
|
|
|
|
|
False -> model
|
|
|
|
|
},
|
|
|
|
|
context.update_consumers([callback], dynamic.string(model.root_path)),
|
|
|
|
|
)
|
2025-07-30 23:02:11 -07:00
|
|
|
ServerUpdateValuePost(response) -> {
|
|
|
|
|
case response {
|
|
|
|
|
Ok(_) -> #(Model(..model, editing: False), blur_hoverbar())
|
|
|
|
|
Error(rsvp.HttpError(response)) -> {
|
|
|
|
|
io.println_error("HTTP error while updating value: " <> response.body)
|
|
|
|
|
#(model, effect.none())
|
|
|
|
|
}
|
|
|
|
|
Error(err) -> {
|
|
|
|
|
echo err
|
|
|
|
|
#(model, effect.none())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
UserClickedCell(row, column) -> assign_selection(model:, to: #(row, column))
|
|
|
|
|
// TODO
|
|
|
|
|
UserDoubleClickedCell(_row, _column) -> #(model, case model.editing {
|
|
|
|
|
True -> effect.none()
|
|
|
|
|
False -> focus_hoverbar()
|
|
|
|
|
})
|
2025-07-25 15:01:31 -07:00
|
|
|
UserPressedDirectionalKey(key) -> {
|
2025-07-30 23:02:11 -07:00
|
|
|
case model.editing, model.selections {
|
|
|
|
|
False, [#(selected_row, selected_col)] -> {
|
2025-07-25 15:01:31 -07:00
|
|
|
let first_row_selected = selected_row == 0
|
2025-08-10 14:32:15 -07:00
|
|
|
// No need to subtract 1 from pkeys size, because there's another
|
|
|
|
|
// row for "insert".
|
|
|
|
|
let last_row_selected = selected_row == dict.size(model.pkeys)
|
2025-07-30 23:02:11 -07:00
|
|
|
let first_col_selected = selected_col == 0
|
|
|
|
|
let last_col_selected = selected_col == dict.size(model.fields) - 1
|
2025-07-25 15:01:31 -07:00
|
|
|
case
|
|
|
|
|
key,
|
|
|
|
|
first_row_selected,
|
|
|
|
|
last_row_selected,
|
|
|
|
|
first_col_selected,
|
|
|
|
|
last_col_selected
|
|
|
|
|
{
|
2025-07-30 23:02:11 -07:00
|
|
|
"ArrowLeft", _, _, False, _ ->
|
|
|
|
|
assign_selection(model:, to: #(selected_row, selected_col - 1))
|
|
|
|
|
"ArrowRight", _, _, _, False ->
|
|
|
|
|
assign_selection(model:, to: #(selected_row, selected_col + 1))
|
|
|
|
|
"ArrowUp", False, _, _, _ ->
|
|
|
|
|
assign_selection(model:, to: #(selected_row - 1, selected_col))
|
|
|
|
|
"ArrowDown", _, False, _, _ ->
|
|
|
|
|
assign_selection(model:, to: #(selected_row + 1, selected_col))
|
2025-07-25 15:01:31 -07:00
|
|
|
_, _, _, _, _ -> #(model, effect.none())
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-30 23:02:11 -07:00
|
|
|
False, [] ->
|
|
|
|
|
case dict.size(model.pkeys) > 0 && dict.size(model.fields) > 0 {
|
|
|
|
|
True -> assign_selection(model:, to: #(0, 0))
|
|
|
|
|
False -> #(model, effect.none())
|
|
|
|
|
}
|
|
|
|
|
_, _ -> #(model, effect.none())
|
2025-07-25 15:01:31 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-30 23:02:11 -07:00
|
|
|
UserPressedEnterKey -> #(model, case model.editing {
|
|
|
|
|
True -> effect.none()
|
|
|
|
|
False -> focus_hoverbar()
|
|
|
|
|
})
|
|
|
|
|
UserPressedTypingKey(key) ->
|
|
|
|
|
case model.editing, model.selections, set.is_empty(model.modifiers_held) {
|
|
|
|
|
False, [_], True -> #(model, overwrite_in_hoverbar(value: key))
|
|
|
|
|
_, _, _ -> #(model, effect.none())
|
|
|
|
|
}
|
|
|
|
|
UserPressedModifierKey(key) -> #(
|
|
|
|
|
Model(..model, modifiers_held: model.modifiers_held |> set.insert(key)),
|
|
|
|
|
effect.none(),
|
|
|
|
|
)
|
|
|
|
|
UserReleasedModifierKey(key) -> #(
|
|
|
|
|
Model(..model, modifiers_held: model.modifiers_held |> set.delete(key)),
|
|
|
|
|
effect.none(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn overwrite_in_hoverbar(value value: String) -> Effect(Msg) {
|
|
|
|
|
use _, _ <- effect.before_paint()
|
|
|
|
|
do_overwrite_in_hoverbar(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "overwriteInHoverbar")
|
|
|
|
|
fn do_overwrite_in_hoverbar(value _value: String) -> Nil {
|
|
|
|
|
Nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn commit_change(model model: Model, value value: Encodable) -> Effect(Msg) {
|
|
|
|
|
case model.selections {
|
|
|
|
|
[#(row, col)] ->
|
|
|
|
|
rsvp.post(
|
|
|
|
|
"./update-value",
|
|
|
|
|
json.object([
|
|
|
|
|
#(
|
|
|
|
|
"column",
|
|
|
|
|
model.fields
|
|
|
|
|
|> dict.get(col)
|
|
|
|
|
|> result.map(fn(field) { field.name })
|
|
|
|
|
|> result.unwrap("")
|
|
|
|
|
|> json.string(),
|
|
|
|
|
),
|
|
|
|
|
#(
|
|
|
|
|
"pkeys",
|
|
|
|
|
model.pkeys
|
|
|
|
|
|> dict.get(row)
|
|
|
|
|
|> result.unwrap(dict.new())
|
|
|
|
|
|> json.dict(fn(x) { x }, encodable.to_json),
|
|
|
|
|
),
|
|
|
|
|
#("value", encodable.to_json(value)),
|
|
|
|
|
]),
|
|
|
|
|
rsvp.expect_ok_response(ServerUpdateValuePost),
|
|
|
|
|
)
|
|
|
|
|
_ -> todo
|
2025-07-25 15:01:31 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 23:02:11 -07:00
|
|
|
/// Updater for when selection is being assigned to a single cell.
|
|
|
|
|
fn assign_selection(
|
|
|
|
|
model model: Model,
|
|
|
|
|
to coords: #(Int, Int),
|
|
|
|
|
) -> #(Model, Effect(Msg)) {
|
|
|
|
|
// Multiple sets of conditions fall back to the "selection actually changed"
|
|
|
|
|
// scenario, so this is a little awkward to write without a return keyword.
|
|
|
|
|
let early_return = case model.selections {
|
|
|
|
|
[prev_coords] ->
|
|
|
|
|
case prev_coords == coords {
|
|
|
|
|
True -> Some(#(model, effect.none()))
|
|
|
|
|
False -> None
|
|
|
|
|
}
|
|
|
|
|
_ -> None
|
|
|
|
|
}
|
|
|
|
|
case early_return {
|
|
|
|
|
Some(value) -> value
|
|
|
|
|
None -> #(
|
|
|
|
|
Model(..model, selections: [coords]),
|
|
|
|
|
effect.batch([
|
|
|
|
|
sync_cell_value_to_hoverbar(
|
|
|
|
|
coords,
|
|
|
|
|
model.fields
|
|
|
|
|
|> dict.get(pair.second(coords))
|
|
|
|
|
|> result.map(fn(f) { f.field_type })
|
|
|
|
|
|> result.unwrap(field_type.Unknown),
|
|
|
|
|
),
|
|
|
|
|
change_selected_attrs([coords]),
|
|
|
|
|
]),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------- Effects -------- //
|
|
|
|
|
|
|
|
|
|
fn focus_hoverbar() -> Effect(Msg) {
|
|
|
|
|
use _dispatch, _root <- effect.before_paint()
|
|
|
|
|
do_focus_hoverbar()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "focusHoverbar")
|
|
|
|
|
fn do_focus_hoverbar() -> Nil {
|
|
|
|
|
Nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn blur_hoverbar() -> Effect(Msg) {
|
2025-07-25 15:01:31 -07:00
|
|
|
use _dispatch, _root <- effect.before_paint()
|
2025-07-30 23:02:11 -07:00
|
|
|
do_blur_hoverbar()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "blurHoverbar")
|
|
|
|
|
fn do_blur_hoverbar() -> Nil {
|
|
|
|
|
Nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Effect that changes [selected=] attributes on assigned children. Should only
|
|
|
|
|
/// be called when the new selection is different than the previous one (that
|
|
|
|
|
/// is, not when selection is "moving" from a cell to the same cell).
|
|
|
|
|
fn change_selected_attrs(to_coords coords: List(#(Int, Int))) -> Effect(msg) {
|
|
|
|
|
use _, _ <- effect.before_paint()
|
2025-07-25 15:01:31 -07:00
|
|
|
do_clear_selected_attrs()
|
2025-07-30 23:02:11 -07:00
|
|
|
coords
|
|
|
|
|
|> list.map(fn(coords) {
|
|
|
|
|
let #(row, column) = coords
|
|
|
|
|
do_set_cell_attr(row:, column:, key: "selected", value: "true")
|
|
|
|
|
})
|
|
|
|
|
Nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_cell_value(coords coords: #(Int, Int), value value: Encodable) {
|
|
|
|
|
use _, _ <- effect.before_paint()
|
|
|
|
|
let #(row, col) = coords
|
|
|
|
|
do_set_cell_attr(
|
|
|
|
|
row,
|
|
|
|
|
col,
|
|
|
|
|
"value",
|
|
|
|
|
value |> encodable.to_json() |> json.to_string(),
|
|
|
|
|
)
|
2025-07-22 00:21:54 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 15:01:31 -07:00
|
|
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "clearSelectedAttrs")
|
|
|
|
|
fn do_clear_selected_attrs() -> Nil {
|
|
|
|
|
Nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 23:02:11 -07:00
|
|
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "setCellAttr")
|
|
|
|
|
fn do_set_cell_attr(
|
|
|
|
|
row _row: Int,
|
|
|
|
|
column _column: Int,
|
|
|
|
|
key _key: String,
|
|
|
|
|
value _value: String,
|
|
|
|
|
) -> Nil {
|
|
|
|
|
Nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn sync_cell_value_to_hoverbar(
|
|
|
|
|
coords coords: #(Int, Int),
|
|
|
|
|
field_type f_type: FieldType,
|
|
|
|
|
) -> Effect(Msg) {
|
|
|
|
|
use _, _root <- effect.before_paint()
|
|
|
|
|
let #(row, column) = coords
|
|
|
|
|
do_sync_cell_value_to_hoverbar(
|
|
|
|
|
row:,
|
|
|
|
|
column:,
|
|
|
|
|
field_type: f_type |> field_type.to_json() |> json.to_string(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "syncCellValueToHoverbar")
|
|
|
|
|
fn do_sync_cell_value_to_hoverbar(
|
|
|
|
|
row _row: Int,
|
|
|
|
|
column _column: Int,
|
|
|
|
|
field_type _field_type: String,
|
|
|
|
|
) -> Nil {
|
2025-07-25 15:01:31 -07:00
|
|
|
Nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 23:02:11 -07:00
|
|
|
// -------- View -------- //
|
|
|
|
|
|
2025-07-22 00:21:54 -07:00
|
|
|
fn view(_: Model) -> Element(Msg) {
|
|
|
|
|
html.div(
|
|
|
|
|
[
|
|
|
|
|
context.on_context_request(
|
|
|
|
|
dynamic.string("root_path"),
|
|
|
|
|
ChildRequestedRootPath,
|
|
|
|
|
),
|
2025-07-25 15:01:31 -07:00
|
|
|
event.on("cell-click", {
|
|
|
|
|
use msg <- decode.field("detail", {
|
|
|
|
|
use row <- decode.field("row", decode.int)
|
|
|
|
|
use column <- decode.field("column", decode.int)
|
|
|
|
|
decode.success(UserClickedCell(row, column))
|
|
|
|
|
})
|
|
|
|
|
decode.success(msg)
|
|
|
|
|
}),
|
2025-07-30 23:02:11 -07:00
|
|
|
event.on("cell-dblclick", {
|
|
|
|
|
use msg <- decode.field("detail", {
|
|
|
|
|
use row <- decode.field("row", decode.int)
|
|
|
|
|
use column <- decode.field("column", decode.int)
|
|
|
|
|
decode.success(UserDoubleClickedCell(row, column))
|
|
|
|
|
})
|
|
|
|
|
decode.success(msg)
|
|
|
|
|
}),
|
|
|
|
|
event.on("edit-start", decode.success(ChildEmittedEditStartEvent)),
|
|
|
|
|
event.on("edit-update", {
|
|
|
|
|
use original <- decode.subfield(
|
|
|
|
|
["detail", "original"],
|
|
|
|
|
encodable.decoder(),
|
|
|
|
|
)
|
|
|
|
|
use edited <- decode.subfield(["detail", "edited"], encodable.decoder())
|
|
|
|
|
decode.success(ChildEmittedEditUpdateEvent(original:, edited:))
|
|
|
|
|
}),
|
|
|
|
|
event.on("edit-end", {
|
|
|
|
|
use original <- decode.subfield(
|
|
|
|
|
["detail", "original"],
|
|
|
|
|
encodable.decoder(),
|
|
|
|
|
)
|
|
|
|
|
use edited <- decode.subfield(["detail", "edited"], encodable.decoder())
|
|
|
|
|
decode.success(ChildEmittedEditEndEvent(original:, edited:))
|
|
|
|
|
}),
|
2025-07-22 00:21:54 -07:00
|
|
|
],
|
|
|
|
|
[component.default_slot([], [])],
|
|
|
|
|
)
|
|
|
|
|
}
|