 |
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