///|
// Internal controller retained for drawers.
priv struct DialogController {
id : String
open : Bool
open_command : @cmd.Cmd
close_command : @cmd.Cmd
on_close : @cmd.Emit[String]?
on_cancel : @cmd.Cmd?
native_open : Bool?
modal : Bool
close_on_escape : Bool
}
///|
#cfg(target="js")
priv struct DialogModel {
open : Bool
generation : Int
} derive(Eq)
///|
#cfg(target="js")
priv enum DialogMessage {
OpenDialog
RequestDialogClose
FinalizeDialogClose(Int)
DialogDidClose(String)
}
///|
const DialogExitDurationMs : Int = 200
///|
const DialogNativeStyle : String = "width:100vw;height:100vh;max-width:none;max-height:none;margin:auto;border:0;padding:0;background:transparent;color:inherit;overflow:visible;pointer-events:none;transition:display 200ms,overlay 200ms;transition-behavior:allow-discrete"
///|
const DialogOverlayStyle : String = "position:fixed;inset:0;z-index:50;background:rgb(0 0 0 / 0.5);opacity:var(--rui-dialog-overlay-opacity,1);pointer-events:auto;transition:opacity 200ms ease"
///|
const DialogPositionerStyle : String = "position:fixed;inset:0;z-index:51;display:grid;place-items:center;overflow-y:auto;padding:1rem;pointer-events:none"
///|
fn DialogController::title_id(self : DialogController) -> String {
"\{self.id}-title"
}
///|
fn DialogController::description_id(self : DialogController) -> String {
"\{self.id}-description"
}
///|
#cfg(target="js")
fn[T] dialog_emit_or_none(emit : @cmd.Emit[T]?, value : T) -> @cmd.Cmd {
if emit is Some(emit) {
emit(value)
} else {
@cmd.none
}
}
///|
fn dialog_state_value(open : Bool) -> String {
if open {
"open"
} else {
"closed"
}
}
///|
fn dialog_overlay_motion_style(open : Bool) -> String {
if open {
"--rui-dialog-overlay-opacity:1"
} else {
"--rui-dialog-overlay-opacity:0;pointer-events:none"
}
}
///|
fn dialog_content_motion_style(open : Bool) -> String {
if open {
"--rui-dialog-content-opacity:1;--rui-dialog-content-transform:translateY(0) scale(1)"
} else {
"--rui-dialog-content-opacity:0;--rui-dialog-content-transform:translateY(-0.5rem) scale(0.95);pointer-events:none"
}
}
///|
fn dialog_state_attrs(
attrs : @html.Attrs?,
slot : String,
open : Bool,
) -> @html.Attrs {
let state = dialog_state_value(open)
let element_attrs = ui_attrs(attrs)
.data_set("slot", slot)
.data_set("state", state)
if open {
ignore(element_attrs.data_set("open", ""))
} else {
ignore(element_attrs.data_set("closed", ""))
}
element_attrs
}
///|
fn dialog_overlay_part(
scope : DialogController,
slot~ : String,
close_on_overlay~ : Bool,
id~ : String?,
class~ : String?,
title~ : String?,
hidden~ : Bool?,
attrs~ : @html.Attrs?,
style~ : Array[String],
) -> @html.Html {
let element_attrs = dialog_state_attrs(attrs, slot, scope.open)
if close_on_overlay {
ignore(
element_attrs.on_click(_ => {
if ui_dialog_should_close_outside() {
scope.close_command
} else {
@cmd.none
}
}),
)
}
@html.div(
style=ui_styles(
[UiBoxSizing, DialogOverlayStyle, dialog_overlay_motion_style(scope.open)],
style,
),
id?,
class?,
title?,
hidden?,
attrs=element_attrs,
@html.nothing,
)
}
///|
fn[C : @html.IsChildren] dialog_control_button(
slot~ : String,
command~ : @cmd.Cmd,
variant~ : ButtonVariant,
size~ : ButtonSize,
disabled~ : Bool,
id~ : String?,
class~ : String?,
title~ : String?,
aria_label~ : String?,
on_click~ : @cmd.Cmd?,
attrs~ : @html.Attrs?,
style~ : Array[String],
children : C,
) -> @html.Html {
let control_attrs = ui_attrs(attrs)
.data_set("slot", slot)
.data_set("variant", button_variant_value(variant))
.data_set("size", button_size_value(size))
ignore(control_attrs.aria_disabled(ui_bool(disabled)))
if aria_label is Some(label) {
ignore(control_attrs.aria_label(label))
}
if disabled {
ignore(control_attrs.data_set("disabled", ""))
} else {
if on_click is Some(on_click) {
ignore(control_attrs.on_click(_ => on_click))
}
ignore(control_attrs.on_click(_ => command))
}
@html.button(
style=ui_styles(
[
UiBoxSizing,
UiFontSans,
UiTextRendering,
UiTransition,
ButtonBaseStyle,
button_variant_style(variant),
button_size_style(size),
ui_disabled_style(disabled),
],
style,
),
id?,
class?,
title?,
type_="button",
disabled~,
attrs=control_attrs,
children,
)
}
///|
#cfg(target="js")
fn stateful_dialog(
id~ : String,
default_open? : Bool = false,
modal? : Bool = true,
close_on_escape? : Bool = true,
on_open_change? : @cmd.Emit[Bool],
on_close? : @cmd.Emit[String],
children : (DialogController) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
let (model, emit) = @rabbita.create_state_with_init(
init=fn(emit) {
(
{ open: default_open, generation: 0, },
@cmd.batch([
ui_dialog_bind(id, emit(RequestDialogClose)),
if default_open {
ui_dialog_show(id, modal)
} else {
@cmd.none
},
]),
)
},
update=fn(current, message, emit) {
match message {
OpenDialog =>
if current.open {
(current, @cmd.none)
} else {
let next = { open: true, generation: current.generation + 1, }
(
next,
@cmd.batch([
dialog_emit_or_none(on_open_change, true),
ui_dialog_show(id, modal),
]),
)
}
RequestDialogClose =>
if current.open {
let generation = current.generation + 1
(
{ open: false, generation, },
@cmd.batch([
dialog_emit_or_none(on_open_change, false),
@rabbita.delay(
emit(FinalizeDialogClose(generation)),
DialogExitDurationMs,
),
]),
)
} else {
(current, @cmd.none)
}
FinalizeDialogClose(generation) =>
if !current.open && current.generation == generation {
(current, ui_dialog_close(id))
} else {
(current, @cmd.none)
}
DialogDidClose(value) =>
(
{ open: false, generation: current.generation + 1, },
@cmd.batch([
if current.open {
dialog_emit_or_none(on_open_change, false)
} else {
@cmd.none
},
dialog_emit_or_none(on_close, value),
]),
)
}
},
)
model.view(model => {
let scope : DialogController = {
id,
open: model.open,
open_command: emit(OpenDialog),
close_command: emit(RequestDialogClose),
on_close: Some(emit.map(value => DialogDidClose(value))),
on_cancel: Some(
if close_on_escape {
emit(RequestDialogClose)
} else {
@cmd.none
},
),
native_open: None,
modal,
close_on_escape,
}
children(scope)
})
}
///|
#cfg(not(target="js"))
fn stateful_dialog(
id~ : String,
default_open? : Bool = false,
modal? : Bool = true,
close_on_escape? : Bool = true,
on_open_change? : @cmd.Emit[Bool],
on_close? : @cmd.Emit[String],
children : (DialogController) -> @html.Html,
) -> @rabbita.Val[@html.Html] {
ignore((on_open_change, on_close))
let scope : DialogController = {
id,
open: default_open,
open_command: @cmd.none,
close_command: @cmd.none,
on_close: None,
on_cancel: None,
native_open: Some(default_open),
modal,
close_on_escape,
}
@rabbita.Val::constant(children(scope))
}