toolkit/content/widgets/popup.xml
author Johann Hofmann <jhofmann@mozilla.com>
Thu, 20 Jul 2017 16:45:02 +0200
changeset 418868 ee312146ee4ec55ac6829edcf5826c99079ee7cc
parent 391339 6055065a2ed5bbfa0dafb62d13d62e33e2742eab
child 419015 49c4c4a3fb814d3efe668dbbc276920e2e1d6cf1
permissions -rw-r--r--
Bug 1371219 - Increase margin of main menu items when accessed through touch. r=mikedeboer MozReview-Commit-ID: 9gjxxc6O5YS

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

<!-- This files relies on these specific Chrome/XBL globals -->
<!-- globals PopupBoxObject -->

<bindings id="popupBindings"
   xmlns="http://www.mozilla.org/xbl"
   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="popup-base">
    <resources>
      <stylesheet src="chrome://global/skin/popup.css"/>
    </resources>

    <implementation implements="nsIDOMXULPopupElement">
      <property name="label" onget="return this.getAttribute('label');"
                             onset="this.setAttribute('label', val); return val;"/>
      <property name="position" onget="return this.getAttribute('position');"
                                onset="this.setAttribute('position', val); return val;"/>
      <property name="popupBoxObject">
        <getter>
          return this.boxObject;
        </getter>
      </property>

      <property name="state" readonly="true"
                onget="return this.popupBoxObject.popupState"/>

      <property name="triggerNode" readonly="true"
                onget="return this.popupBoxObject.triggerNode"/>

      <property name="anchorNode" readonly="true"
                onget="return this.popupBoxObject.anchorNode"/>

      <method name="openPopup">
        <parameter name="aAnchorElement"/>
        <parameter name="aPosition"/>
        <parameter name="aX"/>
        <parameter name="aY"/>
        <parameter name="aIsContextMenu"/>
        <parameter name="aAttributesOverride"/>
        <parameter name="aTriggerEvent"/>
        <body>
        <![CDATA[
          // Allow for passing an options object as the second argument.
          if (arguments.length == 2 &&
              arguments[1] != null &&
              typeof arguments[1] == "object") {
            let params = arguments[1];
            aPosition = params.position;
            aX = params.x;
            aY = params.y;
            aIsContextMenu = params.isContextMenu;
            aAttributesOverride = params.attributesOverride;
            aTriggerEvent = params.triggerEvent;
          }

          try {
            var popupBox = this.popupBoxObject;
            if (popupBox)
              popupBox.openPopup(aAnchorElement, aPosition, aX, aY,
                                 aIsContextMenu, aAttributesOverride, aTriggerEvent);
          } catch (e) {}
        ]]>
        </body>
      </method>

      <method name="openPopupAtScreen">
        <parameter name="aX"/>
        <parameter name="aY"/>
        <parameter name="aIsContextMenu"/>
        <parameter name="aTriggerEvent"/>
        <body>
        <![CDATA[
          try {
            var popupBox = this.popupBoxObject;
            if (popupBox)
              popupBox.openPopupAtScreen(aX, aY, aIsContextMenu, aTriggerEvent);
          } catch (e) {}
        ]]>
        </body>
      </method>

      <method name="openPopupAtScreenRect">
        <parameter name="aPosition"/>
        <parameter name="aX"/>
        <parameter name="aY"/>
        <parameter name="aWidth"/>
        <parameter name="aHeight"/>
        <parameter name="aIsContextMenu"/>
        <parameter name="aAttributesOverride"/>
        <parameter name="aTriggerEvent"/>
        <body>
        <![CDATA[
          try {
            var popupBox = this.popupBoxObject;
            if (popupBox)
              popupBox.openPopupAtScreenRect(aPosition, aX, aY, aWidth, aHeight,
                                             aIsContextMenu, aAttributesOverride, aTriggerEvent);
          } catch (e) {}
        ]]>
        </body>
      </method>

      <method name="showPopup">
        <parameter name="element"/>
        <parameter name="xpos"/>
        <parameter name="ypos"/>
        <parameter name="popuptype"/>
        <parameter name="anchoralignment"/>
        <parameter name="popupalignment"/>
        <body>
        <![CDATA[
          var popupBox = null;
          var menuBox = null;
          try {
            popupBox = this.popupBoxObject;
          } catch (e) {}
          try {
            menuBox = this.parentNode.boxObject;
          } catch (e) {}
          if (menuBox instanceof MenuBoxObject)
            menuBox.openMenu(true);
          else if (popupBox)
            popupBox.showPopup(element, this, xpos, ypos, popuptype, anchoralignment, popupalignment);
        ]]>
        </body>
      </method>

      <method name="hidePopup">
        <parameter name="cancel"/>
        <body>
        <![CDATA[
          var popupBox = null;
          var menuBox = null;
          try {
            popupBox = this.popupBoxObject;
          } catch (e) {}
          try {
            menuBox = this.parentNode.boxObject;
          } catch (e) {}
          if (menuBox instanceof MenuBoxObject)
            menuBox.openMenu(false);
          else if (popupBox instanceof PopupBoxObject)
            popupBox.hidePopup(cancel);
        ]]>
        </body>
      </method>

      <property name="autoPosition">
        <getter>
        <![CDATA[
          return this.popupBoxObject.autoPosition;
        ]]>
        </getter>
        <setter>
        <![CDATA[
          return this.popupBoxObject.autoPosition = val;
        ]]>
        </setter>
      </property>

      <property name="alignmentPosition" readonly="true">
        <getter>
        <![CDATA[
          return this.popupBoxObject.alignmentPosition;
        ]]>
        </getter>
      </property>

      <property name="alignmentOffset" readonly="true">
        <getter>
        <![CDATA[
          return this.popupBoxObject.alignmentOffset;
        ]]>
        </getter>
      </property>

      <method name="enableKeyboardNavigator">
        <parameter name="aEnableKeyboardNavigator"/>
        <body>
        <![CDATA[
          this.popupBoxObject.enableKeyboardNavigator(aEnableKeyboardNavigator);
        ]]>
        </body>
      </method>

      <method name="enableRollup">
        <parameter name="aEnableRollup"/>
        <body>
        <![CDATA[
          this.popupBoxObject.enableRollup(aEnableRollup);
        ]]>
        </body>
      </method>

      <method name="sizeTo">
        <parameter name="aWidth"/>
        <parameter name="aHeight"/>
        <body>
        <![CDATA[
          this.popupBoxObject.sizeTo(aWidth, aHeight);
        ]]>
        </body>
      </method>

      <method name="moveTo">
        <parameter name="aLeft"/>
        <parameter name="aTop"/>
        <body>
        <![CDATA[
          this.popupBoxObject.moveTo(aLeft, aTop);
        ]]>
        </body>
      </method>

      <method name="moveToAnchor">
        <parameter name="aAnchorElement"/>
        <parameter name="aPosition"/>
        <parameter name="aX"/>
        <parameter name="aY"/>
        <parameter name="aAttributesOverride"/>
        <body>
        <![CDATA[
          this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
        ]]>
        </body>
      </method>

      <method name="getOuterScreenRect">
        <body>
        <![CDATA[
          return this.popupBoxObject.getOuterScreenRect();
        ]]>
        </body>
      </method>

      <method name="setConstraintRect">
        <parameter name="aRect"/>
        <body>
        <![CDATA[
          this.popupBoxObject.setConstraintRect(aRect);
        ]]>
        </body>
      </method>
    </implementation>

  </binding>

  <binding id="popup" role="xul:menupopup"
           extends="chrome://global/content/bindings/popup.xml#popup-base">

    <content>
      <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
                          smoothscroll="false">
        <children/>
      </xul:arrowscrollbox>
    </content>

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

    <handlers>
      <handler event="popupshowing" phase="target">
        <![CDATA[
          var array = [];
          var width = 0;
          for (var menuitem = this.firstChild; menuitem; menuitem = menuitem.nextSibling) {
            if (menuitem.localName == "menuitem" && menuitem.hasAttribute("acceltext")) {
              var accel = document.getAnonymousElementByAttribute(menuitem, "anonid", "accel");
              if (accel && accel.boxObject) {
                array.push(accel);
                if (accel.boxObject.width > width)
                  width = accel.boxObject.width;
              }
            }
          }
          for (var i = 0; i < array.length; i++)
            array[i].width = width;
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="panel" role="xul:panel"
           extends="chrome://global/content/bindings/popup.xml#popup-base">
    <implementation implements="nsIDOMXULPopupElement">
      <field name="_prevFocus">0</field>
      <field name="_dragBindingAlive">true</field>
      <constructor>
      <![CDATA[
        if (this.getAttribute("backdrag") == "true" && !this._draggableStarted) {
          this._draggableStarted = true;
          try {
            let tmp = {};
            Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
            let draghandle = new tmp.WindowDraggingElement(this);
            draghandle.mouseDownCheck = function() {
              return this._dragBindingAlive;
            }
          } catch (e) {}
        }
      ]]>
      </constructor>
    </implementation>

    <handlers>
      <handler event="popupshowing"><![CDATA[
        // Capture the previous focus before has a chance to get set inside the panel
        try {
          this._prevFocus = Components.utils
                            .getWeakReference(document.commandDispatcher.focusedElement);
          if (this._prevFocus.get())
            return;
        } catch (ex) { }

        this._prevFocus = Components.utils.getWeakReference(document.activeElement);
      ]]></handler>
      <handler event="popupshown"><![CDATA[
        // Fire event for accessibility APIs
        var alertEvent = document.createEvent("Events");
        alertEvent.initEvent("AlertActive", true, true);
        this.dispatchEvent(alertEvent);
       ]]></handler>
      <handler event="popuphiding"><![CDATA[
        try {
          this._currentFocus = document.commandDispatcher.focusedElement;
        } catch (e) {
          this._currentFocus = document.activeElement;
        }
      ]]></handler>
      <handler event="popuphidden"><![CDATA[
        function doFocus() {
          // Focus was set on an element inside this panel,
          // so we need to move it back to where it was previously
          try {
            let fm = Components.classes["@mozilla.org/focus-manager;1"]
                               .getService(Components.interfaces.nsIFocusManager);
            fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
          } catch (e) {
            prevFocus.focus();
          }
        }
        var currentFocus = this._currentFocus;
        var prevFocus = this._prevFocus ? this._prevFocus.get() : null;
        this._currentFocus = null;
        this._prevFocus = null;

        // Avoid changing focus if focus changed while we hide the popup
        // (This can happen e.g. if the popup is hiding as a result of a
        // click/keypress that focused something)
        let nowFocus;
        try {
          nowFocus = document.commandDispatcher.focusedElement;
        } catch (e) {
          nowFocus = document.activeElement;
        }
        if (nowFocus && nowFocus != currentFocus)
          return;

        if (prevFocus && this.getAttribute("norestorefocus") != "true") {
          // Try to restore focus
          try {
            if (document.commandDispatcher.focusedWindow != window)
              return; // Focus has already been set to a window outside of this panel
          } catch (ex) {}

          if (!currentFocus) {
            doFocus();
            return;
          }
          while (currentFocus) {
            if (currentFocus == this) {
              doFocus();
              return;
            }
            currentFocus = currentFocus.parentNode;
          }
        }
      ]]></handler>
    </handlers>
  </binding>

  <binding id="arrowpanel" extends="chrome://global/content/bindings/popup.xml#panel">
    <content flip="both" side="top" position="bottomcenter topleft" consumeoutsideclicks="false">
      <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">
          <children/>
        </xul:box>
      </xul:vbox>
    </content>
    <implementation>
      <field name="_fadeTimer">null</field>
      <method name="sizeTo">
        <parameter name="aWidth"/>
        <parameter name="aHeight"/>
        <body>
        <![CDATA[
          this.popupBoxObject.sizeTo(aWidth, aHeight);
          if (this.state == "open") {
            this.adjustArrowPosition();
          }
        ]]>
        </body>
      </method>
      <method name="moveToAnchor">
        <parameter name="aAnchorElement"/>
        <parameter name="aPosition"/>
        <parameter name="aX"/>
        <parameter name="aY"/>
        <parameter name="aAttributesOverride"/>
        <body>
        <![CDATA[
          this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
        ]]>
        </body>
      </method>
      <method name="adjustArrowPosition">
        <body>
        <![CDATA[
        var anchor = this.anchorNode;
        if (!anchor) {
          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 (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");
          }
        }
        ]]>
        </body>
      </method>
    </implementation>
    <handlers>
      <handler event="popupshowing" phase="target">
      <![CDATA[
        var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
        arrow.hidden = this.anchorNode == null;
        document.getAnonymousElementByAttribute(this, "anonid", "arrowbox")
                .style.removeProperty("transform");

        this.adjustArrowPosition();

        if (this.getAttribute("animate") != "false") {
          this.setAttribute("animate", "open");
        }

        // set fading
        var fade = this.getAttribute("fade");
        var fadeDelay = 0;
        if (fade == "fast") {
          fadeDelay = 1;
        } else if (fade == "slow") {
          fadeDelay = 4000;
        } else {
          return;
        }

        this._fadeTimer = setTimeout(() => this.hidePopup(true), fadeDelay, this);
      ]]>
      </handler>
      <handler event="popuphiding" phase="target">
        let animate = (this.getAttribute("animate") != "false");

        if (this._fadeTimer) {
          clearTimeout(this._fadeTimer);
          if (animate) {
            this.setAttribute("animate", "fade");
          }
        } else if (animate) {
          this.setAttribute("animate", "cancel");
        }
      </handler>
      <handler event="popupshown" phase="target">
        this.setAttribute("panelopen", "true");
      </handler>
      <handler event="popuphidden" phase="target">
        this.removeAttribute("panelopen");
        if (this.getAttribute("animate") != "false") {
          this.removeAttribute("animate");
        }
      </handler>
      <handler event="popuppositioned" phase="target">
        this.adjustArrowPosition();
      </handler>
    </handlers>
  </binding>

  <binding id="tooltip" role="xul:tooltip"
           extends="chrome://global/content/bindings/popup.xml#popup-base">
    <content>
      <children>
        <xul:label class="tooltip-label" xbl:inherits="xbl:text=label" flex="1"/>
      </children>
    </content>

    <implementation>
      <field name="_mouseOutCount">0</field>
      <field name="_isMouseOver">false</field>

      <property name="label"
                onget="return this.getAttribute('label');"
                onset="this.setAttribute('label', val); return val;"/>

      <property name="page" onset="if (val) this.setAttribute('page', 'true');
                                   else this.removeAttribute('page');
                                   return val;"
                            onget="return this.getAttribute('page') == 'true';"/>
      <property name="textProvider"
                readonly="true">
        <getter>
        <![CDATA[
          if (!this._textProvider) {
            this._textProvider = Components.classes["@mozilla.org/embedcomp/default-tooltiptextprovider;1"]
                                 .getService(Components.interfaces.nsITooltipTextProvider);
          }
          return this._textProvider;
        ]]>
        </getter>
      </property>

      <!-- Given the supplied element within a page, set the tooltip's text to the text
           for that element. Returns true if text was assigned, and false if the no text
           is set, which normally would be used to cancel tooltip display.
        -->
      <method name="fillInPageTooltip">
        <parameter name="tipElement"/>
        <body>
        <![CDATA[
          let tttp = this.textProvider;
          let textObj = {}, dirObj = {};
          let shouldChangeText = tttp.getNodeText(tipElement, textObj, dirObj);
          if (shouldChangeText) {
            this.style.direction = dirObj.value;
            this.label = textObj.value;
          }
          return shouldChangeText;
        ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="mouseover"><![CDATA[
        var rel = event.relatedTarget;
        if (!rel)
          return;

        // find out if the node we entered from is one of our anonymous children
        while (rel) {
          if (rel == this)
            break;
          rel = rel.parentNode;
        }

        // if the exited node is not a descendant of ours, we are entering for the first time
        if (rel != this)
          this._isMouseOver = true;
      ]]></handler>

      <handler event="mouseout"><![CDATA[
        var rel = event.relatedTarget;

        // relatedTarget is null when the titletip is first shown: a mouseout event fires
        // because the mouse is exiting the main window and entering the titletip "window".
        // relatedTarget is also null when the mouse exits the main window completely,
        // so count how many times relatedTarget was null after titletip is first shown
        // and hide popup the 2nd time
        if (!rel) {
          ++this._mouseOutCount;
          if (this._mouseOutCount > 1)
            this.hidePopup();
          return;
        }

        // find out if the node we are entering is one of our anonymous children
        while (rel) {
          if (rel == this)
            break;
          rel = rel.parentNode;
        }

        // if the entered node is not a descendant of ours, hide the tooltip
        if (rel != this && this._isMouseOver) {
          this.hidePopup();
        }
      ]]></handler>

      <handler event="popupshowing"><![CDATA[
        if (this.page && !this.fillInPageTooltip(this.triggerNode)) {
          event.preventDefault();
        }
      ]]></handler>

      <handler event="popuphiding"><![CDATA[
        this._isMouseOver = false;
        this._mouseOutCount = 0;
      ]]></handler>
    </handlers>
  </binding>

  <binding id="popup-scrollbars" extends="chrome://global/content/bindings/popup.xml#popup">
    <content>
      <xul:scrollbox class="popup-internal-box" flex="1" orient="vertical" style="overflow: auto;">
        <children/>
      </xul:scrollbox>
    </content>
    <implementation>
      <field name="AUTOSCROLL_INTERVAL">25</field>
      <field name="NOT_DRAGGING">0</field>
      <field name="DRAG_OVER_BUTTON">-1</field>
      <field name="DRAG_OVER_POPUP">1</field>

      <field name="_draggingState">this.NOT_DRAGGING</field>
      <field name="_scrollTimer">0</field>

      <method name="enableDragScrolling">
        <!-- when overItem is true, drag started over menuitem; when false, drag
             started while the popup was opening.
          -->
        <parameter name="overItem"/>
        <body>
        <![CDATA[
          if (!this._draggingState) {
            this.setCaptureAlways();
            this._draggingState = overItem ? this.DRAG_OVER_POPUP : this.DRAG_OVER_BUTTON;
          }
        ]]>
        </body>
      </method>
      <method name="_clearScrollTimer">
        <body>
        <![CDATA[
          if (this._scrollTimer) {
            this.ownerGlobal.clearInterval(this._scrollTimer);
            this._scrollTimer = 0;
          }
        ]]>
        </body>
      </method>
    </implementation>
    <handlers>
      <handler event="popupshown">
        // Enable drag scrolling even when the mouse wasn't used. The mousemove
        // handler will remove it if the mouse isn't down.
        this.enableDragScrolling(false);
      </handler>

      <handler event="popuphidden">
      <![CDATA[
        this._draggingState = this.NOT_DRAGGING;
        this._clearScrollTimer();
        this.releaseCapture();
      ]]>
      </handler>

      <handler event="mousedown" button="0">
      <![CDATA[
        if (this.state == "open" &&
            (event.target.localName == "menuitem" ||
             event.target.localName == "menu" ||
             event.target.localName == "menucaption")) {
          this.enableDragScrolling(true);
        }
      ]]>
      </handler>
      <handler event="mouseup" button="0">
      <![CDATA[
        this._draggingState = this.NOT_DRAGGING;
        this._clearScrollTimer();
      ]]>
      </handler>
      <handler event="mousemove">
      <![CDATA[
        if (!this._draggingState) {
          return;
        }

        this._clearScrollTimer();

        // If the user released the mouse before the popup opens, we will
        // still be capturing, so check that the button is still pressed. If
        // not, release the capture and do nothing else. This also handles if
        // the dropdown was opened via the keyboard.
        if (!(event.buttons & 1)) {
          this._draggingState = this.NOT_DRAGGING;
          this.releaseCapture();
          return;
        }

        // If dragging outside the top or bottom edge of the popup, but within
        // the popup area horizontally, scroll the list in that direction. The
        // _draggingState flag is used to ensure that scrolling does not start
        // until the mouse has moved over the popup first, preventing scrolling
        // while over the dropdown button.
        let popupRect = this.getOuterScreenRect();
        if (event.screenX >= popupRect.left && event.screenX <= popupRect.right) {
          if (this._draggingState == this.DRAG_OVER_BUTTON) {
            if (event.screenY > popupRect.top && event.screenY < popupRect.bottom) {
              this._draggingState = this.DRAG_OVER_POPUP;
            }
          }

          if (this._draggingState == this.DRAG_OVER_POPUP &&
              (event.screenY <= popupRect.top || event.screenY >= popupRect.bottom)) {
            let scrollAmount = event.screenY <= popupRect.top ? -1 : 1;
            this.scrollBox.scrollByIndex(scrollAmount);

            let win = this.ownerGlobal;
            this._scrollTimer = win.setInterval(() => {
              this.scrollBox.scrollByIndex(scrollAmount);
            }, this.AUTOSCROLL_INTERVAL);
          }
        }
      ]]>
      </handler>
    </handlers>
  </binding>

</bindings>