editing with the hoverbar works pretty smoothly
This commit is contained in:
parent
316a3d8414
commit
aa8bf34642
15 changed files with 867 additions and 174 deletions
|
|
@ -178,11 +178,7 @@ pub async fn lens_page(
|
||||||
let pkey_attrs = fetch_primary_keys_for_rel(lens.class_oid, &mut *client).await?;
|
let pkey_attrs = fetch_primary_keys_for_rel(lens.class_oid, &mut *client).await?;
|
||||||
|
|
||||||
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||||||
struct Row {
|
let rows: Vec<PgRow> = query(&format!(
|
||||||
pkeys: HashMap<String, Encodable>,
|
|
||||||
data: PgRow,
|
|
||||||
}
|
|
||||||
let rows: Vec<Row> = query(&format!(
|
|
||||||
"select {0} from {1}.{2} limit $1",
|
"select {0} from {1}.{2} limit $1",
|
||||||
pkey_attrs
|
pkey_attrs
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -195,18 +191,16 @@ pub async fn lens_page(
|
||||||
))
|
))
|
||||||
.bind(FRONTEND_ROW_LIMIT)
|
.bind(FRONTEND_ROW_LIMIT)
|
||||||
.fetch_all(&mut *client)
|
.fetch_all(&mut *client)
|
||||||
.await?
|
.await?;
|
||||||
.into_iter()
|
let pkeys: Vec<HashMap<String, Encodable>> = rows
|
||||||
|
.iter()
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
let mut pkey_values: HashMap<String, Encodable> = HashMap::new();
|
let mut pkey_values: HashMap<String, Encodable> = HashMap::new();
|
||||||
for attr in pkey_attrs.clone() {
|
for attr in pkey_attrs.clone() {
|
||||||
let field = Field::default_from_attr(&attr);
|
let field = Field::default_from_attr(&attr);
|
||||||
pkey_values.insert(field.name.clone(), field.get_value_encodable(&row).unwrap());
|
pkey_values.insert(field.name.clone(), field.get_value_encodable(row).unwrap());
|
||||||
}
|
|
||||||
Row {
|
|
||||||
pkeys: pkey_values,
|
|
||||||
data: row,
|
|
||||||
}
|
}
|
||||||
|
pkey_values
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -215,13 +209,15 @@ pub async fn lens_page(
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
fields: Vec<Field>,
|
fields: Vec<Field>,
|
||||||
all_columns: Vec<PgAttribute>,
|
all_columns: Vec<PgAttribute>,
|
||||||
rows: Vec<Row>,
|
rows: Vec<PgRow>,
|
||||||
|
pkeys: Vec<HashMap<String, Encodable>>,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
}
|
}
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
all_columns: attrs,
|
all_columns: attrs,
|
||||||
fields,
|
fields,
|
||||||
|
pkeys,
|
||||||
rows,
|
rows,
|
||||||
settings,
|
settings,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<link rel="stylesheet" href="{{ settings.root_path }}/css_dist/viewer.css">
|
<link rel="stylesheet" href="{{ settings.root_path }}/css_dist/viewer.css">
|
||||||
<viewer-controller root-path="{{ settings.root_path }}" n-rows="{{ rows.len() }}" n-columns="{{ fields.len() }}">
|
<viewer-controller root-path="{{ settings.root_path }}" pkeys="{{ pkeys | json }}" fields="{{ fields | json }}">
|
||||||
<table class="viewer">
|
<table class="viewer">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -19,11 +19,11 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for (i, row) in rows.iter().enumerate() %}
|
{% for (i, row) in rows.iter().enumerate() %}
|
||||||
{# TODO: store primary keys in a Vec separate from rows #}
|
{# TODO: store primary keys in a Vec separate from rows #}
|
||||||
<tr data-pkey="{{ row.pkeys | json }}">
|
<tr>
|
||||||
{% for (j, field) in fields.iter().enumerate() %}
|
{% for (j, field) in fields.iter().enumerate() %}
|
||||||
{# Setting max-width is required for overflow to work properly. #}
|
{# Setting max-width is required for overflow to work properly. #}
|
||||||
<td style="width: {{ field.width_px }}px; max-width: {{ field.width_px }}px;">
|
<td style="width: {{ field.width_px }}px; max-width: {{ field.width_px }}px;">
|
||||||
{% match field.get_value_encodable(row.data) %}
|
{% match field.get_value_encodable(row) %}
|
||||||
{% when Ok with (encodable) %}
|
{% when Ok with (encodable) %}
|
||||||
<{{ field.webc_tag() | safe }}
|
<{{ field.webc_tag() | safe }}
|
||||||
{% for (k, v) in field.webc_custom_attrs() %}
|
{% for (k, v) in field.webc_custom_attrs() %}
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
row="{{ i }}"
|
row="{{ i }}"
|
||||||
column="{{ j }}"
|
column="{{ j }}"
|
||||||
value="{{ encodable.inner_as_value() | json }}"
|
value="{{ encodable | json }}"
|
||||||
class="cell"
|
class="cell"
|
||||||
>
|
>
|
||||||
{{ encodable.inner_as_value() | json }}
|
{{ encodable.inner_as_value() | json }}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,35 @@
|
||||||
@use 'globals';
|
@use 'globals';
|
||||||
@use 'hoverbar';
|
@use 'hoverbar';
|
||||||
|
|
||||||
.control-bar__input {
|
.text-editor {
|
||||||
@include globals.reset-input;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
@include globals.reset-input;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-family: globals.$font-family-data;
|
font-family: globals.$font-family-data;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkbox {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ gleam_json = ">= 3.0.2 and < 4.0.0"
|
||||||
gleam_regexp = ">= 1.1.1 and < 2.0.0"
|
gleam_regexp = ">= 1.1.1 and < 2.0.0"
|
||||||
plinth = ">= 0.7.1 and < 1.0.0"
|
plinth = ">= 0.7.1 and < 1.0.0"
|
||||||
gleam_javascript = ">= 1.0.0 and < 2.0.0"
|
gleam_javascript = ">= 1.0.0 and < 2.0.0"
|
||||||
|
rsvp = ">= 1.1.2 and < 2.0.0"
|
||||||
|
gleam_http = ">= 4.1.1 and < 5.0.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gleeunit = ">= 1.0.0 and < 2.0.0"
|
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ packages = [
|
||||||
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
|
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
|
||||||
{ name = "gleam_deque", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_deque", source = "hex", outer_checksum = "64D77068931338CF0D0CB5D37522C3E3CCA7CB7D6C5BACB41648B519CC0133C7" },
|
{ name = "gleam_deque", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_deque", source = "hex", outer_checksum = "64D77068931338CF0D0CB5D37522C3E3CCA7CB7D6C5BACB41648B519CC0133C7" },
|
||||||
{ name = "gleam_erlang", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F91CE62A2D011FA13341F3723DB7DB118541AAA5FE7311BD2716D018F01EF9E3" },
|
{ name = "gleam_erlang", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F91CE62A2D011FA13341F3723DB7DB118541AAA5FE7311BD2716D018F01EF9E3" },
|
||||||
{ name = "gleam_http", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "DB25DFC8530B64B77105405B80686541A0D96F7E2D83D807D6B2155FB9A8B1B8" },
|
{ name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" },
|
||||||
|
{ name = "gleam_http", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "DD0271B32C356FB684EC7E9F48B1E835D0480168848581F68983C0CC371405D4" },
|
||||||
{ name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" },
|
{ name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" },
|
||||||
{ name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" },
|
{ name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" },
|
||||||
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
|
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
|
||||||
|
|
@ -36,6 +37,7 @@ packages = [
|
||||||
{ name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
|
{ name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
|
||||||
{ name = "plinth", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "63BB36AACCCCB82FBE46A862CF85CB88EBE4EF280ECDBAC4B6CB042340B9E1D8" },
|
{ name = "plinth", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "63BB36AACCCCB82FBE46A862CF85CB88EBE4EF280ECDBAC4B6CB042340B9E1D8" },
|
||||||
{ name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" },
|
{ name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" },
|
||||||
|
{ name = "rsvp", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "0C0732577712E7CB0E55F057637E62CD36F35306A5E830DC4874B83DA8CE4638" },
|
||||||
{ name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
|
{ name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
|
||||||
{ name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" },
|
{ name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" },
|
||||||
{ name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
|
{ name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
|
||||||
|
|
@ -45,6 +47,7 @@ packages = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[requirements]
|
[requirements]
|
||||||
|
gleam_http = { version = ">= 4.1.1 and < 5.0.0" }
|
||||||
gleam_javascript = { version = ">= 1.0.0 and < 2.0.0" }
|
gleam_javascript = { version = ">= 1.0.0 and < 2.0.0" }
|
||||||
gleam_json = { version = ">= 3.0.2 and < 4.0.0" }
|
gleam_json = { version = ">= 3.0.2 and < 4.0.0" }
|
||||||
gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" }
|
gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" }
|
||||||
|
|
@ -53,3 +56,4 @@ gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||||
lustre = { version = ">= 5.2.1 and < 6.0.0" }
|
lustre = { version = ">= 5.2.1 and < 6.0.0" }
|
||||||
lustre_dev_tools = { version = ">= 1.9.0 and < 2.0.0" }
|
lustre_dev_tools = { version = ">= 1.9.0 and < 2.0.0" }
|
||||||
plinth = { version = ">= 0.7.1 and < 1.0.0" }
|
plinth = { version = ">= 0.7.1 and < 1.0.0" }
|
||||||
|
rsvp = { version = ">= 1.1.2 and < 2.0.0" }
|
||||||
|
|
|
||||||
|
|
@ -27,40 +27,25 @@ pub fn options_common(
|
||||||
component.on_attribute_change("selected", fn(value) {
|
component.on_attribute_change("selected", fn(value) {
|
||||||
ParentChangedSelected(value != "") |> CommonMsg |> map_msg |> Ok
|
ParentChangedSelected(value != "") |> CommonMsg |> map_msg |> Ok
|
||||||
}),
|
}),
|
||||||
component.on_attribute_change("editing", fn(value) {
|
|
||||||
ParentChangedEditing(value != "") |> CommonMsg |> map_msg |> Ok
|
|
||||||
}),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ModelCommon {
|
pub type ModelCommon {
|
||||||
ModelCommon(
|
ModelCommon(root_path: String, row: Int, column: Int, selected: Bool)
|
||||||
root_path: String,
|
|
||||||
row: Int,
|
|
||||||
column: Int,
|
|
||||||
selected: Bool,
|
|
||||||
editing: Bool,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Msg {
|
pub type Msg {
|
||||||
AncestorChangedRootPath(String)
|
AncestorChangedRootPath(String)
|
||||||
ParentChangedEditing(Bool)
|
|
||||||
ParentChangedRow(Int)
|
ParentChangedRow(Int)
|
||||||
ParentChangedColumn(Int)
|
ParentChangedColumn(Int)
|
||||||
ParentChangedSelected(Bool)
|
ParentChangedSelected(Bool)
|
||||||
UserClickedCell
|
UserClickedCell
|
||||||
|
UserDoubleClickedCell
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(_) -> #(ModelCommon, Effect(CommonMsg)) {
|
pub fn init(_) -> #(ModelCommon, Effect(CommonMsg)) {
|
||||||
#(
|
#(
|
||||||
ModelCommon(
|
ModelCommon(root_path: "", selected: False, row: -1, column: -1),
|
||||||
root_path: "",
|
|
||||||
selected: False,
|
|
||||||
editing: False,
|
|
||||||
row: -1,
|
|
||||||
column: -1,
|
|
||||||
),
|
|
||||||
context.request_context(
|
context.request_context(
|
||||||
context: dynamic.string("root_path"),
|
context: dynamic.string("root_path"),
|
||||||
subscribe: False,
|
subscribe: False,
|
||||||
|
|
@ -90,10 +75,6 @@ pub fn update(
|
||||||
ModelCommon(..model, column:),
|
ModelCommon(..model, column:),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
ParentChangedEditing(editing) -> #(
|
|
||||||
ModelCommon(..model, editing:),
|
|
||||||
effect.none(),
|
|
||||||
)
|
|
||||||
ParentChangedSelected(selected) -> #(
|
ParentChangedSelected(selected) -> #(
|
||||||
ModelCommon(..model, selected:),
|
ModelCommon(..model, selected:),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
|
|
@ -108,6 +89,16 @@ pub fn update(
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
UserDoubleClickedCell -> #(
|
||||||
|
model,
|
||||||
|
event.emit(
|
||||||
|
"cell-dblclick",
|
||||||
|
json.object([
|
||||||
|
#("row", json.int(model.row)),
|
||||||
|
#("column", json.int(model.column)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,7 +119,11 @@ pub fn view(
|
||||||
True -> attr.class("cell__container--selected")
|
True -> attr.class("cell__container--selected")
|
||||||
False -> attr.none()
|
False -> attr.none()
|
||||||
},
|
},
|
||||||
event.on_click(CommonMsg(UserClickedCell) |> map_msg),
|
event.on_click(CommonMsg(UserClickedCell) |> map_msg()),
|
||||||
|
event.on(
|
||||||
|
"dblclick",
|
||||||
|
CommonMsg(UserDoubleClickedCell) |> map_msg() |> decode.success(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[inner()],
|
[inner()],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import gleam/dynamic/decode
|
import gleam/dynamic/decode
|
||||||
import gleam/json
|
import gleam/json
|
||||||
import gleam/option.{type Option}
|
import gleam/option.{type Option, None}
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import lustre.{type App}
|
import lustre.{type App}
|
||||||
import lustre/attribute as attr
|
import lustre/attribute as attr
|
||||||
|
|
@ -10,13 +10,24 @@ import lustre/element.{type Element}
|
||||||
import lustre/element/html
|
import lustre/element/html
|
||||||
|
|
||||||
import cell_common.{type CommonMsg, type ModelCommon}
|
import cell_common.{type CommonMsg, type ModelCommon}
|
||||||
|
import encodable
|
||||||
|
|
||||||
pub const name: String = "cell-text"
|
pub const name: String = "cell-text"
|
||||||
|
|
||||||
pub fn component() -> App(Nil, Model, Msg) {
|
pub fn component() -> App(Nil, Model, Msg) {
|
||||||
lustre.component(init, update, view, [
|
lustre.component(init, update, view, [
|
||||||
component.on_attribute_change("value", fn(value) {
|
component.on_attribute_change("value", fn(value) {
|
||||||
ParentChangedValue(value) |> Ok
|
json.parse(
|
||||||
|
value,
|
||||||
|
encodable.decoder()
|
||||||
|
|> decode.map(fn(encodable) {
|
||||||
|
case encodable {
|
||||||
|
encodable.Text(content) -> ParentChangedValue(content)
|
||||||
|
_ -> ParentChangedValue(None)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|> result.map_error(fn(_) { Nil })
|
||||||
}),
|
}),
|
||||||
..cell_common.options_common(Common)
|
..cell_common.options_common(Common)
|
||||||
])
|
])
|
||||||
|
|
@ -37,7 +48,7 @@ fn init(_) -> #(Model, Effect(Msg)) {
|
||||||
|
|
||||||
pub type Msg {
|
pub type Msg {
|
||||||
Common(CommonMsg)
|
Common(CommonMsg)
|
||||||
ParentChangedValue(String)
|
ParentChangedValue(Option(String))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
|
|
@ -46,14 +57,7 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
let #(common, effect) = cell_common.update(model.common, sub_msg)
|
let #(common, effect) = cell_common.update(model.common, sub_msg)
|
||||||
#(Model(..model, common:), effect)
|
#(Model(..model, common:), effect)
|
||||||
}
|
}
|
||||||
ParentChangedValue(value) -> #(
|
ParentChangedValue(value) -> #(Model(..model, value:), effect.none())
|
||||||
Model(
|
|
||||||
..model,
|
|
||||||
value: json.parse(value, decode.optional(decode.string))
|
|
||||||
|> result.unwrap(option.None),
|
|
||||||
),
|
|
||||||
effect.none(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import gleam/dynamic/decode
|
import gleam/dynamic/decode
|
||||||
import gleam/json
|
import gleam/json
|
||||||
import gleam/option.{type Option}
|
import gleam/option.{type Option, None}
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import lustre.{type App}
|
import lustre.{type App}
|
||||||
import lustre/attribute as attr
|
import lustre/attribute as attr
|
||||||
|
|
@ -10,13 +10,24 @@ import lustre/element.{type Element}
|
||||||
import lustre/element/html
|
import lustre/element/html
|
||||||
|
|
||||||
import cell_common.{type CommonMsg, type ModelCommon}
|
import cell_common.{type CommonMsg, type ModelCommon}
|
||||||
|
import encodable
|
||||||
|
|
||||||
pub const name: String = "cell-uuid"
|
pub const name: String = "cell-uuid"
|
||||||
|
|
||||||
pub fn component() -> App(Nil, Model, Msg) {
|
pub fn component() -> App(Nil, Model, Msg) {
|
||||||
lustre.component(init, update, view, [
|
lustre.component(init, update, view, [
|
||||||
component.on_attribute_change("value", fn(value) {
|
component.on_attribute_change("value", fn(value) {
|
||||||
ParentChangedValue(value) |> Ok
|
json.parse(
|
||||||
|
value,
|
||||||
|
encodable.decoder()
|
||||||
|
|> decode.map(fn(encodable) {
|
||||||
|
case encodable {
|
||||||
|
encodable.Uuid(content) -> ParentChangedValue(content)
|
||||||
|
_ -> ParentChangedValue(None)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|> result.map_error(fn(_) { Nil })
|
||||||
}),
|
}),
|
||||||
..cell_common.options_common(Common)
|
..cell_common.options_common(Common)
|
||||||
])
|
])
|
||||||
|
|
@ -37,7 +48,7 @@ fn init(_) -> #(Model, Effect(Msg)) {
|
||||||
|
|
||||||
pub type Msg {
|
pub type Msg {
|
||||||
Common(CommonMsg)
|
Common(CommonMsg)
|
||||||
ParentChangedValue(String)
|
ParentChangedValue(Option(String))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
|
|
@ -46,14 +57,7 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
let #(common, effect) = cell_common.update(model.common, sub_msg)
|
let #(common, effect) = cell_common.update(model.common, sub_msg)
|
||||||
#(Model(..model, common:), effect)
|
#(Model(..model, common:), effect)
|
||||||
}
|
}
|
||||||
ParentChangedValue(value) -> #(
|
ParentChangedValue(value) -> #(Model(..model, value:), effect.none())
|
||||||
Model(
|
|
||||||
..model,
|
|
||||||
value: json.parse(value, decode.optional(decode.string))
|
|
||||||
|> result.unwrap(option.None),
|
|
||||||
),
|
|
||||||
effect.none(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
57
webc/src/encodable.gleam
Normal file
57
webc/src/encodable.gleam
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import gleam/dynamic/decode.{type Decoder}
|
||||||
|
import gleam/json.{type Json}
|
||||||
|
import gleam/option.{type Option, None, Some}
|
||||||
|
|
||||||
|
pub type Encodable {
|
||||||
|
Integer(Option(Int))
|
||||||
|
Text(Option(String))
|
||||||
|
Timestamptz(Option(String))
|
||||||
|
Uuid(Option(String))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decoder() -> Decoder(Encodable) {
|
||||||
|
use t <- decode.field("t", decode.string)
|
||||||
|
case t {
|
||||||
|
"Integer" -> {
|
||||||
|
use c <- decode.field("c", decode.optional(decode.int))
|
||||||
|
decode.success(Integer(c))
|
||||||
|
}
|
||||||
|
"Text" -> {
|
||||||
|
use c <- decode.field("c", decode.optional(decode.string))
|
||||||
|
decode.success(Text(c))
|
||||||
|
}
|
||||||
|
"Timestamptz" -> {
|
||||||
|
use c <- decode.field("c", decode.optional(decode.string))
|
||||||
|
decode.success(Timestamptz(c))
|
||||||
|
}
|
||||||
|
"Uuid" -> {
|
||||||
|
use c <- decode.field("c", decode.optional(decode.string))
|
||||||
|
decode.success(Uuid(c))
|
||||||
|
}
|
||||||
|
_ -> decode.failure(Text(Some("")), "Encodable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json(value: Encodable) -> Json {
|
||||||
|
json.object([
|
||||||
|
#(
|
||||||
|
"t",
|
||||||
|
json.string(case value {
|
||||||
|
Integer(_) -> "Integer"
|
||||||
|
Text(_) -> "Text"
|
||||||
|
Timestamptz(_) -> "Timestamptz"
|
||||||
|
Uuid(_) -> "Uuid"
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
#("c", case value {
|
||||||
|
Integer(Some(inner)) -> json.int(inner)
|
||||||
|
Integer(None) -> json.null()
|
||||||
|
Text(Some(inner)) -> json.string(inner)
|
||||||
|
Text(None) -> json.null()
|
||||||
|
Timestamptz(Some(inner)) -> json.string(inner)
|
||||||
|
Timestamptz(None) -> json.null()
|
||||||
|
Uuid(Some(inner)) -> json.string(inner)
|
||||||
|
Uuid(None) -> json.null()
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
23
webc/src/field.gleam
Normal file
23
webc/src/field.gleam
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import gleam/dynamic/decode
|
||||||
|
import gleam/option.{type Option}
|
||||||
|
|
||||||
|
import field_type.{type FieldType}
|
||||||
|
|
||||||
|
pub type Field {
|
||||||
|
Field(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
label: Option(String),
|
||||||
|
field_type: FieldType,
|
||||||
|
width_px: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decoder() -> decode.Decoder(Field) {
|
||||||
|
use id <- decode.field("id", decode.string)
|
||||||
|
use name <- decode.field("name", decode.string)
|
||||||
|
use label <- decode.field("label", decode.optional(decode.string))
|
||||||
|
use field_type <- decode.field("field_type", field_type.decoder())
|
||||||
|
use width_px <- decode.field("width_px", decode.int)
|
||||||
|
decode.success(Field(id:, name:, label:, field_type:, width_px:))
|
||||||
|
}
|
||||||
42
webc/src/field_type.gleam
Normal file
42
webc/src/field_type.gleam
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import gleam/dynamic/decode
|
||||||
|
import gleam/json.{type Json}
|
||||||
|
|
||||||
|
pub type FieldType {
|
||||||
|
Integer
|
||||||
|
InterimUser
|
||||||
|
Text
|
||||||
|
Timestamp(format: String)
|
||||||
|
Uuid
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json(field_type: FieldType) -> Json {
|
||||||
|
case field_type {
|
||||||
|
Integer -> json.object([#("t", json.string("Integer"))])
|
||||||
|
InterimUser -> json.object([#("t", json.string("Interim_user"))])
|
||||||
|
Text -> json.object([#("t", json.string("Text"))])
|
||||||
|
Timestamp(format:) ->
|
||||||
|
json.object([
|
||||||
|
#("t", json.string("Timestamp")),
|
||||||
|
#("c", json.object([#("format", json.string(format))])),
|
||||||
|
])
|
||||||
|
Uuid -> json.object([#("t", json.string("Uuid"))])
|
||||||
|
Unknown -> json.object([#("t", json.string("Unknown"))])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decoder() -> decode.Decoder(FieldType) {
|
||||||
|
use variant <- decode.field("t", decode.string)
|
||||||
|
case variant {
|
||||||
|
"Integer" -> decode.success(Integer)
|
||||||
|
"InterimUser" -> decode.success(InterimUser)
|
||||||
|
"Text" -> decode.success(Text)
|
||||||
|
"Timestamp" -> {
|
||||||
|
use format <- decode.subfield(["c", "format"], decode.string)
|
||||||
|
decode.success(Timestamp(format:))
|
||||||
|
}
|
||||||
|
"Uuid" -> decode.success(Uuid)
|
||||||
|
"Unknown" -> decode.success(Unknown)
|
||||||
|
_ -> decode.failure(Unknown, "FieldType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
export function focusHoverbar() {
|
||||||
|
const hoverbar = document.querySelector("viewer-hoverbar");
|
||||||
|
hoverbar?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function blurHoverbar() {
|
||||||
|
const hoverbar = document.querySelector("viewer-hoverbar");
|
||||||
|
hoverbar?.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function overwriteInHoverbar(value) {
|
||||||
|
const hoverbar = document.querySelector("viewer-hoverbar");
|
||||||
|
hoverbar?.overwrite(value);
|
||||||
|
}
|
||||||
|
|
||||||
export function clearSelectedAttrs() {
|
export function clearSelectedAttrs() {
|
||||||
document.querySelectorAll(
|
document.querySelectorAll(
|
||||||
"table.viewer > tbody > tr > td > [selected='true']",
|
"table.viewer > tbody > tr > td > [selected='true']",
|
||||||
|
|
@ -5,12 +20,29 @@ export function clearSelectedAttrs() {
|
||||||
.forEach((element) => element.setAttribute("selected", ""));
|
.forEach((element) => element.setAttribute("selected", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSelectedAttr(row, column) {
|
export function setCellAttr(row, column, key, value) {
|
||||||
const tr = document.querySelectorAll("table.viewer > tbody > tr")[row];
|
const cell = queryCell(row, column);
|
||||||
if (tr) {
|
|
||||||
const cell = [...tr.querySelectorAll(":scope > td > *")][column];
|
|
||||||
if (cell) {
|
if (cell) {
|
||||||
cell.setAttribute("selected", "true");
|
cell.setAttribute(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncCellValueToHoverbar(row, column, fieldType) {
|
||||||
|
const cell = queryCell(row, column);
|
||||||
|
if (cell) {
|
||||||
|
const value = cell.getAttribute("value") ?? "null";
|
||||||
|
const hoverbar = document.querySelector("viewer-hoverbar");
|
||||||
|
if (hoverbar) {
|
||||||
|
hoverbar.setAttribute("value", value);
|
||||||
|
hoverbar.setAttribute("field-type", fieldType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function queryCell(row, column) {
|
||||||
|
const tr = document.querySelectorAll("table.viewer > tbody > tr")[row];
|
||||||
|
if (tr) {
|
||||||
|
return [...tr.querySelectorAll(":scope > td > *")][column];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
|
import gleam/dict.{type Dict}
|
||||||
import gleam/dynamic.{type Dynamic}
|
import gleam/dynamic.{type Dynamic}
|
||||||
import gleam/dynamic/decode
|
import gleam/dynamic/decode
|
||||||
import gleam/int
|
import gleam/http/response.{type Response}
|
||||||
import gleam/io
|
import gleam/io
|
||||||
|
import gleam/json
|
||||||
import gleam/list
|
import gleam/list
|
||||||
import gleam/option.{type Option, Some}
|
import gleam/option.{None, Some}
|
||||||
|
import gleam/pair
|
||||||
|
import gleam/regexp
|
||||||
import gleam/result
|
import gleam/result
|
||||||
|
import gleam/set.{type Set}
|
||||||
import lustre.{type App}
|
import lustre.{type App}
|
||||||
import lustre/component
|
import lustre/component
|
||||||
import lustre/effect.{type Effect}
|
import lustre/effect.{type Effect}
|
||||||
|
|
@ -13,8 +18,12 @@ import lustre/element/html
|
||||||
import lustre/event
|
import lustre/event
|
||||||
import plinth/browser/document
|
import plinth/browser/document
|
||||||
import plinth/browser/event as plinth_event
|
import plinth/browser/event as plinth_event
|
||||||
|
import rsvp
|
||||||
|
|
||||||
import context
|
import context
|
||||||
|
import encodable.{type Encodable}
|
||||||
|
import field.{type Field}
|
||||||
|
import field_type.{type FieldType}
|
||||||
|
|
||||||
pub const name: String = "viewer-controller"
|
pub const name: String = "viewer-controller"
|
||||||
|
|
||||||
|
|
@ -23,11 +32,27 @@ pub fn component() -> App(Nil, Model, Msg) {
|
||||||
component.on_attribute_change("root-path", fn(value) {
|
component.on_attribute_change("root-path", fn(value) {
|
||||||
ParentChangedRootPath(value) |> Ok
|
ParentChangedRootPath(value) |> Ok
|
||||||
}),
|
}),
|
||||||
component.on_attribute_change("n-rows", fn(value) {
|
component.on_attribute_change("pkeys", fn(value) {
|
||||||
int.parse(value) |> result.map(ParentChangedNRows)
|
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)
|
||||||
}),
|
}),
|
||||||
component.on_attribute_change("n-columns", fn(value) {
|
component.on_attribute_change("fields", fn(value) {
|
||||||
int.parse(value) |> result.map(ParentChangedNColumns)
|
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)
|
||||||
}),
|
}),
|
||||||
component.on_attribute_change("root-path", fn(value) {
|
component.on_attribute_change("root-path", fn(value) {
|
||||||
ParentChangedRootPath(value) |> Ok
|
ParentChangedRootPath(value) |> Ok
|
||||||
|
|
@ -35,15 +60,19 @@ pub fn component() -> App(Nil, Model, Msg) {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------- Model -------- //
|
||||||
|
|
||||||
pub type Model {
|
pub type Model {
|
||||||
Model(
|
Model(
|
||||||
root_path: String,
|
root_path: String,
|
||||||
root_path_consumers: List(Dynamic),
|
root_path_consumers: List(Dynamic),
|
||||||
selected_row: Option(Int),
|
selections: List(#(Int, Int)),
|
||||||
selected_column: Option(Int),
|
|
||||||
editing: Bool,
|
editing: Bool,
|
||||||
n_rows: Int,
|
pkeys: Dict(Int, Dict(String, Encodable)),
|
||||||
n_columns: Int,
|
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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,11 +81,11 @@ fn init(_) -> #(Model, Effect(Msg)) {
|
||||||
Model(
|
Model(
|
||||||
root_path: "",
|
root_path: "",
|
||||||
root_path_consumers: [],
|
root_path_consumers: [],
|
||||||
selected_row: option.None,
|
selections: [],
|
||||||
selected_column: option.None,
|
|
||||||
editing: False,
|
editing: False,
|
||||||
n_rows: -1,
|
pkeys: dict.new(),
|
||||||
n_columns: -1,
|
fields: dict.new(),
|
||||||
|
modifiers_held: set.new(),
|
||||||
),
|
),
|
||||||
effect.before_paint(fn(dispatch, _) -> Nil {
|
effect.before_paint(fn(dispatch, _) -> Nil {
|
||||||
document.add_event_listener("keydown", fn(ev) -> Nil {
|
document.add_event_listener("keydown", fn(ev) -> Nil {
|
||||||
|
|
@ -64,6 +93,19 @@ fn init(_) -> #(Model, Effect(Msg)) {
|
||||||
case key {
|
case key {
|
||||||
"ArrowLeft" | "ArrowRight" | "ArrowUp" | "ArrowDown" ->
|
"ArrowLeft" | "ArrowRight" | "ArrowUp" | "ArrowDown" ->
|
||||||
dispatch(UserPressedDirectionalKey(key))
|
dispatch(UserPressedDirectionalKey(key))
|
||||||
|
"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))
|
||||||
_ -> Nil
|
_ -> Nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -71,13 +113,30 @@ fn init(_) -> #(Model, Effect(Msg)) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_typing_key(key: String) -> Bool {
|
||||||
|
let assert Ok(re) =
|
||||||
|
regexp.from_string("^[a-zA-Z0-9!@#$%^&*._/?<>{}[\\]'\"~`-]$")
|
||||||
|
regexp.check(key, with: re)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Update -------- //
|
||||||
|
|
||||||
pub type Msg {
|
pub type Msg {
|
||||||
ParentChangedRootPath(String)
|
ParentChangedRootPath(String)
|
||||||
ParentChangedNRows(Int)
|
ParentChangedPkeys(Dict(Int, Dict(String, Encodable)))
|
||||||
ParentChangedNColumns(Int)
|
ParentChangedFields(Dict(Int, Field))
|
||||||
ChildRequestedRootPath(Dynamic, Bool)
|
ChildEmittedEditStartEvent
|
||||||
|
ChildEmittedEditUpdateEvent(original: Encodable, edited: Encodable)
|
||||||
|
ChildEmittedEditEndEvent(original: Encodable, edited: Encodable)
|
||||||
|
ChildRequestedRootPath(callback: Dynamic, subscribe: Bool)
|
||||||
|
ServerUpdateValuePost(Result(Response(String), rsvp.Error))
|
||||||
UserClickedCell(Int, Int)
|
UserClickedCell(Int, Int)
|
||||||
|
UserDoubleClickedCell(Int, Int)
|
||||||
UserPressedDirectionalKey(String)
|
UserPressedDirectionalKey(String)
|
||||||
|
UserPressedEnterKey
|
||||||
|
UserPressedTypingKey(String)
|
||||||
|
UserPressedModifierKey(String)
|
||||||
|
UserReleasedModifierKey(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
|
|
@ -89,11 +148,22 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
dynamic.string(root_path),
|
dynamic.string(root_path),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ParentChangedNRows(n_rows) -> #(Model(..model, n_rows:), effect.none())
|
ParentChangedPkeys(pkeys) -> #(Model(..model, pkeys:), effect.none())
|
||||||
ParentChangedNColumns(n_columns) -> #(
|
ParentChangedFields(fields) -> #(Model(..model, fields:), effect.none())
|
||||||
Model(..model, n_columns:),
|
ChildEmittedEditStartEvent -> #(
|
||||||
|
Model(..model, editing: True),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
|
ChildEmittedEditUpdateEvent(_original, edited) -> {
|
||||||
|
case model.selections {
|
||||||
|
[coords] -> #(model, set_cell_value(coords, edited))
|
||||||
|
_ -> todo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChildEmittedEditEndEvent(_original, edited) -> #(
|
||||||
|
model,
|
||||||
|
commit_change(model:, value: edited),
|
||||||
|
)
|
||||||
ChildRequestedRootPath(callback, subscribe) -> #(
|
ChildRequestedRootPath(callback, subscribe) -> #(
|
||||||
case subscribe {
|
case subscribe {
|
||||||
True ->
|
True ->
|
||||||
|
|
@ -105,21 +175,32 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
},
|
},
|
||||||
context.update_consumers([callback], dynamic.string(model.root_path)),
|
context.update_consumers([callback], dynamic.string(model.root_path)),
|
||||||
)
|
)
|
||||||
UserClickedCell(row, column) -> #(
|
ServerUpdateValuePost(response) -> {
|
||||||
Model(
|
case response {
|
||||||
..model,
|
Ok(_) -> #(Model(..model, editing: False), blur_hoverbar())
|
||||||
selected_row: option.Some(row),
|
Error(rsvp.HttpError(response)) -> {
|
||||||
selected_column: option.Some(column),
|
io.println_error("HTTP error while updating value: " <> response.body)
|
||||||
),
|
#(model, effect.none())
|
||||||
move_selection(to_row: option.Some(row), to_column: Some(column)),
|
}
|
||||||
)
|
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()
|
||||||
|
})
|
||||||
UserPressedDirectionalKey(key) -> {
|
UserPressedDirectionalKey(key) -> {
|
||||||
case model.editing, model.selected_row, model.selected_column {
|
case model.editing, model.selections {
|
||||||
False, Some(selected_row), Some(selected_column) -> {
|
False, [#(selected_row, selected_col)] -> {
|
||||||
let first_row_selected = selected_row == 0
|
let first_row_selected = selected_row == 0
|
||||||
let last_row_selected = selected_row == model.n_rows - 1
|
let last_row_selected = selected_row == dict.size(model.pkeys) - 1
|
||||||
let first_col_selected = selected_column == 0
|
let first_col_selected = selected_col == 0
|
||||||
let last_col_selected = selected_column == model.n_columns - 1
|
let last_col_selected = selected_col == dict.size(model.fields) - 1
|
||||||
case
|
case
|
||||||
key,
|
key,
|
||||||
first_row_selected,
|
first_row_selected,
|
||||||
|
|
@ -127,65 +208,203 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
first_col_selected,
|
first_col_selected,
|
||||||
last_col_selected
|
last_col_selected
|
||||||
{
|
{
|
||||||
"ArrowLeft", _, _, False, _ -> #(
|
"ArrowLeft", _, _, False, _ ->
|
||||||
Model(..model, selected_column: option.Some(selected_column - 1)),
|
assign_selection(model:, to: #(selected_row, selected_col - 1))
|
||||||
move_selection(
|
"ArrowRight", _, _, _, False ->
|
||||||
to_row: model.selected_row,
|
assign_selection(model:, to: #(selected_row, selected_col + 1))
|
||||||
to_column: Some(selected_column - 1),
|
"ArrowUp", False, _, _, _ ->
|
||||||
),
|
assign_selection(model:, to: #(selected_row - 1, selected_col))
|
||||||
)
|
"ArrowDown", _, False, _, _ ->
|
||||||
"ArrowRight", _, _, _, False -> #(
|
assign_selection(model:, to: #(selected_row + 1, selected_col))
|
||||||
Model(..model, selected_column: option.Some(selected_column + 1)),
|
|
||||||
move_selection(
|
|
||||||
to_row: model.selected_row,
|
|
||||||
to_column: Some(selected_column + 1),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
"ArrowUp", False, _, _, _ -> #(
|
|
||||||
Model(..model, selected_row: option.Some(selected_row - 1)),
|
|
||||||
move_selection(
|
|
||||||
to_row: Some(selected_row - 1),
|
|
||||||
to_column: model.selected_column,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
"ArrowDown", _, False, _, _ -> #(
|
|
||||||
Model(..model, selected_row: option.Some(selected_row + 1)),
|
|
||||||
move_selection(
|
|
||||||
to_row: Some(selected_row + 1),
|
|
||||||
to_column: model.selected_column,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
_, _, _, _, _ -> #(model, effect.none())
|
_, _, _, _, _ -> #(model, effect.none())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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())
|
_, _, _ -> #(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 move_selection(
|
fn overwrite_in_hoverbar(value value: String) -> Effect(Msg) {
|
||||||
to_row to_row: Option(Int),
|
use _, _ <- effect.before_paint()
|
||||||
to_column to_column: Option(Int),
|
do_overwrite_in_hoverbar(value)
|
||||||
) -> Effect(msg) {
|
}
|
||||||
use _dispatch, _root <- effect.before_paint()
|
|
||||||
do_clear_selected_attrs()
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "overwriteInHoverbar")
|
||||||
case to_row, to_column {
|
fn do_overwrite_in_hoverbar(value _value: String) -> Nil {
|
||||||
option.Some(row), option.Some(column) -> do_set_selected_attr(row:, column:)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
use _dispatch, _root <- effect.before_paint()
|
||||||
|
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()
|
||||||
|
do_clear_selected_attrs()
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@external(javascript, "./viewer_controller_component.ffi.mjs", "clearSelectedAttrs")
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "clearSelectedAttrs")
|
||||||
fn do_clear_selected_attrs() -> Nil {
|
fn do_clear_selected_attrs() -> Nil {
|
||||||
Nil
|
Nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@external(javascript, "./viewer_controller_component.ffi.mjs", "setSelectedAttr")
|
@external(javascript, "./viewer_controller_component.ffi.mjs", "setCellAttr")
|
||||||
fn do_set_selected_attr(row _row: Int, column _column: Int) -> Nil {
|
fn do_set_cell_attr(
|
||||||
|
row _row: Int,
|
||||||
|
column _column: Int,
|
||||||
|
key _key: String,
|
||||||
|
value _value: String,
|
||||||
|
) -> Nil {
|
||||||
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 {
|
||||||
|
Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- View -------- //
|
||||||
|
|
||||||
fn view(_: Model) -> Element(Msg) {
|
fn view(_: Model) -> Element(Msg) {
|
||||||
html.div(
|
html.div(
|
||||||
[
|
[
|
||||||
|
|
@ -201,6 +420,31 @@ fn view(_: Model) -> Element(Msg) {
|
||||||
})
|
})
|
||||||
decode.success(msg)
|
decode.success(msg)
|
||||||
}),
|
}),
|
||||||
|
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:))
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
[component.default_slot([], [])],
|
[component.default_slot([], [])],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
19
webc/src/viewer_hoverbar_component.ffi.mjs
Normal file
19
webc/src/viewer_hoverbar_component.ffi.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
export function registerComponentMethod(root, name, callback) {
|
||||||
|
root.host[name] = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function focusAtEnd(root) {
|
||||||
|
const input = root.querySelector('[tabindex="0"]');
|
||||||
|
if (input) {
|
||||||
|
const tmp = input.value;
|
||||||
|
input.value = "";
|
||||||
|
input.focus();
|
||||||
|
input.value = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function blurAll(root) {
|
||||||
|
root.querySelectorAll("input").forEach((element) => {
|
||||||
|
element.blur();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
import gleam/option.{None, Some}
|
import gleam/dynamic.{type Dynamic}
|
||||||
|
import gleam/dynamic/decode
|
||||||
|
import gleam/int
|
||||||
|
import gleam/io
|
||||||
|
import gleam/json
|
||||||
|
import gleam/option.{type Option, None, Some}
|
||||||
|
import gleam/result
|
||||||
import lustre.{type App}
|
import lustre.{type App}
|
||||||
import lustre/attribute as attr
|
import lustre/attribute as attr
|
||||||
import lustre/component
|
import lustre/component
|
||||||
import lustre/effect.{type Effect}
|
import lustre/effect.{type Effect}
|
||||||
import lustre/element.{type Element}
|
import lustre/element.{type Element}
|
||||||
import lustre/element/html
|
import lustre/element/html
|
||||||
|
import lustre/event
|
||||||
|
|
||||||
|
import encodable.{type Encodable}
|
||||||
|
import field_type.{type FieldType}
|
||||||
import hoverbar
|
import hoverbar
|
||||||
|
|
||||||
pub const name: String = "viewer-hoverbar"
|
pub const name: String = "viewer-hoverbar"
|
||||||
|
|
@ -16,10 +25,14 @@ pub fn component() -> App(Nil, Model, Msg) {
|
||||||
ParentChangedRootPath(value) |> Ok
|
ParentChangedRootPath(value) |> Ok
|
||||||
}),
|
}),
|
||||||
component.on_attribute_change("field-type", fn(value) {
|
component.on_attribute_change("field-type", fn(value) {
|
||||||
ParentChangedFieldType(value) |> Ok
|
json.parse(value, field_type.decoder())
|
||||||
|
|> result.map(ParentChangedFieldType)
|
||||||
|
|> result.replace_error(Nil)
|
||||||
}),
|
}),
|
||||||
component.on_attribute_change("value", fn(value) {
|
component.on_attribute_change("value", fn(value) {
|
||||||
ParentChangedValue(value) |> Ok
|
json.parse(value, decode.optional(encodable.decoder()))
|
||||||
|
|> result.map(ParentChangedValue)
|
||||||
|
|> result.replace_error(Nil)
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
@ -28,29 +41,100 @@ pub type Model {
|
||||||
Model(
|
Model(
|
||||||
hoverbar_model: hoverbar.Model,
|
hoverbar_model: hoverbar.Model,
|
||||||
root_path: String,
|
root_path: String,
|
||||||
field_type: String,
|
field_type: FieldType,
|
||||||
value: String,
|
value: Option(Encodable),
|
||||||
|
edit_state: EditState,
|
||||||
|
editing: Bool,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type EditState {
|
||||||
|
Text(value: Option(String))
|
||||||
|
Inactive
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_edit_state(value: Encodable, _: FieldType) -> EditState {
|
||||||
|
case value {
|
||||||
|
encodable.Integer(Some(value)) -> Text(Some(value |> int.to_string))
|
||||||
|
encodable.Integer(None) -> Text(None)
|
||||||
|
encodable.Text(Some(value)) -> Text(Some(value))
|
||||||
|
encodable.Text(None) -> Text(None)
|
||||||
|
encodable.Timestamptz(Some(value)) -> Text(Some(value))
|
||||||
|
encodable.Timestamptz(None) -> Text(None)
|
||||||
|
encodable.Uuid(Some(value)) -> Text(Some(value))
|
||||||
|
encodable.Uuid(None) -> Text(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_edit_state(
|
||||||
|
edit_state: EditState,
|
||||||
|
f_type: FieldType,
|
||||||
|
) -> Result(Encodable, Nil) {
|
||||||
|
case f_type, edit_state {
|
||||||
|
field_type.Text, Text(value) -> Ok(encodable.Text(value))
|
||||||
|
field_type.Integer, Text(Some(value)) ->
|
||||||
|
value |> int.parse |> result.map(Some) |> result.map(encodable.Integer)
|
||||||
|
field_type.Integer, Text(None) -> Ok(encodable.Integer(None))
|
||||||
|
field_type.InterimUser, _ -> todo
|
||||||
|
// TODO validate uuid format
|
||||||
|
field_type.Uuid, Text(value) -> Ok(encodable.Uuid(value))
|
||||||
|
field_type.Timestamp(_format), _ -> todo
|
||||||
|
field_type.Unknown, _ -> Error(Nil)
|
||||||
|
_, Inactive -> Error(Nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn init(_) -> #(Model, Effect(Msg)) {
|
fn init(_) -> #(Model, Effect(Msg)) {
|
||||||
let #(hoverbar_model, hoverbar_effect) = hoverbar.init(Nil)
|
let #(hoverbar_model, hoverbar_effect) = hoverbar.init(Nil)
|
||||||
#(
|
#(
|
||||||
Model(
|
Model(
|
||||||
hoverbar_model: hoverbar.Model(..hoverbar_model, tab_key: "editor"),
|
hoverbar_model: hoverbar.Model(..hoverbar_model, tab_key: "editor"),
|
||||||
root_path: "",
|
root_path: "",
|
||||||
field_type: "",
|
field_type: field_type.Unknown,
|
||||||
value: "",
|
value: None,
|
||||||
|
edit_state: Inactive,
|
||||||
|
editing: False,
|
||||||
),
|
),
|
||||||
|
effect.batch([
|
||||||
hoverbar_effect |> effect.map(HoverbarMsg),
|
hoverbar_effect |> effect.map(HoverbarMsg),
|
||||||
|
register_component_methods(),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn register_component_methods() -> Effect(Msg) {
|
||||||
|
use dispatch, root <- effect.before_paint()
|
||||||
|
register_component_method(root, "focus", fn() {
|
||||||
|
dispatch(ParentRequestedFocus)
|
||||||
|
})
|
||||||
|
register_component_method(root, "blur", fn() { dispatch(ParentRequestedBlur) })
|
||||||
|
register_component_method(root, "overwrite", fn(value: String) {
|
||||||
|
dispatch(ParentRequestedOverwrite(value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@external(javascript, "./viewer_hoverbar_component.ffi.mjs", "registerComponentMethod")
|
||||||
|
fn register_component_method(
|
||||||
|
on _root: Dynamic,
|
||||||
|
name _name: String,
|
||||||
|
callback _callback: cb,
|
||||||
|
) -> Nil {
|
||||||
|
Nil
|
||||||
|
}
|
||||||
|
|
||||||
pub type Msg {
|
pub type Msg {
|
||||||
HoverbarMsg(hoverbar.Msg)
|
HoverbarMsg(hoverbar.Msg)
|
||||||
|
ComponentCycledInputValue
|
||||||
ParentChangedRootPath(String)
|
ParentChangedRootPath(String)
|
||||||
ParentChangedFieldType(String)
|
ParentChangedFieldType(FieldType)
|
||||||
ParentChangedValue(String)
|
ParentChangedValue(Option(Encodable))
|
||||||
|
ParentRequestedFocus
|
||||||
|
ParentRequestedBlur
|
||||||
|
ParentRequestedOverwrite(String)
|
||||||
|
UserBlurredInput
|
||||||
|
UserChangedInput(String)
|
||||||
|
UserFocusedInput
|
||||||
|
UserPressedKey(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
|
|
@ -63,16 +147,141 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
hoverbar_effect |> effect.map(HoverbarMsg),
|
hoverbar_effect |> effect.map(HoverbarMsg),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ComponentCycledInputValue -> #(
|
||||||
|
Model(..model, editing: True),
|
||||||
|
emit_edit_start_event(),
|
||||||
|
)
|
||||||
ParentChangedRootPath(root_path) -> #(
|
ParentChangedRootPath(root_path) -> #(
|
||||||
Model(..model, root_path:),
|
Model(..model, root_path:),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
ParentChangedFieldType(field_type) -> #(
|
ParentChangedFieldType(f_type) -> {
|
||||||
Model(..model, field_type:),
|
#(Model(..model, field_type: f_type), effect.none())
|
||||||
|
}
|
||||||
|
ParentChangedValue(value) -> #(
|
||||||
|
Model(
|
||||||
|
..model,
|
||||||
|
value:,
|
||||||
|
edit_state: value
|
||||||
|
|> option.map(to_edit_state(_, model.field_type))
|
||||||
|
|> option.unwrap(Text(None)),
|
||||||
|
),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
ParentChangedValue(value) -> #(Model(..model, value:), effect.none())
|
ParentRequestedFocus -> {
|
||||||
|
#(model, focus_at_end())
|
||||||
}
|
}
|
||||||
|
ParentRequestedBlur -> {
|
||||||
|
#(model, blur_all())
|
||||||
|
}
|
||||||
|
ParentRequestedOverwrite(value) -> {
|
||||||
|
case model.edit_state {
|
||||||
|
Text(_) -> {
|
||||||
|
let new_model = Model(..model, edit_state: Text(Some(value)))
|
||||||
|
#(
|
||||||
|
new_model,
|
||||||
|
effect.batch([focus_at_end(), try_emit_edit_update_event(new_model)]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ -> #(model, effect.none())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Place responsibility for committing the value on the viewer-controller.
|
||||||
|
// If the blur was due to the user clicking on a different cell, for
|
||||||
|
// example, the controller may wish to prevent the selection change if the
|
||||||
|
// edited value cannot be committed.
|
||||||
|
UserBlurredInput -> try_emit_edit_end_event(model)
|
||||||
|
UserChangedInput(value) -> {
|
||||||
|
case model.editing {
|
||||||
|
True -> {
|
||||||
|
let new_model = Model(..model, edit_state: Text(Some(value)))
|
||||||
|
#(new_model, try_emit_edit_update_event(new_model))
|
||||||
|
}
|
||||||
|
False -> #(model, effect.none())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserFocusedInput -> #(
|
||||||
|
Model(..model, editing: True),
|
||||||
|
emit_edit_start_event(),
|
||||||
|
)
|
||||||
|
UserPressedKey(key) ->
|
||||||
|
case key {
|
||||||
|
"Enter" -> try_emit_edit_end_event(model)
|
||||||
|
"Escape" -> cancel_edit(model)
|
||||||
|
_ -> #(model, effect.none())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cycle the value of the input element so that the cursor is placed at the end.
|
||||||
|
fn focus_at_end() -> Effect(Msg) {
|
||||||
|
use _dispatch, root <- effect.before_paint()
|
||||||
|
do_focus_at_end(in: root)
|
||||||
|
}
|
||||||
|
|
||||||
|
@external(javascript, "./viewer_hoverbar_component.ffi.mjs", "focusAtEnd")
|
||||||
|
fn do_focus_at_end(in _root: Dynamic) -> Nil {
|
||||||
|
Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blur_all() -> Effect(Msg) {
|
||||||
|
use _dispatch, root <- effect.before_paint()
|
||||||
|
do_blur_all(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
@external(javascript, "./viewer_hoverbar_component.ffi.mjs", "blurAll")
|
||||||
|
fn do_blur_all(in _root: Dynamic) -> Nil {
|
||||||
|
Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_edit_start_event() -> Effect(Msg) {
|
||||||
|
event.emit("edit-start", json.null())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_emit_edit_update_event(model: Model) -> Effect(Msg) {
|
||||||
|
case model.value, from_edit_state(model.edit_state, model.field_type) {
|
||||||
|
Some(original_value), Ok(edited_value) -> {
|
||||||
|
event.emit(
|
||||||
|
"edit-update",
|
||||||
|
json.object([
|
||||||
|
#("original", original_value |> encodable.to_json),
|
||||||
|
#("edited", edited_value |> encodable.to_json),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_, _ -> effect.none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_emit_edit_end_event(model: Model) -> #(Model, Effect(Msg)) {
|
||||||
|
case model.value, from_edit_state(model.edit_state, model.field_type) {
|
||||||
|
Some(original_value), Ok(edited_value) -> #(
|
||||||
|
Model(..model, editing: False),
|
||||||
|
event.emit(
|
||||||
|
"edit-end",
|
||||||
|
json.object([
|
||||||
|
#("original", original_value |> encodable.to_json),
|
||||||
|
#("edited", edited_value |> encodable.to_json),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// TODO flag error to user
|
||||||
|
_, _ -> #(model, effect.none())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel_edit(model: Model) -> #(Model, Effect(Msg)) {
|
||||||
|
let new_model =
|
||||||
|
Model(
|
||||||
|
..model,
|
||||||
|
edit_state: model.value
|
||||||
|
|> option.map(to_edit_state(_, model.field_type))
|
||||||
|
|> option.unwrap(Inactive),
|
||||||
|
)
|
||||||
|
#(
|
||||||
|
new_model,
|
||||||
|
effect.batch([try_emit_edit_update_event(new_model), blur_all()]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(model: Model) -> Element(Msg) {
|
fn view(model: Model) -> Element(Msg) {
|
||||||
|
|
@ -97,16 +306,53 @@ fn view(model: Model) -> Element(Msg) {
|
||||||
_ -> element.none()
|
_ -> element.none()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
control_panel: fn(tab_key) {
|
control_panel: fn(_) { None },
|
||||||
case tab_key {
|
|
||||||
"editor" -> None
|
|
||||||
_ -> None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn editor_control_bar(_model: Model) -> Element(Msg) {
|
fn editor_control_bar(model: Model) -> Element(Msg) {
|
||||||
html.input([attr.type_("text"), attr.class("control-bar__input")])
|
case model.edit_state {
|
||||||
|
Text(value) ->
|
||||||
|
html.div([attr.class("text-editor")], [
|
||||||
|
html.input([
|
||||||
|
attr.type_("text"),
|
||||||
|
attr.class("text-editor__input"),
|
||||||
|
attr.value(value |> option.unwrap("")),
|
||||||
|
attr.placeholder(case value |> option.is_none() {
|
||||||
|
True -> "Null"
|
||||||
|
False -> "Empty string"
|
||||||
|
}),
|
||||||
|
attr.tabindex(0),
|
||||||
|
event.on_focus(UserFocusedInput),
|
||||||
|
event.on_blur(UserBlurredInput),
|
||||||
|
event.on_input(UserChangedInput),
|
||||||
|
event.on_keydown(UserPressedKey),
|
||||||
|
]),
|
||||||
|
html.div([attr.class("toggle__container")], [
|
||||||
|
html.input([
|
||||||
|
attr.type_("checkbox"),
|
||||||
|
attr.id("null-checkbox"),
|
||||||
|
attr.class("toggle__checkbox"),
|
||||||
|
case value {
|
||||||
|
Some("") | None -> attr.none()
|
||||||
|
_ -> attr.disabled(True)
|
||||||
|
},
|
||||||
|
attr.checked(value |> option.is_none()),
|
||||||
|
]),
|
||||||
|
html.label(
|
||||||
|
[
|
||||||
|
attr.class("toggle__label"),
|
||||||
|
case value {
|
||||||
|
Some("") | None -> attr.none()
|
||||||
|
_ -> attr.class("toggle__label--disabled")
|
||||||
|
},
|
||||||
|
attr.for("null-checkbox"),
|
||||||
|
],
|
||||||
|
[html.text("Null")],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
Inactive -> html.div([attr.class("editor")], [])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue