TouchDevelop/rt/keyboardMgr.ts

496 строки
14 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev {
export var currentScreen: Screen = null;
export var allScreens: Screen[] = [];
interface HistoryEntry {
url: string;
onPop: () => void;
}
export class HistoryMgr
{
private lastSetHash: string;
// hack to filter out hash change that wasn't according to us
private previousHash: string;
public replaceNext: boolean;
// seems to be used to determine if this is the first loadHash ever
public numReloads = 0;
static instance: HistoryMgr;
private urlStack : HistoryEntry[] = [];
constructor() {
this.lastSetHash = "";
this.replaceNext = false;
Screen.pushModalHash = (s, f) => this.pushModalHash(s, f);
Screen.popModalHash = (s) => this.popModalHash(s);
HistoryMgr.instance = this;
Util.onSetHash = (s, r) => this.setHashHandler(s, r);
Util.onGoBack = () => this.back();
}
/// Used for debugging.
public currentHash() { return this.lastSetHash; }
static urlHash(url: string): string {
var i = url.indexOf('#')
if (i > 0 && i < url.length - 1)
return url.slice(i);
else
return "#hub";
}
static windowHash() {
var h = window.location.href;
return HistoryMgr.urlHash(h);
}
private pushState(h:string)
{
try {
Util.log("pushState: " + h)
h = window.location.href.replace(/#.*/, "") + (h || "")
window.history.pushState(++this.numPush, null, h)
} catch (e) {
// this doesn't work in embedded iframe
}
}
// Called to tell the history manager where we are
// Not expected to change the appearance of the UI
public setHash(h: string, t: string) {
Ticker.dbg("History.setHash|" + h);
var repl = this.replaceNext;
this.replaceNext = false;
if (repl) {
Ticker.dbg("History.setHash is a replace");
}
this.numReloads++;
if (!/^#/.test(h)) h = "#" + h;
if (h == "#hub") h = "#"
this.clearModalSuffix();
Screen.arrivedAtHash(h);
if (this.urlStack.length == 0 || this.urlStack.peek().url != h) {
this.urlStack.push({ url: h, onPop: () => { } });
this.pushState(h)
}
else {
Ticker.dbg("setHash.. ignoring same hash");
}
if (t !== null) {
if (t)
document.title = t + " - TouchDevelop";
else
document.title = "TouchDevelop";
}
}
private pushModalHash(s: string, f: () => void) {
var hash = '#modal-' + s;
if (this.urlStack.length > 0 && this.urlStack.peek().url == hash) {
this.urlStack.pop();
}
this.urlStack.push({ url: hash, onPop: f });
this.pushState(this.topHash())
Screen.arrivedAtHash(hash); // inform the WAB shell there is a modal dialog
}
private popModalHash(s: string) {
Ticker.dbg("popModal " + s);
var hash = '#modal-' + s;
var index = -1;
for (var i = 0; i < this.urlStack.length; i++) {
if (this.urlStack[i].url == hash) {
index = i;
this.urlStack[i].onPop = null;
break;
}
}
if (index < 0) return;
this.popBack(this.urlStack.length - index)
}
private popBack(toPop = 1) {
Ticker.dbg("historyMgr: popBack " + toPop);
var popped = this.urlStack.splice(this.urlStack.length - toPop, toPop);
var seenNonModal = false
for (var i = popped.length - 1; i >= 0; i--) {
var entry = popped[i];
if (!/#modal-/.test(entry.url))
seenNonModal = true
if (entry.onPop) {
var f = entry.onPop;
entry.onPop = null;
f();
}
}
// inform the WAB shell whether the current url is the top page
var last = this.urlStack.peek()
Screen.arrivedAtHash(last ? last.url : "#")
if (seenNonModal)
this.dispatch(this.topHash())
else
this.hashReloaded()
}
public hashReloaded()
{
this.pushState(this.topHash())
}
private topHash()
{
for (var i = this.urlStack.length - 1; i >= 0; i--) {
var t = this.urlStack[i]
if (t && !/^#modal-/.test(t.url)) return t.url
}
return "#"
}
private dispatch(h:string)
{
Util.log("dispatch: " + h)
ModalDialog.dismissCurrent()
this.pushState(h)
this.reload(h)
}
public initialHash()
{
this.setHashHandler(HistoryMgr.windowHash(), true);
if (this.lastSetHash == "" && !this.replaceNext) {
// make sure we get a pop event
this.pushState(null)
this.replaceNext = true;
this.showStartScreen();
}
}
public showStartScreen()
{
}
public reload(hash:string)
{
}
public confirmLoadHash()
{
Ticker.dbg("History.confirmLoadHash");
this.replaceNext = false;
}
public clearModalSuffix(): number {
for (var i = 0; i < this.urlStack.length; i++) {
if (/^#modal-/.test(this.urlStack[i].url)) {
var removed = this.urlStack.splice(i, this.urlStack.length - i);
return removed.length;
}
}
return 0;
}
public clearModalStack()
{
var n = this.clearModalSuffix();
// this.popBack(n)
}
private numPush = 1;
public popState(event:any) {
Ticker.dbg("popState: " + event.state);
// exiting the app?
if (this.urlStack.length == 1) {
window.history.back()
return
}
this.back();
}
public back()
{
this.whenSafe(() => this.popBack())
}
private whenSafe(f:()=>void)
{
if (ProgressOverlay.isActive())
Util.setTimeout(500, () => this.whenSafe(f))
else
f();
}
private setHashHandler(h:string, replace:boolean)
{
this.whenSafe(() => {
if (!h) h = "#"
if (!/^#/.test(h)) h = "#" + h
this.dispatch(h)
})
}
public scriptOrHub(h:string[])
{
var id = h.filter((s) => /^id=/.test(s))[0]
if (id) {
// HTML.showProgressNotification("script not installed yet");
var scr = id.replace(/^id=/, "script:")
if (h[0] == "list")
Util.setHash(h[0] + ":" + h[1] + ":" + scr, true)
else
Util.setHash(scr, true)
} else {
HTML.showErrorNotification("cannot load requested script");
Util.setHash("hub", true)
}
}
public hashChange()
{
Util.log("hashChange: " + HistoryMgr.windowHash())
}
}
export class Screen
{
public init() {}
public hide() {}
public screenId() { return ""; }
public loadHash(h:string[]) {}
public keyDown(e:KeyboardEvent):boolean { return false; }
public applySizes() {}
static pushModalHash = (s:string, removeCb:()=>void) => {};
static popModalHash = (s: string) => { };
static arrivedAtHash = (s:string) => {};
public syncDone() {}
public hashReloaded() {}
private paneState = 0;
public autoHide() { return SizeMgr.portraitMode; }
public sidePaneVisible() { return !this.autoHide() || this.sidePane().style.display == "block"; }
public sidePaneVisibleNow() { return !this.autoHide() || this.paneState > 0; }
public showSidePane()
{
if (!this.autoHide() || this.paneState > 0) return;
Screen.pushModalHash("side", () => this.hideSidePane());
elt("root").setFlag("pane-hidden", false);
this.paneState = 1;
var pane = this.sidePane();
pane.style.display = "block";
pane.style.opacity = "1";
Util.showRightPanel(pane);
}
public sidePane():HTMLElement { return null; }
public hideSidePane()
{
if (!this.autoHide() || this.paneState < 0) return;
Screen.popModalHash("side");
elt("root").setFlag("pane-hidden", true);
var pane = this.sidePane();
this.paneState = -1;
Util.hidePopup(pane, () => {
if (this.paneState < 0 && this.autoHide())
pane.style.display = "none";
});
}
}
export class KeyboardMgr
{
static instance = new KeyboardMgr();
private handlers:any = {};
public register(key:string, cb:(e:KeyboardEvent)=>boolean)
{
if (/^-/.test(key)) return;
this.handlers[key] = cb;
}
public saveState()
{
return Util.flatClone(this.handlers);
}
public loadState(s:any)
{
if (!Util.check(!!s)) return;
this.handlers = s;
}
public triggerKey(name:string)
{
var h = this.handlers[name];
if (h && h.isBtnShortcut)
h();
}
static triggerClick(e:HTMLElement)
{
var h = <ClickHandler>(<any>e).clickHandler;
if (!!h) {
var active = <HTMLElement> document.activeElement;
if (!!active && !!active.blur) active.blur();
e.setFlag("active", true);
Util.setTimeout(150, () => { e.setFlag("active", false) });
h.fireClick(<any>{});
}
}
static elementVisible(e:HTMLElement)
{
while (e) {
if (e.id == "root") return true;
if (window.getComputedStyle(e).display == "none") return false;
e = <HTMLElement> e.parentNode;
}
return false;
}
public btnShortcut(e:HTMLElement, key:string)
{
var handle = () => {
if (KeyboardMgr.elementVisible(e)) {
KeyboardMgr.triggerClick(e);
return true;
}
return false;
}
if (!key) return;
(<any>handle).isBtnShortcut = true;
if (!/^ /.test(key))
key.split(", ").forEach((k) => this.register(k, handle));
e.title = key.replace(/(^| )-/g, "");
}
private previousStoppedEvent:KeyboardEvent;
public keyUp(e:KeyboardEvent) : any
{
Util.normalizeKeyEvent(e);
if (/-(Control|Alt)$/.test(e.keyName))
return true;
var h = this.handlers["*keyup*"];
if (h) {
Ticker.dbg("keyUp.run");
if (h(e)) return true;
}
}
public processKey(e:KeyboardEvent) : any
{
Util.normalizeKeyEvent(e);
if (/-(Control|Alt)$/.test(e.keyName))
return true;
if (Browser.isGecko) {
var isRepeated =
e.type == "keypress" &&
this.previousStoppedEvent &&
this.previousStoppedEvent.type == "keydown";
this.previousStoppedEvent = null;
if (isRepeated)
return e.stopIt();
}
if (this.keyHandler(e)) {
if (Browser.isGecko)
this.previousStoppedEvent = e;
return e.stopIt();
}
}
public attach(inp:HTMLInputElement)
{
inp.onkeydown = Util.catchErrors("textboxKey", (e:KeyboardEvent) => {
e.fromTextBox = true;
return this.processKey(e);
});
}
private keyHandler(e:KeyboardEvent) : boolean
{
if (ProgressOverlay.isKeyboardBlocked()) return true;
if (e.keyName == "Ctrl-Control") return false;
if (/^(Del|Ctrl-[CXVA]|Shift-(Left|Right)|(Ctrl|Shift)-(Ins|Del))$/.test(e.keyName) && e.fromTextBox) return false;
if (TDev.dbg && e.keyName) tick(Ticks.mainKeyEvent)
h = this.handlers["*keydown*"];
if (h) {
if (TDev.dbg && e.keyName)
Ticker.dbg("keyHandler.preCatchAll." + e.keyName)
if (h(e)) return true;
}
var h = this.handlers[e.keyName];
if (h) {
if (TDev.dbg && e.keyName)
Ticker.dbg("keyHandler.byName." + e.keyName);
if (h(e)) return true;
}
h = this.handlers["***"];
if (h) {
if (TDev.dbg && e.keyName)
Ticker.dbg("keyHandler.catchAll." + e.keyName)
if (h(e)) return true;
}
if (currentScreen) {
if (TDev.dbg && e.keyName)
Ticker.dbg("keyHandler.currentScreen." + e.keyName)
if (currentScreen.keyDown(e)) return true;
}
if (e.keyCode == 8 && !e.fromTextBox)
return true;
return false;
}
}
}