// // 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 }