TouchDevelop/rt/modaldialog.ts

670 строки
23 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev
{
export class ModalDialog
{
private overlay = div("modalOverlay");
private dialog = div("modalDialog");
private floating:HTMLElement;
private outerDialog:HTMLElement;
private savedKeyState = null;
private id = Random.uniqueId();
private logView: TDev.RT.AppLogView;
constructor()
{
this.floating = div("modalDialogInner", this.dialog);
this.outerDialog = div("modalDialogOuter", div("modalDialogMid", this.floating));
}
public opacity = 0.85;
public onDismiss:(m:ModalDialog)=>void = null;
public visible = false;
public canDismiss = true;
static current:ModalDialog;
static dismissCurrent()
{
if (ModalDialog.currentIsVisible()) {
ModalDialog.current.canDismiss = true;
ModalDialog.current.dismiss()
}
}
static currentIsVisible()
{
return ModalDialog.current && ModalDialog.current.visible;
}
public show()
{
this.showBare();
}
public addLog() {
if (!this.logView) {
this.logView = new TDev.RT.AppLogView();
this.logView.charts(false);
this.logView.search(false);
this.logView.reversed(true);
this.logView.attachLogEvents();
this.logView.element.style.fontSize = "0.7em";
this.add(this.logView.element);
this.setScroll();
this.stretchWide();
}
}
public addFirst(v:HTMLElement)
{
if (this.dialog.firstChild)
this.dialog.insertBefore(v, this.dialog.firstChild)
else this.add(v)
}
public empty()
{
this.dialog.setChildren([])
}
public add(v:any)
{
this.dialog.appendChildren([v]);
}
public addClass(c:string)
{
this.dialog.className += " " + c;
}
public addHTML(html:string) : HTMLElement
{
var b = div("wall-dialog-body");
Browser.setInnerHTML(b, html);
this.add(b);
return b;
}
public addOuter(v:any)
{
this.outerDialog.appendChildren([v]);
}
public stretchDown(e:HTMLElement, keep = 0)
{
this.outerDialog.className += " modalDialogOuterLong";
this.listheight = SizeMgr.windowHeight - e.offsetTop - (3 + keep) * SizeMgr.topFontSize;
e.style.height = this.listheight + "px";
}
public adjustlistsize() {
if (!this.list || !this.buttondiv)
return;
var howmuchover = this.dialog.scrollHeight - this.dialog.clientHeight;
if (howmuchover > 0) {
this.listheight = Math.max(10 * SizeMgr.topFontSize, this.listheight - howmuchover);
this.list.style.height = this.listheight + "px";
}
}
public stretchWide()
{
this.floating.style.width = 'calc(100% - 4em)';
this.dialog.style.width = '100%';
}
public showBare(what:HTMLElement = null)
{
Ticker.dbg("ModalDialog.showBare0");
if (ModalDialog.current && ModalDialog.current != this && !ModalDialog.current.canDismiss)
return;
this.overlay.style.opacity = this.opacity.toString();
this.overlay.withClick(() => { this.dismiss(); });
this.outerDialog.withClick(() => { this.dismiss() });
this.floating.withClick(() => { });
var root = elt("root");
Util.children(root).forEach(ch => ch.setAttribute("aria-hidden", "true"));
root.appendChildren([this.overlay, what || this.outerDialog]);
var btnsDiv:HTMLElement;
Util.children(this.dialog).forEach((e) => {
if (e.className == "wall-dialog-buttons")
btnsDiv = e;
if (e.withClick && !(<any>e).clickHandler && !(<any>e).onselectstart)
e.withClick(() => {})
});
if (!what) {
Util.showPopup(this.floating);
} else {
this.outerDialog = what;
Util.showPopup(what);
}
if (ModalDialog.current && ModalDialog.current != this) {
ModalDialog.dismissCurrent();
}
if (this.canDismiss)
Screen.pushModalHash("dialog-" + this.id, () => this.dismiss());
this.savedKeyState = KeyboardMgr.instance.saveState();
KeyboardMgr.instance.register("Esc", () => { this.dismiss(); return true });
this.visible = true;
ModalDialog.current = this;
if (btnsDiv) {
var btns = Util.children(btnsDiv).filter((e) => (<any>e).clickHandler);
if (btns.length == 1)
KeyboardMgr.instance.btnShortcut(btns[0], "Enter");
}
elt("root").setFlag("modal-visible", true);
Ticker.dbg("ModalDialog.showBare1");
}
public dismiss()
{
if (!this.canDismiss) {
Ticker.dbg("ModalDialog.dismiss - cannot");
return;
}
Ticker.dbg("ModalDialog.dismiss0");
Screen.popModalHash("dialog-" + this.id);
if (this.logView) this.logView.removeLogEvents();
this.visible = false;
elt("root").setFlag("modal-visible", false);
KeyboardMgr.instance.loadState(this.savedKeyState);
Util.hidePopup(this.outerDialog, () => {
Ticker.dbg("ModalDialog.dismiss1");
this.outerDialog.removeSelf();
Util.children(elt("root")).forEach(ch => ch.removeAttribute("aria-hidden"));
});
Util.fadeOut(this.overlay);
var f = this.onDismiss
this.onDismiss = null
if (f) f(this);
}
static ask(msg:string, confirmation:string, act:()=>void)
{
var m = new ModalDialog();
m.add([
div("wall-dialog-header", confirmation + "?"),
div("wall-dialog-body", msg),
div("wall-dialog-buttons",
HTML.mkButton(lf("cancel"), () => m.dismiss()),
HTML.mkButton(confirmation, () => { act(); m.dismiss(); }))
]);
m.show();
return m;
}
static askMany(header:string, msg:string, options:any)
{
var m = new ModalDialog();
m.add([
div("wall-dialog-header", header),
div("wall-dialog-body", msg),
div("wall-dialog-buttons",
Object.keys(options).map((k:string) =>
HTML.mkButton(k, () => { m.dismiss(); options[k](); })))
]);
m.show();
return m;
}
static askManyWithAdditionalElts(header: string, helpLink: any, msg: any, subHeader: string, subMsg: any, options: any, ...elts: any[]) {
var m = new ModalDialog();
m.add([
div("wall-dialog-header", header, helpLink),
div("wall-dialog-body", msg),
div("wall-dialog-header", subHeader),
div("wall-dialog-body", subMsg),
div("wall-dialog-buttons",
Object.keys(options).map((k: string) =>
HTML.mkButton(k, () => { m.dismiss(); options[k](); }))),
elts
]);
m.show();
return m;
}
static buttons(header: string, msg: string, subheader: string, submsg: string, ...buttons: any[]) {
var m = new ModalDialog();
m.add([
div("wall-dialog-header", header),
div("wall-dialog-body", msg),
subheader && div("wall-dialog-header", subheader),
submsg && div("wall-dialog-body", submsg),
div("wall-dialog-buttons", buttons)
]);
m.show();
return m;
}
static info(title:string, msg:string, btn : string = "ok")
{
var m = new ModalDialog();
m.add([
div("wall-dialog-header", title),
div("wall-dialog-body", msg)])
if (btn)
m.addOk(btn)
m.show();
return m;
}
static infoAsync(title:string, msg:string, btn : string = "ok")
{
var r = new PromiseInv()
var m = ModalDialog.info(title, msg, btn)
m.onDismiss = () => r.success(null)
return r
}
public addButtons(btns:any)
{
this.add(div("wall-dialog-buttons", Object.keys(btns).map(b => HTML.mkButton(b, btns[b]))))
}
public addOk(btn = lf("ok"), f:()=>void = null, cls = "", btns:any = [])
{
var b = HTML.mkButton(btn, () => {
if (f) f();
else this.dismiss();
});
if (cls) b.className += " " + cls;
this.add(div("wall-dialog-buttons", [b, btns]))
return b;
}
public critical() {
this.floating.classList.add('bg-critical');
}
static showText(s:string, title:string = null, msg:string = null, done : () => void = null) : ModalDialog
{
var m = new ModalDialog();
if (title != null)
m.add(div('wall-dialog-header', title));
if (msg != null)
m.add(div('wall-dialog-body', msg));
var elt = HTML.mkTextArea("scriptText");
elt.value = s;
m.dialog.appendChild(elt);
(<any>m).textArea = elt;
m.onDismiss = done;
m.show();
m.stretchDown(elt);
m.stretchWide();
try {
elt.setSelectionRange(0, s.length);
} catch (e) { }
return m;
}
static showTable(headers: string[], values: string[][], ondblclick: (md: ModalDialog, idx: number) => any) : ModalDialog
{
var m = new ModalDialog();
var table = <HTMLTableElement>document.createElement("table");
table.className = "traces";
var hdrRow = <HTMLTableRowElement>document.createElement("tr");
for (var i = 0; i < headers.length; i++) {
var hdr = <HTMLTableHeaderCellElement>document.createElement("th");
hdr.textContent = headers[i];
hdrRow.appendChild(hdr);
}
table.appendChild(hdrRow);
for (var i = 0; i < values.length; i++) {
var row = <HTMLTableRowElement>document.createElement("tr");
row.classList.add("hover");
var rowValues = values[i];
for (var j = 0; j < rowValues.length; j++) {
var v = <HTMLTableCellElement>document.createElement("td");
v.textContent = rowValues[j];
v.ondblclick = function(ev) {
var target:any = ev.target || ev.srcElement;
return ondblclick(m, target.parentElement.sectionRowIndex - 1);
}
row.appendChild(v);
}
table.appendChild(row);
}
m.dialog.appendChild(table);
m.stretchDown(table);
m.show();
return m;
}
public noChrome()
{
this.outerDialog.className += " modalNoChrome";
}
public fullScreen(iMeanIt = false)
{
this.outerDialog.className += " modalNoChrome " + (iMeanIt ? "reallyFullScreen" : "modalFullScreen");
this.outerDialog.setChildren(this.dialog);
}
public fullWhite()
{
this.outerDialog.className += " modalFullWhite";
}
public fullYellow() {
this.outerDialog.className += " modalFullYellow";
}
public setScroll()
{
this.dialog.style.maxHeight = (SizeMgr.windowHeight * 0.8) / SizeMgr.topFontSize + "em";
Util.setupDragToScroll(this.dialog);
}
private list: HTMLElement;
private listheight: number;
private buttondiv: HTMLElement;
private searchbox: HTMLElement;
public showorhidelist(show: boolean) {
var d = show ? "" : "none";
if (this.searchbox)
this.searchbox.style.display = d;
if (this.list)
this.list.style.display = d;
}
// queryAsync returns a promise that returns HTMLElement[]
public choose(boxes:HTMLElement[], options : ModalChooseOptions = {})
{
var progressBar = HTML.mkProgressBar();
var list = this.list = HTML.mkModalList([]);
var search = HTML.mkTextInput("text", lf("choose..."));
search.id = "chooseSearch"
search.placeholder = options.searchHint || (options.queryAsync ? lf("Type to search online...") : lf("Type to search..."));
var autoKeyboard = KeyboardAutoUpdate.mkInput(search, Util.catchErrors("chooseSearch", () => refresh(true)));
search.onkeyup = (ev: KeyboardEvent) => { autoKeyboard.keypress(); }
var limitedMode = !!options.mkSeeMore;
if (limitedMode && boxes.every((b) => !(<any>b).initiallyHidden))
limitedMode = false;
var needKbd = false;
function refresh(onlineOK:boolean) {
var allTerms = search.value;
var terms = allTerms.split(/\s+/).map((s:string) => s.toLowerCase()).filter((s) => s != "");
var res = []
var ids = {}
if (terms.length > 0) limitedMode = false;
var skipCnt = 0;
boxes.forEach((b:HTMLElement) => {
var miss = false;
if (limitedMode && (<any>b).initiallyHidden) {
skipCnt++;
return;
}
var cont = b.textContent.toLowerCase();
terms.forEach((term) => {
if (!miss && cont.indexOf(term) < 0) miss = true;
})
if (!miss) {
ids[cont] = 1;
res.push(b);
Util.highlightWords(b, terms, true);
}
});
if (limitedMode) {
var b = options.mkSeeMore(needKbd ? lf("you can also search") : lf("there is {0} more option{0:s}", skipCnt))
res.push(b.withClick(() => {
limitedMode = false;
refresh(options.initialEmptyQuery);
}));
}
list.setChildren(res);
if (onlineOK && !!options.queryAsync && Cloud.isOnline()) {
progressBar.start();
options.queryAsync(allTerms).then((bxs : HTMLElement[]) => {
progressBar.stop();
if (!autoKeyboard.resultsCurrent(allTerms)) {
return;
}
if (!!bxs) {
bxs.forEach((bx) => {
var bkey = bx.textContent.toLowerCase();
if(!ids[bkey]) {
ids[bkey] = 1;
list.appendChild(bx);
Util.highlightWords(bx, terms, true);
}
});
}
}).done();
}
}
if (!options.noBackground)
this.outerDialog.className += " modalChooser";
if (options.header) {
this.add(div("modalSearchHeader", options.header));
}
if (options.includeSearch !== undefined)
needKbd = options.includeSearch;
else
needKbd = (!!options.queryAsync || boxes.length > 5)
if (needKbd)
this.add(this.searchbox = div("modalSearch", [<HTMLElement>progressBar, search]));
this.add(list);
this.buttondiv = div('wall-dialog-buttons');
if (options.custombuttons !== undefined)
this.buttondiv.appendChildren(options.custombuttons);
else
this.buttondiv.appendChild(HTML.mkButtonTick(lf("cancel"), Ticks.chooseCancel, () => this.dismiss()));
this.add(this.buttondiv);
this.show();
// this has to happen after show() - show() saves the keyboard state so later this handler is removed
if (needKbd)
KeyboardMgr.instance.register("***", (e:KeyboardEvent) => {
if (e.fromTextBox) return false;
var s = Util.keyEventString(e);
if (s) {
search.value += s;
Util.setKeyboardFocus(search);
return true;
}
return false;
});
if (!options.dontStretchDown)
this.stretchDown(list, 2.8);
if (options.adjustListSize)
this.adjustlistsize();
if (options.initialQuery !== undefined) {
options.initialEmptyQuery = true
search.value = options.initialQuery
Util.setKeyboardFocus(search, true)
}
refresh(options.initialEmptyQuery);
}
}
export interface ModalChooseOptions
{
queryAsync?: (terms: string) => Promise;
searchHint?: string;
initialQuery?: string;
initialEmptyQuery?: boolean;
header?: any;
mkSeeMore?: (lbl: string) => HTMLElement;
includeSearch?: boolean; // overrides default of 6+ items
dontStretchDown?: boolean;
adjustListSize?: boolean;
noBackground?: boolean;
custombuttons?: HTMLElement[];
}
export module ProgressOverlay
{
var overlay:HTMLElement;
var msgDiv:HTMLElement;
var addInfo:HTMLElement;
var progress:HTMLElement;
var visible = 0;
var unblockedKeyboard = false;
var logView : TDev.RT.AppLogView;
export var lock:PromiseInv = PromiseInv.as();
export function lockAndShow(msg = lf("working hard"), f?:() => void, splashUrl?:string)
{
lock.done(() => show(msg, f, splashUrl))
}
export function lockAndShowAsync(msg:string)
{
var r = new PromiseInv()
lockAndShow(msg, () => r.success(null));
return r
}
export function bumpShow()
{
Util.assert(isActive())
visible++;
}
export function show(msg = lf("working hard"), f?:() => void, splashUrl? : string)
{
visible++;
Ticker.dbg("ProgressOverlay.show " + visible + " " + msg);
if (visible > 1) {
setMessage(msg);
return;
}
if (!overlay) {
overlay =
div("modalOverlay", div("modalMessage",
msgDiv = div("modalMessageHeader"),
addInfo = div("modalMessagePleaseWait"),
progress = div("modalMessagePleaseWait")
));
overlay.withClick(() => {});
overlay.style.backgroundColor = "white";
overlay.style.opacity = "0.95";
overlay.style.backgroundSize = "cover";
overlay.style.backgroundRepeat = "no-repeat";
}
lock = new PromiseInv();
setMessage(msg);
setSplashArtId(splashUrl);
addInfo.setChildren([lf("please wait...")]);
closeLog();
//Util.cancelAnim(overlay);
overlay.removeSelf();
elt("root").appendChildren([overlay]);
if (f)
// webkit seems to optimize the thing away if we don't give it enough time
Util.setTimeout(Browser.isWebkit ? 100 : 1, f);
}
export function setAddInfo(info:any[])
{
addInfo.setChildren(info);
}
export function hide()
{
if (visible == 0) {
// certain code path call setMessage but don't actually display the overlay
// thus this check fails in the compiled apps
// Util.check(false, "too many hides()");
return;
}
visible--;
Ticker.dbg("ProgressOverlay.hide " + visible);
if (visible == 0) {
lock.success(null);
progress.setChildren([]);
overlay.removeSelf();
unblockedKeyboard = false
closeLog();
}
}
function closeLog() {
if (logView) {
logView.element.removeSelf();
logView.removeLogEvents();
logView = undefined;
}
}
export function setProgress(msg:string)
{
if (progress)
progress.setChildren(msg);
}
export function setMessage(msg:string)
{
if (msgDiv)
msgDiv.setChildren(msg);
}
export function setSplashArtId(url: string) {
if (overlay) overlay.style.backgroundImage = HTML.cssImage(url, 0.3);
}
export function unblockKeyboard() { unblockedKeyboard = true }
export function isKeyboardBlocked() { return isActive() && !unblockedKeyboard; }
export function showLog() {
if (!logView) {
logView = new TDev.RT.AppLogView();
logView.charts(false);
logView.search(false);
logView.reversed(true);
logView.attachLogEvents();
logView.element.style.fontSize = "0.7em";
progress.parentElement.appendChild(logView.element);
}
}
export function isActive() { return visible > 0; }
}
}