|
1 <?xml version="1.0"?> |
|
2 |
|
3 <bindings id="socialChatBindings" |
|
4 xmlns="http://www.mozilla.org/xbl" |
|
5 xmlns:xbl="http://www.mozilla.org/xbl" |
|
6 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> |
|
7 |
|
8 <binding id="chatbox"> |
|
9 <content orient="vertical" mousethrough="never"> |
|
10 <xul:hbox class="chat-titlebar" xbl:inherits="minimized,selected,activity" align="baseline"> |
|
11 <xul:hbox flex="1" onclick="document.getBindingParent(this).onTitlebarClick(event);"> |
|
12 <xul:image class="chat-status-icon" xbl:inherits="src=image"/> |
|
13 <xul:label class="chat-title" flex="1" xbl:inherits="value=label" crop="center"/> |
|
14 </xul:hbox> |
|
15 <xul:toolbarbutton anonid="notification-icon" class="notification-anchor-icon chat-toolbarbutton" |
|
16 oncommand="document.getBindingParent(this).showNotifications(); event.stopPropagation();"/> |
|
17 <xul:toolbarbutton anonid="minimize" class="chat-minimize-button chat-toolbarbutton" |
|
18 oncommand="document.getBindingParent(this).toggle();"/> |
|
19 <xul:toolbarbutton anonid="swap" class="chat-swap-button chat-toolbarbutton" |
|
20 oncommand="document.getBindingParent(this).swapWindows();"/> |
|
21 <xul:toolbarbutton anonid="close" class="chat-close-button chat-toolbarbutton" |
|
22 oncommand="document.getBindingParent(this).close();"/> |
|
23 </xul:hbox> |
|
24 <xul:browser anonid="content" class="chat-frame" flex="1" |
|
25 context="contentAreaContextMenu" |
|
26 disableglobalhistory="true" |
|
27 tooltip="aHTMLTooltip" |
|
28 xbl:inherits="src,origin" type="content"/> |
|
29 </content> |
|
30 |
|
31 <implementation implements="nsIDOMEventListener"> |
|
32 <constructor><![CDATA[ |
|
33 let Social = Components.utils.import("resource:///modules/Social.jsm", {}).Social; |
|
34 this.content.__defineGetter__("popupnotificationanchor", |
|
35 () => document.getAnonymousElementByAttribute(this, "anonid", "notification-icon")); |
|
36 Social.setErrorListener(this.content, function(aBrowser) { |
|
37 aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + |
|
38 encodeURIComponent(aBrowser.getAttribute("origin")), |
|
39 null, null, null, null); |
|
40 }); |
|
41 if (!this.chatbar) { |
|
42 document.getAnonymousElementByAttribute(this, "anonid", "minimize").hidden = true; |
|
43 document.getAnonymousElementByAttribute(this, "anonid", "close").hidden = true; |
|
44 } |
|
45 let contentWindow = this.contentWindow; |
|
46 this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) { |
|
47 if (event.target != this.contentDocument) |
|
48 return; |
|
49 this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true); |
|
50 this.isActive = !this.minimized; |
|
51 // process this._callbacks, then set to null so the chatbox creator |
|
52 // knows to make new callbacks immediately. |
|
53 if (this._callbacks) { |
|
54 for (let callback of this._callbacks) { |
|
55 if (callback) |
|
56 callback(contentWindow); |
|
57 } |
|
58 this._callbacks = null; |
|
59 } |
|
60 |
|
61 // content can send a socialChatActivity event to have the UI update. |
|
62 let chatActivity = function() { |
|
63 this.setAttribute("activity", true); |
|
64 if (this.chatbar) |
|
65 this.chatbar.updateTitlebar(this); |
|
66 }.bind(this); |
|
67 contentWindow.addEventListener("socialChatActivity", chatActivity); |
|
68 contentWindow.addEventListener("unload", function unload() { |
|
69 contentWindow.removeEventListener("unload", unload); |
|
70 contentWindow.removeEventListener("socialChatActivity", chatActivity); |
|
71 }); |
|
72 }, true); |
|
73 if (this.src) |
|
74 this.setAttribute("src", this.src); |
|
75 ]]></constructor> |
|
76 |
|
77 <field name="content" readonly="true"> |
|
78 document.getAnonymousElementByAttribute(this, "anonid", "content"); |
|
79 </field> |
|
80 |
|
81 <property name="contentWindow"> |
|
82 <getter> |
|
83 return this.content.contentWindow; |
|
84 </getter> |
|
85 </property> |
|
86 |
|
87 <property name="contentDocument"> |
|
88 <getter> |
|
89 return this.content.contentDocument; |
|
90 </getter> |
|
91 </property> |
|
92 |
|
93 <property name="minimized"> |
|
94 <getter> |
|
95 return this.getAttribute("minimized") == "true"; |
|
96 </getter> |
|
97 <setter><![CDATA[ |
|
98 // Note that this.isActive is set via our transitionend handler so |
|
99 // the content doesn't see intermediate values. |
|
100 let parent = this.chatbar; |
|
101 if (val) { |
|
102 this.setAttribute("minimized", "true"); |
|
103 // If this chat is the selected one a new one needs to be selected. |
|
104 if (parent && parent.selectedChat == this) |
|
105 parent._selectAnotherChat(); |
|
106 } else { |
|
107 this.removeAttribute("minimized"); |
|
108 // this chat gets selected. |
|
109 if (parent) |
|
110 parent.selectedChat = this; |
|
111 } |
|
112 ]]></setter> |
|
113 </property> |
|
114 |
|
115 <property name="chatbar"> |
|
116 <getter> |
|
117 if (this.parentNode.nodeName == "chatbar") |
|
118 return this.parentNode; |
|
119 return null; |
|
120 </getter> |
|
121 </property> |
|
122 |
|
123 <property name="isActive"> |
|
124 <getter> |
|
125 return this.content.docShell.isActive; |
|
126 </getter> |
|
127 <setter> |
|
128 this.content.docShell.isActive = !!val; |
|
129 |
|
130 // let the chat frame know if it is being shown or hidden |
|
131 let evt = this.contentDocument.createEvent("CustomEvent"); |
|
132 evt.initCustomEvent(val ? "socialFrameShow" : "socialFrameHide", true, true, {}); |
|
133 this.contentDocument.documentElement.dispatchEvent(evt); |
|
134 </setter> |
|
135 </property> |
|
136 |
|
137 <method name="showNotifications"> |
|
138 <body><![CDATA[ |
|
139 PopupNotifications._reshowNotifications(this.content.popupnotificationanchor, |
|
140 this.content); |
|
141 ]]></body> |
|
142 </method> |
|
143 |
|
144 <method name="swapDocShells"> |
|
145 <parameter name="aTarget"/> |
|
146 <body><![CDATA[ |
|
147 aTarget.setAttribute('label', this.contentDocument.title); |
|
148 aTarget.src = this.src; |
|
149 aTarget.content.setAttribute("origin", this.content.getAttribute("origin")); |
|
150 aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className; |
|
151 this.content.socialErrorListener.remove(); |
|
152 aTarget.content.socialErrorListener.remove(); |
|
153 this.content.swapDocShells(aTarget.content); |
|
154 Social.setErrorListener(this.content, function(aBrowser) {}); // 'this' will be destroyed soon. |
|
155 Social.setErrorListener(aTarget.content, function(aBrowser) { |
|
156 aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + |
|
157 encodeURIComponent(aBrowser.getAttribute("origin")), |
|
158 null, null, null, null); |
|
159 }); |
|
160 ]]></body> |
|
161 </method> |
|
162 |
|
163 <method name="onTitlebarClick"> |
|
164 <parameter name="aEvent"/> |
|
165 <body><![CDATA[ |
|
166 if (!this.chatbar) |
|
167 return; |
|
168 if (aEvent.button == 0) { // left-click: toggle minimized. |
|
169 this.toggle(); |
|
170 // if we restored it, we want to focus it. |
|
171 if (!this.minimized) |
|
172 this.chatbar.focus(); |
|
173 } else if (aEvent.button == 1) // middle-click: close chat |
|
174 this.close(); |
|
175 ]]></body> |
|
176 </method> |
|
177 |
|
178 <method name="close"> |
|
179 <body><![CDATA[ |
|
180 if (this.chatbar) |
|
181 this.chatbar.remove(this); |
|
182 else |
|
183 window.close(); |
|
184 ]]></body> |
|
185 </method> |
|
186 |
|
187 <method name="swapWindows"> |
|
188 <body><![CDATA[ |
|
189 let provider = Social._getProviderFromOrigin(this.content.getAttribute("origin")); |
|
190 if (this.chatbar) { |
|
191 this.chatbar.detachChatbox(this, { "centerscreen": "yes" }, win => { |
|
192 win.document.title = provider.name; |
|
193 }); |
|
194 } else { |
|
195 // attach this chatbox to the topmost browser window |
|
196 let findChromeWindowForChats = Cu.import("resource://gre/modules/MozSocialAPI.jsm").findChromeWindowForChats; |
|
197 let win = findChromeWindowForChats(); |
|
198 let chatbar = win.SocialChatBar.chatbar; |
|
199 chatbar.openChat(provider, "about:blank", win => { |
|
200 let cb = chatbar.selectedChat; |
|
201 this.swapDocShells(cb); |
|
202 |
|
203 // chatboxForURL is a map of URL -> chatbox used to avoid opening |
|
204 // duplicate chat windows. Ensure reattached chat windows aren't |
|
205 // registered with about:blank as their URL, otherwise reattaching |
|
206 // more than one chat window isn't possible. |
|
207 chatbar.chatboxForURL.delete("about:blank"); |
|
208 chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb)); |
|
209 |
|
210 chatbar.focus(); |
|
211 this.close(); |
|
212 }); |
|
213 } |
|
214 ]]></body> |
|
215 </method> |
|
216 |
|
217 <method name="toggle"> |
|
218 <body><![CDATA[ |
|
219 this.minimized = !this.minimized; |
|
220 ]]></body> |
|
221 </method> |
|
222 </implementation> |
|
223 |
|
224 <handlers> |
|
225 <handler event="focus" phase="capturing"> |
|
226 if (this.chatbar) |
|
227 this.chatbar.selectedChat = this; |
|
228 </handler> |
|
229 <handler event="DOMTitleChanged"><![CDATA[ |
|
230 this.setAttribute('label', this.contentDocument.title); |
|
231 if (this.chatbar) |
|
232 this.chatbar.updateTitlebar(this); |
|
233 ]]></handler> |
|
234 <handler event="DOMLinkAdded"><![CDATA[ |
|
235 // much of this logic is from DOMLinkHandler in browser.js |
|
236 // this sets the presence icon for a chat user, we simply use favicon style updating |
|
237 let link = event.originalTarget; |
|
238 let rel = link.rel && link.rel.toLowerCase(); |
|
239 if (!link || !link.ownerDocument || !rel || !link.href) |
|
240 return; |
|
241 if (link.rel.indexOf("icon") < 0) |
|
242 return; |
|
243 |
|
244 let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}).ContentLinkHandler; |
|
245 let uri = ContentLinkHandler.getLinkIconURI(link); |
|
246 if (!uri) |
|
247 return; |
|
248 |
|
249 // we made it this far, use it |
|
250 this.setAttribute('image', uri.spec); |
|
251 if (this.chatbar) |
|
252 this.chatbar.updateTitlebar(this); |
|
253 ]]></handler> |
|
254 <handler event="transitionend"> |
|
255 if (this.isActive == this.minimized) |
|
256 this.isActive = !this.minimized; |
|
257 </handler> |
|
258 </handlers> |
|
259 </binding> |
|
260 |
|
261 <binding id="chatbar"> |
|
262 <content> |
|
263 <xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1"> |
|
264 <xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/> |
|
265 <xul:toolbarbutton anonid="nub" class="chatbar-button" type="menu" collapsed="true" mousethrough="never"> |
|
266 <xul:menupopup anonid="nubMenu" oncommand="document.getBindingParent(this).showChat(event.target.chat)"/> |
|
267 </xul:toolbarbutton> |
|
268 <children/> |
|
269 </xul:hbox> |
|
270 </content> |
|
271 |
|
272 <implementation implements="nsIDOMEventListener"> |
|
273 <constructor> |
|
274 // to avoid reflows we cache the width of the nub. |
|
275 this.cachedWidthNub = 0; |
|
276 this._selectedChat = null; |
|
277 </constructor> |
|
278 |
|
279 <field name="innerbox" readonly="true"> |
|
280 document.getAnonymousElementByAttribute(this, "anonid", "innerbox"); |
|
281 </field> |
|
282 |
|
283 <field name="menupopup" readonly="true"> |
|
284 document.getAnonymousElementByAttribute(this, "anonid", "nubMenu"); |
|
285 </field> |
|
286 |
|
287 <field name="nub" readonly="true"> |
|
288 document.getAnonymousElementByAttribute(this, "anonid", "nub"); |
|
289 </field> |
|
290 |
|
291 <method name="focus"> |
|
292 <body><![CDATA[ |
|
293 if (!this.selectedChat) |
|
294 return; |
|
295 Services.focus.focusedWindow = this.selectedChat.contentWindow; |
|
296 ]]></body> |
|
297 </method> |
|
298 |
|
299 <method name="_isChatFocused"> |
|
300 <parameter name="aChatbox"/> |
|
301 <body><![CDATA[ |
|
302 // If there are no XBL bindings for the chat it can't be focused. |
|
303 if (!aChatbox.content) |
|
304 return false; |
|
305 let fw = Services.focus.focusedWindow; |
|
306 if (!fw) |
|
307 return false; |
|
308 // We want to see if the focused window is in the subtree below our browser... |
|
309 let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor) |
|
310 .getInterface(Ci.nsIWebNavigation) |
|
311 .QueryInterface(Ci.nsIDocShell) |
|
312 .chromeEventHandler; |
|
313 return containingBrowser == aChatbox.content; |
|
314 ]]></body> |
|
315 </method> |
|
316 |
|
317 <property name="selectedChat"> |
|
318 <getter><![CDATA[ |
|
319 return this._selectedChat; |
|
320 ]]></getter> |
|
321 <setter><![CDATA[ |
|
322 // this is pretty horrible, but we: |
|
323 // * want to avoid doing touching 'selected' attribute when the |
|
324 // specified chat is already selected. |
|
325 // * remove 'activity' attribute on newly selected tab *even if* |
|
326 // newly selected is already selected. |
|
327 // * need to handle either current or new being null. |
|
328 if (this._selectedChat != val) { |
|
329 if (this._selectedChat) { |
|
330 this._selectedChat.removeAttribute("selected"); |
|
331 } |
|
332 this._selectedChat = val; |
|
333 if (val) { |
|
334 this._selectedChat.setAttribute("selected", "true"); |
|
335 } |
|
336 } |
|
337 if (val) { |
|
338 this._selectedChat.removeAttribute("activity"); |
|
339 } |
|
340 ]]></setter> |
|
341 </property> |
|
342 |
|
343 <field name="menuitemMap">new WeakMap()</field> |
|
344 <field name="chatboxForURL">new Map();</field> |
|
345 |
|
346 <property name="hasCollapsedChildren"> |
|
347 <getter><![CDATA[ |
|
348 return !!this.querySelector("[collapsed]"); |
|
349 ]]></getter> |
|
350 </property> |
|
351 |
|
352 <property name="collapsedChildren"> |
|
353 <getter><![CDATA[ |
|
354 // A generator yielding all collapsed chatboxes, in the order in |
|
355 // which they should be restored. |
|
356 let child = this.lastElementChild; |
|
357 while (child) { |
|
358 if (child.collapsed) |
|
359 yield child; |
|
360 child = child.previousElementSibling; |
|
361 } |
|
362 ]]></getter> |
|
363 </property> |
|
364 |
|
365 <property name="visibleChildren"> |
|
366 <getter><![CDATA[ |
|
367 // A generator yielding all non-collapsed chatboxes. |
|
368 let child = this.firstElementChild; |
|
369 while (child) { |
|
370 if (!child.collapsed) |
|
371 yield child; |
|
372 child = child.nextElementSibling; |
|
373 } |
|
374 ]]></getter> |
|
375 </property> |
|
376 |
|
377 <property name="collapsibleChildren"> |
|
378 <getter><![CDATA[ |
|
379 // A generator yielding all children which are able to be collapsed |
|
380 // in the order in which they should be collapsed. |
|
381 // (currently this is all visible ones other than the selected one.) |
|
382 for (let child of this.visibleChildren) |
|
383 if (child != this.selectedChat) |
|
384 yield child; |
|
385 ]]></getter> |
|
386 </property> |
|
387 |
|
388 <method name="_selectAnotherChat"> |
|
389 <body><![CDATA[ |
|
390 // Select a different chat (as the currently selected one is no |
|
391 // longer suitable as the selection - maybe it is being minimized or |
|
392 // closed.) We only select non-minimized and non-collapsed chats, |
|
393 // and if none are found, set the selectedChat to null. |
|
394 // It's possible in the future we will track most-recently-selected |
|
395 // chats or similar to find the "best" candidate - for now though |
|
396 // the choice is somewhat arbitrary. |
|
397 let moveFocus = this.selectedChat && this._isChatFocused(this.selectedChat); |
|
398 for (let other of this.children) { |
|
399 if (other != this.selectedChat && !other.minimized && !other.collapsed) { |
|
400 this.selectedChat = other; |
|
401 if (moveFocus) |
|
402 this.focus(); |
|
403 return; |
|
404 } |
|
405 } |
|
406 // can't find another - so set no chat as selected. |
|
407 this.selectedChat = null; |
|
408 ]]></body> |
|
409 </method> |
|
410 |
|
411 <method name="updateTitlebar"> |
|
412 <parameter name="aChatbox"/> |
|
413 <body><![CDATA[ |
|
414 if (aChatbox.collapsed) { |
|
415 let menuitem = this.menuitemMap.get(aChatbox); |
|
416 if (aChatbox.getAttribute("activity")) { |
|
417 menuitem.setAttribute("activity", true); |
|
418 this.nub.setAttribute("activity", true); |
|
419 } |
|
420 menuitem.setAttribute("label", aChatbox.getAttribute("label")); |
|
421 menuitem.setAttribute("image", aChatbox.getAttribute("image")); |
|
422 } |
|
423 ]]></body> |
|
424 </method> |
|
425 |
|
426 <method name="calcTotalWidthOf"> |
|
427 <parameter name="aElement"/> |
|
428 <body><![CDATA[ |
|
429 let cs = document.defaultView.getComputedStyle(aElement); |
|
430 let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight); |
|
431 return aElement.getBoundingClientRect().width + margins; |
|
432 ]]></body> |
|
433 </method> |
|
434 |
|
435 <method name="getTotalChildWidth"> |
|
436 <parameter name="aChatbox"/> |
|
437 <body><![CDATA[ |
|
438 // These are from the CSS for the chatbox and must be kept in sync. |
|
439 // We can't use calcTotalWidthOf due to the transitions... |
|
440 const CHAT_WIDTH_OPEN = 260; |
|
441 const CHAT_WIDTH_MINIMIZED = 160; |
|
442 return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : CHAT_WIDTH_OPEN; |
|
443 ]]></body> |
|
444 </method> |
|
445 |
|
446 <method name="collapseChat"> |
|
447 <parameter name="aChatbox"/> |
|
448 <body><![CDATA[ |
|
449 // we ensure that the cached width for a child of this type is |
|
450 // up-to-date so we can use it when resizing. |
|
451 this.getTotalChildWidth(aChatbox); |
|
452 aChatbox.collapsed = true; |
|
453 aChatbox.isActive = false; |
|
454 let menu = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem"); |
|
455 menu.setAttribute("class", "menuitem-iconic"); |
|
456 menu.setAttribute("label", aChatbox.contentDocument.title); |
|
457 menu.setAttribute("image", aChatbox.getAttribute("image")); |
|
458 menu.chat = aChatbox; |
|
459 this.menuitemMap.set(aChatbox, menu); |
|
460 this.menupopup.appendChild(menu); |
|
461 this.nub.collapsed = false; |
|
462 ]]></body> |
|
463 </method> |
|
464 |
|
465 <method name="showChat"> |
|
466 <parameter name="aChatbox"/> |
|
467 <parameter name="aMode"/> |
|
468 <body><![CDATA[ |
|
469 if ((aMode != "minimized") && aChatbox.minimized) |
|
470 aChatbox.minimized = false; |
|
471 if (this.selectedChat != aChatbox) |
|
472 this.selectedChat = aChatbox; |
|
473 if (!aChatbox.collapsed) |
|
474 return; // already showing - no more to do. |
|
475 this._showChat(aChatbox); |
|
476 // showing a collapsed chat might mean another needs to be collapsed |
|
477 // to make room... |
|
478 this.resize(); |
|
479 ]]></body> |
|
480 </method> |
|
481 |
|
482 <method name="_showChat"> |
|
483 <parameter name="aChatbox"/> |
|
484 <body><![CDATA[ |
|
485 // the actual implementation - doesn't check for overflow, assumes |
|
486 // collapsed, etc. |
|
487 let menuitem = this.menuitemMap.get(aChatbox); |
|
488 this.menuitemMap.delete(aChatbox); |
|
489 this.menupopup.removeChild(menuitem); |
|
490 aChatbox.collapsed = false; |
|
491 aChatbox.isActive = !aChatbox.minimized; |
|
492 ]]></body> |
|
493 </method> |
|
494 |
|
495 <method name="remove"> |
|
496 <parameter name="aChatbox"/> |
|
497 <body><![CDATA[ |
|
498 this._remove(aChatbox); |
|
499 // The removal of a chat may mean a collapsed one can spring up, |
|
500 // or that the popup should be hidden. We also defer the selection |
|
501 // of another chat until after a resize, as a new candidate may |
|
502 // become uncollapsed after the resize. |
|
503 this.resize(); |
|
504 if (this.selectedChat == aChatbox) { |
|
505 this._selectAnotherChat(); |
|
506 } |
|
507 ]]></body> |
|
508 </method> |
|
509 |
|
510 <method name="_remove"> |
|
511 <parameter name="aChatbox"/> |
|
512 <body><![CDATA[ |
|
513 aChatbox.content.socialErrorListener.remove(); |
|
514 this.removeChild(aChatbox); |
|
515 // child might have been collapsed. |
|
516 let menuitem = this.menuitemMap.get(aChatbox); |
|
517 if (menuitem) { |
|
518 this.menuitemMap.delete(aChatbox); |
|
519 this.menupopup.removeChild(menuitem); |
|
520 } |
|
521 this.chatboxForURL.delete(aChatbox.src); |
|
522 ]]></body> |
|
523 </method> |
|
524 |
|
525 <method name="removeAll"> |
|
526 <body><![CDATA[ |
|
527 this.selectedChat = null; |
|
528 while (this.firstElementChild) { |
|
529 this._remove(this.firstElementChild); |
|
530 } |
|
531 // and the nub/popup must also die. |
|
532 this.nub.collapsed = true; |
|
533 ]]></body> |
|
534 </method> |
|
535 |
|
536 <method name="openChat"> |
|
537 <parameter name="aProvider"/> |
|
538 <parameter name="aURL"/> |
|
539 <parameter name="aCallback"/> |
|
540 <parameter name="aMode"/> |
|
541 <body><![CDATA[ |
|
542 let cb = this.chatboxForURL.get(aURL); |
|
543 if (cb) { |
|
544 cb = cb.get(); |
|
545 if (cb.parentNode) { |
|
546 this.showChat(cb, aMode); |
|
547 if (aCallback) { |
|
548 if (cb._callbacks == null) { |
|
549 // DOMContentLoaded has already fired, so callback now. |
|
550 aCallback(cb.contentWindow); |
|
551 } else { |
|
552 // DOMContentLoaded for this chat is yet to fire... |
|
553 cb._callbacks.push(aCallback); |
|
554 } |
|
555 } |
|
556 return; |
|
557 } |
|
558 this.chatboxForURL.delete(aURL); |
|
559 } |
|
560 cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox"); |
|
561 // _callbacks is a javascript property instead of a <field> as it |
|
562 // must exist before the (possibly delayed) bindings are created. |
|
563 cb._callbacks = [aCallback]; |
|
564 // src also a javascript property; the src attribute is set in the ctor. |
|
565 cb.src = aURL; |
|
566 if (aMode == "minimized") |
|
567 cb.setAttribute("minimized", "true"); |
|
568 cb.setAttribute("origin", aProvider.origin); |
|
569 this.insertBefore(cb, this.firstChild); |
|
570 this.selectedChat = cb; |
|
571 this.chatboxForURL.set(aURL, Cu.getWeakReference(cb)); |
|
572 this.resize(); |
|
573 ]]></body> |
|
574 </method> |
|
575 |
|
576 <method name="resize"> |
|
577 <body><![CDATA[ |
|
578 // Checks the current size against the collapsed state of children |
|
579 // and collapses or expands as necessary such that as many as possible |
|
580 // are shown. |
|
581 // So 2 basic strategies: |
|
582 // * Collapse/Expand one at a time until we can't collapse/expand any |
|
583 // more - but this is one reflow per change. |
|
584 // * Calculate the dimensions ourself and choose how many to collapse |
|
585 // or expand based on this, then do them all in one go. This is one |
|
586 // reflow regardless of how many we change. |
|
587 // So we go the more complicated but more efficient second option... |
|
588 let availWidth = this.getBoundingClientRect().width; |
|
589 let currentWidth = 0; |
|
590 if (!this.nub.collapsed) { // the nub is visible. |
|
591 if (!this.cachedWidthNub) |
|
592 this.cachedWidthNub = this.calcTotalWidthOf(this.nub); |
|
593 currentWidth += this.cachedWidthNub; |
|
594 } |
|
595 for (let child of this.visibleChildren) { |
|
596 currentWidth += this.getTotalChildWidth(child); |
|
597 } |
|
598 |
|
599 if (currentWidth > availWidth) { |
|
600 // we need to collapse some. |
|
601 let toCollapse = []; |
|
602 for (let child of this.collapsibleChildren) { |
|
603 if (currentWidth <= availWidth) |
|
604 break; |
|
605 toCollapse.push(child); |
|
606 currentWidth -= this.getTotalChildWidth(child); |
|
607 } |
|
608 if (toCollapse.length) { |
|
609 for (let child of toCollapse) |
|
610 this.collapseChat(child); |
|
611 } |
|
612 } else if (currentWidth < availWidth) { |
|
613 // we *might* be able to expand some - see how many. |
|
614 // XXX - if this was clever, it could know when removing the nub |
|
615 // leaves enough space to show all collapsed |
|
616 let toShow = []; |
|
617 for (let child of this.collapsedChildren) { |
|
618 currentWidth += this.getTotalChildWidth(child); |
|
619 if (currentWidth > availWidth) |
|
620 break; |
|
621 toShow.push(child); |
|
622 } |
|
623 for (let child of toShow) |
|
624 this._showChat(child); |
|
625 |
|
626 // If none remain collapsed remove the nub. |
|
627 if (!this.hasCollapsedChildren) { |
|
628 this.nub.collapsed = true; |
|
629 } |
|
630 } |
|
631 // else: achievement unlocked - we are pixel-perfect! |
|
632 ]]></body> |
|
633 </method> |
|
634 |
|
635 <method name="handleEvent"> |
|
636 <parameter name="aEvent"/> |
|
637 <body><![CDATA[ |
|
638 if (aEvent.type == "resize") { |
|
639 this.resize(); |
|
640 } |
|
641 ]]></body> |
|
642 </method> |
|
643 |
|
644 <method name="_getDragTarget"> |
|
645 <parameter name="event"/> |
|
646 <body><![CDATA[ |
|
647 return event.target.localName == "chatbox" ? event.target : null; |
|
648 ]]></body> |
|
649 </method> |
|
650 |
|
651 <!-- Moves a chatbox to a new window. --> |
|
652 <method name="detachChatbox"> |
|
653 <parameter name="aChatbox"/> |
|
654 <parameter name="aOptions"/> |
|
655 <parameter name="aCallback"/> |
|
656 <body><![CDATA[ |
|
657 let options = ""; |
|
658 for (let name in aOptions) |
|
659 options += "," + name + "=" + aOptions[name]; |
|
660 |
|
661 let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul", |
|
662 "_blank", "chrome,all,dialog=no" + options); |
|
663 |
|
664 otherWin.addEventListener("load", function _chatLoad(event) { |
|
665 if (event.target != otherWin.document) |
|
666 return; |
|
667 |
|
668 otherWin.removeEventListener("load", _chatLoad, true); |
|
669 let otherChatbox = otherWin.document.getElementById("chatter"); |
|
670 aChatbox.swapDocShells(otherChatbox); |
|
671 aChatbox.close(); |
|
672 if (aCallback) |
|
673 aCallback(otherWin); |
|
674 }, true); |
|
675 ]]></body> |
|
676 </method> |
|
677 |
|
678 </implementation> |
|
679 |
|
680 <handlers> |
|
681 <handler event="popupshown"><![CDATA[ |
|
682 this.nub.removeAttribute("activity"); |
|
683 ]]></handler> |
|
684 <handler event="load"><![CDATA[ |
|
685 window.addEventListener("resize", this, true); |
|
686 ]]></handler> |
|
687 <handler event="unload"><![CDATA[ |
|
688 window.removeEventListener("resize", this, true); |
|
689 ]]></handler> |
|
690 |
|
691 <handler event="dragstart"><![CDATA[ |
|
692 // chat window dragging is essentially duplicated from tabbrowser.xml |
|
693 // to acheive the same visual experience |
|
694 let chatbox = this._getDragTarget(event); |
|
695 if (!chatbox) { |
|
696 return; |
|
697 } |
|
698 |
|
699 let dt = event.dataTransfer; |
|
700 // we do not set a url in the drag data to prevent moving to tabbrowser |
|
701 // or otherwise having unexpected drop handlers do something with our |
|
702 // chatbox |
|
703 dt.mozSetDataAt("application/x-moz-chatbox", chatbox, 0); |
|
704 |
|
705 // Set the cursor to an arrow during tab drags. |
|
706 dt.mozCursor = "default"; |
|
707 |
|
708 // Create a canvas to which we capture the current tab. |
|
709 // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired |
|
710 // canvas size (in CSS pixels) to the window's backing resolution in order |
|
711 // to get a full-resolution drag image for use on HiDPI displays. |
|
712 let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); |
|
713 let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom; |
|
714 let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); |
|
715 canvas.mozOpaque = true; |
|
716 canvas.width = 160 * scale; |
|
717 canvas.height = 90 * scale; |
|
718 PageThumbs.captureToCanvas(chatbox.contentWindow, canvas); |
|
719 dt.setDragImage(canvas, -16 * scale, -16 * scale); |
|
720 |
|
721 event.stopPropagation(); |
|
722 ]]></handler> |
|
723 |
|
724 <handler event="dragend"><![CDATA[ |
|
725 let dt = event.dataTransfer; |
|
726 let draggedChat = dt.mozGetDataAt("application/x-moz-chatbox", 0); |
|
727 if (dt.mozUserCancelled || dt.dropEffect != "none") { |
|
728 return; |
|
729 } |
|
730 |
|
731 let eX = event.screenX; |
|
732 let eY = event.screenY; |
|
733 // screen.availLeft et. al. only check the screen that this window is on, |
|
734 // but we want to look at the screen the tab is being dropped onto. |
|
735 let sX = {}, sY = {}, sWidth = {}, sHeight = {}; |
|
736 Cc["@mozilla.org/gfx/screenmanager;1"] |
|
737 .getService(Ci.nsIScreenManager) |
|
738 .screenForRect(eX, eY, 1, 1) |
|
739 .GetAvailRect(sX, sY, sWidth, sHeight); |
|
740 // default size for the chat window as used in chatWindow.xul, use them |
|
741 // here to attempt to keep the window fully within the screen when |
|
742 // opening at the drop point. If the user has resized the window to |
|
743 // something larger (which gets persisted), at least a good portion of |
|
744 // the window should still be within the screen. |
|
745 let winWidth = 400; |
|
746 let winHeight = 420; |
|
747 // ensure new window entirely within screen |
|
748 let left = Math.min(Math.max(eX, sX.value), |
|
749 sX.value + sWidth.value - winWidth); |
|
750 let top = Math.min(Math.max(eY, sY.value), |
|
751 sY.value + sHeight.value - winHeight); |
|
752 |
|
753 let provider = Social._getProviderFromOrigin(draggedChat.content.getAttribute("origin")); |
|
754 this.detachChatbox(draggedChat, { screenX: left, screenY: top }, win => { |
|
755 win.document.title = provider.name; |
|
756 }); |
|
757 |
|
758 event.stopPropagation(); |
|
759 ]]></handler> |
|
760 </handlers> |
|
761 </binding> |
|
762 |
|
763 </bindings> |