|
1 <?xml version="1.0"?> |
|
2 <!-- This Source Code Form is subject to the terms of the Mozilla Public |
|
3 - License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> |
|
5 |
|
6 <bindings id="browserPanelUIBindings" |
|
7 xmlns="http://www.mozilla.org/xbl" |
|
8 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" |
|
9 xmlns:xbl="http://www.mozilla.org/xbl"> |
|
10 |
|
11 <binding id="panelmultiview"> |
|
12 <resources> |
|
13 <stylesheet src="chrome://browser/content/customizableui/panelUI.css"/> |
|
14 </resources> |
|
15 <content> |
|
16 <xul:box anonid="viewContainer" class="panel-viewcontainer" xbl:inherits="panelopen,viewtype,transitioning"> |
|
17 <xul:stack anonid="viewStack" xbl:inherits="viewtype" viewtype="main" class="panel-viewstack"> |
|
18 <xul:vbox anonid="mainViewContainer" class="panel-mainview"/> |
|
19 |
|
20 <!-- Used to capture click events over the PanelUI-mainView if we're in |
|
21 subview mode. That way, any click on the PanelUI-mainView causes us |
|
22 to revert to the mainView mode, whereupon PanelUI-click-capture then |
|
23 allows click events to go through it. --> |
|
24 <xul:vbox anonid="clickCapturer" class="panel-clickcapturer"/> |
|
25 |
|
26 <!-- We manually set display: none (via a CSS attribute selector) on the |
|
27 subviews that are not being displayed. We're using this over a deck |
|
28 because a deck assumes the size of its largest child, regardless of |
|
29 whether or not it is shown. That's not good for our case, since we |
|
30 want to allow each subview to be uniquely sized. --> |
|
31 <xul:vbox anonid="subViews" class="panel-subviews" xbl:inherits="panelopen"> |
|
32 <children includes="panelview"/> |
|
33 </xul:vbox> |
|
34 </xul:stack> |
|
35 </xul:box> |
|
36 </content> |
|
37 <implementation implements="nsIDOMEventListener"> |
|
38 <field name="_clickCapturer" readonly="true"> |
|
39 document.getAnonymousElementByAttribute(this, "anonid", "clickCapturer"); |
|
40 </field> |
|
41 <field name="_viewContainer" readonly="true"> |
|
42 document.getAnonymousElementByAttribute(this, "anonid", "viewContainer"); |
|
43 </field> |
|
44 <field name="_mainViewContainer" readonly="true"> |
|
45 document.getAnonymousElementByAttribute(this, "anonid", "mainViewContainer"); |
|
46 </field> |
|
47 <field name="_subViews" readonly="true"> |
|
48 document.getAnonymousElementByAttribute(this, "anonid", "subViews"); |
|
49 </field> |
|
50 <field name="_viewStack" readonly="true"> |
|
51 document.getAnonymousElementByAttribute(this, "anonid", "viewStack"); |
|
52 </field> |
|
53 <field name="_panel" readonly="true"> |
|
54 this.parentNode; |
|
55 </field> |
|
56 |
|
57 <field name="_currentSubView">null</field> |
|
58 <field name="_anchorElement">null</field> |
|
59 <field name="_mainViewHeight">0</field> |
|
60 <field name="_subViewObserver">null</field> |
|
61 <field name="__transitioning">false</field> |
|
62 <field name="_ignoreMutations">false</field> |
|
63 |
|
64 <property name="showingSubView" readonly="true" |
|
65 onget="return this._viewStack.getAttribute('viewtype') == 'subview'"/> |
|
66 <property name="_mainViewId" onget="return this.getAttribute('mainViewId');" onset="this.setAttribute('mainViewId', val); return val;"/> |
|
67 <property name="_mainView" readonly="true" |
|
68 onget="return this._mainViewId ? document.getElementById(this._mainViewId) : null;"/> |
|
69 <property name="showingSubViewAsMainView" readonly="true" |
|
70 onget="return this.getAttribute('mainViewIsSubView') == 'true'"/> |
|
71 |
|
72 <property name="ignoreMutations"> |
|
73 <getter> |
|
74 return this._ignoreMutations; |
|
75 </getter> |
|
76 <setter><![CDATA[ |
|
77 this._ignoreMutations = val; |
|
78 if (!val && this._panel.state == "open") { |
|
79 if (this.showingSubView) { |
|
80 this._syncContainerWithSubView(); |
|
81 } else { |
|
82 this._syncContainerWithMainView(); |
|
83 } |
|
84 } |
|
85 ]]></setter> |
|
86 </property> |
|
87 |
|
88 <property name="_transitioning"> |
|
89 <getter> |
|
90 return this.__transitioning; |
|
91 </getter> |
|
92 <setter><![CDATA[ |
|
93 this.__transitioning = val; |
|
94 if (val) { |
|
95 this.setAttribute("transitioning", "true"); |
|
96 } else { |
|
97 this.removeAttribute("transitioning"); |
|
98 } |
|
99 ]]></setter> |
|
100 </property> |
|
101 <constructor><![CDATA[ |
|
102 this._clickCapturer.addEventListener("click", this); |
|
103 this._panel.addEventListener("popupshowing", this); |
|
104 this._panel.addEventListener("popupshown", this); |
|
105 this._panel.addEventListener("popuphidden", this); |
|
106 this._subViews.addEventListener("overflow", this); |
|
107 this._mainViewContainer.addEventListener("overflow", this); |
|
108 |
|
109 // Get a MutationObserver ready to react to subview size changes. We |
|
110 // only attach this MutationObserver when a subview is being displayed. |
|
111 this._subViewObserver = |
|
112 new MutationObserver(this._syncContainerWithSubView.bind(this)); |
|
113 this._mainViewObserver = |
|
114 new MutationObserver(this._syncContainerWithMainView.bind(this)); |
|
115 |
|
116 this._mainViewContainer.setAttribute("panelid", |
|
117 this._panel.id); |
|
118 |
|
119 if (this._mainView) { |
|
120 this.setMainView(this._mainView); |
|
121 } |
|
122 this.setAttribute("viewtype", "main"); |
|
123 ]]></constructor> |
|
124 |
|
125 <destructor><![CDATA[ |
|
126 if (this._mainView) { |
|
127 this._mainView.removeAttribute("mainview"); |
|
128 } |
|
129 this._mainViewObserver.disconnect(); |
|
130 this._subViewObserver.disconnect(); |
|
131 this._panel.removeEventListener("popupshowing", this); |
|
132 this._panel.removeEventListener("popupshown", this); |
|
133 this._panel.removeEventListener("popuphidden", this); |
|
134 this._subViews.removeEventListener("overflow", this); |
|
135 this._mainViewContainer.removeEventListener("overflow", this); |
|
136 this._clickCapturer.removeEventListener("click", this); |
|
137 ]]></destructor> |
|
138 |
|
139 <method name="setMainView"> |
|
140 <parameter name="aNewMainView"/> |
|
141 <body><![CDATA[ |
|
142 if (this._mainView) { |
|
143 this._mainViewObserver.disconnect(); |
|
144 this._subViews.appendChild(this._mainView); |
|
145 this._mainView.removeAttribute("mainview"); |
|
146 } |
|
147 this._mainViewId = aNewMainView.id; |
|
148 aNewMainView.setAttribute("mainview", "true"); |
|
149 this._mainViewContainer.appendChild(aNewMainView); |
|
150 ]]></body> |
|
151 </method> |
|
152 |
|
153 <method name="showMainView"> |
|
154 <body><![CDATA[ |
|
155 if (this.showingSubView) { |
|
156 let viewNode = this._currentSubView; |
|
157 let evt = document.createEvent("CustomEvent"); |
|
158 evt.initCustomEvent("ViewHiding", true, true, viewNode); |
|
159 viewNode.dispatchEvent(evt); |
|
160 |
|
161 viewNode.removeAttribute("current"); |
|
162 this._currentSubView = null; |
|
163 |
|
164 this._subViewObserver.disconnect(); |
|
165 |
|
166 this._transitioning = true; |
|
167 |
|
168 this._viewContainer.addEventListener("transitionend", function trans() { |
|
169 this._viewContainer.removeEventListener("transitionend", trans); |
|
170 this._transitioning = false; |
|
171 }.bind(this)); |
|
172 this._viewContainer.style.height = this._mainViewHeight + "px"; |
|
173 |
|
174 this.setAttribute("viewtype", "main"); |
|
175 } |
|
176 |
|
177 this._shiftMainView(); |
|
178 ]]></body> |
|
179 </method> |
|
180 |
|
181 <method name="showSubView"> |
|
182 <parameter name="aViewId"/> |
|
183 <parameter name="aAnchor"/> |
|
184 <body><![CDATA[ |
|
185 let viewNode = this.querySelector("#" + aViewId); |
|
186 viewNode.setAttribute("current", true); |
|
187 // Emit the ViewShowing event so that the widget definition has a chance |
|
188 // to lazily populate the subview with things. |
|
189 let evt = document.createEvent("CustomEvent"); |
|
190 evt.initCustomEvent("ViewShowing", true, true, viewNode); |
|
191 viewNode.dispatchEvent(evt); |
|
192 if (evt.defaultPrevented) { |
|
193 return; |
|
194 } |
|
195 |
|
196 this._currentSubView = viewNode; |
|
197 |
|
198 // Now we have to transition the panel. There are a few parts to this: |
|
199 // |
|
200 // 1) The main view content gets shifted so that the center of the anchor |
|
201 // node is at the left-most edge of the panel. |
|
202 // 2) The subview deck slides in so that it takes up almost all of the |
|
203 // panel. |
|
204 // 3) If the subview is taller then the main panel contents, then the panel |
|
205 // must grow to meet that new height. Otherwise, it must shrink. |
|
206 // |
|
207 // All three of these actions make use of CSS transformations, so they |
|
208 // should all occur simultaneously. |
|
209 this.setAttribute("viewtype", "subview"); |
|
210 this._shiftMainView(aAnchor); |
|
211 |
|
212 this._mainViewHeight = this._viewStack.clientHeight; |
|
213 |
|
214 this._transitioning = true; |
|
215 this._viewContainer.addEventListener("transitionend", function trans() { |
|
216 this._viewContainer.removeEventListener("transitionend", trans); |
|
217 this._transitioning = false; |
|
218 }.bind(this)); |
|
219 |
|
220 let newHeight = this._heightOfSubview(viewNode, this._subViews); |
|
221 this._viewContainer.style.height = newHeight + "px"; |
|
222 |
|
223 this._subViewObserver.observe(viewNode, { |
|
224 attributes: true, |
|
225 characterData: true, |
|
226 childList: true, |
|
227 subtree: true |
|
228 }); |
|
229 ]]></body> |
|
230 </method> |
|
231 |
|
232 <method name="_shiftMainView"> |
|
233 <parameter name="aAnchor"/> |
|
234 <body><![CDATA[ |
|
235 if (aAnchor) { |
|
236 // We need to find the edge of the anchor, relative to the main panel. |
|
237 // Then we need to add half the width of the anchor. This is the target |
|
238 // that we need to transition to. |
|
239 let anchorRect = aAnchor.getBoundingClientRect(); |
|
240 let mainViewRect = this._mainViewContainer.getBoundingClientRect(); |
|
241 let center = aAnchor.clientWidth / 2; |
|
242 let direction = aAnchor.ownerDocument.defaultView.getComputedStyle(aAnchor, null).direction; |
|
243 let edge, target; |
|
244 if (direction == "ltr") { |
|
245 edge = anchorRect.left - mainViewRect.left; |
|
246 target = "-" + (edge + center); |
|
247 } else { |
|
248 edge = mainViewRect.right - anchorRect.right; |
|
249 target = edge + center; |
|
250 } |
|
251 this._mainViewContainer.style.transform = "translateX(" + target + "px)"; |
|
252 aAnchor.setAttribute("panel-multiview-anchor", true); |
|
253 } else { |
|
254 this._mainViewContainer.style.transform = ""; |
|
255 if (this.anchorElement) |
|
256 this.anchorElement.removeAttribute("panel-multiview-anchor"); |
|
257 } |
|
258 this.anchorElement = aAnchor; |
|
259 ]]></body> |
|
260 </method> |
|
261 |
|
262 <method name="handleEvent"> |
|
263 <parameter name="aEvent"/> |
|
264 <body><![CDATA[ |
|
265 if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) { |
|
266 // Shouldn't act on e.g. context menus being shown from within the panel. |
|
267 return; |
|
268 } |
|
269 switch (aEvent.type) { |
|
270 case "click": |
|
271 if (aEvent.originalTarget == this._clickCapturer) { |
|
272 this.showMainView(); |
|
273 } |
|
274 break; |
|
275 case "overflow": |
|
276 if (aEvent.target.localName == "vbox") { |
|
277 // Resize the right view on the next tick. |
|
278 if (this.showingSubView) { |
|
279 setTimeout(this._syncContainerWithSubView.bind(this), 0); |
|
280 } else if (!this.transitioning) { |
|
281 setTimeout(this._syncContainerWithMainView.bind(this), 0); |
|
282 } |
|
283 } |
|
284 break; |
|
285 case "popupshowing": |
|
286 this.setAttribute("panelopen", "true"); |
|
287 // Bug 941196 - The panel can get taller when opening a subview. Disabling |
|
288 // autoPositioning means that the panel won't jump around if an opened |
|
289 // subview causes the panel to exceed the dimensions of the screen in the |
|
290 // direction that the panel originally opened in. This property resets |
|
291 // every time the popup closes, which is why we have to set it each time. |
|
292 this._panel.autoPosition = false; |
|
293 this._syncContainerWithMainView(); |
|
294 |
|
295 this._mainViewObserver.observe(this._mainView, { |
|
296 attributes: true, |
|
297 characterData: true, |
|
298 childList: true, |
|
299 subtree: true |
|
300 }); |
|
301 |
|
302 break; |
|
303 case "popupshown": |
|
304 this._setMaxHeight(); |
|
305 break; |
|
306 case "popuphidden": |
|
307 this.removeAttribute("panelopen"); |
|
308 this._mainView.style.removeProperty("height"); |
|
309 this.showMainView(); |
|
310 this._mainViewObserver.disconnect(); |
|
311 break; |
|
312 } |
|
313 ]]></body> |
|
314 </method> |
|
315 |
|
316 <method name="_shouldSetHeight"> |
|
317 <body><![CDATA[ |
|
318 return this.getAttribute("nosubviews") != "true"; |
|
319 ]]></body> |
|
320 </method> |
|
321 |
|
322 <method name="_setMaxHeight"> |
|
323 <body><![CDATA[ |
|
324 if (!this._shouldSetHeight()) |
|
325 return; |
|
326 |
|
327 // Ignore the mutation that'll fire when we set the height of |
|
328 // the main view. |
|
329 this.ignoreMutations = true; |
|
330 this._mainView.style.height = |
|
331 this.getBoundingClientRect().height + "px"; |
|
332 this.ignoreMutations = false; |
|
333 ]]></body> |
|
334 </method> |
|
335 <method name="_syncContainerWithSubView"> |
|
336 <body><![CDATA[ |
|
337 // Check that this panel is still alive: |
|
338 if (!this._panel || !this._panel.parentNode) { |
|
339 return; |
|
340 } |
|
341 |
|
342 if (!this.ignoreMutations && this.showingSubView) { |
|
343 let newHeight = this._heightOfSubview(this._currentSubView, this._subViews); |
|
344 this._viewContainer.style.height = newHeight + "px"; |
|
345 } |
|
346 ]]></body> |
|
347 </method> |
|
348 <method name="_syncContainerWithMainView"> |
|
349 <body><![CDATA[ |
|
350 // Check that this panel is still alive: |
|
351 if (!this._panel || !this._panel.parentNode || !this._shouldSetHeight()) { |
|
352 return; |
|
353 } |
|
354 |
|
355 if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) { |
|
356 let height; |
|
357 if (this.showingSubViewAsMainView) { |
|
358 height = this._heightOfSubview(this._mainView); |
|
359 } else { |
|
360 height = this._mainView.scrollHeight; |
|
361 } |
|
362 this._viewContainer.style.height = height + "px"; |
|
363 } |
|
364 ]]></body> |
|
365 </method> |
|
366 |
|
367 <method name="_heightOfSubview"> |
|
368 <parameter name="aSubview"/> |
|
369 <parameter name="aContainerToCheck"/> |
|
370 <body><![CDATA[ |
|
371 function getFullHeight(element) { |
|
372 //XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative |
|
373 // that works with overflow: auto elements. Fortunately for us, |
|
374 // we have exactly 1 (potentially) scrolling element in here (the subview body), |
|
375 // and rounding 1 value is OK - rounding more than 1 and adding them means we get |
|
376 // off-by-1 errors. Now we might be off by a subpixel, but we care less about that. |
|
377 // So, use scrollHeight *only* if the element is vertically scrollable. |
|
378 let height; |
|
379 let elementCS; |
|
380 if (element.scrollTopMax) { |
|
381 height = element.scrollHeight; |
|
382 // Bounding client rects include borders, scrollHeight doesn't: |
|
383 elementCS = win.getComputedStyle(element); |
|
384 height += parseFloat(elementCS.borderTopWidth) + |
|
385 parseFloat(elementCS.borderBottomWidth); |
|
386 } else { |
|
387 height = element.getBoundingClientRect().height; |
|
388 if (height > 0) { |
|
389 elementCS = win.getComputedStyle(element); |
|
390 } |
|
391 } |
|
392 if (elementCS) { |
|
393 // Include margins - but not borders or paddings because they |
|
394 // were dealt with above. |
|
395 height += parseFloat(elementCS.marginTop) + parseFloat(elementCS.marginBottom); |
|
396 } |
|
397 return height; |
|
398 } |
|
399 let win = aSubview.ownerDocument.defaultView; |
|
400 let body = aSubview.querySelector(".panel-subview-body"); |
|
401 let height = getFullHeight(body || aSubview); |
|
402 if (body) { |
|
403 let header = aSubview.querySelector(".panel-subview-header"); |
|
404 let footer = aSubview.querySelector(".panel-subview-footer"); |
|
405 height += (header ? getFullHeight(header) : 0) + |
|
406 (footer ? getFullHeight(footer) : 0); |
|
407 } |
|
408 if (aContainerToCheck) { |
|
409 let containerCS = win.getComputedStyle(aContainerToCheck); |
|
410 height += parseFloat(containerCS.paddingTop) + parseFloat(containerCS.paddingBottom); |
|
411 } |
|
412 return Math.round(height); |
|
413 ]]></body> |
|
414 </method> |
|
415 |
|
416 </implementation> |
|
417 </binding> |
|
418 |
|
419 <binding id="panelview"> |
|
420 <implementation> |
|
421 <property name="panelMultiView" readonly="true"> |
|
422 <getter><![CDATA[ |
|
423 if (this.parentNode.localName != "panelmultiview") { |
|
424 return document.getBindingParent(this.parentNode); |
|
425 } |
|
426 |
|
427 return this.parentNode; |
|
428 ]]></getter> |
|
429 </property> |
|
430 </implementation> |
|
431 </binding> |
|
432 </bindings> |