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">
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"/>
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();"/>
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"/>
31 <implementation implements="nsIDOMEventListener">
32 <constructor><![CDATA[
33 this.content.__defineGetter__("popupnotificationanchor",
34 () => document.getAnonymousElementByAttribute(this, "anonid", "notification-icon"));
37 document.getAnonymousElementByAttribute(this, "anonid", "minimize").hidden = true;
38 document.getAnonymousElementByAttribute(this, "anonid", "close").hidden = true;
40 let contentWindow = this.contentWindow;
41 // process this._callbacks, then set to null so the chatbox creator
42 // knows to make new callbacks immediately.
43 if (this._callbacks) {
44 for (let callback of this._callbacks) {
47 this._callbacks = null;
49 this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
50 if (event.target != this.contentDocument)
52 this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true);
53 this.isActive = !this.minimized;
54 this._deferredChatLoaded.resolve(this);
57 this.setAttribute("src", this.src);
60 <field name="_deferredChatLoaded" readonly="true">
64 <property name="promiseChatLoaded">
66 return this._deferredChatLoaded.promise;
70 <field name="content" readonly="true">
71 document.getAnonymousElementByAttribute(this, "anonid", "content");
74 <property name="contentWindow">
76 return this.content.contentWindow;
80 <property name="contentDocument">
82 return this.content.contentDocument;
86 <property name="minimized">
88 return this.getAttribute("minimized") == "true";
91 // Note that this.isActive is set via our transitionend handler so
92 // the content doesn't see intermediate values.
93 let parent = this.chatbar;
95 this.setAttribute("minimized", "true");
96 // If this chat is the selected one a new one needs to be selected.
97 if (parent && parent.selectedChat == this)
98 parent._selectAnotherChat();
100 this.removeAttribute("minimized");
101 // this chat gets selected.
103 parent.selectedChat = this;
108 <property name="chatbar">
110 if (this.parentNode.nodeName == "chatbar")
111 return this.parentNode;
116 <property name="isActive">
118 return this.content.docShell.isActive;
121 this.content.docShell.isActive = !!val;
123 // let the chat frame know if it is being shown or hidden
124 let evt = this.contentDocument.createEvent("CustomEvent");
125 evt.initCustomEvent(val ? "socialFrameShow" : "socialFrameHide", true, true, {});
126 this.contentDocument.documentElement.dispatchEvent(evt);
130 <method name="showNotifications">
132 PopupNotifications._reshowNotifications(this.content.popupnotificationanchor,
137 <method name="swapDocShells">
138 <parameter name="aTarget"/>
140 aTarget.setAttribute('label', this.contentDocument.title);
141 aTarget.src = this.src;
142 aTarget.content.setAttribute("origin", this.content.getAttribute("origin"));
143 aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className;
144 this.content.swapDocShells(aTarget.content);
148 <method name="onTitlebarClick">
149 <parameter name="aEvent"/>
153 if (aEvent.button == 0) { // left-click: toggle minimized.
155 // if we restored it, we want to focus it.
157 this.chatbar.focus();
158 } else if (aEvent.button == 1) // middle-click: close chat
163 <method name="close">
166 this.chatbar.remove(this);
172 <method name="swapWindows">
174 let deferred = Promise.defer();
175 let title = this.getAttribute("label");
177 this.chatbar.detachChatbox(this, { "centerscreen": "yes" }).then(
179 chatbox.contentWindow.document.title = title;
180 deferred.resolve(chatbox);
184 // attach this chatbox to the topmost browser window
185 let Chat = Cu.import("resource:///modules/Chat.jsm").Chat;
186 let win = Chat.findChromeWindowForChats();
187 let chatbar = win.document.getElementById("pinnedchats");
188 let origin = this.content.getAttribute("origin");
189 let cb = chatbar.openChat(origin, title, "about:blank");
190 cb.promiseChatLoaded.then(
192 this.swapDocShells(cb);
194 // chatboxForURL is a map of URL -> chatbox used to avoid opening
195 // duplicate chat windows. Ensure reattached chat windows aren't
196 // registered with about:blank as their URL, otherwise reattaching
197 // more than one chat window isn't possible.
198 chatbar.chatboxForURL.delete("about:blank");
199 chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb));
203 deferred.resolve(cb);
207 return deferred.promise;
211 <method name="toggle">
213 this.minimized = !this.minimized;
219 <handler event="focus" phase="capturing">
221 this.chatbar.selectedChat = this;
223 <handler event="DOMTitleChanged"><![CDATA[
224 this.setAttribute('label', this.contentDocument.title);
226 this.chatbar.updateTitlebar(this);
228 <handler event="DOMLinkAdded"><![CDATA[
229 // much of this logic is from DOMLinkHandler in browser.js
230 // this sets the presence icon for a chat user, we simply use favicon style updating
231 let link = event.originalTarget;
232 let rel = link.rel && link.rel.toLowerCase();
233 if (!link || !link.ownerDocument || !rel || !link.href)
235 if (link.rel.indexOf("icon") < 0)
238 let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}).ContentLinkHandler;
239 let uri = ContentLinkHandler.getLinkIconURI(link);
243 // we made it this far, use it
244 this.setAttribute('image', uri.spec);
246 this.chatbar.updateTitlebar(this);
248 <handler event="transitionend">
249 if (this.isActive == this.minimized)
250 this.isActive = !this.minimized;
255 <binding id="chatbar">
257 <xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1">
258 <xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/>
259 <xul:toolbarbutton anonid="nub" class="chatbar-button" type="menu" collapsed="true" mousethrough="never">
260 <xul:menupopup anonid="nubMenu" oncommand="document.getBindingParent(this).showChat(event.target.chat)"/>
266 <implementation implements="nsIDOMEventListener">
268 // to avoid reflows we cache the width of the nub.
269 this.cachedWidthNub = 0;
270 this._selectedChat = null;
273 <field name="innerbox" readonly="true">
274 document.getAnonymousElementByAttribute(this, "anonid", "innerbox");
277 <field name="menupopup" readonly="true">
278 document.getAnonymousElementByAttribute(this, "anonid", "nubMenu");
281 <field name="nub" readonly="true">
282 document.getAnonymousElementByAttribute(this, "anonid", "nub");
285 <method name="focus">
287 if (!this.selectedChat)
289 Services.focus.focusedWindow = this.selectedChat.contentWindow;
293 <method name="_isChatFocused">
294 <parameter name="aChatbox"/>
296 // If there are no XBL bindings for the chat it can't be focused.
297 if (!aChatbox.content)
299 let fw = Services.focus.focusedWindow;
302 // We want to see if the focused window is in the subtree below our browser...
303 let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor)
304 .getInterface(Ci.nsIWebNavigation)
305 .QueryInterface(Ci.nsIDocShell)
307 return containingBrowser == aChatbox.content;
311 <property name="selectedChat">
313 return this._selectedChat;
316 // this is pretty horrible, but we:
317 // * want to avoid doing touching 'selected' attribute when the
318 // specified chat is already selected.
319 // * remove 'activity' attribute on newly selected tab *even if*
320 // newly selected is already selected.
321 // * need to handle either current or new being null.
322 if (this._selectedChat != val) {
323 if (this._selectedChat) {
324 this._selectedChat.removeAttribute("selected");
326 this._selectedChat = val;
328 this._selectedChat.setAttribute("selected", "true");
332 this._selectedChat.removeAttribute("activity");
337 <field name="menuitemMap">new WeakMap()</field>
338 <field name="chatboxForURL">new Map();</field>
340 <property name="hasCollapsedChildren">
342 return !!this.querySelector("[collapsed]");
346 <property name="collapsedChildren">
348 // A generator yielding all collapsed chatboxes, in the order in
349 // which they should be restored.
350 let child = this.lastElementChild;
354 child = child.previousElementSibling;
359 <property name="visibleChildren">
361 // A generator yielding all non-collapsed chatboxes.
362 let child = this.firstElementChild;
364 if (!child.collapsed)
366 child = child.nextElementSibling;
371 <property name="collapsibleChildren">
373 // A generator yielding all children which are able to be collapsed
374 // in the order in which they should be collapsed.
375 // (currently this is all visible ones other than the selected one.)
376 for (let child of this.visibleChildren)
377 if (child != this.selectedChat)
382 <method name="_selectAnotherChat">
384 // Select a different chat (as the currently selected one is no
385 // longer suitable as the selection - maybe it is being minimized or
386 // closed.) We only select non-minimized and non-collapsed chats,
387 // and if none are found, set the selectedChat to null.
388 // It's possible in the future we will track most-recently-selected
389 // chats or similar to find the "best" candidate - for now though
390 // the choice is somewhat arbitrary.
391 let moveFocus = this.selectedChat && this._isChatFocused(this.selectedChat);
392 for (let other of this.children) {
393 if (other != this.selectedChat && !other.minimized && !other.collapsed) {
394 this.selectedChat = other;
400 // can't find another - so set no chat as selected.
401 this.selectedChat = null;
405 <method name="updateTitlebar">
406 <parameter name="aChatbox"/>
408 if (aChatbox.collapsed) {
409 let menuitem = this.menuitemMap.get(aChatbox);
410 if (aChatbox.getAttribute("activity")) {
411 menuitem.setAttribute("activity", true);
412 this.nub.setAttribute("activity", true);
414 menuitem.setAttribute("label", aChatbox.getAttribute("label"));
415 menuitem.setAttribute("image", aChatbox.getAttribute("image"));
420 <method name="calcTotalWidthOf">
421 <parameter name="aElement"/>
423 let cs = document.defaultView.getComputedStyle(aElement);
424 let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight);
425 return aElement.getBoundingClientRect().width + margins;
429 <method name="getTotalChildWidth">
430 <parameter name="aChatbox"/>
432 // These are from the CSS for the chatbox and must be kept in sync.
433 // We can't use calcTotalWidthOf due to the transitions...
434 const CHAT_WIDTH_OPEN = 260;
435 const CHAT_WIDTH_MINIMIZED = 160;
436 return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : CHAT_WIDTH_OPEN;
440 <method name="collapseChat">
441 <parameter name="aChatbox"/>
443 // we ensure that the cached width for a child of this type is
444 // up-to-date so we can use it when resizing.
445 this.getTotalChildWidth(aChatbox);
446 aChatbox.collapsed = true;
447 aChatbox.isActive = false;
448 let menu = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem");
449 menu.setAttribute("class", "menuitem-iconic");
450 menu.setAttribute("label", aChatbox.contentDocument.title);
451 menu.setAttribute("image", aChatbox.getAttribute("image"));
452 menu.chat = aChatbox;
453 this.menuitemMap.set(aChatbox, menu);
454 this.menupopup.appendChild(menu);
455 this.nub.collapsed = false;
459 <method name="showChat">
460 <parameter name="aChatbox"/>
461 <parameter name="aMode"/>
463 if ((aMode != "minimized") && aChatbox.minimized)
464 aChatbox.minimized = false;
465 if (this.selectedChat != aChatbox)
466 this.selectedChat = aChatbox;
467 if (!aChatbox.collapsed)
468 return; // already showing - no more to do.
469 this._showChat(aChatbox);
470 // showing a collapsed chat might mean another needs to be collapsed
476 <method name="_showChat">
477 <parameter name="aChatbox"/>
479 // the actual implementation - doesn't check for overflow, assumes
481 let menuitem = this.menuitemMap.get(aChatbox);
482 this.menuitemMap.delete(aChatbox);
483 this.menupopup.removeChild(menuitem);
484 aChatbox.collapsed = false;
485 aChatbox.isActive = !aChatbox.minimized;
489 <method name="remove">
490 <parameter name="aChatbox"/>
492 this._remove(aChatbox);
493 // The removal of a chat may mean a collapsed one can spring up,
494 // or that the popup should be hidden. We also defer the selection
495 // of another chat until after a resize, as a new candidate may
496 // become uncollapsed after the resize.
498 if (this.selectedChat == aChatbox) {
499 this._selectAnotherChat();
504 <method name="_remove">
505 <parameter name="aChatbox"/>
507 this.removeChild(aChatbox);
508 // child might have been collapsed.
509 let menuitem = this.menuitemMap.get(aChatbox);
511 this.menuitemMap.delete(aChatbox);
512 this.menupopup.removeChild(menuitem);
514 this.chatboxForURL.delete(aChatbox.src);
518 <method name="openChat">
519 <parameter name="aOrigin"/>
520 <parameter name="aTitle"/>
521 <parameter name="aURL"/>
522 <parameter name="aMode"/>
523 <parameter name="aCallback"/>
525 let cb = this.chatboxForURL.get(aURL);
529 this.showChat(cb, aMode);
531 if (cb._callbacks == null) {
532 // Chatbox has already been created, so callback now.
535 // Chatbox is yet to have bindings created...
536 cb._callbacks.push(aCallback);
541 this.chatboxForURL.delete(aURL);
543 cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox");
546 // _callbacks is a javascript property instead of a <field> as it
547 // must exist before the (possibly delayed) bindings are created.
548 cb._callbacks.push(aCallback);
550 // src also a javascript property; the src attribute is set in the ctor.
552 if (aMode == "minimized")
553 cb.setAttribute("minimized", "true");
554 cb.setAttribute("origin", aOrigin);
555 cb.setAttribute("label", aTitle);
556 this.insertBefore(cb, this.firstChild);
557 this.selectedChat = cb;
558 this.chatboxForURL.set(aURL, Cu.getWeakReference(cb));
564 <method name="resize">
566 // Checks the current size against the collapsed state of children
567 // and collapses or expands as necessary such that as many as possible
569 // So 2 basic strategies:
570 // * Collapse/Expand one at a time until we can't collapse/expand any
571 // more - but this is one reflow per change.
572 // * Calculate the dimensions ourself and choose how many to collapse
573 // or expand based on this, then do them all in one go. This is one
574 // reflow regardless of how many we change.
575 // So we go the more complicated but more efficient second option...
576 let availWidth = this.getBoundingClientRect().width;
577 let currentWidth = 0;
578 if (!this.nub.collapsed) { // the nub is visible.
579 if (!this.cachedWidthNub)
580 this.cachedWidthNub = this.calcTotalWidthOf(this.nub);
581 currentWidth += this.cachedWidthNub;
583 for (let child of this.visibleChildren) {
584 currentWidth += this.getTotalChildWidth(child);
587 if (currentWidth > availWidth) {
588 // we need to collapse some.
590 for (let child of this.collapsibleChildren) {
591 if (currentWidth <= availWidth)
593 toCollapse.push(child);
594 currentWidth -= this.getTotalChildWidth(child);
596 if (toCollapse.length) {
597 for (let child of toCollapse)
598 this.collapseChat(child);
600 } else if (currentWidth < availWidth) {
601 // we *might* be able to expand some - see how many.
602 // XXX - if this was clever, it could know when removing the nub
603 // leaves enough space to show all collapsed
605 for (let child of this.collapsedChildren) {
606 currentWidth += this.getTotalChildWidth(child);
607 if (currentWidth > availWidth)
611 for (let child of toShow)
612 this._showChat(child);
614 // If none remain collapsed remove the nub.
615 if (!this.hasCollapsedChildren) {
616 this.nub.collapsed = true;
619 // else: achievement unlocked - we are pixel-perfect!
623 <method name="handleEvent">
624 <parameter name="aEvent"/>
626 if (aEvent.type == "resize") {
632 <method name="_getDragTarget">
633 <parameter name="event"/>
635 return event.target.localName == "chatbox" ? event.target : null;
639 <!-- Moves a chatbox to a new window. Returns a promise that is resolved
640 once the move to the other window is complete.
642 <method name="detachChatbox">
643 <parameter name="aChatbox"/>
644 <parameter name="aOptions"/>
646 let deferred = Promise.defer();
648 for (let name in aOptions)
649 options += "," + name + "=" + aOptions[name];
651 let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul",
652 "_blank", "chrome,all,dialog=no" + options);
654 otherWin.addEventListener("load", function _chatLoad(event) {
655 if (event.target != otherWin.document)
658 otherWin.removeEventListener("load", _chatLoad, true);
659 let otherChatbox = otherWin.document.getElementById("chatter");
660 aChatbox.swapDocShells(otherChatbox);
662 deferred.resolve(otherChatbox);
664 return deferred.promise;
671 <handler event="popupshown"><![CDATA[
672 this.nub.removeAttribute("activity");
674 <handler event="load"><![CDATA[
675 window.addEventListener("resize", this, true);
677 <handler event="unload"><![CDATA[
678 window.removeEventListener("resize", this, true);
681 <handler event="dragstart"><![CDATA[
682 // chat window dragging is essentially duplicated from tabbrowser.xml
683 // to acheive the same visual experience
684 let chatbox = this._getDragTarget(event);
689 let dt = event.dataTransfer;
690 // we do not set a url in the drag data to prevent moving to tabbrowser
691 // or otherwise having unexpected drop handlers do something with our
693 dt.mozSetDataAt("application/x-moz-chatbox", chatbox, 0);
695 // Set the cursor to an arrow during tab drags.
696 dt.mozCursor = "default";
698 // Create a canvas to which we capture the current tab.
699 // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
700 // canvas size (in CSS pixels) to the window's backing resolution in order
701 // to get a full-resolution drag image for use on HiDPI displays.
702 let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils);
703 let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
704 let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
705 canvas.mozOpaque = true;
706 canvas.width = 160 * scale;
707 canvas.height = 90 * scale;
708 PageThumbs.captureToCanvas(chatbox.contentWindow, canvas);
709 dt.setDragImage(canvas, -16 * scale, -16 * scale);
711 event.stopPropagation();
714 <handler event="dragend"><![CDATA[
715 let dt = event.dataTransfer;
716 let draggedChat = dt.mozGetDataAt("application/x-moz-chatbox", 0);
717 if (dt.mozUserCancelled || dt.dropEffect != "none") {
721 let eX = event.screenX;
722 let eY = event.screenY;
723 // screen.availLeft et. al. only check the screen that this window is on,
724 // but we want to look at the screen the tab is being dropped onto.
725 let sX = {}, sY = {}, sWidth = {}, sHeight = {};
726 Cc["@mozilla.org/gfx/screenmanager;1"]
727 .getService(Ci.nsIScreenManager)
728 .screenForRect(eX, eY, 1, 1)
729 .GetAvailRect(sX, sY, sWidth, sHeight);
730 // default size for the chat window as used in chatWindow.xul, use them
731 // here to attempt to keep the window fully within the screen when
732 // opening at the drop point. If the user has resized the window to
733 // something larger (which gets persisted), at least a good portion of
734 // the window should still be within the screen.
737 // ensure new window entirely within screen
738 let left = Math.min(Math.max(eX, sX.value),
739 sX.value + sWidth.value - winWidth);
740 let top = Math.min(Math.max(eY, sY.value),
741 sY.value + sHeight.value - winHeight);
743 let title = draggedChat.content.getAttribute("title");
744 this.detachChatbox(draggedChat, { screenX: left, screenY: top }).then(
746 chatbox.contentWindow.document.title = title;
749 event.stopPropagation();