michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * ContentAreaObserver manages tracking the viewable area within the browser. michael@0: * It also handles certain tasks like positioning of input elements within michael@0: * content when the viewable area changes. michael@0: * michael@0: * ContentAreaObserver creates styles that content can apply and also fires michael@0: * events when things change. The 'width' and 'height' properties of the michael@0: * styles are updated whenever various parts of the UI are resized. michael@0: * michael@0: * styles: window-width, window-height michael@0: * events: SizeChanged michael@0: * michael@0: * The innerWidth/innerHeight of the main chrome window. michael@0: * michael@0: * styles: content-width, content-height michael@0: * events: ContentSizeChanged michael@0: * michael@0: * The area of the window dedicated to web content; this is equal to the michael@0: * innerWidth/Height minus any toolbars or similar chrome. michael@0: * michael@0: * styles: viewable-width, viewable-height michael@0: * events: ViewableSizeChanged michael@0: * michael@0: * The portion of the content area that is not obscured by the on-screen michael@0: * keyboard. michael@0: * michael@0: * ContentAreaObserver also manages soft keyboard related events. Events michael@0: * fired include: michael@0: * michael@0: * KeyboardChanged - fired after the visibility of the soft keyboard changes michael@0: * and after any view related changes are made to accomidate view dim michael@0: * changes as a result. michael@0: * michael@0: * MozDeckOffsetChanging, MozDeckOffsetChanged - interim events fired when michael@0: * the browser deck is being repositioned to move focused elements out of michael@0: * the way of the keyboard. michael@0: */ michael@0: michael@0: var ContentAreaObserver = { michael@0: styles: {}, michael@0: _shiftAmount: 0, michael@0: _deckTransitioning: false, michael@0: michael@0: /* michael@0: * Properties michael@0: */ michael@0: michael@0: get width() { michael@0: return window.innerWidth || 1366; michael@0: }, michael@0: michael@0: get height() { michael@0: return window.innerHeight || 768; michael@0: }, michael@0: michael@0: get contentHeight() { michael@0: return this._getContentHeightForWindow(this.height); michael@0: }, michael@0: michael@0: get viewableHeight() { michael@0: return this._getViewableHeightForContent(this.contentHeight); michael@0: }, michael@0: michael@0: get isKeyboardOpened() { michael@0: return Services.metro.keyboardVisible; michael@0: }, michael@0: michael@0: get isKeyboardTransitioning() { michael@0: return this._deckTransitioning; michael@0: }, michael@0: michael@0: get viewstate() { michael@0: if (this.width < Services.prefs.getIntPref("browser.ui.snapped.maxWidth")) { michael@0: return "snapped"; michael@0: } michael@0: return (this.height > this.width) ? "portrait" : "landscape"; michael@0: }, michael@0: michael@0: /* michael@0: * Public apis michael@0: */ michael@0: michael@0: init: function init() { michael@0: window.addEventListener("resize", this, false); michael@0: michael@0: // Message manager msgs we listen for michael@0: messageManager.addMessageListener("Content:RepositionInfoResponse", this); michael@0: michael@0: // Observer msgs we listen for michael@0: Services.obs.addObserver(this, "metro_softkeyboard_shown", false); michael@0: Services.obs.addObserver(this, "metro_softkeyboard_hidden", false); michael@0: michael@0: // setup initial values for browser form repositioning michael@0: this.shiftBrowserDeck(0); michael@0: michael@0: // initialize our custom width and height styles michael@0: this._initStyles(); michael@0: michael@0: // apply default styling michael@0: this.update(); michael@0: }, michael@0: michael@0: shutdown: function shutdown() { michael@0: messageManager.removeMessageListener("Content:RepositionInfoResponse", this); michael@0: Services.obs.removeObserver(this, "metro_softkeyboard_shown"); michael@0: Services.obs.removeObserver(this, "metro_softkeyboard_hidden"); michael@0: }, michael@0: michael@0: update: function cao_update (width, height) { michael@0: let oldWidth = parseInt(this.styles["window-width"].width); michael@0: let oldHeight = parseInt(this.styles["window-height"].height); michael@0: michael@0: let newWidth = width || this.width; michael@0: let newHeight = height || this.height; michael@0: michael@0: if (newHeight == oldHeight && newWidth == oldWidth) { michael@0: return; michael@0: } michael@0: michael@0: this.styles["window-width"].width = newWidth + "px"; michael@0: this.styles["window-width"].maxWidth = newWidth + "px"; michael@0: this.styles["window-height"].height = newHeight + "px"; michael@0: this.styles["window-height"].maxHeight = newHeight + "px"; michael@0: michael@0: this._updateViewState(); michael@0: michael@0: this.updateContentArea(newWidth, this._getContentHeightForWindow(newHeight)); michael@0: this._dispatchBrowserEvent("SizeChanged"); michael@0: }, michael@0: michael@0: updateContentArea: function cao_updateContentArea (width, height) { michael@0: let oldHeight = parseInt(this.styles["content-height"].height); michael@0: let oldWidth = parseInt(this.styles["content-width"].width); michael@0: michael@0: let newWidth = width || this.width; michael@0: let newHeight = height || this.contentHeight; michael@0: michael@0: if (newHeight == oldHeight && newWidth == oldWidth) { michael@0: return; michael@0: } michael@0: michael@0: this.styles["content-height"].height = newHeight + "px"; michael@0: this.styles["content-height"].maxHeight = newHeight + "px"; michael@0: this.styles["content-width"].width = newWidth + "px"; michael@0: this.styles["content-width"].maxWidth = newWidth + "px"; michael@0: michael@0: this.updateViewableArea(newWidth, this._getViewableHeightForContent(newHeight)); michael@0: this._dispatchBrowserEvent("ContentSizeChanged"); michael@0: }, michael@0: michael@0: updateViewableArea: function cao_updateViewableArea (width, height) { michael@0: let oldHeight = parseInt(this.styles["viewable-height"].height); michael@0: let oldWidth = parseInt(this.styles["viewable-width"].width); michael@0: michael@0: let newWidth = width || this.width; michael@0: let newHeight = height || this.viewableHeight; michael@0: michael@0: if (newHeight == oldHeight && newWidth == oldWidth) { michael@0: return; michael@0: } michael@0: michael@0: this.styles["viewable-height"].height = newHeight + "px"; michael@0: this.styles["viewable-height"].maxHeight = newHeight + "px"; michael@0: this.styles["viewable-width"].width = newWidth + "px"; michael@0: this.styles["viewable-width"].maxWidth = newWidth + "px"; michael@0: michael@0: this.updateAppBarPosition(); michael@0: michael@0: // Update the back/tab button states. If the keyboard is up michael@0: // these are hidden. michael@0: BrowserUI._updateButtons(); michael@0: michael@0: this._dispatchBrowserEvent("ViewableSizeChanged"); michael@0: }, michael@0: michael@0: updateAppBarPosition: function updateAppBarPosition(aForceDown) { michael@0: // Adjust the app and find bar position above the soft keyboard michael@0: let keyboardHeight = aForceDown ? 0 : Services.metro.keyboardHeight; michael@0: Elements.navbar.style.bottom = keyboardHeight + "px"; michael@0: Elements.contextappbar.style.bottom = keyboardHeight + "px"; michael@0: Elements.findbar.style.bottom = keyboardHeight + "px"; michael@0: }, michael@0: michael@0: /* michael@0: * Called by BrowserUI right before we blur the nav bar edit. We use michael@0: * this to get a head start on shuffling app bars around before the michael@0: * soft keyboard transitions down. michael@0: */ michael@0: navBarWillBlur: function navBarWillBlur() { michael@0: this.updateAppBarPosition(true); michael@0: }, michael@0: michael@0: onBrowserCreated: function onBrowserCreated(aBrowser) { michael@0: let notificationBox = aBrowser.parentNode.parentNode; michael@0: notificationBox.classList.add("content-width"); michael@0: notificationBox.classList.add("content-height"); michael@0: }, michael@0: michael@0: /* michael@0: * Event handling michael@0: */ michael@0: michael@0: _onKeyboardDisplayChanging: function _onKeyboardDisplayChanging(aNewState) { michael@0: if (aNewState) { michael@0: Elements.stack.setAttribute("keyboardVisible", true); michael@0: } else { michael@0: Elements.stack.removeAttribute("keyboardVisible"); michael@0: } michael@0: michael@0: this.updateViewableArea(); michael@0: michael@0: if (!aNewState) { michael@0: this.shiftBrowserDeck(0); michael@0: return; michael@0: } michael@0: michael@0: // Request info about the target form element to see if we michael@0: // need to reposition the browser above the keyboard. michael@0: if (SelectionHelperUI.layerMode === 2 /*kContentLayer*/) { michael@0: Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:RepositionInfoRequest", { michael@0: viewHeight: this.viewableHeight, michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: _onRepositionResponse: function _onRepositionResponse(aJsonMsg) { michael@0: if (!aJsonMsg.reposition || !this.isKeyboardOpened) { michael@0: this.shiftBrowserDeck(0); michael@0: return; michael@0: } michael@0: this.shiftBrowserDeck(aJsonMsg.raiseContent); michael@0: }, michael@0: michael@0: observe: function cao_observe(aSubject, aTopic, aData) { michael@0: // Note these are fired before the transition starts. Also per MS specs michael@0: // we should not do anything "heavy" here. We have about 100ms before michael@0: // windows just ignores the event and starts the animation. michael@0: switch (aTopic) { michael@0: case "metro_softkeyboard_hidden": michael@0: case "metro_softkeyboard_shown": michael@0: // Mark that we are in a transition state. Auto-complete and other michael@0: // popups will wait for an event before displaying anything. michael@0: this._deckTransitioning = true; michael@0: // This also insures tap events are delivered before we fire michael@0: // this event. Form repositioning requires the selection handler michael@0: // be running before we send this. michael@0: let self = this; michael@0: let state = aTopic == "metro_softkeyboard_shown"; michael@0: Util.executeSoon(function () { michael@0: self._onKeyboardDisplayChanging(state); michael@0: }); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function cao_handleEvent(aEvent) { michael@0: switch (aEvent.type) { michael@0: case 'resize': michael@0: if (aEvent.target != window) michael@0: return; michael@0: ContentAreaObserver.update(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: receiveMessage: function sh_receiveMessage(aMessage) { michael@0: switch (aMessage.name) { michael@0: case "Content:RepositionInfoResponse": michael@0: this._onRepositionResponse(aMessage.json); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * Internal helpers michael@0: */ michael@0: michael@0: _updateViewState: function (aState) { michael@0: let oldViewstate = Elements.windowState.getAttribute("viewstate"); michael@0: let viewstate = aState || this.viewstate; michael@0: if (viewstate != oldViewstate) { michael@0: Elements.windowState.setAttribute("viewstate", viewstate); michael@0: Services.obs.notifyObservers(null, "metro_viewstate_changed", viewstate); michael@0: } michael@0: }, michael@0: michael@0: shiftBrowserDeck: function (aAmount) { michael@0: if (aAmount == 0) { michael@0: this._deckTransitioning = false; michael@0: this._dispatchWindowEvent("KeyboardChanged", this.isKeyboardOpened); michael@0: } michael@0: michael@0: if (this._shiftAmount == aAmount) michael@0: return; michael@0: michael@0: this._shiftAmount = aAmount; michael@0: this._dispatchWindowEvent("MozDeckOffsetChanging", aAmount); michael@0: michael@0: // Elements.browsers is the deck all browsers sit in michael@0: let self = this; michael@0: Elements.browsers.addEventListener("transitionend", function () { michael@0: Elements.browsers.removeEventListener("transitionend", arguments.callee, true); michael@0: self._deckTransitioning = false; michael@0: self._dispatchWindowEvent("MozDeckOffsetChanged", aAmount); michael@0: self._dispatchWindowEvent("KeyboardChanged", self.isKeyboardOpened); michael@0: }, true); michael@0: michael@0: // selectAtPoint bug 858471 michael@0: //Elements.browsers.style.transform = "translateY(" + (-1 * aAmount) + "px)"; michael@0: Elements.browsers.style.marginTop = "" + (-1 * aAmount) + "px"; michael@0: }, michael@0: michael@0: _dispatchWindowEvent: function _dispatchWindowEvent(aEventName, aDetail) { michael@0: let event = document.createEvent("UIEvents"); michael@0: event.initUIEvent(aEventName, true, false, window, aDetail); michael@0: window.dispatchEvent(event); michael@0: }, michael@0: michael@0: _dispatchBrowserEvent: function (aName, aDetail) { michael@0: setTimeout(function() { michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent(aName, true, false); michael@0: Elements.browsers.dispatchEvent(event); michael@0: }, 0); michael@0: }, michael@0: michael@0: _initStyles: function _initStyles() { michael@0: let stylesheet = document.styleSheets[0]; michael@0: for (let style of ["window-width", "window-height", michael@0: "content-height", "content-width", michael@0: "viewable-height", "viewable-width"]) { michael@0: let index = stylesheet.insertRule("." + style + " {}", stylesheet.cssRules.length); michael@0: this.styles[style] = stylesheet.cssRules[index].style; michael@0: } michael@0: }, michael@0: michael@0: _getContentHeightForWindow: function (windowHeight) { michael@0: return windowHeight; michael@0: }, michael@0: michael@0: _getViewableHeightForContent: function (contentHeight) { michael@0: let keyboardHeight = Services.metro.keyboardHeight; michael@0: return contentHeight - keyboardHeight; michael@0: }, michael@0: michael@0: _debugDumpDims: function _debugDumpDims() { michael@0: let width = parseInt(this.styles["window-width"].width); michael@0: let height = parseInt(this.styles["window-height"].height); michael@0: Util.dumpLn("window:", width, height); michael@0: width = parseInt(this.styles["content-width"].width); michael@0: height = parseInt(this.styles["content-height"].height); michael@0: Util.dumpLn("content:", width, height); michael@0: width = parseInt(this.styles["viewable-width"].width); michael@0: height = parseInt(this.styles["viewable-height"].height); michael@0: Util.dumpLn("viewable:", width, height); michael@0: } michael@0: };