Wed, 31 Dec 2014 13:27:57 +0100
Ignore runtime configuration files generated during quality assurance.
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/.
5 // the "exported" symbols
6 let SocialUI,
7 SocialChatBar,
8 SocialFlyout,
9 SocialMarks,
10 SocialShare,
11 SocialSidebar,
12 SocialStatus;
14 (function() {
16 // The minimum sizes for the auto-resize panel code.
17 const PANEL_MIN_HEIGHT = 100;
18 const PANEL_MIN_WIDTH = 330;
20 XPCOMUtils.defineLazyModuleGetter(this, "SharedFrame",
21 "resource:///modules/SharedFrame.jsm");
23 XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() {
24 let tmp = {};
25 Cu.import("resource:///modules/Social.jsm", tmp);
26 return tmp.OpenGraphBuilder;
27 });
29 XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() {
30 let tmp = {};
31 Cu.import("resource:///modules/Social.jsm", tmp);
32 return tmp.DynamicResizeWatcher;
33 });
35 XPCOMUtils.defineLazyGetter(this, "sizeSocialPanelToContent", function() {
36 let tmp = {};
37 Cu.import("resource:///modules/Social.jsm", tmp);
38 return tmp.sizeSocialPanelToContent;
39 });
41 XPCOMUtils.defineLazyGetter(this, "CreateSocialStatusWidget", function() {
42 let tmp = {};
43 Cu.import("resource:///modules/Social.jsm", tmp);
44 return tmp.CreateSocialStatusWidget;
45 });
47 XPCOMUtils.defineLazyGetter(this, "CreateSocialMarkWidget", function() {
48 let tmp = {};
49 Cu.import("resource:///modules/Social.jsm", tmp);
50 return tmp.CreateSocialMarkWidget;
51 });
53 SocialUI = {
54 _initialized: false,
56 // Called on delayed startup to initialize the UI
57 init: function SocialUI_init() {
58 if (this._initialized) {
59 return;
60 }
62 Services.obs.addObserver(this, "social:ambient-notification-changed", false);
63 Services.obs.addObserver(this, "social:profile-changed", false);
64 Services.obs.addObserver(this, "social:frameworker-error", false);
65 Services.obs.addObserver(this, "social:providers-changed", false);
66 Services.obs.addObserver(this, "social:provider-reload", false);
67 Services.obs.addObserver(this, "social:provider-enabled", false);
68 Services.obs.addObserver(this, "social:provider-disabled", false);
70 Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
72 gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true);
73 document.getElementById("PanelUI-popup").addEventListener("popupshown", SocialMarks.updatePanelButtons, true);
75 // menupopups that list social providers. we only populate them when shown,
76 // and if it has not been done already.
77 document.getElementById("viewSidebarMenu").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
78 document.getElementById("social-statusarea-popup").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
80 Social.init().then((update) => {
81 if (update)
82 this._providersChanged();
83 // handle SessionStore for the sidebar state
84 SocialSidebar.restoreWindowState();
85 });
87 this._initialized = true;
88 },
90 // Called on window unload
91 uninit: function SocialUI_uninit() {
92 if (!this._initialized) {
93 return;
94 }
95 SocialSidebar.saveWindowState();
97 Services.obs.removeObserver(this, "social:ambient-notification-changed");
98 Services.obs.removeObserver(this, "social:profile-changed");
99 Services.obs.removeObserver(this, "social:frameworker-error");
100 Services.obs.removeObserver(this, "social:providers-changed");
101 Services.obs.removeObserver(this, "social:provider-reload");
102 Services.obs.removeObserver(this, "social:provider-enabled");
103 Services.obs.removeObserver(this, "social:provider-disabled");
105 Services.prefs.removeObserver("social.toast-notifications.enabled", this);
107 document.getElementById("PanelUI-popup").removeEventListener("popupshown", SocialMarks.updatePanelButtons, true);
108 document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
109 document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
111 this._initialized = false;
112 },
114 observe: function SocialUI_observe(subject, topic, data) {
115 // Exceptions here sometimes don't get reported properly, report them
116 // manually :(
117 try {
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;
146 // Provider-specific notifications
147 case "social:ambient-notification-changed":
148 SocialStatus.updateButton(data);
149 break;
150 case "social:profile-changed":
151 // make sure anything that happens here only affects the provider for
152 // which the profile is changing, and that anything we call actually
153 // needs to change based on profile data.
154 SocialStatus.updateButton(data);
155 break;
156 case "social:frameworker-error":
157 if (this.enabled && SocialSidebar.provider && SocialSidebar.provider.origin == data) {
158 SocialSidebar.setSidebarErrorMessage();
159 }
160 break;
162 case "nsPref:changed":
163 if (data == "social.toast-notifications.enabled") {
164 SocialSidebar.updateToggleNotifications();
165 }
166 break;
167 }
168 } catch (e) {
169 Components.utils.reportError(e + "\n" + e.stack);
170 throw e;
171 }
172 },
174 _providersChanged: function() {
175 SocialSidebar.clearProviderMenus();
176 SocialSidebar.update();
177 SocialChatBar.update();
178 SocialShare.populateProviderMenu();
179 SocialStatus.populateToolbarPalette();
180 SocialMarks.populateToolbarPalette();
181 SocialShare.update();
182 },
184 // This handles "ActivateSocialFeature" events fired against content documents
185 // in this window.
186 _activationEventHandler: function SocialUI_activationHandler(e) {
187 let targetDoc;
188 let node;
189 if (e.target instanceof HTMLDocument) {
190 // version 0 support
191 targetDoc = e.target;
192 node = targetDoc.documentElement
193 } else {
194 targetDoc = e.target.ownerDocument;
195 node = e.target;
196 }
197 if (!(targetDoc instanceof HTMLDocument))
198 return;
200 // Ignore events fired in background tabs or iframes
201 if (targetDoc.defaultView != content)
202 return;
204 // If we are in PB mode, we silently do nothing (bug 829404 exists to
205 // do something sensible here...)
206 if (PrivateBrowsingUtils.isWindowPrivate(window))
207 return;
209 // If the last event was received < 1s ago, ignore this one
210 let now = Date.now();
211 if (now - Social.lastEventReceived < 1000)
212 return;
213 Social.lastEventReceived = now;
215 // We only want to activate if it is as a result of user input.
216 let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
217 .getInterface(Ci.nsIDOMWindowUtils);
218 if (!dwu.isHandlingUserInput) {
219 Cu.reportError("attempt to activate provider without user input from " + targetDoc.nodePrincipal.origin);
220 return;
221 }
223 let data = node.getAttribute("data-service");
224 if (data) {
225 try {
226 data = JSON.parse(data);
227 } catch(e) {
228 Cu.reportError("Social Service manifest parse error: "+e);
229 return;
230 }
231 }
232 Social.installProvider(targetDoc, data, function(manifest) {
233 Social.activateFromOrigin(manifest.origin, function(provider) {
234 if (provider.sidebarURL) {
235 SocialSidebar.show(provider.origin);
236 }
237 if (provider.postActivationURL) {
238 openUILinkIn(provider.postActivationURL, "tab");
239 }
240 });
241 });
242 },
244 showLearnMore: function() {
245 let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
246 openUILinkIn(url, "tab");
247 },
249 closeSocialPanelForLinkTraversal: function (target, linkNode) {
250 // No need to close the panel if this traversal was not retargeted
251 if (target == "" || target == "_self")
252 return;
254 // Check to see whether this link traversal was in a social panel
255 let win = linkNode.ownerDocument.defaultView;
256 let container = win.QueryInterface(Ci.nsIInterfaceRequestor)
257 .getInterface(Ci.nsIWebNavigation)
258 .QueryInterface(Ci.nsIDocShell)
259 .chromeEventHandler;
260 let containerParent = container.parentNode;
261 if (containerParent.classList.contains("social-panel") &&
262 containerParent instanceof Ci.nsIDOMXULPopupElement) {
263 // allow the link traversal to finish before closing the panel
264 setTimeout(() => {
265 containerParent.hidePopup();
266 }, 0);
267 }
268 },
270 get _chromeless() {
271 // Is this a popup window that doesn't want chrome shown?
272 let docElem = document.documentElement;
273 // extrachrome is not restored during session restore, so we need
274 // to check for the toolbar as well.
275 let chromeless = docElem.getAttribute("chromehidden").contains("extrachrome") ||
276 docElem.getAttribute('chromehidden').contains("toolbar");
277 // This property is "fixed" for a window, so avoid doing the check above
278 // multiple times...
279 delete this._chromeless;
280 this._chromeless = chromeless;
281 return chromeless;
282 },
284 get enabled() {
285 // Returns whether social is enabled *for this window*.
286 if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window))
287 return false;
288 return Social.providers.length > 0;
289 },
291 // called on tab/urlbar/location changes and after customization. Update
292 // anything that is tab specific.
293 updateState: function() {
294 if (!this.enabled)
295 return;
296 SocialMarks.update();
297 SocialShare.update();
298 }
299 }
301 SocialChatBar = {
302 get chatbar() {
303 return document.getElementById("pinnedchats");
304 },
305 // Whether the chatbar is available for this window. Note that in full-screen
306 // mode chats are available, but not shown.
307 get isAvailable() {
308 return SocialUI.enabled;
309 },
310 // Does this chatbar have any chats (whether minimized, collapsed or normal)
311 get hasChats() {
312 return !!this.chatbar.firstElementChild;
313 },
314 openChat: function(aProvider, aURL, aCallback, aMode) {
315 this.update();
316 if (!this.isAvailable)
317 return false;
318 this.chatbar.openChat(aProvider, aURL, aCallback, aMode);
319 // We only want to focus the chat if it is as a result of user input.
320 let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
321 .getInterface(Ci.nsIDOMWindowUtils);
322 if (dwu.isHandlingUserInput)
323 this.chatbar.focus();
324 return true;
325 },
326 update: function() {
327 let command = document.getElementById("Social:FocusChat");
328 if (!this.isAvailable) {
329 this.chatbar.hidden = command.hidden = true;
330 } else {
331 this.chatbar.hidden = command.hidden = false;
332 }
333 command.setAttribute("disabled", command.hidden ? "true" : "false");
334 },
335 focus: function SocialChatBar_focus() {
336 this.chatbar.focus();
337 }
338 }
340 SocialFlyout = {
341 get panel() {
342 return document.getElementById("social-flyout-panel");
343 },
345 get iframe() {
346 if (!this.panel.firstChild)
347 this._createFrame();
348 return this.panel.firstChild;
349 },
351 dispatchPanelEvent: function(name) {
352 let doc = this.iframe.contentDocument;
353 let evt = doc.createEvent("CustomEvent");
354 evt.initCustomEvent(name, true, true, {});
355 doc.documentElement.dispatchEvent(evt);
356 },
358 _createFrame: function() {
359 let panel = this.panel;
360 if (!SocialUI.enabled || panel.firstChild)
361 return;
362 // create and initialize the panel for this window
363 let iframe = document.createElement("iframe");
364 iframe.setAttribute("type", "content");
365 iframe.setAttribute("class", "social-panel-frame");
366 iframe.setAttribute("flex", "1");
367 iframe.setAttribute("tooltip", "aHTMLTooltip");
368 iframe.setAttribute("origin", SocialSidebar.provider.origin);
369 panel.appendChild(iframe);
370 },
372 setFlyoutErrorMessage: function SF_setFlyoutErrorMessage() {
373 this.iframe.removeAttribute("src");
374 this.iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
375 encodeURIComponent(this.iframe.getAttribute("origin")),
376 null, null, null, null);
377 sizeSocialPanelToContent(this.panel, this.iframe);
378 },
380 unload: function() {
381 let panel = this.panel;
382 panel.hidePopup();
383 if (!panel.firstChild)
384 return
385 let iframe = panel.firstChild;
386 if (iframe.socialErrorListener)
387 iframe.socialErrorListener.remove();
388 panel.removeChild(iframe);
389 },
391 onShown: function(aEvent) {
392 let panel = this.panel;
393 let iframe = this.iframe;
394 this._dynamicResizer = new DynamicResizeWatcher();
395 iframe.docShell.isActive = true;
396 iframe.docShell.isAppTab = true;
397 if (iframe.contentDocument.readyState == "complete") {
398 this._dynamicResizer.start(panel, iframe);
399 this.dispatchPanelEvent("socialFrameShow");
400 } else {
401 // first time load, wait for load and dispatch after load
402 iframe.addEventListener("load", function panelBrowserOnload(e) {
403 iframe.removeEventListener("load", panelBrowserOnload, true);
404 setTimeout(function() {
405 if (SocialFlyout._dynamicResizer) { // may go null if hidden quickly
406 SocialFlyout._dynamicResizer.start(panel, iframe);
407 SocialFlyout.dispatchPanelEvent("socialFrameShow");
408 }
409 }, 0);
410 }, true);
411 }
412 },
414 onHidden: function(aEvent) {
415 this._dynamicResizer.stop();
416 this._dynamicResizer = null;
417 this.iframe.docShell.isActive = false;
418 this.dispatchPanelEvent("socialFrameHide");
419 },
421 load: function(aURL, cb) {
422 if (!SocialSidebar.provider)
423 return;
425 this.panel.hidden = false;
426 let iframe = this.iframe;
427 // same url with only ref difference does not cause a new load, so we
428 // want to go right to the callback
429 let src = iframe.contentDocument && iframe.contentDocument.documentURIObject;
430 if (!src || !src.equalsExceptRef(Services.io.newURI(aURL, null, null))) {
431 iframe.addEventListener("load", function documentLoaded() {
432 iframe.removeEventListener("load", documentLoaded, true);
433 cb();
434 }, true);
435 // Force a layout flush by calling .clientTop so
436 // that the docShell of this frame is created
437 iframe.clientTop;
438 Social.setErrorListener(iframe, SocialFlyout.setFlyoutErrorMessage.bind(SocialFlyout))
439 iframe.setAttribute("src", aURL);
440 } else {
441 // we still need to set the src to trigger the contents hashchange event
442 // for ref changes
443 iframe.setAttribute("src", aURL);
444 cb();
445 }
446 },
448 open: function(aURL, yOffset, aCallback) {
449 // Hide any other social panels that may be open.
450 document.getElementById("social-notification-panel").hidePopup();
452 if (!SocialUI.enabled)
453 return;
454 let panel = this.panel;
455 let iframe = this.iframe;
457 this.load(aURL, function() {
458 sizeSocialPanelToContent(panel, iframe);
459 let anchor = document.getElementById("social-sidebar-browser");
460 if (panel.state == "open") {
461 panel.moveToAnchor(anchor, "start_before", 0, yOffset, false);
462 } else {
463 panel.openPopup(anchor, "start_before", 0, yOffset, false, false);
464 }
465 if (aCallback) {
466 try {
467 aCallback(iframe.contentWindow);
468 } catch(e) {
469 Cu.reportError(e);
470 }
471 }
472 });
473 }
474 }
476 SocialShare = {
477 get panel() {
478 return document.getElementById("social-share-panel");
479 },
481 get iframe() {
482 // first element is our menu vbox.
483 if (this.panel.childElementCount == 1)
484 return null;
485 else
486 return this.panel.lastChild;
487 },
489 uninit: function () {
490 if (this.iframe) {
491 this.iframe.remove();
492 }
493 },
495 _createFrame: function() {
496 let panel = this.panel;
497 if (!SocialUI.enabled || this.iframe)
498 return;
499 this.panel.hidden = false;
500 // create and initialize the panel for this window
501 let iframe = document.createElement("iframe");
502 iframe.setAttribute("type", "content");
503 iframe.setAttribute("class", "social-share-frame");
504 iframe.setAttribute("context", "contentAreaContextMenu");
505 iframe.setAttribute("tooltip", "aHTMLTooltip");
506 iframe.setAttribute("flex", "1");
507 panel.appendChild(iframe);
508 this.populateProviderMenu();
509 },
511 getSelectedProvider: function() {
512 let provider;
513 let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin");
514 if (lastProviderOrigin) {
515 provider = Social._getProviderFromOrigin(lastProviderOrigin);
516 }
517 // if they have a provider selected in the sidebar use that for the initial
518 // default in share
519 if (!provider)
520 provider = SocialSidebar.provider;
521 // if our provider has no shareURL, select the first one that does
522 if (!provider || !provider.shareURL) {
523 let providers = [p for (p of Social.providers) if (p.shareURL)];
524 provider = providers.length > 0 && providers[0];
525 }
526 return provider;
527 },
529 populateProviderMenu: function() {
530 if (!this.iframe)
531 return;
532 let providers = [p for (p of Social.providers) if (p.shareURL)];
533 let hbox = document.getElementById("social-share-provider-buttons");
534 // selectable providers are inserted before the provider-menu seperator,
535 // remove any menuitems in that area
536 while (hbox.firstChild) {
537 hbox.removeChild(hbox.firstChild);
538 }
539 // reset our share toolbar
540 // only show a selection if there is more than one
541 if (!SocialUI.enabled || providers.length < 2) {
542 this.panel.firstChild.hidden = true;
543 return;
544 }
545 let selectedProvider = this.getSelectedProvider();
546 for (let provider of providers) {
547 let button = document.createElement("toolbarbutton");
548 button.setAttribute("class", "toolbarbutton share-provider-button");
549 button.setAttribute("type", "radio");
550 button.setAttribute("group", "share-providers");
551 button.setAttribute("image", provider.iconURL);
552 button.setAttribute("tooltiptext", provider.name);
553 button.setAttribute("origin", provider.origin);
554 button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin')); this.checked=true;");
555 if (provider == selectedProvider) {
556 this.defaultButton = button;
557 }
558 hbox.appendChild(button);
559 }
560 if (!this.defaultButton) {
561 this.defaultButton = hbox.firstChild
562 }
563 this.defaultButton.setAttribute("checked", "true");
564 this.panel.firstChild.hidden = false;
565 },
567 get shareButton() {
568 return document.getElementById("social-share-button");
569 },
571 canSharePage: function(aURI) {
572 // we do not enable sharing from private sessions
573 if (PrivateBrowsingUtils.isWindowPrivate(window))
574 return false;
576 if (!aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https')))
577 return false;
578 return true;
579 },
581 update: function() {
582 let shareButton = this.shareButton;
583 shareButton.hidden = !SocialUI.enabled ||
584 [p for (p of Social.providers) if (p.shareURL)].length == 0;
585 shareButton.disabled = shareButton.hidden || !this.canSharePage(gBrowser.currentURI);
587 // also update the relevent command's disabled state so the keyboard
588 // shortcut only works when available.
589 let cmd = document.getElementById("Social:SharePage");
590 if (shareButton.disabled)
591 cmd.setAttribute("disabled", "true");
592 else
593 cmd.removeAttribute("disabled");
594 },
596 onShowing: function() {
597 this.shareButton.setAttribute("open", "true");
598 },
600 onHidden: function() {
601 this.shareButton.removeAttribute("open");
602 this.iframe.setAttribute("src", "data:text/plain;charset=utf8,");
603 this.currentShare = null;
604 },
606 setErrorMessage: function() {
607 let iframe = this.iframe;
608 if (!iframe)
609 return;
611 iframe.removeAttribute("src");
612 iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
613 encodeURIComponent(iframe.getAttribute("origin")),
614 null, null, null, null);
615 sizeSocialPanelToContent(this.panel, iframe);
616 },
618 sharePage: function(providerOrigin, graphData) {
619 // if providerOrigin is undefined, we use the last-used provider, or the
620 // current/default provider. The provider selection in the share panel
621 // will call sharePage with an origin for us to switch to.
622 this._createFrame();
623 let iframe = this.iframe;
624 let provider;
625 if (providerOrigin)
626 provider = Social._getProviderFromOrigin(providerOrigin);
627 else
628 provider = this.getSelectedProvider();
629 if (!provider || !provider.shareURL)
630 return;
632 // graphData is an optional param that either defines the full set of data
633 // to be shared, or partial data about the current page. It is set by a call
634 // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST
635 // define at least url. If it is undefined, we're sharing the current url in
636 // the browser tab.
637 let sharedURI = graphData ? Services.io.newURI(graphData.url, null, null) :
638 gBrowser.currentURI;
639 if (!this.canSharePage(sharedURI))
640 return;
642 // the point of this action type is that we can use existing share
643 // endpoints (e.g. oexchange) that do not support additional
644 // socialapi functionality. One tweak is that we shoot an event
645 // containing the open graph data.
646 let pageData = graphData ? graphData : this.currentShare;
647 if (!pageData || sharedURI == gBrowser.currentURI) {
648 pageData = OpenGraphBuilder.getData(gBrowser);
649 if (graphData) {
650 // overwrite data retreived from page with data given to us as a param
651 for (let p in graphData) {
652 pageData[p] = graphData[p];
653 }
654 }
655 }
656 this.currentShare = pageData;
658 let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData);
660 this._dynamicResizer = new DynamicResizeWatcher();
661 // if we've already loaded this provider/page share endpoint, we don't want
662 // to add another load event listener.
663 let reload = true;
664 let endpointMatch = shareEndpoint == iframe.getAttribute("src");
665 let docLoaded = iframe.contentDocument && iframe.contentDocument.readyState == "complete";
666 if (endpointMatch && docLoaded) {
667 reload = shareEndpoint != iframe.contentDocument.location.spec;
668 }
669 if (!reload) {
670 this._dynamicResizer.start(this.panel, iframe);
671 iframe.docShell.isActive = true;
672 iframe.docShell.isAppTab = true;
673 let evt = iframe.contentDocument.createEvent("CustomEvent");
674 evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
675 iframe.contentDocument.documentElement.dispatchEvent(evt);
676 } else {
677 // first time load, wait for load and dispatch after load
678 iframe.addEventListener("load", function panelBrowserOnload(e) {
679 iframe.removeEventListener("load", panelBrowserOnload, true);
680 iframe.docShell.isActive = true;
681 iframe.docShell.isAppTab = true;
682 setTimeout(function() {
683 if (SocialShare._dynamicResizer) { // may go null if hidden quickly
684 SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
685 }
686 }, 0);
687 let evt = iframe.contentDocument.createEvent("CustomEvent");
688 evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
689 iframe.contentDocument.documentElement.dispatchEvent(evt);
690 }, true);
691 }
692 // always ensure that origin belongs to the endpoint
693 let uri = Services.io.newURI(shareEndpoint, null, null);
694 iframe.setAttribute("origin", provider.origin);
695 iframe.setAttribute("src", shareEndpoint);
697 let navBar = document.getElementById("nav-bar");
698 let anchor = document.getAnonymousElementByAttribute(this.shareButton, "class", "toolbarbutton-icon");
699 this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
700 Social.setErrorListener(iframe, this.setErrorMessage.bind(this));
701 }
702 };
704 SocialSidebar = {
705 // Whether the sidebar can be shown for this window.
706 get canShow() {
707 if (!SocialUI.enabled || document.mozFullScreen)
708 return false;
709 return Social.providers.some(p => p.sidebarURL);
710 },
712 // Whether the user has toggled the sidebar on (for windows where it can appear)
713 get opened() {
714 let broadcaster = document.getElementById("socialSidebarBroadcaster");
715 return !broadcaster.hidden;
716 },
718 restoreWindowState: function() {
719 // Window state is used to allow different sidebar providers in each window.
720 // We also store the provider used in a pref as the default sidebar to
721 // maintain that state for users who do not restore window state. The
722 // existence of social.sidebar.provider means the sidebar is open with that
723 // provider.
724 this._initialized = true;
725 if (!this.canShow)
726 return;
728 if (Services.prefs.prefHasUserValue("social.provider.current")) {
729 // "upgrade" when the first window opens if we have old prefs. We get the
730 // values from prefs this one time, window state will be saved when this
731 // window is closed.
732 let origin = Services.prefs.getCharPref("social.provider.current");
733 Services.prefs.clearUserPref("social.provider.current");
734 // social.sidebar.open default was true, but we only opened if there was
735 // a current provider
736 let opened = origin && true;
737 if (Services.prefs.prefHasUserValue("social.sidebar.open")) {
738 opened = origin && Services.prefs.getBoolPref("social.sidebar.open");
739 Services.prefs.clearUserPref("social.sidebar.open");
740 }
741 let data = {
742 "hidden": !opened,
743 "origin": origin
744 };
745 SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data));
746 }
748 let data = SessionStore.getWindowValue(window, "socialSidebar");
749 // if this window doesn't have it's own state, use the state from the opener
750 if (!data && window.opener && !window.opener.closed) {
751 try {
752 data = SessionStore.getWindowValue(window.opener, "socialSidebar");
753 } catch(e) {
754 // Window is not tracked, which happens on osx if the window is opened
755 // from the hidden window. That happens when you close the last window
756 // without quiting firefox, then open a new window.
757 }
758 }
759 if (data) {
760 data = JSON.parse(data);
761 document.getElementById("social-sidebar-browser").setAttribute("origin", data.origin);
762 if (!data.hidden)
763 this.show(data.origin);
764 } else if (Services.prefs.prefHasUserValue("social.sidebar.provider")) {
765 // no window state, use the global state if it is available
766 this.show(Services.prefs.getCharPref("social.sidebar.provider"));
767 }
768 },
770 saveWindowState: function() {
771 let broadcaster = document.getElementById("socialSidebarBroadcaster");
772 let sidebarOrigin = document.getElementById("social-sidebar-browser").getAttribute("origin");
773 let data = {
774 "hidden": broadcaster.hidden,
775 "origin": sidebarOrigin
776 };
778 // Save a global state for users who do not restore state.
779 if (broadcaster.hidden)
780 Services.prefs.clearUserPref("social.sidebar.provider");
781 else
782 Services.prefs.setCharPref("social.sidebar.provider", sidebarOrigin);
784 try {
785 SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data));
786 } catch(e) {
787 // window not tracked during uninit
788 }
789 },
791 setSidebarVisibilityState: function(aEnabled) {
792 let sbrowser = document.getElementById("social-sidebar-browser");
793 // it's possible we'll be called twice with aEnabled=false so let's
794 // just assume we may often be called with the same state.
795 if (aEnabled == sbrowser.docShellIsActive)
796 return;
797 sbrowser.docShellIsActive = aEnabled;
798 let evt = sbrowser.contentDocument.createEvent("CustomEvent");
799 evt.initCustomEvent(aEnabled ? "socialFrameShow" : "socialFrameHide", true, true, {});
800 sbrowser.contentDocument.documentElement.dispatchEvent(evt);
801 },
803 updateToggleNotifications: function() {
804 let command = document.getElementById("Social:ToggleNotifications");
805 command.setAttribute("checked", Services.prefs.getBoolPref("social.toast-notifications.enabled"));
806 command.setAttribute("hidden", !SocialUI.enabled);
807 },
809 update: function SocialSidebar_update() {
810 // ensure we never update before restoreWindowState
811 if (!this._initialized)
812 return;
813 this.ensureProvider();
814 this.updateToggleNotifications();
815 this._updateHeader();
816 clearTimeout(this._unloadTimeoutId);
817 // Hide the toggle menu item if the sidebar cannot appear
818 let command = document.getElementById("Social:ToggleSidebar");
819 command.setAttribute("hidden", this.canShow ? "false" : "true");
821 // Hide the sidebar if it cannot appear, or has been toggled off.
822 // Also set the command "checked" state accordingly.
823 let hideSidebar = !this.canShow || !this.opened;
824 let broadcaster = document.getElementById("socialSidebarBroadcaster");
825 broadcaster.hidden = hideSidebar;
826 command.setAttribute("checked", !hideSidebar);
828 let sbrowser = document.getElementById("social-sidebar-browser");
830 if (hideSidebar) {
831 sbrowser.removeEventListener("load", SocialSidebar._loadListener, true);
832 this.setSidebarVisibilityState(false);
833 // If we've been disabled, unload the sidebar content immediately;
834 // if the sidebar was just toggled to invisible, wait a timeout
835 // before unloading.
836 if (!this.canShow) {
837 this.unloadSidebar();
838 } else {
839 this._unloadTimeoutId = setTimeout(
840 this.unloadSidebar,
841 Services.prefs.getIntPref("social.sidebar.unload_timeout_ms")
842 );
843 }
844 } else {
845 sbrowser.setAttribute("origin", this.provider.origin);
846 if (this.provider.errorState == "frameworker-error") {
847 SocialSidebar.setSidebarErrorMessage();
848 return;
849 }
851 // Make sure the right sidebar URL is loaded
852 if (sbrowser.getAttribute("src") != this.provider.sidebarURL) {
853 // we check readyState right after setting src, we need a new content
854 // viewer to ensure we are checking against the correct document.
855 sbrowser.docShell.createAboutBlankContentViewer(null);
856 Social.setErrorListener(sbrowser, this.setSidebarErrorMessage.bind(this));
857 // setting isAppTab causes clicks on untargeted links to open new tabs
858 sbrowser.docShell.isAppTab = true;
859 sbrowser.setAttribute("src", this.provider.sidebarURL);
860 PopupNotifications.locationChange(sbrowser);
861 }
863 // if the document has not loaded, delay until it is
864 if (sbrowser.contentDocument.readyState != "complete") {
865 document.getElementById("social-sidebar-button").setAttribute("loading", "true");
866 sbrowser.addEventListener("load", SocialSidebar._loadListener, true);
867 } else {
868 this.setSidebarVisibilityState(true);
869 }
870 }
871 this._updateCheckedMenuItems(this.opened && this.provider ? this.provider.origin : null);
872 },
874 _loadListener: function SocialSidebar_loadListener() {
875 let sbrowser = document.getElementById("social-sidebar-browser");
876 sbrowser.removeEventListener("load", SocialSidebar._loadListener, true);
877 document.getElementById("social-sidebar-button").removeAttribute("loading");
878 SocialSidebar.setSidebarVisibilityState(true);
879 },
881 unloadSidebar: function SocialSidebar_unloadSidebar() {
882 let sbrowser = document.getElementById("social-sidebar-browser");
883 if (!sbrowser.hasAttribute("origin"))
884 return;
886 sbrowser.stop();
887 sbrowser.removeAttribute("origin");
888 sbrowser.setAttribute("src", "about:blank");
889 // We need to explicitly create a new content viewer because the old one
890 // doesn't get destroyed until about:blank has loaded (which does not happen
891 // as long as the element is hidden).
892 sbrowser.docShell.createAboutBlankContentViewer(null);
893 SocialFlyout.unload();
894 },
896 _unloadTimeoutId: 0,
898 setSidebarErrorMessage: function() {
899 let sbrowser = document.getElementById("social-sidebar-browser");
900 // a frameworker error "trumps" a sidebar error.
901 let origin = sbrowser.getAttribute("origin");
902 if (origin) {
903 origin = "&origin=" + encodeURIComponent(origin);
904 }
905 if (this.provider.errorState == "frameworker-error") {
906 sbrowser.setAttribute("src", "about:socialerror?mode=workerFailure" + origin);
907 } else {
908 let url = encodeURIComponent(this.provider.sidebarURL);
909 sbrowser.loadURI("about:socialerror?mode=tryAgain&url=" + url + origin, null, null);
910 }
911 },
913 _provider: null,
914 ensureProvider: function() {
915 if (this._provider)
916 return;
917 // origin for sidebar is persisted, so get the previously selected sidebar
918 // first, otherwise fallback to the first provider in the list
919 let sbrowser = document.getElementById("social-sidebar-browser");
920 let origin = sbrowser.getAttribute("origin");
921 let providers = [p for (p of Social.providers) if (p.sidebarURL)];
922 let provider;
923 if (origin)
924 provider = Social._getProviderFromOrigin(origin);
925 if (!provider && providers.length > 0)
926 provider = providers[0];
927 if (provider)
928 this.provider = provider;
929 },
931 get provider() {
932 return this._provider;
933 },
935 set provider(provider) {
936 if (!provider || provider.sidebarURL) {
937 this._provider = provider;
938 this._updateHeader();
939 this._updateCheckedMenuItems(provider && provider.origin);
940 this.update();
941 }
942 },
944 disableProvider: function(origin) {
945 if (this._provider && this._provider.origin == origin) {
946 this._provider = null;
947 // force a selection of the next provider if there is one
948 this.ensureProvider();
949 }
950 },
952 _updateHeader: function() {
953 let provider = this.provider;
954 let image, title;
955 if (provider) {
956 image = "url(" + (provider.icon32URL || provider.iconURL) + ")";
957 title = provider.name;
958 }
959 document.getElementById("social-sidebar-favico").style.listStyleImage = image;
960 document.getElementById("social-sidebar-title").value = title;
961 },
963 _updateCheckedMenuItems: function(origin) {
964 // update selected menuitems
965 let menuitems = document.getElementsByClassName("social-provider-menuitem");
966 for (let mi of menuitems) {
967 if (origin && mi.getAttribute("origin") == origin) {
968 mi.setAttribute("checked", "true");
969 mi.setAttribute("oncommand", "SocialSidebar.hide();");
970 } else if (mi.getAttribute("checked")) {
971 mi.removeAttribute("checked");
972 mi.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));");
973 }
974 }
975 },
977 show: function(origin) {
978 // always show the sidebar, and set the provider
979 let broadcaster = document.getElementById("socialSidebarBroadcaster");
980 broadcaster.hidden = false;
981 if (origin)
982 this.provider = Social._getProviderFromOrigin(origin);
983 else
984 SocialSidebar.update();
985 this.saveWindowState();
986 },
988 hide: function() {
989 let broadcaster = document.getElementById("socialSidebarBroadcaster");
990 broadcaster.hidden = true;
991 this._updateCheckedMenuItems();
992 this.clearProviderMenus();
993 SocialSidebar.update();
994 this.saveWindowState();
995 },
997 toggleSidebar: function SocialSidebar_toggle() {
998 let broadcaster = document.getElementById("socialSidebarBroadcaster");
999 if (broadcaster.hidden)
1000 this.show();
1001 else
1002 this.hide();
1003 },
1005 populateSidebarMenu: function(event) {
1006 // Providers are removed from the view->sidebar menu when there is a change
1007 // in providers, so we only have to populate onshowing if there are no
1008 // provider menus. We populate this menu so long as there are enabled
1009 // providers with sidebars.
1010 let popup = event.target;
1011 let providerMenuSeps = popup.getElementsByClassName("social-provider-menu");
1012 if (providerMenuSeps[0].previousSibling.nodeName == "menuseparator")
1013 SocialSidebar.populateProviderMenu(providerMenuSeps[0]);
1014 },
1016 clearProviderMenus: function() {
1017 // called when there is a change in the provider list we clear all menus,
1018 // they will be repopulated when the menu is shown
1019 let providerMenuSeps = document.getElementsByClassName("social-provider-menu");
1020 for (let providerMenuSep of providerMenuSeps) {
1021 while (providerMenuSep.previousSibling.nodeName == "menuitem") {
1022 let menu = providerMenuSep.parentNode;
1023 menu.removeChild(providerMenuSep.previousSibling);
1024 }
1025 }
1026 },
1028 populateProviderMenu: function(providerMenuSep) {
1029 let menu = providerMenuSep.parentNode;
1030 // selectable providers are inserted before the provider-menu seperator,
1031 // remove any menuitems in that area
1032 while (providerMenuSep.previousSibling.nodeName == "menuitem") {
1033 menu.removeChild(providerMenuSep.previousSibling);
1034 }
1035 // only show a selection in the sidebar header menu if there is more than one
1036 let providers = [p for (p of Social.providers) if (p.sidebarURL)];
1037 if (providers.length < 2 && menu.id != "viewSidebarMenu") {
1038 providerMenuSep.hidden = true;
1039 return;
1040 }
1041 let topSep = providerMenuSep.previousSibling;
1042 for (let provider of providers) {
1043 let menuitem = document.createElement("menuitem");
1044 menuitem.className = "menuitem-iconic social-provider-menuitem";
1045 menuitem.setAttribute("image", provider.iconURL);
1046 menuitem.setAttribute("label", provider.name);
1047 menuitem.setAttribute("origin", provider.origin);
1048 if (this.opened && provider == this.provider) {
1049 menuitem.setAttribute("checked", "true");
1050 menuitem.setAttribute("oncommand", "SocialSidebar.hide();");
1051 } else {
1052 menuitem.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));");
1053 }
1054 menu.insertBefore(menuitem, providerMenuSep);
1055 }
1056 topSep.hidden = topSep.nextSibling == providerMenuSep;
1057 providerMenuSep.hidden = !providerMenuSep.nextSibling;
1058 }
1059 }
1061 // this helper class is used by removable/customizable buttons to handle
1062 // widget creation/destruction
1064 // When a provider is installed we show all their UI so the user will see the
1065 // functionality of what they installed. The user can later customize the UI,
1066 // moving buttons around or off the toolbar.
1067 //
1068 // On startup, we create the button widgets of any enabled provider.
1069 // CustomizableUI handles placement and persistence of placement.
1070 function ToolbarHelper(type, createButtonFn, listener) {
1071 this._createButton = createButtonFn;
1072 this._type = type;
1074 if (listener) {
1075 CustomizableUI.addListener(listener);
1076 // remove this listener on window close
1077 window.addEventListener("unload", () => {
1078 CustomizableUI.removeListener(listener);
1079 });
1080 }
1081 }
1083 ToolbarHelper.prototype = {
1084 idFromOrigin: function(origin) {
1085 // this id needs to pass the checks in CustomizableUI, so remove characters
1086 // that wont pass.
1087 return this._type + "-" + Services.io.newURI(origin, null, null).hostPort.replace(/[\.:]/g,'-');
1088 },
1090 // should be called on disable of a provider
1091 removeProviderButton: function(origin) {
1092 CustomizableUI.destroyWidget(this.idFromOrigin(origin));
1093 },
1095 clearPalette: function() {
1096 [this.removeProviderButton(p.origin) for (p of Social.providers)];
1097 },
1099 // should be called on enable of a provider
1100 populatePalette: function() {
1101 if (!Social.enabled) {
1102 this.clearPalette();
1103 return;
1104 }
1106 // create any buttons that do not exist yet if they have been persisted
1107 // as a part of the UI (otherwise they belong in the palette).
1108 for (let provider of Social.providers) {
1109 let id = this.idFromOrigin(provider.origin);
1110 this._createButton(id, provider);
1111 }
1112 }
1113 }
1115 let SocialStatusWidgetListener = {
1116 _getNodeOrigin: function(aWidgetId) {
1117 // we rely on the button id being the same as the widget.
1118 let node = document.getElementById(aWidgetId);
1119 if (!node)
1120 return null
1121 if (!node.classList.contains("social-status-button"))
1122 return null
1123 return node.getAttribute("origin");
1124 },
1125 onWidgetAdded: function(aWidgetId, aArea, aPosition) {
1126 let origin = this._getNodeOrigin(aWidgetId);
1127 if (origin)
1128 SocialStatus.updateButton(origin);
1129 },
1130 onWidgetRemoved: function(aWidgetId, aPrevArea) {
1131 let origin = this._getNodeOrigin(aWidgetId);
1132 if (!origin)
1133 return;
1134 // When a widget is demoted to the palette ('removed'), it's visual
1135 // style should change.
1136 SocialStatus.updateButton(origin);
1137 SocialStatus._removeFrame(origin);
1138 }
1139 }
1141 SocialStatus = {
1142 populateToolbarPalette: function() {
1143 this._toolbarHelper.populatePalette();
1145 for (let provider of Social.providers)
1146 this.updateButton(provider.origin);
1147 },
1149 removeProvider: function(origin) {
1150 this._removeFrame(origin);
1151 this._toolbarHelper.removeProviderButton(origin);
1152 },
1154 reloadProvider: function(origin) {
1155 let button = document.getElementById(this._toolbarHelper.idFromOrigin(origin));
1156 if (button && button.getAttribute("open") == "true")
1157 document.getElementById("social-notification-panel").hidePopup();
1158 this._removeFrame(origin);
1159 },
1161 _removeFrame: function(origin) {
1162 let notificationFrameId = "social-status-" + origin;
1163 let frame = document.getElementById(notificationFrameId);
1164 if (frame) {
1165 SharedFrame.forgetGroup(frame.id);
1166 frame.parentNode.removeChild(frame);
1167 }
1168 },
1170 get _toolbarHelper() {
1171 delete this._toolbarHelper;
1172 this._toolbarHelper = new ToolbarHelper("social-status-button",
1173 CreateSocialStatusWidget,
1174 SocialStatusWidgetListener);
1175 return this._toolbarHelper;
1176 },
1178 get _dynamicResizer() {
1179 delete this._dynamicResizer;
1180 this._dynamicResizer = new DynamicResizeWatcher();
1181 return this._dynamicResizer;
1182 },
1184 // status panels are one-per button per-process, we swap the docshells between
1185 // windows when necessary
1186 _attachNotificatonPanel: function(aParent, aButton, provider) {
1187 aParent.hidden = !SocialUI.enabled;
1188 let notificationFrameId = "social-status-" + provider.origin;
1189 let frame = document.getElementById(notificationFrameId);
1191 // If the button was customized to a new location, we we'll destroy the
1192 // iframe and start fresh.
1193 if (frame && frame.parentNode != aParent) {
1194 SharedFrame.forgetGroup(frame.id);
1195 frame.parentNode.removeChild(frame);
1196 frame = null;
1197 }
1199 if (!frame) {
1200 frame = SharedFrame.createFrame(
1201 notificationFrameId, /* frame name */
1202 aParent, /* parent */
1203 {
1204 "type": "content",
1205 "mozbrowser": "true",
1206 "class": "social-panel-frame",
1207 "id": notificationFrameId,
1208 "tooltip": "aHTMLTooltip",
1209 "context": "contentAreaContextMenu",
1210 "flex": "1",
1212 // work around bug 793057 - by making the panel roughly the final size
1213 // we are more likely to have the anchor in the correct position.
1214 "style": "width: " + PANEL_MIN_WIDTH + "px;",
1216 "origin": provider.origin,
1217 "src": provider.statusURL
1218 }
1219 );
1221 if (frame.socialErrorListener)
1222 frame.socialErrorListener.remove();
1223 if (frame.docShell) {
1224 frame.docShell.isActive = false;
1225 Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this));
1226 }
1227 } else {
1228 frame.setAttribute("origin", provider.origin);
1229 SharedFrame.updateURL(notificationFrameId, provider.statusURL);
1230 }
1231 aButton.setAttribute("notificationFrameId", notificationFrameId);
1232 },
1234 updateButton: function(origin) {
1235 let id = this._toolbarHelper.idFromOrigin(origin);
1236 let widget = CustomizableUI.getWidget(id);
1237 if (!widget)
1238 return;
1239 let button = widget.forWindow(window).node;
1240 if (button) {
1241 // we only grab the first notification, ignore all others
1242 let place = CustomizableUI.getPlaceForItem(button);
1243 let provider = Social._getProviderFromOrigin(origin);
1244 let icons = provider.ambientNotificationIcons;
1245 let iconNames = Object.keys(icons);
1246 let notif = icons[iconNames[0]];
1248 // The image and tooltip need to be updated for both
1249 // ambient notification and profile changes.
1250 let iconURL = provider.icon32URL || provider.iconURL;
1251 let tooltiptext;
1252 if (!notif || place == "palette") {
1253 button.style.listStyleImage = "url(" + iconURL + ")";
1254 button.setAttribute("badge", "");
1255 button.setAttribute("aria-label", "");
1256 button.setAttribute("tooltiptext", provider.name);
1257 return;
1258 }
1259 button.style.listStyleImage = "url(" + (notif.iconURL || iconURL) + ")";
1260 button.setAttribute("tooltiptext", notif.label || provider.name);
1262 let badge = notif.counter || "";
1263 button.setAttribute("badge", badge);
1264 let ariaLabel = notif.label;
1265 // if there is a badge value, we must use a localizable string to insert it.
1266 if (badge)
1267 ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText",
1268 [ariaLabel, badge]);
1269 button.setAttribute("aria-label", ariaLabel);
1270 }
1271 },
1273 showPopup: function(aToolbarButton) {
1274 // attach our notification panel if necessary
1275 let origin = aToolbarButton.getAttribute("origin");
1276 let provider = Social._getProviderFromOrigin(origin);
1278 // if we're a slice in the hamburger, use that panel instead
1279 let widgetGroup = CustomizableUI.getWidget(aToolbarButton.getAttribute("id"));
1280 let widget = widgetGroup.forWindow(window);
1281 let panel, showingEvent, hidingEvent;
1282 let inMenuPanel = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL;
1283 if (inMenuPanel) {
1284 panel = document.getElementById("PanelUI-socialapi");
1285 this._attachNotificatonPanel(panel, aToolbarButton, provider);
1286 widget.node.setAttribute("closemenu", "none");
1287 showingEvent = "ViewShowing";
1288 hidingEvent = "ViewHiding";
1289 } else {
1290 panel = document.getElementById("social-notification-panel");
1291 this._attachNotificatonPanel(panel, aToolbarButton, provider);
1292 showingEvent = "popupshown";
1293 hidingEvent = "popuphidden";
1294 }
1295 let notificationFrameId = aToolbarButton.getAttribute("notificationFrameId");
1296 let notificationFrame = document.getElementById(notificationFrameId);
1298 let wasAlive = SharedFrame.isGroupAlive(notificationFrameId);
1299 SharedFrame.setOwner(notificationFrameId, notificationFrame);
1301 // Clear dimensions on all browsers so the panel size will
1302 // only use the selected browser.
1303 let frameIter = panel.firstElementChild;
1304 while (frameIter) {
1305 frameIter.collapsed = (frameIter != notificationFrame);
1306 frameIter = frameIter.nextElementSibling;
1307 }
1309 function dispatchPanelEvent(name) {
1310 let evt = notificationFrame.contentDocument.createEvent("CustomEvent");
1311 evt.initCustomEvent(name, true, true, {});
1312 notificationFrame.contentDocument.documentElement.dispatchEvent(evt);
1313 }
1315 // we only use a dynamic resizer when we're located the toolbar.
1316 let dynamicResizer = inMenuPanel ? null : this._dynamicResizer;
1317 panel.addEventListener(hidingEvent, function onpopuphiding() {
1318 panel.removeEventListener(hidingEvent, onpopuphiding);
1319 aToolbarButton.removeAttribute("open");
1320 if (dynamicResizer)
1321 dynamicResizer.stop();
1322 notificationFrame.docShell.isActive = false;
1323 dispatchPanelEvent("socialFrameHide");
1324 });
1326 panel.addEventListener(showingEvent, function onpopupshown() {
1327 panel.removeEventListener(showingEvent, onpopupshown);
1328 // This attribute is needed on both the button and the
1329 // containing toolbaritem since the buttons on OS X have
1330 // moz-appearance:none, while their container gets
1331 // moz-appearance:toolbarbutton due to the way that toolbar buttons
1332 // get combined on OS X.
1333 let initFrameShow = () => {
1334 notificationFrame.docShell.isActive = true;
1335 notificationFrame.docShell.isAppTab = true;
1336 if (dynamicResizer)
1337 dynamicResizer.start(panel, notificationFrame);
1338 dispatchPanelEvent("socialFrameShow");
1339 };
1340 if (!inMenuPanel)
1341 aToolbarButton.setAttribute("open", "true");
1342 if (notificationFrame.contentDocument &&
1343 notificationFrame.contentDocument.readyState == "complete" && wasAlive) {
1344 initFrameShow();
1345 } else {
1346 // first time load, wait for load and dispatch after load
1347 notificationFrame.addEventListener("load", function panelBrowserOnload(e) {
1348 notificationFrame.removeEventListener("load", panelBrowserOnload, true);
1349 initFrameShow();
1350 }, true);
1351 }
1352 });
1354 if (inMenuPanel) {
1355 PanelUI.showSubView("PanelUI-socialapi", widget.node,
1356 CustomizableUI.AREA_PANEL);
1357 } else {
1358 let anchor = document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-badge-container");
1359 // Bug 849216 - open the popup in a setTimeout so we avoid the auto-rollup
1360 // handling from preventing it being opened in some cases.
1361 setTimeout(function() {
1362 panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
1363 }, 0);
1364 }
1365 },
1367 setPanelErrorMessage: function(aNotificationFrame) {
1368 if (!aNotificationFrame)
1369 return;
1371 let src = aNotificationFrame.getAttribute("src");
1372 aNotificationFrame.removeAttribute("src");
1373 let origin = aNotificationFrame.getAttribute("origin");
1374 aNotificationFrame.webNavigation.loadURI("about:socialerror?mode=tryAgainOnly&url=" +
1375 encodeURIComponent(src) + "&origin=" +
1376 encodeURIComponent(origin),
1377 null, null, null, null);
1378 let panel = aNotificationFrame.parentNode;
1379 sizeSocialPanelToContent(panel, aNotificationFrame);
1380 },
1382 };
1385 /**
1386 * SocialMarks
1387 *
1388 * Handles updates to toolbox and signals all buttons to update when necessary.
1389 */
1390 SocialMarks = {
1391 update: function() {
1392 // signal each button to update itself
1393 let currentButtons = document.querySelectorAll('toolbarbutton[type="socialmark"]');
1394 for (let elt of currentButtons)
1395 elt.update();
1396 },
1398 updatePanelButtons: function() {
1399 // querySelectorAll does not work on the menu panel the panel, so we have to
1400 // do this the hard way.
1401 let providers = SocialMarks.getProviders();
1402 let panel = document.getElementById("PanelUI-popup");
1403 for (let p of providers) {
1404 let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin);
1405 let widget = CustomizableUI.getWidget(widgetId);
1406 if (!widget)
1407 continue;
1408 let node = widget.forWindow(window).node;
1409 if (node)
1410 node.update();
1411 }
1412 },
1414 getProviders: function() {
1415 // only rely on providers that the user has placed in the UI somewhere. This
1416 // also means that populateToolbarPalette must be called prior to using this
1417 // method, otherwise you get a big fat zero. For our use case with context
1418 // menu's, this is ok.
1419 let tbh = this._toolbarHelper;
1420 return [p for (p of Social.providers) if (p.markURL &&
1421 document.getElementById(tbh.idFromOrigin(p.origin)))];
1422 },
1424 populateContextMenu: function() {
1425 // only show a selection if enabled and there is more than one
1426 let providers = this.getProviders();
1428 // remove all previous entries by class
1429 let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))];
1430 [m.parentNode.removeChild(m) for (m of menus)];
1432 let contextMenus = [
1433 {
1434 type: "link",
1435 id: "context-marklinkMenu",
1436 label: "social.marklinkMenu.label"
1437 },
1438 {
1439 type: "page",
1440 id: "context-markpageMenu",
1441 label: "social.markpageMenu.label"
1442 }
1443 ];
1444 for (let cfg of contextMenus) {
1445 this._populateContextPopup(cfg, providers);
1446 }
1447 this.updatePanelButtons();
1448 },
1450 MENU_LIMIT: 3, // adjustable for testing
1451 _populateContextPopup: function(menuInfo, providers) {
1452 let menu = document.getElementById(menuInfo.id);
1453 let popup = menu.firstChild;
1454 for (let provider of providers) {
1455 // We show up to MENU_LIMIT providers as single menuitems's at the top
1456 // level of the context menu, if we have more than that, dump them *all*
1457 // into the menu popup.
1458 let mi = document.createElement("menuitem");
1459 mi.setAttribute("oncommand", "gContextMenu.markLink(this.getAttribute('origin'));");
1460 mi.setAttribute("origin", provider.origin);
1461 mi.setAttribute("image", provider.iconURL);
1462 if (providers.length <= this.MENU_LIMIT) {
1463 // an extra class to make enable/disable easy
1464 mi.setAttribute("class", "menuitem-iconic context-socialmarks context-mark"+menuInfo.type);
1465 let menuLabel = gNavigatorBundle.getFormattedString(menuInfo.label, [provider.name]);
1466 mi.setAttribute("label", menuLabel);
1467 menu.parentNode.insertBefore(mi, menu);
1468 } else {
1469 mi.setAttribute("class", "menuitem-iconic context-socialmarks");
1470 mi.setAttribute("label", provider.name);
1471 popup.appendChild(mi);
1472 }
1473 }
1474 },
1476 populateToolbarPalette: function() {
1477 this._toolbarHelper.populatePalette();
1478 this.populateContextMenu();
1479 },
1481 removeProvider: function(origin) {
1482 this._toolbarHelper.removeProviderButton(origin);
1483 },
1485 get _toolbarHelper() {
1486 delete this._toolbarHelper;
1487 this._toolbarHelper = new ToolbarHelper("social-mark-button", CreateSocialMarkWidget);
1488 return this._toolbarHelper;
1489 },
1491 markLink: function(aOrigin, aUrl) {
1492 // find the button for this provider, and open it
1493 let id = this._toolbarHelper.idFromOrigin(aOrigin);
1494 document.getElementById(id).markLink(aUrl);
1495 }
1496 };
1498 })();