michael@0: // This Source Code Form is subject to the terms of the Mozilla Public michael@0: // License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: // file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: // the "exported" symbols michael@0: let SocialUI, michael@0: SocialChatBar, michael@0: SocialFlyout, michael@0: SocialMarks, michael@0: SocialShare, michael@0: SocialSidebar, michael@0: SocialStatus; michael@0: michael@0: (function() { michael@0: michael@0: // The minimum sizes for the auto-resize panel code. michael@0: const PANEL_MIN_HEIGHT = 100; michael@0: const PANEL_MIN_WIDTH = 330; michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SharedFrame", michael@0: "resource:///modules/SharedFrame.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() { michael@0: let tmp = {}; michael@0: Cu.import("resource:///modules/Social.jsm", tmp); michael@0: return tmp.OpenGraphBuilder; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() { michael@0: let tmp = {}; michael@0: Cu.import("resource:///modules/Social.jsm", tmp); michael@0: return tmp.DynamicResizeWatcher; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "sizeSocialPanelToContent", function() { michael@0: let tmp = {}; michael@0: Cu.import("resource:///modules/Social.jsm", tmp); michael@0: return tmp.sizeSocialPanelToContent; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "CreateSocialStatusWidget", function() { michael@0: let tmp = {}; michael@0: Cu.import("resource:///modules/Social.jsm", tmp); michael@0: return tmp.CreateSocialStatusWidget; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "CreateSocialMarkWidget", function() { michael@0: let tmp = {}; michael@0: Cu.import("resource:///modules/Social.jsm", tmp); michael@0: return tmp.CreateSocialMarkWidget; michael@0: }); michael@0: michael@0: SocialUI = { michael@0: _initialized: false, michael@0: michael@0: // Called on delayed startup to initialize the UI michael@0: init: function SocialUI_init() { michael@0: if (this._initialized) { michael@0: return; michael@0: } michael@0: michael@0: Services.obs.addObserver(this, "social:ambient-notification-changed", false); michael@0: Services.obs.addObserver(this, "social:profile-changed", false); michael@0: Services.obs.addObserver(this, "social:frameworker-error", false); michael@0: Services.obs.addObserver(this, "social:providers-changed", false); michael@0: Services.obs.addObserver(this, "social:provider-reload", false); michael@0: Services.obs.addObserver(this, "social:provider-enabled", false); michael@0: Services.obs.addObserver(this, "social:provider-disabled", false); michael@0: michael@0: Services.prefs.addObserver("social.toast-notifications.enabled", this, false); michael@0: michael@0: gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true); michael@0: document.getElementById("PanelUI-popup").addEventListener("popupshown", SocialMarks.updatePanelButtons, true); michael@0: michael@0: // menupopups that list social providers. we only populate them when shown, michael@0: // and if it has not been done already. michael@0: document.getElementById("viewSidebarMenu").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); michael@0: document.getElementById("social-statusarea-popup").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); michael@0: michael@0: Social.init().then((update) => { michael@0: if (update) michael@0: this._providersChanged(); michael@0: // handle SessionStore for the sidebar state michael@0: SocialSidebar.restoreWindowState(); michael@0: }); michael@0: michael@0: this._initialized = true; michael@0: }, michael@0: michael@0: // Called on window unload michael@0: uninit: function SocialUI_uninit() { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: SocialSidebar.saveWindowState(); michael@0: michael@0: Services.obs.removeObserver(this, "social:ambient-notification-changed"); michael@0: Services.obs.removeObserver(this, "social:profile-changed"); michael@0: Services.obs.removeObserver(this, "social:frameworker-error"); michael@0: Services.obs.removeObserver(this, "social:providers-changed"); michael@0: Services.obs.removeObserver(this, "social:provider-reload"); michael@0: Services.obs.removeObserver(this, "social:provider-enabled"); michael@0: Services.obs.removeObserver(this, "social:provider-disabled"); michael@0: michael@0: Services.prefs.removeObserver("social.toast-notifications.enabled", this); michael@0: michael@0: document.getElementById("PanelUI-popup").removeEventListener("popupshown", SocialMarks.updatePanelButtons, true); michael@0: document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); michael@0: document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); michael@0: michael@0: this._initialized = false; michael@0: }, michael@0: michael@0: observe: function SocialUI_observe(subject, topic, data) { michael@0: // Exceptions here sometimes don't get reported properly, report them michael@0: // manually :( michael@0: try { michael@0: switch (topic) { michael@0: case "social:provider-enabled": michael@0: SocialMarks.populateToolbarPalette(); michael@0: SocialStatus.populateToolbarPalette(); michael@0: break; michael@0: case "social:provider-disabled": michael@0: SocialMarks.removeProvider(data); michael@0: SocialStatus.removeProvider(data); michael@0: SocialSidebar.disableProvider(data); michael@0: break; michael@0: case "social:provider-reload": michael@0: SocialStatus.reloadProvider(data); michael@0: // if the reloaded provider is our current provider, fall through michael@0: // to social:providers-changed so the ui will be reset michael@0: if (!SocialSidebar.provider || SocialSidebar.provider.origin != data) michael@0: return; michael@0: // currently only the sidebar and flyout have a selected provider. michael@0: // sidebar provider has changed (possibly to null), ensure the content michael@0: // is unloaded and the frames are reset, they will be loaded in michael@0: // providers-changed below if necessary. michael@0: SocialSidebar.unloadSidebar(); michael@0: SocialFlyout.unload(); michael@0: // fall through to providers-changed to ensure the reloaded provider michael@0: // is correctly reflected in any UI and the multi-provider menu michael@0: case "social:providers-changed": michael@0: this._providersChanged(); michael@0: break; michael@0: michael@0: // Provider-specific notifications michael@0: case "social:ambient-notification-changed": michael@0: SocialStatus.updateButton(data); michael@0: break; michael@0: case "social:profile-changed": michael@0: // make sure anything that happens here only affects the provider for michael@0: // which the profile is changing, and that anything we call actually michael@0: // needs to change based on profile data. michael@0: SocialStatus.updateButton(data); michael@0: break; michael@0: case "social:frameworker-error": michael@0: if (this.enabled && SocialSidebar.provider && SocialSidebar.provider.origin == data) { michael@0: SocialSidebar.setSidebarErrorMessage(); michael@0: } michael@0: break; michael@0: michael@0: case "nsPref:changed": michael@0: if (data == "social.toast-notifications.enabled") { michael@0: SocialSidebar.updateToggleNotifications(); michael@0: } michael@0: break; michael@0: } michael@0: } catch (e) { michael@0: Components.utils.reportError(e + "\n" + e.stack); michael@0: throw e; michael@0: } michael@0: }, michael@0: michael@0: _providersChanged: function() { michael@0: SocialSidebar.clearProviderMenus(); michael@0: SocialSidebar.update(); michael@0: SocialChatBar.update(); michael@0: SocialShare.populateProviderMenu(); michael@0: SocialStatus.populateToolbarPalette(); michael@0: SocialMarks.populateToolbarPalette(); michael@0: SocialShare.update(); michael@0: }, michael@0: michael@0: // This handles "ActivateSocialFeature" events fired against content documents michael@0: // in this window. michael@0: _activationEventHandler: function SocialUI_activationHandler(e) { michael@0: let targetDoc; michael@0: let node; michael@0: if (e.target instanceof HTMLDocument) { michael@0: // version 0 support michael@0: targetDoc = e.target; michael@0: node = targetDoc.documentElement michael@0: } else { michael@0: targetDoc = e.target.ownerDocument; michael@0: node = e.target; michael@0: } michael@0: if (!(targetDoc instanceof HTMLDocument)) michael@0: return; michael@0: michael@0: // Ignore events fired in background tabs or iframes michael@0: if (targetDoc.defaultView != content) michael@0: return; michael@0: michael@0: // If we are in PB mode, we silently do nothing (bug 829404 exists to michael@0: // do something sensible here...) michael@0: if (PrivateBrowsingUtils.isWindowPrivate(window)) michael@0: return; michael@0: michael@0: // If the last event was received < 1s ago, ignore this one michael@0: let now = Date.now(); michael@0: if (now - Social.lastEventReceived < 1000) michael@0: return; michael@0: Social.lastEventReceived = now; michael@0: michael@0: // We only want to activate if it is as a result of user input. michael@0: let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: if (!dwu.isHandlingUserInput) { michael@0: Cu.reportError("attempt to activate provider without user input from " + targetDoc.nodePrincipal.origin); michael@0: return; michael@0: } michael@0: michael@0: let data = node.getAttribute("data-service"); michael@0: if (data) { michael@0: try { michael@0: data = JSON.parse(data); michael@0: } catch(e) { michael@0: Cu.reportError("Social Service manifest parse error: "+e); michael@0: return; michael@0: } michael@0: } michael@0: Social.installProvider(targetDoc, data, function(manifest) { michael@0: Social.activateFromOrigin(manifest.origin, function(provider) { michael@0: if (provider.sidebarURL) { michael@0: SocialSidebar.show(provider.origin); michael@0: } michael@0: if (provider.postActivationURL) { michael@0: openUILinkIn(provider.postActivationURL, "tab"); michael@0: } michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: showLearnMore: function() { michael@0: let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api"; michael@0: openUILinkIn(url, "tab"); michael@0: }, michael@0: michael@0: closeSocialPanelForLinkTraversal: function (target, linkNode) { michael@0: // No need to close the panel if this traversal was not retargeted michael@0: if (target == "" || target == "_self") michael@0: return; michael@0: michael@0: // Check to see whether this link traversal was in a social panel michael@0: let win = linkNode.ownerDocument.defaultView; michael@0: let container = win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell) michael@0: .chromeEventHandler; michael@0: let containerParent = container.parentNode; michael@0: if (containerParent.classList.contains("social-panel") && michael@0: containerParent instanceof Ci.nsIDOMXULPopupElement) { michael@0: // allow the link traversal to finish before closing the panel michael@0: setTimeout(() => { michael@0: containerParent.hidePopup(); michael@0: }, 0); michael@0: } michael@0: }, michael@0: michael@0: get _chromeless() { michael@0: // Is this a popup window that doesn't want chrome shown? michael@0: let docElem = document.documentElement; michael@0: // extrachrome is not restored during session restore, so we need michael@0: // to check for the toolbar as well. michael@0: let chromeless = docElem.getAttribute("chromehidden").contains("extrachrome") || michael@0: docElem.getAttribute('chromehidden').contains("toolbar"); michael@0: // This property is "fixed" for a window, so avoid doing the check above michael@0: // multiple times... michael@0: delete this._chromeless; michael@0: this._chromeless = chromeless; michael@0: return chromeless; michael@0: }, michael@0: michael@0: get enabled() { michael@0: // Returns whether social is enabled *for this window*. michael@0: if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window)) michael@0: return false; michael@0: return Social.providers.length > 0; michael@0: }, michael@0: michael@0: // called on tab/urlbar/location changes and after customization. Update michael@0: // anything that is tab specific. michael@0: updateState: function() { michael@0: if (!this.enabled) michael@0: return; michael@0: SocialMarks.update(); michael@0: SocialShare.update(); michael@0: } michael@0: } michael@0: michael@0: SocialChatBar = { michael@0: get chatbar() { michael@0: return document.getElementById("pinnedchats"); michael@0: }, michael@0: // Whether the chatbar is available for this window. Note that in full-screen michael@0: // mode chats are available, but not shown. michael@0: get isAvailable() { michael@0: return SocialUI.enabled; michael@0: }, michael@0: // Does this chatbar have any chats (whether minimized, collapsed or normal) michael@0: get hasChats() { michael@0: return !!this.chatbar.firstElementChild; michael@0: }, michael@0: openChat: function(aProvider, aURL, aCallback, aMode) { michael@0: this.update(); michael@0: if (!this.isAvailable) michael@0: return false; michael@0: this.chatbar.openChat(aProvider, aURL, aCallback, aMode); michael@0: // We only want to focus the chat if it is as a result of user input. michael@0: let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: if (dwu.isHandlingUserInput) michael@0: this.chatbar.focus(); michael@0: return true; michael@0: }, michael@0: update: function() { michael@0: let command = document.getElementById("Social:FocusChat"); michael@0: if (!this.isAvailable) { michael@0: this.chatbar.hidden = command.hidden = true; michael@0: } else { michael@0: this.chatbar.hidden = command.hidden = false; michael@0: } michael@0: command.setAttribute("disabled", command.hidden ? "true" : "false"); michael@0: }, michael@0: focus: function SocialChatBar_focus() { michael@0: this.chatbar.focus(); michael@0: } michael@0: } michael@0: michael@0: SocialFlyout = { michael@0: get panel() { michael@0: return document.getElementById("social-flyout-panel"); michael@0: }, michael@0: michael@0: get iframe() { michael@0: if (!this.panel.firstChild) michael@0: this._createFrame(); michael@0: return this.panel.firstChild; michael@0: }, michael@0: michael@0: dispatchPanelEvent: function(name) { michael@0: let doc = this.iframe.contentDocument; michael@0: let evt = doc.createEvent("CustomEvent"); michael@0: evt.initCustomEvent(name, true, true, {}); michael@0: doc.documentElement.dispatchEvent(evt); michael@0: }, michael@0: michael@0: _createFrame: function() { michael@0: let panel = this.panel; michael@0: if (!SocialUI.enabled || panel.firstChild) michael@0: return; michael@0: // create and initialize the panel for this window michael@0: let iframe = document.createElement("iframe"); michael@0: iframe.setAttribute("type", "content"); michael@0: iframe.setAttribute("class", "social-panel-frame"); michael@0: iframe.setAttribute("flex", "1"); michael@0: iframe.setAttribute("tooltip", "aHTMLTooltip"); michael@0: iframe.setAttribute("origin", SocialSidebar.provider.origin); michael@0: panel.appendChild(iframe); michael@0: }, michael@0: michael@0: setFlyoutErrorMessage: function SF_setFlyoutErrorMessage() { michael@0: this.iframe.removeAttribute("src"); michael@0: this.iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + michael@0: encodeURIComponent(this.iframe.getAttribute("origin")), michael@0: null, null, null, null); michael@0: sizeSocialPanelToContent(this.panel, this.iframe); michael@0: }, michael@0: michael@0: unload: function() { michael@0: let panel = this.panel; michael@0: panel.hidePopup(); michael@0: if (!panel.firstChild) michael@0: return michael@0: let iframe = panel.firstChild; michael@0: if (iframe.socialErrorListener) michael@0: iframe.socialErrorListener.remove(); michael@0: panel.removeChild(iframe); michael@0: }, michael@0: michael@0: onShown: function(aEvent) { michael@0: let panel = this.panel; michael@0: let iframe = this.iframe; michael@0: this._dynamicResizer = new DynamicResizeWatcher(); michael@0: iframe.docShell.isActive = true; michael@0: iframe.docShell.isAppTab = true; michael@0: if (iframe.contentDocument.readyState == "complete") { michael@0: this._dynamicResizer.start(panel, iframe); michael@0: this.dispatchPanelEvent("socialFrameShow"); michael@0: } else { michael@0: // first time load, wait for load and dispatch after load michael@0: iframe.addEventListener("load", function panelBrowserOnload(e) { michael@0: iframe.removeEventListener("load", panelBrowserOnload, true); michael@0: setTimeout(function() { michael@0: if (SocialFlyout._dynamicResizer) { // may go null if hidden quickly michael@0: SocialFlyout._dynamicResizer.start(panel, iframe); michael@0: SocialFlyout.dispatchPanelEvent("socialFrameShow"); michael@0: } michael@0: }, 0); michael@0: }, true); michael@0: } michael@0: }, michael@0: michael@0: onHidden: function(aEvent) { michael@0: this._dynamicResizer.stop(); michael@0: this._dynamicResizer = null; michael@0: this.iframe.docShell.isActive = false; michael@0: this.dispatchPanelEvent("socialFrameHide"); michael@0: }, michael@0: michael@0: load: function(aURL, cb) { michael@0: if (!SocialSidebar.provider) michael@0: return; michael@0: michael@0: this.panel.hidden = false; michael@0: let iframe = this.iframe; michael@0: // same url with only ref difference does not cause a new load, so we michael@0: // want to go right to the callback michael@0: let src = iframe.contentDocument && iframe.contentDocument.documentURIObject; michael@0: if (!src || !src.equalsExceptRef(Services.io.newURI(aURL, null, null))) { michael@0: iframe.addEventListener("load", function documentLoaded() { michael@0: iframe.removeEventListener("load", documentLoaded, true); michael@0: cb(); michael@0: }, true); michael@0: // Force a layout flush by calling .clientTop so michael@0: // that the docShell of this frame is created michael@0: iframe.clientTop; michael@0: Social.setErrorListener(iframe, SocialFlyout.setFlyoutErrorMessage.bind(SocialFlyout)) michael@0: iframe.setAttribute("src", aURL); michael@0: } else { michael@0: // we still need to set the src to trigger the contents hashchange event michael@0: // for ref changes michael@0: iframe.setAttribute("src", aURL); michael@0: cb(); michael@0: } michael@0: }, michael@0: michael@0: open: function(aURL, yOffset, aCallback) { michael@0: // Hide any other social panels that may be open. michael@0: document.getElementById("social-notification-panel").hidePopup(); michael@0: michael@0: if (!SocialUI.enabled) michael@0: return; michael@0: let panel = this.panel; michael@0: let iframe = this.iframe; michael@0: michael@0: this.load(aURL, function() { michael@0: sizeSocialPanelToContent(panel, iframe); michael@0: let anchor = document.getElementById("social-sidebar-browser"); michael@0: if (panel.state == "open") { michael@0: panel.moveToAnchor(anchor, "start_before", 0, yOffset, false); michael@0: } else { michael@0: panel.openPopup(anchor, "start_before", 0, yOffset, false, false); michael@0: } michael@0: if (aCallback) { michael@0: try { michael@0: aCallback(iframe.contentWindow); michael@0: } catch(e) { michael@0: Cu.reportError(e); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: michael@0: SocialShare = { michael@0: get panel() { michael@0: return document.getElementById("social-share-panel"); michael@0: }, michael@0: michael@0: get iframe() { michael@0: // first element is our menu vbox. michael@0: if (this.panel.childElementCount == 1) michael@0: return null; michael@0: else michael@0: return this.panel.lastChild; michael@0: }, michael@0: michael@0: uninit: function () { michael@0: if (this.iframe) { michael@0: this.iframe.remove(); michael@0: } michael@0: }, michael@0: michael@0: _createFrame: function() { michael@0: let panel = this.panel; michael@0: if (!SocialUI.enabled || this.iframe) michael@0: return; michael@0: this.panel.hidden = false; michael@0: // create and initialize the panel for this window michael@0: let iframe = document.createElement("iframe"); michael@0: iframe.setAttribute("type", "content"); michael@0: iframe.setAttribute("class", "social-share-frame"); michael@0: iframe.setAttribute("context", "contentAreaContextMenu"); michael@0: iframe.setAttribute("tooltip", "aHTMLTooltip"); michael@0: iframe.setAttribute("flex", "1"); michael@0: panel.appendChild(iframe); michael@0: this.populateProviderMenu(); michael@0: }, michael@0: michael@0: getSelectedProvider: function() { michael@0: let provider; michael@0: let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin"); michael@0: if (lastProviderOrigin) { michael@0: provider = Social._getProviderFromOrigin(lastProviderOrigin); michael@0: } michael@0: // if they have a provider selected in the sidebar use that for the initial michael@0: // default in share michael@0: if (!provider) michael@0: provider = SocialSidebar.provider; michael@0: // if our provider has no shareURL, select the first one that does michael@0: if (!provider || !provider.shareURL) { michael@0: let providers = [p for (p of Social.providers) if (p.shareURL)]; michael@0: provider = providers.length > 0 && providers[0]; michael@0: } michael@0: return provider; michael@0: }, michael@0: michael@0: populateProviderMenu: function() { michael@0: if (!this.iframe) michael@0: return; michael@0: let providers = [p for (p of Social.providers) if (p.shareURL)]; michael@0: let hbox = document.getElementById("social-share-provider-buttons"); michael@0: // selectable providers are inserted before the provider-menu seperator, michael@0: // remove any menuitems in that area michael@0: while (hbox.firstChild) { michael@0: hbox.removeChild(hbox.firstChild); michael@0: } michael@0: // reset our share toolbar michael@0: // only show a selection if there is more than one michael@0: if (!SocialUI.enabled || providers.length < 2) { michael@0: this.panel.firstChild.hidden = true; michael@0: return; michael@0: } michael@0: let selectedProvider = this.getSelectedProvider(); michael@0: for (let provider of providers) { michael@0: let button = document.createElement("toolbarbutton"); michael@0: button.setAttribute("class", "toolbarbutton share-provider-button"); michael@0: button.setAttribute("type", "radio"); michael@0: button.setAttribute("group", "share-providers"); michael@0: button.setAttribute("image", provider.iconURL); michael@0: button.setAttribute("tooltiptext", provider.name); michael@0: button.setAttribute("origin", provider.origin); michael@0: button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin')); this.checked=true;"); michael@0: if (provider == selectedProvider) { michael@0: this.defaultButton = button; michael@0: } michael@0: hbox.appendChild(button); michael@0: } michael@0: if (!this.defaultButton) { michael@0: this.defaultButton = hbox.firstChild michael@0: } michael@0: this.defaultButton.setAttribute("checked", "true"); michael@0: this.panel.firstChild.hidden = false; michael@0: }, michael@0: michael@0: get shareButton() { michael@0: return document.getElementById("social-share-button"); michael@0: }, michael@0: michael@0: canSharePage: function(aURI) { michael@0: // we do not enable sharing from private sessions michael@0: if (PrivateBrowsingUtils.isWindowPrivate(window)) michael@0: return false; michael@0: michael@0: if (!aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https'))) michael@0: return false; michael@0: return true; michael@0: }, michael@0: michael@0: update: function() { michael@0: let shareButton = this.shareButton; michael@0: shareButton.hidden = !SocialUI.enabled || michael@0: [p for (p of Social.providers) if (p.shareURL)].length == 0; michael@0: shareButton.disabled = shareButton.hidden || !this.canSharePage(gBrowser.currentURI); michael@0: michael@0: // also update the relevent command's disabled state so the keyboard michael@0: // shortcut only works when available. michael@0: let cmd = document.getElementById("Social:SharePage"); michael@0: if (shareButton.disabled) michael@0: cmd.setAttribute("disabled", "true"); michael@0: else michael@0: cmd.removeAttribute("disabled"); michael@0: }, michael@0: michael@0: onShowing: function() { michael@0: this.shareButton.setAttribute("open", "true"); michael@0: }, michael@0: michael@0: onHidden: function() { michael@0: this.shareButton.removeAttribute("open"); michael@0: this.iframe.setAttribute("src", "data:text/plain;charset=utf8,"); michael@0: this.currentShare = null; michael@0: }, michael@0: michael@0: setErrorMessage: function() { michael@0: let iframe = this.iframe; michael@0: if (!iframe) michael@0: return; michael@0: michael@0: iframe.removeAttribute("src"); michael@0: iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + michael@0: encodeURIComponent(iframe.getAttribute("origin")), michael@0: null, null, null, null); michael@0: sizeSocialPanelToContent(this.panel, iframe); michael@0: }, michael@0: michael@0: sharePage: function(providerOrigin, graphData) { michael@0: // if providerOrigin is undefined, we use the last-used provider, or the michael@0: // current/default provider. The provider selection in the share panel michael@0: // will call sharePage with an origin for us to switch to. michael@0: this._createFrame(); michael@0: let iframe = this.iframe; michael@0: let provider; michael@0: if (providerOrigin) michael@0: provider = Social._getProviderFromOrigin(providerOrigin); michael@0: else michael@0: provider = this.getSelectedProvider(); michael@0: if (!provider || !provider.shareURL) michael@0: return; michael@0: michael@0: // graphData is an optional param that either defines the full set of data michael@0: // to be shared, or partial data about the current page. It is set by a call michael@0: // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST michael@0: // define at least url. If it is undefined, we're sharing the current url in michael@0: // the browser tab. michael@0: let sharedURI = graphData ? Services.io.newURI(graphData.url, null, null) : michael@0: gBrowser.currentURI; michael@0: if (!this.canSharePage(sharedURI)) michael@0: return; michael@0: michael@0: // the point of this action type is that we can use existing share michael@0: // endpoints (e.g. oexchange) that do not support additional michael@0: // socialapi functionality. One tweak is that we shoot an event michael@0: // containing the open graph data. michael@0: let pageData = graphData ? graphData : this.currentShare; michael@0: if (!pageData || sharedURI == gBrowser.currentURI) { michael@0: pageData = OpenGraphBuilder.getData(gBrowser); michael@0: if (graphData) { michael@0: // overwrite data retreived from page with data given to us as a param michael@0: for (let p in graphData) { michael@0: pageData[p] = graphData[p]; michael@0: } michael@0: } michael@0: } michael@0: this.currentShare = pageData; michael@0: michael@0: let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData); michael@0: michael@0: this._dynamicResizer = new DynamicResizeWatcher(); michael@0: // if we've already loaded this provider/page share endpoint, we don't want michael@0: // to add another load event listener. michael@0: let reload = true; michael@0: let endpointMatch = shareEndpoint == iframe.getAttribute("src"); michael@0: let docLoaded = iframe.contentDocument && iframe.contentDocument.readyState == "complete"; michael@0: if (endpointMatch && docLoaded) { michael@0: reload = shareEndpoint != iframe.contentDocument.location.spec; michael@0: } michael@0: if (!reload) { michael@0: this._dynamicResizer.start(this.panel, iframe); michael@0: iframe.docShell.isActive = true; michael@0: iframe.docShell.isAppTab = true; michael@0: let evt = iframe.contentDocument.createEvent("CustomEvent"); michael@0: evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData)); michael@0: iframe.contentDocument.documentElement.dispatchEvent(evt); michael@0: } else { michael@0: // first time load, wait for load and dispatch after load michael@0: iframe.addEventListener("load", function panelBrowserOnload(e) { michael@0: iframe.removeEventListener("load", panelBrowserOnload, true); michael@0: iframe.docShell.isActive = true; michael@0: iframe.docShell.isAppTab = true; michael@0: setTimeout(function() { michael@0: if (SocialShare._dynamicResizer) { // may go null if hidden quickly michael@0: SocialShare._dynamicResizer.start(iframe.parentNode, iframe); michael@0: } michael@0: }, 0); michael@0: let evt = iframe.contentDocument.createEvent("CustomEvent"); michael@0: evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData)); michael@0: iframe.contentDocument.documentElement.dispatchEvent(evt); michael@0: }, true); michael@0: } michael@0: // always ensure that origin belongs to the endpoint michael@0: let uri = Services.io.newURI(shareEndpoint, null, null); michael@0: iframe.setAttribute("origin", provider.origin); michael@0: iframe.setAttribute("src", shareEndpoint); michael@0: michael@0: let navBar = document.getElementById("nav-bar"); michael@0: let anchor = document.getAnonymousElementByAttribute(this.shareButton, "class", "toolbarbutton-icon"); michael@0: this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); michael@0: Social.setErrorListener(iframe, this.setErrorMessage.bind(this)); michael@0: } michael@0: }; michael@0: michael@0: SocialSidebar = { michael@0: // Whether the sidebar can be shown for this window. michael@0: get canShow() { michael@0: if (!SocialUI.enabled || document.mozFullScreen) michael@0: return false; michael@0: return Social.providers.some(p => p.sidebarURL); michael@0: }, michael@0: michael@0: // Whether the user has toggled the sidebar on (for windows where it can appear) michael@0: get opened() { michael@0: let broadcaster = document.getElementById("socialSidebarBroadcaster"); michael@0: return !broadcaster.hidden; michael@0: }, michael@0: michael@0: restoreWindowState: function() { michael@0: // Window state is used to allow different sidebar providers in each window. michael@0: // We also store the provider used in a pref as the default sidebar to michael@0: // maintain that state for users who do not restore window state. The michael@0: // existence of social.sidebar.provider means the sidebar is open with that michael@0: // provider. michael@0: this._initialized = true; michael@0: if (!this.canShow) michael@0: return; michael@0: michael@0: if (Services.prefs.prefHasUserValue("social.provider.current")) { michael@0: // "upgrade" when the first window opens if we have old prefs. We get the michael@0: // values from prefs this one time, window state will be saved when this michael@0: // window is closed. michael@0: let origin = Services.prefs.getCharPref("social.provider.current"); michael@0: Services.prefs.clearUserPref("social.provider.current"); michael@0: // social.sidebar.open default was true, but we only opened if there was michael@0: // a current provider michael@0: let opened = origin && true; michael@0: if (Services.prefs.prefHasUserValue("social.sidebar.open")) { michael@0: opened = origin && Services.prefs.getBoolPref("social.sidebar.open"); michael@0: Services.prefs.clearUserPref("social.sidebar.open"); michael@0: } michael@0: let data = { michael@0: "hidden": !opened, michael@0: "origin": origin michael@0: }; michael@0: SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data)); michael@0: } michael@0: michael@0: let data = SessionStore.getWindowValue(window, "socialSidebar"); michael@0: // if this window doesn't have it's own state, use the state from the opener michael@0: if (!data && window.opener && !window.opener.closed) { michael@0: try { michael@0: data = SessionStore.getWindowValue(window.opener, "socialSidebar"); michael@0: } catch(e) { michael@0: // Window is not tracked, which happens on osx if the window is opened michael@0: // from the hidden window. That happens when you close the last window michael@0: // without quiting firefox, then open a new window. michael@0: } michael@0: } michael@0: if (data) { michael@0: data = JSON.parse(data); michael@0: document.getElementById("social-sidebar-browser").setAttribute("origin", data.origin); michael@0: if (!data.hidden) michael@0: this.show(data.origin); michael@0: } else if (Services.prefs.prefHasUserValue("social.sidebar.provider")) { michael@0: // no window state, use the global state if it is available michael@0: this.show(Services.prefs.getCharPref("social.sidebar.provider")); michael@0: } michael@0: }, michael@0: michael@0: saveWindowState: function() { michael@0: let broadcaster = document.getElementById("socialSidebarBroadcaster"); michael@0: let sidebarOrigin = document.getElementById("social-sidebar-browser").getAttribute("origin"); michael@0: let data = { michael@0: "hidden": broadcaster.hidden, michael@0: "origin": sidebarOrigin michael@0: }; michael@0: michael@0: // Save a global state for users who do not restore state. michael@0: if (broadcaster.hidden) michael@0: Services.prefs.clearUserPref("social.sidebar.provider"); michael@0: else michael@0: Services.prefs.setCharPref("social.sidebar.provider", sidebarOrigin); michael@0: michael@0: try { michael@0: SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data)); michael@0: } catch(e) { michael@0: // window not tracked during uninit michael@0: } michael@0: }, michael@0: michael@0: setSidebarVisibilityState: function(aEnabled) { michael@0: let sbrowser = document.getElementById("social-sidebar-browser"); michael@0: // it's possible we'll be called twice with aEnabled=false so let's michael@0: // just assume we may often be called with the same state. michael@0: if (aEnabled == sbrowser.docShellIsActive) michael@0: return; michael@0: sbrowser.docShellIsActive = aEnabled; michael@0: let evt = sbrowser.contentDocument.createEvent("CustomEvent"); michael@0: evt.initCustomEvent(aEnabled ? "socialFrameShow" : "socialFrameHide", true, true, {}); michael@0: sbrowser.contentDocument.documentElement.dispatchEvent(evt); michael@0: }, michael@0: michael@0: updateToggleNotifications: function() { michael@0: let command = document.getElementById("Social:ToggleNotifications"); michael@0: command.setAttribute("checked", Services.prefs.getBoolPref("social.toast-notifications.enabled")); michael@0: command.setAttribute("hidden", !SocialUI.enabled); michael@0: }, michael@0: michael@0: update: function SocialSidebar_update() { michael@0: // ensure we never update before restoreWindowState michael@0: if (!this._initialized) michael@0: return; michael@0: this.ensureProvider(); michael@0: this.updateToggleNotifications(); michael@0: this._updateHeader(); michael@0: clearTimeout(this._unloadTimeoutId); michael@0: // Hide the toggle menu item if the sidebar cannot appear michael@0: let command = document.getElementById("Social:ToggleSidebar"); michael@0: command.setAttribute("hidden", this.canShow ? "false" : "true"); michael@0: michael@0: // Hide the sidebar if it cannot appear, or has been toggled off. michael@0: // Also set the command "checked" state accordingly. michael@0: let hideSidebar = !this.canShow || !this.opened; michael@0: let broadcaster = document.getElementById("socialSidebarBroadcaster"); michael@0: broadcaster.hidden = hideSidebar; michael@0: command.setAttribute("checked", !hideSidebar); michael@0: michael@0: let sbrowser = document.getElementById("social-sidebar-browser"); michael@0: michael@0: if (hideSidebar) { michael@0: sbrowser.removeEventListener("load", SocialSidebar._loadListener, true); michael@0: this.setSidebarVisibilityState(false); michael@0: // If we've been disabled, unload the sidebar content immediately; michael@0: // if the sidebar was just toggled to invisible, wait a timeout michael@0: // before unloading. michael@0: if (!this.canShow) { michael@0: this.unloadSidebar(); michael@0: } else { michael@0: this._unloadTimeoutId = setTimeout( michael@0: this.unloadSidebar, michael@0: Services.prefs.getIntPref("social.sidebar.unload_timeout_ms") michael@0: ); michael@0: } michael@0: } else { michael@0: sbrowser.setAttribute("origin", this.provider.origin); michael@0: if (this.provider.errorState == "frameworker-error") { michael@0: SocialSidebar.setSidebarErrorMessage(); michael@0: return; michael@0: } michael@0: michael@0: // Make sure the right sidebar URL is loaded michael@0: if (sbrowser.getAttribute("src") != this.provider.sidebarURL) { michael@0: // we check readyState right after setting src, we need a new content michael@0: // viewer to ensure we are checking against the correct document. michael@0: sbrowser.docShell.createAboutBlankContentViewer(null); michael@0: Social.setErrorListener(sbrowser, this.setSidebarErrorMessage.bind(this)); michael@0: // setting isAppTab causes clicks on untargeted links to open new tabs michael@0: sbrowser.docShell.isAppTab = true; michael@0: sbrowser.setAttribute("src", this.provider.sidebarURL); michael@0: PopupNotifications.locationChange(sbrowser); michael@0: } michael@0: michael@0: // if the document has not loaded, delay until it is michael@0: if (sbrowser.contentDocument.readyState != "complete") { michael@0: document.getElementById("social-sidebar-button").setAttribute("loading", "true"); michael@0: sbrowser.addEventListener("load", SocialSidebar._loadListener, true); michael@0: } else { michael@0: this.setSidebarVisibilityState(true); michael@0: } michael@0: } michael@0: this._updateCheckedMenuItems(this.opened && this.provider ? this.provider.origin : null); michael@0: }, michael@0: michael@0: _loadListener: function SocialSidebar_loadListener() { michael@0: let sbrowser = document.getElementById("social-sidebar-browser"); michael@0: sbrowser.removeEventListener("load", SocialSidebar._loadListener, true); michael@0: document.getElementById("social-sidebar-button").removeAttribute("loading"); michael@0: SocialSidebar.setSidebarVisibilityState(true); michael@0: }, michael@0: michael@0: unloadSidebar: function SocialSidebar_unloadSidebar() { michael@0: let sbrowser = document.getElementById("social-sidebar-browser"); michael@0: if (!sbrowser.hasAttribute("origin")) michael@0: return; michael@0: michael@0: sbrowser.stop(); michael@0: sbrowser.removeAttribute("origin"); michael@0: sbrowser.setAttribute("src", "about:blank"); michael@0: // We need to explicitly create a new content viewer because the old one michael@0: // doesn't get destroyed until about:blank has loaded (which does not happen michael@0: // as long as the element is hidden). michael@0: sbrowser.docShell.createAboutBlankContentViewer(null); michael@0: SocialFlyout.unload(); michael@0: }, michael@0: michael@0: _unloadTimeoutId: 0, michael@0: michael@0: setSidebarErrorMessage: function() { michael@0: let sbrowser = document.getElementById("social-sidebar-browser"); michael@0: // a frameworker error "trumps" a sidebar error. michael@0: let origin = sbrowser.getAttribute("origin"); michael@0: if (origin) { michael@0: origin = "&origin=" + encodeURIComponent(origin); michael@0: } michael@0: if (this.provider.errorState == "frameworker-error") { michael@0: sbrowser.setAttribute("src", "about:socialerror?mode=workerFailure" + origin); michael@0: } else { michael@0: let url = encodeURIComponent(this.provider.sidebarURL); michael@0: sbrowser.loadURI("about:socialerror?mode=tryAgain&url=" + url + origin, null, null); michael@0: } michael@0: }, michael@0: michael@0: _provider: null, michael@0: ensureProvider: function() { michael@0: if (this._provider) michael@0: return; michael@0: // origin for sidebar is persisted, so get the previously selected sidebar michael@0: // first, otherwise fallback to the first provider in the list michael@0: let sbrowser = document.getElementById("social-sidebar-browser"); michael@0: let origin = sbrowser.getAttribute("origin"); michael@0: let providers = [p for (p of Social.providers) if (p.sidebarURL)]; michael@0: let provider; michael@0: if (origin) michael@0: provider = Social._getProviderFromOrigin(origin); michael@0: if (!provider && providers.length > 0) michael@0: provider = providers[0]; michael@0: if (provider) michael@0: this.provider = provider; michael@0: }, michael@0: michael@0: get provider() { michael@0: return this._provider; michael@0: }, michael@0: michael@0: set provider(provider) { michael@0: if (!provider || provider.sidebarURL) { michael@0: this._provider = provider; michael@0: this._updateHeader(); michael@0: this._updateCheckedMenuItems(provider && provider.origin); michael@0: this.update(); michael@0: } michael@0: }, michael@0: michael@0: disableProvider: function(origin) { michael@0: if (this._provider && this._provider.origin == origin) { michael@0: this._provider = null; michael@0: // force a selection of the next provider if there is one michael@0: this.ensureProvider(); michael@0: } michael@0: }, michael@0: michael@0: _updateHeader: function() { michael@0: let provider = this.provider; michael@0: let image, title; michael@0: if (provider) { michael@0: image = "url(" + (provider.icon32URL || provider.iconURL) + ")"; michael@0: title = provider.name; michael@0: } michael@0: document.getElementById("social-sidebar-favico").style.listStyleImage = image; michael@0: document.getElementById("social-sidebar-title").value = title; michael@0: }, michael@0: michael@0: _updateCheckedMenuItems: function(origin) { michael@0: // update selected menuitems michael@0: let menuitems = document.getElementsByClassName("social-provider-menuitem"); michael@0: for (let mi of menuitems) { michael@0: if (origin && mi.getAttribute("origin") == origin) { michael@0: mi.setAttribute("checked", "true"); michael@0: mi.setAttribute("oncommand", "SocialSidebar.hide();"); michael@0: } else if (mi.getAttribute("checked")) { michael@0: mi.removeAttribute("checked"); michael@0: mi.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: show: function(origin) { michael@0: // always show the sidebar, and set the provider michael@0: let broadcaster = document.getElementById("socialSidebarBroadcaster"); michael@0: broadcaster.hidden = false; michael@0: if (origin) michael@0: this.provider = Social._getProviderFromOrigin(origin); michael@0: else michael@0: SocialSidebar.update(); michael@0: this.saveWindowState(); michael@0: }, michael@0: michael@0: hide: function() { michael@0: let broadcaster = document.getElementById("socialSidebarBroadcaster"); michael@0: broadcaster.hidden = true; michael@0: this._updateCheckedMenuItems(); michael@0: this.clearProviderMenus(); michael@0: SocialSidebar.update(); michael@0: this.saveWindowState(); michael@0: }, michael@0: michael@0: toggleSidebar: function SocialSidebar_toggle() { michael@0: let broadcaster = document.getElementById("socialSidebarBroadcaster"); michael@0: if (broadcaster.hidden) michael@0: this.show(); michael@0: else michael@0: this.hide(); michael@0: }, michael@0: michael@0: populateSidebarMenu: function(event) { michael@0: // Providers are removed from the view->sidebar menu when there is a change michael@0: // in providers, so we only have to populate onshowing if there are no michael@0: // provider menus. We populate this menu so long as there are enabled michael@0: // providers with sidebars. michael@0: let popup = event.target; michael@0: let providerMenuSeps = popup.getElementsByClassName("social-provider-menu"); michael@0: if (providerMenuSeps[0].previousSibling.nodeName == "menuseparator") michael@0: SocialSidebar.populateProviderMenu(providerMenuSeps[0]); michael@0: }, michael@0: michael@0: clearProviderMenus: function() { michael@0: // called when there is a change in the provider list we clear all menus, michael@0: // they will be repopulated when the menu is shown michael@0: let providerMenuSeps = document.getElementsByClassName("social-provider-menu"); michael@0: for (let providerMenuSep of providerMenuSeps) { michael@0: while (providerMenuSep.previousSibling.nodeName == "menuitem") { michael@0: let menu = providerMenuSep.parentNode; michael@0: menu.removeChild(providerMenuSep.previousSibling); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: populateProviderMenu: function(providerMenuSep) { michael@0: let menu = providerMenuSep.parentNode; michael@0: // selectable providers are inserted before the provider-menu seperator, michael@0: // remove any menuitems in that area michael@0: while (providerMenuSep.previousSibling.nodeName == "menuitem") { michael@0: menu.removeChild(providerMenuSep.previousSibling); michael@0: } michael@0: // only show a selection in the sidebar header menu if there is more than one michael@0: let providers = [p for (p of Social.providers) if (p.sidebarURL)]; michael@0: if (providers.length < 2 && menu.id != "viewSidebarMenu") { michael@0: providerMenuSep.hidden = true; michael@0: return; michael@0: } michael@0: let topSep = providerMenuSep.previousSibling; michael@0: for (let provider of providers) { michael@0: let menuitem = document.createElement("menuitem"); michael@0: menuitem.className = "menuitem-iconic social-provider-menuitem"; michael@0: menuitem.setAttribute("image", provider.iconURL); michael@0: menuitem.setAttribute("label", provider.name); michael@0: menuitem.setAttribute("origin", provider.origin); michael@0: if (this.opened && provider == this.provider) { michael@0: menuitem.setAttribute("checked", "true"); michael@0: menuitem.setAttribute("oncommand", "SocialSidebar.hide();"); michael@0: } else { michael@0: menuitem.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));"); michael@0: } michael@0: menu.insertBefore(menuitem, providerMenuSep); michael@0: } michael@0: topSep.hidden = topSep.nextSibling == providerMenuSep; michael@0: providerMenuSep.hidden = !providerMenuSep.nextSibling; michael@0: } michael@0: } michael@0: michael@0: // this helper class is used by removable/customizable buttons to handle michael@0: // widget creation/destruction michael@0: michael@0: // When a provider is installed we show all their UI so the user will see the michael@0: // functionality of what they installed. The user can later customize the UI, michael@0: // moving buttons around or off the toolbar. michael@0: // michael@0: // On startup, we create the button widgets of any enabled provider. michael@0: // CustomizableUI handles placement and persistence of placement. michael@0: function ToolbarHelper(type, createButtonFn, listener) { michael@0: this._createButton = createButtonFn; michael@0: this._type = type; michael@0: michael@0: if (listener) { michael@0: CustomizableUI.addListener(listener); michael@0: // remove this listener on window close michael@0: window.addEventListener("unload", () => { michael@0: CustomizableUI.removeListener(listener); michael@0: }); michael@0: } michael@0: } michael@0: michael@0: ToolbarHelper.prototype = { michael@0: idFromOrigin: function(origin) { michael@0: // this id needs to pass the checks in CustomizableUI, so remove characters michael@0: // that wont pass. michael@0: return this._type + "-" + Services.io.newURI(origin, null, null).hostPort.replace(/[\.:]/g,'-'); michael@0: }, michael@0: michael@0: // should be called on disable of a provider michael@0: removeProviderButton: function(origin) { michael@0: CustomizableUI.destroyWidget(this.idFromOrigin(origin)); michael@0: }, michael@0: michael@0: clearPalette: function() { michael@0: [this.removeProviderButton(p.origin) for (p of Social.providers)]; michael@0: }, michael@0: michael@0: // should be called on enable of a provider michael@0: populatePalette: function() { michael@0: if (!Social.enabled) { michael@0: this.clearPalette(); michael@0: return; michael@0: } michael@0: michael@0: // create any buttons that do not exist yet if they have been persisted michael@0: // as a part of the UI (otherwise they belong in the palette). michael@0: for (let provider of Social.providers) { michael@0: let id = this.idFromOrigin(provider.origin); michael@0: this._createButton(id, provider); michael@0: } michael@0: } michael@0: } michael@0: michael@0: let SocialStatusWidgetListener = { michael@0: _getNodeOrigin: function(aWidgetId) { michael@0: // we rely on the button id being the same as the widget. michael@0: let node = document.getElementById(aWidgetId); michael@0: if (!node) michael@0: return null michael@0: if (!node.classList.contains("social-status-button")) michael@0: return null michael@0: return node.getAttribute("origin"); michael@0: }, michael@0: onWidgetAdded: function(aWidgetId, aArea, aPosition) { michael@0: let origin = this._getNodeOrigin(aWidgetId); michael@0: if (origin) michael@0: SocialStatus.updateButton(origin); michael@0: }, michael@0: onWidgetRemoved: function(aWidgetId, aPrevArea) { michael@0: let origin = this._getNodeOrigin(aWidgetId); michael@0: if (!origin) michael@0: return; michael@0: // When a widget is demoted to the palette ('removed'), it's visual michael@0: // style should change. michael@0: SocialStatus.updateButton(origin); michael@0: SocialStatus._removeFrame(origin); michael@0: } michael@0: } michael@0: michael@0: SocialStatus = { michael@0: populateToolbarPalette: function() { michael@0: this._toolbarHelper.populatePalette(); michael@0: michael@0: for (let provider of Social.providers) michael@0: this.updateButton(provider.origin); michael@0: }, michael@0: michael@0: removeProvider: function(origin) { michael@0: this._removeFrame(origin); michael@0: this._toolbarHelper.removeProviderButton(origin); michael@0: }, michael@0: michael@0: reloadProvider: function(origin) { michael@0: let button = document.getElementById(this._toolbarHelper.idFromOrigin(origin)); michael@0: if (button && button.getAttribute("open") == "true") michael@0: document.getElementById("social-notification-panel").hidePopup(); michael@0: this._removeFrame(origin); michael@0: }, michael@0: michael@0: _removeFrame: function(origin) { michael@0: let notificationFrameId = "social-status-" + origin; michael@0: let frame = document.getElementById(notificationFrameId); michael@0: if (frame) { michael@0: SharedFrame.forgetGroup(frame.id); michael@0: frame.parentNode.removeChild(frame); michael@0: } michael@0: }, michael@0: michael@0: get _toolbarHelper() { michael@0: delete this._toolbarHelper; michael@0: this._toolbarHelper = new ToolbarHelper("social-status-button", michael@0: CreateSocialStatusWidget, michael@0: SocialStatusWidgetListener); michael@0: return this._toolbarHelper; michael@0: }, michael@0: michael@0: get _dynamicResizer() { michael@0: delete this._dynamicResizer; michael@0: this._dynamicResizer = new DynamicResizeWatcher(); michael@0: return this._dynamicResizer; michael@0: }, michael@0: michael@0: // status panels are one-per button per-process, we swap the docshells between michael@0: // windows when necessary michael@0: _attachNotificatonPanel: function(aParent, aButton, provider) { michael@0: aParent.hidden = !SocialUI.enabled; michael@0: let notificationFrameId = "social-status-" + provider.origin; michael@0: let frame = document.getElementById(notificationFrameId); michael@0: michael@0: // If the button was customized to a new location, we we'll destroy the michael@0: // iframe and start fresh. michael@0: if (frame && frame.parentNode != aParent) { michael@0: SharedFrame.forgetGroup(frame.id); michael@0: frame.parentNode.removeChild(frame); michael@0: frame = null; michael@0: } michael@0: michael@0: if (!frame) { michael@0: frame = SharedFrame.createFrame( michael@0: notificationFrameId, /* frame name */ michael@0: aParent, /* parent */ michael@0: { michael@0: "type": "content", michael@0: "mozbrowser": "true", michael@0: "class": "social-panel-frame", michael@0: "id": notificationFrameId, michael@0: "tooltip": "aHTMLTooltip", michael@0: "context": "contentAreaContextMenu", michael@0: "flex": "1", michael@0: michael@0: // work around bug 793057 - by making the panel roughly the final size michael@0: // we are more likely to have the anchor in the correct position. michael@0: "style": "width: " + PANEL_MIN_WIDTH + "px;", michael@0: michael@0: "origin": provider.origin, michael@0: "src": provider.statusURL michael@0: } michael@0: ); michael@0: michael@0: if (frame.socialErrorListener) michael@0: frame.socialErrorListener.remove(); michael@0: if (frame.docShell) { michael@0: frame.docShell.isActive = false; michael@0: Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this)); michael@0: } michael@0: } else { michael@0: frame.setAttribute("origin", provider.origin); michael@0: SharedFrame.updateURL(notificationFrameId, provider.statusURL); michael@0: } michael@0: aButton.setAttribute("notificationFrameId", notificationFrameId); michael@0: }, michael@0: michael@0: updateButton: function(origin) { michael@0: let id = this._toolbarHelper.idFromOrigin(origin); michael@0: let widget = CustomizableUI.getWidget(id); michael@0: if (!widget) michael@0: return; michael@0: let button = widget.forWindow(window).node; michael@0: if (button) { michael@0: // we only grab the first notification, ignore all others michael@0: let place = CustomizableUI.getPlaceForItem(button); michael@0: let provider = Social._getProviderFromOrigin(origin); michael@0: let icons = provider.ambientNotificationIcons; michael@0: let iconNames = Object.keys(icons); michael@0: let notif = icons[iconNames[0]]; michael@0: michael@0: // The image and tooltip need to be updated for both michael@0: // ambient notification and profile changes. michael@0: let iconURL = provider.icon32URL || provider.iconURL; michael@0: let tooltiptext; michael@0: if (!notif || place == "palette") { michael@0: button.style.listStyleImage = "url(" + iconURL + ")"; michael@0: button.setAttribute("badge", ""); michael@0: button.setAttribute("aria-label", ""); michael@0: button.setAttribute("tooltiptext", provider.name); michael@0: return; michael@0: } michael@0: button.style.listStyleImage = "url(" + (notif.iconURL || iconURL) + ")"; michael@0: button.setAttribute("tooltiptext", notif.label || provider.name); michael@0: michael@0: let badge = notif.counter || ""; michael@0: button.setAttribute("badge", badge); michael@0: let ariaLabel = notif.label; michael@0: // if there is a badge value, we must use a localizable string to insert it. michael@0: if (badge) michael@0: ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText", michael@0: [ariaLabel, badge]); michael@0: button.setAttribute("aria-label", ariaLabel); michael@0: } michael@0: }, michael@0: michael@0: showPopup: function(aToolbarButton) { michael@0: // attach our notification panel if necessary michael@0: let origin = aToolbarButton.getAttribute("origin"); michael@0: let provider = Social._getProviderFromOrigin(origin); michael@0: michael@0: // if we're a slice in the hamburger, use that panel instead michael@0: let widgetGroup = CustomizableUI.getWidget(aToolbarButton.getAttribute("id")); michael@0: let widget = widgetGroup.forWindow(window); michael@0: let panel, showingEvent, hidingEvent; michael@0: let inMenuPanel = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL; michael@0: if (inMenuPanel) { michael@0: panel = document.getElementById("PanelUI-socialapi"); michael@0: this._attachNotificatonPanel(panel, aToolbarButton, provider); michael@0: widget.node.setAttribute("closemenu", "none"); michael@0: showingEvent = "ViewShowing"; michael@0: hidingEvent = "ViewHiding"; michael@0: } else { michael@0: panel = document.getElementById("social-notification-panel"); michael@0: this._attachNotificatonPanel(panel, aToolbarButton, provider); michael@0: showingEvent = "popupshown"; michael@0: hidingEvent = "popuphidden"; michael@0: } michael@0: let notificationFrameId = aToolbarButton.getAttribute("notificationFrameId"); michael@0: let notificationFrame = document.getElementById(notificationFrameId); michael@0: michael@0: let wasAlive = SharedFrame.isGroupAlive(notificationFrameId); michael@0: SharedFrame.setOwner(notificationFrameId, notificationFrame); michael@0: michael@0: // Clear dimensions on all browsers so the panel size will michael@0: // only use the selected browser. michael@0: let frameIter = panel.firstElementChild; michael@0: while (frameIter) { michael@0: frameIter.collapsed = (frameIter != notificationFrame); michael@0: frameIter = frameIter.nextElementSibling; michael@0: } michael@0: michael@0: function dispatchPanelEvent(name) { michael@0: let evt = notificationFrame.contentDocument.createEvent("CustomEvent"); michael@0: evt.initCustomEvent(name, true, true, {}); michael@0: notificationFrame.contentDocument.documentElement.dispatchEvent(evt); michael@0: } michael@0: michael@0: // we only use a dynamic resizer when we're located the toolbar. michael@0: let dynamicResizer = inMenuPanel ? null : this._dynamicResizer; michael@0: panel.addEventListener(hidingEvent, function onpopuphiding() { michael@0: panel.removeEventListener(hidingEvent, onpopuphiding); michael@0: aToolbarButton.removeAttribute("open"); michael@0: if (dynamicResizer) michael@0: dynamicResizer.stop(); michael@0: notificationFrame.docShell.isActive = false; michael@0: dispatchPanelEvent("socialFrameHide"); michael@0: }); michael@0: michael@0: panel.addEventListener(showingEvent, function onpopupshown() { michael@0: panel.removeEventListener(showingEvent, onpopupshown); michael@0: // This attribute is needed on both the button and the michael@0: // containing toolbaritem since the buttons on OS X have michael@0: // moz-appearance:none, while their container gets michael@0: // moz-appearance:toolbarbutton due to the way that toolbar buttons michael@0: // get combined on OS X. michael@0: let initFrameShow = () => { michael@0: notificationFrame.docShell.isActive = true; michael@0: notificationFrame.docShell.isAppTab = true; michael@0: if (dynamicResizer) michael@0: dynamicResizer.start(panel, notificationFrame); michael@0: dispatchPanelEvent("socialFrameShow"); michael@0: }; michael@0: if (!inMenuPanel) michael@0: aToolbarButton.setAttribute("open", "true"); michael@0: if (notificationFrame.contentDocument && michael@0: notificationFrame.contentDocument.readyState == "complete" && wasAlive) { michael@0: initFrameShow(); michael@0: } else { michael@0: // first time load, wait for load and dispatch after load michael@0: notificationFrame.addEventListener("load", function panelBrowserOnload(e) { michael@0: notificationFrame.removeEventListener("load", panelBrowserOnload, true); michael@0: initFrameShow(); michael@0: }, true); michael@0: } michael@0: }); michael@0: michael@0: if (inMenuPanel) { michael@0: PanelUI.showSubView("PanelUI-socialapi", widget.node, michael@0: CustomizableUI.AREA_PANEL); michael@0: } else { michael@0: let anchor = document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-badge-container"); michael@0: // Bug 849216 - open the popup in a setTimeout so we avoid the auto-rollup michael@0: // handling from preventing it being opened in some cases. michael@0: setTimeout(function() { michael@0: panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); michael@0: }, 0); michael@0: } michael@0: }, michael@0: michael@0: setPanelErrorMessage: function(aNotificationFrame) { michael@0: if (!aNotificationFrame) michael@0: return; michael@0: michael@0: let src = aNotificationFrame.getAttribute("src"); michael@0: aNotificationFrame.removeAttribute("src"); michael@0: let origin = aNotificationFrame.getAttribute("origin"); michael@0: aNotificationFrame.webNavigation.loadURI("about:socialerror?mode=tryAgainOnly&url=" + michael@0: encodeURIComponent(src) + "&origin=" + michael@0: encodeURIComponent(origin), michael@0: null, null, null, null); michael@0: let panel = aNotificationFrame.parentNode; michael@0: sizeSocialPanelToContent(panel, aNotificationFrame); michael@0: }, michael@0: michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * SocialMarks michael@0: * michael@0: * Handles updates to toolbox and signals all buttons to update when necessary. michael@0: */ michael@0: SocialMarks = { michael@0: update: function() { michael@0: // signal each button to update itself michael@0: let currentButtons = document.querySelectorAll('toolbarbutton[type="socialmark"]'); michael@0: for (let elt of currentButtons) michael@0: elt.update(); michael@0: }, michael@0: michael@0: updatePanelButtons: function() { michael@0: // querySelectorAll does not work on the menu panel the panel, so we have to michael@0: // do this the hard way. michael@0: let providers = SocialMarks.getProviders(); michael@0: let panel = document.getElementById("PanelUI-popup"); michael@0: for (let p of providers) { michael@0: let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin); michael@0: let widget = CustomizableUI.getWidget(widgetId); michael@0: if (!widget) michael@0: continue; michael@0: let node = widget.forWindow(window).node; michael@0: if (node) michael@0: node.update(); michael@0: } michael@0: }, michael@0: michael@0: getProviders: function() { michael@0: // only rely on providers that the user has placed in the UI somewhere. This michael@0: // also means that populateToolbarPalette must be called prior to using this michael@0: // method, otherwise you get a big fat zero. For our use case with context michael@0: // menu's, this is ok. michael@0: let tbh = this._toolbarHelper; michael@0: return [p for (p of Social.providers) if (p.markURL && michael@0: document.getElementById(tbh.idFromOrigin(p.origin)))]; michael@0: }, michael@0: michael@0: populateContextMenu: function() { michael@0: // only show a selection if enabled and there is more than one michael@0: let providers = this.getProviders(); michael@0: michael@0: // remove all previous entries by class michael@0: let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))]; michael@0: [m.parentNode.removeChild(m) for (m of menus)]; michael@0: michael@0: let contextMenus = [ michael@0: { michael@0: type: "link", michael@0: id: "context-marklinkMenu", michael@0: label: "social.marklinkMenu.label" michael@0: }, michael@0: { michael@0: type: "page", michael@0: id: "context-markpageMenu", michael@0: label: "social.markpageMenu.label" michael@0: } michael@0: ]; michael@0: for (let cfg of contextMenus) { michael@0: this._populateContextPopup(cfg, providers); michael@0: } michael@0: this.updatePanelButtons(); michael@0: }, michael@0: michael@0: MENU_LIMIT: 3, // adjustable for testing michael@0: _populateContextPopup: function(menuInfo, providers) { michael@0: let menu = document.getElementById(menuInfo.id); michael@0: let popup = menu.firstChild; michael@0: for (let provider of providers) { michael@0: // We show up to MENU_LIMIT providers as single menuitems's at the top michael@0: // level of the context menu, if we have more than that, dump them *all* michael@0: // into the menu popup. michael@0: let mi = document.createElement("menuitem"); michael@0: mi.setAttribute("oncommand", "gContextMenu.markLink(this.getAttribute('origin'));"); michael@0: mi.setAttribute("origin", provider.origin); michael@0: mi.setAttribute("image", provider.iconURL); michael@0: if (providers.length <= this.MENU_LIMIT) { michael@0: // an extra class to make enable/disable easy michael@0: mi.setAttribute("class", "menuitem-iconic context-socialmarks context-mark"+menuInfo.type); michael@0: let menuLabel = gNavigatorBundle.getFormattedString(menuInfo.label, [provider.name]); michael@0: mi.setAttribute("label", menuLabel); michael@0: menu.parentNode.insertBefore(mi, menu); michael@0: } else { michael@0: mi.setAttribute("class", "menuitem-iconic context-socialmarks"); michael@0: mi.setAttribute("label", provider.name); michael@0: popup.appendChild(mi); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: populateToolbarPalette: function() { michael@0: this._toolbarHelper.populatePalette(); michael@0: this.populateContextMenu(); michael@0: }, michael@0: michael@0: removeProvider: function(origin) { michael@0: this._toolbarHelper.removeProviderButton(origin); michael@0: }, michael@0: michael@0: get _toolbarHelper() { michael@0: delete this._toolbarHelper; michael@0: this._toolbarHelper = new ToolbarHelper("social-mark-button", CreateSocialMarkWidget); michael@0: return this._toolbarHelper; michael@0: }, michael@0: michael@0: markLink: function(aOrigin, aUrl) { michael@0: // find the button for this provider, and open it michael@0: let id = this._toolbarHelper.idFromOrigin(aOrigin); michael@0: document.getElementById(id).markLink(aUrl); michael@0: } michael@0: }; michael@0: michael@0: })();