Mozilla Cross-Reference mozilla-central
mozilla/ browser/ base/ content/ browser-social.js
Hg Log
Hg Blame
Diff file
Raw file
view using tree:
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 // the "exported" symbols
6 let SocialUI,
7     SocialFlyout,
8     SocialMarks,
9     SocialShare,
10     SocialSidebar,
11     SocialStatus,
12     SocialActivationListener;
13 
14 (function() {
15 
16 XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame",
17   "resource:///modules/PanelFrame.jsm");
18 
19 XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() {
20   let tmp = {};
21   Cu.import("resource:///modules/Social.jsm", tmp);
22   return tmp.OpenGraphBuilder;
23 });
24 
25 XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() {
26   let tmp = {};
27   Cu.import("resource:///modules/Social.jsm", tmp);
28   return tmp.DynamicResizeWatcher;
29 });
30 
31 XPCOMUtils.defineLazyGetter(this, "sizeSocialPanelToContent", function() {
32   let tmp = {};
33   Cu.import("resource:///modules/Social.jsm", tmp);
34   return tmp.sizeSocialPanelToContent;
35 });
36 
37 XPCOMUtils.defineLazyGetter(this, "CreateSocialStatusWidget", function() {
38   let tmp = {};
39   Cu.import("resource:///modules/Social.jsm", tmp);
40   return tmp.CreateSocialStatusWidget;
41 });
42 
43 XPCOMUtils.defineLazyGetter(this, "CreateSocialMarkWidget", function() {
44   let tmp = {};
45   Cu.import("resource:///modules/Social.jsm", tmp);
46   return tmp.CreateSocialMarkWidget;
47 });
48 
49 XPCOMUtils.defineLazyGetter(this, "hookWindowCloseForPanelClose", function() {
50   let tmp = {};
51   Cu.import("resource://gre/modules/MozSocialAPI.jsm", tmp);
52   return tmp.hookWindowCloseForPanelClose;
53 });
54 
55 SocialUI = {
56   _initialized: false,
57 
58   // Called on delayed startup to initialize the UI
59   init: function SocialUI_init() {
60     if (this._initialized) {
61       return;
62     }
63 
64     Services.obs.addObserver(this, "social:ambient-notification-changed", false);
65     Services.obs.addObserver(this, "social:profile-changed", false);
66     Services.obs.addObserver(this, "social:frameworker-error", false);
67     Services.obs.addObserver(this, "social:providers-changed", false);
68     Services.obs.addObserver(this, "social:provider-reload", false);
69     Services.obs.addObserver(this, "social:provider-enabled", false);
70     Services.obs.addObserver(this, "social:provider-disabled", false);
71 
72     Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
73 
74     CustomizableUI.addListener(this);
75     SocialActivationListener.init();
76 
77     // menupopups that list social providers. we only populate them when shown,
78     // and if it has not been done already.
79     document.getElementById("viewSidebarMenu").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
80     document.getElementById("social-statusarea-popup").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
81 
82     Social.init().then((update) => {
83       if (update)
84         this._providersChanged();
85       // handle SessionStore for the sidebar state
86       SocialSidebar.restoreWindowState();
87     });
88 
89     this._initialized = true;
90   },
91 
92   // Called on window unload
93   uninit: function SocialUI_uninit() {
94     if (!this._initialized) {
95       return;
96     }
97     SocialSidebar.saveWindowState();
98 
99     Services.obs.removeObserver(this, "social:ambient-notification-changed");
100     Services.obs.removeObserver(this, "social:profile-changed");
101     Services.obs.removeObserver(this, "social:frameworker-error");
102     Services.obs.removeObserver(this, "social:providers-changed");
103     Services.obs.removeObserver(this, "social:provider-reload");
104     Services.obs.removeObserver(this, "social:provider-enabled");
105     Services.obs.removeObserver(this, "social:provider-disabled");
106 
107     Services.prefs.removeObserver("social.toast-notifications.enabled", this);
108     CustomizableUI.removeListener(this);
109     SocialActivationListener.uninit();
110 
111     document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
112     document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
113 
114     this._initialized = false;
115   },
116 
117   observe: function SocialUI_observe(subject, topic, data) {
118     switch (topic) {
119       case "social:provider-enabled":
120         SocialMarks.populateToolbarPalette();
121         SocialStatus.populateToolbarPalette();
122         break;
123       case "social:provider-disabled":
124         SocialMarks.removeProvider(data);
125         SocialStatus.removeProvider(data);
126         SocialSidebar.disableProvider(data);
127         break;
128       case "social:provider-reload":
129         SocialStatus.reloadProvider(data);
130         // if the reloaded provider is our current provider, fall through
131         // to social:providers-changed so the ui will be reset
132         if (!SocialSidebar.provider || SocialSidebar.provider.origin != data)
133           return;
134         // currently only the sidebar and flyout have a selected provider.
135         // sidebar provider has changed (possibly to null), ensure the content
136         // is unloaded and the frames are reset, they will be loaded in
137         // providers-changed below if necessary.
138         SocialSidebar.unloadSidebar();
139         SocialFlyout.unload();
140         // fall through to providers-changed to ensure the reloaded provider
141         // is correctly reflected in any UI and the multi-provider menu
142       case "social:providers-changed":
143         this._providersChanged();
144         break;
145       // Provider-specific notifications
146       case "social:ambient-notification-changed":
147         SocialStatus.updateButton(data);
148         break;
149       case "social:profile-changed":
150         // make sure anything that happens here only affects the provider for
151         // which the profile is changing, and that anything we call actually
152         // needs to change based on profile data.
153         SocialStatus.updateButton(data);
154         break;
155       case "social:frameworker-error":
156         if (this.enabled && SocialSidebar.provider && SocialSidebar.provider.origin == data) {
157           SocialSidebar.setSidebarErrorMessage();
158         }
159         break;
160       case "nsPref:changed":
161         if (data == "social.toast-notifications.enabled") {
162           SocialSidebar.updateToggleNotifications();
163         }
164         break;
165     }
166   },
167 
168   _providersChanged: function() {
169     SocialSidebar.clearProviderMenus();
170     SocialSidebar.update();
171     SocialShare.populateProviderMenu();
172     SocialStatus.populateToolbarPalette();
173     SocialMarks.populateToolbarPalette();
174   },
175 
176   showLearnMore: function() {
177     let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
178     openUILinkIn(url, "tab");
179   },
180 
181   closeSocialPanelForLinkTraversal: function (target, linkNode) {
182     // No need to close the panel if this traversal was not retargeted
183     if (target == "" || target == "_self")
184       return;
185 
186     // Check to see whether this link traversal was in a social panel
187     let win = linkNode.ownerDocument.defaultView;
188     let container = win.QueryInterface(Ci.nsIInterfaceRequestor)
189                                   .getInterface(Ci.nsIWebNavigation)
190                                   .QueryInterface(Ci.nsIDocShell)
191                                   .chromeEventHandler;
192     let containerParent = container.parentNode;
193     if (containerParent.classList.contains("social-panel") &&
194         containerParent instanceof Ci.nsIDOMXULPopupElement) {
195       // allow the link traversal to finish before closing the panel
196       setTimeout(() => {
197         containerParent.hidePopup();
198       }, 0);
199     }
200   },
201 
202   get _chromeless() {
203     // Is this a popup window that doesn't want chrome shown?
204     let docElem = document.documentElement;
205     // extrachrome is not restored during session restore, so we need
206     // to check for the toolbar as well.
207     let chromeless = docElem.getAttribute("chromehidden").contains("extrachrome") ||
208                      docElem.getAttribute('chromehidden').contains("toolbar");
209     // This property is "fixed" for a window, so avoid doing the check above
210     // multiple times...
211     delete this._chromeless;
212     this._chromeless = chromeless;
213     return chromeless;
214   },
215 
216   get enabled() {
217     // Returns whether social is enabled *for this window*.
218     if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window))
219       return false;
220     return Social.providers.length > 0;
221   },
222 
223   canShareOrMarkPage: function(aURI) {
224     // Bug 898706 we do not enable social in private sessions since frameworker
225     // would be shared between private and non-private windows
226     if (PrivateBrowsingUtils.isWindowPrivate(window))
227       return false;
228 
229     return (aURI && (aURI.schemeIs('http') || aURI.schemeIs('https')));
230   },
231 
232   onCustomizeEnd: function(aWindow) {
233     if (aWindow != window)
234       return;
235     // customization mode gets buttons out of sync with command updating, fix
236     // the disabled state
237     let canShare = this.canShareOrMarkPage(gBrowser.currentURI);
238     let shareButton = SocialShare.shareButton;
239     if (shareButton) {
240       if (canShare) {
241         shareButton.removeAttribute("disabled")
242       } else {
243         shareButton.setAttribute("disabled", "true")
244       }
245     }
246     // update the disabled state of the button based on the command
247     for (let node of SocialMarks.nodes) {
248       if (canShare) {
249         node.removeAttribute("disabled")
250       } else {
251         node.setAttribute("disabled", "true")
252       }
253     }
254   },
255 
256   // called on tab/urlbar/location changes and after customization. Update
257   // anything that is tab specific.
258   updateState: function() {
259     if (location == "about:customizing")
260       return;
261     goSetCommandEnabled("Social:PageShareOrMark", this.canShareOrMarkPage(gBrowser.currentURI));
262     if (!SocialUI.enabled)
263       return;
264     // larger update that may change button icons
265     SocialMarks.update();
266   }
267 }
268 
269 // message manager handlers
270 SocialActivationListener = {
271   init: function() {
272     messageManager.addMessageListener("Social:Activation", this);
273   },
274   uninit: function() {
275     messageManager.removeMessageListener("Social:Activation", this);
276   },
277   receiveMessage: function(aMessage) {
278     let data = aMessage.json;
279     let browser = aMessage.target;
280     data.window = window;
281     // if the source if the message is the share panel, we do a one-click
282     // installation. The source of activations is controlled by the
283     // social.directories preference
284     let options;
285     if (browser == SocialShare.iframe && Services.prefs.getBoolPref("social.share.activationPanelEnabled")) {
286       options = { bypassContentCheck: true, bypassInstallPanel: true };
287     }
288 
289     // If we are in PB mode, we silently do nothing (bug 829404 exists to
290     // do something sensible here...)
291     if (PrivateBrowsingUtils.isWindowPrivate(window))
292       return;
293     Social.installProvider(data, function(manifest) {
294       Social.activateFromOrigin(manifest.origin, function(provider) {
295         if (provider.sidebarURL) {
296           SocialSidebar.show(provider.origin);
297         }
298         if (provider.shareURL) {
299           // Ensure that the share button is somewhere usable.
300           // SocialShare.shareButton may return null if it is in the menu-panel
301           // and has never been visible, so we check the widget directly. If
302           // there is no area for the widget we move it into the toolbar.
303           let widget = CustomizableUI.getWidget("social-share-button");
304           if (!widget.areaType) {
305             CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
306             // ensure correct state
307             SocialUI.onCustomizeEnd(window);
308           }
309 
310           // make this new provider the selected provider. If the panel hasn't
311           // been opened, we need to make the frame first.
312           SocialShare._createFrame();
313           SocialShare.iframe.setAttribute('src', 'data:text/plain;charset=utf8,');
314           SocialShare.iframe.setAttribute('origin', provider.origin);
315           // get the right button selected
316           SocialShare.populateProviderMenu();
317           if (SocialShare.panel.state == "open") {
318             SocialShare.sharePage(provider.origin);
319           }
320         }
321         if (provider.postActivationURL) {
322           // if activated from an open share panel, we load the landing page in
323           // a background tab
324           gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"});
325         }
326       });
327     }, options);
328   }
329 }
330 
331 SocialFlyout = {
332   get panel() {
333     return document.getElementById("social-flyout-panel");
334   },
335 
336   get iframe() {
337     if (!this.panel.firstChild)
338       this._createFrame();
339     return this.panel.firstChild;
340   },
341 
342   dispatchPanelEvent: function(name) {
343     let doc = this.iframe.contentDocument;
344     let evt = doc.createEvent("CustomEvent");
345     evt.initCustomEvent(name, true, true, {});
346     doc.documentElement.dispatchEvent(evt);
347   },
348 
349   _createFrame: function() {
350     let panel = this.panel;
351     if (!SocialUI.enabled || panel.firstChild)
352       return;
353     // create and initialize the panel for this window
354     let iframe = document.createElement("iframe");
355     iframe.setAttribute("type", "content");
356     iframe.setAttribute("class", "social-panel-frame");
357     iframe.setAttribute("flex", "1");
358     iframe.setAttribute("tooltip", "aHTMLTooltip");
359     iframe.setAttribute("origin", SocialSidebar.provider.origin);
360     panel.appendChild(iframe);
361   },
362 
363   setFlyoutErrorMessage: function SF_setFlyoutErrorMessage() {
364     this.iframe.removeAttribute("src");
365     this.iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
366                                  encodeURIComponent(this.iframe.getAttribute("origin")),
367                                  null, null, null, null);
368     sizeSocialPanelToContent(this.panel, this.iframe);
369   },
370 
371   unload: function() {
372     let panel = this.panel;
373     panel.hidePopup();
374     if (!panel.firstChild)
375       return
376     let iframe = panel.firstChild;
377     if (iframe.socialErrorListener)
378       iframe.socialErrorListener.remove();
379     panel.removeChild(iframe);
380   },
381 
382   onShown: function(aEvent) {
383     let panel = this.panel;
384     let iframe = this.iframe;
385     this._dynamicResizer = new DynamicResizeWatcher();
386     iframe.docShell.isActive = true;
387     iframe.docShell.isAppTab = true;
388     if (iframe.contentDocument.readyState == "complete") {
389       this._dynamicResizer.start(panel, iframe);
390       this.dispatchPanelEvent("socialFrameShow");
391     } else {
392       // first time load, wait for load and dispatch after load
393       iframe.addEventListener("load", function panelBrowserOnload(e) {
394         iframe.removeEventListener("load", panelBrowserOnload, true);
395         setTimeout(function() {
396           if (SocialFlyout._dynamicResizer) { // may go null if hidden quickly
397             SocialFlyout._dynamicResizer.start(panel, iframe);
398             SocialFlyout.dispatchPanelEvent("socialFrameShow");
399           }
400         }, 0);
401       }, true);
402     }
403   },
404 
405   onHidden: function(aEvent) {
406     this._dynamicResizer.stop();
407     this._dynamicResizer = null;
408     this.iframe.docShell.isActive = false;
409     this.dispatchPanelEvent("socialFrameHide");
410   },
411 
412   load: function(aURL, cb) {
413     if (!SocialSidebar.provider)
414       return;
415 
416     this.panel.hidden = false;
417     let iframe = this.iframe;
418     // same url with only ref difference does not cause a new load, so we
419     // want to go right to the callback
420     let src = iframe.contentDocument && iframe.contentDocument.documentURIObject;
421     if (!src || !src.equalsExceptRef(Services.io.newURI(aURL, null, null))) {
422       iframe.addEventListener("load", function documentLoaded() {
423         iframe.removeEventListener("load", documentLoaded, true);
424         cb();
425       }, true);
426       Social.setErrorListener(iframe, SocialFlyout.setFlyoutErrorMessage.bind(SocialFlyout))
427       iframe.setAttribute("src", aURL);
428     } else {
429       // we still need to set the src to trigger the contents hashchange event
430       // for ref changes
431       iframe.setAttribute("src", aURL);
432       cb();
433     }
434   },
435 
436   open: function(aURL, yOffset, aCallback) {
437     // Hide any other social panels that may be open.
438     document.getElementById("social-notification-panel").hidePopup();
439 
440     if (!SocialUI.enabled)
441       return;
442     let panel = this.panel;
443     let iframe = this.iframe;
444 
445     this.load(aURL, function() {
446       sizeSocialPanelToContent(panel, iframe);
447       let anchor = document.getElementById("social-sidebar-browser");
448       if (panel.state == "open") {
449         panel.moveToAnchor(anchor, "start_before", 0, yOffset, false);
450       } else {
451         panel.openPopup(anchor, "start_before", 0, yOffset, false, false);
452       }
453       if (aCallback) {
454         try {
455           aCallback(iframe.contentWindow);
456         } catch(e) {
457           Cu.reportError(e);
458         }
459       }
460     });
461   }
462 }
463 
464 SocialShare = {
465   get _dynamicResizer() {
466     delete this._dynamicResizer;
467     this._dynamicResizer = new DynamicResizeWatcher();
468     return this._dynamicResizer;
469   },
470 
471   // Share panel may be attached to the overflow or menu button depending on
472   // customization, we need to manage open state of the anchor.
473   get anchor() {
474     let widget = CustomizableUI.getWidget("social-share-button");
475     return widget.forWindow(window).anchor;
476   },
477   get panel() {
478     return document.getElementById("social-share-panel");
479   },
480 
481   get iframe() {
482     // panel.firstChild is our toolbar hbox, panel.lastChild is the iframe
483     // container hbox used for an interstitial "loading" graphic
484     return this.panel.lastChild.firstChild;
485   },
486 
487   uninit: function () {
488     if (this.iframe) {
489       this.iframe.remove();
490     }
491   },
492 
493   _createFrame: function() {
494     let panel = this.panel;
495     if (this.iframe)
496       return;
497     this.panel.hidden = false;
498     // create and initialize the panel for this window
499     let iframe = document.createElement("browser");
500     iframe.setAttribute("type", "content");
501     iframe.setAttribute("class", "social-share-frame");
502     iframe.setAttribute("context", "contentAreaContextMenu");
503     iframe.setAttribute("tooltip", "aHTMLTooltip");
504     iframe.setAttribute("disableglobalhistory", "true");
505     iframe.setAttribute("flex", "1");
506     panel.lastChild.appendChild(iframe);
507     iframe.addEventListener("load", function _firstload() {
508       iframe.removeEventListener("load", _firstload, true);
509       iframe.messageManager.loadFrameScript("chrome://browser/content/content.js", true);
510     }, true);
511     this.populateProviderMenu();
512   },
513 
514   getSelectedProvider: function() {
515     let provider;
516     let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin");
517     if (lastProviderOrigin) {
518       provider = Social._getProviderFromOrigin(lastProviderOrigin);
519     }
520     return provider;
521   },
522 
523   createTooltip: function(event) {
524     let tt = event.target;
525     let provider = Social._getProviderFromOrigin(tt.triggerNode.getAttribute("origin"));
526     tt.firstChild.setAttribute("value", provider.name);
527     tt.lastChild.setAttribute("value", provider.origin);
528   },
529 
530   populateProviderMenu: function() {
531     if (!this.iframe)
532       return;
533     let providers = [p for (p of Social.providers) if (p.shareURL)];
534     let hbox = document.getElementById("social-share-provider-buttons");
535     // remove everything before the add-share-provider button (which should also
536     // be lastChild if any share providers were added)
537     let addButton = document.getElementById("add-share-provider");
538     while (hbox.lastChild != addButton) {
539       hbox.removeChild(hbox.lastChild);
540     }
541     let selectedProvider = this.getSelectedProvider();
542     for (let provider of providers) {
543       let button = document.createElement("toolbarbutton");
544       button.setAttribute("class", "toolbarbutton-1 share-provider-button");
545       button.setAttribute("type", "radio");
546       button.setAttribute("group", "share-providers");
547       button.setAttribute("image", provider.iconURL);
548       button.setAttribute("tooltip", "share-button-tooltip");
549       button.setAttribute("origin", provider.origin);
550       button.setAttribute("label", provider.name);
551       button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
552       if (provider == selectedProvider) {
553         this.defaultButton = button;
554       }
555       hbox.appendChild(button);
556     }
557     if (!this.defaultButton) {
558       this.defaultButton = addButton;
559     }
560     this.defaultButton.setAttribute("checked", "true");
561   },
562 
563   get shareButton() {
564     // web-panels (bookmark/sidebar) don't include customizableui, so
565     // nsContextMenu fails when accessing shareButton, breaking
566     // browser_bug409481.js.
567     if (!window.CustomizableUI)
568       return null;
569     let widget = CustomizableUI.getWidget("social-share-button");
570     if (!widget || !widget.areaType)
571       return null;
572     return widget.forWindow(window).node;
573   },
574 
575   _onclick: function() {
576     Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(0);
577   },
578 
579   onShowing: function() {
580     this.anchor.setAttribute("open", "true");
581     this.iframe.addEventListener("click", this._onclick, true);
582   },
583 
584   onHidden: function() {
585     this.anchor.removeAttribute("open");
586     this.iframe.removeEventListener("click", this._onclick, true);
587     this.iframe.setAttribute("src", "data:text/plain;charset=utf8,");
588     // make sure that the frame is unloaded after it is hidden
589     this.iframe.docShell.createAboutBlankContentViewer(null);
590     this.currentShare = null;
591     // share panel use is over, purge any history
592     if (this.iframe.sessionHistory) {
593       let purge = this.iframe.sessionHistory.count;
594       if (purge > 0)
595         this.iframe.sessionHistory.PurgeHistory(purge);
596     }
597   },
598 
599   setErrorMessage: function() {
600     let iframe = this.iframe;
601     if (!iframe)
602       return;
603 
604     let url;
605     let origin = iframe.getAttribute("origin");
606     if (!origin) {
607       // directory site is down
608       url = "about:socialerror?mode=tryAgainOnly&directory=1&url=" + encodeURIComponent(iframe.getAttribute("src"));
609     } else {
610       url = "about:socialerror?mode=compactInfo&origin=" + encodeURIComponent(origin);
611     }
612     iframe.webNavigation.loadURI(url, null, null, null, null);
613     sizeSocialPanelToContent(this.panel, iframe);
614   },
615 
616   sharePage: function(providerOrigin, graphData, target) {
617     // if providerOrigin is undefined, we use the last-used provider, or the
618     // current/default provider.  The provider selection in the share panel
619     // will call sharePage with an origin for us to switch to.
620     this._createFrame();
621     let iframe = this.iframe;
622 
623     // graphData is an optional param that either defines the full set of data
624     // to be shared, or partial data about the current page. It is set by a call
625     // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST
626     // define at least url. If it is undefined, we're sharing the current url in
627     // the browser tab.
628     let pageData = graphData ? graphData : this.currentShare;
629     let sharedURI = pageData ? Services.io.newURI(pageData.url, null, null) :
630                                 gBrowser.currentURI;
631     if (!SocialUI.canShareOrMarkPage(sharedURI))
632       return;
633 
634     // the point of this action type is that we can use existing share
635     // endpoints (e.g. oexchange) that do not support additional
636     // socialapi functionality.  One tweak is that we shoot an event
637     // containing the open graph data.
638     let _dataFn;
639     if (!pageData || sharedURI == gBrowser.currentURI) {
640       messageManager.addMessageListener("PageMetadata:PageDataResult", _dataFn = (msg) => {
641         messageManager.removeMessageListener("PageMetadata:PageDataResult", _dataFn);
642         let pageData = msg.json;
643         if (graphData) {
644           // overwrite data retreived from page with data given to us as a param
645           for (let p in graphData) {
646             pageData[p] = graphData[p];
647           }
648         }
649         this.sharePage(providerOrigin, pageData, target);
650       });
651       gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData");
652       return;
653     }
654     // if this is a share of a selected item, get any microdata
655     if (!pageData.microdata && target) {
656       messageManager.addMessageListener("PageMetadata:MicrodataResult", _dataFn = (msg) => {
657         messageManager.removeMessageListener("PageMetadata:MicrodataResult", _dataFn);
658         pageData.microdata = msg.data;
659         this.sharePage(providerOrigin, pageData, target);
660       });
661       gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetMicrodata", null, { target });
662       return;
663     }
664     this.currentShare = pageData;
665 
666     let provider;
667     if (providerOrigin)
668       provider = Social._getProviderFromOrigin(providerOrigin);
669     else
670       provider = this.getSelectedProvider();
671     if (!provider || !provider.shareURL) {
672       this.showDirectory();
673       return;
674     }
675     // check the menu button
676     let hbox = document.getElementById("social-share-provider-buttons");
677     let btn = hbox.querySelector("[origin='" + provider.origin + "']");
678     if (btn)
679       btn.checked = true;
680 
681     let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData);
682 
683     this._dynamicResizer.stop();
684     let size = provider.getPageSize("share");
685     if (size) {
686       // let the css on the share panel define width, but height
687       // calculations dont work on all sites, so we allow that to be
688       // defined.
689       delete size.width;
690     }
691 
692     // if we've already loaded this provider/page share endpoint, we don't want
693     // to add another load event listener.
694     let endpointMatch = shareEndpoint == iframe.getAttribute("src");
695     if (endpointMatch) {
696       this._dynamicResizer.start(iframe.parentNode, iframe, size);
697       iframe.docShell.isActive = true;
698       iframe.docShell.isAppTab = true;
699       let evt = iframe.contentDocument.createEvent("CustomEvent");
700       evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
701       iframe.contentDocument.documentElement.dispatchEvent(evt);
702     } else {
703       iframe.parentNode.setAttribute("loading", "true");
704       // first time load, wait for load and dispatch after load
705       iframe.addEventListener("load", function panelBrowserOnload(e) {
706         iframe.removeEventListener("load", panelBrowserOnload, true);
707         iframe.docShell.isActive = true;
708         iframe.docShell.isAppTab = true;
709         iframe.parentNode.removeAttribute("loading");
710         // to support standard share endpoints mimick window.open by setting
711         // window.opener, some share endpoints rely on w.opener to know they
712         // should close the window when done.
713         iframe.contentWindow.opener = iframe.contentWindow;
714 
715         SocialShare._dynamicResizer.start(iframe.parentNode, iframe, size);
716 
717         let evt = iframe.contentDocument.createEvent("CustomEvent");
718         evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
719         iframe.contentDocument.documentElement.dispatchEvent(evt);
720       }, true);
721     }
722     // if the user switched between share providers we do not want that history
723     // available.
724     if (iframe.sessionHistory) {
725       let purge = iframe.sessionHistory.count;
726       if (purge > 0)
727         iframe.sessionHistory.PurgeHistory(purge);
728     }
729 
730     // always ensure that origin belongs to the endpoint
731     let uri = Services.io.newURI(shareEndpoint, null, null);
732     iframe.setAttribute("origin", provider.origin);
733     iframe.setAttribute("src", shareEndpoint);
734     this._openPanel();
735   },
736 
737   showDirectory: function() {
738     this._createFrame();
739     let iframe = this.iframe;
740     if (iframe.getAttribute("src") == "about:providerdirectory")
741       return;
742     iframe.removeAttribute("origin");
743     iframe.parentNode.setAttribute("loading", "true");
744     iframe.addEventListener("DOMContentLoaded", function _dcl(e) {
745       iframe.removeEventListener("DOMContentLoaded", _dcl, true);
746       iframe.parentNode.removeAttribute("loading");
747     }, true);
748 
749     iframe.addEventListener("load", function panelBrowserOnload(e) {
750       iframe.removeEventListener("load", panelBrowserOnload, true);
751 
752       hookWindowCloseForPanelClose(iframe.contentWindow);
753       SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
754 
755       iframe.addEventListener("unload", function panelBrowserOnload(e) {
756         iframe.removeEventListener("unload", panelBrowserOnload, true);
757         SocialShare._dynamicResizer.stop();
758       }, true);
759     }, true);
760     iframe.setAttribute("src", "about:providerdirectory");
761     this._openPanel();
762   },
763 
764   _openPanel: function() {
765     let anchor = document.getAnonymousElementByAttribute(this.anchor, "class", "toolbarbutton-icon");
766     this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
767     Social.setErrorListener(this.iframe, this.setErrorMessage.bind(this));
768     Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0);
769   }
770 };
771 
772 SocialSidebar = {
773   _openStartTime: 0,
774 
775   // Whether the sidebar can be shown for this window.
776   get canShow() {
777     if (!SocialUI.enabled || document.mozFullScreen)
778       return false;
779     return Social.providers.some(p => p.sidebarURL);
780   },
781 
782   // Whether the user has toggled the sidebar on (for windows where it can appear)
783   get opened() {
784     let broadcaster = document.getElementById("socialSidebarBroadcaster");
785     return !broadcaster.hidden;
786   },
787 
788   restoreWindowState: function() {
789     // Window state is used to allow different sidebar providers in each window.
790     // We also store the provider used in a pref as the default sidebar to
791     // maintain that state for users who do not restore window state. The
792     // existence of social.sidebar.provider means the sidebar is open with that
793     // provider.
794     this._initialized = true;
795     if (!this.canShow)
796       return;
797 
798     if (Services.prefs.prefHasUserValue("social.provider.current")) {
799       // "upgrade" when the first window opens if we have old prefs.  We get the
800       // values from prefs this one time, window state will be saved when this
801       // window is closed.
802       let origin = Services.prefs.getCharPref("social.provider.current");
803       Services.prefs.clearUserPref("social.provider.current");
804       // social.sidebar.open default was true, but we only opened if there was
805       // a current provider
806       let opened = origin && true;
807       if (Services.prefs.prefHasUserValue("social.sidebar.open")) {
808         opened = origin && Services.prefs.getBoolPref("social.sidebar.open");
809         Services.prefs.clearUserPref("social.sidebar.open");
810       }
811       let data = {
812         "hidden": !opened,
813         "origin": origin
814       };
815       SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data));
816     }
817 
818     let data = SessionStore.getWindowValue(window, "socialSidebar");
819     // if this window doesn't have it's own state, use the state from the opener
820     if (!data && window.opener && !window.opener.closed) {
821       try {
822         data = SessionStore.getWindowValue(window.opener, "socialSidebar");
823       } catch(e) {
824         // Window is not tracked, which happens on osx if the window is opened
825         // from the hidden window. That happens when you close the last window
826         // without quiting firefox, then open a new window.
827       }
828     }
829     if (data) {
830       data = JSON.parse(data);
831       document.getElementById("social-sidebar-browser").setAttribute("origin", data.origin);
832       if (!data.hidden)
833         this.show(data.origin);
834     } else if (Services.prefs.prefHasUserValue("social.sidebar.provider")) {
835       // no window state, use the global state if it is available
836       this.show(Services.prefs.getCharPref("social.sidebar.provider"));
837     }
838   },
839 
840   saveWindowState: function() {
841     let broadcaster = document.getElementById("socialSidebarBroadcaster");
842     let sidebarOrigin = document.getElementById("social-sidebar-browser").getAttribute("origin");
843     let data = {
844       "hidden": broadcaster.hidden,
845       "origin": sidebarOrigin
846     };
847     if (broadcaster.hidden) {
848       Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_OPEN_DURATION").add(Date.now()  / 1000 - this._openStartTime);
849     } else {
850       this._openStartTime = Date.now() / 1000;
851     }
852 
853     // Save a global state for users who do not restore state.
854     if (broadcaster.hidden)
855       Services.prefs.clearUserPref("social.sidebar.provider");
856     else
857       Services.prefs.setCharPref("social.sidebar.provider", sidebarOrigin);
858 
859     try {
860       SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data));
861     } catch(e) {
862       // window not tracked during uninit
863     }
864   },
865 
866   setSidebarVisibilityState: function(aEnabled) {
867     let sbrowser = document.getElementById("social-sidebar-browser");
868     // it's possible we'll be called twice with aEnabled=false so let's
869     // just assume we may often be called with the same state.
870     if (aEnabled == sbrowser.docShellIsActive)
871       return;
872     sbrowser.docShellIsActive = aEnabled;
873     let evt = sbrowser.contentDocument.createEvent("CustomEvent");
874     evt.initCustomEvent(aEnabled ? "socialFrameShow" : "socialFrameHide", true, true, {});
875     sbrowser.contentDocument.documentElement.dispatchEvent(evt);
876   },
877 
878   updateToggleNotifications: function() {
879     let command = document.getElementById("Social:ToggleNotifications");
880     command.setAttribute("checked", Services.prefs.getBoolPref("social.toast-notifications.enabled"));
881     command.setAttribute("hidden", !SocialUI.enabled);
882   },
883 
884   update: function SocialSidebar_update() {
885     // ensure we never update before restoreWindowState
886     if (!this._initialized)
887       return;
888     this.ensureProvider();
889     this.updateToggleNotifications();
890     this._updateHeader();
891     clearTimeout(this._unloadTimeoutId);
892     // Hide the toggle menu item if the sidebar cannot appear
893     let command = document.getElementById("Social:ToggleSidebar");
894     command.setAttribute("hidden", this.canShow ? "false" : "true");
895 
896     // Hide the sidebar if it cannot appear, or has been toggled off.
897     // Also set the command "checked" state accordingly.
898     let hideSidebar = !this.canShow || !this.opened;
899     let broadcaster = document.getElementById("socialSidebarBroadcaster");
900     broadcaster.hidden = hideSidebar;
901     command.setAttribute("checked", !hideSidebar);
902 
903     let sbrowser = document.getElementById("social-sidebar-browser");
904 
905     if (hideSidebar) {
906       sbrowser.removeEventListener("load", SocialSidebar._loadListener, true);
907       this.setSidebarVisibilityState(false);
908       // If we've been disabled, unload the sidebar content immediately;
909       // if the sidebar was just toggled to invisible, wait a timeout
910       // before unloading.
911       if (!this.canShow) {
912         this.unloadSidebar();
913       } else {
914         this._unloadTimeoutId = setTimeout(
915           this.unloadSidebar,
916           Services.prefs.getIntPref("social.sidebar.unload_timeout_ms")
917         );
918       }
919     } else {
920       sbrowser.setAttribute("origin", this.provider.origin);
921       if (this.provider.errorState == "frameworker-error") {
922         SocialSidebar.setSidebarErrorMessage();
923         return;
924       }
925 
926       // Make sure the right sidebar URL is loaded
927       if (sbrowser.getAttribute("src") != this.provider.sidebarURL) {
928         // we check readyState right after setting src, we need a new content
929         // viewer to ensure we are checking against the correct document.
930         sbrowser.docShell.createAboutBlankContentViewer(null);
931         Social.setErrorListener(sbrowser, this.setSidebarErrorMessage.bind(this));
932         // setting isAppTab causes clicks on untargeted links to open new tabs
933         sbrowser.docShell.isAppTab = true;
934         sbrowser.setAttribute("src", this.provider.sidebarURL);
935         PopupNotifications.locationChange(sbrowser);
936       }
937 
938       // if the document has not loaded, delay until it is
939       if (sbrowser.contentDocument.readyState != "complete") {
940         document.getElementById("social-sidebar-button").setAttribute("loading", "true");
941         sbrowser.addEventListener("load", SocialSidebar._loadListener, true);
942       } else {
943         this.setSidebarVisibilityState(true);
944       }
945     }
946     this._updateCheckedMenuItems(this.opened && this.provider ? this.provider.origin : null);
947   },
948 
949   _onclick: function() {
950     Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(3);
951   },
952 
953   _loadListener: function SocialSidebar_loadListener() {
954     let sbrowser = document.getElementById("social-sidebar-browser");
955     sbrowser.removeEventListener("load", SocialSidebar._loadListener, true);
956     document.getElementById("social-sidebar-button").removeAttribute("loading");
957     SocialSidebar.setSidebarVisibilityState(true);
958     sbrowser.addEventListener("click", SocialSidebar._onclick, true);
959   },
960 
961   unloadSidebar: function SocialSidebar_unloadSidebar() {
962     let sbrowser = document.getElementById("social-sidebar-browser");
963     if (!sbrowser.hasAttribute("origin"))
964       return;
965 
966     sbrowser.removeEventListener("click", SocialSidebar._onclick, true);
967     sbrowser.stop();
968     sbrowser.removeAttribute("origin");
969     sbrowser.setAttribute("src", "about:blank");
970     // We need to explicitly create a new content viewer because the old one
971     // doesn't get destroyed until about:blank has loaded (which does not happen
972     // as long as the element is hidden).
973     sbrowser.docShell.createAboutBlankContentViewer(null);
974     SocialFlyout.unload();
975   },
976 
977   _unloadTimeoutId: 0,
978 
979   setSidebarErrorMessage: function() {
980     let sbrowser = document.getElementById("social-sidebar-browser");
981     // a frameworker error "trumps" a sidebar error.
982     let origin = sbrowser.getAttribute("origin");
983     if (origin) {
984       origin = "&origin=" + encodeURIComponent(origin);
985     }
986     if (this.provider.errorState == "frameworker-error") {
987       sbrowser.setAttribute("src", "about:socialerror?mode=workerFailure" + origin);
988     } else {
989       let url = encodeURIComponent(this.provider.sidebarURL);
990       sbrowser.loadURI("about:socialerror?mode=tryAgain&url=" + url + origin, null, null);
991     }
992   },
993 
994   _provider: null,
995   ensureProvider: function() {
996     if (this._provider)
997       return;
998     // origin for sidebar is persisted, so get the previously selected sidebar
999     // first, otherwise fallback to the first provider in the list
1000     let sbrowser = document.getElementById("social-sidebar-browser");
1001     let origin = sbrowser.getAttribute("origin");
1002     let providers = [p for (p of Social.providers) if (p.sidebarURL)];
1003     let provider;
1004     if (origin)
1005       provider = Social._getProviderFromOrigin(origin);
1006     if (!provider && providers.length > 0)
1007       provider = providers[0];
1008     if (provider)
1009       this.provider = provider;
1010   },
1011 
1012   get provider() {
1013     return this._provider;
1014   },
1015 
1016   set provider(provider) {
1017     if (!provider || provider.sidebarURL) {
1018       this._provider = provider;
1019       this._updateHeader();
1020       this._updateCheckedMenuItems(provider && provider.origin);
1021       this.update();
1022     }
1023   },
1024 
1025   disableProvider: function(origin) {
1026     if (this._provider && this._provider.origin == origin) {
1027       this._provider = null;
1028       // force a selection of the next provider if there is one
1029       this.ensureProvider();
1030     }
1031   },
1032 
1033   _updateHeader: function() {
1034     let provider = this.provider;
1035     let image, title;
1036     if (provider) {
1037       image = "url(" + (provider.icon32URL || provider.iconURL) + ")";
1038       title = provider.name;
1039     }
1040     document.getElementById("social-sidebar-favico").style.listStyleImage = image;
1041     document.getElementById("social-sidebar-title").value = title;
1042   },
1043 
1044   _updateCheckedMenuItems: function(origin) {
1045     // update selected menuitems
1046     let menuitems = document.getElementsByClassName("social-provider-menuitem");
1047     for (let mi of menuitems) {
1048       if (origin && mi.getAttribute("origin") == origin) {
1049         mi.setAttribute("checked", "true");
1050         mi.setAttribute("oncommand", "SocialSidebar.hide();");
1051       } else if (mi.getAttribute("checked")) {
1052         mi.removeAttribute("checked");
1053         mi.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));");
1054       }
1055     }
1056   },
1057 
1058   show: function(origin) {
1059     // always show the sidebar, and set the provider
1060     let broadcaster = document.getElementById("socialSidebarBroadcaster");
1061     broadcaster.hidden = false;
1062     if (origin)
1063       this.provider = Social._getProviderFromOrigin(origin);
1064     else
1065       SocialSidebar.update();
1066     this.saveWindowState();
1067     Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_STATE").add(true);
1068   },
1069 
1070   hide: function() {
1071     let broadcaster = document.getElementById("socialSidebarBroadcaster");
1072     broadcaster.hidden = true;
1073     this._updateCheckedMenuItems();
1074     this.clearProviderMenus();
1075     SocialSidebar.update();
1076     this.saveWindowState();
1077     Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_STATE").add(false);
1078   },
1079 
1080   toggleSidebar: function SocialSidebar_toggle() {
1081     let broadcaster = document.getElementById("socialSidebarBroadcaster");
1082     if (broadcaster.hidden)
1083       this.show();
1084     else
1085       this.hide();
1086   },
1087 
1088   populateSidebarMenu: function(event) {
1089     // Providers are removed from the view->sidebar menu when there is a change
1090     // in providers, so we only have to populate onshowing if there are no
1091     // provider menus. We populate this menu so long as there are enabled
1092     // providers with sidebars.
1093     let popup = event.target;
1094     let providerMenuSeps = popup.getElementsByClassName("social-provider-menu");
1095     if (providerMenuSeps[0].previousSibling.nodeName == "menuseparator")
1096       SocialSidebar.populateProviderMenu(providerMenuSeps[0]);
1097   },
1098 
1099   clearProviderMenus: function() {
1100     // called when there is a change in the provider list we clear all menus,
1101     // they will be repopulated when the menu is shown
1102     let providerMenuSeps = document.getElementsByClassName("social-provider-menu");
1103     for (let providerMenuSep of providerMenuSeps) {
1104       while (providerMenuSep.previousSibling.nodeName == "menuitem") {
1105         let menu = providerMenuSep.parentNode;
1106         menu.removeChild(providerMenuSep.previousSibling);
1107       }
1108     }
1109   },
1110 
1111   populateProviderMenu: function(providerMenuSep) {
1112     let menu = providerMenuSep.parentNode;
1113     // selectable providers are inserted before the provider-menu seperator,
1114     // remove any menuitems in that area
1115     while (providerMenuSep.previousSibling.nodeName == "menuitem") {
1116       menu.removeChild(providerMenuSep.previousSibling);
1117     }
1118     // only show a selection in the sidebar header menu if there is more than one
1119     let providers = [p for (p of Social.providers) if (p.sidebarURL)];
1120     if (providers.length < 2 && menu.id != "viewSidebarMenu") {
1121       providerMenuSep.hidden = true;
1122       return;
1123     }
1124     let topSep = providerMenuSep.previousSibling;
1125     for (let provider of providers) {
1126       let menuitem = document.createElement("menuitem");
1127       menuitem.className = "menuitem-iconic social-provider-menuitem";
1128       menuitem.setAttribute("image", provider.iconURL);
1129       menuitem.setAttribute("label", provider.name);
1130       menuitem.setAttribute("origin", provider.origin);
1131       if (this.opened && provider == this.provider) {
1132         menuitem.setAttribute("checked", "true");
1133         menuitem.setAttribute("oncommand", "SocialSidebar.hide();");
1134       } else {
1135         menuitem.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));");
1136       }
1137       menu.insertBefore(menuitem, providerMenuSep);
1138     }
1139     topSep.hidden = topSep.nextSibling == providerMenuSep;
1140     providerMenuSep.hidden = !providerMenuSep.nextSibling;
1141   }
1142 }
1143 
1144 // this helper class is used by removable/customizable buttons to handle
1145 // widget creation/destruction
1146 
1147 // When a provider is installed we show all their UI so the user will see the
1148 // functionality of what they installed. The user can later customize the UI,
1149 // moving buttons around or off the toolbar.
1150 //
1151 // On startup, we create the button widgets of any enabled provider.
1152 // CustomizableUI handles placement and persistence of placement.
1153 function ToolbarHelper(type, createButtonFn, listener) {
1154   this._createButton = createButtonFn;
1155   this._type = type;
1156 
1157   if (listener) {
1158     CustomizableUI.addListener(listener);
1159     // remove this listener on window close
1160     window.addEventListener("unload", () => {
1161       CustomizableUI.removeListener(listener);
1162     });
1163   }
1164 }
1165 
1166 ToolbarHelper.prototype = {
1167   idFromOrigin: function(origin) {
1168     // this id needs to pass the checks in CustomizableUI, so remove characters
1169     // that wont pass.
1170     return this._type + "-" + Services.io.newURI(origin, null, null).hostPort.replace(/[\.:]/g,'-');
1171   },
1172 
1173   // should be called on disable of a provider
1174   removeProviderButton: function(origin) {
1175     CustomizableUI.destroyWidget(this.idFromOrigin(origin));
1176   },
1177 
1178   clearPalette: function() {
1179     [this.removeProviderButton(p.origin) for (p of Social.providers)];
1180   },
1181 
1182   // should be called on enable of a provider
1183   populatePalette: function() {
1184     if (!Social.enabled) {
1185       this.clearPalette();
1186       return;
1187     }
1188 
1189     // create any buttons that do not exist yet if they have been persisted
1190     // as a part of the UI (otherwise they belong in the palette).
1191     for (let provider of Social.providers) {
1192       let id = this.idFromOrigin(provider.origin);
1193       this._createButton(id, provider);
1194     }
1195   }
1196 }
1197 
1198 let SocialStatusWidgetListener = {
1199   _getNodeOrigin: function(aWidgetId) {
1200     // we rely on the button id being the same as the widget.
1201     let node = document.getElementById(aWidgetId);
1202     if (!node)
1203       return null
1204     if (!node.classList.contains("social-status-button"))
1205       return null
1206     return node.getAttribute("origin");
1207   },
1208   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
1209     let origin = this._getNodeOrigin(aWidgetId);
1210     if (origin)
1211       SocialStatus.updateButton(origin);
1212   },
1213   onWidgetRemoved: function(aWidgetId, aPrevArea) {
1214     let origin = this._getNodeOrigin(aWidgetId);
1215     if (!origin)
1216       return;
1217     // When a widget is demoted to the palette ('removed'), it's visual
1218     // style should change.
1219     SocialStatus.updateButton(origin);
1220     SocialStatus._removeFrame(origin);
1221   }
1222 }
1223 
1224 SocialStatus = {
1225   populateToolbarPalette: function() {
1226     this._toolbarHelper.populatePalette();
1227 
1228     for (let provider of Social.providers)
1229       this.updateButton(provider.origin);
1230   },
1231 
1232   removeProvider: function(origin) {
1233     this._removeFrame(origin);
1234     this._toolbarHelper.removeProviderButton(origin);
1235   },
1236 
1237   reloadProvider: function(origin) {
1238     let button = document.getElementById(this._toolbarHelper.idFromOrigin(origin));
1239     if (button && button.getAttribute("open") == "true")
1240       document.getElementById("social-notification-panel").hidePopup();
1241     this._removeFrame(origin);
1242   },
1243 
1244   _removeFrame: function(origin) {
1245     let notificationFrameId = "social-status-" + origin;
1246     let frame = document.getElementById(notificationFrameId);
1247     if (frame) {
1248       frame.parentNode.removeChild(frame);
1249     }
1250   },
1251 
1252   get _toolbarHelper() {
1253     delete this._toolbarHelper;
1254     this._toolbarHelper = new ToolbarHelper("social-status-button",
1255                                             CreateSocialStatusWidget,
1256                                             SocialStatusWidgetListener);
1257     return this._toolbarHelper;
1258   },
1259 
1260   updateButton: function(origin) {
1261     let id = this._toolbarHelper.idFromOrigin(origin);
1262     let widget = CustomizableUI.getWidget(id);
1263     if (!widget)
1264       return;
1265     let button = widget.forWindow(window).node;
1266     if (button) {
1267       // we only grab the first notification, ignore all others
1268       let provider = Social._getProviderFromOrigin(origin);
1269       let icons = provider.ambientNotificationIcons;
1270       let iconNames = Object.keys(icons);
1271       let notif = icons[iconNames[0]];
1272 
1273       // The image and tooltip need to be updated for both
1274       // ambient notification and profile changes.
1275       let iconURL = provider.icon32URL || provider.iconURL;
1276       let tooltiptext;
1277       if (!notif || !widget.areaType) {
1278         button.style.listStyleImage = "url(" + iconURL + ")";
1279         button.setAttribute("badge", "");
1280         button.setAttribute("aria-label", "");
1281         button.setAttribute("tooltiptext", provider.name);
1282         return;
1283       }
1284       button.style.listStyleImage = "url(" + (notif.iconURL || iconURL) + ")";
1285       button.setAttribute("tooltiptext", notif.label || provider.name);
1286 
1287       let badge = notif.counter || "";
1288       button.setAttribute("badge", badge);
1289       let ariaLabel = notif.label;
1290       // if there is a badge value, we must use a localizable string to insert it.
1291       if (badge)
1292         ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText",
1293                                                         [ariaLabel, badge]);
1294       button.setAttribute("aria-label", ariaLabel);
1295     }
1296   },
1297 
1298   _onclose: function(frame) {
1299     frame.removeEventListener("close", this._onclose, true);
1300     frame.removeEventListener("click", this._onclick, true);
1301     if (frame.socialErrorListener)
1302       frame.socialErrorListener.remove();
1303   },
1304 
1305   _onclick: function() {
1306     Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(1);
1307   },
1308 
1309   showPopup: function(aToolbarButton) {
1310     // attach our notification panel if necessary
1311     let origin = aToolbarButton.getAttribute("origin");
1312     let provider = Social._getProviderFromOrigin(origin);
1313 
1314     PanelFrame.showPopup(window, aToolbarButton, "social", origin,
1315                          provider.statusURL, provider.getPageSize("status"),
1316                          (frame) => {
1317                           frame.addEventListener("close", () => { SocialStatus._onclose(frame) }, true);
1318                           frame.addEventListener("click", this._onclick, true);
1319                           Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this));
1320                         });
1321     Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(1);
1322   },
1323 
1324   setPanelErrorMessage: function(aNotificationFrame) {
1325     if (!aNotificationFrame)
1326       return;
1327 
1328     let src = aNotificationFrame.getAttribute("src");
1329     aNotificationFrame.removeAttribute("src");
1330     let origin = aNotificationFrame.getAttribute("origin");
1331     aNotificationFrame.webNavigation.loadURI("about:socialerror?mode=tryAgainOnly&url=" +
1332                                             encodeURIComponent(src) + "&origin=" +
1333                                             encodeURIComponent(origin),
1334                                             null, null, null, null);
1335     let panel = aNotificationFrame.parentNode;
1336     sizeSocialPanelToContent(panel, aNotificationFrame);
1337   },
1338 
1339 };
1340 
1341 
1342 /**
1343  * SocialMarks
1344  *
1345  * Handles updates to toolbox and signals all buttons to update when necessary.
1346  */
1347 SocialMarks = {
1348   get nodes() {
1349     let providers = [p for (p of Social.providers) if (p.markURL)];
1350     for (let p of providers) {
1351       let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin);
1352       let widget = CustomizableUI.getWidget(widgetId);
1353       if (!widget)
1354         continue;
1355       let node = widget.forWindow(window).node;
1356       if (node)
1357         yield node;
1358     }
1359   },
1360   update: function() {
1361     // querySelectorAll does not work on the menu panel, so we have to do this
1362     // the hard way.
1363     for (let node of this.nodes) {
1364       // xbl binding is not complete on startup when buttons are not in toolbar,
1365       // verify update is available
1366       if (node.update) {
1367         node.update();
1368       }
1369     }
1370   },
1371 
1372   getProviders: function() {
1373     // only rely on providers that the user has placed in the UI somewhere. This
1374     // also means that populateToolbarPalette must be called prior to using this
1375     // method, otherwise you get a big fat zero. For our use case with context
1376     // menu's, this is ok.
1377     let tbh = this._toolbarHelper;
1378     return [p for (p of Social.providers) if (p.markURL &&
1379                                               document.getElementById(tbh.idFromOrigin(p.origin)))];
1380   },
1381 
1382   populateContextMenu: function() {
1383     // only show a selection if enabled and there is more than one
1384     let providers = this.getProviders();
1385 
1386     // remove all previous entries by class
1387     let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))];
1388     [m.parentNode.removeChild(m) for (m of menus)];
1389 
1390     let contextMenus = [
1391       {
1392         type: "link",
1393         id: "context-marklinkMenu",
1394         label: "social.marklinkMenu.label"
1395       },
1396       {
1397         type: "page",
1398         id: "context-markpageMenu",
1399         label: "social.markpageMenu.label"
1400       }
1401     ];
1402     for (let cfg of contextMenus) {
1403       this._populateContextPopup(cfg, providers);
1404     }
1405     this.update();
1406   },
1407 
1408   MENU_LIMIT: 3, // adjustable for testing
1409   _populateContextPopup: function(menuInfo, providers) {
1410     let menu = document.getElementById(menuInfo.id);
1411     let popup = menu.firstChild;
1412     for (let provider of providers) {
1413       // We show up to MENU_LIMIT providers as single menuitems's at the top
1414       // level of the context menu, if we have more than that, dump them *all*
1415       // into the menu popup.
1416       let mi = document.createElement("menuitem");
1417       mi.setAttribute("oncommand", "gContextMenu.markLink(this.getAttribute('origin'));");
1418       mi.setAttribute("origin", provider.origin);
1419       mi.setAttribute("image", provider.iconURL);
1420       if (providers.length <= this.MENU_LIMIT) {
1421         // an extra class to make enable/disable easy
1422         mi.setAttribute("class", "menuitem-iconic context-socialmarks context-mark"+menuInfo.type);
1423         let menuLabel = gNavigatorBundle.getFormattedString(menuInfo.label, [provider.name]);
1424         mi.setAttribute("label", menuLabel);
1425         menu.parentNode.insertBefore(mi, menu);
1426       } else {
1427         mi.setAttribute("class", "menuitem-iconic context-socialmarks");
1428         mi.setAttribute("label", provider.name);
1429         popup.appendChild(mi);
1430       }
1431     }
1432   },
1433 
1434   populateToolbarPalette: function() {
1435     this._toolbarHelper.populatePalette();
1436     this.populateContextMenu();
1437   },
1438 
1439   removeProvider: function(origin) {
1440     this._toolbarHelper.removeProviderButton(origin);
1441   },
1442 
1443   get _toolbarHelper() {
1444     delete this._toolbarHelper;
1445     this._toolbarHelper = new ToolbarHelper("social-mark-button", CreateSocialMarkWidget);
1446     return this._toolbarHelper;
1447   },
1448 
1449   markLink: function(aOrigin, aUrl, aTarget) {
1450     // find the button for this provider, and open it
1451     let id = this._toolbarHelper.idFromOrigin(aOrigin);
1452     document.getElementById(id).markLink(aUrl, aTarget);
1453   }
1454 };
1455 
1456 })();
1457 
view http://hg.mozilla.org/mozilla-central/rev/ /browser/base/content/browser-social.js