<?xml version="1.0"?>

<!-- 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/. -->

<bindings id="placesMenuBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xbl="http://www.mozilla.org/xbl"
          xmlns:html="http://www.w3.org/1999/xhtml"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

  <binding id="places-popup-base"
           extends="chrome://global/content/bindings/popup.xml#popup">
    <content>
      <xul:hbox flex="1">
        <xul:vbox class="menupopup-drop-indicator-bar" hidden="true">
          <xul:image class="menupopup-drop-indicator" mousethrough="always"/>
        </xul:vbox>
        <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
                            smoothscroll="false">
          <children/>
        </xul:arrowscrollbox>
      </xul:hbox>
    </content>

    <implementation>

      <field name="AppConstants" readonly="true">
        (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
      </field>

      <field name="_indicatorBar">
        document.getAnonymousElementByAttribute(this, "class",
                                                "menupopup-drop-indicator-bar");
      </field>

      <field name="_scrollBox">
        document.getAnonymousElementByAttribute(this, "class",
                                                "popup-internal-box");
      </field>

      <!-- This is the view that manage the popup -->
      <field name="_rootView">PlacesUIUtils.getViewForNode(this);</field>

      <!-- Check if we should hide the drop indicator for the target -->
      <method name="_hideDropIndicator">
        <parameter name="aEvent"/>
        <body><![CDATA[
          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);
        ]]></body>
      </method>

      <!-- This function returns information about where to drop when
           dragging over this popup insertion point -->
      <method name="_getDropPoint">
        <parameter name="aEvent"/>
          <body><![CDATA[
            // Can't drop if the menu isn't a folder
            let resultNode = this._placesNode;

            if (!PlacesUtils.nodeIsFolder(resultNode) ||
                PlacesControllerDragHelper.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;

            // Calculate positions taking care of arrowscrollbox
            let scrollbox = this._scrollBox;
            let eventY = aEvent.layerY + (scrollbox.boxObject.y - this.boxObject.y);
            let scrollboxOffset = scrollbox.scrollBoxObject.y -
                                  (scrollbox.boxObject.y - this.boxObject.y);
            let eltY = elt.boxObject.y - scrollboxOffset;
            let eltHeight = elt.boxObject.height;

            if (!elt._placesNode) {
              // If we are dragging over a non places node drop at the end.
              dropPoint.ip = new InsertionPoint(
                                  PlacesUtils.getConcreteItemId(resultNode),
                                  -1,
                                  Ci.nsITreeView.DROP_ON);
              // 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.lastChild &&
                  elt.lastChild.hasAttribute("placespopup"))
                dropPoint.folderElt = elt;
              return dropPoint;
            }

            let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode) ?
                            elt._placesNode.title : null;
            if ((PlacesUtils.nodeIsFolder(elt._placesNode) &&
                 !PlacesUIUtils.isContentsReadOnly(elt._placesNode)) ||
                PlacesUtils.nodeIsTagQuery(elt._placesNode)) {
              // This is a folder or a tag container.
              if (eventY - eltY < eltHeight * 0.20) {
                // If mouse is in the top part of the element, drop above folder.
                dropPoint.ip = new InsertionPoint(
                                    PlacesUtils.getConcreteItemId(resultNode),
                                    -1,
                                    Ci.nsITreeView.DROP_BEFORE,
                                    tagName,
                                    elt._placesNode.itemId);
                return dropPoint;
              }
              else if (eventY - eltY < eltHeight * 0.80) {
                // If mouse is in the middle of the element, drop inside folder.
                dropPoint.ip = new InsertionPoint(
                                    PlacesUtils.getConcreteItemId(elt._placesNode),
                                    -1,
                                    Ci.nsITreeView.DROP_ON,
                                    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 InsertionPoint(
                                  PlacesUtils.getConcreteItemId(resultNode),
                                  -1,
                                  Ci.nsITreeView.DROP_BEFORE,
                                  tagName,
                                  elt._placesNode.itemId);
              return dropPoint;
            }

            // Drop below the item.
            dropPoint.ip = new InsertionPoint(
                                PlacesUtils.getConcreteItemId(resultNode),
                                -1,
                                Ci.nsITreeView.DROP_AFTER,
                                tagName,
                                elt._placesNode.itemId);
            return dropPoint;
        ]]></body>
      </method>

      <!-- 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. -->
      <field name="_overFolder"><![CDATA[({
        _self: this,
        _folder: {elt: null,
                  openTimer: null,
                  hoverTime: 350,
                  closeTimer: null},
        _closeMenuTimer: null,

        get elt() {
          return this._folder.elt;
        },
        set elt(val) {
          return this._folder.elt = val;
        },

        get openTimer() {
          return this._folder.openTimer;
        },
        set openTimer(val) {
          return this._folder.openTimer = val;
        },

        get hoverTime() {
          return this._folder.hoverTime;
        },
        set hoverTime(val) {
          return this._folder.hoverTime = val;
        },

        get closeTimer() {
          return this._folder.closeTimer;
        },
        set closeTimer(val) {
          return this._folder.closeTimer = val;
        },

        get closeMenuTimer() {
          return this._closeMenuTimer;
        },
        set closeMenuTimer(val) {
          return 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.lastChild.setAttribute("autoopened", "true");
            this._folder.elt.lastChild.showPopup(this._folder.elt);
            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.lastChild) {
            if (!this._folder.elt.lastChild.hasAttribute("dragover"))
              this._folder.elt.lastChild.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;
          }
        }
      })]]></field>

      <method name="_cleanupDragDetails">
        <body><![CDATA[
          // Called on dragend and drop.
          PlacesControllerDragHelper.currentDropTarget = null;
          this._rootView._draggedElt = null;
          this.removeAttribute("dragover");
          this.removeAttribute("dragstart");
          this._indicatorBar.hidden = true;
        ]]></body>
      </method>

    </implementation>

    <handlers>
      <handler event="DOMMenuItemActive"><![CDATA[
        let elt = event.target;
        if (elt.parentNode != this)
          return;

        if (this.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 elt = event.target;
          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, null);
        }
      ]]></handler>

      <handler event="DOMMenuItemInactive"><![CDATA[
        let elt = event.target;
        if (elt.parentNode != this)
          return;

        if (window.XULBrowserWindow)
          window.XULBrowserWindow.setOverLink("", null);
      ]]></handler>

      <handler event="dragstart"><![CDATA[
        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 (!PlacesControllerDragHelper.canMoveNode(draggedElt, elt))
          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();
      ]]></handler>

      <handler event="drop"><![CDATA[
        PlacesControllerDragHelper.currentDropTarget = event.target;

        let dropPoint = this._getDropPoint(event);
        if (dropPoint && dropPoint.ip) {
          PlacesControllerDragHelper.onDrop(dropPoint.ip, event.dataTransfer)
                                    .then(null, Components.utils.reportError);
          event.preventDefault();
        }

        this._cleanupDragDetails();
        event.stopPropagation();
      ]]></handler>

      <handler event="dragover"><![CDATA[
        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 anonid = event.originalTarget.getAttribute('anonid');
        let scrollDir = 0;
        if (anonid == "scrollbutton-up") {
          scrollDir = -1;
        } else if (anonid == "scrollbutton-down") {
          scrollDir = 1;
        }
        if (scrollDir != 0) {
          this._scrollBox.scrollByIndex(scrollDir, false);
        }

        // 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 sbo = this._scrollBox.scrollBoxObject;
        let newMarginTop = 0;
        if (scrollDir == 0) {
          let elt = this.firstChild;
          while (elt && event.screenY > elt.boxObject.screenY +
                                        elt.boxObject.height / 2)
            elt = elt.nextSibling;
          newMarginTop = elt ? elt.boxObject.screenY - sbo.screenY :
                               sbo.height;
        }
        else if (scrollDir == 1)
          newMarginTop = sbo.height;

        // Set the new marginTop based on arrowscrollbox.
        newMarginTop += sbo.y - this._scrollBox.boxObject.y;
        this._indicatorBar.firstChild.style.marginTop = newMarginTop + "px";
        this._indicatorBar.hidden = false;

        event.preventDefault();
        event.stopPropagation();
      ]]></handler>

      <handler event="dragexit"><![CDATA[
        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();
      ]]></handler>

      <handler event="dragend"><![CDATA[
        this._cleanupDragDetails();
      ]]></handler>

    </handlers>
  </binding>

  <!-- Most of this is copied from the arrowpanel binding in popup.xml -->
  <binding id="places-popup-arrow"
           extends="chrome://browser/content/places/menu.xml#places-popup-base">
    <content flip="both" side="top" position="bottomcenter topright">
      <xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
               xbl:inherits="side,panelopen">
        <xul:box anonid="arrowbox" class="panel-arrowbox">
          <xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
        </xul:box>
        <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1">
          <xul:vbox class="menupopup-drop-indicator-bar" hidden="true">
            <xul:image class="menupopup-drop-indicator" mousethrough="always"/>
          </xul:vbox>
          <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
                              smoothscroll="false">
            <children/>
          </xul:arrowscrollbox>
        </xul:box>
      </xul:vbox>
    </content>

    <implementation>
      <constructor><![CDATA[
        this.style.pointerEvents = 'none';
      ]]></constructor>
      <method name="adjustArrowPosition">
        <body><![CDATA[
          var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");

          var anchor = this.anchorNode;
          if (!anchor) {
            arrow.hidden = true;
            return;
          }

          var container = document.getAnonymousElementByAttribute(this, "anonid", "container");
          var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox");

          var position = this.alignmentPosition;
          var offset = this.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.orient = "horizontal";
            arrowbox.orient = "vertical";
            if (position.indexOf("_after") > 0) {
              arrowbox.pack = "end";
            } else {
              arrowbox.pack = "start";
            }
            arrowbox.style.transform = "translate(0, " + -offset + "px)";

            // The assigned side stays the same regardless of direction.
            var isRTL = (window.getComputedStyle(this).direction == "rtl");

            if (position.indexOf("start_") == 0) {
              container.dir = "reverse";
              this.setAttribute("side", isRTL ? "left" : "right");
            }
            else {
              container.dir = "";
              this.setAttribute("side", isRTL ? "right" : "left");
            }
          }
          else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
            container.orient = "";
            arrowbox.orient = "";
            if (position.indexOf("_end") > 0) {
              arrowbox.pack = "end";
            } else {
              arrowbox.pack = "start";
            }
            arrowbox.style.transform = "translate(" + -offset + "px, 0)";

            if (position.indexOf("before_") == 0) {
              container.dir = "reverse";
              this.setAttribute("side", "bottom");
            }
            else {
              container.dir = "";
              this.setAttribute("side", "top");
            }
          }

          arrow.hidden = false;
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="popupshowing" phase="target"><![CDATA[
        this.adjustArrowPosition();
        this.setAttribute("animate", "open");
      ]]></handler>
      <handler event="popupshown" phase="target"><![CDATA[
        this.setAttribute("panelopen", "true");
        let disablePointerEvents;
        if (!this.hasAttribute("disablepointereventsfortransition")) {
          let container = document.getAnonymousElementByAttribute(this, "anonid", "container");
          let cs = getComputedStyle(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");
        }
      ]]></handler>
      <handler event="transitionend"><![CDATA[
        if (event.originalTarget.getAttribute("anonid") == "container" &&
            event.propertyName == "transform") {
          this.style.removeProperty("pointer-events");
        }
      ]]></handler>
      <handler event="popuphiding" phase="target"><![CDATA[
        this.setAttribute("animate", "cancel");
      ]]></handler>
      <handler event="popuphidden" phase="target"><![CDATA[
        this.removeAttribute("panelopen");
        if (this.getAttribute("disablepointereventsfortransition") == "true") {
          this.style.pointerEvents = 'none';
        }
        this.removeAttribute("animate");
      ]]></handler>
    </handlers>
  </binding>
</bindings>
