510 lines
17 KiB
JavaScript
510 lines
17 KiB
JavaScript
|
define("dijit/_HasDropDown", [
|
||
|
"dojo/_base/declare", // declare
|
||
|
"dojo/_base/Deferred",
|
||
|
"dojo/_base/event", // event.stop
|
||
|
"dojo/dom", // dom.isDescendant
|
||
|
"dojo/dom-attr", // domAttr.set
|
||
|
"dojo/dom-class", // domClass.add domClass.contains domClass.remove
|
||
|
"dojo/dom-geometry", // domGeometry.marginBox domGeometry.position
|
||
|
"dojo/dom-style", // domStyle.set
|
||
|
"dojo/has", // has("touch")
|
||
|
"dojo/keys", // keys.DOWN_ARROW keys.ENTER keys.ESCAPE
|
||
|
"dojo/_base/lang", // lang.hitch lang.isFunction
|
||
|
"dojo/on",
|
||
|
"dojo/window", // winUtils.getBox
|
||
|
"./registry", // registry.byNode()
|
||
|
"./focus",
|
||
|
"./popup",
|
||
|
"./_FocusMixin"
|
||
|
], function(declare, Deferred, event,dom, domAttr, domClass, domGeometry, domStyle, has, keys, lang, on,
|
||
|
winUtils, registry, focus, popup, _FocusMixin){
|
||
|
|
||
|
|
||
|
// module:
|
||
|
// dijit/_HasDropDown
|
||
|
|
||
|
return declare("dijit._HasDropDown", _FocusMixin, {
|
||
|
// summary:
|
||
|
// Mixin for widgets that need drop down ability.
|
||
|
|
||
|
// _buttonNode: [protected] DomNode
|
||
|
// The button/icon/node to click to display the drop down.
|
||
|
// Can be set via a data-dojo-attach-point assignment.
|
||
|
// If missing, then either focusNode or domNode (if focusNode is also missing) will be used.
|
||
|
_buttonNode: null,
|
||
|
|
||
|
// _arrowWrapperNode: [protected] DomNode
|
||
|
// Will set CSS class dijitUpArrow, dijitDownArrow, dijitRightArrow etc. on this node depending
|
||
|
// on where the drop down is set to be positioned.
|
||
|
// Can be set via a data-dojo-attach-point assignment.
|
||
|
// If missing, then _buttonNode will be used.
|
||
|
_arrowWrapperNode: null,
|
||
|
|
||
|
// _popupStateNode: [protected] DomNode
|
||
|
// The node to set the popupActive class on.
|
||
|
// Can be set via a data-dojo-attach-point assignment.
|
||
|
// If missing, then focusNode or _buttonNode (if focusNode is missing) will be used.
|
||
|
_popupStateNode: null,
|
||
|
|
||
|
// _aroundNode: [protected] DomNode
|
||
|
// The node to display the popup around.
|
||
|
// Can be set via a data-dojo-attach-point assignment.
|
||
|
// If missing, then domNode will be used.
|
||
|
_aroundNode: null,
|
||
|
|
||
|
// dropDown: [protected] Widget
|
||
|
// The widget to display as a popup. This widget *must* be
|
||
|
// defined before the startup function is called.
|
||
|
dropDown: null,
|
||
|
|
||
|
// autoWidth: [protected] Boolean
|
||
|
// Set to true to make the drop down at least as wide as this
|
||
|
// widget. Set to false if the drop down should just be its
|
||
|
// default width
|
||
|
autoWidth: true,
|
||
|
|
||
|
// forceWidth: [protected] Boolean
|
||
|
// Set to true to make the drop down exactly as wide as this
|
||
|
// widget. Overrides autoWidth.
|
||
|
forceWidth: false,
|
||
|
|
||
|
// maxHeight: [protected] Integer
|
||
|
// The max height for our dropdown.
|
||
|
// Any dropdown taller than this will have scrollbars.
|
||
|
// Set to 0 for no max height, or -1 to limit height to available space in viewport
|
||
|
maxHeight: 0,
|
||
|
|
||
|
// dropDownPosition: [const] String[]
|
||
|
// This variable controls the position of the drop down.
|
||
|
// It's an array of strings with the following values:
|
||
|
//
|
||
|
// - before: places drop down to the left of the target node/widget, or to the right in
|
||
|
// the case of RTL scripts like Hebrew and Arabic
|
||
|
// - after: places drop down to the right of the target node/widget, or to the left in
|
||
|
// the case of RTL scripts like Hebrew and Arabic
|
||
|
// - above: drop down goes above target node
|
||
|
// - below: drop down goes below target node
|
||
|
//
|
||
|
// The list is positions is tried, in order, until a position is found where the drop down fits
|
||
|
// within the viewport.
|
||
|
//
|
||
|
dropDownPosition: ["below","above"],
|
||
|
|
||
|
// _stopClickEvents: Boolean
|
||
|
// When set to false, the click events will not be stopped, in
|
||
|
// case you want to use them in your subclass
|
||
|
_stopClickEvents: true,
|
||
|
|
||
|
_onDropDownMouseDown: function(/*Event*/ e){
|
||
|
// summary:
|
||
|
// Callback when the user mousedown's on the arrow icon
|
||
|
if(this.disabled || this.readOnly){ return; }
|
||
|
|
||
|
// Prevent default to stop things like text selection, but don't stop propagation, so that:
|
||
|
// 1. TimeTextBox etc. can focus the <input> on mousedown
|
||
|
// 2. dropDownButtonActive class applied by _CssStateMixin (on button depress)
|
||
|
// 3. user defined onMouseDown handler fires
|
||
|
e.preventDefault();
|
||
|
|
||
|
this._docHandler = this.connect(this.ownerDocument, "mouseup", "_onDropDownMouseUp");
|
||
|
|
||
|
this.toggleDropDown();
|
||
|
},
|
||
|
|
||
|
_onDropDownMouseUp: function(/*Event?*/ e){
|
||
|
// summary:
|
||
|
// Callback when the user lifts their mouse after mouse down on the arrow icon.
|
||
|
// If the drop down is a simple menu and the mouse is over the menu, we execute it, otherwise, we focus our
|
||
|
// drop down widget. If the event is missing, then we are not
|
||
|
// a mouseup event.
|
||
|
//
|
||
|
// This is useful for the common mouse movement pattern
|
||
|
// with native browser `<select>` nodes:
|
||
|
//
|
||
|
// 1. mouse down on the select node (probably on the arrow)
|
||
|
// 2. move mouse to a menu item while holding down the mouse button
|
||
|
// 3. mouse up. this selects the menu item as though the user had clicked it.
|
||
|
if(e && this._docHandler){
|
||
|
this.disconnect(this._docHandler);
|
||
|
}
|
||
|
var dropDown = this.dropDown, overMenu = false;
|
||
|
|
||
|
if(e && this._opened){
|
||
|
// This code deals with the corner-case when the drop down covers the original widget,
|
||
|
// because it's so large. In that case mouse-up shouldn't select a value from the menu.
|
||
|
// Find out if our target is somewhere in our dropdown widget,
|
||
|
// but not over our _buttonNode (the clickable node)
|
||
|
var c = domGeometry.position(this._buttonNode, true);
|
||
|
if(!(e.pageX >= c.x && e.pageX <= c.x + c.w) ||
|
||
|
!(e.pageY >= c.y && e.pageY <= c.y + c.h)){
|
||
|
var t = e.target;
|
||
|
while(t && !overMenu){
|
||
|
if(domClass.contains(t, "dijitPopup")){
|
||
|
overMenu = true;
|
||
|
}else{
|
||
|
t = t.parentNode;
|
||
|
}
|
||
|
}
|
||
|
if(overMenu){
|
||
|
t = e.target;
|
||
|
if(dropDown.onItemClick){
|
||
|
var menuItem;
|
||
|
while(t && !(menuItem = registry.byNode(t))){
|
||
|
t = t.parentNode;
|
||
|
}
|
||
|
if(menuItem && menuItem.onClick && menuItem.getParent){
|
||
|
menuItem.getParent().onItemClick(menuItem, e);
|
||
|
}
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if(this._opened){
|
||
|
if(dropDown.focus && dropDown.autoFocus !== false){
|
||
|
// Focus the dropdown widget - do it on a delay so that we
|
||
|
// don't steal back focus from the dropdown.
|
||
|
this._focusDropDownTimer = this.defer(function(){
|
||
|
dropDown.focus();
|
||
|
delete this._focusDropDownTimer;
|
||
|
});
|
||
|
}
|
||
|
}else{
|
||
|
// The drop down arrow icon probably can't receive focus, but widget itself should get focus.
|
||
|
// defer() needed to make it work on IE (test DateTextBox)
|
||
|
this.defer("focus");
|
||
|
}
|
||
|
|
||
|
if(has("touch")){
|
||
|
this._justGotMouseUp = true;
|
||
|
this.defer(function(){
|
||
|
this._justGotMouseUp = false;
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_onDropDownClick: function(/*Event*/ e){
|
||
|
if(has("touch") && !this._justGotMouseUp){
|
||
|
// If there was no preceding mousedown/mouseup (like on android), then simulate them to
|
||
|
// toggle the drop down.
|
||
|
//
|
||
|
// The if(has("touch") is necessary since IE and desktop safari get spurious onclick events
|
||
|
// when there are nested tables (specifically, clicking on a table that holds a dijit/form/Select,
|
||
|
// but not on the Select itself, causes an onclick event on the Select)
|
||
|
this._onDropDownMouseDown(e);
|
||
|
this._onDropDownMouseUp(e);
|
||
|
}
|
||
|
|
||
|
// The drop down was already opened on mousedown/keydown; just need to call stopEvent().
|
||
|
if(this._stopClickEvents){
|
||
|
event.stop(e);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
buildRendering: function(){
|
||
|
this.inherited(arguments);
|
||
|
|
||
|
this._buttonNode = this._buttonNode || this.focusNode || this.domNode;
|
||
|
this._popupStateNode = this._popupStateNode || this.focusNode || this._buttonNode;
|
||
|
|
||
|
// Add a class to the "dijitDownArrowButton" type class to _buttonNode so theme can set direction of arrow
|
||
|
// based on where drop down will normally appear
|
||
|
var defaultPos = {
|
||
|
"after" : this.isLeftToRight() ? "Right" : "Left",
|
||
|
"before" : this.isLeftToRight() ? "Left" : "Right",
|
||
|
"above" : "Up",
|
||
|
"below" : "Down",
|
||
|
"left" : "Left",
|
||
|
"right" : "Right"
|
||
|
}[this.dropDownPosition[0]] || this.dropDownPosition[0] || "Down";
|
||
|
domClass.add(this._arrowWrapperNode || this._buttonNode, "dijit" + defaultPos + "ArrowButton");
|
||
|
},
|
||
|
|
||
|
postCreate: function(){
|
||
|
// summary:
|
||
|
// set up nodes and connect our mouse and keyboard events
|
||
|
|
||
|
this.inherited(arguments);
|
||
|
|
||
|
var keyboardEventNode = this.focusNode || this.domNode;
|
||
|
this.own(
|
||
|
on(this._buttonNode, "mousedown", lang.hitch(this, "_onDropDownMouseDown")),
|
||
|
on(this._buttonNode, "click", lang.hitch(this, "_onDropDownClick")),
|
||
|
on(keyboardEventNode, "keydown", lang.hitch(this, "_onKey")),
|
||
|
on(keyboardEventNode, "keyup", lang.hitch(this, "_onKeyUp"))
|
||
|
);
|
||
|
},
|
||
|
|
||
|
destroy: function(){
|
||
|
if(this.dropDown){
|
||
|
// Destroy the drop down, unless it's already been destroyed. This can happen because
|
||
|
// the drop down is a direct child of <body> even though it's logically my child.
|
||
|
if(!this.dropDown._destroyed){
|
||
|
this.dropDown.destroyRecursive();
|
||
|
}
|
||
|
delete this.dropDown;
|
||
|
}
|
||
|
this.inherited(arguments);
|
||
|
},
|
||
|
|
||
|
_onKey: function(/*Event*/ e){
|
||
|
// summary:
|
||
|
// Callback when the user presses a key while focused on the button node
|
||
|
|
||
|
if(this.disabled || this.readOnly){ return; }
|
||
|
var d = this.dropDown, target = e.target;
|
||
|
if(d && this._opened && d.handleKey){
|
||
|
if(d.handleKey(e) === false){
|
||
|
/* false return code means that the drop down handled the key */
|
||
|
event.stop(e);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
if(d && this._opened && e.keyCode == keys.ESCAPE){
|
||
|
this.closeDropDown();
|
||
|
event.stop(e);
|
||
|
}else if(!this._opened &&
|
||
|
(e.keyCode == keys.DOWN_ARROW ||
|
||
|
( (e.keyCode == keys.ENTER || e.keyCode == keys.SPACE) &&
|
||
|
//ignore enter and space if the event is for a text input
|
||
|
((target.tagName || "").toLowerCase() !== 'input' ||
|
||
|
(target.type && target.type.toLowerCase() !== 'text'))))){
|
||
|
// Toggle the drop down, but wait until keyup so that the drop down doesn't
|
||
|
// get a stray keyup event, or in the case of key-repeat (because user held
|
||
|
// down key for too long), stray keydown events
|
||
|
this._toggleOnKeyUp = true;
|
||
|
event.stop(e);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_onKeyUp: function(){
|
||
|
if(this._toggleOnKeyUp){
|
||
|
delete this._toggleOnKeyUp;
|
||
|
this.toggleDropDown();
|
||
|
var d = this.dropDown; // drop down may not exist until toggleDropDown() call
|
||
|
if(d && d.focus){
|
||
|
this.defer(lang.hitch(d, "focus"), 1);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_onBlur: function(){
|
||
|
// summary:
|
||
|
// Called magically when focus has shifted away from this widget and it's dropdown
|
||
|
|
||
|
// Don't focus on button if the user has explicitly focused on something else (happens
|
||
|
// when user clicks another control causing the current popup to close)..
|
||
|
// But if focus is inside of the drop down then reset focus to me, because IE doesn't like
|
||
|
// it when you display:none a node with focus.
|
||
|
var focusMe = focus.curNode && this.dropDown && dom.isDescendant(focus.curNode, this.dropDown.domNode);
|
||
|
|
||
|
this.closeDropDown(focusMe);
|
||
|
|
||
|
this.inherited(arguments);
|
||
|
},
|
||
|
|
||
|
isLoaded: function(){
|
||
|
// summary:
|
||
|
// Returns true if the dropdown exists and it's data is loaded. This can
|
||
|
// be overridden in order to force a call to loadDropDown().
|
||
|
// tags:
|
||
|
// protected
|
||
|
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
loadDropDown: function(/*Function*/ loadCallback){
|
||
|
// summary:
|
||
|
// Creates the drop down if it doesn't exist, loads the data
|
||
|
// if there's an href and it hasn't been loaded yet, and then calls
|
||
|
// the given callback.
|
||
|
// tags:
|
||
|
// protected
|
||
|
|
||
|
// TODO: for 2.0, change API to return a Deferred, instead of calling loadCallback?
|
||
|
loadCallback();
|
||
|
},
|
||
|
|
||
|
loadAndOpenDropDown: function(){
|
||
|
// summary:
|
||
|
// Creates the drop down if it doesn't exist, loads the data
|
||
|
// if there's an href and it hasn't been loaded yet, and
|
||
|
// then opens the drop down. This is basically a callback when the
|
||
|
// user presses the down arrow button to open the drop down.
|
||
|
// returns: Deferred
|
||
|
// Deferred for the drop down widget that
|
||
|
// fires when drop down is created and loaded
|
||
|
// tags:
|
||
|
// protected
|
||
|
var d = new Deferred(),
|
||
|
afterLoad = lang.hitch(this, function(){
|
||
|
this.openDropDown();
|
||
|
d.resolve(this.dropDown);
|
||
|
});
|
||
|
if(!this.isLoaded()){
|
||
|
this.loadDropDown(afterLoad);
|
||
|
}else{
|
||
|
afterLoad();
|
||
|
}
|
||
|
return d;
|
||
|
},
|
||
|
|
||
|
toggleDropDown: function(){
|
||
|
// summary:
|
||
|
// Callback when the user presses the down arrow button or presses
|
||
|
// the down arrow key to open/close the drop down.
|
||
|
// Toggle the drop-down widget; if it is up, close it, if not, open it
|
||
|
// tags:
|
||
|
// protected
|
||
|
|
||
|
if(this.disabled || this.readOnly){ return; }
|
||
|
if(!this._opened){
|
||
|
this.loadAndOpenDropDown();
|
||
|
}else{
|
||
|
this.closeDropDown();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
openDropDown: function(){
|
||
|
// summary:
|
||
|
// Opens the dropdown for this widget. To be called only when this.dropDown
|
||
|
// has been created and is ready to display (ie, it's data is loaded).
|
||
|
// returns:
|
||
|
// return value of dijit/popup.open()
|
||
|
// tags:
|
||
|
// protected
|
||
|
|
||
|
var dropDown = this.dropDown,
|
||
|
ddNode = dropDown.domNode,
|
||
|
aroundNode = this._aroundNode || this.domNode,
|
||
|
self = this;
|
||
|
|
||
|
// Prepare our popup's height and honor maxHeight if it exists.
|
||
|
|
||
|
// TODO: isn't maxHeight dependent on the return value from dijit/popup.open(),
|
||
|
// ie, dependent on how much space is available (BK)
|
||
|
|
||
|
if(!this._preparedNode){
|
||
|
this._preparedNode = true;
|
||
|
// Check if we have explicitly set width and height on the dropdown widget dom node
|
||
|
if(ddNode.style.width){
|
||
|
this._explicitDDWidth = true;
|
||
|
}
|
||
|
if(ddNode.style.height){
|
||
|
this._explicitDDHeight = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Code for resizing dropdown (height limitation, or increasing width to match my width)
|
||
|
if(this.maxHeight || this.forceWidth || this.autoWidth){
|
||
|
var myStyle = {
|
||
|
display: "",
|
||
|
visibility: "hidden"
|
||
|
};
|
||
|
if(!this._explicitDDWidth){
|
||
|
myStyle.width = "";
|
||
|
}
|
||
|
if(!this._explicitDDHeight){
|
||
|
myStyle.height = "";
|
||
|
}
|
||
|
domStyle.set(ddNode, myStyle);
|
||
|
|
||
|
// Figure out maximum height allowed (if there is a height restriction)
|
||
|
var maxHeight = this.maxHeight;
|
||
|
if(maxHeight == -1){
|
||
|
// limit height to space available in viewport either above or below my domNode
|
||
|
// (whichever side has more room)
|
||
|
var viewport = winUtils.getBox(this.ownerDocument),
|
||
|
position = domGeometry.position(aroundNode, false);
|
||
|
maxHeight = Math.floor(Math.max(position.y, viewport.h - (position.y + position.h)));
|
||
|
}
|
||
|
|
||
|
// Attach dropDown to DOM and make make visibility:hidden rather than display:none
|
||
|
// so we call startup() and also get the size
|
||
|
popup.moveOffScreen(dropDown);
|
||
|
|
||
|
if(dropDown.startup && !dropDown._started){
|
||
|
dropDown.startup(); // this has to be done after being added to the DOM
|
||
|
}
|
||
|
// Get size of drop down, and determine if vertical scroll bar needed. If no scroll bar needed,
|
||
|
// use overflow:visible rather than overflow:hidden so off-by-one errors don't hide drop down border.
|
||
|
var mb = domGeometry.getMarginSize(ddNode);
|
||
|
var overHeight = (maxHeight && mb.h > maxHeight);
|
||
|
domStyle.set(ddNode, {
|
||
|
overflowX: "visible",
|
||
|
overflowY: overHeight ? "auto" : "visible"
|
||
|
});
|
||
|
if(overHeight){
|
||
|
mb.h = maxHeight;
|
||
|
if("w" in mb){
|
||
|
mb.w += 16; // room for vertical scrollbar
|
||
|
}
|
||
|
}else{
|
||
|
delete mb.h;
|
||
|
}
|
||
|
|
||
|
// Adjust dropdown width to match or be larger than my width
|
||
|
if(this.forceWidth){
|
||
|
mb.w = aroundNode.offsetWidth;
|
||
|
}else if(this.autoWidth){
|
||
|
mb.w = Math.max(mb.w, aroundNode.offsetWidth);
|
||
|
}else{
|
||
|
delete mb.w;
|
||
|
}
|
||
|
|
||
|
// And finally, resize the dropdown to calculated height and width
|
||
|
if(lang.isFunction(dropDown.resize)){
|
||
|
dropDown.resize(mb);
|
||
|
}else{
|
||
|
domGeometry.setMarginBox(ddNode, mb);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var retVal = popup.open({
|
||
|
parent: this,
|
||
|
popup: dropDown,
|
||
|
around: aroundNode,
|
||
|
orient: this.dropDownPosition,
|
||
|
onExecute: function(){
|
||
|
self.closeDropDown(true);
|
||
|
},
|
||
|
onCancel: function(){
|
||
|
self.closeDropDown(true);
|
||
|
},
|
||
|
onClose: function(){
|
||
|
domAttr.set(self._popupStateNode, "popupActive", false);
|
||
|
domClass.remove(self._popupStateNode, "dijitHasDropDownOpen");
|
||
|
self._set("_opened", false); // use set() because _CssStateMixin is watching
|
||
|
}
|
||
|
});
|
||
|
domAttr.set(this._popupStateNode, "popupActive", "true");
|
||
|
domClass.add(this._popupStateNode, "dijitHasDropDownOpen");
|
||
|
this._set("_opened", true); // use set() because _CssStateMixin is watching
|
||
|
this.domNode.setAttribute("aria-expanded", "true");
|
||
|
|
||
|
return retVal;
|
||
|
},
|
||
|
|
||
|
closeDropDown: function(/*Boolean*/ focus){
|
||
|
// summary:
|
||
|
// Closes the drop down on this widget
|
||
|
// focus:
|
||
|
// If true, refocuses the button widget
|
||
|
// tags:
|
||
|
// protected
|
||
|
|
||
|
if(this._focusDropDownTimer){
|
||
|
this._focusDropDownTimer.remove();
|
||
|
delete this._focusDropDownTimer;
|
||
|
}
|
||
|
if(this._opened){
|
||
|
this.domNode.setAttribute("aria-expanded", "false");
|
||
|
if(focus){ this.focus(); }
|
||
|
popup.close(this.dropDown);
|
||
|
this._opened = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
});
|
||
|
});
|