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