diff options
Diffstat (limited to 'browser/components/places/content/places-menupopup.js')
-rw-r--r-- | browser/components/places/content/places-menupopup.js | 771 |
1 files changed, 771 insertions, 0 deletions
diff --git a/browser/components/places/content/places-menupopup.js b/browser/components/places/content/places-menupopup.js new file mode 100644 index 0000000000..8915384e3a --- /dev/null +++ b/browser/components/places/content/places-menupopup.js @@ -0,0 +1,771 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* eslint-env mozilla/browser-window */ +/* import-globals-from controller.js */ + +// This is loaded into all XUL windows. Wrap in a block to prevent +// leaking to window scope. +{ + const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + + class MozPlacesPopup extends MozElements.MozMenuPopup { + constructor() { + super(); + + const event_names = [ + "DOMMenuItemActive", + "DOMMenuItemInactive", + "dragstart", + "drop", + "dragover", + "dragexit", + "dragend", + ]; + for (let event_name of event_names) { + this.addEventListener(event_name, ev => this[`on_${event_name}`](ev)); + } + } + + get markup() { + return ` + <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> + <hbox flex="1" part="innerbox"> + <vbox part="drop-indicator-bar" hidden="true"> + <image part="drop-indicator"/> + </vbox> + <arrowscrollbox class="menupopup-arrowscrollbox" flex="1" orient="vertical" + exportparts="scrollbox: arrowscrollbox-scrollbox" + smoothscroll="false" part="arrowscrollbox"> + <html:slot></html:slot> + </arrowscrollbox> + </hbox> + `; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + /** + * Sub-menus should be opened when the mouse drags over them, and closed + * when the mouse drags off. The overFolder object manages opening and + * closing of folders when the mouse hovers. + */ + this._overFolder = { + _self: this, + _folder: { + elt: null, + openTimer: null, + hoverTime: 350, + closeTimer: null, + }, + _closeMenuTimer: null, + + get elt() { + return this._folder.elt; + }, + set elt(val) { + this._folder.elt = val; + }, + + get openTimer() { + return this._folder.openTimer; + }, + set openTimer(val) { + this._folder.openTimer = val; + }, + + get hoverTime() { + return this._folder.hoverTime; + }, + set hoverTime(val) { + this._folder.hoverTime = val; + }, + + get closeTimer() { + return this._folder.closeTimer; + }, + set closeTimer(val) { + this._folder.closeTimer = val; + }, + + get closeMenuTimer() { + return this._closeMenuTimer; + }, + set closeMenuTimer(val) { + this._closeMenuTimer = val; + }, + + setTimer: function OF__setTimer(aTime) { + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT); + return timer; + }, + + notify: function OF__notify(aTimer) { + // Function to process all timer notifications. + + if (aTimer == this._folder.openTimer) { + // Timer to open a submenu that's being dragged over. + this._folder.elt.lastElementChild.setAttribute( + "autoopened", + "true" + ); + this._folder.elt.lastElementChild.openPopup(); + this._folder.openTimer = null; + } else if (aTimer == this._folder.closeTimer) { + // Timer to close a submenu that's been dragged off of. + // Only close the submenu if the mouse isn't being dragged over any + // of its child menus. + var draggingOverChild = PlacesControllerDragHelper.draggingOverChildNode( + this._folder.elt + ); + if (draggingOverChild) { + this._folder.elt = null; + } + this.clear(); + + // Close any parent folders which aren't being dragged over. + // (This is necessary because of the above code that keeps a folder + // open while its children are being dragged over.) + if (!draggingOverChild) { + this.closeParentMenus(); + } + } else if (aTimer == this.closeMenuTimer) { + // Timer to close this menu after the drag exit. + var popup = this._self; + // if we are no more dragging we can leave the menu open to allow + // for better D&D bookmark organization + if ( + PlacesControllerDragHelper.getSession() && + !PlacesControllerDragHelper.draggingOverChildNode( + popup.parentNode + ) + ) { + popup.hidePopup(); + // Close any parent menus that aren't being dragged over; + // otherwise they'll stay open because they couldn't close + // while this menu was being dragged over. + this.closeParentMenus(); + } + this._closeMenuTimer = null; + } + }, + + // Helper function to close all parent menus of this menu, + // as long as none of the parent's children are currently being + // dragged over. + closeParentMenus: function OF__closeParentMenus() { + var popup = this._self; + var parent = popup.parentNode; + while (parent) { + if (parent.localName == "menupopup" && parent._placesNode) { + if ( + PlacesControllerDragHelper.draggingOverChildNode( + parent.parentNode + ) + ) { + break; + } + parent.hidePopup(); + } + parent = parent.parentNode; + } + }, + + // The mouse is no longer dragging over the stored menubutton. + // Close the menubutton, clear out drag styles, and clear all + // timers for opening/closing it. + clear: function OF__clear() { + if (this._folder.elt && this._folder.elt.lastElementChild) { + if (!this._folder.elt.lastElementChild.hasAttribute("dragover")) { + this._folder.elt.lastElementChild.hidePopup(); + } + // remove menuactive style + this._folder.elt.removeAttribute("_moz-menuactive"); + this._folder.elt = null; + } + if (this._folder.openTimer) { + this._folder.openTimer.cancel(); + this._folder.openTimer = null; + } + if (this._folder.closeTimer) { + this._folder.closeTimer.cancel(); + this._folder.closeTimer = null; + } + }, + }; + } + + get _indicatorBar() { + if (!this.__indicatorBar) { + this.__indicatorBar = this.shadowRoot.querySelector( + "[part=drop-indicator-bar]" + ); + } + return this.__indicatorBar; + } + + /** + * This is the view that manages the popup. + */ + get _rootView() { + if (!this.__rootView) { + this.__rootView = PlacesUIUtils.getViewForNode(this); + } + return this.__rootView; + } + + /** + * Check if we should hide the drop indicator for the target + */ + _hideDropIndicator(aEvent) { + let target = aEvent.target; + + // Don't draw the drop indicator outside of markers or if current + // node is not a Places node. + let betweenMarkers = + this._startMarker.compareDocumentPosition(target) & + Node.DOCUMENT_POSITION_FOLLOWING && + this._endMarker.compareDocumentPosition(target) & + Node.DOCUMENT_POSITION_PRECEDING; + + // Hide the dropmarker if current node is not a Places node. + return !(target && target._placesNode && betweenMarkers); + } + + /** + * This function returns information about where to drop when + * dragging over this popup insertion point + */ + _getDropPoint(aEvent) { + // Can't drop if the menu isn't a folder + let resultNode = this._placesNode; + + if ( + !PlacesUtils.nodeIsFolder(resultNode) || + this._rootView.controller.disallowInsertion(resultNode) + ) { + return null; + } + + var dropPoint = { ip: null, folderElt: null }; + + // The element we are dragging over + let elt = aEvent.target; + if (elt.localName == "menupopup") { + elt = elt.parentNode; + } + + let eventY = aEvent.clientY; + let { y: eltY, height: eltHeight } = elt.getBoundingClientRect(); + + if (!elt._placesNode) { + // If we are dragging over a non places node drop at the end. + dropPoint.ip = new PlacesInsertionPoint({ + parentId: PlacesUtils.getConcreteItemId(resultNode), + parentGuid: PlacesUtils.getConcreteItemGuid(resultNode), + }); + // We can set folderElt if we are dropping over a static menu that + // has an internal placespopup. + let isMenu = + elt.localName == "menu" || + (elt.localName == "toolbarbutton" && + elt.getAttribute("type") == "menu"); + if ( + isMenu && + elt.lastElementChild && + elt.lastElementChild.hasAttribute("placespopup") + ) { + dropPoint.folderElt = elt; + } + return dropPoint; + } + + let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) + ? elt._placesNode.title + : null; + if ( + (PlacesUtils.nodeIsFolder(elt._placesNode) && + !PlacesUIUtils.isFolderReadOnly(elt._placesNode)) || + PlacesUtils.nodeIsTagQuery(elt._placesNode) + ) { + // This is a folder or a tag container. + if (eventY - eltY < eltHeight * 0.2) { + // If mouse is in the top part of the element, drop above folder. + dropPoint.ip = new PlacesInsertionPoint({ + parentId: PlacesUtils.getConcreteItemId(resultNode), + parentGuid: PlacesUtils.getConcreteItemGuid(resultNode), + orientation: Ci.nsITreeView.DROP_BEFORE, + tagName, + dropNearNode: elt._placesNode, + }); + return dropPoint; + } else if (eventY - eltY < eltHeight * 0.8) { + // If mouse is in the middle of the element, drop inside folder. + dropPoint.ip = new PlacesInsertionPoint({ + parentId: PlacesUtils.getConcreteItemId(elt._placesNode), + parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode), + tagName, + }); + dropPoint.folderElt = elt; + return dropPoint; + } + } else if (eventY - eltY <= eltHeight / 2) { + // This is a non-folder node or a readonly folder. + // If the mouse is above the middle, drop above this item. + dropPoint.ip = new PlacesInsertionPoint({ + parentId: PlacesUtils.getConcreteItemId(resultNode), + parentGuid: PlacesUtils.getConcreteItemGuid(resultNode), + orientation: Ci.nsITreeView.DROP_BEFORE, + tagName, + dropNearNode: elt._placesNode, + }); + return dropPoint; + } + + // Drop below the item. + dropPoint.ip = new PlacesInsertionPoint({ + parentId: PlacesUtils.getConcreteItemId(resultNode), + parentGuid: PlacesUtils.getConcreteItemGuid(resultNode), + orientation: Ci.nsITreeView.DROP_AFTER, + tagName, + dropNearNode: elt._placesNode, + }); + return dropPoint; + } + + _cleanupDragDetails() { + // Called on dragend and drop. + PlacesControllerDragHelper.currentDropTarget = null; + this._rootView._draggedElt = null; + this.removeAttribute("dragover"); + this.removeAttribute("dragstart"); + this._indicatorBar.hidden = true; + } + + on_DOMMenuItemActive(event) { + let elt = event.target; + if (elt.parentNode != this) { + return; + } + + if (AppConstants.platform === "macosx") { + // XXX: The following check is a temporary hack until bug 420033 is + // resolved. + let parentElt = elt.parent; + while (parentElt) { + if ( + parentElt.id == "bookmarksMenuPopup" || + parentElt.id == "goPopup" + ) { + return; + } + parentElt = parentElt.parentNode; + } + } + + if (window.XULBrowserWindow) { + let placesNode = elt._placesNode; + + var linkURI; + if (placesNode && PlacesUtils.nodeIsURI(placesNode)) { + linkURI = placesNode.uri; + } else if (elt.hasAttribute("targetURI")) { + linkURI = elt.getAttribute("targetURI"); + } + + if (linkURI) { + window.XULBrowserWindow.setOverLink(linkURI); + } + } + } + + on_DOMMenuItemInactive(event) { + let elt = event.target; + if (elt.parentNode != this) { + return; + } + + if (window.XULBrowserWindow) { + window.XULBrowserWindow.setOverLink(""); + } + } + + on_dragstart(event) { + let elt = event.target; + if (!elt._placesNode) { + return; + } + + let draggedElt = elt._placesNode; + + // Force a copy action if parent node is a query or we are dragging a + // not-removable node. + if (!this._rootView.controller.canMoveNode(draggedElt)) { + event.dataTransfer.effectAllowed = "copyLink"; + } + + // Activate the view and cache the dragged element. + this._rootView._draggedElt = draggedElt; + this._rootView.controller.setDataTransfer(event); + this.setAttribute("dragstart", "true"); + event.stopPropagation(); + } + + on_drop(event) { + PlacesControllerDragHelper.currentDropTarget = event.target; + + let dropPoint = this._getDropPoint(event); + if (dropPoint && dropPoint.ip) { + PlacesControllerDragHelper.onDrop( + dropPoint.ip, + event.dataTransfer + ).catch(Cu.reportError); + event.preventDefault(); + } + + this._cleanupDragDetails(); + event.stopPropagation(); + } + + on_dragover(event) { + PlacesControllerDragHelper.currentDropTarget = event.target; + let dt = event.dataTransfer; + + let dropPoint = this._getDropPoint(event); + if ( + !dropPoint || + !dropPoint.ip || + !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt) + ) { + this._indicatorBar.hidden = true; + event.stopPropagation(); + return; + } + + // Mark this popup as being dragged over. + this.setAttribute("dragover", "true"); + + if (dropPoint.folderElt) { + // We are dragging over a folder. + // _overFolder should take the care of opening it on a timer. + if ( + this._overFolder.elt && + this._overFolder.elt != dropPoint.folderElt + ) { + // We are dragging over a new folder, let's clear old values + this._overFolder.clear(); + } + if (!this._overFolder.elt) { + this._overFolder.elt = dropPoint.folderElt; + // Create the timer to open this folder. + this._overFolder.openTimer = this._overFolder.setTimer( + this._overFolder.hoverTime + ); + } + // Since we are dropping into a folder set the corresponding style. + dropPoint.folderElt.setAttribute("_moz-menuactive", true); + } else { + // We are not dragging over a folder. + // Clear out old _overFolder information. + this._overFolder.clear(); + } + + // Autoscroll the popup strip if we drag over the scroll buttons. + let scrollDir = 0; + if (event.originalTarget == this.scrollBox._scrollButtonUp) { + scrollDir = -1; + } else if (event.originalTarget == this.scrollBox._scrollButtonDown) { + scrollDir = 1; + } + if (scrollDir != 0) { + this.scrollBox.scrollByIndex(scrollDir, true); + } + + // Check if we should hide the drop indicator for this target. + if (dropPoint.folderElt || this._hideDropIndicator(event)) { + this._indicatorBar.hidden = true; + event.preventDefault(); + event.stopPropagation(); + return; + } + + // We should display the drop indicator relative to the arrowscrollbox. + let scrollRect = this.scrollBox.getBoundingClientRect(); + let newMarginTop = 0; + if (scrollDir == 0) { + let elt = this.firstElementChild; + while ( + elt && + event.screenY > elt.screenY + elt.getBoundingClientRect().height / 2 + ) { + elt = elt.nextElementSibling; + } + newMarginTop = elt + ? elt.screenY - this.scrollBox.screenY + : scrollRect.height; + } else if (scrollDir == 1) { + newMarginTop = scrollRect.height; + } + + // Set the new marginTop based on arrowscrollbox. + newMarginTop += scrollRect.y - this.scrollBox.getBoundingClientRect().y; + this._indicatorBar.firstElementChild.style.marginTop = + newMarginTop + "px"; + this._indicatorBar.hidden = false; + + event.preventDefault(); + event.stopPropagation(); + } + + on_dragexit(event) { + PlacesControllerDragHelper.currentDropTarget = null; + this.removeAttribute("dragover"); + + // If we have not moved to a valid new target clear the drop indicator + // this happens when moving out of the popup. + let target = event.relatedTarget; + if (!target || !this.contains(target)) { + this._indicatorBar.hidden = true; + } + + // Close any folder being hovered over + if (this._overFolder.elt) { + this._overFolder.closeTimer = this._overFolder.setTimer( + this._overFolder.hoverTime + ); + } + + // The autoopened attribute is set when this folder was automatically + // opened after the user dragged over it. If this attribute is set, + // auto-close the folder on drag exit. + // We should also try to close this popup if the drag has started + // from here, the timer will check if we are dragging over a child. + if (this.hasAttribute("autoopened") || this.hasAttribute("dragstart")) { + this._overFolder.closeMenuTimer = this._overFolder.setTimer( + this._overFolder.hoverTime + ); + } + + event.stopPropagation(); + } + + on_dragend(event) { + this._cleanupDragDetails(); + } + } + + customElements.define("places-popup", MozPlacesPopup, { + extends: "menupopup", + }); + + class MozPlacesPopupArrow extends MozPlacesPopup { + constructor() { + super(); + + const event_names = [ + "popupshowing", + "popuppositioned", + "popupshown", + "transitionend", + "popuphiding", + "popuphidden", + ]; + for (let event_name of event_names) { + this.addEventListener(event_name, ev => this[`on_${event_name}`](ev)); + } + } + + static get inheritedAttributes() { + return { + ".panel-arrowcontent": "align,dir,orient,pack", + }; + } + + get markup() { + return ` + <html:link rel="stylesheet" href="chrome://global/skin/global.css"/> + <vbox class="panel-arrowcontainer" flex="1"> + <box class="panel-arrowbox" part="arrowbox"> + <image class="panel-arrow" part="arrow"/> + </box> + <box class="panel-arrowcontent" part="arrowcontent" flex="1"> + <vbox part="drop-indicator-bar" hidden="true"> + <image part="drop-indicator"/> + </vbox> + <arrowscrollbox class="menupopup-arrowscrollbox" flex="1" + orient="vertical" smoothscroll="false" + part="arrowscrollbox"> + <html:slot/> + </arrowscrollbox> + </box> + </vbox> + `; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + super.connectedCallback(); + this.initializeAttributeInheritance(); + + this.setAttribute("flip", "both"); + this.setAttribute("side", "top"); + this.setAttribute("position", "bottomcenter topright"); + this.style.pointerEvents = "none"; + } + + get container() { + return this.shadowRoot.querySelector(".panel-arrowcontainer"); + } + get arrowbox() { + return this.shadowRoot.querySelector(".panel-arrowbox"); + } + get arrow() { + return this.shadowRoot.querySelector(".panel-arrow"); + } + + adjustArrowPosition(event) { + let arrow = this.arrow; + + let anchor = this.anchorNode; + if (!anchor) { + arrow.hidden = true; + return; + } + + let container = this.container; + let arrowbox = this.arrowbox; + + var position = event.alignmentPosition; + var offset = event.alignmentOffset; + + this.setAttribute("arrowposition", position); + + // if this panel has a "sliding" arrow, we may have previously set margins... + arrowbox.style.removeProperty("transform"); + if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) { + container.setAttribute("orient", "horizontal"); + arrowbox.setAttribute("orient", "vertical"); + if (position.indexOf("_after") > 0) { + arrowbox.setAttribute("pack", "end"); + } else { + arrowbox.setAttribute("pack", "start"); + } + arrowbox.style.transform = "translate(0, " + -offset + "px)"; + + // The assigned side stays the same regardless of direction. + let isRTL = this.matches(":-moz-locale-dir(rtl)"); + + if (position.indexOf("start_") == 0) { + container.style.MozBoxDirection = "reverse"; + this.setAttribute("side", isRTL ? "left" : "right"); + } else { + container.style.removeProperty("-moz-box-direction"); + this.setAttribute("side", isRTL ? "right" : "left"); + } + } else if ( + position.indexOf("before_") == 0 || + position.indexOf("after_") == 0 + ) { + container.removeAttribute("orient"); + arrowbox.removeAttribute("orient"); + if (position.indexOf("_end") > 0) { + arrowbox.setAttribute("pack", "end"); + } else { + arrowbox.setAttribute("pack", "start"); + } + arrowbox.style.transform = "translate(" + -offset + "px, 0)"; + + if (position.indexOf("before_") == 0) { + container.style.MozBoxDirection = "reverse"; + this.setAttribute("side", "bottom"); + } else { + container.style.removeProperty("-moz-box-direction"); + this.setAttribute("side", "top"); + } + } + + arrow.hidden = false; + } + + on_popupshowing(event) { + if (event.target == this) { + this.setAttribute("animate", "open"); + } + } + + on_popuppositioned(event) { + if (event.target == this) { + this.adjustArrowPosition(event); + } + } + + on_popupshown(event) { + if (event.target != this) { + return; + } + + this.setAttribute("panelopen", "true"); + let disablePointerEvents; + if (!this.hasAttribute("disablepointereventsfortransition")) { + let cs = getComputedStyle(this.container); + let transitionProp = cs.transitionProperty; + let transitionTime = parseFloat(cs.transitionDuration); + disablePointerEvents = + (transitionProp.includes("transform") || transitionProp == "all") && + transitionTime > 0; + this.setAttribute( + "disablepointereventsfortransition", + disablePointerEvents + ); + } else { + disablePointerEvents = + this.getAttribute("disablepointereventsfortransition") == "true"; + } + if (!disablePointerEvents) { + this.style.removeProperty("pointer-events"); + } + } + + on_transitionend(event) { + if ( + event.originalTarget.classList.contains("panel-arrowcontainer") && + (event.propertyName == "transform" || + event.propertyName == "-moz-window-transform") + ) { + this.style.removeProperty("pointer-events"); + } + } + + on_popuphiding(event) { + if (event.target == this) { + this.setAttribute("animate", "cancel"); + } + } + + on_popuphidden(event) { + if (event.target == this) { + this.removeAttribute("panelopen"); + if (this.getAttribute("disablepointereventsfortransition") == "true") { + this.style.pointerEvents = "none"; + } + this.removeAttribute("animate"); + } + } + } + + customElements.define("places-popup-arrow", MozPlacesPopupArrow, { + extends: "menupopup", + }); +} |