/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is mozilla.org code. * * The Initial Developer of the Original Code is * Pierre Chanial . * Portions created by the Initial Developer are Copyright (C) 1998 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ var BookmarksMenu = { _selection:null, _target:null, _orientation:null, ////////////////////////////////////////////////////////////////////////// // Fill a context menu popup with menuitems appropriate for the current // selection. createContextMenu: function (aEvent) { var target = document.popupNode; if (!this.isBTBookmark(target.id) && target.id != "bookmarks-ptf") return false; target.focus() // buttons in the pt have -moz-user-focus: ignore --> this._selection = this.getBTSelection(target); this._orientation = this.getBTOrientation(aEvent, target); this._target = this.getBTTarget(target, this._orientation); BookmarksCommand.createContextMenu(aEvent, this._selection); this.onCommandUpdate(); aEvent.target.addEventListener("mousemove", BookmarksMenuController.onMouseMove, false); return true; }, ///////////////////////////////////////////////////////////////////////// // Clean up after closing the context menu popup destroyContextMenu: function (aEvent) { if (content) content.focus(); BookmarksMenuDNDObserver.onDragRemoveFeedBack(document.popupNode); // needed on cancel aEvent.target.removeEventListener("mousemove", BookmarksMenuController.onMouseMove, false); }, ///////////////////////////////////////////////////////////////////////////// // returns the formatted selection from aNode getBTSelection: function (aNode) { var item; switch (aNode.id) { case "bookmarks-ptf": item = "NC:PersonalToolbarFolder"; break; case "BookmarksMenu": item = "NC:BookmarksRoot"; break; default: item = aNode.id; } if (!this.isBTBookmark(item)) return {length:0}; var parent = this.getBTContainer(aNode); var isExpanded = aNode.hasAttribute("open") && aNode.open; var selection = {}; selection.item = [RDF.GetResource(item)]; selection.parent = [RDF.GetResource(parent)]; selection.isExpanded = [isExpanded]; selection.length = selection.item.length; BookmarksUtils.checkSelection(selection); return selection; }, ///////////////////////////////////////////////////////////////////////// // returns the insertion target from aNode getBTTarget: function (aNode, aOrientation) { var item, parent, index; switch (aNode.id) { case "BookmarksMenu": case "bookmarks-button": parent = "NC:BookmarksRoot"; break; case "bookmarks-ptf": item = BookmarksToolbar.getLastVisibleBookmark(); // Continue to next case. case "bookmarks-chevron": parent = "NC:PersonalToolbarFolder"; break; default: if (aOrientation == BookmarksUtils.DROP_ON) parent = aNode.id else { parent = this.getBTContainer(aNode); item = aNode; } } parent = RDF.GetResource(parent); if (aOrientation == BookmarksUtils.DROP_ON) return BookmarksUtils.getTargetFromFolder(parent); item = RDF.GetResource(item.id); RDFC.Init(BMDS, parent); index = RDFC.IndexOf(item); if (aOrientation == BookmarksUtils.DROP_AFTER) ++index; return { parent: parent, index: index }; }, ///////////////////////////////////////////////////////////////////////// // returns the parent resource of a node in the personal toolbar. // this is determined by inspecting the source element and walking up the // DOM tree to find the appropriate containing node. getBTContainer: function (aNode) { var parent; var item = aNode.id; if (!this.isBTBookmark(item)) return "NC:BookmarksRoot" parent = aNode.parentNode.parentNode; parent = parent.id; switch (parent) { case "BookmarksMenu": case "bookmarks-button": return "NC:BookmarksRoot"; case "PersonalToolbar": case "bookmarks-chevron": return "NC:PersonalToolbarFolder"; default: return parent; } }, /////////////////////////////////////////////////////////////////////////// // returns true if the node is a bookmark, a folder or a bookmark separator isBTBookmark: function (aURI) { if (!aURI) return false; var type = BookmarksUtils.resolveType(aURI); return (type == "BookmarkSeparator" || type == "Bookmark" || type == "Folder" || type == "FolderGroup" || type == "PersonalToolbarFolder") }, ///////////////////////////////////////////////////////////////////////// // returns true if the node is a container. --> isBTContainer: function (aTarget) { return aTarget.localName == "menu" || (aTarget.localName == "toolbarbutton" && (aTarget.getAttribute("container") == "true" || aTarget.getAttribute("group") == "true")); }, ///////////////////////////////////////////////////////////////////////// // returns BookmarksUtils.DROP_BEFORE, DROP_ON or DROP_AFTER accordingly // to the event coordinates. Skin authors could break us, we'll cross that // bridge when they turn us 90degrees. --> getBTOrientation: function (aEvent, aTarget) { var target if (!aTarget) target = aEvent.target; else target = aTarget; if (target.localName == "menu" && target.parentNode.localName != "menupopup") return BookmarksUtils.DROP_ON; if (target.id == "bookmarks-ptf") { return target.hasChildNodes() ? BookmarksUtils.DROP_AFTER : BookmarksUtils.DROP_ON; } if (target.id == "bookmarks-chevron") return BookmarksUtils.DROP_ON; var overButtonBoxObject = target.boxObject.QueryInterface(Components.interfaces.nsIBoxObject); var overParentBoxObject = target.parentNode.boxObject.QueryInterface(Components.interfaces.nsIBoxObject); var size, border; var coordValue, clientCoordValue; switch (target.localName) { case "toolbarseparator": case "toolbarbutton": size = overButtonBoxObject.width; coordValue = overButtonBoxObject.x; clientCoordValue = aEvent.clientX; break; case "menuseparator": case "menu": case "menuitem": size = overButtonBoxObject.height; coordValue = overButtonBoxObject.screenY; clientCoordValue = aEvent.screenY; break; default: return BookmarksUtils.DROP_ON; } if (this.isBTContainer(target)) if (target.localName == "toolbarbutton") { // the DROP_BEFORE area excludes the label var iconNode = document.getAnonymousElementByAttribute(target, "class", "toolbarbutton-icon"); border = parseInt(document.defaultView.getComputedStyle(target,"").getPropertyValue("padding-left")) + parseInt(document.defaultView.getComputedStyle(iconNode ,"").getPropertyValue("width")); border = Math.min(size/5,Math.max(border,4)); } else border = size/5; else border = size/2; // in the first region? if (clientCoordValue-coordValue < border) return BookmarksUtils.DROP_BEFORE; // in the last region? if (clientCoordValue-coordValue >= size-border) return BookmarksUtils.DROP_AFTER; // must be in the middle somewhere return BookmarksUtils.DROP_ON; }, ///////////////////////////////////////////////////////////////////////// // expand the folder targeted by the context menu. expandBTFolder: function () { var target = document.popupNode.lastChild; if (document.popupNode.open) target.hidePopup(); else target.showPopup(document.popupNode); }, onCommandUpdate: function () { var selection = this._selection; var target = this._target; BookmarksController.onCommandUpdate(selection, target); if (document.popupNode.id == "bookmarks-ptf") { // disabling 'cut' and 'copy' on the empty area of the personal toolbar var commandNode = document.getElementById("cmd_bm_cut"); commandNode.setAttribute("disabled", "true"); commandNode = document.getElementById("cmd_bm_copy"); commandNode.setAttribute("disabled", "true"); } }, loadBookmark: function (aEvent, aDS) { if (this.isBTBookmark(aEvent.target.id)) BookmarksUtils.loadBookmarkBrowser(aEvent, aDS); }, //////////////////////////////////////////////// // loads a bookmark with the middle mouse button loadBookmarkMiddleClick: function (aEvent, aDS) { if (aEvent.type != "click" || aEvent.button != 1) return; // unlike for command events, we have to close the menus manually for (var node = aEvent.target; node != aEvent.currentTarget; node = node.parentNode) { if (node.nodeType == node.ELEMENT_NODE && node.tagName == "menupopup") node.hidePopup(); } this.loadBookmark(aEvent, aDS); } } var BookmarksMenuController = { supportsCommand: BookmarksController.supportsCommand, isCommandEnabled: function (aCommand) { // warning: this is not the function called in BookmarksController.onCommandUpdate var selection = BookmarksMenu._selection; var target = BookmarksMenu._target; if (selection) return BookmarksController.isCommandEnabled(aCommand, selection, target); return false; }, doCommand: function (aCommand) { BookmarksMenuDNDObserver.onDragRemoveFeedBack(document.popupNode); var selection = BookmarksMenu._selection; var target = BookmarksMenu._target; switch (aCommand) { case "cmd_bm_expandfolder": setTimeout(BookmarksMenu.expandBTFolder, 0); break; default: BookmarksController.doCommand(aCommand, selection, target); } }, onMouseMove: function (aEvent) { var command = aEvent.target.getAttribute("command"); var isDisabled = aEvent.target.getAttribute("disabled") if (isDisabled != "true" && (command == "cmd_bm_newfolder" || command == "cmd_bm_paste")) { BookmarksMenuDNDObserver.onDragSetFeedBack(document.popupNode, BookmarksMenu._orientation); } else { BookmarksMenuDNDObserver.onDragRemoveFeedBack(document.popupNode); } } } var BookmarksMenuDNDObserver = { //////////////////// // Public methods // //////////////////// onDragStart: function (aEvent, aXferData, aDragAction) { var target = aEvent.target; // Prevent dragging from an invalid region if (!this.canDrop(aEvent)) return; // Prevent dragging out of menupopups on non Win32 platforms. // a) on Mac drag from menus is generally regarded as being satanic // b) on Linux, this causes an X-server crash, (bug 151336) // c) on Windows, there is no hang or crash associated with this, so we'll leave // the functionality there. if (navigator.platform != "Win32" && target.localName != "toolbarbutton") return; // bail if dragging from the empty area of the bookmarks toolbar if (target.id == "bookmarks-ptf") return // a drag start is fired when leaving an open toolbarbutton(type=menu) // (see bug 143031) if (this.isContainer(target) && target.getAttribute("group") != "true") { if (this.isPlatformNotSupported) return; if (!aEvent.shiftKey && !aEvent.altKey && !aEvent.ctrlKey) return; // menus open on mouse down target.firstChild.hidePopup(); } var selection = BookmarksMenu.getBTSelection(target); aXferData.data = BookmarksUtils.getXferDataFromSelection(selection); }, onDragOver: function(aEvent, aFlavour, aDragSession) { var orientation = BookmarksMenu.getBTOrientation(aEvent) if (aDragSession.canDrop) this.onDragSetFeedBack(aEvent.target, orientation); if (orientation != this.mCurrentDropPosition) { // emulating onDragExit and onDragEnter events since the drop region // has changed on the target. this.onDragExit(aEvent, aDragSession); this.onDragEnter(aEvent, aDragSession); } if (this.isPlatformNotSupported) return; if (this.isTimerSupported || !aDragSession.sourceNode) return; this.onDragOverCheckTimers(); }, onDragEnter: function (aEvent, aDragSession) { var target = aEvent.target; var orientation = BookmarksMenu.getBTOrientation(aEvent); if (target.localName == "menupopup" || target.id == "bookmarks-ptf") target = target.parentNode; if (aDragSession.canDrop) { this.onDragSetFeedBack(target, orientation); this.onDragEnterSetTimer(target, aDragSession); } this.mCurrentDragOverTarget = target; this.mCurrentDropPosition = orientation; }, onDragExit: function (aEvent, aDragSession) { var target = aEvent.target; if (target.localName == "menupopup" || target.id == "bookmarks-ptf") target = target.parentNode; this.onDragRemoveFeedBack(target); this.onDragExitSetTimer(target, aDragSession); this.mCurrentDragOverTarget = null; this.mCurrentDropPosition = null; }, onDrop: function (aEvent, aXferData, aDragSession) { var target = aEvent.target; this.onDragRemoveFeedBack(target); var selection = BookmarksUtils.getSelectionFromXferData(aDragSession); var orientation = BookmarksMenu.getBTOrientation(aEvent); // For RTL PersonalBar bookmarks buttons, orientation should be inverted (only in drop case) // because "before" (to the left) on the screen translates to "after" in the collection of items. if (target.localName == "toolbarbutton") if (window.getComputedStyle(document.getElementById("PersonalToolbar"),'').direction == 'rtl') if (orientation == BookmarksUtils.DROP_AFTER) orientation = BookmarksUtils.DROP_BEFORE; else if (orientation == BookmarksUtils.DROP_BEFORE) orientation = BookmarksUtils.DROP_AFTER; var selTarget = BookmarksMenu.getBTTarget(target, orientation); const kDSIID = Components.interfaces.nsIDragService; const kCopyAction = kDSIID.DRAGDROP_ACTION_COPY + kDSIID.DRAGDROP_ACTION_LINK; // hide the 'open in tab' menuseparator because bookmarks // can be inserted after it if they are dropped after the last bookmark // a more comprehensive fix would be in the menupopup template builder var menuTarget = (target.localName == "toolbarbutton" || target.localName == "menu") && orientation == BookmarksUtils.DROP_ON? target.lastChild:target.parentNode; if (menuTarget.hasChildNodes() && menuTarget.lastChild.id == "openintabs-menuitem") { menuTarget.removeChild(menuTarget.lastChild.previousSibling); } if (aDragSession.dragAction & kCopyAction) BookmarksUtils.insertSelection("drag", selection, selTarget); else BookmarksUtils.moveSelection("drag", selection, selTarget); var chevron = document.getElementById("bookmarks-chevron"); if (chevron.getAttribute("open") == "true") { BookmarksToolbar.resizeFunc(null); BookmarksToolbar.updateOverflowMenu(document.getElementById("bookmarks-chevron-popup")); } // show again the menuseparator if (menuTarget.hasChildNodes() && menuTarget.lastChild.id == "openintabs-menuitem") { var element = document.createElementNS(XUL_NS, "menuseparator"); menuTarget.insertBefore(element, menuTarget.lastChild); } }, canDrop: function (aEvent, aDragSession) { var target = aEvent.target; // onDragStart calls this without a drag session // There will be no sourceNode for drags from external apps if (aDragSession && aDragSession.sourceNode) { var orientation = BookmarksMenu.getBTOrientation(aEvent, target); if (target == aDragSession.sourceNode || (target == aDragSession.sourceNode.previousSibling && orientation == BookmarksUtils.DROP_AFTER) || (target == aDragSession.sourceNode.nextSibling && orientation == BookmarksUtils.DROP_BEFORE)) return false; } return BookmarksMenu.isBTBookmark(target.id) && target.id != "NC:SystemBookmarksStaticRoot" && target.id.substring(0,5) != "find:" || target.id == "BookmarksMenu" || target.id == "bookmarks-button" || target.id == "bookmarks-chevron" || target.id == "bookmarks-ptf"; }, canHandleMultipleItems: true, getSupportedFlavours: function () { var flavourSet = new FlavourSet(); flavourSet.appendFlavour("moz/rdfitem"); flavourSet.appendFlavour("text/x-moz-url"); flavourSet.appendFlavour("application/x-moz-file", "nsIFile"); flavourSet.appendFlavour("text/unicode"); return flavourSet; }, //////////////////////////////////// // Private methods and properties // //////////////////////////////////// springLoadedMenuDelay: 350, // milliseconds isPlatformNotSupported: navigator.platform.indexOf("Mac") != -1, // see bug 136524 // Needs to be dynamically overridden (to |true|) in the case of an external drag: see bug 232795. isTimerSupported: navigator.platform.indexOf("Win") == -1, mCurrentDragOverTarget: null, mCurrentDropPosition: null, loadTimer : null, closeTimer : null, loadTarget : null, closeTarget: null, _observers : null, get mObservers () { if (!this._observers) { this._observers = [ document.getElementById("bookmarks-ptf"), document.getElementById("BookmarksMenu").parentNode, document.getElementById("bookmarks-chevron").parentNode, document.getElementById("PersonalToolbar") ] } return this._observers; }, getObserverForNode: function (aNode) { if (!aNode) return null; var node = aNode; var observer; do { for (var i=0; i < this.mObservers.length; i++) { observer = this.mObservers[i]; if (observer == node) return observer; } node = node.parentNode; } while (node != document) return null; }, onDragCloseMenu: function (aNode) { var children = aNode.childNodes; for (var i = 0; i < children.length; i++) { if (this.isContainer(children[i]) && children[i].getAttribute("open") == "true") { this.onDragCloseMenu(children[i].lastChild); if (children[i] != this.mCurrentDragOverTarget || this.mCurrentDropPosition != BookmarksUtils.DROP_ON) children[i].lastChild.hidePopup(); } } }, onDragCloseTarget: function () { var currentObserver = this.getObserverForNode(this.mCurrentDragOverTarget); // close all the menus not hovered by the mouse for (var i=0; i < this.mObservers.length; i++) { if (currentObserver != this.mObservers[i]) this.onDragCloseMenu(this.mObservers[i]); else this.onDragCloseMenu(this.mCurrentDragOverTarget.parentNode); } }, onDragLoadTarget: function (aTarget) { if (!this.mCurrentDragOverTarget) return; // Load the current menu if (this.mCurrentDropPosition == BookmarksUtils.DROP_ON && this.isContainer(aTarget) && aTarget.getAttribute("group") != "true") aTarget.lastChild.showPopup(aTarget); }, onDragOverCheckTimers: function () { var now = new Date().getTime(); if (this.closeTimer && now-this.springLoadedMenuDelay>this.closeTimer) { this.onDragCloseTarget(); this.closeTimer = null; } if (this.loadTimer && (now-this.springLoadedMenuDelay>this.loadTimer)) { this.onDragLoadTarget(this.loadTarget); this.loadTimer = null; } }, onDragEnterSetTimer: function (aTarget, aDragSession) { if (this.isPlatformNotSupported) return; if (this.isTimerSupported || !aDragSession.sourceNode) { var targetToBeLoaded = aTarget; clearTimeout(this.loadTimer); if (aTarget == aDragSession.sourceNode) return; var This = this; this.loadTimer=setTimeout(function () {This.onDragLoadTarget(targetToBeLoaded)}, This.springLoadedMenuDelay); } else { var now = new Date().getTime(); this.loadTimer = now; this.loadTarget = aTarget; } }, onDragExitSetTimer: function (aTarget, aDragSession) { if (this.isPlatformNotSupported) return; var This = this; if (this.isTimerSupported || !aDragSession.sourceNode) { clearTimeout(this.closeTimer) this.closeTimer=setTimeout(function () {This.onDragCloseTarget()}, This.springLoadedMenuDelay); } else { var now = new Date().getTime(); this.closeTimer = now; this.closeTarget = aTarget; this.loadTimer = null; // If the user isn't rearranging within the menu, close it // To do so, we exploit a Mac bug: timeout set during // drag and drop on Windows and Mac are fired only after that the drop is released. // timeouts will pile up, we may have a better approach but for the moment, this one // correctly close the menus after a drop/cancel outside the personal toolbar. // The if statement in the function has been introduced to deal with rare but reproducible // missing Exit events. if (aDragSession.sourceNode.localName != "menuitem" && aDragSession.sourceNode.localName != "menu") setTimeout(function () { if (This.mCurrentDragOverTarget) {This.onDragRemoveFeedBack(This.mCurrentDragOverTarget); This.mCurrentDragOverTarget=null} This.loadTimer=null; This.onDragCloseTarget() }, 0); } }, onDragSetFeedBack: function (aTarget, aOrientation) { switch (aTarget.localName) { case "toolbarseparator": case "toolbarbutton": switch (aOrientation) { case BookmarksUtils.DROP_BEFORE: aTarget.setAttribute("dragover-left", "true"); break; case BookmarksUtils.DROP_AFTER: aTarget.setAttribute("dragover-right", "true"); break; case BookmarksUtils.DROP_ON: aTarget.setAttribute("dragover-top" , "true"); aTarget.setAttribute("dragover-bottom", "true"); aTarget.setAttribute("dragover-left" , "true"); aTarget.setAttribute("dragover-right" , "true"); break; } break; case "menuseparator": case "menu": case "menuitem": switch (aOrientation) { case BookmarksUtils.DROP_BEFORE: aTarget.setAttribute("dragover-top", "true"); break; case BookmarksUtils.DROP_AFTER: aTarget.setAttribute("dragover-bottom", "true"); break; case BookmarksUtils.DROP_ON: break; } break; case "toolbar": var newTarget = BookmarksToolbar.getLastVisibleBookmark(); if (newTarget) newTarget.setAttribute("dragover-right", "true"); break; case "hbox": case "menupopup": break; default: dump("No feedback for: "+aTarget.localName+"\n"); } }, onDragRemoveFeedBack: function (aTarget) { var newTarget; var bt; if (aTarget.id == "PersonalToolbar" || aTarget.id == "bookmarks-ptf") { newTarget = BookmarksToolbar.getLastVisibleBookmark(); if (newTarget) newTarget.removeAttribute("dragover-right"); } else { aTarget.removeAttribute("dragover-left"); aTarget.removeAttribute("dragover-right"); aTarget.removeAttribute("dragover-top"); aTarget.removeAttribute("dragover-bottom"); } }, onDropSetFeedBack: function (aTarget) { //XXX Not yet... }, isContainer: function (aTarget) { return aTarget.localName == "menu" || aTarget.localName == "toolbarbutton" && aTarget.getAttribute("type") == "menu"; } } var BookmarksToolbar = { ///////////////////////////////////////////////////////////////////////////// // make bookmarks toolbar act like menus openMenuButton: null, autoOpenMenu: function (aTarget) { if (this.openMenuButton && this.openMenuButton != aTarget && aTarget.localName == "toolbarbutton" && (aTarget.type == "menu" || aTarget.type == "menu-button")) { this.openMenuButton.open = false; aTarget.open = true; } }, onMenuOpen: function (aTarget) { if (aTarget.parentNode.localName == "toolbarbutton") this.openMenuButton = aTarget.parentNode; }, onMenuClose: function (aTarget) { if (aTarget.parentNode.localName == "toolbarbutton") this.openMenuButton = null; }, ///////////////////////////////////////////////////////////////////////////// // returns the node of the last visible bookmark on the toolbar --> getLastVisibleBookmark: function () { var buttons = document.getElementById("bookmarks-ptf"); var button = buttons.firstChild; if (!button) return null; // empty bookmarks toolbar do { if (button.collapsed) return button.previousSibling; button = button.nextSibling; } while (button) return buttons.lastChild; }, updateOverflowMenu: function (aMenuPopup) { var hbox = document.getElementById("bookmarks-ptf"); for (var i = 0; i < hbox.childNodes.length; i++) { var button = hbox.childNodes[i]; var menu = aMenuPopup.childNodes[i]; if (menu.hidden == button.collapsed) menu.hidden = !menu.hidden; } }, resizeFunc: function(event) { if (!event) // timer callback case BookmarksToolbarRDFObserver._overflowTimerInEffect = false; // XXXcst - work around bug 295340 (broken event targets) on 1.8 branch //else if (event.target != document) // return; // only interested in chrome resizes var buttons = document.getElementById("bookmarks-ptf"); if (!buttons) return; var chevron = document.getElementById("bookmarks-chevron"); if (!buttons.firstChild) { // No bookmarks means no chevron chevron.collapsed = true; return; } chevron.collapsed = false; var chevronWidth = chevron.boxObject.width; chevron.collapsed = true; var remainingWidth = buttons.boxObject.width; var overflowed = false; for (var i=0; i