1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/base/content/browser-social.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1498 @@ 1.4 +// This Source Code Form is subject to the terms of the Mozilla Public 1.5 +// License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 +// file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.7 + 1.8 +// the "exported" symbols 1.9 +let SocialUI, 1.10 + SocialChatBar, 1.11 + SocialFlyout, 1.12 + SocialMarks, 1.13 + SocialShare, 1.14 + SocialSidebar, 1.15 + SocialStatus; 1.16 + 1.17 +(function() { 1.18 + 1.19 +// The minimum sizes for the auto-resize panel code. 1.20 +const PANEL_MIN_HEIGHT = 100; 1.21 +const PANEL_MIN_WIDTH = 330; 1.22 + 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "SharedFrame", 1.24 + "resource:///modules/SharedFrame.jsm"); 1.25 + 1.26 +XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() { 1.27 + let tmp = {}; 1.28 + Cu.import("resource:///modules/Social.jsm", tmp); 1.29 + return tmp.OpenGraphBuilder; 1.30 +}); 1.31 + 1.32 +XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() { 1.33 + let tmp = {}; 1.34 + Cu.import("resource:///modules/Social.jsm", tmp); 1.35 + return tmp.DynamicResizeWatcher; 1.36 +}); 1.37 + 1.38 +XPCOMUtils.defineLazyGetter(this, "sizeSocialPanelToContent", function() { 1.39 + let tmp = {}; 1.40 + Cu.import("resource:///modules/Social.jsm", tmp); 1.41 + return tmp.sizeSocialPanelToContent; 1.42 +}); 1.43 + 1.44 +XPCOMUtils.defineLazyGetter(this, "CreateSocialStatusWidget", function() { 1.45 + let tmp = {}; 1.46 + Cu.import("resource:///modules/Social.jsm", tmp); 1.47 + return tmp.CreateSocialStatusWidget; 1.48 +}); 1.49 + 1.50 +XPCOMUtils.defineLazyGetter(this, "CreateSocialMarkWidget", function() { 1.51 + let tmp = {}; 1.52 + Cu.import("resource:///modules/Social.jsm", tmp); 1.53 + return tmp.CreateSocialMarkWidget; 1.54 +}); 1.55 + 1.56 +SocialUI = { 1.57 + _initialized: false, 1.58 + 1.59 + // Called on delayed startup to initialize the UI 1.60 + init: function SocialUI_init() { 1.61 + if (this._initialized) { 1.62 + return; 1.63 + } 1.64 + 1.65 + Services.obs.addObserver(this, "social:ambient-notification-changed", false); 1.66 + Services.obs.addObserver(this, "social:profile-changed", false); 1.67 + Services.obs.addObserver(this, "social:frameworker-error", false); 1.68 + Services.obs.addObserver(this, "social:providers-changed", false); 1.69 + Services.obs.addObserver(this, "social:provider-reload", false); 1.70 + Services.obs.addObserver(this, "social:provider-enabled", false); 1.71 + Services.obs.addObserver(this, "social:provider-disabled", false); 1.72 + 1.73 + Services.prefs.addObserver("social.toast-notifications.enabled", this, false); 1.74 + 1.75 + gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true); 1.76 + document.getElementById("PanelUI-popup").addEventListener("popupshown", SocialMarks.updatePanelButtons, true); 1.77 + 1.78 + // menupopups that list social providers. we only populate them when shown, 1.79 + // and if it has not been done already. 1.80 + document.getElementById("viewSidebarMenu").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); 1.81 + document.getElementById("social-statusarea-popup").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); 1.82 + 1.83 + Social.init().then((update) => { 1.84 + if (update) 1.85 + this._providersChanged(); 1.86 + // handle SessionStore for the sidebar state 1.87 + SocialSidebar.restoreWindowState(); 1.88 + }); 1.89 + 1.90 + this._initialized = true; 1.91 + }, 1.92 + 1.93 + // Called on window unload 1.94 + uninit: function SocialUI_uninit() { 1.95 + if (!this._initialized) { 1.96 + return; 1.97 + } 1.98 + SocialSidebar.saveWindowState(); 1.99 + 1.100 + Services.obs.removeObserver(this, "social:ambient-notification-changed"); 1.101 + Services.obs.removeObserver(this, "social:profile-changed"); 1.102 + Services.obs.removeObserver(this, "social:frameworker-error"); 1.103 + Services.obs.removeObserver(this, "social:providers-changed"); 1.104 + Services.obs.removeObserver(this, "social:provider-reload"); 1.105 + Services.obs.removeObserver(this, "social:provider-enabled"); 1.106 + Services.obs.removeObserver(this, "social:provider-disabled"); 1.107 + 1.108 + Services.prefs.removeObserver("social.toast-notifications.enabled", this); 1.109 + 1.110 + document.getElementById("PanelUI-popup").removeEventListener("popupshown", SocialMarks.updatePanelButtons, true); 1.111 + document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); 1.112 + document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); 1.113 + 1.114 + this._initialized = false; 1.115 + }, 1.116 + 1.117 + observe: function SocialUI_observe(subject, topic, data) { 1.118 + // Exceptions here sometimes don't get reported properly, report them 1.119 + // manually :( 1.120 + try { 1.121 + switch (topic) { 1.122 + case "social:provider-enabled": 1.123 + SocialMarks.populateToolbarPalette(); 1.124 + SocialStatus.populateToolbarPalette(); 1.125 + break; 1.126 + case "social:provider-disabled": 1.127 + SocialMarks.removeProvider(data); 1.128 + SocialStatus.removeProvider(data); 1.129 + SocialSidebar.disableProvider(data); 1.130 + break; 1.131 + case "social:provider-reload": 1.132 + SocialStatus.reloadProvider(data); 1.133 + // if the reloaded provider is our current provider, fall through 1.134 + // to social:providers-changed so the ui will be reset 1.135 + if (!SocialSidebar.provider || SocialSidebar.provider.origin != data) 1.136 + return; 1.137 + // currently only the sidebar and flyout have a selected provider. 1.138 + // sidebar provider has changed (possibly to null), ensure the content 1.139 + // is unloaded and the frames are reset, they will be loaded in 1.140 + // providers-changed below if necessary. 1.141 + SocialSidebar.unloadSidebar(); 1.142 + SocialFlyout.unload(); 1.143 + // fall through to providers-changed to ensure the reloaded provider 1.144 + // is correctly reflected in any UI and the multi-provider menu 1.145 + case "social:providers-changed": 1.146 + this._providersChanged(); 1.147 + break; 1.148 + 1.149 + // Provider-specific notifications 1.150 + case "social:ambient-notification-changed": 1.151 + SocialStatus.updateButton(data); 1.152 + break; 1.153 + case "social:profile-changed": 1.154 + // make sure anything that happens here only affects the provider for 1.155 + // which the profile is changing, and that anything we call actually 1.156 + // needs to change based on profile data. 1.157 + SocialStatus.updateButton(data); 1.158 + break; 1.159 + case "social:frameworker-error": 1.160 + if (this.enabled && SocialSidebar.provider && SocialSidebar.provider.origin == data) { 1.161 + SocialSidebar.setSidebarErrorMessage(); 1.162 + } 1.163 + break; 1.164 + 1.165 + case "nsPref:changed": 1.166 + if (data == "social.toast-notifications.enabled") { 1.167 + SocialSidebar.updateToggleNotifications(); 1.168 + } 1.169 + break; 1.170 + } 1.171 + } catch (e) { 1.172 + Components.utils.reportError(e + "\n" + e.stack); 1.173 + throw e; 1.174 + } 1.175 + }, 1.176 + 1.177 + _providersChanged: function() { 1.178 + SocialSidebar.clearProviderMenus(); 1.179 + SocialSidebar.update(); 1.180 + SocialChatBar.update(); 1.181 + SocialShare.populateProviderMenu(); 1.182 + SocialStatus.populateToolbarPalette(); 1.183 + SocialMarks.populateToolbarPalette(); 1.184 + SocialShare.update(); 1.185 + }, 1.186 + 1.187 + // This handles "ActivateSocialFeature" events fired against content documents 1.188 + // in this window. 1.189 + _activationEventHandler: function SocialUI_activationHandler(e) { 1.190 + let targetDoc; 1.191 + let node; 1.192 + if (e.target instanceof HTMLDocument) { 1.193 + // version 0 support 1.194 + targetDoc = e.target; 1.195 + node = targetDoc.documentElement 1.196 + } else { 1.197 + targetDoc = e.target.ownerDocument; 1.198 + node = e.target; 1.199 + } 1.200 + if (!(targetDoc instanceof HTMLDocument)) 1.201 + return; 1.202 + 1.203 + // Ignore events fired in background tabs or iframes 1.204 + if (targetDoc.defaultView != content) 1.205 + return; 1.206 + 1.207 + // If we are in PB mode, we silently do nothing (bug 829404 exists to 1.208 + // do something sensible here...) 1.209 + if (PrivateBrowsingUtils.isWindowPrivate(window)) 1.210 + return; 1.211 + 1.212 + // If the last event was received < 1s ago, ignore this one 1.213 + let now = Date.now(); 1.214 + if (now - Social.lastEventReceived < 1000) 1.215 + return; 1.216 + Social.lastEventReceived = now; 1.217 + 1.218 + // We only want to activate if it is as a result of user input. 1.219 + let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor) 1.220 + .getInterface(Ci.nsIDOMWindowUtils); 1.221 + if (!dwu.isHandlingUserInput) { 1.222 + Cu.reportError("attempt to activate provider without user input from " + targetDoc.nodePrincipal.origin); 1.223 + return; 1.224 + } 1.225 + 1.226 + let data = node.getAttribute("data-service"); 1.227 + if (data) { 1.228 + try { 1.229 + data = JSON.parse(data); 1.230 + } catch(e) { 1.231 + Cu.reportError("Social Service manifest parse error: "+e); 1.232 + return; 1.233 + } 1.234 + } 1.235 + Social.installProvider(targetDoc, data, function(manifest) { 1.236 + Social.activateFromOrigin(manifest.origin, function(provider) { 1.237 + if (provider.sidebarURL) { 1.238 + SocialSidebar.show(provider.origin); 1.239 + } 1.240 + if (provider.postActivationURL) { 1.241 + openUILinkIn(provider.postActivationURL, "tab"); 1.242 + } 1.243 + }); 1.244 + }); 1.245 + }, 1.246 + 1.247 + showLearnMore: function() { 1.248 + let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api"; 1.249 + openUILinkIn(url, "tab"); 1.250 + }, 1.251 + 1.252 + closeSocialPanelForLinkTraversal: function (target, linkNode) { 1.253 + // No need to close the panel if this traversal was not retargeted 1.254 + if (target == "" || target == "_self") 1.255 + return; 1.256 + 1.257 + // Check to see whether this link traversal was in a social panel 1.258 + let win = linkNode.ownerDocument.defaultView; 1.259 + let container = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.260 + .getInterface(Ci.nsIWebNavigation) 1.261 + .QueryInterface(Ci.nsIDocShell) 1.262 + .chromeEventHandler; 1.263 + let containerParent = container.parentNode; 1.264 + if (containerParent.classList.contains("social-panel") && 1.265 + containerParent instanceof Ci.nsIDOMXULPopupElement) { 1.266 + // allow the link traversal to finish before closing the panel 1.267 + setTimeout(() => { 1.268 + containerParent.hidePopup(); 1.269 + }, 0); 1.270 + } 1.271 + }, 1.272 + 1.273 + get _chromeless() { 1.274 + // Is this a popup window that doesn't want chrome shown? 1.275 + let docElem = document.documentElement; 1.276 + // extrachrome is not restored during session restore, so we need 1.277 + // to check for the toolbar as well. 1.278 + let chromeless = docElem.getAttribute("chromehidden").contains("extrachrome") || 1.279 + docElem.getAttribute('chromehidden').contains("toolbar"); 1.280 + // This property is "fixed" for a window, so avoid doing the check above 1.281 + // multiple times... 1.282 + delete this._chromeless; 1.283 + this._chromeless = chromeless; 1.284 + return chromeless; 1.285 + }, 1.286 + 1.287 + get enabled() { 1.288 + // Returns whether social is enabled *for this window*. 1.289 + if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window)) 1.290 + return false; 1.291 + return Social.providers.length > 0; 1.292 + }, 1.293 + 1.294 + // called on tab/urlbar/location changes and after customization. Update 1.295 + // anything that is tab specific. 1.296 + updateState: function() { 1.297 + if (!this.enabled) 1.298 + return; 1.299 + SocialMarks.update(); 1.300 + SocialShare.update(); 1.301 + } 1.302 +} 1.303 + 1.304 +SocialChatBar = { 1.305 + get chatbar() { 1.306 + return document.getElementById("pinnedchats"); 1.307 + }, 1.308 + // Whether the chatbar is available for this window. Note that in full-screen 1.309 + // mode chats are available, but not shown. 1.310 + get isAvailable() { 1.311 + return SocialUI.enabled; 1.312 + }, 1.313 + // Does this chatbar have any chats (whether minimized, collapsed or normal) 1.314 + get hasChats() { 1.315 + return !!this.chatbar.firstElementChild; 1.316 + }, 1.317 + openChat: function(aProvider, aURL, aCallback, aMode) { 1.318 + this.update(); 1.319 + if (!this.isAvailable) 1.320 + return false; 1.321 + this.chatbar.openChat(aProvider, aURL, aCallback, aMode); 1.322 + // We only want to focus the chat if it is as a result of user input. 1.323 + let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor) 1.324 + .getInterface(Ci.nsIDOMWindowUtils); 1.325 + if (dwu.isHandlingUserInput) 1.326 + this.chatbar.focus(); 1.327 + return true; 1.328 + }, 1.329 + update: function() { 1.330 + let command = document.getElementById("Social:FocusChat"); 1.331 + if (!this.isAvailable) { 1.332 + this.chatbar.hidden = command.hidden = true; 1.333 + } else { 1.334 + this.chatbar.hidden = command.hidden = false; 1.335 + } 1.336 + command.setAttribute("disabled", command.hidden ? "true" : "false"); 1.337 + }, 1.338 + focus: function SocialChatBar_focus() { 1.339 + this.chatbar.focus(); 1.340 + } 1.341 +} 1.342 + 1.343 +SocialFlyout = { 1.344 + get panel() { 1.345 + return document.getElementById("social-flyout-panel"); 1.346 + }, 1.347 + 1.348 + get iframe() { 1.349 + if (!this.panel.firstChild) 1.350 + this._createFrame(); 1.351 + return this.panel.firstChild; 1.352 + }, 1.353 + 1.354 + dispatchPanelEvent: function(name) { 1.355 + let doc = this.iframe.contentDocument; 1.356 + let evt = doc.createEvent("CustomEvent"); 1.357 + evt.initCustomEvent(name, true, true, {}); 1.358 + doc.documentElement.dispatchEvent(evt); 1.359 + }, 1.360 + 1.361 + _createFrame: function() { 1.362 + let panel = this.panel; 1.363 + if (!SocialUI.enabled || panel.firstChild) 1.364 + return; 1.365 + // create and initialize the panel for this window 1.366 + let iframe = document.createElement("iframe"); 1.367 + iframe.setAttribute("type", "content"); 1.368 + iframe.setAttribute("class", "social-panel-frame"); 1.369 + iframe.setAttribute("flex", "1"); 1.370 + iframe.setAttribute("tooltip", "aHTMLTooltip"); 1.371 + iframe.setAttribute("origin", SocialSidebar.provider.origin); 1.372 + panel.appendChild(iframe); 1.373 + }, 1.374 + 1.375 + setFlyoutErrorMessage: function SF_setFlyoutErrorMessage() { 1.376 + this.iframe.removeAttribute("src"); 1.377 + this.iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + 1.378 + encodeURIComponent(this.iframe.getAttribute("origin")), 1.379 + null, null, null, null); 1.380 + sizeSocialPanelToContent(this.panel, this.iframe); 1.381 + }, 1.382 + 1.383 + unload: function() { 1.384 + let panel = this.panel; 1.385 + panel.hidePopup(); 1.386 + if (!panel.firstChild) 1.387 + return 1.388 + let iframe = panel.firstChild; 1.389 + if (iframe.socialErrorListener) 1.390 + iframe.socialErrorListener.remove(); 1.391 + panel.removeChild(iframe); 1.392 + }, 1.393 + 1.394 + onShown: function(aEvent) { 1.395 + let panel = this.panel; 1.396 + let iframe = this.iframe; 1.397 + this._dynamicResizer = new DynamicResizeWatcher(); 1.398 + iframe.docShell.isActive = true; 1.399 + iframe.docShell.isAppTab = true; 1.400 + if (iframe.contentDocument.readyState == "complete") { 1.401 + this._dynamicResizer.start(panel, iframe); 1.402 + this.dispatchPanelEvent("socialFrameShow"); 1.403 + } else { 1.404 + // first time load, wait for load and dispatch after load 1.405 + iframe.addEventListener("load", function panelBrowserOnload(e) { 1.406 + iframe.removeEventListener("load", panelBrowserOnload, true); 1.407 + setTimeout(function() { 1.408 + if (SocialFlyout._dynamicResizer) { // may go null if hidden quickly 1.409 + SocialFlyout._dynamicResizer.start(panel, iframe); 1.410 + SocialFlyout.dispatchPanelEvent("socialFrameShow"); 1.411 + } 1.412 + }, 0); 1.413 + }, true); 1.414 + } 1.415 + }, 1.416 + 1.417 + onHidden: function(aEvent) { 1.418 + this._dynamicResizer.stop(); 1.419 + this._dynamicResizer = null; 1.420 + this.iframe.docShell.isActive = false; 1.421 + this.dispatchPanelEvent("socialFrameHide"); 1.422 + }, 1.423 + 1.424 + load: function(aURL, cb) { 1.425 + if (!SocialSidebar.provider) 1.426 + return; 1.427 + 1.428 + this.panel.hidden = false; 1.429 + let iframe = this.iframe; 1.430 + // same url with only ref difference does not cause a new load, so we 1.431 + // want to go right to the callback 1.432 + let src = iframe.contentDocument && iframe.contentDocument.documentURIObject; 1.433 + if (!src || !src.equalsExceptRef(Services.io.newURI(aURL, null, null))) { 1.434 + iframe.addEventListener("load", function documentLoaded() { 1.435 + iframe.removeEventListener("load", documentLoaded, true); 1.436 + cb(); 1.437 + }, true); 1.438 + // Force a layout flush by calling .clientTop so 1.439 + // that the docShell of this frame is created 1.440 + iframe.clientTop; 1.441 + Social.setErrorListener(iframe, SocialFlyout.setFlyoutErrorMessage.bind(SocialFlyout)) 1.442 + iframe.setAttribute("src", aURL); 1.443 + } else { 1.444 + // we still need to set the src to trigger the contents hashchange event 1.445 + // for ref changes 1.446 + iframe.setAttribute("src", aURL); 1.447 + cb(); 1.448 + } 1.449 + }, 1.450 + 1.451 + open: function(aURL, yOffset, aCallback) { 1.452 + // Hide any other social panels that may be open. 1.453 + document.getElementById("social-notification-panel").hidePopup(); 1.454 + 1.455 + if (!SocialUI.enabled) 1.456 + return; 1.457 + let panel = this.panel; 1.458 + let iframe = this.iframe; 1.459 + 1.460 + this.load(aURL, function() { 1.461 + sizeSocialPanelToContent(panel, iframe); 1.462 + let anchor = document.getElementById("social-sidebar-browser"); 1.463 + if (panel.state == "open") { 1.464 + panel.moveToAnchor(anchor, "start_before", 0, yOffset, false); 1.465 + } else { 1.466 + panel.openPopup(anchor, "start_before", 0, yOffset, false, false); 1.467 + } 1.468 + if (aCallback) { 1.469 + try { 1.470 + aCallback(iframe.contentWindow); 1.471 + } catch(e) { 1.472 + Cu.reportError(e); 1.473 + } 1.474 + } 1.475 + }); 1.476 + } 1.477 +} 1.478 + 1.479 +SocialShare = { 1.480 + get panel() { 1.481 + return document.getElementById("social-share-panel"); 1.482 + }, 1.483 + 1.484 + get iframe() { 1.485 + // first element is our menu vbox. 1.486 + if (this.panel.childElementCount == 1) 1.487 + return null; 1.488 + else 1.489 + return this.panel.lastChild; 1.490 + }, 1.491 + 1.492 + uninit: function () { 1.493 + if (this.iframe) { 1.494 + this.iframe.remove(); 1.495 + } 1.496 + }, 1.497 + 1.498 + _createFrame: function() { 1.499 + let panel = this.panel; 1.500 + if (!SocialUI.enabled || this.iframe) 1.501 + return; 1.502 + this.panel.hidden = false; 1.503 + // create and initialize the panel for this window 1.504 + let iframe = document.createElement("iframe"); 1.505 + iframe.setAttribute("type", "content"); 1.506 + iframe.setAttribute("class", "social-share-frame"); 1.507 + iframe.setAttribute("context", "contentAreaContextMenu"); 1.508 + iframe.setAttribute("tooltip", "aHTMLTooltip"); 1.509 + iframe.setAttribute("flex", "1"); 1.510 + panel.appendChild(iframe); 1.511 + this.populateProviderMenu(); 1.512 + }, 1.513 + 1.514 + getSelectedProvider: function() { 1.515 + let provider; 1.516 + let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin"); 1.517 + if (lastProviderOrigin) { 1.518 + provider = Social._getProviderFromOrigin(lastProviderOrigin); 1.519 + } 1.520 + // if they have a provider selected in the sidebar use that for the initial 1.521 + // default in share 1.522 + if (!provider) 1.523 + provider = SocialSidebar.provider; 1.524 + // if our provider has no shareURL, select the first one that does 1.525 + if (!provider || !provider.shareURL) { 1.526 + let providers = [p for (p of Social.providers) if (p.shareURL)]; 1.527 + provider = providers.length > 0 && providers[0]; 1.528 + } 1.529 + return provider; 1.530 + }, 1.531 + 1.532 + populateProviderMenu: function() { 1.533 + if (!this.iframe) 1.534 + return; 1.535 + let providers = [p for (p of Social.providers) if (p.shareURL)]; 1.536 + let hbox = document.getElementById("social-share-provider-buttons"); 1.537 + // selectable providers are inserted before the provider-menu seperator, 1.538 + // remove any menuitems in that area 1.539 + while (hbox.firstChild) { 1.540 + hbox.removeChild(hbox.firstChild); 1.541 + } 1.542 + // reset our share toolbar 1.543 + // only show a selection if there is more than one 1.544 + if (!SocialUI.enabled || providers.length < 2) { 1.545 + this.panel.firstChild.hidden = true; 1.546 + return; 1.547 + } 1.548 + let selectedProvider = this.getSelectedProvider(); 1.549 + for (let provider of providers) { 1.550 + let button = document.createElement("toolbarbutton"); 1.551 + button.setAttribute("class", "toolbarbutton share-provider-button"); 1.552 + button.setAttribute("type", "radio"); 1.553 + button.setAttribute("group", "share-providers"); 1.554 + button.setAttribute("image", provider.iconURL); 1.555 + button.setAttribute("tooltiptext", provider.name); 1.556 + button.setAttribute("origin", provider.origin); 1.557 + button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin')); this.checked=true;"); 1.558 + if (provider == selectedProvider) { 1.559 + this.defaultButton = button; 1.560 + } 1.561 + hbox.appendChild(button); 1.562 + } 1.563 + if (!this.defaultButton) { 1.564 + this.defaultButton = hbox.firstChild 1.565 + } 1.566 + this.defaultButton.setAttribute("checked", "true"); 1.567 + this.panel.firstChild.hidden = false; 1.568 + }, 1.569 + 1.570 + get shareButton() { 1.571 + return document.getElementById("social-share-button"); 1.572 + }, 1.573 + 1.574 + canSharePage: function(aURI) { 1.575 + // we do not enable sharing from private sessions 1.576 + if (PrivateBrowsingUtils.isWindowPrivate(window)) 1.577 + return false; 1.578 + 1.579 + if (!aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https'))) 1.580 + return false; 1.581 + return true; 1.582 + }, 1.583 + 1.584 + update: function() { 1.585 + let shareButton = this.shareButton; 1.586 + shareButton.hidden = !SocialUI.enabled || 1.587 + [p for (p of Social.providers) if (p.shareURL)].length == 0; 1.588 + shareButton.disabled = shareButton.hidden || !this.canSharePage(gBrowser.currentURI); 1.589 + 1.590 + // also update the relevent command's disabled state so the keyboard 1.591 + // shortcut only works when available. 1.592 + let cmd = document.getElementById("Social:SharePage"); 1.593 + if (shareButton.disabled) 1.594 + cmd.setAttribute("disabled", "true"); 1.595 + else 1.596 + cmd.removeAttribute("disabled"); 1.597 + }, 1.598 + 1.599 + onShowing: function() { 1.600 + this.shareButton.setAttribute("open", "true"); 1.601 + }, 1.602 + 1.603 + onHidden: function() { 1.604 + this.shareButton.removeAttribute("open"); 1.605 + this.iframe.setAttribute("src", "data:text/plain;charset=utf8,"); 1.606 + this.currentShare = null; 1.607 + }, 1.608 + 1.609 + setErrorMessage: function() { 1.610 + let iframe = this.iframe; 1.611 + if (!iframe) 1.612 + return; 1.613 + 1.614 + iframe.removeAttribute("src"); 1.615 + iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + 1.616 + encodeURIComponent(iframe.getAttribute("origin")), 1.617 + null, null, null, null); 1.618 + sizeSocialPanelToContent(this.panel, iframe); 1.619 + }, 1.620 + 1.621 + sharePage: function(providerOrigin, graphData) { 1.622 + // if providerOrigin is undefined, we use the last-used provider, or the 1.623 + // current/default provider. The provider selection in the share panel 1.624 + // will call sharePage with an origin for us to switch to. 1.625 + this._createFrame(); 1.626 + let iframe = this.iframe; 1.627 + let provider; 1.628 + if (providerOrigin) 1.629 + provider = Social._getProviderFromOrigin(providerOrigin); 1.630 + else 1.631 + provider = this.getSelectedProvider(); 1.632 + if (!provider || !provider.shareURL) 1.633 + return; 1.634 + 1.635 + // graphData is an optional param that either defines the full set of data 1.636 + // to be shared, or partial data about the current page. It is set by a call 1.637 + // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST 1.638 + // define at least url. If it is undefined, we're sharing the current url in 1.639 + // the browser tab. 1.640 + let sharedURI = graphData ? Services.io.newURI(graphData.url, null, null) : 1.641 + gBrowser.currentURI; 1.642 + if (!this.canSharePage(sharedURI)) 1.643 + return; 1.644 + 1.645 + // the point of this action type is that we can use existing share 1.646 + // endpoints (e.g. oexchange) that do not support additional 1.647 + // socialapi functionality. One tweak is that we shoot an event 1.648 + // containing the open graph data. 1.649 + let pageData = graphData ? graphData : this.currentShare; 1.650 + if (!pageData || sharedURI == gBrowser.currentURI) { 1.651 + pageData = OpenGraphBuilder.getData(gBrowser); 1.652 + if (graphData) { 1.653 + // overwrite data retreived from page with data given to us as a param 1.654 + for (let p in graphData) { 1.655 + pageData[p] = graphData[p]; 1.656 + } 1.657 + } 1.658 + } 1.659 + this.currentShare = pageData; 1.660 + 1.661 + let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData); 1.662 + 1.663 + this._dynamicResizer = new DynamicResizeWatcher(); 1.664 + // if we've already loaded this provider/page share endpoint, we don't want 1.665 + // to add another load event listener. 1.666 + let reload = true; 1.667 + let endpointMatch = shareEndpoint == iframe.getAttribute("src"); 1.668 + let docLoaded = iframe.contentDocument && iframe.contentDocument.readyState == "complete"; 1.669 + if (endpointMatch && docLoaded) { 1.670 + reload = shareEndpoint != iframe.contentDocument.location.spec; 1.671 + } 1.672 + if (!reload) { 1.673 + this._dynamicResizer.start(this.panel, iframe); 1.674 + iframe.docShell.isActive = true; 1.675 + iframe.docShell.isAppTab = true; 1.676 + let evt = iframe.contentDocument.createEvent("CustomEvent"); 1.677 + evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData)); 1.678 + iframe.contentDocument.documentElement.dispatchEvent(evt); 1.679 + } else { 1.680 + // first time load, wait for load and dispatch after load 1.681 + iframe.addEventListener("load", function panelBrowserOnload(e) { 1.682 + iframe.removeEventListener("load", panelBrowserOnload, true); 1.683 + iframe.docShell.isActive = true; 1.684 + iframe.docShell.isAppTab = true; 1.685 + setTimeout(function() { 1.686 + if (SocialShare._dynamicResizer) { // may go null if hidden quickly 1.687 + SocialShare._dynamicResizer.start(iframe.parentNode, iframe); 1.688 + } 1.689 + }, 0); 1.690 + let evt = iframe.contentDocument.createEvent("CustomEvent"); 1.691 + evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData)); 1.692 + iframe.contentDocument.documentElement.dispatchEvent(evt); 1.693 + }, true); 1.694 + } 1.695 + // always ensure that origin belongs to the endpoint 1.696 + let uri = Services.io.newURI(shareEndpoint, null, null); 1.697 + iframe.setAttribute("origin", provider.origin); 1.698 + iframe.setAttribute("src", shareEndpoint); 1.699 + 1.700 + let navBar = document.getElementById("nav-bar"); 1.701 + let anchor = document.getAnonymousElementByAttribute(this.shareButton, "class", "toolbarbutton-icon"); 1.702 + this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); 1.703 + Social.setErrorListener(iframe, this.setErrorMessage.bind(this)); 1.704 + } 1.705 +}; 1.706 + 1.707 +SocialSidebar = { 1.708 + // Whether the sidebar can be shown for this window. 1.709 + get canShow() { 1.710 + if (!SocialUI.enabled || document.mozFullScreen) 1.711 + return false; 1.712 + return Social.providers.some(p => p.sidebarURL); 1.713 + }, 1.714 + 1.715 + // Whether the user has toggled the sidebar on (for windows where it can appear) 1.716 + get opened() { 1.717 + let broadcaster = document.getElementById("socialSidebarBroadcaster"); 1.718 + return !broadcaster.hidden; 1.719 + }, 1.720 + 1.721 + restoreWindowState: function() { 1.722 + // Window state is used to allow different sidebar providers in each window. 1.723 + // We also store the provider used in a pref as the default sidebar to 1.724 + // maintain that state for users who do not restore window state. The 1.725 + // existence of social.sidebar.provider means the sidebar is open with that 1.726 + // provider. 1.727 + this._initialized = true; 1.728 + if (!this.canShow) 1.729 + return; 1.730 + 1.731 + if (Services.prefs.prefHasUserValue("social.provider.current")) { 1.732 + // "upgrade" when the first window opens if we have old prefs. We get the 1.733 + // values from prefs this one time, window state will be saved when this 1.734 + // window is closed. 1.735 + let origin = Services.prefs.getCharPref("social.provider.current"); 1.736 + Services.prefs.clearUserPref("social.provider.current"); 1.737 + // social.sidebar.open default was true, but we only opened if there was 1.738 + // a current provider 1.739 + let opened = origin && true; 1.740 + if (Services.prefs.prefHasUserValue("social.sidebar.open")) { 1.741 + opened = origin && Services.prefs.getBoolPref("social.sidebar.open"); 1.742 + Services.prefs.clearUserPref("social.sidebar.open"); 1.743 + } 1.744 + let data = { 1.745 + "hidden": !opened, 1.746 + "origin": origin 1.747 + }; 1.748 + SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data)); 1.749 + } 1.750 + 1.751 + let data = SessionStore.getWindowValue(window, "socialSidebar"); 1.752 + // if this window doesn't have it's own state, use the state from the opener 1.753 + if (!data && window.opener && !window.opener.closed) { 1.754 + try { 1.755 + data = SessionStore.getWindowValue(window.opener, "socialSidebar"); 1.756 + } catch(e) { 1.757 + // Window is not tracked, which happens on osx if the window is opened 1.758 + // from the hidden window. That happens when you close the last window 1.759 + // without quiting firefox, then open a new window. 1.760 + } 1.761 + } 1.762 + if (data) { 1.763 + data = JSON.parse(data); 1.764 + document.getElementById("social-sidebar-browser").setAttribute("origin", data.origin); 1.765 + if (!data.hidden) 1.766 + this.show(data.origin); 1.767 + } else if (Services.prefs.prefHasUserValue("social.sidebar.provider")) { 1.768 + // no window state, use the global state if it is available 1.769 + this.show(Services.prefs.getCharPref("social.sidebar.provider")); 1.770 + } 1.771 + }, 1.772 + 1.773 + saveWindowState: function() { 1.774 + let broadcaster = document.getElementById("socialSidebarBroadcaster"); 1.775 + let sidebarOrigin = document.getElementById("social-sidebar-browser").getAttribute("origin"); 1.776 + let data = { 1.777 + "hidden": broadcaster.hidden, 1.778 + "origin": sidebarOrigin 1.779 + }; 1.780 + 1.781 + // Save a global state for users who do not restore state. 1.782 + if (broadcaster.hidden) 1.783 + Services.prefs.clearUserPref("social.sidebar.provider"); 1.784 + else 1.785 + Services.prefs.setCharPref("social.sidebar.provider", sidebarOrigin); 1.786 + 1.787 + try { 1.788 + SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data)); 1.789 + } catch(e) { 1.790 + // window not tracked during uninit 1.791 + } 1.792 + }, 1.793 + 1.794 + setSidebarVisibilityState: function(aEnabled) { 1.795 + let sbrowser = document.getElementById("social-sidebar-browser"); 1.796 + // it's possible we'll be called twice with aEnabled=false so let's 1.797 + // just assume we may often be called with the same state. 1.798 + if (aEnabled == sbrowser.docShellIsActive) 1.799 + return; 1.800 + sbrowser.docShellIsActive = aEnabled; 1.801 + let evt = sbrowser.contentDocument.createEvent("CustomEvent"); 1.802 + evt.initCustomEvent(aEnabled ? "socialFrameShow" : "socialFrameHide", true, true, {}); 1.803 + sbrowser.contentDocument.documentElement.dispatchEvent(evt); 1.804 + }, 1.805 + 1.806 + updateToggleNotifications: function() { 1.807 + let command = document.getElementById("Social:ToggleNotifications"); 1.808 + command.setAttribute("checked", Services.prefs.getBoolPref("social.toast-notifications.enabled")); 1.809 + command.setAttribute("hidden", !SocialUI.enabled); 1.810 + }, 1.811 + 1.812 + update: function SocialSidebar_update() { 1.813 + // ensure we never update before restoreWindowState 1.814 + if (!this._initialized) 1.815 + return; 1.816 + this.ensureProvider(); 1.817 + this.updateToggleNotifications(); 1.818 + this._updateHeader(); 1.819 + clearTimeout(this._unloadTimeoutId); 1.820 + // Hide the toggle menu item if the sidebar cannot appear 1.821 + let command = document.getElementById("Social:ToggleSidebar"); 1.822 + command.setAttribute("hidden", this.canShow ? "false" : "true"); 1.823 + 1.824 + // Hide the sidebar if it cannot appear, or has been toggled off. 1.825 + // Also set the command "checked" state accordingly. 1.826 + let hideSidebar = !this.canShow || !this.opened; 1.827 + let broadcaster = document.getElementById("socialSidebarBroadcaster"); 1.828 + broadcaster.hidden = hideSidebar; 1.829 + command.setAttribute("checked", !hideSidebar); 1.830 + 1.831 + let sbrowser = document.getElementById("social-sidebar-browser"); 1.832 + 1.833 + if (hideSidebar) { 1.834 + sbrowser.removeEventListener("load", SocialSidebar._loadListener, true); 1.835 + this.setSidebarVisibilityState(false); 1.836 + // If we've been disabled, unload the sidebar content immediately; 1.837 + // if the sidebar was just toggled to invisible, wait a timeout 1.838 + // before unloading. 1.839 + if (!this.canShow) { 1.840 + this.unloadSidebar(); 1.841 + } else { 1.842 + this._unloadTimeoutId = setTimeout( 1.843 + this.unloadSidebar, 1.844 + Services.prefs.getIntPref("social.sidebar.unload_timeout_ms") 1.845 + ); 1.846 + } 1.847 + } else { 1.848 + sbrowser.setAttribute("origin", this.provider.origin); 1.849 + if (this.provider.errorState == "frameworker-error") { 1.850 + SocialSidebar.setSidebarErrorMessage(); 1.851 + return; 1.852 + } 1.853 + 1.854 + // Make sure the right sidebar URL is loaded 1.855 + if (sbrowser.getAttribute("src") != this.provider.sidebarURL) { 1.856 + // we check readyState right after setting src, we need a new content 1.857 + // viewer to ensure we are checking against the correct document. 1.858 + sbrowser.docShell.createAboutBlankContentViewer(null); 1.859 + Social.setErrorListener(sbrowser, this.setSidebarErrorMessage.bind(this)); 1.860 + // setting isAppTab causes clicks on untargeted links to open new tabs 1.861 + sbrowser.docShell.isAppTab = true; 1.862 + sbrowser.setAttribute("src", this.provider.sidebarURL); 1.863 + PopupNotifications.locationChange(sbrowser); 1.864 + } 1.865 + 1.866 + // if the document has not loaded, delay until it is 1.867 + if (sbrowser.contentDocument.readyState != "complete") { 1.868 + document.getElementById("social-sidebar-button").setAttribute("loading", "true"); 1.869 + sbrowser.addEventListener("load", SocialSidebar._loadListener, true); 1.870 + } else { 1.871 + this.setSidebarVisibilityState(true); 1.872 + } 1.873 + } 1.874 + this._updateCheckedMenuItems(this.opened && this.provider ? this.provider.origin : null); 1.875 + }, 1.876 + 1.877 + _loadListener: function SocialSidebar_loadListener() { 1.878 + let sbrowser = document.getElementById("social-sidebar-browser"); 1.879 + sbrowser.removeEventListener("load", SocialSidebar._loadListener, true); 1.880 + document.getElementById("social-sidebar-button").removeAttribute("loading"); 1.881 + SocialSidebar.setSidebarVisibilityState(true); 1.882 + }, 1.883 + 1.884 + unloadSidebar: function SocialSidebar_unloadSidebar() { 1.885 + let sbrowser = document.getElementById("social-sidebar-browser"); 1.886 + if (!sbrowser.hasAttribute("origin")) 1.887 + return; 1.888 + 1.889 + sbrowser.stop(); 1.890 + sbrowser.removeAttribute("origin"); 1.891 + sbrowser.setAttribute("src", "about:blank"); 1.892 + // We need to explicitly create a new content viewer because the old one 1.893 + // doesn't get destroyed until about:blank has loaded (which does not happen 1.894 + // as long as the element is hidden). 1.895 + sbrowser.docShell.createAboutBlankContentViewer(null); 1.896 + SocialFlyout.unload(); 1.897 + }, 1.898 + 1.899 + _unloadTimeoutId: 0, 1.900 + 1.901 + setSidebarErrorMessage: function() { 1.902 + let sbrowser = document.getElementById("social-sidebar-browser"); 1.903 + // a frameworker error "trumps" a sidebar error. 1.904 + let origin = sbrowser.getAttribute("origin"); 1.905 + if (origin) { 1.906 + origin = "&origin=" + encodeURIComponent(origin); 1.907 + } 1.908 + if (this.provider.errorState == "frameworker-error") { 1.909 + sbrowser.setAttribute("src", "about:socialerror?mode=workerFailure" + origin); 1.910 + } else { 1.911 + let url = encodeURIComponent(this.provider.sidebarURL); 1.912 + sbrowser.loadURI("about:socialerror?mode=tryAgain&url=" + url + origin, null, null); 1.913 + } 1.914 + }, 1.915 + 1.916 + _provider: null, 1.917 + ensureProvider: function() { 1.918 + if (this._provider) 1.919 + return; 1.920 + // origin for sidebar is persisted, so get the previously selected sidebar 1.921 + // first, otherwise fallback to the first provider in the list 1.922 + let sbrowser = document.getElementById("social-sidebar-browser"); 1.923 + let origin = sbrowser.getAttribute("origin"); 1.924 + let providers = [p for (p of Social.providers) if (p.sidebarURL)]; 1.925 + let provider; 1.926 + if (origin) 1.927 + provider = Social._getProviderFromOrigin(origin); 1.928 + if (!provider && providers.length > 0) 1.929 + provider = providers[0]; 1.930 + if (provider) 1.931 + this.provider = provider; 1.932 + }, 1.933 + 1.934 + get provider() { 1.935 + return this._provider; 1.936 + }, 1.937 + 1.938 + set provider(provider) { 1.939 + if (!provider || provider.sidebarURL) { 1.940 + this._provider = provider; 1.941 + this._updateHeader(); 1.942 + this._updateCheckedMenuItems(provider && provider.origin); 1.943 + this.update(); 1.944 + } 1.945 + }, 1.946 + 1.947 + disableProvider: function(origin) { 1.948 + if (this._provider && this._provider.origin == origin) { 1.949 + this._provider = null; 1.950 + // force a selection of the next provider if there is one 1.951 + this.ensureProvider(); 1.952 + } 1.953 + }, 1.954 + 1.955 + _updateHeader: function() { 1.956 + let provider = this.provider; 1.957 + let image, title; 1.958 + if (provider) { 1.959 + image = "url(" + (provider.icon32URL || provider.iconURL) + ")"; 1.960 + title = provider.name; 1.961 + } 1.962 + document.getElementById("social-sidebar-favico").style.listStyleImage = image; 1.963 + document.getElementById("social-sidebar-title").value = title; 1.964 + }, 1.965 + 1.966 + _updateCheckedMenuItems: function(origin) { 1.967 + // update selected menuitems 1.968 + let menuitems = document.getElementsByClassName("social-provider-menuitem"); 1.969 + for (let mi of menuitems) { 1.970 + if (origin && mi.getAttribute("origin") == origin) { 1.971 + mi.setAttribute("checked", "true"); 1.972 + mi.setAttribute("oncommand", "SocialSidebar.hide();"); 1.973 + } else if (mi.getAttribute("checked")) { 1.974 + mi.removeAttribute("checked"); 1.975 + mi.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));"); 1.976 + } 1.977 + } 1.978 + }, 1.979 + 1.980 + show: function(origin) { 1.981 + // always show the sidebar, and set the provider 1.982 + let broadcaster = document.getElementById("socialSidebarBroadcaster"); 1.983 + broadcaster.hidden = false; 1.984 + if (origin) 1.985 + this.provider = Social._getProviderFromOrigin(origin); 1.986 + else 1.987 + SocialSidebar.update(); 1.988 + this.saveWindowState(); 1.989 + }, 1.990 + 1.991 + hide: function() { 1.992 + let broadcaster = document.getElementById("socialSidebarBroadcaster"); 1.993 + broadcaster.hidden = true; 1.994 + this._updateCheckedMenuItems(); 1.995 + this.clearProviderMenus(); 1.996 + SocialSidebar.update(); 1.997 + this.saveWindowState(); 1.998 + }, 1.999 + 1.1000 + toggleSidebar: function SocialSidebar_toggle() { 1.1001 + let broadcaster = document.getElementById("socialSidebarBroadcaster"); 1.1002 + if (broadcaster.hidden) 1.1003 + this.show(); 1.1004 + else 1.1005 + this.hide(); 1.1006 + }, 1.1007 + 1.1008 + populateSidebarMenu: function(event) { 1.1009 + // Providers are removed from the view->sidebar menu when there is a change 1.1010 + // in providers, so we only have to populate onshowing if there are no 1.1011 + // provider menus. We populate this menu so long as there are enabled 1.1012 + // providers with sidebars. 1.1013 + let popup = event.target; 1.1014 + let providerMenuSeps = popup.getElementsByClassName("social-provider-menu"); 1.1015 + if (providerMenuSeps[0].previousSibling.nodeName == "menuseparator") 1.1016 + SocialSidebar.populateProviderMenu(providerMenuSeps[0]); 1.1017 + }, 1.1018 + 1.1019 + clearProviderMenus: function() { 1.1020 + // called when there is a change in the provider list we clear all menus, 1.1021 + // they will be repopulated when the menu is shown 1.1022 + let providerMenuSeps = document.getElementsByClassName("social-provider-menu"); 1.1023 + for (let providerMenuSep of providerMenuSeps) { 1.1024 + while (providerMenuSep.previousSibling.nodeName == "menuitem") { 1.1025 + let menu = providerMenuSep.parentNode; 1.1026 + menu.removeChild(providerMenuSep.previousSibling); 1.1027 + } 1.1028 + } 1.1029 + }, 1.1030 + 1.1031 + populateProviderMenu: function(providerMenuSep) { 1.1032 + let menu = providerMenuSep.parentNode; 1.1033 + // selectable providers are inserted before the provider-menu seperator, 1.1034 + // remove any menuitems in that area 1.1035 + while (providerMenuSep.previousSibling.nodeName == "menuitem") { 1.1036 + menu.removeChild(providerMenuSep.previousSibling); 1.1037 + } 1.1038 + // only show a selection in the sidebar header menu if there is more than one 1.1039 + let providers = [p for (p of Social.providers) if (p.sidebarURL)]; 1.1040 + if (providers.length < 2 && menu.id != "viewSidebarMenu") { 1.1041 + providerMenuSep.hidden = true; 1.1042 + return; 1.1043 + } 1.1044 + let topSep = providerMenuSep.previousSibling; 1.1045 + for (let provider of providers) { 1.1046 + let menuitem = document.createElement("menuitem"); 1.1047 + menuitem.className = "menuitem-iconic social-provider-menuitem"; 1.1048 + menuitem.setAttribute("image", provider.iconURL); 1.1049 + menuitem.setAttribute("label", provider.name); 1.1050 + menuitem.setAttribute("origin", provider.origin); 1.1051 + if (this.opened && provider == this.provider) { 1.1052 + menuitem.setAttribute("checked", "true"); 1.1053 + menuitem.setAttribute("oncommand", "SocialSidebar.hide();"); 1.1054 + } else { 1.1055 + menuitem.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));"); 1.1056 + } 1.1057 + menu.insertBefore(menuitem, providerMenuSep); 1.1058 + } 1.1059 + topSep.hidden = topSep.nextSibling == providerMenuSep; 1.1060 + providerMenuSep.hidden = !providerMenuSep.nextSibling; 1.1061 + } 1.1062 +} 1.1063 + 1.1064 +// this helper class is used by removable/customizable buttons to handle 1.1065 +// widget creation/destruction 1.1066 + 1.1067 +// When a provider is installed we show all their UI so the user will see the 1.1068 +// functionality of what they installed. The user can later customize the UI, 1.1069 +// moving buttons around or off the toolbar. 1.1070 +// 1.1071 +// On startup, we create the button widgets of any enabled provider. 1.1072 +// CustomizableUI handles placement and persistence of placement. 1.1073 +function ToolbarHelper(type, createButtonFn, listener) { 1.1074 + this._createButton = createButtonFn; 1.1075 + this._type = type; 1.1076 + 1.1077 + if (listener) { 1.1078 + CustomizableUI.addListener(listener); 1.1079 + // remove this listener on window close 1.1080 + window.addEventListener("unload", () => { 1.1081 + CustomizableUI.removeListener(listener); 1.1082 + }); 1.1083 + } 1.1084 +} 1.1085 + 1.1086 +ToolbarHelper.prototype = { 1.1087 + idFromOrigin: function(origin) { 1.1088 + // this id needs to pass the checks in CustomizableUI, so remove characters 1.1089 + // that wont pass. 1.1090 + return this._type + "-" + Services.io.newURI(origin, null, null).hostPort.replace(/[\.:]/g,'-'); 1.1091 + }, 1.1092 + 1.1093 + // should be called on disable of a provider 1.1094 + removeProviderButton: function(origin) { 1.1095 + CustomizableUI.destroyWidget(this.idFromOrigin(origin)); 1.1096 + }, 1.1097 + 1.1098 + clearPalette: function() { 1.1099 + [this.removeProviderButton(p.origin) for (p of Social.providers)]; 1.1100 + }, 1.1101 + 1.1102 + // should be called on enable of a provider 1.1103 + populatePalette: function() { 1.1104 + if (!Social.enabled) { 1.1105 + this.clearPalette(); 1.1106 + return; 1.1107 + } 1.1108 + 1.1109 + // create any buttons that do not exist yet if they have been persisted 1.1110 + // as a part of the UI (otherwise they belong in the palette). 1.1111 + for (let provider of Social.providers) { 1.1112 + let id = this.idFromOrigin(provider.origin); 1.1113 + this._createButton(id, provider); 1.1114 + } 1.1115 + } 1.1116 +} 1.1117 + 1.1118 +let SocialStatusWidgetListener = { 1.1119 + _getNodeOrigin: function(aWidgetId) { 1.1120 + // we rely on the button id being the same as the widget. 1.1121 + let node = document.getElementById(aWidgetId); 1.1122 + if (!node) 1.1123 + return null 1.1124 + if (!node.classList.contains("social-status-button")) 1.1125 + return null 1.1126 + return node.getAttribute("origin"); 1.1127 + }, 1.1128 + onWidgetAdded: function(aWidgetId, aArea, aPosition) { 1.1129 + let origin = this._getNodeOrigin(aWidgetId); 1.1130 + if (origin) 1.1131 + SocialStatus.updateButton(origin); 1.1132 + }, 1.1133 + onWidgetRemoved: function(aWidgetId, aPrevArea) { 1.1134 + let origin = this._getNodeOrigin(aWidgetId); 1.1135 + if (!origin) 1.1136 + return; 1.1137 + // When a widget is demoted to the palette ('removed'), it's visual 1.1138 + // style should change. 1.1139 + SocialStatus.updateButton(origin); 1.1140 + SocialStatus._removeFrame(origin); 1.1141 + } 1.1142 +} 1.1143 + 1.1144 +SocialStatus = { 1.1145 + populateToolbarPalette: function() { 1.1146 + this._toolbarHelper.populatePalette(); 1.1147 + 1.1148 + for (let provider of Social.providers) 1.1149 + this.updateButton(provider.origin); 1.1150 + }, 1.1151 + 1.1152 + removeProvider: function(origin) { 1.1153 + this._removeFrame(origin); 1.1154 + this._toolbarHelper.removeProviderButton(origin); 1.1155 + }, 1.1156 + 1.1157 + reloadProvider: function(origin) { 1.1158 + let button = document.getElementById(this._toolbarHelper.idFromOrigin(origin)); 1.1159 + if (button && button.getAttribute("open") == "true") 1.1160 + document.getElementById("social-notification-panel").hidePopup(); 1.1161 + this._removeFrame(origin); 1.1162 + }, 1.1163 + 1.1164 + _removeFrame: function(origin) { 1.1165 + let notificationFrameId = "social-status-" + origin; 1.1166 + let frame = document.getElementById(notificationFrameId); 1.1167 + if (frame) { 1.1168 + SharedFrame.forgetGroup(frame.id); 1.1169 + frame.parentNode.removeChild(frame); 1.1170 + } 1.1171 + }, 1.1172 + 1.1173 + get _toolbarHelper() { 1.1174 + delete this._toolbarHelper; 1.1175 + this._toolbarHelper = new ToolbarHelper("social-status-button", 1.1176 + CreateSocialStatusWidget, 1.1177 + SocialStatusWidgetListener); 1.1178 + return this._toolbarHelper; 1.1179 + }, 1.1180 + 1.1181 + get _dynamicResizer() { 1.1182 + delete this._dynamicResizer; 1.1183 + this._dynamicResizer = new DynamicResizeWatcher(); 1.1184 + return this._dynamicResizer; 1.1185 + }, 1.1186 + 1.1187 + // status panels are one-per button per-process, we swap the docshells between 1.1188 + // windows when necessary 1.1189 + _attachNotificatonPanel: function(aParent, aButton, provider) { 1.1190 + aParent.hidden = !SocialUI.enabled; 1.1191 + let notificationFrameId = "social-status-" + provider.origin; 1.1192 + let frame = document.getElementById(notificationFrameId); 1.1193 + 1.1194 + // If the button was customized to a new location, we we'll destroy the 1.1195 + // iframe and start fresh. 1.1196 + if (frame && frame.parentNode != aParent) { 1.1197 + SharedFrame.forgetGroup(frame.id); 1.1198 + frame.parentNode.removeChild(frame); 1.1199 + frame = null; 1.1200 + } 1.1201 + 1.1202 + if (!frame) { 1.1203 + frame = SharedFrame.createFrame( 1.1204 + notificationFrameId, /* frame name */ 1.1205 + aParent, /* parent */ 1.1206 + { 1.1207 + "type": "content", 1.1208 + "mozbrowser": "true", 1.1209 + "class": "social-panel-frame", 1.1210 + "id": notificationFrameId, 1.1211 + "tooltip": "aHTMLTooltip", 1.1212 + "context": "contentAreaContextMenu", 1.1213 + "flex": "1", 1.1214 + 1.1215 + // work around bug 793057 - by making the panel roughly the final size 1.1216 + // we are more likely to have the anchor in the correct position. 1.1217 + "style": "width: " + PANEL_MIN_WIDTH + "px;", 1.1218 + 1.1219 + "origin": provider.origin, 1.1220 + "src": provider.statusURL 1.1221 + } 1.1222 + ); 1.1223 + 1.1224 + if (frame.socialErrorListener) 1.1225 + frame.socialErrorListener.remove(); 1.1226 + if (frame.docShell) { 1.1227 + frame.docShell.isActive = false; 1.1228 + Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this)); 1.1229 + } 1.1230 + } else { 1.1231 + frame.setAttribute("origin", provider.origin); 1.1232 + SharedFrame.updateURL(notificationFrameId, provider.statusURL); 1.1233 + } 1.1234 + aButton.setAttribute("notificationFrameId", notificationFrameId); 1.1235 + }, 1.1236 + 1.1237 + updateButton: function(origin) { 1.1238 + let id = this._toolbarHelper.idFromOrigin(origin); 1.1239 + let widget = CustomizableUI.getWidget(id); 1.1240 + if (!widget) 1.1241 + return; 1.1242 + let button = widget.forWindow(window).node; 1.1243 + if (button) { 1.1244 + // we only grab the first notification, ignore all others 1.1245 + let place = CustomizableUI.getPlaceForItem(button); 1.1246 + let provider = Social._getProviderFromOrigin(origin); 1.1247 + let icons = provider.ambientNotificationIcons; 1.1248 + let iconNames = Object.keys(icons); 1.1249 + let notif = icons[iconNames[0]]; 1.1250 + 1.1251 + // The image and tooltip need to be updated for both 1.1252 + // ambient notification and profile changes. 1.1253 + let iconURL = provider.icon32URL || provider.iconURL; 1.1254 + let tooltiptext; 1.1255 + if (!notif || place == "palette") { 1.1256 + button.style.listStyleImage = "url(" + iconURL + ")"; 1.1257 + button.setAttribute("badge", ""); 1.1258 + button.setAttribute("aria-label", ""); 1.1259 + button.setAttribute("tooltiptext", provider.name); 1.1260 + return; 1.1261 + } 1.1262 + button.style.listStyleImage = "url(" + (notif.iconURL || iconURL) + ")"; 1.1263 + button.setAttribute("tooltiptext", notif.label || provider.name); 1.1264 + 1.1265 + let badge = notif.counter || ""; 1.1266 + button.setAttribute("badge", badge); 1.1267 + let ariaLabel = notif.label; 1.1268 + // if there is a badge value, we must use a localizable string to insert it. 1.1269 + if (badge) 1.1270 + ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText", 1.1271 + [ariaLabel, badge]); 1.1272 + button.setAttribute("aria-label", ariaLabel); 1.1273 + } 1.1274 + }, 1.1275 + 1.1276 + showPopup: function(aToolbarButton) { 1.1277 + // attach our notification panel if necessary 1.1278 + let origin = aToolbarButton.getAttribute("origin"); 1.1279 + let provider = Social._getProviderFromOrigin(origin); 1.1280 + 1.1281 + // if we're a slice in the hamburger, use that panel instead 1.1282 + let widgetGroup = CustomizableUI.getWidget(aToolbarButton.getAttribute("id")); 1.1283 + let widget = widgetGroup.forWindow(window); 1.1284 + let panel, showingEvent, hidingEvent; 1.1285 + let inMenuPanel = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL; 1.1286 + if (inMenuPanel) { 1.1287 + panel = document.getElementById("PanelUI-socialapi"); 1.1288 + this._attachNotificatonPanel(panel, aToolbarButton, provider); 1.1289 + widget.node.setAttribute("closemenu", "none"); 1.1290 + showingEvent = "ViewShowing"; 1.1291 + hidingEvent = "ViewHiding"; 1.1292 + } else { 1.1293 + panel = document.getElementById("social-notification-panel"); 1.1294 + this._attachNotificatonPanel(panel, aToolbarButton, provider); 1.1295 + showingEvent = "popupshown"; 1.1296 + hidingEvent = "popuphidden"; 1.1297 + } 1.1298 + let notificationFrameId = aToolbarButton.getAttribute("notificationFrameId"); 1.1299 + let notificationFrame = document.getElementById(notificationFrameId); 1.1300 + 1.1301 + let wasAlive = SharedFrame.isGroupAlive(notificationFrameId); 1.1302 + SharedFrame.setOwner(notificationFrameId, notificationFrame); 1.1303 + 1.1304 + // Clear dimensions on all browsers so the panel size will 1.1305 + // only use the selected browser. 1.1306 + let frameIter = panel.firstElementChild; 1.1307 + while (frameIter) { 1.1308 + frameIter.collapsed = (frameIter != notificationFrame); 1.1309 + frameIter = frameIter.nextElementSibling; 1.1310 + } 1.1311 + 1.1312 + function dispatchPanelEvent(name) { 1.1313 + let evt = notificationFrame.contentDocument.createEvent("CustomEvent"); 1.1314 + evt.initCustomEvent(name, true, true, {}); 1.1315 + notificationFrame.contentDocument.documentElement.dispatchEvent(evt); 1.1316 + } 1.1317 + 1.1318 + // we only use a dynamic resizer when we're located the toolbar. 1.1319 + let dynamicResizer = inMenuPanel ? null : this._dynamicResizer; 1.1320 + panel.addEventListener(hidingEvent, function onpopuphiding() { 1.1321 + panel.removeEventListener(hidingEvent, onpopuphiding); 1.1322 + aToolbarButton.removeAttribute("open"); 1.1323 + if (dynamicResizer) 1.1324 + dynamicResizer.stop(); 1.1325 + notificationFrame.docShell.isActive = false; 1.1326 + dispatchPanelEvent("socialFrameHide"); 1.1327 + }); 1.1328 + 1.1329 + panel.addEventListener(showingEvent, function onpopupshown() { 1.1330 + panel.removeEventListener(showingEvent, onpopupshown); 1.1331 + // This attribute is needed on both the button and the 1.1332 + // containing toolbaritem since the buttons on OS X have 1.1333 + // moz-appearance:none, while their container gets 1.1334 + // moz-appearance:toolbarbutton due to the way that toolbar buttons 1.1335 + // get combined on OS X. 1.1336 + let initFrameShow = () => { 1.1337 + notificationFrame.docShell.isActive = true; 1.1338 + notificationFrame.docShell.isAppTab = true; 1.1339 + if (dynamicResizer) 1.1340 + dynamicResizer.start(panel, notificationFrame); 1.1341 + dispatchPanelEvent("socialFrameShow"); 1.1342 + }; 1.1343 + if (!inMenuPanel) 1.1344 + aToolbarButton.setAttribute("open", "true"); 1.1345 + if (notificationFrame.contentDocument && 1.1346 + notificationFrame.contentDocument.readyState == "complete" && wasAlive) { 1.1347 + initFrameShow(); 1.1348 + } else { 1.1349 + // first time load, wait for load and dispatch after load 1.1350 + notificationFrame.addEventListener("load", function panelBrowserOnload(e) { 1.1351 + notificationFrame.removeEventListener("load", panelBrowserOnload, true); 1.1352 + initFrameShow(); 1.1353 + }, true); 1.1354 + } 1.1355 + }); 1.1356 + 1.1357 + if (inMenuPanel) { 1.1358 + PanelUI.showSubView("PanelUI-socialapi", widget.node, 1.1359 + CustomizableUI.AREA_PANEL); 1.1360 + } else { 1.1361 + let anchor = document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-badge-container"); 1.1362 + // Bug 849216 - open the popup in a setTimeout so we avoid the auto-rollup 1.1363 + // handling from preventing it being opened in some cases. 1.1364 + setTimeout(function() { 1.1365 + panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); 1.1366 + }, 0); 1.1367 + } 1.1368 + }, 1.1369 + 1.1370 + setPanelErrorMessage: function(aNotificationFrame) { 1.1371 + if (!aNotificationFrame) 1.1372 + return; 1.1373 + 1.1374 + let src = aNotificationFrame.getAttribute("src"); 1.1375 + aNotificationFrame.removeAttribute("src"); 1.1376 + let origin = aNotificationFrame.getAttribute("origin"); 1.1377 + aNotificationFrame.webNavigation.loadURI("about:socialerror?mode=tryAgainOnly&url=" + 1.1378 + encodeURIComponent(src) + "&origin=" + 1.1379 + encodeURIComponent(origin), 1.1380 + null, null, null, null); 1.1381 + let panel = aNotificationFrame.parentNode; 1.1382 + sizeSocialPanelToContent(panel, aNotificationFrame); 1.1383 + }, 1.1384 + 1.1385 +}; 1.1386 + 1.1387 + 1.1388 +/** 1.1389 + * SocialMarks 1.1390 + * 1.1391 + * Handles updates to toolbox and signals all buttons to update when necessary. 1.1392 + */ 1.1393 +SocialMarks = { 1.1394 + update: function() { 1.1395 + // signal each button to update itself 1.1396 + let currentButtons = document.querySelectorAll('toolbarbutton[type="socialmark"]'); 1.1397 + for (let elt of currentButtons) 1.1398 + elt.update(); 1.1399 + }, 1.1400 + 1.1401 + updatePanelButtons: function() { 1.1402 + // querySelectorAll does not work on the menu panel the panel, so we have to 1.1403 + // do this the hard way. 1.1404 + let providers = SocialMarks.getProviders(); 1.1405 + let panel = document.getElementById("PanelUI-popup"); 1.1406 + for (let p of providers) { 1.1407 + let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin); 1.1408 + let widget = CustomizableUI.getWidget(widgetId); 1.1409 + if (!widget) 1.1410 + continue; 1.1411 + let node = widget.forWindow(window).node; 1.1412 + if (node) 1.1413 + node.update(); 1.1414 + } 1.1415 + }, 1.1416 + 1.1417 + getProviders: function() { 1.1418 + // only rely on providers that the user has placed in the UI somewhere. This 1.1419 + // also means that populateToolbarPalette must be called prior to using this 1.1420 + // method, otherwise you get a big fat zero. For our use case with context 1.1421 + // menu's, this is ok. 1.1422 + let tbh = this._toolbarHelper; 1.1423 + return [p for (p of Social.providers) if (p.markURL && 1.1424 + document.getElementById(tbh.idFromOrigin(p.origin)))]; 1.1425 + }, 1.1426 + 1.1427 + populateContextMenu: function() { 1.1428 + // only show a selection if enabled and there is more than one 1.1429 + let providers = this.getProviders(); 1.1430 + 1.1431 + // remove all previous entries by class 1.1432 + let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))]; 1.1433 + [m.parentNode.removeChild(m) for (m of menus)]; 1.1434 + 1.1435 + let contextMenus = [ 1.1436 + { 1.1437 + type: "link", 1.1438 + id: "context-marklinkMenu", 1.1439 + label: "social.marklinkMenu.label" 1.1440 + }, 1.1441 + { 1.1442 + type: "page", 1.1443 + id: "context-markpageMenu", 1.1444 + label: "social.markpageMenu.label" 1.1445 + } 1.1446 + ]; 1.1447 + for (let cfg of contextMenus) { 1.1448 + this._populateContextPopup(cfg, providers); 1.1449 + } 1.1450 + this.updatePanelButtons(); 1.1451 + }, 1.1452 + 1.1453 + MENU_LIMIT: 3, // adjustable for testing 1.1454 + _populateContextPopup: function(menuInfo, providers) { 1.1455 + let menu = document.getElementById(menuInfo.id); 1.1456 + let popup = menu.firstChild; 1.1457 + for (let provider of providers) { 1.1458 + // We show up to MENU_LIMIT providers as single menuitems's at the top 1.1459 + // level of the context menu, if we have more than that, dump them *all* 1.1460 + // into the menu popup. 1.1461 + let mi = document.createElement("menuitem"); 1.1462 + mi.setAttribute("oncommand", "gContextMenu.markLink(this.getAttribute('origin'));"); 1.1463 + mi.setAttribute("origin", provider.origin); 1.1464 + mi.setAttribute("image", provider.iconURL); 1.1465 + if (providers.length <= this.MENU_LIMIT) { 1.1466 + // an extra class to make enable/disable easy 1.1467 + mi.setAttribute("class", "menuitem-iconic context-socialmarks context-mark"+menuInfo.type); 1.1468 + let menuLabel = gNavigatorBundle.getFormattedString(menuInfo.label, [provider.name]); 1.1469 + mi.setAttribute("label", menuLabel); 1.1470 + menu.parentNode.insertBefore(mi, menu); 1.1471 + } else { 1.1472 + mi.setAttribute("class", "menuitem-iconic context-socialmarks"); 1.1473 + mi.setAttribute("label", provider.name); 1.1474 + popup.appendChild(mi); 1.1475 + } 1.1476 + } 1.1477 + }, 1.1478 + 1.1479 + populateToolbarPalette: function() { 1.1480 + this._toolbarHelper.populatePalette(); 1.1481 + this.populateContextMenu(); 1.1482 + }, 1.1483 + 1.1484 + removeProvider: function(origin) { 1.1485 + this._toolbarHelper.removeProviderButton(origin); 1.1486 + }, 1.1487 + 1.1488 + get _toolbarHelper() { 1.1489 + delete this._toolbarHelper; 1.1490 + this._toolbarHelper = new ToolbarHelper("social-mark-button", CreateSocialMarkWidget); 1.1491 + return this._toolbarHelper; 1.1492 + }, 1.1493 + 1.1494 + markLink: function(aOrigin, aUrl) { 1.1495 + // find the button for this provider, and open it 1.1496 + let id = this._toolbarHelper.idFromOrigin(aOrigin); 1.1497 + document.getElementById(id).markLink(aUrl); 1.1498 + } 1.1499 +}; 1.1500 + 1.1501 +})();