Mozilla Cross-Reference mozilla-central
mozilla/ browser/ base/ content/ browser-places.js
Hg Log
Hg Blame
Diff file
Raw file
view using tree:
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 ////////////////////////////////////////////////////////////////////////////////
6 //// StarUI
7 
8 var StarUI = {
9   _itemId: -1,
10   uri: null,
11   _batching: false,
12 
13   _element: function(aID) {
14     return document.getElementById(aID);
15   },
16 
17   // Edit-bookmark panel
18   get panel() {
19     delete this.panel;
20     var element = this._element("editBookmarkPanel");
21     // initially the panel is hidden
22     // to avoid impacting startup / new window performance
23     element.hidden = false;
24     element.addEventListener("popuphidden", this, false);
25     element.addEventListener("keypress", this, false);
26     return this.panel = element;
27   },
28 
29   // Array of command elements to disable when the panel is opened.
30   get _blockedCommands() {
31     delete this._blockedCommands;
32     return this._blockedCommands =
33       ["cmd_close", "cmd_closeWindow"].map(function (id) this._element(id), this);
34   },
35 
36   _blockCommands: function SU__blockCommands() {
37     this._blockedCommands.forEach(function (elt) {
38       // make sure not to permanently disable this item (see bug 409155)
39       if (elt.hasAttribute("wasDisabled"))
40         return;
41       if (elt.getAttribute("disabled") == "true") {
42         elt.setAttribute("wasDisabled", "true");
43       } else {
44         elt.setAttribute("wasDisabled", "false");
45         elt.setAttribute("disabled", "true");
46       }
47     });
48   },
49 
50   _restoreCommandsState: function SU__restoreCommandsState() {
51     this._blockedCommands.forEach(function (elt) {
52       if (elt.getAttribute("wasDisabled") != "true")
53         elt.removeAttribute("disabled");
54       elt.removeAttribute("wasDisabled");
55     });
56   },
57 
58   // nsIDOMEventListener
59   handleEvent: function SU_handleEvent(aEvent) {
60     switch (aEvent.type) {
61       case "popuphidden":
62         if (aEvent.originalTarget == this.panel) {
63           if (!this._element("editBookmarkPanelContent").hidden)
64             this.quitEditMode();
65 
66           if (this._anchorToolbarButton) {
67             this._anchorToolbarButton.removeAttribute("open");
68             this._anchorToolbarButton = null;
69           }
70           this._restoreCommandsState();
71           this._itemId = -1;
72           if (this._batching) {
73             PlacesUtils.transactionManager.endBatch(false);
74             this._batching = false;
75           }
76 
77           switch (this._actionOnHide) {
78             case "cancel": {
79               PlacesUtils.transactionManager.undoTransaction();
80               break;
81             }
82             case "remove": {
83               // Remove all bookmarks for the bookmark's url, this also removes
84               // the tags for the url.
85               PlacesUtils.transactionManager.beginBatch(null);
86               let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
87               for (let i = 0; i < itemIds.length; i++) {
88                 let txn = new PlacesRemoveItemTransaction(itemIds[i]);
89                 PlacesUtils.transactionManager.doTransaction(txn);
90               }
91               PlacesUtils.transactionManager.endBatch(false);
92               break;
93             }
94           }
95           this._actionOnHide = "";
96         }
97         break;
98       case "keypress":
99         if (aEvent.defaultPrevented) {
100           // The event has already been consumed inside of the panel.
101           break;
102         }
103         switch (aEvent.keyCode) {
104           case KeyEvent.DOM_VK_ESCAPE:
105             if (!this._element("editBookmarkPanelContent").hidden)
106               this.cancelButtonOnCommand();
107             break;
108           case KeyEvent.DOM_VK_RETURN:
109             if (aEvent.target.classList.contains("expander-up") ||
110                 aEvent.target.classList.contains("expander-down") ||
111                 aEvent.target.id == "editBMPanel_newFolderButton")  {
112               //XXX Why is this necessary? The defaultPrevented check should
113               //    be enough.
114               break;
115             }
116             this.panel.hidePopup();
117             break;
118         }
119         break;
120     }
121   },
122 
123   _overlayLoaded: false,
124   _overlayLoading: false,
125   showEditBookmarkPopup:
126   function SU_showEditBookmarkPopup(aItemId, aAnchorElement, aPosition) {
127     // Performance: load the overlay the first time the panel is opened
128     // (see bug 392443).
129     if (this._overlayLoading)
130       return;
131 
132     if (this._overlayLoaded) {
133       this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition);
134       return;
135     }
136 
137     this._overlayLoading = true;
138     document.loadOverlay(
139       "chrome://browser/content/places/editBookmarkOverlay.xul",
140       (function (aSubject, aTopic, aData) {
141         // Move the header (star, title, button) into the grid,
142         // so that it aligns nicely with the other items (bug 484022).
143         let header = this._element("editBookmarkPanelHeader");
144         let rows = this._element("editBookmarkPanelGrid").lastChild;
145         rows.insertBefore(header, rows.firstChild);
146         header.hidden = false;
147 
148         this._overlayLoading = false;
149         this._overlayLoaded = true;
150         this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition);
151       }).bind(this)
152     );
153   },
154 
155   _doShowEditBookmarkPanel:
156   function SU__doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition) {
157     if (this.panel.state != "closed")
158       return;
159 
160     this._blockCommands(); // un-done in the popuphiding handler
161 
162     // Set panel title:
163     // if we are batching, i.e. the bookmark has been added now,
164     // then show Page Bookmarked, else if the bookmark did already exist,
165     // we are about editing it, then use Edit This Bookmark.
166     this._element("editBookmarkPanelTitle").value =
167       this._batching ?
168         gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
169         gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle");
170 
171     // No description; show the Done, Cancel;
172     this._element("editBookmarkPanelDescription").textContent = "";
173     this._element("editBookmarkPanelBottomButtons").hidden = false;
174     this._element("editBookmarkPanelContent").hidden = false;
175 
176     // The remove button is shown only if we're not already batching, i.e.
177     // if the cancel button/ESC does not remove the bookmark.
178     this._element("editBookmarkPanelRemoveButton").hidden = this._batching;
179 
180     // The label of the remove button differs if the URI is bookmarked
181     // multiple times.
182     var bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
183     var forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
184     var label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length);
185     this._element("editBookmarkPanelRemoveButton").label = label;
186 
187     // unset the unstarred state, if set
188     this._element("editBookmarkPanelStarIcon").removeAttribute("unstarred");
189 
190     this._itemId = aItemId !== undefined ? aItemId : this._itemId;
191     this.beginBatch();
192 
193     if (aAnchorElement) {
194       // Set the open=true attribute if the anchor is a
195       // descendent of a toolbarbutton.
196       let parent = aAnchorElement.parentNode;
197       while (parent) {
198         if (parent.localName == "toolbarbutton") {
199           break;
200         }
201         parent = parent.parentNode;
202       }
203       if (parent) {
204         this._anchorToolbarButton = parent;
205         parent.setAttribute("open", "true");
206       }
207     }
208     this.panel.openPopup(aAnchorElement, aPosition);
209 
210     gEditItemOverlay.initPanel(this._itemId,
211                                { hiddenRows: ["description", "location",
212                                               "loadInSidebar", "keyword"] });
213   },
214 
215   panelShown:
216   function SU_panelShown(aEvent) {
217     if (aEvent.target == this.panel) {
218       if (!this._element("editBookmarkPanelContent").hidden) {
219         let fieldToFocus = "editBMPanel_" +
220           gPrefService.getCharPref("browser.bookmarks.editDialog.firstEditField");
221         var elt = this._element(fieldToFocus);
222         elt.focus();
223         elt.select();
224       }
225       else {
226         // Note this isn't actually used anymore, we should remove this
227         // once we decide not to bring back the page bookmarked notification
228         this.panel.focus();
229       }
230     }
231   },
232 
233   quitEditMode: function SU_quitEditMode() {
234     this._element("editBookmarkPanelContent").hidden = true;
235     this._element("editBookmarkPanelBottomButtons").hidden = true;
236     gEditItemOverlay.uninitPanel(true);
237   },
238 
239   cancelButtonOnCommand: function SU_cancelButtonOnCommand() {
240     this._actionOnHide = "cancel";
241     this.panel.hidePopup(true);
242   },
243 
244   removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
245     this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
246     this._actionOnHide = "remove";
247     this.panel.hidePopup();
248   },
249 
250   beginBatch: function SU_beginBatch() {
251     if (!this._batching) {
252       PlacesUtils.transactionManager.beginBatch(null);
253       this._batching = true;
254     }
255   }
256 }
257 
258 ////////////////////////////////////////////////////////////////////////////////
259 //// PlacesCommandHook
260 
261 var PlacesCommandHook = {
262   /**
263    * Adds a bookmark to the page loaded in the given browser.
264    *
265    * @param aBrowser
266    *        a <browser> element.
267    * @param [optional] aParent
268    *        The folder in which to create a new bookmark if the page loaded in
269    *        aBrowser isn't bookmarked yet, defaults to the unfiled root.
270    * @param [optional] aShowEditUI
271    *        whether or not to show the edit-bookmark UI for the bookmark item
272    */  
273   bookmarkPage: function PCH_bookmarkPage(aBrowser, aParent, aShowEditUI) {
274     var uri = aBrowser.currentURI;
275     var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
276     if (itemId == -1) {
277       // Bug 1148838 - Make this code work for full page plugins.
278       var title;
279       var description;
280       var charset;
281       try {
282         let isErrorPage = /^about:(neterror|certerror|blocked)/
283                           .test(aBrowser.contentDocumentAsCPOW.documentURI);
284         title = isErrorPage ? PlacesUtils.history.getPageTitle(uri)
285                             : aBrowser.contentTitle;
286         title = title || uri.spec;
287         description = PlacesUIUtils.getDescriptionFromDocument(aBrowser.contentDocumentAsCPOW);
288         charset = aBrowser.characterSet;
289       }
290       catch (e) { }
291 
292       if (aShowEditUI) {
293         // If we bookmark the page here (i.e. page was not "starred" already)
294         // but open right into the "edit" state, start batching here, so
295         // "Cancel" in that state removes the bookmark.
296         StarUI.beginBatch();
297       }
298 
299       var parent = aParent != undefined ?
300                    aParent : PlacesUtils.unfiledBookmarksFolderId;
301       var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description };
302       var txn = new PlacesCreateBookmarkTransaction(uri, parent, 
303                                                     PlacesUtils.bookmarks.DEFAULT_INDEX,
304                                                     title, null, [descAnno]);
305       PlacesUtils.transactionManager.doTransaction(txn);
306       itemId = txn.item.id;
307       // Set the character-set
308       if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser))
309         PlacesUtils.setCharsetForURI(uri, charset);
310     }
311 
312     // Revert the contents of the location bar
313     if (gURLBar)
314       gURLBar.handleRevert();
315 
316     // If it was not requested to open directly in "edit" mode, we are done.
317     if (!aShowEditUI)
318       return;
319 
320     // Try to dock the panel to:
321     // 1. the bookmarks menu button
322     // 2. the page-proxy-favicon
323     // 3. the content area
324     if (BookmarkingUI.anchor) {
325       StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor,
326                                    "bottomcenter topright");
327       return;
328     }
329 
330     let pageProxyFavicon = document.getElementById("page-proxy-favicon");
331     if (isElementVisible(pageProxyFavicon)) {
332       StarUI.showEditBookmarkPopup(itemId, pageProxyFavicon,
333                                    "bottomcenter topright");
334     } else {
335       StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap");
336     }
337   },
338 
339   /**
340    * Adds a bookmark to the page loaded in the current tab. 
341    */
342   bookmarkCurrentPage: function PCH_bookmarkCurrentPage(aShowEditUI, aParent) {
343     this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI);
344   },
345 
346   /**
347    * Adds a bookmark to the page targeted by a link.
348    * @param aParent
349    *        The folder in which to create a new bookmark if aURL isn't
350    *        bookmarked.
351    * @param aURL (string)
352    *        the address of the link target
353    * @param aTitle
354    *        The link text
355    */
356   bookmarkLink: function PCH_bookmarkLink(aParent, aURL, aTitle) {
357     var linkURI = makeURI(aURL);
358     var itemId = PlacesUtils.getMostRecentBookmarkForURI(linkURI);
359     if (itemId == -1) {
360       PlacesUIUtils.showBookmarkDialog({ action: "add"
361                                        , type: "bookmark"
362                                        , uri: linkURI
363                                        , title: aTitle
364                                        , hiddenRows: [ "description"
365                                                      , "location"
366                                                      , "loadInSidebar"
367                                                      , "keyword" ]
368                                        }, window);
369     }
370     else {
371       PlacesUIUtils.showBookmarkDialog({ action: "edit"
372                                        , type: "bookmark"
373                                        , itemId: itemId
374                                        }, window);
375     }
376   },
377 
378   /**
379    * List of nsIURI objects characterizing the tabs currently open in the
380    * browser, modulo pinned tabs.  The URIs will be in the order in which their
381    * corresponding tabs appeared and duplicates are discarded.
382    */
383   get uniqueCurrentPages() {
384     let uniquePages = {};
385     let URIs = [];
386     gBrowser.visibleTabs.forEach(function (tab) {
387       let spec = tab.linkedBrowser.currentURI.spec;
388       if (!tab.pinned && !(spec in uniquePages)) {
389         uniquePages[spec] = null;
390         URIs.push(tab.linkedBrowser.currentURI);
391       }
392     });
393     return URIs;
394   },
395 
396   /**
397    * Adds a folder with bookmarks to all of the currently open tabs in this 
398    * window.
399    */
400   bookmarkCurrentPages: function PCH_bookmarkCurrentPages() {
401     let pages = this.uniqueCurrentPages;
402     if (pages.length > 1) {
403     PlacesUIUtils.showBookmarkDialog({ action: "add"
404                                      , type: "folder"
405                                      , URIList: pages
406                                      , hiddenRows: [ "description" ]
407                                      }, window);
408     }
409   },
410 
411   /**
412    * Updates disabled state for the "Bookmark All Tabs" command.
413    */
414   updateBookmarkAllTabsCommand:
415   function PCH_updateBookmarkAllTabsCommand() {
416     // There's nothing to do in non-browser windows.
417     if (window.location.href != getBrowserURL())
418       return;
419 
420     // Disable "Bookmark All Tabs" if there are less than two
421     // "unique current pages".
422     goSetCommandEnabled("Browser:BookmarkAllTabs",
423                         this.uniqueCurrentPages.length >= 2);
424   },
425 
426   /**
427    * Adds a Live Bookmark to a feed associated with the current page. 
428    * @param     url
429    *            The nsIURI of the page the feed was attached to
430    * @title     title
431    *            The title of the feed. Optional.
432    * @subtitle  subtitle
433    *            A short description of the feed. Optional.
434    */
435   addLiveBookmark: function PCH_addLiveBookmark(url, feedTitle, feedSubtitle) {
436     var feedURI = makeURI(url);
437 
438     var doc = gBrowser.contentDocumentAsCPOW;
439     var title = (arguments.length > 1) ? feedTitle : doc.title;
440 
441     var description;
442     if (arguments.length > 2)
443       description = feedSubtitle;
444     else
445       description = PlacesUIUtils.getDescriptionFromDocument(doc);
446 
447     var toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId, -1);
448     PlacesUIUtils.showBookmarkDialog({ action: "add"
449                                      , type: "livemark"
450                                      , feedURI: feedURI
451                                      , siteURI: gBrowser.currentURI
452                                      , title: title
453                                      , description: description
454                                      , defaultInsertionPoint: toolbarIP
455                                      , hiddenRows: [ "feedLocation"
456                                                    , "siteLocation"
457                                                    , "description" ]
458                                      }, window);
459   },
460 
461   /**
462    * Opens the Places Organizer. 
463    * @param   aLeftPaneRoot
464    *          The query to select in the organizer window - options
465    *          are: History, AllBookmarks, BookmarksMenu, BookmarksToolbar,
466    *          UnfiledBookmarks, Tags and Downloads.
467    */
468   showPlacesOrganizer: function PCH_showPlacesOrganizer(aLeftPaneRoot) {
469     var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
470     // Due to bug 528706, getMostRecentWindow can return closed windows.
471     if (!organizer || organizer.closed) {
472       // No currently open places window, so open one with the specified mode.
473       openDialog("chrome://browser/content/places/places.xul", 
474                  "", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot);
475     }
476     else {
477       organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
478       organizer.focus();
479     }
480   }
481 };
482 
483 ////////////////////////////////////////////////////////////////////////////////
484 //// HistoryMenu
485 
486 XPCOMUtils.defineLazyModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils",
487   "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm");
488 
489 // View for the history menu.
490 function HistoryMenu(aPopupShowingEvent) {
491   // Workaround for Bug 610187.  The sidebar does not include all the Places
492   // views definitions, and we don't need them there.
493   // Defining the prototype inheritance in the prototype itself would cause
494   // browser.js to halt on "PlacesMenu is not defined" error.
495   this.__proto__.__proto__ = PlacesMenu.prototype;
496   PlacesMenu.call(this, aPopupShowingEvent,
497                   "place:sort=4&maxResults=15");
498 }
499 
500 HistoryMenu.prototype = {
501   _getClosedTabCount() {
502     // SessionStore doesn't track the hidden window, so just return zero then.
503     if (window == Services.appShell.hiddenDOMWindow) {
504       return 0;
505     }
506 
507     return SessionStore.getClosedTabCount(window);
508   },
509 
510   toggleRecentlyClosedTabs: function HM_toggleRecentlyClosedTabs() {
511     // enable/disable the Recently Closed Tabs sub menu
512     var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0];
513 
514     // no restorable tabs, so disable menu
515     if (this._getClosedTabCount() == 0)
516       undoMenu.setAttribute("disabled", true);
517     else
518       undoMenu.removeAttribute("disabled");
519   },
520 
521   /**
522    * Populate when the history menu is opened
523    */
524   populateUndoSubmenu: function PHM_populateUndoSubmenu() {
525     var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0];
526     var undoPopup = undoMenu.firstChild;
527 
528     // remove existing menu items
529     while (undoPopup.hasChildNodes())
530       undoPopup.removeChild(undoPopup.firstChild);
531 
532     // no restorable tabs, so make sure menu is disabled, and return
533     if (this._getClosedTabCount() == 0) {
534       undoMenu.setAttribute("disabled", true);
535       return;
536     }
537 
538     // enable menu
539     undoMenu.removeAttribute("disabled");
540 
541     // populate menu
542     let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(window, "menuitem");
543     undoPopup.appendChild(tabsFragment);
544   },
545 
546   toggleRecentlyClosedWindows: function PHM_toggleRecentlyClosedWindows() {
547     // enable/disable the Recently Closed Windows sub menu
548     var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0];
549 
550     // no restorable windows, so disable menu
551     if (SessionStore.getClosedWindowCount() == 0)
552       undoMenu.setAttribute("disabled", true);
553     else
554       undoMenu.removeAttribute("disabled");
555   },
556 
557   /**
558    * Populate when the history menu is opened
559    */
560   populateUndoWindowSubmenu: function PHM_populateUndoWindowSubmenu() {
561     let undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0];
562     let undoPopup = undoMenu.firstChild;
563     let menuLabelString = gNavigatorBundle.getString("menuUndoCloseWindowLabel");
564     let menuLabelStringSingleTab =
565       gNavigatorBundle.getString("menuUndoCloseWindowSingleTabLabel");
566 
567     // remove existing menu items
568     while (undoPopup.hasChildNodes())
569       undoPopup.removeChild(undoPopup.firstChild);
570 
571     // no restorable windows, so make sure menu is disabled, and return
572     if (SessionStore.getClosedWindowCount() == 0) {
573       undoMenu.setAttribute("disabled", true);
574       return;
575     }
576 
577     // enable menu
578     undoMenu.removeAttribute("disabled");
579 
580     // populate menu
581     let windowsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(window, "menuitem");
582     undoPopup.appendChild(windowsFragment);
583   },
584 
585   toggleTabsFromOtherComputers: function PHM_toggleTabsFromOtherComputers() {
586     // This is a no-op if MOZ_SERVICES_SYNC isn't defined
587 #ifdef MOZ_SERVICES_SYNC
588     // Enable/disable the Tabs From Other Computers menu. Some of the menus handled
589     // by HistoryMenu do not have this menuitem.
590     let menuitem = this._rootElt.getElementsByClassName("syncTabsMenuItem")[0];
591     if (!menuitem)
592       return;
593 
594     if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) {
595       menuitem.setAttribute("hidden", true);
596       return;
597     }
598 
599     let enabled = PlacesUIUtils.shouldEnableTabsFromOtherComputersMenuitem();
600     menuitem.setAttribute("disabled", !enabled);
601     menuitem.setAttribute("hidden", false);
602 #endif
603   },
604 
605   _onPopupShowing: function HM__onPopupShowing(aEvent) {
606     PlacesMenu.prototype._onPopupShowing.apply(this, arguments);
607 
608     // Don't handle events for submenus.
609     if (aEvent.target != aEvent.currentTarget)
610       return;
611 
612     this.toggleRecentlyClosedTabs();
613     this.toggleRecentlyClosedWindows();
614     this.toggleTabsFromOtherComputers();
615   },
616 
617   _onCommand: function HM__onCommand(aEvent) {
618     let placesNode = aEvent.target._placesNode;
619     if (placesNode) {
620       if (!PrivateBrowsingUtils.isWindowPrivate(window))
621         PlacesUIUtils.markPageAsTyped(placesNode.uri);
622       openUILink(placesNode.uri, aEvent, { ignoreAlt: true });
623     }
624   }
625 };
626 
627 ////////////////////////////////////////////////////////////////////////////////
628 //// BookmarksEventHandler
629 
630 /**
631  * Functions for handling events in the Bookmarks Toolbar and menu.
632  */
633 var BookmarksEventHandler = {
634   /**
635    * Handler for click event for an item in the bookmarks toolbar or menu.
636    * Menus and submenus from the folder buttons bubble up to this handler.
637    * Left-click is handled in the onCommand function.
638    * When items are middle-clicked (or clicked with modifier), open in tabs.
639    * If the click came through a menu, close the menu.
640    * @param aEvent
641    *        DOMEvent for the click
642    * @param aView
643    *        The places view which aEvent should be associated with.
644    */
645   onClick: function BEH_onClick(aEvent, aView) {
646     // Only handle middle-click or left-click with modifiers.
647 #ifdef XP_MACOSX
648     var modifKey = aEvent.metaKey || aEvent.shiftKey;
649 #else
650     var modifKey = aEvent.ctrlKey || aEvent.shiftKey;
651 #endif
652     if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey))
653       return;
654 
655     var target = aEvent.originalTarget;
656     // If this event bubbled up from a menu or menuitem, close the menus.
657     // Do this before opening tabs, to avoid hiding the open tabs confirm-dialog.
658     if (target.localName == "menu" || target.localName == "menuitem") {
659       for (node = target.parentNode; node; node = node.parentNode) {
660         if (node.localName == "menupopup")
661           node.hidePopup();
662         else if (node.localName != "menu" &&
663                  node.localName != "splitmenu" &&
664                  node.localName != "hbox" &&
665                  node.localName != "vbox" )
666           break;
667       }
668     }
669 
670     if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) {
671       // Don't open the root folder in tabs when the empty area on the toolbar
672       // is middle-clicked or when a non-bookmark item except for Open in Tabs)
673       // in a bookmarks menupopup is middle-clicked.
674       if (target.localName == "menu" || target.localName == "toolbarbutton")
675         PlacesUIUtils.openContainerNodeInTabs(target._placesNode, aEvent, aView);
676     }
677     else if (aEvent.button == 1) {
678       // left-clicks with modifier are already served by onCommand
679       this.onCommand(aEvent, aView);
680     }
681   },
682 
683   /**
684    * Handler for command event for an item in the bookmarks toolbar.
685    * Menus and submenus from the folder buttons bubble up to this handler.
686    * Opens the item.
687    * @param aEvent 
688    *        DOMEvent for the command
689    * @param aView
690    *        The places view which aEvent should be associated with.
691    */
692   onCommand: function BEH_onCommand(aEvent, aView) {
693     var target = aEvent.originalTarget;
694     if (target._placesNode)
695       PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView);
696   },
697 
698   fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) {
699     var node;
700     var cropped = false;
701     var targetURI;
702 
703     if (aDocument.tooltipNode.localName == "treechildren") {
704       var tree = aDocument.tooltipNode.parentNode;
705       var tbo = tree.treeBoxObject;
706       var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
707       if (cell.row == -1)
708         return false;
709       node = tree.view.nodeForTreeIndex(cell.row);
710       cropped = tbo.isCellCropped(cell.row, cell.col);
711     }
712     else {
713       // Check whether the tooltipNode is a Places node.
714       // In such a case use it, otherwise check for targetURI attribute.
715       var tooltipNode = aDocument.tooltipNode;
716       if (tooltipNode._placesNode)
717         node = tooltipNode._placesNode;
718       else {
719         // This is a static non-Places node.
720         targetURI = tooltipNode.getAttribute("targetURI");
721       }
722     }
723 
724     if (!node && !targetURI)
725       return false;
726 
727     // Show node.label as tooltip's title for non-Places nodes.
728     var title = node ? node.title : tooltipNode.label;
729 
730     // Show URL only for Places URI-nodes or nodes with a targetURI attribute.
731     var url;
732     if (targetURI || PlacesUtils.nodeIsURI(node))
733       url = targetURI || node.uri;
734 
735     // Show tooltip for containers only if their title is cropped.
736     if (!cropped && !url)
737       return false;
738 
739     var tooltipTitle = aDocument.getElementById("bhtTitleText");
740     tooltipTitle.hidden = (!title || (title == url));
741     if (!tooltipTitle.hidden)
742       tooltipTitle.textContent = title;
743 
744     var tooltipUrl = aDocument.getElementById("bhtUrlText");
745     tooltipUrl.hidden = !url;
746     if (!tooltipUrl.hidden)
747       tooltipUrl.value = url;
748 
749     // Show tooltip.
750     return true;
751   }
752 };
753 
754 ////////////////////////////////////////////////////////////////////////////////
755 //// PlacesMenuDNDHandler
756 
757 // Handles special drag and drop functionality for Places menus that are not
758 // part of a Places view (e.g. the bookmarks menu in the menubar).
759 var PlacesMenuDNDHandler = {
760   _springLoadDelayMs: 350,
761   _closeDelayMs: 500,
762   _loadTimer: null,
763   _closeTimer: null,
764   _closingTimerNode: null,
765 
766   /**
767    * Called when the user enters the <menu> element during a drag.
768    * @param   event
769    *          The DragEnter event that spawned the opening. 
770    */
771   onDragEnter: function PMDH_onDragEnter(event) {
772     // Opening menus in a Places popup is handled by the view itself.
773     if (!this._isStaticContainer(event.target))
774       return;
775 
776     // If we re-enter the same menu or anchor before the close timer runs out,
777     // we should ensure that we do not close:
778     if (this._closeTimer && this._closingTimerNode === event.currentTarget) {
779       this._closeTimer.cancel();
780       this._closingTimerNode = null;
781       this._closeTimer = null;
782     }
783 
784     PlacesControllerDragHelper.currentDropTarget = event.target;
785     let popup = event.target.lastChild;
786     if (this._loadTimer || popup.state === "showing" || popup.state === "open")
787       return;
788 
789     this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
790     this._loadTimer.initWithCallback(() => {
791       this._loadTimer = null;
792       popup.setAttribute("autoopened", "true");
793       popup.showPopup(popup);
794     }, this._springLoadDelayMs, Ci.nsITimer.TYPE_ONE_SHOT);
795     event.preventDefault();
796     event.stopPropagation();
797   },
798 
799   /**
800    * Handles dragleave on the <menu> element.
801    */
802   onDragLeave: function PMDH_onDragLeave(event) {
803     // Handle menu-button separate targets.
804     if (event.relatedTarget === event.currentTarget ||
805         (event.relatedTarget &&
806          event.relatedTarget.parentNode === event.currentTarget))
807       return;
808 
809     // Closing menus in a Places popup is handled by the view itself.
810     if (!this._isStaticContainer(event.target))
811       return;
812 
813     PlacesControllerDragHelper.currentDropTarget = null;
814     let popup = event.target.lastChild;
815 
816     if (this._loadTimer) {
817       this._loadTimer.cancel();
818       this._loadTimer = null;
819     }
820     this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
821     this._closingTimerNode = event.currentTarget;
822     this._closeTimer.initWithCallback(function() {
823       this._closeTimer = null;
824       this._closingTimerNode = null;
825       let node = PlacesControllerDragHelper.currentDropTarget;
826       let inHierarchy = false;
827       while (node && !inHierarchy) {
828         inHierarchy = node == event.target;
829         node = node.parentNode;
830       }
831       if (!inHierarchy && popup && popup.hasAttribute("autoopened")) {
832         popup.removeAttribute("autoopened");
833         popup.hidePopup();
834       }
835     }, this._closeDelayMs, Ci.nsITimer.TYPE_ONE_SHOT);
836   },
837 
838   /**
839    * Determines if a XUL element represents a static container.
840    * @returns true if the element is a container element (menu or 
841    *`         menu-toolbarbutton), false otherwise.
842    */
843   _isStaticContainer: function PMDH__isContainer(node) {
844     let isMenu = node.localName == "menu" ||
845                  (node.localName == "toolbarbutton" &&
846                   (node.getAttribute("type") == "menu" ||
847                    node.getAttribute("type") == "menu-button"));
848     let isStatic = !("_placesNode" in node) && node.lastChild &&
849                    node.lastChild.hasAttribute("placespopup") &&
850                    !node.parentNode.hasAttribute("placespopup");
851     return isMenu && isStatic;
852   },
853 
854   /**
855    * Called when the user drags over the <menu> element.
856    * @param   event
857    *          The DragOver event. 
858    */
859   onDragOver: function PMDH_onDragOver(event) {
860     let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
861                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
862                                 Ci.nsITreeView.DROP_ON);
863     if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer))
864       event.preventDefault();
865 
866     event.stopPropagation();
867   },
868 
869   /**
870    * Called when the user drops on the <menu> element.
871    * @param   event
872    *          The Drop event. 
873    */
874   onDrop: function PMDH_onDrop(event) {
875     // Put the item at the end of bookmark menu.
876     let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
877                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
878                                 Ci.nsITreeView.DROP_ON);
879     PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
880     PlacesControllerDragHelper.currentDropTarget = null;
881     event.stopPropagation();
882   }
883 };
884 
885 ////////////////////////////////////////////////////////////////////////////////
886 //// PlacesToolbarHelper
887 
888 /**
889  * This object handles the initialization and uninitialization of the bookmarks
890  * toolbar.
891  */
892 let PlacesToolbarHelper = {
893   _place: "place:folder=TOOLBAR",
894 
895   get _viewElt() {
896     return document.getElementById("PlacesToolbar");
897   },
898 
899   get _placeholder() {
900     return document.getElementById("bookmarks-toolbar-placeholder");
901   },
902 
903   init: function PTH_init(forceToolbarOverflowCheck) {
904     let viewElt = this._viewElt;
905     if (!viewElt || viewElt._placesView)
906       return;
907 
908     // CustomizableUI.addListener is idempotent, so we can safely
909     // call this multiple times.
910     CustomizableUI.addListener(this);
911 
912     // If the bookmarks toolbar item is:
913     // - not in a toolbar, or;
914     // - the toolbar is collapsed, or;
915     // - the toolbar is hidden some other way:
916     // don't initialize.  Also, there is no need to initialize the toolbar if
917     // customizing, because that will happen when the customization is done.
918     let toolbar = this._getParentToolbar(viewElt);
919     if (!toolbar || toolbar.collapsed || this._isCustomizing ||
920         getComputedStyle(toolbar, "").display == "none")
921       return;
922 
923     new PlacesToolbar(this._place);
924     if (forceToolbarOverflowCheck) {
925       viewElt._placesView.updateOverflowStatus();
926     }
927     this._shouldWrap = false;
928     this._setupPlaceholder();
929   },
930 
931   uninit: function PTH_uninit() {
932     CustomizableUI.removeListener(this);
933   },
934 
935   customizeStart: function PTH_customizeStart() {
936     try {
937       let viewElt = this._viewElt;
938       if (viewElt && viewElt._placesView)
939         viewElt._placesView.uninit();
940     } finally {
941       this._isCustomizing = true;
942     }
943     this._shouldWrap = this._getShouldWrap();
944   },
945 
946   customizeChange: function PTH_customizeChange() {
947     this._setupPlaceholder();
948   },
949 
950   _setupPlaceholder: function PTH_setupPlaceholder() {
951     let placeholder = this._placeholder;
952     if (!placeholder) {
953       return;
954     }
955 
956     let shouldWrapNow = this._getShouldWrap();
957     if (this._shouldWrap != shouldWrapNow) {
958       if (shouldWrapNow) {
959         placeholder.setAttribute("wrap", "true");
960       } else {
961         placeholder.removeAttribute("wrap");
962       }
963       this._shouldWrap = shouldWrapNow;
964     }
965   },
966 
967   customizeDone: function PTH_customizeDone() {
968     this._isCustomizing = false;
969     this.init(true);
970   },
971 
972   _getShouldWrap: function PTH_getShouldWrap() {
973     let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
974     let area = placement && placement.area;
975     let areaType = area && CustomizableUI.getAreaType(area);
976     return !area || CustomizableUI.TYPE_MENU_PANEL == areaType;
977   },
978 
979   onPlaceholderCommand: function () {
980     let widgetGroup = CustomizableUI.getWidget("personal-bookmarks");
981     let widget = widgetGroup.forWindow(window);
982     if (widget.overflowed ||
983         widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
984       PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar");
985     }
986   },
987 
988   _getParentToolbar: function(element) {
989     while (element) {
990       if (element.localName == "toolbar") {
991         return element;
992       }
993       element = element.parentNode;
994     }
995     return null;
996   },
997 
998   onWidgetUnderflow: function(aNode, aContainer) {
999     // The view gets broken by being removed and reinserted by the overflowable
1000     // toolbar, so we have to force an uninit and reinit.
1001     let win = aNode.ownerDocument.defaultView;
1002     if (aNode.id == "personal-bookmarks" && win == window) {
1003       this._resetView();
1004     }
1005   },
1006 
1007   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
1008     if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) {
1009       // It's possible (with the "Add to Menu", "Add to Toolbar" context
1010       // options) that the Places Toolbar Items have been moved without
1011       // letting us prepare and handle it with with customizeStart and
1012       // customizeDone. If that's the case, we need to reset the views
1013       // since they're probably broken from the DOM reparenting.
1014       this._resetView();
1015     }
1016   },
1017 
1018   _resetView: function() {
1019     if (this._viewElt) {
1020       // It's possible that the placesView might not exist, and we need to
1021       // do a full init. This could happen if the Bookmarks Toolbar Items are
1022       // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar"
1023       // context menu option, outside of customize mode.
1024       if (this._viewElt._placesView) {
1025         this._viewElt._placesView.uninit();
1026       }
1027       this.init(true);
1028     }
1029   },
1030 };
1031 
1032 ////////////////////////////////////////////////////////////////////////////////
1033 //// BookmarkingUI
1034 
1035 /**
1036  * Handles the bookmarks menu-button in the toolbar.
1037  */
1038 
1039 let BookmarkingUI = {
1040   BOOKMARK_BUTTON_ID: "bookmarks-menu-button",
1041   BOOKMARK_BUTTON_SHORTCUT: "addBookmarkAsKb",
1042   get button() {
1043     delete this.button;
1044     let widgetGroup = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID);
1045     return this.button = widgetGroup.forWindow(window).node;
1046   },
1047 
1048   /* Can't make this a self-deleting getter because it's anonymous content
1049    * and might lose/regain bindings at some point. */
1050   get star() {
1051     return document.getAnonymousElementByAttribute(this.button, "anonid",
1052                                                    "button");
1053   },
1054 
1055   get anchor() {
1056     if (!this._shouldUpdateStarState()) {
1057       return null;
1058     }
1059     let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
1060                                .forWindow(window);
1061     if (widget.overflowed)
1062       return widget.anchor;
1063 
1064     let star = this.star;
1065     return star ? document.getAnonymousElementByAttribute(star, "class",
1066                                                           "toolbarbutton-icon")
1067                 : null;
1068   },
1069 
1070   get notifier() {
1071     delete this.notifier;
1072     return this.notifier = document.getElementById("bookmarked-notification-anchor");
1073   },
1074 
1075   get dropmarkerNotifier() {
1076     delete this.dropmarkerNotifier;
1077     return this.dropmarkerNotifier = document.getElementById("bookmarked-notification-dropmarker-anchor");
1078   },
1079 
1080   get broadcaster() {
1081     delete this.broadcaster;
1082     let broadcaster = document.getElementById("bookmarkThisPageBroadcaster");
1083     return this.broadcaster = broadcaster;
1084   },
1085 
1086   STATUS_UPDATING: -1,
1087   STATUS_UNSTARRED: 0,
1088   STATUS_STARRED: 1,
1089   get status() {
1090     if (!this._shouldUpdateStarState()) {
1091       return this.STATUS_UNSTARRED;
1092     }
1093     if (this._pendingStmt)
1094       return this.STATUS_UPDATING;
1095     return this.button.hasAttribute("starred") ? this.STATUS_STARRED
1096                                                : this.STATUS_UNSTARRED;
1097   },
1098 
1099   get _starredTooltip()
1100   {
1101     delete this._starredTooltip;
1102     return this._starredTooltip =
1103       this._getFormattedTooltip("starButtonOn.tooltip2");
1104   },
1105 
1106   get _unstarredTooltip()
1107   {
1108     delete this._unstarredTooltip;
1109     return this._unstarredTooltip =
1110       this._getFormattedTooltip("starButtonOff.tooltip2");
1111   },
1112 
1113   _getFormattedTooltip: function(strId) {
1114     let args = [];
1115     let shortcut = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT);
1116     if (shortcut)
1117       args.push(ShortcutUtils.prettifyShortcut(shortcut));
1118     return gNavigatorBundle.getFormattedString(strId, args);
1119   },
1120 
1121   /**
1122    * The type of the area in which the button is currently located.
1123    * When in the panel, we don't update the button's icon.
1124    */
1125   _currentAreaType: null,
1126   _shouldUpdateStarState: function() {
1127     return this._currentAreaType == CustomizableUI.TYPE_TOOLBAR;
1128   },
1129 
1130   /**
1131    * The popup contents must be updated when the user customizes the UI, or
1132    * changes the personal toolbar collapsed status.  In such a case, any needed
1133    * change should be handled in the popupshowing helper, for performance
1134    * reasons.
1135    */
1136   _popupNeedsUpdate: true,
1137   onToolbarVisibilityChange: function BUI_onToolbarVisibilityChange() {
1138     this._popupNeedsUpdate = true;
1139   },
1140 
1141   onPopupShowing: function BUI_onPopupShowing(event) {
1142     // Don't handle events for submenus.
1143     if (event.target != event.currentTarget)
1144       return;
1145 
1146     // Ideally this code would never be reached, but if you click the outer
1147     // button's border, some cpp code for the menu button's so-called XBL binding
1148     // decides to open the popup even though the dropmarker is invisible.
1149     if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
1150       this._showSubview();
1151       event.preventDefault();
1152       event.stopPropagation();
1153       return;
1154     }
1155 
1156     let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
1157                                .forWindow(window);
1158     if (widget.overflowed) {
1159       // Don't open a popup in the overflow popup, rather just open the Library.
1160       event.preventDefault();
1161       widget.node.removeAttribute("closemenu");
1162       PlacesCommandHook.showPlacesOrganizer("BookmarksMenu");
1163       return;
1164     }
1165 
1166     if (!this._popupNeedsUpdate)
1167       return;
1168     this._popupNeedsUpdate = false;
1169 
1170     let popup = event.target;
1171     let getPlacesAnonymousElement =
1172       aAnonId => document.getAnonymousElementByAttribute(popup.parentNode,
1173                                                          "placesanonid",
1174                                                          aAnonId);
1175 
1176     let viewToolbarMenuitem = getPlacesAnonymousElement("view-toolbar");
1177     if (viewToolbarMenuitem) {
1178       // Update View bookmarks toolbar checkbox menuitem.
1179       viewToolbarMenuitem.classList.add("subviewbutton");
1180       let personalToolbar = document.getElementById("PersonalToolbar");
1181       viewToolbarMenuitem.setAttribute("checked", !personalToolbar.collapsed);
1182     }
1183   },
1184 
1185   attachPlacesView: function(event, node) {
1186     // If the view is already there, bail out early.
1187     if (node.parentNode._placesView)
1188       return;
1189 
1190     new PlacesMenu(event, "place:folder=BOOKMARKS_MENU", {
1191       extraClasses: {
1192         entry: "subviewbutton",
1193         footer: "panel-subview-footer"
1194       },
1195       insertionPoint: ".panel-subview-footer"
1196     });
1197   },
1198 
1199   /**
1200    * Handles star styling based on page proxy state changes.
1201    */
1202   onPageProxyStateChanged: function BUI_onPageProxyStateChanged(aState) {
1203     if (!this._shouldUpdateStarState() || !this.star) {
1204       return;
1205     }
1206 
1207     if (aState == "invalid") {
1208       this.star.setAttribute("disabled", "true");
1209       this.button.removeAttribute("starred");
1210       this.button.setAttribute("buttontooltiptext", "");
1211     }
1212     else {
1213       this.star.removeAttribute("disabled");
1214       this._updateStar();
1215     }
1216     this._updateToolbarStyle();
1217   },
1218 
1219   _updateCustomizationState: function BUI__updateCustomizationState() {
1220     let placement = CustomizableUI.getPlacementOfWidget(this.BOOKMARK_BUTTON_ID);
1221     this._currentAreaType = placement && CustomizableUI.getAreaType(placement.area);
1222   },
1223 
1224   _updateToolbarStyle: function BUI__updateToolbarStyle() {
1225     let onPersonalToolbar = false;
1226     if (this._currentAreaType == CustomizableUI.TYPE_TOOLBAR) {
1227       let personalToolbar = document.getElementById("PersonalToolbar");
1228       onPersonalToolbar = this.button.parentNode == personalToolbar ||
1229                           this.button.parentNode.parentNode == personalToolbar;
1230     }
1231 
1232     if (onPersonalToolbar)
1233       this.button.classList.add("bookmark-item");
1234     else
1235       this.button.classList.remove("bookmark-item");
1236   },
1237 
1238   _uninitView: function BUI__uninitView() {
1239     // When an element with a placesView attached is removed and re-inserted,
1240     // XBL reapplies the binding causing any kind of issues and possible leaks,
1241     // so kill current view and let popupshowing generate a new one.
1242     if (this.button._placesView)
1243       this.button._placesView.uninit();
1244 
1245     // We have to do the same thing for the "special" views underneath the
1246     // the bookmarks menu.
1247     const kSpecialViewNodeIDs = ["BMB_bookmarksToolbar", "BMB_unsortedBookmarks"];
1248     for (let viewNodeID of kSpecialViewNodeIDs) {
1249       let elem = document.getElementById(viewNodeID);
1250       if (elem && elem._placesView) {
1251         elem._placesView.uninit();
1252       }
1253     }
1254   },
1255 
1256   onCustomizeStart: function BUI_customizeStart(aWindow) {
1257     if (aWindow == window) {
1258       this._uninitView();
1259       this._isCustomizing = true;
1260     }
1261   },
1262 
1263   onWidgetAdded: function BUI_widgetAdded(aWidgetId) {
1264     if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
1265       this._onWidgetWasMoved();
1266     }
1267   },
1268 
1269   onWidgetRemoved: function BUI_widgetRemoved(aWidgetId) {
1270     if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
1271       this._onWidgetWasMoved();
1272     }
1273   },
1274 
1275   onWidgetReset: function BUI_widgetReset(aNode, aContainer) {
1276     if (aNode == this.button) {
1277       this._onWidgetWasMoved();
1278     }
1279   },
1280 
1281   onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode, aContainer) {
1282     if (aNode == this.button) {
1283       this._onWidgetWasMoved();
1284     }
1285   },
1286 
1287   _onWidgetWasMoved: function BUI_widgetWasMoved() {
1288     let usedToUpdateStarState = this._shouldUpdateStarState();
1289     this._updateCustomizationState();
1290     if (!usedToUpdateStarState && this._shouldUpdateStarState()) {
1291       this.updateStarState();
1292     } else if (usedToUpdateStarState && !this._shouldUpdateStarState()) {
1293       this._updateStar();
1294     }
1295     // If we're moved outside of customize mode, we need to uninit
1296     // our view so it gets reconstructed.
1297     if (!this._isCustomizing) {
1298       this._uninitView();
1299     }
1300     this._updateToolbarStyle();
1301   },
1302 
1303   onCustomizeEnd: function BUI_customizeEnd(aWindow) {
1304     if (aWindow == window) {
1305       this._isCustomizing = false;
1306       this.onToolbarVisibilityChange();
1307       this._updateToolbarStyle();
1308     }
1309   },
1310 
1311   init: function() {
1312     CustomizableUI.addListener(this);
1313     this._updateCustomizationState();
1314   },
1315 
1316   _hasBookmarksObserver: false,
1317   _itemIds: [],
1318   uninit: function BUI_uninit() {
1319     this._updateBookmarkPageMenuItem(true);
1320     CustomizableUI.removeListener(this);
1321 
1322     this._uninitView();
1323 
1324     if (this._hasBookmarksObserver) {
1325       PlacesUtils.removeLazyBookmarkObserver(this);
1326     }
1327 
1328     if (this._pendingStmt) {
1329       this._pendingStmt.cancel();
1330       delete this._pendingStmt;
1331     }
1332   },
1333 
1334   onLocationChange: function BUI_onLocationChange() {
1335     if (this._uri && gBrowser.currentURI.equals(this._uri)) {
1336       return;
1337     }
1338     this.updateStarState();
1339   },
1340 
1341   updateStarState: function BUI_updateStarState() {
1342     // Reset tracked values.
1343     this._uri = gBrowser.currentURI;
1344     this._itemIds = [];
1345 
1346     if (this._pendingStmt) {
1347       this._pendingStmt.cancel();
1348       delete this._pendingStmt;
1349     }
1350 
1351     // We can load about:blank before the actual page, but there is no point in handling that page.
1352     if (isBlankPageURL(this._uri.spec)) {
1353       return;
1354     }
1355 
1356     this._pendingStmt = PlacesUtils.asyncGetBookmarkIds(this._uri, (aItemIds, aURI) => {
1357       // Safety check that the bookmarked URI equals the tracked one.
1358       if (!aURI.equals(this._uri)) {
1359         Components.utils.reportError("BookmarkingUI did not receive current URI");
1360         return;
1361       }
1362 
1363       // It's possible that onItemAdded gets called before the async statement
1364       // calls back.  For such an edge case, retain all unique entries from both
1365       // arrays.
1366       this._itemIds = this._itemIds.filter(
1367         function (id) aItemIds.indexOf(id) == -1
1368       ).concat(aItemIds);
1369 
1370       this._updateStar();
1371 
1372       // Start observing bookmarks if needed.
1373       if (!this._hasBookmarksObserver) {
1374         try {
1375           PlacesUtils.addLazyBookmarkObserver(this);
1376           this._hasBookmarksObserver = true;
1377         } catch(ex) {
1378           Components.utils.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
1379         }
1380       }
1381 
1382       delete this._pendingStmt;
1383     });
1384   },
1385 
1386   _updateStar: function BUI__updateStar() {
1387     if (!this._shouldUpdateStarState()) {
1388       if (this.button.hasAttribute("starred")) {
1389         this.button.removeAttribute("starred");
1390         this.button.removeAttribute("buttontooltiptext");
1391       }
1392       return;
1393     }
1394 
1395     if (this._itemIds.length > 0) {
1396       this.button.setAttribute("starred", "true");
1397       this.button.setAttribute("buttontooltiptext", this._starredTooltip);
1398       if (this.button.getAttribute("overflowedItem") == "true") {
1399         this.button.setAttribute("label", this._starButtonOverflowedStarredLabel);
1400       }
1401     }
1402     else {
1403       this.button.removeAttribute("starred");
1404       this.button.setAttribute("buttontooltiptext", this._unstarredTooltip);
1405       if (this.button.getAttribute("overflowedItem") == "true") {
1406         this.button.setAttribute("label", this._starButtonOverflowedLabel);
1407       }
1408     }
1409   },
1410 
1411   /**
1412    * forceReset is passed when we're destroyed and the label should go back
1413    * to the default (Bookmark This Page) for OS X.
1414    */
1415   _updateBookmarkPageMenuItem: function BUI__updateBookmarkPageMenuItem(forceReset) {
1416     let isStarred = !forceReset && this._itemIds.length > 0;
1417     let label = isStarred ? "editlabel" : "bookmarklabel";
1418     this.broadcaster.setAttribute("label", this.broadcaster.getAttribute(label));
1419   },
1420 
1421   onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) {
1422     this._updateBookmarkPageMenuItem();
1423     PlacesCommandHook.updateBookmarkAllTabsCommand();
1424   },
1425 
1426   _showBookmarkedNotification: function BUI_showBookmarkedNotification() {
1427     function getCenteringTransformForRects(rectToPosition, referenceRect) {
1428       let topDiff = referenceRect.top - rectToPosition.top;
1429       let leftDiff = referenceRect.left - rectToPosition.left;
1430       let heightDiff = referenceRect.height - rectToPosition.height;
1431       let widthDiff = referenceRect.width - rectToPosition.width;
1432       return [(leftDiff + .5 * widthDiff) + "px", (topDiff + .5 * heightDiff) + "px"];
1433     }
1434 
1435     if (this._notificationTimeout) {
1436       clearTimeout(this._notificationTimeout);
1437     }
1438 
1439     if (this.notifier.style.transform == '') {
1440       // Get all the relevant nodes and computed style objects
1441       let dropmarker = document.getAnonymousElementByAttribute(this.button, "anonid", "dropmarker");
1442       let dropmarkerIcon = document.getAnonymousElementByAttribute(dropmarker, "class", "dropmarker-icon");
1443       let dropmarkerStyle = getComputedStyle(dropmarkerIcon);
1444 
1445       // Check for RTL and get bounds
1446       let isRTL = getComputedStyle(this.button).direction == "rtl";
1447       let buttonRect = this.button.getBoundingClientRect();
1448       let notifierRect = this.notifier.getBoundingClientRect();
1449       let dropmarkerRect = dropmarkerIcon.getBoundingClientRect();
1450       let dropmarkerNotifierRect = this.dropmarkerNotifier.getBoundingClientRect();
1451 
1452       // Compute, but do not set, transform for star icon
1453       let [translateX, translateY] = getCenteringTransformForRects(notifierRect, buttonRect);
1454       let starIconTransform = "translate(" +  translateX + ", " + translateY + ")";
1455       if (isRTL) {
1456         starIconTransform += " scaleX(-1)";
1457       }
1458 
1459       // Compute, but do not set, transform for dropmarker
1460       [translateX, translateY] = getCenteringTransformForRects(dropmarkerNotifierRect, dropmarkerRect);
1461       let dropmarkerTransform = "translate(" + translateX + ", " + translateY + ")";
1462 
1463       // Do all layout invalidation in one go:
1464       this.notifier.style.transform = starIconTransform;
1465       this.dropmarkerNotifier.style.transform = dropmarkerTransform;
1466 
1467       let dropmarkerAnimationNode = this.dropmarkerNotifier.firstChild;
1468       dropmarkerAnimationNode.style.MozImageRegion = dropmarkerStyle.MozImageRegion;
1469       dropmarkerAnimationNode.style.listStyleImage = dropmarkerStyle.listStyleImage;
1470     }
1471 
1472     let isInOverflowPanel = this.button.getAttribute("overflowedItem") == "true";
1473     if (!isInOverflowPanel) {
1474       this.notifier.setAttribute("notification", "finish");
1475       this.button.setAttribute("notification", "finish");
1476       this.dropmarkerNotifier.setAttribute("notification", "finish");
1477     }
1478 
1479     this._notificationTimeout = setTimeout( () => {
1480       this.notifier.removeAttribute("notification");
1481       this.dropmarkerNotifier.removeAttribute("notification");
1482       this.button.removeAttribute("notification");
1483 
1484       this.dropmarkerNotifier.style.transform = '';
1485       this.notifier.style.transform = '';
1486     }, 1000);
1487   },
1488 
1489   _showSubview: function() {
1490     let view = document.getElementById("PanelUI-bookmarks");
1491     view.addEventListener("ViewShowing", this);
1492     view.addEventListener("ViewHiding", this);
1493     let anchor = document.getElementById(this.BOOKMARK_BUTTON_ID);
1494     anchor.setAttribute("closemenu", "none");
1495     PanelUI.showSubView("PanelUI-bookmarks", anchor,
1496                         CustomizableUI.AREA_PANEL);
1497   },
1498 
1499   onCommand: function BUI_onCommand(aEvent) {
1500     if (aEvent.target != aEvent.currentTarget) {
1501       return;
1502     }
1503 
1504     // Handle special case when the button is in the panel.
1505     let isBookmarked = this._itemIds.length > 0;
1506 
1507     if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
1508       this._showSubview();
1509       return;
1510     }
1511     let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
1512                                .forWindow(window);
1513     if (widget.overflowed) {
1514       // Allow to close the panel if the page is already bookmarked, cause
1515       // we are going to open the edit bookmark panel.
1516       if (isBookmarked)
1517         widget.node.removeAttribute("closemenu");
1518       else
1519         widget.node.setAttribute("closemenu", "none");
1520     }
1521 
1522     // Ignore clicks on the star if we are updating its state.
1523     if (!this._pendingStmt) {
1524       if (!isBookmarked)
1525         this._showBookmarkedNotification();
1526       PlacesCommandHook.bookmarkCurrentPage(isBookmarked);
1527     }
1528   },
1529 
1530   handleEvent: function BUI_handleEvent(aEvent) {
1531     switch (aEvent.type) {
1532       case "ViewShowing":
1533         this.onPanelMenuViewShowing(aEvent);
1534         break;
1535       case "ViewHiding":
1536         this.onPanelMenuViewHiding(aEvent);
1537         break;
1538     }
1539   },
1540 
1541   onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) {
1542     this._updateBookmarkPageMenuItem();
1543     // Update checked status of the toolbar toggle.
1544     let viewToolbar = document.getElementById("panelMenu_viewBookmarksToolbar");
1545     let personalToolbar = document.getElementById("PersonalToolbar");
1546     if (personalToolbar.collapsed)
1547       viewToolbar.removeAttribute("checked");
1548     else
1549       viewToolbar.setAttribute("checked", "true");
1550     // Get all statically placed buttons to supply them with keyboard shortcuts.
1551     let staticButtons = viewToolbar.parentNode.getElementsByTagName("toolbarbutton");
1552     for (let i = 0, l = staticButtons.length; i < l; ++i)
1553       CustomizableUI.addShortcut(staticButtons[i]);
1554     // Setup the Places view.
1555     this._panelMenuView = new PlacesPanelMenuView("place:folder=BOOKMARKS_MENU",
1556                                                   "panelMenu_bookmarksMenu",
1557                                                   "panelMenu_bookmarksMenu", {
1558                                                     extraClasses: {
1559                                                       entry: "subviewbutton",
1560                                                       footer: "panel-subview-footer"
1561                                                     }
1562                                                   });
1563     aEvent.target.removeEventListener("ViewShowing", this);
1564   },
1565 
1566   onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) {
1567     this._panelMenuView.uninit();
1568     delete this._panelMenuView;
1569     aEvent.target.removeEventListener("ViewHiding", this);
1570   },
1571 
1572   onPanelMenuViewCommand: function BUI_onPanelMenuViewCommand(aEvent, aView) {
1573     let target = aEvent.originalTarget;
1574     if (!target._placesNode)
1575       return;
1576     if (PlacesUtils.nodeIsContainer(target._placesNode))
1577       PlacesCommandHook.showPlacesOrganizer([ "BookmarksMenu", target._placesNode.itemId ]);
1578     else
1579       PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView);
1580     PanelUI.hide();
1581   },
1582 
1583   // nsINavBookmarkObserver
1584   onItemAdded: function BUI_onItemAdded(aItemId, aParentId, aIndex, aItemType,
1585                                         aURI) {
1586     if (aURI && aURI.equals(this._uri)) {
1587       // If a new bookmark has been added to the tracked uri, register it.
1588       if (this._itemIds.indexOf(aItemId) == -1) {
1589         this._itemIds.push(aItemId);
1590         // Only need to update the UI if it wasn't marked as starred before:
1591         if (this._itemIds.length == 1) {
1592           this._updateStar();
1593         }
1594       }
1595     }
1596   },
1597 
1598   onItemRemoved: function BUI_onItemRemoved(aItemId) {
1599     let index = this._itemIds.indexOf(aItemId);
1600     // If one of the tracked bookmarks has been removed, unregister it.
1601     if (index != -1) {
1602       this._itemIds.splice(index, 1);
1603       // Only need to update the UI if the page is no longer starred
1604       if (this._itemIds.length == 0) {
1605         this._updateStar();
1606       }
1607     }
1608   },
1609 
1610   onItemChanged: function BUI_onItemChanged(aItemId, aProperty,
1611                                             aIsAnnotationProperty, aNewValue) {
1612     if (aProperty == "uri") {
1613       let index = this._itemIds.indexOf(aItemId);
1614       // If the changed bookmark was tracked, check if it is now pointing to
1615       // a different uri and unregister it.
1616       if (index != -1 && aNewValue != this._uri.spec) {
1617         this._itemIds.splice(index, 1);
1618         // Only need to update the UI if the page is no longer starred
1619         if (this._itemIds.length == 0) {
1620           this._updateStar();
1621         }
1622       }
1623       // If another bookmark is now pointing to the tracked uri, register it.
1624       else if (index == -1 && aNewValue == this._uri.spec) {
1625         this._itemIds.push(aItemId);
1626         // Only need to update the UI if it wasn't marked as starred before:
1627         if (this._itemIds.length == 1) {
1628           this._updateStar();
1629         }
1630       }
1631     }
1632   },
1633 
1634   onBeginUpdateBatch: function () {},
1635   onEndUpdateBatch: function () {},
1636   onBeforeItemRemoved: function () {},
1637   onItemVisited: function () {},
1638   onItemMoved: function () {},
1639 
1640   // CustomizableUI events:
1641   _starButtonLabel: null,
1642   get _starButtonOverflowedLabel() {
1643     delete this._starButtonOverflowedLabel;
1644     return this._starButtonOverflowedLabel =
1645       gNavigatorBundle.getString("starButtonOverflowed.label");
1646   },
1647   get _starButtonOverflowedStarredLabel() {
1648     delete this._starButtonOverflowedStarredLabel;
1649     return this._starButtonOverflowedStarredLabel =
1650       gNavigatorBundle.getString("starButtonOverflowedStarred.label");
1651   },
1652   onWidgetOverflow: function(aNode, aContainer) {
1653     let win = aNode.ownerDocument.defaultView;
1654     if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window)
1655       return;
1656 
1657     let currentLabel = aNode.getAttribute("label");
1658     if (!this._starButtonLabel)
1659       this._starButtonLabel = currentLabel;
1660 
1661     if (currentLabel == this._starButtonLabel) {
1662       let desiredLabel = this._itemIds.length > 0 ? this._starButtonOverflowedStarredLabel
1663                                                  : this._starButtonOverflowedLabel;
1664       aNode.setAttribute("label", desiredLabel);
1665     }
1666   },
1667 
1668   onWidgetUnderflow: function(aNode, aContainer) {
1669     let win = aNode.ownerDocument.defaultView;
1670     if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window)
1671       return;
1672 
1673     // The view gets broken by being removed and reinserted. Uninit
1674     // here so popupshowing will generate a new one:
1675     this._uninitView();
1676 
1677     if (aNode.getAttribute("label") != this._starButtonLabel)
1678       aNode.setAttribute("label", this._starButtonLabel);
1679   },
1680 
1681   QueryInterface: XPCOMUtils.generateQI([
1682     Ci.nsINavBookmarkObserver
1683   ])
1684 };
1685 
view http://hg.mozilla.org/mozilla-central/rev/ /browser/base/content/browser-places.js