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