gecko-dev/toolkit/content/widgets/popup.xml
Sam Foster bae82564b3 Bug 1352075 - Implement new animation for opening/closing the arrow panels. r=dao,mossop
* Use new panel animation when opening arrow-panels (including bookmarks menu) to fade in and drop into position
* Linux/GTK is (still) excluded
* New animation is non-directional (i.e. LTR vs. RTL)

This was landed then backed out due to test failures. New since last review:

* Make opacity & transform transition durations equal - ensuring popup is not still moving when popupshown is fired
* Fix missing comma in transition-duration values
* Add animating attribute to the arrowpanel binding to disable pointer-events during the opening transition (via :jaws)
* Wait for popupshown rather than transitionend in bookmark reparenting test
* Fix specificity of CSS rules for panels/bookmarks-menu on edges other than the top (via :jaws)

MozReview-Commit-ID: DTnvyMryf5Y

--HG--
extra : rebase_source : 77895818356b1b366e93c6a8c508ae2b74dbca5c
2017-07-27 11:07:23 -07:00

778 lines
25 KiB
XML

<?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");
// the animating attribute prevents user interaction during transition
// it is removed when popupshown fires
this.setAttribute("animating", "true");
}
// 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.removeAttribute("animating");
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>