 |
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