//
// Copyright (c) 2015, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 17 Feb 2015 Andy Frank Creation
//
using concurrent
using dom
using graphics
**
** Popup window which can be closed clicking outside of element.
**
** See also: [docDomkit]`docDomkit::Modals#popup`
**
@Js class Popup : Elem
{
new make() : super()
{
this.uid = nextId.getAndIncrement
this.style.addClass("domkit-Popup")
this.onEvent("keydown", false) |e| { if (e.key == Key.esc) close }
this->tabIndex = 0
}
** Where to align Popup relative to open(x,y):
** - Align.left: align left edge popup to (x,y)
** - Align.center: center popup with (x,y)
** - Align.right: align right edge of popup to (x,y)
Align halign := Align.left
** Return 'true' if this popup currently open.
Bool isOpen { private set }
** Open this popup in the current Window. If popup is already
** open this method does nothing. This method always invokes
** `fitBounds` to verify popup does not overflow viewport.
Void open(Float x, Float y)
{
if (isOpen) return
this.openPos = Point(x, y)
this.style.setAll([
"left": "${x}px",
"top": "${y}px",
"-webkit-transform": "scale(1)",
"opacity": "0.0"
])
body := Win.cur.doc.body
body.add(Elem {
it.id = "domkitPopup-mask-$uid"
it.style.addClass("domkit-Popup-mask")
it.onEvent("mousedown", false) |e| {
if (e.target == this || this.containsChild(e.target)) return
close
}
it.add(this)
})
fitBounds
onBeforeOpen
this.transition([
"opacity": "1"
], null, 100ms) { this.focus; fireOpen(null) }
}
** Close this popup. If popup is already closed
** this method does nothing.
Void close()
{
this.transition(["transform": "scale(0.75)", "opacity": "0"], null, 100ms)
{
mask := Win.cur.doc.elemById("domkitPopup-mask-$uid")
mask?.parent?.remove(mask)
fireClose(null)
}
}
**
** Fit popup with current window bounds. This may move the origin of
** where popup is opened, or modify the width or height, or both.
**
** This method is called automatically by `open`. For content that
** is asynchronusly loaded after popup is visible, and that may modify
** the initial size, it is good practice to invoke this method to
** verify content does not overflow the viewport.
**
** If popup is not open, this method does nothing.
**
Void fitBounds()
{
// isOpen may not be set yet, so check if mounted.
if (this.parent == null) return
x := openPos.x
y := openPos.y
sz := this.size
// shift halign if needed
switch (halign)
{
case Align.center: x = gutter.max(x - (sz.w.toInt / 2)); this.style->left = "${x}px"
case Align.right: x = gutter.max(x - sz.w.toInt); this.style->left = "${x}px"
}
// adjust if outside viewport
vp := Win.cur.viewport
if (sz.w + gutter + gutter > vp.w) this.style->width = "${vp.w-gutter-gutter}px"
if (sz.h + gutter + gutter > vp.h) this.style->height = "${vp.h-gutter-gutter}px"
// refresh size
sz = this.size
if ((x + sz.w + gutter) > vp.w) this.style->left = "${vp.w-sz.w-gutter}px"
if ((y + sz.h + gutter) > vp.h) this.style->top = "${vp.h-sz.h-gutter}px"
}
** Protected sub-class callback invoked directly before popup is visible.
protected virtual Void onBeforeOpen() {}
** Callback when popup is opened.
Void onOpen(|This| f) { cbOpen = f }
** Callback when popup is closed.
Void onClose(|This| f) { cbClose = f }
** Internal callback when popup is closed.
internal Void _onClose(|This| f) { _cbClose = f }
private Void fireOpen(Event? e) { cbOpen?.call(this); isOpen=true }
private Void fireClose(Event? e)
{
_cbClose?.call(this)
cbClose?.call(this)
isOpen = false
}
private const Int uid
private static const AtomicInt nextId := AtomicInt(0)
private static const Float gutter := 12f
private Point? openPos
private Func? cbOpen
private Func? cbClose
private Func? _cbClose
}