AccordionContainer.js 20 KB

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