Mozilla Cross-Reference mozilla-central
mozilla/ browser/ base/ content/ browser-readinglist.js
Hg Log
Hg Blame
Diff file
Raw file
view using tree:
1 /*
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6 
7 XPCOMUtils.defineLazyModuleGetter(this, "ReadingList",
8   "resource:///modules/readinglist/ReadingList.jsm");
9 
10 const READINGLIST_COMMAND_ID = "readingListSidebar";
11 
12 let ReadingListUI = {
13   /**
14    * Frame-script messages we want to listen to.
15    * @type {[string]}
16    */
17   MESSAGES: [
18     "ReadingList:GetVisibility",
19     "ReadingList:ToggleVisibility",
20     "ReadingList:ShowIntro",
21   ],
22 
23   /**
24    * Add-to-ReadingList toolbar button in the URLbar.
25    * @type {Element}
26    */
27   toolbarButton: null,
28 
29   /**
30    * Whether this object is currently registered as a listener with ReadingList.
31    * Used to avoid inadvertantly loading the ReadLingList.jsm module on startup.
32    * @type {Boolean}
33    */
34   listenerRegistered: false,
35 
36   /**
37    * Initialize the ReadingList UI.
38    */
39   init() {
40     this.toolbarButton = document.getElementById("readinglist-addremove-button");
41 
42     Preferences.observe("browser.readinglist.enabled", this.updateUI, this);
43 
44     const mm = window.messageManager;
45     for (let msg of this.MESSAGES) {
46       mm.addMessageListener(msg, this);
47     }
48 
49     this.updateUI();
50   },
51 
52   /**
53    * Un-initialize the ReadingList UI.
54    */
55   uninit() {
56     Preferences.ignore("browser.readinglist.enabled", this.updateUI, this);
57 
58     const mm = window.messageManager;
59     for (let msg of this.MESSAGES) {
60       mm.removeMessageListener(msg, this);
61     }
62 
63     if (this.listenerRegistered) {
64       ReadingList.removeListener(this);
65       this.listenerRegistered = false;
66     }
67   },
68 
69   /**
70    * Whether the ReadingList feature is enabled or not.
71    * @type {boolean}
72    */
73   get enabled() {
74     return Preferences.get("browser.readinglist.enabled", false);
75   },
76 
77   /**
78    * Whether the ReadingList sidebar is currently open or not.
79    * @type {boolean}
80    */
81   get isSidebarOpen() {
82     return SidebarUI.isOpen && SidebarUI.currentID == READINGLIST_COMMAND_ID;
83   },
84 
85   /**
86    * Update the UI status, ensuring the UI is shown or hidden depending on
87    * whether the feature is enabled or not.
88    */
89   updateUI() {
90     let enabled = this.enabled;
91     if (enabled) {
92       // This is a no-op if we're already registered.
93       ReadingList.addListener(this);
94       this.listenerRegistered = true;
95     } else {
96       if (this.listenerRegistered) {
97         // This is safe to call if we're not currently registered, but we don't
98         // want to forcibly load the normally lazy-loaded module on startup.
99         ReadingList.removeListener(this);
100         this.listenerRegistered = false;
101       }
102 
103       this.hideSidebar();
104     }
105 
106     document.getElementById(READINGLIST_COMMAND_ID).setAttribute("hidden", !enabled);
107   },
108 
109   /**
110    * Show the ReadingList sidebar.
111    * @return {Promise}
112    */
113   showSidebar() {
114     if (this.enabled) {
115       return SidebarUI.show(READINGLIST_COMMAND_ID);
116     }
117   },
118 
119   /**
120    * Hide the ReadingList sidebar, if it is currently shown.
121    */
122   hideSidebar() {
123     if (this.isSidebarOpen) {
124       SidebarUI.hide();
125     }
126   },
127 
128   /**
129    * Re-refresh the ReadingList bookmarks submenu when it opens.
130    *
131    * @param {Element} target - Menu element opening.
132    */
133   onReadingListPopupShowing: Task.async(function* (target) {
134     if (target.id == "BMB_readingListPopup") {
135       // Setting this class in the .xul file messes with the way
136       // browser-places.js inserts bookmarks in the menu.
137       document.getElementById("BMB_viewReadingListSidebar")
138               .classList.add("panel-subview-footer");
139     }
140 
141     while (!target.firstChild.id)
142       target.firstChild.remove();
143 
144     let classList = "menuitem-iconic bookmark-item menuitem-with-favicon";
145     let insertPoint = target.firstChild;
146     if (insertPoint.classList.contains("subviewbutton"))
147       classList += " subviewbutton";
148 
149     let hasItems = false;
150     yield ReadingList.forEachItem(item => {
151       hasItems = true;
152 
153       let menuitem = document.createElement("menuitem");
154       menuitem.setAttribute("label", item.title || item.url);
155       menuitem.setAttribute("class", classList);
156 
157       let node = menuitem._placesNode = {
158         // Passing the PlacesUtils.nodeIsURI check is required for the
159         // onCommand handler to load our URI.
160         type: Ci.nsINavHistoryResultNode.RESULT_TYPE_URI,
161 
162         // makes PlacesUIUtils.canUserRemove return false.
163         // The context menu is broken without this.
164         parent: {type: Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER},
165 
166         // A -1 id makes this item a non-bookmark, which avoids calling
167         // PlacesUtils.annotations.itemHasAnnotation to check if the
168         // bookmark should be opened in the sidebar (this call fails for
169         // readinglist item, and breaks loading our URI).
170         itemId: -1,
171 
172         // Used by the tooltip and onCommand handlers.
173         uri: item.url,
174 
175         // Used by the tooltip.
176         title: item.title
177       };
178 
179       Favicons.getFaviconURLForPage(item.uri, uri => {
180         if (uri) {
181           menuitem.setAttribute("image",
182                                 Favicons.getFaviconLinkForIcon(uri).spec);
183         }
184       });
185 
186       target.insertBefore(menuitem, insertPoint);
187     }, {sort: "addedOn", descending: true});
188 
189     if (!hasItems) {
190       let menuitem = document.createElement("menuitem");
191       let bundle =
192         Services.strings.createBundle("chrome://browser/locale/places/places.properties");
193       menuitem.setAttribute("label", bundle.GetStringFromName("bookmarksMenuEmptyFolder"));
194       menuitem.setAttribute("class", "bookmark-item");
195       menuitem.setAttribute("disabled", true);
196       target.insertBefore(menuitem, insertPoint);
197     }
198   }),
199 
200   /**
201    * Hide the ReadingList sidebar, if it is currently shown.
202    */
203   toggleSidebar() {
204     if (this.enabled) {
205       SidebarUI.toggle(READINGLIST_COMMAND_ID);
206     }
207   },
208 
209   /**
210    * Respond to messages.
211    */
212   receiveMessage(message) {
213     switch (message.name) {
214       case "ReadingList:GetVisibility": {
215         if (message.target.messageManager) {
216           message.target.messageManager.sendAsyncMessage("ReadingList:VisibilityStatus",
217             { isOpen: this.isSidebarOpen });
218         }
219         break;
220       }
221 
222       case "ReadingList:ToggleVisibility": {
223         this.toggleSidebar();
224         break;
225       }
226 
227       case "ReadingList:ShowIntro": {
228         if (this.enabled && !Preferences.get("browser.readinglist.introShown", false)) {
229           Preferences.set("browser.readinglist.introShown", true);
230           this.showSidebar();
231         }
232         break;
233       }
234     }
235   },
236 
237   /**
238    * Handles toolbar button styling based on page proxy state changes.
239    *
240    * @see SetPageProxyState()
241    *
242    * @param {string} state - New state. Either "valid" or "invalid".
243    */
244   onPageProxyStateChanged: Task.async(function* (state) {
245     if (!this.toolbarButton) {
246       // nothing to do if we have no button.
247       return;
248     }
249 
250     let uri;
251     if (this.enabled && state == "valid") {
252       uri = gBrowser.currentURI;
253       if (uri.schemeIs("about"))
254         uri = ReaderParent.parseReaderUrl(uri.spec);
255       else if (!uri.schemeIs("http") && !uri.schemeIs("https"))
256         uri = null;
257     }
258 
259     let msg = {topic: "UpdateActiveItem", url: null};
260     if (!uri) {
261       this.toolbarButton.setAttribute("hidden", true);
262       if (this.isSidebarOpen)
263         document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
264       return;
265     }
266 
267     let isInList = yield ReadingList.hasItemForURL(uri);
268 
269     if (window.closed) {
270       // Skip updating the UI if the window was closed since our hasItemForURL call.
271       return;
272     }
273 
274     if (this.isSidebarOpen) {
275       if (isInList)
276         msg.url = typeof uri == "string" ? uri : uri.spec;
277       document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
278     }
279     this.setToolbarButtonState(isInList);
280   }),
281 
282   /**
283    * Set the state of the ReadingList toolbar button in the urlbar.
284    * If the current tab's page is in the ReadingList (active), sets the button
285    * to allow removing the page. Otherwise, sets the button to allow adding the
286    * page (not active).
287    *
288    * @param {boolean} active - True if the button should be active (page is
289    *                           already in the list).
290    */
291   setToolbarButtonState(active) {
292     this.toolbarButton.setAttribute("already-added", active);
293 
294     let type = (active ? "remove" : "add");
295     let tooltip = gNavigatorBundle.getString(`readingList.urlbar.${type}`);
296     this.toolbarButton.setAttribute("tooltiptext", tooltip);
297 
298     this.toolbarButton.removeAttribute("hidden");
299   },
300 
301   /**
302    * Toggle a page (from a browser) in the ReadingList, adding if it's not already added, or
303    * removing otherwise.
304    *
305    * @param {<xul:browser>} browser - Browser with page to toggle.
306    * @returns {Promise} Promise resolved when operation has completed.
307    */
308   togglePageByBrowser: Task.async(function* (browser) {
309     let uri = browser.currentURI;
310     if (uri.spec.startsWith("about:reader?"))
311       uri = ReaderParent.parseReaderUrl(uri.spec);
312     if (!uri)
313       return;
314 
315     let item = yield ReadingList.itemForURL(uri);
316     if (item) {
317       yield item.delete();
318     } else {
319       yield ReadingList.addItemFromBrowser(browser, uri);
320     }
321   }),
322 
323   /**
324    * Checks if a given item matches the current tab in this window.
325    *
326    * @param {ReadingListItem} item - Item to check
327    * @returns True if match, false otherwise.
328    */
329   isItemForCurrentBrowser(item) {
330     let currentURL = gBrowser.currentURI.spec;
331     if (currentURL.startsWith("about:reader?"))
332       currentURL = ReaderParent.parseReaderUrl(currentURL);
333 
334     if (item.url == currentURL || item.resolvedURL == currentURL) {
335       return true;
336     }
337     return false;
338   },
339 
340   /**
341    * ReadingList event handler for when an item is added.
342    *
343    * @param {ReadingListItem} item - Item added.
344    */
345   onItemAdded(item) {
346     if (!Services.prefs.getBoolPref("browser.readinglist.sidebarEverOpened")) {
347       SidebarUI.show("readingListSidebar");
348     }
349     if (this.isItemForCurrentBrowser(item)) {
350       this.setToolbarButtonState(true);
351       if (this.isSidebarOpen) {
352         let msg = {topic: "UpdateActiveItem", url: item.url};
353         document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
354       }
355     }
356   },
357 
358   /**
359    * ReadingList event handler for when an item is deleted.
360    *
361    * @param {ReadingListItem} item - Item deleted.
362    */
363   onItemDeleted(item) {
364     if (this.isItemForCurrentBrowser(item)) {
365       this.setToolbarButtonState(false);
366     }
367   },
368 };
369 
view http://hg.mozilla.org/mozilla-central/rev/ /browser/base/content/browser-readinglist.js