place.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. define("dijit/place", [
  2. "dojo/_base/array", // array.forEach array.map array.some
  3. "dojo/dom-geometry", // domGeometry.position
  4. "dojo/dom-style", // domStyle.getComputedStyle
  5. "dojo/_base/kernel", // kernel.deprecated
  6. "dojo/_base/window", // win.body
  7. "./Viewport", // getEffectiveBox
  8. "." // dijit (defining dijit.place to match API doc)
  9. ], function(array, domGeometry, domStyle, kernel, win, Viewport, dijit){
  10. // module:
  11. // dijit/place
  12. // summary:
  13. // Code to place a popup relative to another node
  14. function _place(/*DomNode*/ node, choices, layoutNode, aroundNodeCoords){
  15. // summary:
  16. // Given a list of spots to put node, put it at the first spot where it fits,
  17. // of if it doesn't fit anywhere then the place with the least overflow
  18. // choices: Array
  19. // Array of elements like: {corner: 'TL', pos: {x: 10, y: 20} }
  20. // Above example says to put the top-left corner of the node at (10,20)
  21. // layoutNode: Function(node, aroundNodeCorner, nodeCorner, size)
  22. // for things like tooltip, they are displayed differently (and have different dimensions)
  23. // based on their orientation relative to the parent. This adjusts the popup based on orientation.
  24. // It also passes in the available size for the popup, which is useful for tooltips to
  25. // tell them that their width is limited to a certain amount. layoutNode() may return a value expressing
  26. // how much the popup had to be modified to fit into the available space. This is used to determine
  27. // what the best placement is.
  28. // aroundNodeCoords: Object
  29. // Size of aroundNode, ex: {w: 200, h: 50}
  30. // get {x: 10, y: 10, w: 100, h:100} type obj representing position of
  31. // viewport over document
  32. var view = Viewport.getEffectiveBox(node.ownerDocument);
  33. // This won't work if the node is inside a <div style="position: relative">,
  34. // so reattach it to win.doc.body. (Otherwise, the positioning will be wrong
  35. // and also it might get cutoff)
  36. if(!node.parentNode || String(node.parentNode.tagName).toLowerCase() != "body"){
  37. win.body().appendChild(node);
  38. }
  39. var best = null;
  40. array.some(choices, function(choice){
  41. var corner = choice.corner;
  42. var pos = choice.pos;
  43. var overflow = 0;
  44. // calculate amount of space available given specified position of node
  45. var spaceAvailable = {
  46. w: {
  47. 'L': view.l + view.w - pos.x,
  48. 'R': pos.x - view.l,
  49. 'M': view.w
  50. }[corner.charAt(1)],
  51. h: {
  52. 'T': view.t + view.h - pos.y,
  53. 'B': pos.y - view.t,
  54. 'M': view.h
  55. }[corner.charAt(0)]
  56. };
  57. // Clear left/right position settings set earlier so they don't interfere with calculations,
  58. // specifically when layoutNode() (a.k.a. Tooltip.orient()) measures natural width of Tooltip
  59. var s = node.style;
  60. s.left = s.right = "auto";
  61. // configure node to be displayed in given position relative to button
  62. // (need to do this in order to get an accurate size for the node, because
  63. // a tooltip's size changes based on position, due to triangle)
  64. if(layoutNode){
  65. var res = layoutNode(node, choice.aroundCorner, corner, spaceAvailable, aroundNodeCoords);
  66. overflow = typeof res == "undefined" ? 0 : res;
  67. }
  68. // get node's size
  69. var style = node.style;
  70. var oldDisplay = style.display;
  71. var oldVis = style.visibility;
  72. if(style.display == "none"){
  73. style.visibility = "hidden";
  74. style.display = "";
  75. }
  76. var bb = domGeometry.position(node);
  77. style.display = oldDisplay;
  78. style.visibility = oldVis;
  79. // coordinates and size of node with specified corner placed at pos,
  80. // and clipped by viewport
  81. var
  82. startXpos = {
  83. 'L': pos.x,
  84. 'R': pos.x - bb.w,
  85. 'M': Math.max(view.l, Math.min(view.l + view.w, pos.x + (bb.w >> 1)) - bb.w) // M orientation is more flexible
  86. }[corner.charAt(1)],
  87. startYpos = {
  88. 'T': pos.y,
  89. 'B': pos.y - bb.h,
  90. 'M': Math.max(view.t, Math.min(view.t + view.h, pos.y + (bb.h >> 1)) - bb.h)
  91. }[corner.charAt(0)],
  92. startX = Math.max(view.l, startXpos),
  93. startY = Math.max(view.t, startYpos),
  94. endX = Math.min(view.l + view.w, startXpos + bb.w),
  95. endY = Math.min(view.t + view.h, startYpos + bb.h),
  96. width = endX - startX,
  97. height = endY - startY;
  98. overflow += (bb.w - width) + (bb.h - height);
  99. if(best == null || overflow < best.overflow){
  100. best = {
  101. corner: corner,
  102. aroundCorner: choice.aroundCorner,
  103. x: startX,
  104. y: startY,
  105. w: width,
  106. h: height,
  107. overflow: overflow,
  108. spaceAvailable: spaceAvailable
  109. };
  110. }
  111. return !overflow;
  112. });
  113. // In case the best position is not the last one we checked, need to call
  114. // layoutNode() again.
  115. if(best.overflow && layoutNode){
  116. layoutNode(node, best.aroundCorner, best.corner, best.spaceAvailable, aroundNodeCoords);
  117. }
  118. // And then position the node. Do this last, after the layoutNode() above
  119. // has sized the node, due to browser quirks when the viewport is scrolled
  120. // (specifically that a Tooltip will shrink to fit as though the window was
  121. // scrolled to the left).
  122. var s = node.style;
  123. s.top = best.y + "px";
  124. s.left = best.x + "px";
  125. s.right = "auto"; // needed for FF or else tooltip goes to far left
  126. return best;
  127. }
  128. /*=====
  129. dijit.place.__Position = function(){
  130. // x: Integer
  131. // horizontal coordinate in pixels, relative to document body
  132. // y: Integer
  133. // vertical coordinate in pixels, relative to document body
  134. this.x = x;
  135. this.y = y;
  136. };
  137. =====*/
  138. /*=====
  139. dijit.place.__Rectangle = function(){
  140. // x: Integer
  141. // horizontal offset in pixels, relative to document body
  142. // y: Integer
  143. // vertical offset in pixels, relative to document body
  144. // w: Integer
  145. // width in pixels. Can also be specified as "width" for backwards-compatibility.
  146. // h: Integer
  147. // height in pixels. Can also be specified as "height" from backwards-compatibility.
  148. this.x = x;
  149. this.y = y;
  150. this.w = w;
  151. this.h = h;
  152. };
  153. =====*/
  154. return (dijit.place = {
  155. // summary:
  156. // Code to place a DOMNode relative to another DOMNode.
  157. // Load using require(["dijit/place"], function(place){ ... }).
  158. at: function(node, pos, corners, padding){
  159. // summary:
  160. // Positions one of the node's corners at specified position
  161. // such that node is fully visible in viewport.
  162. // description:
  163. // NOTE: node is assumed to be absolutely or relatively positioned.
  164. // node: DOMNode
  165. // The node to position
  166. // pos: dijit.place.__Position
  167. // Object like {x: 10, y: 20}
  168. // corners: String[]
  169. // Array of Strings representing order to try corners in, like ["TR", "BL"].
  170. // Possible values are:
  171. // * "BL" - bottom left
  172. // * "BR" - bottom right
  173. // * "TL" - top left
  174. // * "TR" - top right
  175. // padding: dijit.place.__Position?
  176. // optional param to set padding, to put some buffer around the element you want to position.
  177. // example:
  178. // Try to place node's top right corner at (10,20).
  179. // If that makes node go (partially) off screen, then try placing
  180. // bottom left corner at (10,20).
  181. // | place(node, {x: 10, y: 20}, ["TR", "BL"])
  182. var choices = array.map(corners, function(corner){
  183. var c = { corner: corner, pos: {x:pos.x,y:pos.y} };
  184. if(padding){
  185. c.pos.x += corner.charAt(1) == 'L' ? padding.x : -padding.x;
  186. c.pos.y += corner.charAt(0) == 'T' ? padding.y : -padding.y;
  187. }
  188. return c;
  189. });
  190. return _place(node, choices);
  191. },
  192. around: function(
  193. /*DomNode*/ node,
  194. /*DomNode || dijit.place.__Rectangle*/ anchor,
  195. /*String[]*/ positions,
  196. /*Boolean*/ leftToRight,
  197. /*Function?*/ layoutNode){
  198. // summary:
  199. // Position node adjacent or kitty-corner to anchor
  200. // such that it's fully visible in viewport.
  201. //
  202. // description:
  203. // Place node such that corner of node touches a corner of
  204. // aroundNode, and that node is fully visible.
  205. //
  206. // anchor:
  207. // Either a DOMNode or a __Rectangle (object with x, y, width, height).
  208. //
  209. // positions:
  210. // Ordered list of positions to try matching up.
  211. // * before: places drop down to the left of the anchor node/widget, or to the right in the case
  212. // of RTL scripts like Hebrew and Arabic; aligns either the top of the drop down
  213. // with the top of the anchor, or the bottom of the drop down with bottom of the anchor.
  214. // * after: places drop down to the right of the anchor node/widget, or to the left in the case
  215. // of RTL scripts like Hebrew and Arabic; aligns either the top of the drop down
  216. // with the top of the anchor, or the bottom of the drop down with bottom of the anchor.
  217. // * before-centered: centers drop down to the left of the anchor node/widget, or to the right
  218. // in the case of RTL scripts like Hebrew and Arabic
  219. // * after-centered: centers drop down to the right of the anchor node/widget, or to the left
  220. // in the case of RTL scripts like Hebrew and Arabic
  221. // * above-centered: drop down is centered above anchor node
  222. // * above: drop down goes above anchor node, left sides aligned
  223. // * above-alt: drop down goes above anchor node, right sides aligned
  224. // * below-centered: drop down is centered above anchor node
  225. // * below: drop down goes below anchor node
  226. // * below-alt: drop down goes below anchor node, right sides aligned
  227. //
  228. // layoutNode: Function(node, aroundNodeCorner, nodeCorner)
  229. // For things like tooltip, they are displayed differently (and have different dimensions)
  230. // based on their orientation relative to the parent. This adjusts the popup based on orientation.
  231. //
  232. // leftToRight:
  233. // True if widget is LTR, false if widget is RTL. Affects the behavior of "above" and "below"
  234. // positions slightly.
  235. //
  236. // example:
  237. // | placeAroundNode(node, aroundNode, {'BL':'TL', 'TR':'BR'});
  238. // This will try to position node such that node's top-left corner is at the same position
  239. // as the bottom left corner of the aroundNode (ie, put node below
  240. // aroundNode, with left edges aligned). If that fails it will try to put
  241. // the bottom-right corner of node where the top right corner of aroundNode is
  242. // (ie, put node above aroundNode, with right edges aligned)
  243. //
  244. // if around is a DOMNode (or DOMNode id), convert to coordinates
  245. var aroundNodePos = (typeof anchor == "string" || "offsetWidth" in anchor)
  246. ? domGeometry.position(anchor, true)
  247. : anchor;
  248. // Compute position and size of visible part of anchor (it may be partially hidden by ancestor nodes w/scrollbars)
  249. if(anchor.parentNode){
  250. // ignore nodes between position:relative and position:absolute
  251. var sawPosAbsolute = domStyle.getComputedStyle(anchor).position == "absolute";
  252. var parent = anchor.parentNode;
  253. while(parent && parent.nodeType == 1 && parent.nodeName != "BODY"){ //ignoring the body will help performance
  254. var parentPos = domGeometry.position(parent, true),
  255. pcs = domStyle.getComputedStyle(parent);
  256. if(/relative|absolute/.test(pcs.position)){
  257. sawPosAbsolute = false;
  258. }
  259. if(!sawPosAbsolute && /hidden|auto|scroll/.test(pcs.overflow)){
  260. var bottomYCoord = Math.min(aroundNodePos.y + aroundNodePos.h, parentPos.y + parentPos.h);
  261. var rightXCoord = Math.min(aroundNodePos.x + aroundNodePos.w, parentPos.x + parentPos.w);
  262. aroundNodePos.x = Math.max(aroundNodePos.x, parentPos.x);
  263. aroundNodePos.y = Math.max(aroundNodePos.y, parentPos.y);
  264. aroundNodePos.h = bottomYCoord - aroundNodePos.y;
  265. aroundNodePos.w = rightXCoord - aroundNodePos.x;
  266. }
  267. if(pcs.position == "absolute"){
  268. sawPosAbsolute = true;
  269. }
  270. parent = parent.parentNode;
  271. }
  272. }
  273. var x = aroundNodePos.x,
  274. y = aroundNodePos.y,
  275. width = "w" in aroundNodePos ? aroundNodePos.w : (aroundNodePos.w = aroundNodePos.width),
  276. 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);
  277. // Convert positions arguments into choices argument for _place()
  278. var choices = [];
  279. function push(aroundCorner, corner){
  280. choices.push({
  281. aroundCorner: aroundCorner,
  282. corner: corner,
  283. pos: {
  284. x: {
  285. 'L': x,
  286. 'R': x + width,
  287. 'M': x + (width >> 1)
  288. }[aroundCorner.charAt(1)],
  289. y: {
  290. 'T': y,
  291. 'B': y + height,
  292. 'M': y + (height >> 1)
  293. }[aroundCorner.charAt(0)]
  294. }
  295. })
  296. }
  297. array.forEach(positions, function(pos){
  298. var ltr = leftToRight;
  299. switch(pos){
  300. case "above-centered":
  301. push("TM", "BM");
  302. break;
  303. case "below-centered":
  304. push("BM", "TM");
  305. break;
  306. case "after-centered":
  307. ltr = !ltr;
  308. // fall through
  309. case "before-centered":
  310. push(ltr ? "ML" : "MR", ltr ? "MR" : "ML");
  311. break;
  312. case "after":
  313. ltr = !ltr;
  314. // fall through
  315. case "before":
  316. push(ltr ? "TL" : "TR", ltr ? "TR" : "TL");
  317. push(ltr ? "BL" : "BR", ltr ? "BR" : "BL");
  318. break;
  319. case "below-alt":
  320. ltr = !ltr;
  321. // fall through
  322. case "below":
  323. // first try to align left borders, next try to align right borders (or reverse for RTL mode)
  324. push(ltr ? "BL" : "BR", ltr ? "TL" : "TR");
  325. push(ltr ? "BR" : "BL", ltr ? "TR" : "TL");
  326. break;
  327. case "above-alt":
  328. ltr = !ltr;
  329. // fall through
  330. case "above":
  331. // first try to align left borders, next try to align right borders (or reverse for RTL mode)
  332. push(ltr ? "TL" : "TR", ltr ? "BL" : "BR");
  333. push(ltr ? "TR" : "TL", ltr ? "BR" : "BL");
  334. break;
  335. default:
  336. // To assist dijit/_base/place, accept arguments of type {aroundCorner: "BL", corner: "TL"}.
  337. // Not meant to be used directly.
  338. push(pos.aroundCorner, pos.corner);
  339. }
  340. });
  341. var position = _place(node, choices, layoutNode, {w: width, h: height});
  342. position.aroundNodePos = aroundNodePos;
  343. return position;
  344. }
  345. });
  346. });