368 lines
14 KiB
JavaScript
368 lines
14 KiB
JavaScript
define("dijit/place", [
|
|
"dojo/_base/array", // array.forEach array.map array.some
|
|
"dojo/dom-geometry", // domGeometry.position
|
|
"dojo/dom-style", // domStyle.getComputedStyle
|
|
"dojo/_base/kernel", // kernel.deprecated
|
|
"dojo/_base/window", // win.body
|
|
"dojo/window", // winUtils.getBox
|
|
"./main" // dijit (defining dijit.place to match API doc)
|
|
], function(array, domGeometry, domStyle, kernel, win, winUtils, dijit){
|
|
|
|
// module:
|
|
// dijit/place
|
|
|
|
|
|
function _place(/*DomNode*/ node, choices, layoutNode, aroundNodeCoords){
|
|
// summary:
|
|
// Given a list of spots to put node, put it at the first spot where it fits,
|
|
// of if it doesn't fit anywhere then the place with the least overflow
|
|
// choices: Array
|
|
// Array of elements like: {corner: 'TL', pos: {x: 10, y: 20} }
|
|
// Above example says to put the top-left corner of the node at (10,20)
|
|
// layoutNode: Function(node, aroundNodeCorner, nodeCorner, size)
|
|
// for things like tooltip, they are displayed differently (and have different dimensions)
|
|
// based on their orientation relative to the parent. This adjusts the popup based on orientation.
|
|
// It also passes in the available size for the popup, which is useful for tooltips to
|
|
// tell them that their width is limited to a certain amount. layoutNode() may return a value expressing
|
|
// how much the popup had to be modified to fit into the available space. This is used to determine
|
|
// what the best placement is.
|
|
// aroundNodeCoords: Object
|
|
// Size of aroundNode, ex: {w: 200, h: 50}
|
|
|
|
// get {x: 10, y: 10, w: 100, h:100} type obj representing position of
|
|
// viewport over document
|
|
var view = winUtils.getBox(node.ownerDocument);
|
|
|
|
// This won't work if the node is inside a <div style="position: relative">,
|
|
// so reattach it to win.doc.body. (Otherwise, the positioning will be wrong
|
|
// and also it might get cutoff)
|
|
if(!node.parentNode || String(node.parentNode.tagName).toLowerCase() != "body"){
|
|
win.body(node.ownerDocument).appendChild(node);
|
|
}
|
|
|
|
var best = null;
|
|
array.some(choices, function(choice){
|
|
var corner = choice.corner;
|
|
var pos = choice.pos;
|
|
var overflow = 0;
|
|
|
|
// calculate amount of space available given specified position of node
|
|
var spaceAvailable = {
|
|
w: {
|
|
'L': view.l + view.w - pos.x,
|
|
'R': pos.x - view.l,
|
|
'M': view.w
|
|
}[corner.charAt(1)],
|
|
h: {
|
|
'T': view.t + view.h - pos.y,
|
|
'B': pos.y - view.t,
|
|
'M': view.h
|
|
}[corner.charAt(0)]
|
|
};
|
|
|
|
// Clear left/right position settings set earlier so they don't interfere with calculations,
|
|
// specifically when layoutNode() (a.k.a. Tooltip.orient()) measures natural width of Tooltip
|
|
var s = node.style;
|
|
s.left = s.right = "auto";
|
|
|
|
// configure node to be displayed in given position relative to button
|
|
// (need to do this in order to get an accurate size for the node, because
|
|
// a tooltip's size changes based on position, due to triangle)
|
|
if(layoutNode){
|
|
var res = layoutNode(node, choice.aroundCorner, corner, spaceAvailable, aroundNodeCoords);
|
|
overflow = typeof res == "undefined" ? 0 : res;
|
|
}
|
|
|
|
// get node's size
|
|
var style = node.style;
|
|
var oldDisplay = style.display;
|
|
var oldVis = style.visibility;
|
|
if(style.display == "none"){
|
|
style.visibility = "hidden";
|
|
style.display = "";
|
|
}
|
|
var bb = domGeometry.position(node);
|
|
style.display = oldDisplay;
|
|
style.visibility = oldVis;
|
|
|
|
// coordinates and size of node with specified corner placed at pos,
|
|
// and clipped by viewport
|
|
var
|
|
startXpos = {
|
|
'L': pos.x,
|
|
'R': pos.x - bb.w,
|
|
'M': Math.max(view.l, Math.min(view.l + view.w, pos.x + (bb.w >> 1)) - bb.w) // M orientation is more flexible
|
|
}[corner.charAt(1)],
|
|
startYpos = {
|
|
'T': pos.y,
|
|
'B': pos.y - bb.h,
|
|
'M': Math.max(view.t, Math.min(view.t + view.h, pos.y + (bb.h >> 1)) - bb.h)
|
|
}[corner.charAt(0)],
|
|
startX = Math.max(view.l, startXpos),
|
|
startY = Math.max(view.t, startYpos),
|
|
endX = Math.min(view.l + view.w, startXpos + bb.w),
|
|
endY = Math.min(view.t + view.h, startYpos + bb.h),
|
|
width = endX - startX,
|
|
height = endY - startY;
|
|
|
|
overflow += (bb.w - width) + (bb.h - height);
|
|
|
|
if(best == null || overflow < best.overflow){
|
|
best = {
|
|
corner: corner,
|
|
aroundCorner: choice.aroundCorner,
|
|
x: startX,
|
|
y: startY,
|
|
w: width,
|
|
h: height,
|
|
overflow: overflow,
|
|
spaceAvailable: spaceAvailable
|
|
};
|
|
}
|
|
|
|
return !overflow;
|
|
});
|
|
|
|
// In case the best position is not the last one we checked, need to call
|
|
// layoutNode() again.
|
|
if(best.overflow && layoutNode){
|
|
layoutNode(node, best.aroundCorner, best.corner, best.spaceAvailable, aroundNodeCoords);
|
|
}
|
|
|
|
// And then position the node. Do this last, after the layoutNode() above
|
|
// has sized the node, due to browser quirks when the viewport is scrolled
|
|
// (specifically that a Tooltip will shrink to fit as though the window was
|
|
// scrolled to the left).
|
|
//
|
|
// In RTL mode, set style.right rather than style.left so in the common case,
|
|
// window resizes move the popup along with the aroundNode.
|
|
var l = domGeometry.isBodyLtr(node.ownerDocument),
|
|
s = node.style;
|
|
s.top = best.y + "px";
|
|
s[l ? "left" : "right"] = (l ? best.x : view.w - best.x - best.w) + "px";
|
|
s[l ? "right" : "left"] = "auto"; // needed for FF or else tooltip goes to far left
|
|
|
|
return best;
|
|
}
|
|
|
|
var place = {
|
|
// summary:
|
|
// Code to place a DOMNode relative to another DOMNode.
|
|
// Load using require(["dijit/place"], function(place){ ... }).
|
|
|
|
at: function(node, pos, corners, padding){
|
|
// summary:
|
|
// Positions one of the node's corners at specified position
|
|
// such that node is fully visible in viewport.
|
|
// description:
|
|
// NOTE: node is assumed to be absolutely or relatively positioned.
|
|
// node: DOMNode
|
|
// The node to position
|
|
// pos: dijit/place.__Position
|
|
// Object like {x: 10, y: 20}
|
|
// corners: String[]
|
|
// Array of Strings representing order to try corners in, like ["TR", "BL"].
|
|
// Possible values are:
|
|
//
|
|
// - "BL" - bottom left
|
|
// - "BR" - bottom right
|
|
// - "TL" - top left
|
|
// - "TR" - top right
|
|
// padding: dijit/place.__Position?
|
|
// optional param to set padding, to put some buffer around the element you want to position.
|
|
// example:
|
|
// Try to place node's top right corner at (10,20).
|
|
// If that makes node go (partially) off screen, then try placing
|
|
// bottom left corner at (10,20).
|
|
// | place(node, {x: 10, y: 20}, ["TR", "BL"])
|
|
var choices = array.map(corners, function(corner){
|
|
var c = { corner: corner, pos: {x:pos.x,y:pos.y} };
|
|
if(padding){
|
|
c.pos.x += corner.charAt(1) == 'L' ? padding.x : -padding.x;
|
|
c.pos.y += corner.charAt(0) == 'T' ? padding.y : -padding.y;
|
|
}
|
|
return c;
|
|
});
|
|
|
|
return _place(node, choices);
|
|
},
|
|
|
|
around: function(
|
|
/*DomNode*/ node,
|
|
/*DomNode|dijit/place.__Rectangle*/ anchor,
|
|
/*String[]*/ positions,
|
|
/*Boolean*/ leftToRight,
|
|
/*Function?*/ layoutNode){
|
|
|
|
// summary:
|
|
// Position node adjacent or kitty-corner to anchor
|
|
// such that it's fully visible in viewport.
|
|
// description:
|
|
// Place node such that corner of node touches a corner of
|
|
// aroundNode, and that node is fully visible.
|
|
// anchor:
|
|
// Either a DOMNode or a rectangle (object with x, y, width, height).
|
|
// positions:
|
|
// Ordered list of positions to try matching up.
|
|
//
|
|
// - before: places drop down to the left of the anchor node/widget, or to the right in the case
|
|
// of RTL scripts like Hebrew and Arabic; aligns either the top of the drop down
|
|
// with the top of the anchor, or the bottom of the drop down with bottom of the anchor.
|
|
// - after: places drop down to the right of the anchor node/widget, or to the left in the case
|
|
// of RTL scripts like Hebrew and Arabic; aligns either the top of the drop down
|
|
// with the top of the anchor, or the bottom of the drop down with bottom of the anchor.
|
|
// - before-centered: centers drop down to the left of the anchor node/widget, or to the right
|
|
// in the case of RTL scripts like Hebrew and Arabic
|
|
// - after-centered: centers drop down to the right of the anchor node/widget, or to the left
|
|
// in the case of RTL scripts like Hebrew and Arabic
|
|
// - above-centered: drop down is centered above anchor node
|
|
// - above: drop down goes above anchor node, left sides aligned
|
|
// - above-alt: drop down goes above anchor node, right sides aligned
|
|
// - below-centered: drop down is centered above anchor node
|
|
// - below: drop down goes below anchor node
|
|
// - below-alt: drop down goes below anchor node, right sides aligned
|
|
// layoutNode: Function(node, aroundNodeCorner, nodeCorner)
|
|
// For things like tooltip, they are displayed differently (and have different dimensions)
|
|
// based on their orientation relative to the parent. This adjusts the popup based on orientation.
|
|
// leftToRight:
|
|
// True if widget is LTR, false if widget is RTL. Affects the behavior of "above" and "below"
|
|
// positions slightly.
|
|
// example:
|
|
// | placeAroundNode(node, aroundNode, {'BL':'TL', 'TR':'BR'});
|
|
// This will try to position node such that node's top-left corner is at the same position
|
|
// as the bottom left corner of the aroundNode (ie, put node below
|
|
// aroundNode, with left edges aligned). If that fails it will try to put
|
|
// the bottom-right corner of node where the top right corner of aroundNode is
|
|
// (ie, put node above aroundNode, with right edges aligned)
|
|
//
|
|
|
|
// if around is a DOMNode (or DOMNode id), convert to coordinates
|
|
var aroundNodePos = (typeof anchor == "string" || "offsetWidth" in anchor)
|
|
? domGeometry.position(anchor, true)
|
|
: anchor;
|
|
|
|
// Compute position and size of visible part of anchor (it may be partially hidden by ancestor nodes w/scrollbars)
|
|
if(anchor.parentNode){
|
|
// ignore nodes between position:relative and position:absolute
|
|
var sawPosAbsolute = domStyle.getComputedStyle(anchor).position == "absolute";
|
|
var parent = anchor.parentNode;
|
|
while(parent && parent.nodeType == 1 && parent.nodeName != "BODY"){ //ignoring the body will help performance
|
|
var parentPos = domGeometry.position(parent, true),
|
|
pcs = domStyle.getComputedStyle(parent);
|
|
if(/relative|absolute/.test(pcs.position)){
|
|
sawPosAbsolute = false;
|
|
}
|
|
if(!sawPosAbsolute && /hidden|auto|scroll/.test(pcs.overflow)){
|
|
var bottomYCoord = Math.min(aroundNodePos.y + aroundNodePos.h, parentPos.y + parentPos.h);
|
|
var rightXCoord = Math.min(aroundNodePos.x + aroundNodePos.w, parentPos.x + parentPos.w);
|
|
aroundNodePos.x = Math.max(aroundNodePos.x, parentPos.x);
|
|
aroundNodePos.y = Math.max(aroundNodePos.y, parentPos.y);
|
|
aroundNodePos.h = bottomYCoord - aroundNodePos.y;
|
|
aroundNodePos.w = rightXCoord - aroundNodePos.x;
|
|
}
|
|
if(pcs.position == "absolute"){
|
|
sawPosAbsolute = true;
|
|
}
|
|
parent = parent.parentNode;
|
|
}
|
|
}
|
|
|
|
var x = aroundNodePos.x,
|
|
y = aroundNodePos.y,
|
|
width = "w" in aroundNodePos ? aroundNodePos.w : (aroundNodePos.w = aroundNodePos.width),
|
|
height = "h" in aroundNodePos ? aroundNodePos.h : (kernel.deprecated("place.around: dijit/place.__Rectangle: { x:"+x+", y:"+y+", height:"+aroundNodePos.height+", width:"+width+" } has been deprecated. Please use { x:"+x+", y:"+y+", h:"+aroundNodePos.height+", w:"+width+" }", "", "2.0"), aroundNodePos.h = aroundNodePos.height);
|
|
|
|
// Convert positions arguments into choices argument for _place()
|
|
var choices = [];
|
|
function push(aroundCorner, corner){
|
|
choices.push({
|
|
aroundCorner: aroundCorner,
|
|
corner: corner,
|
|
pos: {
|
|
x: {
|
|
'L': x,
|
|
'R': x + width,
|
|
'M': x + (width >> 1)
|
|
}[aroundCorner.charAt(1)],
|
|
y: {
|
|
'T': y,
|
|
'B': y + height,
|
|
'M': y + (height >> 1)
|
|
}[aroundCorner.charAt(0)]
|
|
}
|
|
})
|
|
}
|
|
array.forEach(positions, function(pos){
|
|
var ltr = leftToRight;
|
|
switch(pos){
|
|
case "above-centered":
|
|
push("TM", "BM");
|
|
break;
|
|
case "below-centered":
|
|
push("BM", "TM");
|
|
break;
|
|
case "after-centered":
|
|
ltr = !ltr;
|
|
// fall through
|
|
case "before-centered":
|
|
push(ltr ? "ML" : "MR", ltr ? "MR" : "ML");
|
|
break;
|
|
case "after":
|
|
ltr = !ltr;
|
|
// fall through
|
|
case "before":
|
|
push(ltr ? "TL" : "TR", ltr ? "TR" : "TL");
|
|
push(ltr ? "BL" : "BR", ltr ? "BR" : "BL");
|
|
break;
|
|
case "below-alt":
|
|
ltr = !ltr;
|
|
// fall through
|
|
case "below":
|
|
// first try to align left borders, next try to align right borders (or reverse for RTL mode)
|
|
push(ltr ? "BL" : "BR", ltr ? "TL" : "TR");
|
|
push(ltr ? "BR" : "BL", ltr ? "TR" : "TL");
|
|
break;
|
|
case "above-alt":
|
|
ltr = !ltr;
|
|
// fall through
|
|
case "above":
|
|
// first try to align left borders, next try to align right borders (or reverse for RTL mode)
|
|
push(ltr ? "TL" : "TR", ltr ? "BL" : "BR");
|
|
push(ltr ? "TR" : "TL", ltr ? "BR" : "BL");
|
|
break;
|
|
default:
|
|
// To assist dijit/_base/place, accept arguments of type {aroundCorner: "BL", corner: "TL"}.
|
|
// Not meant to be used directly.
|
|
push(pos.aroundCorner, pos.corner);
|
|
}
|
|
});
|
|
|
|
var position = _place(node, choices, layoutNode, {w: width, h: height});
|
|
position.aroundNodePos = aroundNodePos;
|
|
|
|
return position;
|
|
}
|
|
};
|
|
|
|
/*=====
|
|
place.__Position = {
|
|
// x: Integer
|
|
// horizontal coordinate in pixels, relative to document body
|
|
// y: Integer
|
|
// vertical coordinate in pixels, relative to document body
|
|
};
|
|
place.__Rectangle = {
|
|
// x: Integer
|
|
// horizontal offset in pixels, relative to document body
|
|
// y: Integer
|
|
// vertical offset in pixels, relative to document body
|
|
// w: Integer
|
|
// width in pixels. Can also be specified as "width" for backwards-compatibility.
|
|
// h: Integer
|
|
// height in pixels. Can also be specified as "height" for backwards-compatibility.
|
|
};
|
|
=====*/
|
|
|
|
return dijit.place = place; // setting dijit.place for back-compat, remove for 2.0
|
|
});
|