///|
#cfg(target="js")
priv struct DialogLayerBinding {
element : @dom.Element
dialog : @dom.HTMLDialogElement
request_close : Ref[() -> Unit]
return_focus : Ref[@dom.HTMLElement?]
}
///|
#cfg(target="js")
let dialog_layers : Map[String, DialogLayerBinding] = Map([])
///|
#cfg(target="js")
let dialog_stack : Ref[Array[String]] = Ref([])
///|
#cfg(target="js")
let dialog_bound_document : Ref[@dom.Document?] = Ref(None)
///|
#cfg(target="js")
let dialog_outside_press_had_floating : Ref[Bool] = Ref(false)
///|
#cfg(target="js")
fn dialog_update_stack(id : String, open : Bool) -> Unit {
let next : Array[String] = []
for current in dialog_stack.val {
if current != id {
next.push(current)
}
}
if open {
next.push(id)
}
dialog_stack.val = next
}
///|
#cfg(target="js")
fn dialog_live_layer(id : String) -> DialogLayerBinding? {
guard dialog_layers.get(id) is Some(layer) else { return None }
guard ui_element_by_id(id) is Some(element) else {
dialog_update_stack(id, false)
return None
}
if !element.get_is_connected() ||
!layer.element.is_same_node(element.as_node()) ||
!layer.dialog.open() {
dialog_update_stack(id, false)
return None
}
Some(layer)
}
///|
#cfg(target="js")
fn dialog_top_layer() -> DialogLayerBinding? {
let stack = dialog_stack.val
for index = stack.length() - 1; index >= 0; index = index - 1 {
let id = stack[index]
if dialog_live_layer(id) is Some(layer) {
return Some(layer)
}
}
None
}
///|
#cfg(target="js")
fn dialog_restore_focus(layer : DialogLayerBinding) -> Unit {
if layer.element.get_attribute("data-modal").unwrap_or("") != "false" ||
layer.element.get_attribute("data-restore-focus").unwrap_or("") == "false" {
return
}
let document = @dom.document()
if document.get_active_element() is Some(active) {
let active_is_body = if document.get_body() is Some(body) {
active.is_same_node(body.as_node())
} else {
false
}
if !active_is_body && !layer.element.contains(active.as_node()) {
return
}
}
let final_id = layer.element
.get_attribute("data-final-focus-id")
.unwrap_or("")
let target = if ui_element_by_id(final_id) is Some(element) &&
element.to_html_element() is Some(html) {
Some(html)
} else {
layer.return_focus.val
}
if target is Some(html) && html.get_is_connected() {
html.focus_with_options(prevent_scroll=true)
}
}
///|
#cfg(target="js")
fn dialog_bind_document_events() -> Unit {
let document = @dom.document()
if dialog_bound_document.val is Some(bound) &&
bound.to_node().is_same_node(document.to_node()) {
return
}
dialog_bound_document.val = Some(document)
document.add_event_listener_with_options(
"pointerdown",
_ => dialog_outside_press_had_floating.val = floating_top_layer() is Some(_),
capture=true,
)
document.add_event_listener("keydown", event => {
guard event.to_keyboard_event() is Some(keyboard) else { return }
if keyboard.key() != "Escape" ||
event.get_default_prevented() ||
floating_top_layer() is Some(_) {
return
}
guard dialog_top_layer() is Some(layer) else { return }
if layer.element.get_attribute("data-close-on-escape").unwrap_or("") !=
"true" {
return
}
event.prevent_default()
event.stop_propagation()
(layer.request_close.val)()
})
document.add_event_listener("click", event => {
if event.get_default_prevented() {
return
}
let had_floating = dialog_outside_press_had_floating.val
dialog_outside_press_had_floating.val = false
if had_floating || floating_top_layer() is Some(_) {
return
}
guard dialog_top_layer() is Some(layer) else { return }
if layer.element.get_attribute("data-modal").unwrap_or("") != "false" {
return
}
let selector = "[data-slot=\"dialog-content\"],[data-slot=\"sheet-content\"],[data-slot=\"drawer-content\"]"
if layer.element.query_selector(selector) is Some(content) &&
floating_event_inside(event, content) {
return
}
if layer.element.get_attribute("data-close-on-outside").unwrap_or("") ==
"true" {
(layer.request_close.val)()
}
})
}
///|
#cfg(target="js")
fn bind_dialog_layer(id : String, request_close : () -> Unit) -> Unit {
guard ui_element_by_id(id) is Some(element) else { return }
guard element.to_html_dialog_element() is Some(dialog) else { return }
if dialog_layers.get(id) is Some(layer) &&
layer.element.is_same_node(element.as_node()) {
layer.request_close.val = request_close
return
}
let layer : DialogLayerBinding = {
element,
dialog,
request_close: Ref(request_close),
return_focus: Ref(None),
}
dialog_layers[id] = layer
dialog_bind_document_events()
dialog.add_event_listener("close", _ => {
dialog_update_stack(id, false)
if ui_has_window() {
@dom.window().queue_microtask(() => dialog_restore_focus(layer))
} else {
dialog_restore_focus(layer)
}
})
}
///|
#cfg(target="js")
fn should_close_dialog_outside() -> Bool {
let had_floating = dialog_outside_press_had_floating.val
dialog_outside_press_had_floating.val = false
!had_floating && floating_top_layer() is None
}
///|
#cfg(target="js")
fn ui_dialog_should_close_outside() -> Bool {
should_close_dialog_outside()
}
///|
#cfg(not(target="js"))
fn ui_dialog_should_close_outside() -> Bool {
true
}
///|
#cfg(target="js")
fn mark_dialog_layer(id : String, open : Bool) -> Unit {
dialog_update_stack(id, open)
if !open {
return
}
guard dialog_layers.get(id) is Some(layer) else { return }
if @dom.document().get_active_element() is Some(active) &&
!layer.element.contains(active.as_node()) &&
active.to_html_element() is Some(html) {
layer.return_focus.val = Some(html)
}
if layer.element.get_attribute("data-modal").unwrap_or("") != "false" ||
layer.element.get_attribute("data-initial-focus").unwrap_or("") == "false" {
return
}
fn focus_initial() -> Unit {
if !layer.dialog.open() {
return
}
let requested_id = layer.element
.get_attribute("data-initial-focus-id")
.unwrap_or("")
let requested = ui_element_by_id(requested_id)
let selector = "button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])"
let target = if requested is Some(element) &&
layer.element.contains(element.as_node()) {
Some(element)
} else {
match layer.element.query_selector(selector) {
Some(element) => Some(element)
None => Some(layer.element)
}
}
if target is Some(element) {
ui_focus_element_without_scroll(element)
}
}
if ui_has_window() {
ignore(@dom.window().request_animation_frame(_ => focus_initial()))
} else {
focus_initial()
}
}
///|
#cfg(target="js")
fn ui_dialog_bind(id : String, request_close : @cmd.Cmd) -> @cmd.Cmd {
@cmd.custom_cmd(kind=@cmd.AfterRender, scheduler => {
ui_after_mount(
id,
() => bind_dialog_layer(id, () => scheduler.add(request_close)),
purpose="dialog-bind",
)
})
}
///|
#cfg(not(target="js"))
fn ui_dialog_bind(id : String, request_close : @cmd.Cmd) -> @cmd.Cmd {
ignore((id, request_close))
@cmd.none
}
///|
#cfg(target="js")
fn ui_dialog_show(id : String, modal : Bool) -> @cmd.Cmd {
@cmd.custom_cmd(kind=@cmd.after_render, _ => {
ui_after_mount(
id,
() => {
if @dom.document().get_element_by_id(id).to_option() is Some(element) &&
element.to_html_element() is Some(html_element) &&
html_element.to_html_dialog_element() is Some(dialog) {
mark_dialog_layer(id, true)
if !dialog.open() {
if modal {
dialog.show_modal()
} else {
dialog.show()
}
}
}
},
purpose="dialog-show",
)
})
}
///|
#cfg(not(target="js"))
fn ui_dialog_show(id : String, modal : Bool) -> @cmd.Cmd {
ignore((id, modal))
@cmd.none
}
///|
#cfg(target="js")
fn ui_dialog_close(id : String) -> @cmd.Cmd {
@cmd.custom_cmd(kind=@cmd.after_render, _ => {
if @dom.document().get_element_by_id(id).to_option() is Some(element) &&
element.to_html_element() is Some(html_element) &&
html_element.to_html_dialog_element() is Some(dialog) {
if dialog.open() {
dialog.close()
}
mark_dialog_layer(id, false)
}
})
}
///|
#cfg(not(target="js"))
fn ui_dialog_close(id : String) -> @cmd.Cmd {
ignore(id)
@cmd.none
}
///|
#cfg(target="js")
fn ui_focus_id(id : String) -> @cmd.Cmd {
@cmd.custom_cmd(kind=@cmd.after_render, _ => {
if @dom.document().get_element_by_id(id).to_option() is Some(element) &&
element.to_html_element() is Some(html_element) {
html_element.focus()
}
})
}
///|
#cfg(not(target="js"))
fn ui_focus_id(id : String) -> @cmd.Cmd {
ignore(id)
@cmd.none
}
///|
#cfg(target="js")
fn ui_scroll_id(id : String, end_ : Bool) -> @cmd.Cmd {
@cmd.custom_cmd(kind=@cmd.after_render, _ => {
if @dom.document().get_element_by_id(id).to_option() is Some(element) {
if end_ {
element.set_scroll_top(element.get_scroll_height())
} else {
element.set_scroll_top(0.0)
}
}
})
}
///|
#cfg(not(target="js"))
fn ui_scroll_id(id : String, end_ : Bool) -> @cmd.Cmd {
ignore((id, end_))
@cmd.none
}