StackContainer.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. if (!dojo._hasResource["dijit.layout.StackContainer"]) { // _hasResource
  2. // checks added by
  3. // build. Do not use
  4. // _hasResource
  5. // directly in your
  6. // code.
  7. dojo._hasResource["dijit.layout.StackContainer"] = true;
  8. dojo.provide("dijit.layout.StackContainer");
  9. dojo.require("dijit._Templated");
  10. dojo.require("dijit.layout._LayoutWidget");
  11. dojo.require("dijit.form.Button");
  12. dojo.declare("dijit.layout.StackContainer", dijit.layout._LayoutWidget,
  13. // summary
  14. // A container that has multiple children, but shows only
  15. // one child at a time (like looking at the pages in a book one by
  16. // one).
  17. //
  18. // Publishes topics <widgetId>-addChild, <widgetId>-removeChild, and
  19. // <widgetId>-selectChild
  20. //
  21. // Can be base class for container, Wizard, Show, etc.
  22. {
  23. // doLayout: Boolean
  24. // if true, change the size of my currently displayed child to match my
  25. // size
  26. doLayout : true,
  27. _started : false,
  28. // selectedChildWidget: Widget
  29. // References the currently selected child widget, if any
  30. postCreate : function() {
  31. dijit.setWaiRole((this.containerNode || this.domNode), "tabpanel");
  32. this.connect(this.domNode, "onkeypress", this._onKeyPress);
  33. },
  34. startup : function() {
  35. if (this._started) {
  36. return;
  37. }
  38. var children = this.getChildren();
  39. // Setup each page panel
  40. dojo.forEach(children, this._setupChild, this);
  41. // Figure out which child to initially display
  42. dojo.some(children, function(child) {
  43. if (child.selected) {
  44. this.selectedChildWidget = child;
  45. }
  46. return child.selected;
  47. }, this);
  48. var selected = this.selectedChildWidget;
  49. // Default to the first child
  50. if (!selected && children[0]) {
  51. selected = this.selectedChildWidget = children[0];
  52. selected.selected = true;
  53. }
  54. if (selected) {
  55. this._showChild(selected);
  56. }
  57. // Now publish information about myself so any StackControllers can
  58. // initialize..
  59. dojo.publish(this.id + "-startup", [{
  60. children : children,
  61. selected : selected
  62. }]);
  63. this.inherited("startup", arguments);
  64. this._started = true;
  65. },
  66. _setupChild : function(/* Widget */page) {
  67. // Summary: prepare the given child
  68. page.domNode.style.display = "none";
  69. // since we are setting the width/height of the child elements, they
  70. // need
  71. // to be position:relative, or IE has problems (See bug #2033)
  72. page.domNode.style.position = "relative";
  73. return page; // dijit._Widget
  74. },
  75. addChild : function(/* Widget */child, /* Integer? */insertIndex) {
  76. // summary: Adds a widget to the stack
  77. dijit._Container.prototype.addChild.apply(this, arguments);
  78. child = this._setupChild(child);
  79. if (this._started) {
  80. // in case the tab titles have overflowed from one line to two
  81. // lines
  82. this.layout();
  83. dojo.publish(this.id + "-addChild", [child, insertIndex]);
  84. // if this is the first child, then select it
  85. if (!this.selectedChildWidget) {
  86. this.selectChild(child);
  87. }
  88. }
  89. },
  90. removeChild : function(/* Widget */page) {
  91. // summary: Removes the pane from the stack
  92. dijit._Container.prototype.removeChild.apply(this, arguments);
  93. // If we are being destroyed than don't run the code below (to
  94. // select another page), because we are deleting
  95. // every page one by one
  96. if (this._beingDestroyed) {
  97. return;
  98. }
  99. if (this._started) {
  100. // this will notify any tablists to remove a button; do this
  101. // first because it may affect sizing
  102. dojo.publish(this.id + "-removeChild", [page]);
  103. // in case the tab titles now take up one line instead of two
  104. // lines
  105. this.layout();
  106. }
  107. if (this.selectedChildWidget === page) {
  108. this.selectedChildWidget = undefined;
  109. if (this._started) {
  110. var children = this.getChildren();
  111. if (children.length) {
  112. this.selectChild(children[0]);
  113. }
  114. }
  115. }
  116. },
  117. selectChild : function(/* Widget */page) {
  118. // summary:
  119. // Show the given widget (which must be one of my children)
  120. page = dijit.byId(page);
  121. if (this.selectedChildWidget != page) {
  122. // Deselect old page and select new one
  123. this._transition(page, this.selectedChildWidget);
  124. this.selectedChildWidget = page;
  125. dojo.publish(this.id + "-selectChild", [page]);
  126. }
  127. },
  128. _transition : function(/* Widget */newWidget, /* Widget */oldWidget) {
  129. if (oldWidget) {
  130. this._hideChild(oldWidget);
  131. }
  132. this._showChild(newWidget);
  133. // Size the new widget, in case this is the first time it's being
  134. // shown,
  135. // or I have been resized since the last time it was shown.
  136. // page must be visible for resizing to work
  137. if (this.doLayout && newWidget.resize) {
  138. newWidget.resize(this._containerContentBox || this._contentBox);
  139. }
  140. },
  141. _adjacent : function(/* Boolean */forward) {
  142. // summary: Gets the next/previous child widget in this container
  143. // from the current selection
  144. var children = this.getChildren();
  145. var index = dojo.indexOf(children, this.selectedChildWidget);
  146. index += forward ? 1 : children.length - 1;
  147. return children[index % children.length]; // dijit._Widget
  148. },
  149. forward : function() {
  150. // Summary: advance to next page
  151. this.selectChild(this._adjacent(true));
  152. },
  153. back : function() {
  154. // Summary: go back to previous page
  155. this.selectChild(this._adjacent(false));
  156. },
  157. _onKeyPress : function(e) {
  158. dojo.publish(this.id + "-containerKeyPress", [{
  159. e : e,
  160. page : this
  161. }]);
  162. },
  163. layout : function() {
  164. if (this.doLayout && this.selectedChildWidget
  165. && this.selectedChildWidget.resize) {
  166. this.selectedChildWidget.resize(this._contentBox);
  167. }
  168. },
  169. _showChild : function(/* Widget */page) {
  170. var children = this.getChildren();
  171. page.isFirstChild = (page == children[0]);
  172. page.isLastChild = (page == children[children.length - 1]);
  173. page.selected = true;
  174. page.domNode.style.display = "";
  175. if (page._loadCheck) {
  176. page._loadCheck(); // trigger load in ContentPane
  177. }
  178. if (page.onShow) {
  179. page.onShow();
  180. }
  181. },
  182. _hideChild : function(/* Widget */page) {
  183. page.selected = false;
  184. page.domNode.style.display = "none";
  185. if (page.onHide) {
  186. page.onHide();
  187. }
  188. },
  189. closeChild : function(/* Widget */page) {
  190. // summary
  191. // callback when user clicks the [X] to remove a page
  192. // if onClose() returns true then remove and destroy the childd
  193. var remove = page.onClose(this, page);
  194. if (remove) {
  195. this.removeChild(page);
  196. // makes sure we can clean up executeScripts in ContentPane
  197. // onUnLoad
  198. page.destroy();
  199. }
  200. },
  201. destroy : function() {
  202. this._beingDestroyed = true;
  203. this.inherited("destroy", arguments);
  204. }
  205. });
  206. dojo.declare("dijit.layout.StackController", [dijit._Widget,
  207. dijit._Templated, dijit._Container], {
  208. // summary:
  209. // Set of buttons to select a page in a page list.
  210. // Monitors the specified StackContainer, and whenever a page is
  211. // added, deleted, or selected, updates itself accordingly.
  212. templateString : "<span wairole='tablist' dojoAttachEvent='onkeypress' class='dijitStackController'></span>",
  213. // containerId: String
  214. // the id of the page container that I point to
  215. containerId : "",
  216. // buttonWidget: String
  217. // the name of the button widget to create to correspond to each
  218. // page
  219. buttonWidget : "dijit.layout._StackButton",
  220. postCreate : function() {
  221. dijit.setWaiRole(this.domNode, "tablist");
  222. this.pane2button = {}; // mapping from panes to buttons
  223. this._subscriptions = [
  224. dojo.subscribe(this.containerId + "-startup", this,
  225. "onStartup"),
  226. dojo.subscribe(this.containerId + "-addChild",
  227. this, "onAddChild"),
  228. dojo.subscribe(this.containerId + "-removeChild",
  229. this, "onRemoveChild"),
  230. dojo.subscribe(this.containerId + "-selectChild",
  231. this, "onSelectChild"),
  232. dojo.subscribe(this.containerId
  233. + "-containerKeyPress", this,
  234. "onContainerKeyPress")];
  235. },
  236. onStartup : function(/* Object */info) {
  237. // summary: called after StackContainer has finished
  238. // initializing
  239. dojo.forEach(info.children, this.onAddChild, this);
  240. this.onSelectChild(info.selected);
  241. },
  242. destroy : function() {
  243. dojo.forEach(this._subscriptions, dojo.unsubscribe);
  244. this.inherited("destroy", arguments);
  245. },
  246. onAddChild : function(/* Widget */page, /* Integer? */insertIndex) {
  247. // summary:
  248. // Called whenever a page is added to the container.
  249. // Create button corresponding to the page.
  250. // add a node that will be promoted to the button widget
  251. var refNode = document.createElement("span");
  252. this.domNode.appendChild(refNode);
  253. // create an instance of the button widget
  254. var cls = dojo.getObject(this.buttonWidget);
  255. var button = new cls({
  256. label : page.title,
  257. closeButton : page.closable
  258. }, refNode);
  259. this.addChild(button, insertIndex);
  260. this.pane2button[page] = button;
  261. page.controlButton = button; // this value might be
  262. // overwritten if two tabs
  263. // point to same container
  264. dojo.connect(button, "onClick", dojo.hitch(this,
  265. "onButtonClick", page));
  266. dojo.connect(button, "onClickCloseButton", dojo.hitch(this,
  267. "onCloseButtonClick", page));
  268. if (!this._currentChild) { // put the first child into the
  269. // tab order
  270. button.focusNode.setAttribute("tabIndex", "0");
  271. this._currentChild = page;
  272. }
  273. },
  274. onRemoveChild : function(/* Widget */page) {
  275. // summary:
  276. // Called whenever a page is removed from the container.
  277. // Remove the button corresponding to the page.
  278. if (this._currentChild === page) {
  279. this._currentChild = null;
  280. }
  281. var button = this.pane2button[page];
  282. if (button) {
  283. // TODO? if current child { reassign }
  284. button.destroy();
  285. }
  286. this.pane2button[page] = null;
  287. },
  288. onSelectChild : function(/* Widget */page) {
  289. // summary:
  290. // Called when a page has been selected in the
  291. // StackContainer, either by me or by another
  292. // StackController
  293. if (!page) {
  294. return;
  295. }
  296. if (this._currentChild) {
  297. var oldButton = this.pane2button[this._currentChild];
  298. oldButton.setChecked(false);
  299. oldButton.focusNode.setAttribute("tabIndex", "-1");
  300. }
  301. var newButton = this.pane2button[page];
  302. newButton.setChecked(true);
  303. this._currentChild = page;
  304. newButton.focusNode.setAttribute("tabIndex", "0");
  305. },
  306. onButtonClick : function(/* Widget */page) {
  307. // summary:
  308. // Called whenever one of my child buttons is pressed in an
  309. // attempt to select a page
  310. var container = dijit.byId(this.containerId); // TODO: do
  311. // this via
  312. // topics?
  313. container.selectChild(page);
  314. },
  315. onCloseButtonClick : function(/* Widget */page) {
  316. // summary:
  317. // Called whenever one of my child buttons [X] is pressed in
  318. // an attempt to close a page
  319. var container = dijit.byId(this.containerId);
  320. container.closeChild(page);
  321. var b = this.pane2button[this._currentChild];
  322. if (b) {
  323. dijit.focus(b.focusNode || b.domNode);
  324. }
  325. },
  326. // TODO: this is a bit redundant with forward, back api in
  327. // StackContainer
  328. adjacent : function(/* Boolean */forward) {
  329. // find currently focused button in children array
  330. var children = this.getChildren();
  331. var current = dojo.indexOf(children,
  332. this.pane2button[this._currentChild]);
  333. // pick next button to focus on
  334. var offset = forward ? 1 : children.length - 1;
  335. return children[(current + offset) % children.length]; // dijit._Widget
  336. },
  337. onkeypress : function(/* Event */e) {
  338. // summary:
  339. // Handle keystrokes on the page list, for advancing to
  340. // next/previous button
  341. // and closing the current page if the page is closable.
  342. if (this.disabled || e.altKey) {
  343. return;
  344. }
  345. var forward = true;
  346. if (e.ctrlKey || !e._djpage) {
  347. var k = dojo.keys;
  348. switch (e.keyCode) {
  349. case k.LEFT_ARROW :
  350. case k.UP_ARROW :
  351. case k.PAGE_UP :
  352. forward = false;
  353. // fall through
  354. case k.RIGHT_ARROW :
  355. case k.DOWN_ARROW :
  356. case k.PAGE_DOWN :
  357. this.adjacent(forward).onClick();
  358. dojo.stopEvent(e);
  359. break;
  360. case k.DELETE :
  361. if (this._currentChild.closable) {
  362. this.onCloseButtonClick(this._currentChild);
  363. }
  364. dojo.stopEvent(e);
  365. break;
  366. default :
  367. if (e.ctrlKey) {
  368. if (e.keyCode == k.TAB) {
  369. this.adjacent(!e.shiftKey).onClick();
  370. dojo.stopEvent(e);
  371. } else if (e.keyChar == "w") {
  372. if (this._currentChild.closable) {
  373. this
  374. .onCloseButtonClick(this._currentChild);
  375. }
  376. dojo.stopEvent(e); // avoid browser tab
  377. // closing.
  378. }
  379. }
  380. }
  381. }
  382. },
  383. onContainerKeyPress : function(/* Object */info) {
  384. info.e._djpage = info.page;
  385. this.onkeypress(info.e);
  386. }
  387. });
  388. dojo.declare("dijit.layout._StackButton", dijit.form.ToggleButton, {
  389. // summary
  390. // Internal widget used by StackContainer.
  391. // The button-like or tab-like object you click to select or
  392. // delete a page
  393. tabIndex : "-1", // StackContainer buttons are not in the tab
  394. // order by default
  395. postCreate : function(/* Event */evt) {
  396. dijit.setWaiRole((this.focusNode || this.domNode), "tab");
  397. this.inherited("postCreate", arguments);
  398. },
  399. onClick : function(/* Event */evt) {
  400. // summary: This is for TabContainer where the tabs are
  401. // <span> rather than button,
  402. // so need to set focus explicitly (on some browsers)
  403. dijit.focus(this.focusNode);
  404. // ... now let StackController catch the event and tell me
  405. // what to do
  406. },
  407. onClickCloseButton : function(/* Event */evt) {
  408. // summary
  409. // StackContainer connects to this function; if your widget
  410. // contains a close button
  411. // then clicking it should call this function.
  412. evt.stopPropagation();
  413. }
  414. });
  415. // These arguments can be specified for the children of a StackContainer.
  416. // Since any widget can be specified as a StackContainer child, mix them
  417. // into the base widget class. (This is a hack, but it's effective.)
  418. dojo.extend(dijit._Widget, {
  419. // title: String
  420. // Title of this widget. Used by TabContainer to the name the
  421. // tab, etc.
  422. title : "",
  423. // selected: Boolean
  424. // Is this child currently selected?
  425. selected : false,
  426. // closable: Boolean
  427. // True if user can close (destroy) this child, such as (for
  428. // example) clicking the X on the tab.
  429. closable : false, // true if user can close this tab pane
  430. onClose : function() {
  431. // summary: Callback if someone tries to close the child,
  432. // child will be closed if func returns true
  433. return true;
  434. }
  435. });
  436. }