|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /* |
|
6 * ContentAreaObserver manages tracking the viewable area within the browser. |
|
7 * It also handles certain tasks like positioning of input elements within |
|
8 * content when the viewable area changes. |
|
9 * |
|
10 * ContentAreaObserver creates styles that content can apply and also fires |
|
11 * events when things change. The 'width' and 'height' properties of the |
|
12 * styles are updated whenever various parts of the UI are resized. |
|
13 * |
|
14 * styles: window-width, window-height |
|
15 * events: SizeChanged |
|
16 * |
|
17 * The innerWidth/innerHeight of the main chrome window. |
|
18 * |
|
19 * styles: content-width, content-height |
|
20 * events: ContentSizeChanged |
|
21 * |
|
22 * The area of the window dedicated to web content; this is equal to the |
|
23 * innerWidth/Height minus any toolbars or similar chrome. |
|
24 * |
|
25 * styles: viewable-width, viewable-height |
|
26 * events: ViewableSizeChanged |
|
27 * |
|
28 * The portion of the content area that is not obscured by the on-screen |
|
29 * keyboard. |
|
30 * |
|
31 * ContentAreaObserver also manages soft keyboard related events. Events |
|
32 * fired include: |
|
33 * |
|
34 * KeyboardChanged - fired after the visibility of the soft keyboard changes |
|
35 * and after any view related changes are made to accomidate view dim |
|
36 * changes as a result. |
|
37 * |
|
38 * MozDeckOffsetChanging, MozDeckOffsetChanged - interim events fired when |
|
39 * the browser deck is being repositioned to move focused elements out of |
|
40 * the way of the keyboard. |
|
41 */ |
|
42 |
|
43 var ContentAreaObserver = { |
|
44 styles: {}, |
|
45 _shiftAmount: 0, |
|
46 _deckTransitioning: false, |
|
47 |
|
48 /* |
|
49 * Properties |
|
50 */ |
|
51 |
|
52 get width() { |
|
53 return window.innerWidth || 1366; |
|
54 }, |
|
55 |
|
56 get height() { |
|
57 return window.innerHeight || 768; |
|
58 }, |
|
59 |
|
60 get contentHeight() { |
|
61 return this._getContentHeightForWindow(this.height); |
|
62 }, |
|
63 |
|
64 get viewableHeight() { |
|
65 return this._getViewableHeightForContent(this.contentHeight); |
|
66 }, |
|
67 |
|
68 get isKeyboardOpened() { |
|
69 return Services.metro.keyboardVisible; |
|
70 }, |
|
71 |
|
72 get isKeyboardTransitioning() { |
|
73 return this._deckTransitioning; |
|
74 }, |
|
75 |
|
76 get viewstate() { |
|
77 if (this.width < Services.prefs.getIntPref("browser.ui.snapped.maxWidth")) { |
|
78 return "snapped"; |
|
79 } |
|
80 return (this.height > this.width) ? "portrait" : "landscape"; |
|
81 }, |
|
82 |
|
83 /* |
|
84 * Public apis |
|
85 */ |
|
86 |
|
87 init: function init() { |
|
88 window.addEventListener("resize", this, false); |
|
89 |
|
90 // Message manager msgs we listen for |
|
91 messageManager.addMessageListener("Content:RepositionInfoResponse", this); |
|
92 |
|
93 // Observer msgs we listen for |
|
94 Services.obs.addObserver(this, "metro_softkeyboard_shown", false); |
|
95 Services.obs.addObserver(this, "metro_softkeyboard_hidden", false); |
|
96 |
|
97 // setup initial values for browser form repositioning |
|
98 this.shiftBrowserDeck(0); |
|
99 |
|
100 // initialize our custom width and height styles |
|
101 this._initStyles(); |
|
102 |
|
103 // apply default styling |
|
104 this.update(); |
|
105 }, |
|
106 |
|
107 shutdown: function shutdown() { |
|
108 messageManager.removeMessageListener("Content:RepositionInfoResponse", this); |
|
109 Services.obs.removeObserver(this, "metro_softkeyboard_shown"); |
|
110 Services.obs.removeObserver(this, "metro_softkeyboard_hidden"); |
|
111 }, |
|
112 |
|
113 update: function cao_update (width, height) { |
|
114 let oldWidth = parseInt(this.styles["window-width"].width); |
|
115 let oldHeight = parseInt(this.styles["window-height"].height); |
|
116 |
|
117 let newWidth = width || this.width; |
|
118 let newHeight = height || this.height; |
|
119 |
|
120 if (newHeight == oldHeight && newWidth == oldWidth) { |
|
121 return; |
|
122 } |
|
123 |
|
124 this.styles["window-width"].width = newWidth + "px"; |
|
125 this.styles["window-width"].maxWidth = newWidth + "px"; |
|
126 this.styles["window-height"].height = newHeight + "px"; |
|
127 this.styles["window-height"].maxHeight = newHeight + "px"; |
|
128 |
|
129 this._updateViewState(); |
|
130 |
|
131 this.updateContentArea(newWidth, this._getContentHeightForWindow(newHeight)); |
|
132 this._dispatchBrowserEvent("SizeChanged"); |
|
133 }, |
|
134 |
|
135 updateContentArea: function cao_updateContentArea (width, height) { |
|
136 let oldHeight = parseInt(this.styles["content-height"].height); |
|
137 let oldWidth = parseInt(this.styles["content-width"].width); |
|
138 |
|
139 let newWidth = width || this.width; |
|
140 let newHeight = height || this.contentHeight; |
|
141 |
|
142 if (newHeight == oldHeight && newWidth == oldWidth) { |
|
143 return; |
|
144 } |
|
145 |
|
146 this.styles["content-height"].height = newHeight + "px"; |
|
147 this.styles["content-height"].maxHeight = newHeight + "px"; |
|
148 this.styles["content-width"].width = newWidth + "px"; |
|
149 this.styles["content-width"].maxWidth = newWidth + "px"; |
|
150 |
|
151 this.updateViewableArea(newWidth, this._getViewableHeightForContent(newHeight)); |
|
152 this._dispatchBrowserEvent("ContentSizeChanged"); |
|
153 }, |
|
154 |
|
155 updateViewableArea: function cao_updateViewableArea (width, height) { |
|
156 let oldHeight = parseInt(this.styles["viewable-height"].height); |
|
157 let oldWidth = parseInt(this.styles["viewable-width"].width); |
|
158 |
|
159 let newWidth = width || this.width; |
|
160 let newHeight = height || this.viewableHeight; |
|
161 |
|
162 if (newHeight == oldHeight && newWidth == oldWidth) { |
|
163 return; |
|
164 } |
|
165 |
|
166 this.styles["viewable-height"].height = newHeight + "px"; |
|
167 this.styles["viewable-height"].maxHeight = newHeight + "px"; |
|
168 this.styles["viewable-width"].width = newWidth + "px"; |
|
169 this.styles["viewable-width"].maxWidth = newWidth + "px"; |
|
170 |
|
171 this.updateAppBarPosition(); |
|
172 |
|
173 // Update the back/tab button states. If the keyboard is up |
|
174 // these are hidden. |
|
175 BrowserUI._updateButtons(); |
|
176 |
|
177 this._dispatchBrowserEvent("ViewableSizeChanged"); |
|
178 }, |
|
179 |
|
180 updateAppBarPosition: function updateAppBarPosition(aForceDown) { |
|
181 // Adjust the app and find bar position above the soft keyboard |
|
182 let keyboardHeight = aForceDown ? 0 : Services.metro.keyboardHeight; |
|
183 Elements.navbar.style.bottom = keyboardHeight + "px"; |
|
184 Elements.contextappbar.style.bottom = keyboardHeight + "px"; |
|
185 Elements.findbar.style.bottom = keyboardHeight + "px"; |
|
186 }, |
|
187 |
|
188 /* |
|
189 * Called by BrowserUI right before we blur the nav bar edit. We use |
|
190 * this to get a head start on shuffling app bars around before the |
|
191 * soft keyboard transitions down. |
|
192 */ |
|
193 navBarWillBlur: function navBarWillBlur() { |
|
194 this.updateAppBarPosition(true); |
|
195 }, |
|
196 |
|
197 onBrowserCreated: function onBrowserCreated(aBrowser) { |
|
198 let notificationBox = aBrowser.parentNode.parentNode; |
|
199 notificationBox.classList.add("content-width"); |
|
200 notificationBox.classList.add("content-height"); |
|
201 }, |
|
202 |
|
203 /* |
|
204 * Event handling |
|
205 */ |
|
206 |
|
207 _onKeyboardDisplayChanging: function _onKeyboardDisplayChanging(aNewState) { |
|
208 if (aNewState) { |
|
209 Elements.stack.setAttribute("keyboardVisible", true); |
|
210 } else { |
|
211 Elements.stack.removeAttribute("keyboardVisible"); |
|
212 } |
|
213 |
|
214 this.updateViewableArea(); |
|
215 |
|
216 if (!aNewState) { |
|
217 this.shiftBrowserDeck(0); |
|
218 return; |
|
219 } |
|
220 |
|
221 // Request info about the target form element to see if we |
|
222 // need to reposition the browser above the keyboard. |
|
223 if (SelectionHelperUI.layerMode === 2 /*kContentLayer*/) { |
|
224 Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:RepositionInfoRequest", { |
|
225 viewHeight: this.viewableHeight, |
|
226 }); |
|
227 } |
|
228 }, |
|
229 |
|
230 _onRepositionResponse: function _onRepositionResponse(aJsonMsg) { |
|
231 if (!aJsonMsg.reposition || !this.isKeyboardOpened) { |
|
232 this.shiftBrowserDeck(0); |
|
233 return; |
|
234 } |
|
235 this.shiftBrowserDeck(aJsonMsg.raiseContent); |
|
236 }, |
|
237 |
|
238 observe: function cao_observe(aSubject, aTopic, aData) { |
|
239 // Note these are fired before the transition starts. Also per MS specs |
|
240 // we should not do anything "heavy" here. We have about 100ms before |
|
241 // windows just ignores the event and starts the animation. |
|
242 switch (aTopic) { |
|
243 case "metro_softkeyboard_hidden": |
|
244 case "metro_softkeyboard_shown": |
|
245 // Mark that we are in a transition state. Auto-complete and other |
|
246 // popups will wait for an event before displaying anything. |
|
247 this._deckTransitioning = true; |
|
248 // This also insures tap events are delivered before we fire |
|
249 // this event. Form repositioning requires the selection handler |
|
250 // be running before we send this. |
|
251 let self = this; |
|
252 let state = aTopic == "metro_softkeyboard_shown"; |
|
253 Util.executeSoon(function () { |
|
254 self._onKeyboardDisplayChanging(state); |
|
255 }); |
|
256 break; |
|
257 } |
|
258 }, |
|
259 |
|
260 handleEvent: function cao_handleEvent(aEvent) { |
|
261 switch (aEvent.type) { |
|
262 case 'resize': |
|
263 if (aEvent.target != window) |
|
264 return; |
|
265 ContentAreaObserver.update(); |
|
266 break; |
|
267 } |
|
268 }, |
|
269 |
|
270 receiveMessage: function sh_receiveMessage(aMessage) { |
|
271 switch (aMessage.name) { |
|
272 case "Content:RepositionInfoResponse": |
|
273 this._onRepositionResponse(aMessage.json); |
|
274 break; |
|
275 } |
|
276 }, |
|
277 |
|
278 /* |
|
279 * Internal helpers |
|
280 */ |
|
281 |
|
282 _updateViewState: function (aState) { |
|
283 let oldViewstate = Elements.windowState.getAttribute("viewstate"); |
|
284 let viewstate = aState || this.viewstate; |
|
285 if (viewstate != oldViewstate) { |
|
286 Elements.windowState.setAttribute("viewstate", viewstate); |
|
287 Services.obs.notifyObservers(null, "metro_viewstate_changed", viewstate); |
|
288 } |
|
289 }, |
|
290 |
|
291 shiftBrowserDeck: function (aAmount) { |
|
292 if (aAmount == 0) { |
|
293 this._deckTransitioning = false; |
|
294 this._dispatchWindowEvent("KeyboardChanged", this.isKeyboardOpened); |
|
295 } |
|
296 |
|
297 if (this._shiftAmount == aAmount) |
|
298 return; |
|
299 |
|
300 this._shiftAmount = aAmount; |
|
301 this._dispatchWindowEvent("MozDeckOffsetChanging", aAmount); |
|
302 |
|
303 // Elements.browsers is the deck all browsers sit in |
|
304 let self = this; |
|
305 Elements.browsers.addEventListener("transitionend", function () { |
|
306 Elements.browsers.removeEventListener("transitionend", arguments.callee, true); |
|
307 self._deckTransitioning = false; |
|
308 self._dispatchWindowEvent("MozDeckOffsetChanged", aAmount); |
|
309 self._dispatchWindowEvent("KeyboardChanged", self.isKeyboardOpened); |
|
310 }, true); |
|
311 |
|
312 // selectAtPoint bug 858471 |
|
313 //Elements.browsers.style.transform = "translateY(" + (-1 * aAmount) + "px)"; |
|
314 Elements.browsers.style.marginTop = "" + (-1 * aAmount) + "px"; |
|
315 }, |
|
316 |
|
317 _dispatchWindowEvent: function _dispatchWindowEvent(aEventName, aDetail) { |
|
318 let event = document.createEvent("UIEvents"); |
|
319 event.initUIEvent(aEventName, true, false, window, aDetail); |
|
320 window.dispatchEvent(event); |
|
321 }, |
|
322 |
|
323 _dispatchBrowserEvent: function (aName, aDetail) { |
|
324 setTimeout(function() { |
|
325 let event = document.createEvent("Events"); |
|
326 event.initEvent(aName, true, false); |
|
327 Elements.browsers.dispatchEvent(event); |
|
328 }, 0); |
|
329 }, |
|
330 |
|
331 _initStyles: function _initStyles() { |
|
332 let stylesheet = document.styleSheets[0]; |
|
333 for (let style of ["window-width", "window-height", |
|
334 "content-height", "content-width", |
|
335 "viewable-height", "viewable-width"]) { |
|
336 let index = stylesheet.insertRule("." + style + " {}", stylesheet.cssRules.length); |
|
337 this.styles[style] = stylesheet.cssRules[index].style; |
|
338 } |
|
339 }, |
|
340 |
|
341 _getContentHeightForWindow: function (windowHeight) { |
|
342 return windowHeight; |
|
343 }, |
|
344 |
|
345 _getViewableHeightForContent: function (contentHeight) { |
|
346 let keyboardHeight = Services.metro.keyboardHeight; |
|
347 return contentHeight - keyboardHeight; |
|
348 }, |
|
349 |
|
350 _debugDumpDims: function _debugDumpDims() { |
|
351 let width = parseInt(this.styles["window-width"].width); |
|
352 let height = parseInt(this.styles["window-height"].height); |
|
353 Util.dumpLn("window:", width, height); |
|
354 width = parseInt(this.styles["content-width"].width); |
|
355 height = parseInt(this.styles["content-height"].height); |
|
356 Util.dumpLn("content:", width, height); |
|
357 width = parseInt(this.styles["viewable-width"].width); |
|
358 height = parseInt(this.styles["viewable-height"].height); |
|
359 Util.dumpLn("viewable:", width, height); |
|
360 } |
|
361 }; |