AccordionContainer.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. /*
  2. Copyright (c) 2004-2012, The Dojo Foundation All Rights Reserved.
  3. Available via Academic Free License >= 2.1 OR the modified BSD license.
  4. see: http://dojotoolkit.org/license for details
  5. */
  6. if(!dojo._hasResource["dijit.layout.AccordionContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit.layout.AccordionContainer"] = true;
  8. dojo.provide("dijit.layout.AccordionContainer");
  9. dojo.require("dijit._Container");
  10. dojo.require("dijit._Templated");
  11. dojo.require("dijit._CssStateMixin");
  12. dojo.require("dijit.layout.StackContainer");
  13. dojo.require("dijit.layout.ContentPane");
  14. dojo.require("dijit.layout.AccordionPane");
  15. //dojo.require("dijit.layout.AccordionPane "); // for back compat, remove for 2.0
  16. // Design notes:
  17. //
  18. // An AccordionContainer is a StackContainer, but each child (typically ContentPane)
  19. // is wrapped in a _AccordionInnerContainer. This is hidden from the caller.
  20. //
  21. // The resulting markup will look like:
  22. //
  23. // <div class=dijitAccordionContainer>
  24. // <div class=dijitAccordionInnerContainer> (one pane)
  25. // <div class=dijitAccordionTitle> (title bar) ... </div>
  26. // <div class=dijtAccordionChildWrapper> (content pane) </div>
  27. // </div>
  28. // </div>
  29. //
  30. // Normally the dijtAccordionChildWrapper is hidden for all but one child (the shown
  31. // child), so the space for the content pane is all the title bars + the one dijtAccordionChildWrapper,
  32. // which on claro has a 1px border plus a 2px bottom margin.
  33. //
  34. // During animation there are two dijtAccordionChildWrapper's shown, so we need
  35. // to compensate for that.
  36. dojo.declare(
  37. "dijit.layout.AccordionContainer",
  38. dijit.layout.StackContainer,
  39. {
  40. // summary:
  41. // Holds a set of panes where every pane's title is visible, but only one pane's content is visible at a time,
  42. // and switching between panes is visualized by sliding the other panes up/down.
  43. // example:
  44. // | <div dojoType="dijit.layout.AccordionContainer">
  45. // | <div dojoType="dijit.layout.ContentPane" title="pane 1">
  46. // | </div>
  47. // | <div dojoType="dijit.layout.ContentPane" title="pane 2">
  48. // | <p>This is some text</p>
  49. // | </div>
  50. // | </div>
  51. // duration: Integer
  52. // Amount of time (in ms) it takes to slide panes
  53. duration: dijit.defaultDuration,
  54. // buttonWidget: [const] String
  55. // The name of the widget used to display the title of each pane
  56. buttonWidget: "dijit.layout._AccordionButton",
  57. /*=====
  58. // _verticalSpace: Number
  59. // Pixels of space available for the open pane
  60. // (my content box size minus the cumulative size of all the title bars)
  61. _verticalSpace: 0,
  62. =====*/
  63. baseClass: "dijitAccordionContainer",
  64. buildRendering: function(){
  65. this.inherited(arguments);
  66. this.domNode.style.overflow = "hidden"; // TODO: put this in dijit.css
  67. dijit.setWaiRole(this.domNode, "tablist"); // TODO: put this in template
  68. },
  69. startup: function(){
  70. if(this._started){ return; }
  71. this.inherited(arguments);
  72. if(this.selectedChildWidget){
  73. var style = this.selectedChildWidget.containerNode.style;
  74. style.display = "";
  75. style.overflow = "auto";
  76. this.selectedChildWidget._wrapperWidget.set("selected", true);
  77. }
  78. },
  79. layout: function(){
  80. // Implement _LayoutWidget.layout() virtual method.
  81. // Set the height of the open pane based on what room remains.
  82. var openPane = this.selectedChildWidget;
  83. if(!openPane){ return;}
  84. // space taken up by title, plus wrapper div (with border/margin) for open pane
  85. var wrapperDomNode = openPane._wrapperWidget.domNode,
  86. wrapperDomNodeMargin = dojo._getMarginExtents(wrapperDomNode),
  87. wrapperDomNodePadBorder = dojo._getPadBorderExtents(wrapperDomNode),
  88. wrapperContainerNode = openPane._wrapperWidget.containerNode,
  89. wrapperContainerNodeMargin = dojo._getMarginExtents(wrapperContainerNode),
  90. wrapperContainerNodePadBorder = dojo._getPadBorderExtents(wrapperContainerNode),
  91. mySize = this._contentBox;
  92. // get cumulative height of all the unselected title bars
  93. var totalCollapsedHeight = 0;
  94. dojo.forEach(this.getChildren(), function(child){
  95. if(child != openPane){
  96. totalCollapsedHeight += dojo._getMarginSize(child._wrapperWidget.domNode).h;
  97. }
  98. });
  99. this._verticalSpace = mySize.h - totalCollapsedHeight - wrapperDomNodeMargin.h
  100. - wrapperDomNodePadBorder.h - wrapperContainerNodeMargin.h - wrapperContainerNodePadBorder.h
  101. - openPane._buttonWidget.getTitleHeight();
  102. // Memo size to make displayed child
  103. this._containerContentBox = {
  104. h: this._verticalSpace,
  105. w: this._contentBox.w - wrapperDomNodeMargin.w - wrapperDomNodePadBorder.w
  106. - wrapperContainerNodeMargin.w - wrapperContainerNodePadBorder.w
  107. };
  108. if(openPane){
  109. openPane.resize(this._containerContentBox);
  110. }
  111. },
  112. _setupChild: function(child){
  113. // Overrides _LayoutWidget._setupChild().
  114. // Put wrapper widget around the child widget, showing title
  115. child._wrapperWidget = new dijit.layout._AccordionInnerContainer({
  116. contentWidget: child,
  117. buttonWidget: this.buttonWidget,
  118. id: child.id + "_wrapper",
  119. dir: child.dir,
  120. lang: child.lang,
  121. parent: this
  122. });
  123. this.inherited(arguments);
  124. },
  125. addChild: function(/*dijit._Widget*/ child, /*Integer?*/ insertIndex){
  126. if(this._started){
  127. // Adding a child to a started Accordion is complicated because children have
  128. // wrapper widgets. Default code path (calling this.inherited()) would add
  129. // the new child inside another child's wrapper.
  130. // First add in child as a direct child of this AccordionContainer
  131. dojo.place(child.domNode, this.containerNode, insertIndex);
  132. if(!child._started){
  133. child.startup();
  134. }
  135. // Then stick the wrapper widget around the child widget
  136. this._setupChild(child);
  137. // Code below copied from StackContainer
  138. dojo.publish(this.id+"-addChild", [child, insertIndex]);
  139. this.layout();
  140. if(!this.selectedChildWidget){
  141. this.selectChild(child);
  142. }
  143. }else{
  144. // We haven't been started yet so just add in the child widget directly,
  145. // and the wrapper will be created on startup()
  146. this.inherited(arguments);
  147. }
  148. },
  149. removeChild: function(child){
  150. // Overrides _LayoutWidget.removeChild().
  151. // Destroy wrapper widget first, before StackContainer.getChildren() call.
  152. // Replace wrapper widget with true child widget (ContentPane etc.).
  153. // This step only happens if the AccordionContainer has been started; otherwise there's no wrapper.
  154. if(child._wrapperWidget){
  155. dojo.place(child.domNode, child._wrapperWidget.domNode, "after");
  156. child._wrapperWidget.destroy();
  157. delete child._wrapperWidget;
  158. }
  159. dojo.removeClass(child.domNode, "dijitHidden");
  160. this.inherited(arguments);
  161. },
  162. getChildren: function(){
  163. // Overrides _Container.getChildren() to return content panes rather than internal AccordionInnerContainer panes
  164. return dojo.map(this.inherited(arguments), function(child){
  165. return child.declaredClass == "dijit.layout._AccordionInnerContainer" ? child.contentWidget : child;
  166. }, this);
  167. },
  168. destroy: function(){
  169. if(this._animation){
  170. this._animation.stop();
  171. }
  172. dojo.forEach(this.getChildren(), function(child){
  173. // If AccordionContainer has been started, then each child has a wrapper widget which
  174. // also needs to be destroyed.
  175. if(child._wrapperWidget){
  176. child._wrapperWidget.destroy();
  177. }else{
  178. child.destroyRecursive();
  179. }
  180. });
  181. this.inherited(arguments);
  182. },
  183. _showChild: function(child){
  184. // Override StackContainer._showChild() to set visibility of _wrapperWidget.containerNode
  185. child._wrapperWidget.containerNode.style.display="block";
  186. return this.inherited(arguments);
  187. },
  188. _hideChild: function(child){
  189. // Override StackContainer._showChild() to set visibility of _wrapperWidget.containerNode
  190. child._wrapperWidget.containerNode.style.display="none";
  191. this.inherited(arguments);
  192. },
  193. _transition: function(/*dijit._Widget?*/ newWidget, /*dijit._Widget?*/ oldWidget, /*Boolean*/ animate){
  194. // Overrides StackContainer._transition() to provide sliding of title bars etc.
  195. if(dojo.isIE < 8){
  196. // workaround animation bugs by not animating; not worth supporting animation for IE6 & 7
  197. animate = false;
  198. }
  199. if(this._animation){
  200. // there's an in-progress animation. speedily end it so we can do the newly requested one
  201. this._animation.stop(true);
  202. delete this._animation;
  203. }
  204. var self = this;
  205. if(newWidget){
  206. newWidget._wrapperWidget.set("selected", true);
  207. var d = this._showChild(newWidget); // prepare widget to be slid in
  208. // Size the new widget, in case this is the first time it's being shown,
  209. // or I have been resized since the last time it was shown.
  210. // Note that page must be visible for resizing to work.
  211. if(this.doLayout && newWidget.resize){
  212. newWidget.resize(this._containerContentBox);
  213. }
  214. }
  215. if(oldWidget){
  216. oldWidget._wrapperWidget.set("selected", false);
  217. if(!animate){
  218. this._hideChild(oldWidget);
  219. }
  220. }
  221. if(animate){
  222. var newContents = newWidget._wrapperWidget.containerNode,
  223. oldContents = oldWidget._wrapperWidget.containerNode;
  224. // During the animation we will be showing two dijitAccordionChildWrapper nodes at once,
  225. // which on claro takes up 4px extra space (compared to stable AccordionContainer).
  226. // Have to compensate for that by immediately shrinking the pane being closed.
  227. var wrapperContainerNode = newWidget._wrapperWidget.containerNode,
  228. wrapperContainerNodeMargin = dojo._getMarginExtents(wrapperContainerNode),
  229. wrapperContainerNodePadBorder = dojo._getPadBorderExtents(wrapperContainerNode),
  230. animationHeightOverhead = wrapperContainerNodeMargin.h + wrapperContainerNodePadBorder.h;
  231. oldContents.style.height = (self._verticalSpace - animationHeightOverhead) + "px";
  232. this._animation = new dojo.Animation({
  233. node: newContents,
  234. duration: this.duration,
  235. curve: [1, this._verticalSpace - animationHeightOverhead - 1],
  236. onAnimate: function(value){
  237. value = Math.floor(value); // avoid fractional values
  238. newContents.style.height = value + "px";
  239. oldContents.style.height = (self._verticalSpace - animationHeightOverhead - value) + "px";
  240. },
  241. onEnd: function(){
  242. delete self._animation;
  243. newContents.style.height = "auto";
  244. oldWidget._wrapperWidget.containerNode.style.display = "none";
  245. oldContents.style.height = "auto";
  246. self._hideChild(oldWidget);
  247. }
  248. });
  249. this._animation.onStop = this._animation.onEnd;
  250. this._animation.play();
  251. }
  252. return d; // If child has an href, promise that fires when the widget has finished loading
  253. },
  254. // note: we are treating the container as controller here
  255. _onKeyPress: function(/*Event*/ e, /*dijit._Widget*/ fromTitle){
  256. // summary:
  257. // Handle keypress events
  258. // description:
  259. // This is called from a handler on AccordionContainer.domNode
  260. // (setup in StackContainer), and is also called directly from
  261. // the click handler for accordion labels
  262. if(this.disabled || e.altKey || !(fromTitle || e.ctrlKey)){
  263. return;
  264. }
  265. var k = dojo.keys,
  266. c = e.charOrCode;
  267. if((fromTitle && (c == k.LEFT_ARROW || c == k.UP_ARROW)) ||
  268. (e.ctrlKey && c == k.PAGE_UP)){
  269. this._adjacent(false)._buttonWidget._onTitleClick();
  270. dojo.stopEvent(e);
  271. }else if((fromTitle && (c == k.RIGHT_ARROW || c == k.DOWN_ARROW)) ||
  272. (e.ctrlKey && (c == k.PAGE_DOWN || c == k.TAB))){
  273. this._adjacent(true)._buttonWidget._onTitleClick();
  274. dojo.stopEvent(e);
  275. }
  276. }
  277. }
  278. );
  279. dojo.declare("dijit.layout._AccordionInnerContainer",
  280. [dijit._Widget, dijit._CssStateMixin], {
  281. // summary:
  282. // Internal widget placed as direct child of AccordionContainer.containerNode.
  283. // When other widgets are added as children to an AccordionContainer they are wrapped in
  284. // this widget.
  285. /*=====
  286. // buttonWidget: String
  287. // Name of class to use to instantiate title
  288. // (Wish we didn't have a separate widget for just the title but maintaining it
  289. // for backwards compatibility, is it worth it?)
  290. buttonWidget: null,
  291. =====*/
  292. /*=====
  293. // contentWidget: dijit._Widget
  294. // Pointer to the real child widget
  295. contentWidget: null,
  296. =====*/
  297. baseClass: "dijitAccordionInnerContainer",
  298. // tell nested layout widget that we will take care of sizing
  299. isContainer: true,
  300. isLayoutContainer: true,
  301. buildRendering: function(){
  302. // Builds a template like:
  303. // <div class=dijitAccordionInnerContainer>
  304. // Button
  305. // <div class=dijitAccordionChildWrapper>
  306. // ContentPane
  307. // </div>
  308. // </div>
  309. // Create wrapper div, placed where the child is now
  310. this.domNode = dojo.place("<div class='" + this.baseClass + "'>", this.contentWidget.domNode, "after");
  311. // wrapper div's first child is the button widget (ie, the title bar)
  312. var child = this.contentWidget,
  313. cls = dojo.getObject(this.buttonWidget);
  314. this.button = child._buttonWidget = (new cls({
  315. contentWidget: child,
  316. label: child.title,
  317. title: child.tooltip,
  318. dir: child.dir,
  319. lang: child.lang,
  320. iconClass: child.iconClass,
  321. id: child.id + "_button",
  322. parent: this.parent
  323. })).placeAt(this.domNode);
  324. // and then the actual content widget (changing it from prior-sibling to last-child),
  325. // wrapped by a <div class=dijitAccordionChildWrapper>
  326. this.containerNode = dojo.place("<div class='dijitAccordionChildWrapper' style='display:none'>", this.domNode);
  327. dojo.place(this.contentWidget.domNode, this.containerNode);
  328. },
  329. postCreate: function(){
  330. this.inherited(arguments);
  331. // Map changes in content widget's title etc. to changes in the button
  332. var button = this.button;
  333. this._contentWidgetWatches = [
  334. this.contentWidget.watch('title', dojo.hitch(this, function(name, oldValue, newValue){
  335. button.set("label", newValue);
  336. })),
  337. this.contentWidget.watch('tooltip', dojo.hitch(this, function(name, oldValue, newValue){
  338. button.set("title", newValue);
  339. })),
  340. this.contentWidget.watch('iconClass', dojo.hitch(this, function(name, oldValue, newValue){
  341. button.set("iconClass", newValue);
  342. }))
  343. ];
  344. },
  345. _setSelectedAttr: function(/*Boolean*/ isSelected){
  346. this._set("selected", isSelected);
  347. this.button.set("selected", isSelected);
  348. if(isSelected){
  349. var cw = this.contentWidget;
  350. if(cw.onSelected){ cw.onSelected(); }
  351. }
  352. },
  353. startup: function(){
  354. // Called by _Container.addChild()
  355. this.contentWidget.startup();
  356. },
  357. destroy: function(){
  358. this.button.destroyRecursive();
  359. dojo.forEach(this._contentWidgetWatches || [], function(w){ w.unwatch(); });
  360. delete this.contentWidget._buttonWidget;
  361. delete this.contentWidget._wrapperWidget;
  362. this.inherited(arguments);
  363. },
  364. destroyDescendants: function(){
  365. // since getChildren isn't working for me, have to code this manually
  366. this.contentWidget.destroyRecursive();
  367. }
  368. });
  369. dojo.declare("dijit.layout._AccordionButton",
  370. [dijit._Widget, dijit._Templated, dijit._CssStateMixin],
  371. {
  372. // summary:
  373. // The title bar to click to open up an accordion pane.
  374. // Internal widget used by AccordionContainer.
  375. // tags:
  376. // private
  377. templateString: dojo.cache("dijit.layout", "templates/AccordionButton.html", "<div dojoAttachEvent='onclick:_onTitleClick' class='dijitAccordionTitle'>\n\t<div dojoAttachPoint='titleNode,focusNode' dojoAttachEvent='onkeypress:_onTitleKeyPress'\n\t\t\tclass='dijitAccordionTitleFocus' role=\"tab\" aria-expanded=\"false\"\n\t\t><span class='dijitInline dijitAccordionArrow' role=\"presentation\"></span\n\t\t><span class='arrowTextUp' role=\"presentation\">+</span\n\t\t><span class='arrowTextDown' role=\"presentation\">-</span\n\t\t><img src=\"${_blankGif}\" alt=\"\" class=\"dijitIcon\" dojoAttachPoint='iconNode' style=\"vertical-align: middle\" role=\"presentation\"/>\n\t\t<span role=\"presentation\" dojoAttachPoint='titleTextNode' class='dijitAccordionText'></span>\n\t</div>\n</div>\n"),
  378. attributeMap: dojo.mixin(dojo.clone(dijit.layout.ContentPane.prototype.attributeMap), {
  379. label: {node: "titleTextNode", type: "innerHTML" },
  380. title: {node: "titleTextNode", type: "attribute", attribute: "title"},
  381. iconClass: { node: "iconNode", type: "class" }
  382. }),
  383. baseClass: "dijitAccordionTitle",
  384. getParent: function(){
  385. // summary:
  386. // Returns the AccordionContainer parent.
  387. // tags:
  388. // private
  389. return this.parent;
  390. },
  391. buildRendering: function(){
  392. this.inherited(arguments);
  393. var titleTextNodeId = this.id.replace(' ','_');
  394. dojo.attr(this.titleTextNode, "id", titleTextNodeId+"_title");
  395. dijit.setWaiState(this.focusNode, "labelledby", dojo.attr(this.titleTextNode, "id"));
  396. dojo.setSelectable(this.domNode, false);
  397. },
  398. getTitleHeight: function(){
  399. // summary:
  400. // Returns the height of the title dom node.
  401. return dojo._getMarginSize(this.domNode).h; // Integer
  402. },
  403. // TODO: maybe the parent should set these methods directly rather than forcing the code
  404. // into the button widget?
  405. _onTitleClick: function(){
  406. // summary:
  407. // Callback when someone clicks my title.
  408. var parent = this.getParent();
  409. parent.selectChild(this.contentWidget, true);
  410. dijit.focus(this.focusNode);
  411. },
  412. _onTitleKeyPress: function(/*Event*/ evt){
  413. return this.getParent()._onKeyPress(evt, this.contentWidget);
  414. },
  415. _setSelectedAttr: function(/*Boolean*/ isSelected){
  416. this._set("selected", isSelected);
  417. dijit.setWaiState(this.focusNode, "expanded", isSelected);
  418. dijit.setWaiState(this.focusNode, "selected", isSelected);
  419. this.focusNode.setAttribute("tabIndex", isSelected ? "0" : "-1");
  420. }
  421. });
  422. }