diff --git a/web/src/at_record_web/pages/crate.gleam b/web/src/at_record_web/pages/crate.gleam index 95e3c67..f7085e7 100644 --- a/web/src/at_record_web/pages/crate.gleam +++ b/web/src/at_record_web/pages/crate.gleam @@ -15,7 +15,7 @@ import at_record_web/route import at_record_web/ui/app_bar as bar import at_record_web/ui/controls as ctl import at_record_web/ui/covers as cov -import gleam/dynamic/decode +import at_record_web/ui/infinite_scroll as scroll import gleam/int import gleam/list import gleam/option.{type Option} @@ -79,42 +79,22 @@ fn loaded_state( hero(hero_bar, hero_count(view, items), ctl.badge("◆ GROWING", ctl.Accent)), html.div([attr.class("body list-body")], [ toolbar(view, display_toggle(display)), - html.div([attr.class("list-scroll"), on_near_bottom(can_load_more)], [ - body(display, visible), - loading_skeletons(display, loading_more), - load_more(shown, total, has_more, loading_more), - ]), + html.div( + [ + attr.class("list-scroll"), + scroll.on_near_bottom(ShowMoreCrate, can_load_more), + ], + [ + body(display, visible), + loading_skeletons(display, loading_more), + load_more(shown, total, has_more, loading_more), + ], + ), ]), bar.fab("ADD", route.to_path(Scan)), ]) } -/// Fires ShowMoreCrate once the container nears its bottom, unless there is -/// nothing left to fetch or a fetch is already in flight; `can_load_more` -/// folds both checks in so the decoder can just bail out early otherwise. -fn on_near_bottom(can_load_more: Bool) -> attr.Attribute(Msg) { - event.on("scroll", case can_load_more { - False -> decode.failure(ShowMoreCrate, "nothing to load") - True -> { - use top <- decode.subfield(["target", "scrollTop"], scroll_metric()) - use scroll_height <- decode.subfield( - ["target", "scrollHeight"], - scroll_metric(), - ) - use client <- decode.subfield(["target", "clientHeight"], scroll_metric()) - case top +. client >=. scroll_height -. 120.0 { - True -> decode.success(ShowMoreCrate) - False -> decode.failure(ShowMoreCrate, "not near bottom") - } - } - }) -} - -/// DOM scroll metrics come back as plain JS numbers; accept int or float. -fn scroll_metric() -> decode.Decoder(Float) { - decode.one_of(decode.float, [decode.int |> decode.map(int.to_float)]) -} - /// Below the windowed list: a "showing N of M" line (also a subtle button, /// so loading works without scrolling) and, while a fetch is in flight, a /// muted "LOADING MORE" marker. Once everything is loaded this renders diff --git a/web/src/at_record_web/ui/infinite_scroll.gleam b/web/src/at_record_web/ui/infinite_scroll.gleam index 931813b..9f63658 100644 --- a/web/src/at_record_web/ui/infinite_scroll.gleam +++ b/web/src/at_record_web/ui/infinite_scroll.gleam @@ -20,7 +20,7 @@ pub fn view( on_load_more: msg, status: Status, ) -> Element(msg) { - html.div([attr.class("results-scroll"), on_scroll_near_bottom(on_load_more)], [ + html.div([attr.class("results-scroll"), on_near_bottom(on_load_more, True)], [ html.ul([attr.class("results")], rows), footer(status), ]) @@ -38,16 +38,20 @@ fn hint(message: String) -> Element(msg) { html.p([attr.class("hint load-more")], [text(message)]) } -/// Fire `on_load_more` when the container nears its bottom; suppress the message -/// otherwise by failing the decoder. -fn on_scroll_near_bottom(on_load_more: msg) -> attr.Attribute(msg) { - event.on("scroll", { - use top <- decode.subfield(["target", "scrollTop"], number()) - use scroll_height <- decode.subfield(["target", "scrollHeight"], number()) - use client <- decode.subfield(["target", "clientHeight"], number()) - case top +. client >=. scroll_height -. 120.0 { - True -> decode.success(on_load_more) - False -> decode.failure(on_load_more, "not near bottom") +/// Fire `on_load_more` once the container nears its bottom, unless `enabled` +/// is `False` (e.g. nothing left to fetch, or already loading), in which case +/// the decoder always fails without even reading the scroll metrics. +pub fn on_near_bottom(on_load_more: msg, enabled: Bool) -> attr.Attribute(msg) { + event.on("scroll", case enabled { + False -> decode.failure(on_load_more, "disabled") + True -> { + use top <- decode.subfield(["target", "scrollTop"], number()) + use scroll_height <- decode.subfield(["target", "scrollHeight"], number()) + use client <- decode.subfield(["target", "clientHeight"], number()) + case top +. client >=. scroll_height -. 120.0 { + True -> decode.success(on_load_more) + False -> decode.failure(on_load_more, "not near bottom") + } } }) }