BorderContainer.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. define("dijit/layout/BorderContainer", [
  2. "dojo/_base/array", // array.filter array.forEach array.map
  3. "dojo/cookie", // cookie
  4. "dojo/_base/declare", // declare
  5. "dojo/dom-class", // domClass.add domClass.remove domClass.toggle
  6. "dojo/dom-construct", // domConstruct.destroy domConstruct.place
  7. "dojo/dom-geometry", // domGeometry.marginBox
  8. "dojo/dom-style", // domStyle.style
  9. "dojo/_base/event", // event.stop
  10. "dojo/keys",
  11. "dojo/_base/lang", // lang.getObject lang.hitch
  12. "dojo/on",
  13. "dojo/touch",
  14. "dojo/_base/window", // win.body win.doc win.doc.createElement
  15. "../_WidgetBase",
  16. "../_Widget",
  17. "../_TemplatedMixin",
  18. "./_LayoutWidget",
  19. "./utils" // layoutUtils.layoutChildren
  20. ], function(array, cookie, declare, domClass, domConstruct, domGeometry, domStyle, event, keys, lang, on, touch, win,
  21. _WidgetBase, _Widget, _TemplatedMixin, _LayoutWidget, layoutUtils){
  22. /*=====
  23. var _WidgetBase = dijit._WidgetBase;
  24. var _Widget = dijit._Widget;
  25. var _TemplatedMixin = dijit._TemplatedMixin;
  26. var _LayoutWidget = dijit.layout._LayoutWidget;
  27. =====*/
  28. // module:
  29. // dijit/layout/BorderContainer
  30. // summary:
  31. // Provides layout in up to 5 regions, a mandatory center with optional borders along its 4 sides.
  32. var _Splitter = declare("dijit.layout._Splitter", [_Widget, _TemplatedMixin ],
  33. {
  34. // summary:
  35. // A draggable spacer between two items in a `dijit.layout.BorderContainer`.
  36. // description:
  37. // This is instantiated by `dijit.layout.BorderContainer`. Users should not
  38. // create it directly.
  39. // tags:
  40. // private
  41. /*=====
  42. // container: [const] dijit.layout.BorderContainer
  43. // Pointer to the parent BorderContainer
  44. container: null,
  45. // child: [const] dijit.layout._LayoutWidget
  46. // Pointer to the pane associated with this splitter
  47. child: null,
  48. // region: [const] String
  49. // Region of pane associated with this splitter.
  50. // "top", "bottom", "left", "right".
  51. region: null,
  52. =====*/
  53. // live: [const] Boolean
  54. // If true, the child's size changes and the child widget is redrawn as you drag the splitter;
  55. // otherwise, the size doesn't change until you drop the splitter (by mouse-up)
  56. live: true,
  57. templateString: '<div class="dijitSplitter" data-dojo-attach-event="onkeypress:_onKeyPress,press:_startDrag,onmouseenter:_onMouse,onmouseleave:_onMouse" tabIndex="0" role="separator"><div class="dijitSplitterThumb"></div></div>',
  58. constructor: function(){
  59. this._handlers = [];
  60. },
  61. postMixInProperties: function(){
  62. this.inherited(arguments);
  63. this.horizontal = /top|bottom/.test(this.region);
  64. this._factor = /top|left/.test(this.region) ? 1 : -1;
  65. this._cookieName = this.container.id + "_" + this.region;
  66. },
  67. buildRendering: function(){
  68. this.inherited(arguments);
  69. domClass.add(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V"));
  70. if(this.container.persist){
  71. // restore old size
  72. var persistSize = cookie(this._cookieName);
  73. if(persistSize){
  74. this.child.domNode.style[this.horizontal ? "height" : "width"] = persistSize;
  75. }
  76. }
  77. },
  78. _computeMaxSize: function(){
  79. // summary:
  80. // Return the maximum size that my corresponding pane can be set to
  81. var dim = this.horizontal ? 'h' : 'w',
  82. childSize = domGeometry.getMarginBox(this.child.domNode)[dim],
  83. center = array.filter(this.container.getChildren(), function(child){ return child.region == "center";})[0],
  84. spaceAvailable = domGeometry.getMarginBox(center.domNode)[dim]; // can expand until center is crushed to 0
  85. return Math.min(this.child.maxSize, childSize + spaceAvailable);
  86. },
  87. _startDrag: function(e){
  88. if(!this.cover){
  89. this.cover = win.doc.createElement('div');
  90. domClass.add(this.cover, "dijitSplitterCover");
  91. domConstruct.place(this.cover, this.child.domNode, "after");
  92. }
  93. domClass.add(this.cover, "dijitSplitterCoverActive");
  94. // Safeguard in case the stop event was missed. Shouldn't be necessary if we always get the mouse up.
  95. if(this.fake){ domConstruct.destroy(this.fake); }
  96. if(!(this._resize = this.live)){ //TODO: disable live for IE6?
  97. // create fake splitter to display at old position while we drag
  98. (this.fake = this.domNode.cloneNode(true)).removeAttribute("id");
  99. domClass.add(this.domNode, "dijitSplitterShadow");
  100. domConstruct.place(this.fake, this.domNode, "after");
  101. }
  102. domClass.add(this.domNode, "dijitSplitterActive dijitSplitter" + (this.horizontal ? "H" : "V") + "Active");
  103. if(this.fake){
  104. domClass.remove(this.fake, "dijitSplitterHover dijitSplitter" + (this.horizontal ? "H" : "V") + "Hover");
  105. }
  106. //Performance: load data info local vars for onmousevent function closure
  107. var factor = this._factor,
  108. isHorizontal = this.horizontal,
  109. axis = isHorizontal ? "pageY" : "pageX",
  110. pageStart = e[axis],
  111. splitterStyle = this.domNode.style,
  112. dim = isHorizontal ? 'h' : 'w',
  113. childStart = domGeometry.getMarginBox(this.child.domNode)[dim],
  114. max = this._computeMaxSize(),
  115. min = this.child.minSize || 20,
  116. region = this.region,
  117. splitterAttr = region == "top" || region == "bottom" ? "top" : "left", // style attribute of splitter to adjust
  118. splitterStart = parseInt(splitterStyle[splitterAttr], 10),
  119. resize = this._resize,
  120. layoutFunc = lang.hitch(this.container, "_layoutChildren", this.child.id),
  121. de = win.doc;
  122. this._handlers = this._handlers.concat([
  123. on(de, touch.move, this._drag = function(e, forceResize){
  124. var delta = e[axis] - pageStart,
  125. childSize = factor * delta + childStart,
  126. boundChildSize = Math.max(Math.min(childSize, max), min);
  127. if(resize || forceResize){
  128. layoutFunc(boundChildSize);
  129. }
  130. // TODO: setting style directly (usually) sets content box size, need to set margin box size
  131. splitterStyle[splitterAttr] = delta + splitterStart + factor*(boundChildSize - childSize) + "px";
  132. }),
  133. on(de, "dragstart", event.stop),
  134. on(win.body(), "selectstart", event.stop),
  135. on(de, touch.release, lang.hitch(this, "_stopDrag"))
  136. ]);
  137. event.stop(e);
  138. },
  139. _onMouse: function(e){
  140. // summary:
  141. // Handler for onmouseenter / onmouseleave events
  142. var o = (e.type == "mouseover" || e.type == "mouseenter");
  143. domClass.toggle(this.domNode, "dijitSplitterHover", o);
  144. domClass.toggle(this.domNode, "dijitSplitter" + (this.horizontal ? "H" : "V") + "Hover", o);
  145. },
  146. _stopDrag: function(e){
  147. try{
  148. if(this.cover){
  149. domClass.remove(this.cover, "dijitSplitterCoverActive");
  150. }
  151. if(this.fake){ domConstruct.destroy(this.fake); }
  152. domClass.remove(this.domNode, "dijitSplitterActive dijitSplitter"
  153. + (this.horizontal ? "H" : "V") + "Active dijitSplitterShadow");
  154. this._drag(e); //TODO: redundant with onmousemove?
  155. this._drag(e, true);
  156. }finally{
  157. this._cleanupHandlers();
  158. delete this._drag;
  159. }
  160. if(this.container.persist){
  161. cookie(this._cookieName, this.child.domNode.style[this.horizontal ? "height" : "width"], {expires:365});
  162. }
  163. },
  164. _cleanupHandlers: function(){
  165. var h;
  166. while(h = this._handlers.pop()){ h.remove(); }
  167. },
  168. _onKeyPress: function(/*Event*/ e){
  169. // should we apply typematic to this?
  170. this._resize = true;
  171. var horizontal = this.horizontal;
  172. var tick = 1;
  173. switch(e.charOrCode){
  174. case horizontal ? keys.UP_ARROW : keys.LEFT_ARROW:
  175. tick *= -1;
  176. // break;
  177. case horizontal ? keys.DOWN_ARROW : keys.RIGHT_ARROW:
  178. break;
  179. default:
  180. // this.inherited(arguments);
  181. return;
  182. }
  183. var childSize = domGeometry.getMarginSize(this.child.domNode)[ horizontal ? 'h' : 'w' ] + this._factor * tick;
  184. this.container._layoutChildren(this.child.id, Math.max(Math.min(childSize, this._computeMaxSize()), this.child.minSize));
  185. event.stop(e);
  186. },
  187. destroy: function(){
  188. this._cleanupHandlers();
  189. delete this.child;
  190. delete this.container;
  191. delete this.cover;
  192. delete this.fake;
  193. this.inherited(arguments);
  194. }
  195. });
  196. var _Gutter = declare("dijit.layout._Gutter", [_Widget, _TemplatedMixin],
  197. {
  198. // summary:
  199. // Just a spacer div to separate side pane from center pane.
  200. // Basically a trick to lookup the gutter/splitter width from the theme.
  201. // description:
  202. // Instantiated by `dijit.layout.BorderContainer`. Users should not
  203. // create directly.
  204. // tags:
  205. // private
  206. templateString: '<div class="dijitGutter" role="presentation"></div>',
  207. postMixInProperties: function(){
  208. this.inherited(arguments);
  209. this.horizontal = /top|bottom/.test(this.region);
  210. },
  211. buildRendering: function(){
  212. this.inherited(arguments);
  213. domClass.add(this.domNode, "dijitGutter" + (this.horizontal ? "H" : "V"));
  214. }
  215. });
  216. var BorderContainer = declare("dijit.layout.BorderContainer", _LayoutWidget, {
  217. // summary:
  218. // Provides layout in up to 5 regions, a mandatory center with optional borders along its 4 sides.
  219. //
  220. // description:
  221. // A BorderContainer is a box with a specified size, such as style="width: 500px; height: 500px;",
  222. // that contains a child widget marked region="center" and optionally children widgets marked
  223. // region equal to "top", "bottom", "leading", "trailing", "left" or "right".
  224. // Children along the edges will be laid out according to width or height dimensions and may
  225. // include optional splitters (splitter="true") to make them resizable by the user. The remaining
  226. // space is designated for the center region.
  227. //
  228. // The outer size must be specified on the BorderContainer node. Width must be specified for the sides
  229. // and height for the top and bottom, respectively. No dimensions should be specified on the center;
  230. // it will fill the remaining space. Regions named "leading" and "trailing" may be used just like
  231. // "left" and "right" except that they will be reversed in right-to-left environments.
  232. //
  233. // For complex layouts, multiple children can be specified for a single region. In this case, the
  234. // layoutPriority flag on the children determines which child is closer to the edge (low layoutPriority)
  235. // and which child is closer to the center (high layoutPriority). layoutPriority can also be used
  236. // instead of the design attribute to control layout precedence of horizontal vs. vertical panes.
  237. // example:
  238. // | <div data-dojo-type="dijit.layout.BorderContainer" data-dojo-props="design: 'sidebar', gutters: false"
  239. // | style="width: 400px; height: 300px;">
  240. // | <div data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region: 'top'">header text</div>
  241. // | <div data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region: 'right', splitter: true" style="width: 200px;">table of contents</div>
  242. // | <div data-dojo-type="dijit.layout.ContentPane" data-dojo-props="region: 'center'">client area</div>
  243. // | </div>
  244. // design: String
  245. // Which design is used for the layout:
  246. // - "headline" (default) where the top and bottom extend
  247. // the full width of the container
  248. // - "sidebar" where the left and right sides extend from top to bottom.
  249. design: "headline",
  250. // gutters: [const] Boolean
  251. // Give each pane a border and margin.
  252. // Margin determined by domNode.paddingLeft.
  253. // When false, only resizable panes have a gutter (i.e. draggable splitter) for resizing.
  254. gutters: true,
  255. // liveSplitters: [const] Boolean
  256. // Specifies whether splitters resize as you drag (true) or only upon mouseup (false)
  257. liveSplitters: true,
  258. // persist: Boolean
  259. // Save splitter positions in a cookie.
  260. persist: false,
  261. baseClass: "dijitBorderContainer",
  262. // _splitterClass: Function||String
  263. // Optional hook to override the default Splitter widget used by BorderContainer
  264. _splitterClass: _Splitter,
  265. postMixInProperties: function(){
  266. // change class name to indicate that BorderContainer is being used purely for
  267. // layout (like LayoutContainer) rather than for pretty formatting.
  268. if(!this.gutters){
  269. this.baseClass += "NoGutter";
  270. }
  271. this.inherited(arguments);
  272. },
  273. startup: function(){
  274. if(this._started){ return; }
  275. array.forEach(this.getChildren(), this._setupChild, this);
  276. this.inherited(arguments);
  277. },
  278. _setupChild: function(/*dijit._Widget*/ child){
  279. // Override _LayoutWidget._setupChild().
  280. var region = child.region;
  281. if(region){
  282. this.inherited(arguments);
  283. domClass.add(child.domNode, this.baseClass+"Pane");
  284. var ltr = this.isLeftToRight();
  285. if(region == "leading"){ region = ltr ? "left" : "right"; }
  286. if(region == "trailing"){ region = ltr ? "right" : "left"; }
  287. // Create draggable splitter for resizing pane,
  288. // or alternately if splitter=false but BorderContainer.gutters=true then
  289. // insert dummy div just for spacing
  290. if(region != "center" && (child.splitter || this.gutters) && !child._splitterWidget){
  291. var _Splitter = child.splitter ? this._splitterClass : _Gutter;
  292. if(lang.isString(_Splitter)){
  293. _Splitter = lang.getObject(_Splitter); // for back-compat, remove in 2.0
  294. }
  295. var splitter = new _Splitter({
  296. id: child.id + "_splitter",
  297. container: this,
  298. child: child,
  299. region: region,
  300. live: this.liveSplitters
  301. });
  302. splitter.isSplitter = true;
  303. child._splitterWidget = splitter;
  304. domConstruct.place(splitter.domNode, child.domNode, "after");
  305. // Splitters aren't added as Contained children, so we need to call startup explicitly
  306. splitter.startup();
  307. }
  308. child.region = region; // TODO: technically wrong since it overwrites "trailing" with "left" etc.
  309. }
  310. },
  311. layout: function(){
  312. // Implement _LayoutWidget.layout() virtual method.
  313. this._layoutChildren();
  314. },
  315. addChild: function(/*dijit._Widget*/ child, /*Integer?*/ insertIndex){
  316. // Override _LayoutWidget.addChild().
  317. this.inherited(arguments);
  318. if(this._started){
  319. this.layout(); //OPT
  320. }
  321. },
  322. removeChild: function(/*dijit._Widget*/ child){
  323. // Override _LayoutWidget.removeChild().
  324. var region = child.region;
  325. var splitter = child._splitterWidget;
  326. if(splitter){
  327. splitter.destroy();
  328. delete child._splitterWidget;
  329. }
  330. this.inherited(arguments);
  331. if(this._started){
  332. this._layoutChildren();
  333. }
  334. // Clean up whatever style changes we made to the child pane.
  335. // Unclear how height and width should be handled.
  336. domClass.remove(child.domNode, this.baseClass+"Pane");
  337. domStyle.set(child.domNode, {
  338. top: "auto",
  339. bottom: "auto",
  340. left: "auto",
  341. right: "auto",
  342. position: "static"
  343. });
  344. domStyle.set(child.domNode, region == "top" || region == "bottom" ? "width" : "height", "auto");
  345. },
  346. getChildren: function(){
  347. // Override _LayoutWidget.getChildren() to only return real children, not the splitters.
  348. return array.filter(this.inherited(arguments), function(widget){
  349. return !widget.isSplitter;
  350. });
  351. },
  352. // TODO: remove in 2.0
  353. getSplitter: function(/*String*/region){
  354. // summary:
  355. // Returns the widget responsible for rendering the splitter associated with region
  356. // tags:
  357. // deprecated
  358. return array.filter(this.getChildren(), function(child){
  359. return child.region == region;
  360. })[0]._splitterWidget;
  361. },
  362. resize: function(newSize, currentSize){
  363. // Overrides _LayoutWidget.resize().
  364. // resetting potential padding to 0px to provide support for 100% width/height + padding
  365. // TODO: this hack doesn't respect the box model and is a temporary fix
  366. if(!this.cs || !this.pe){
  367. var node = this.domNode;
  368. this.cs = domStyle.getComputedStyle(node);
  369. this.pe = domGeometry.getPadExtents(node, this.cs);
  370. this.pe.r = domStyle.toPixelValue(node, this.cs.paddingRight);
  371. this.pe.b = domStyle.toPixelValue(node, this.cs.paddingBottom);
  372. domStyle.set(node, "padding", "0px");
  373. }
  374. this.inherited(arguments);
  375. },
  376. _layoutChildren: function(/*String?*/ changedChildId, /*Number?*/ changedChildSize){
  377. // summary:
  378. // This is the main routine for setting size/position of each child.
  379. // description:
  380. // With no arguments, measures the height of top/bottom panes, the width
  381. // of left/right panes, and then sizes all panes accordingly.
  382. //
  383. // With changedRegion specified (as "left", "top", "bottom", or "right"),
  384. // it changes that region's width/height to changedRegionSize and
  385. // then resizes other regions that were affected.
  386. // changedChildId:
  387. // Id of the child which should be resized because splitter was dragged.
  388. // changedChildSize:
  389. // The new width/height (in pixels) to make specified child
  390. if(!this._borderBox || !this._borderBox.h){
  391. // We are currently hidden, or we haven't been sized by our parent yet.
  392. // Abort. Someone will resize us later.
  393. return;
  394. }
  395. // Generate list of wrappers of my children in the order that I want layoutChildren()
  396. // to process them (i.e. from the outside to the inside)
  397. var wrappers = array.map(this.getChildren(), function(child, idx){
  398. return {
  399. pane: child,
  400. weight: [
  401. child.region == "center" ? Infinity : 0,
  402. child.layoutPriority,
  403. (this.design == "sidebar" ? 1 : -1) * (/top|bottom/.test(child.region) ? 1 : -1),
  404. idx
  405. ]
  406. };
  407. }, this);
  408. wrappers.sort(function(a, b){
  409. var aw = a.weight, bw = b.weight;
  410. for(var i=0; i<aw.length; i++){
  411. if(aw[i] != bw[i]){
  412. return aw[i] - bw[i];
  413. }
  414. }
  415. return 0;
  416. });
  417. // Make new list, combining the externally specified children with splitters and gutters
  418. var childrenAndSplitters = [];
  419. array.forEach(wrappers, function(wrapper){
  420. var pane = wrapper.pane;
  421. childrenAndSplitters.push(pane);
  422. if(pane._splitterWidget){
  423. childrenAndSplitters.push(pane._splitterWidget);
  424. }
  425. });
  426. // Compute the box in which to lay out my children
  427. var dim = {
  428. l: this.pe.l,
  429. t: this.pe.t,
  430. w: this._borderBox.w - this.pe.w,
  431. h: this._borderBox.h - this.pe.h
  432. };
  433. // Layout the children, possibly changing size due to a splitter drag
  434. layoutUtils.layoutChildren(this.domNode, dim, childrenAndSplitters,
  435. changedChildId, changedChildSize);
  436. },
  437. destroyRecursive: function(){
  438. // Destroy splitters first, while getChildren() still works
  439. array.forEach(this.getChildren(), function(child){
  440. var splitter = child._splitterWidget;
  441. if(splitter){
  442. splitter.destroy();
  443. }
  444. delete child._splitterWidget;
  445. });
  446. // Then destroy the real children, and myself
  447. this.inherited(arguments);
  448. }
  449. });
  450. // This argument can be specified for the children of a BorderContainer.
  451. // Since any widget can be specified as a LayoutContainer child, mix it
  452. // into the base widget class. (This is a hack, but it's effective.)
  453. lang.extend(_WidgetBase, {
  454. // region: [const] String
  455. // Parameter for children of `dijit.layout.BorderContainer`.
  456. // Values: "top", "bottom", "leading", "trailing", "left", "right", "center".
  457. // See the `dijit.layout.BorderContainer` description for details.
  458. region: '',
  459. // layoutPriority: [const] Number
  460. // Parameter for children of `dijit.layout.BorderContainer`.
  461. // Children with a higher layoutPriority will be placed closer to the BorderContainer center,
  462. // between children with a lower layoutPriority.
  463. layoutPriority: 0,
  464. // splitter: [const] Boolean
  465. // Parameter for child of `dijit.layout.BorderContainer` where region != "center".
  466. // If true, enables user to resize the widget by putting a draggable splitter between
  467. // this widget and the region=center widget.
  468. splitter: false,
  469. // minSize: [const] Number
  470. // Parameter for children of `dijit.layout.BorderContainer`.
  471. // Specifies a minimum size (in pixels) for this widget when resized by a splitter.
  472. minSize: 0,
  473. // maxSize: [const] Number
  474. // Parameter for children of `dijit.layout.BorderContainer`.
  475. // Specifies a maximum size (in pixels) for this widget when resized by a splitter.
  476. maxSize: Infinity
  477. });
  478. // For monkey patching
  479. BorderContainer._Splitter = _Splitter;
  480. BorderContainer._Gutter = _Gutter;
  481. return BorderContainer;
  482. });