StackController.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. define("dijit/layout/StackController", [
  2. "dojo/_base/array", // array.forEach array.indexOf array.map
  3. "dojo/_base/declare", // declare
  4. "dojo/_base/event", // event.stop
  5. "dojo/keys", // keys
  6. "dojo/_base/lang", // lang.getObject
  7. "dojo/_base/sniff", // has("ie")
  8. "../focus", // focus.focus()
  9. "../registry", // registry.byId
  10. "../_Widget",
  11. "../_TemplatedMixin",
  12. "../_Container",
  13. "../form/ToggleButton",
  14. "dojo/i18n!../nls/common"
  15. ], function(array, declare, event, keys, lang, has,
  16. focus, registry, _Widget, _TemplatedMixin, _Container, ToggleButton){
  17. /*=====
  18. var _Widget = dijit._Widget;
  19. var _TemplatedMixin = dijit._TemplatedMixin;
  20. var _Container = dijit._Container;
  21. var ToggleButton = dijit.form.ToggleButton;
  22. =====*/
  23. // module:
  24. // dijit/layout/StackController
  25. // summary:
  26. // Set of buttons to select a page in a `dijit.layout.StackContainer`
  27. var StackButton = declare("dijit.layout._StackButton", ToggleButton, {
  28. // summary:
  29. // Internal widget used by StackContainer.
  30. // description:
  31. // The button-like or tab-like object you click to select or delete a page
  32. // tags:
  33. // private
  34. // Override _FormWidget.tabIndex.
  35. // StackContainer buttons are not in the tab order by default.
  36. // Probably we should be calling this.startupKeyNavChildren() instead.
  37. tabIndex: "-1",
  38. // closeButton: Boolean
  39. // When true, display close button for this tab
  40. closeButton: false,
  41. _setCheckedAttr: function(/*Boolean*/ value, /*Boolean?*/ priorityChange){
  42. this.inherited(arguments);
  43. this.focusNode.removeAttribute("aria-pressed");
  44. },
  45. buildRendering: function(/*Event*/ evt){
  46. this.inherited(arguments);
  47. (this.focusNode || this.domNode).setAttribute("role", "tab");
  48. },
  49. onClick: function(/*Event*/ /*===== evt =====*/){
  50. // summary:
  51. // This is for TabContainer where the tabs are <span> rather than button,
  52. // so need to set focus explicitly (on some browsers)
  53. // Note that you shouldn't override this method, but you can connect to it.
  54. focus.focus(this.focusNode);
  55. // ... now let StackController catch the event and tell me what to do
  56. },
  57. onClickCloseButton: function(/*Event*/ evt){
  58. // summary:
  59. // StackContainer connects to this function; if your widget contains a close button
  60. // then clicking it should call this function.
  61. // Note that you shouldn't override this method, but you can connect to it.
  62. evt.stopPropagation();
  63. }
  64. });
  65. var StackController = declare("dijit.layout.StackController", [_Widget, _TemplatedMixin, _Container], {
  66. // summary:
  67. // Set of buttons to select a page in a `dijit.layout.StackContainer`
  68. // description:
  69. // Monitors the specified StackContainer, and whenever a page is
  70. // added, deleted, or selected, updates itself accordingly.
  71. baseClass: "dijitStackController",
  72. templateString: "<span role='tablist' data-dojo-attach-event='onkeypress'></span>",
  73. // containerId: [const] String
  74. // The id of the page container that I point to
  75. containerId: "",
  76. // buttonWidget: [const] Constructor
  77. // The button widget to create to correspond to each page
  78. buttonWidget: StackButton,
  79. constructor: function(){
  80. this.pane2button = {}; // mapping from pane id to buttons
  81. this.pane2connects = {}; // mapping from pane id to this.connect() handles
  82. this.pane2watches = {}; // mapping from pane id to watch() handles
  83. },
  84. postCreate: function(){
  85. this.inherited(arguments);
  86. // Listen to notifications from StackContainer
  87. this.subscribe(this.containerId+"-startup", "onStartup");
  88. this.subscribe(this.containerId+"-addChild", "onAddChild");
  89. this.subscribe(this.containerId+"-removeChild", "onRemoveChild");
  90. this.subscribe(this.containerId+"-selectChild", "onSelectChild");
  91. this.subscribe(this.containerId+"-containerKeyPress", "onContainerKeyPress");
  92. },
  93. onStartup: function(/*Object*/ info){
  94. // summary:
  95. // Called after StackContainer has finished initializing
  96. // tags:
  97. // private
  98. array.forEach(info.children, this.onAddChild, this);
  99. if(info.selected){
  100. // Show button corresponding to selected pane (unless selected
  101. // is null because there are no panes)
  102. this.onSelectChild(info.selected);
  103. }
  104. },
  105. destroy: function(){
  106. for(var pane in this.pane2button){
  107. this.onRemoveChild(registry.byId(pane));
  108. }
  109. this.inherited(arguments);
  110. },
  111. onAddChild: function(/*dijit._Widget*/ page, /*Integer?*/ insertIndex){
  112. // summary:
  113. // Called whenever a page is added to the container.
  114. // Create button corresponding to the page.
  115. // tags:
  116. // private
  117. // create an instance of the button widget
  118. // (remove typeof buttonWidget == string support in 2.0)
  119. var cls = lang.isString(this.buttonWidget) ? lang.getObject(this.buttonWidget) : this.buttonWidget;
  120. var button = new cls({
  121. id: this.id + "_" + page.id,
  122. label: page.title,
  123. dir: page.dir,
  124. lang: page.lang,
  125. textDir: page.textDir,
  126. showLabel: page.showTitle,
  127. iconClass: page.iconClass,
  128. closeButton: page.closable,
  129. title: page.tooltip
  130. });
  131. button.focusNode.setAttribute("aria-selected", "false");
  132. // map from page attribute to corresponding tab button attribute
  133. var pageAttrList = ["title", "showTitle", "iconClass", "closable", "tooltip"],
  134. buttonAttrList = ["label", "showLabel", "iconClass", "closeButton", "title"];
  135. // watch() so events like page title changes are reflected in tab button
  136. this.pane2watches[page.id] = array.map(pageAttrList, function(pageAttr, idx){
  137. return page.watch(pageAttr, function(name, oldVal, newVal){
  138. button.set(buttonAttrList[idx], newVal);
  139. });
  140. });
  141. // connections so that clicking a tab button selects the corresponding page
  142. this.pane2connects[page.id] = [
  143. this.connect(button, 'onClick', lang.hitch(this,"onButtonClick", page)),
  144. this.connect(button, 'onClickCloseButton', lang.hitch(this,"onCloseButtonClick", page))
  145. ];
  146. this.addChild(button, insertIndex);
  147. this.pane2button[page.id] = button;
  148. page.controlButton = button; // this value might be overwritten if two tabs point to same container
  149. if(!this._currentChild){ // put the first child into the tab order
  150. button.focusNode.setAttribute("tabIndex", "0");
  151. button.focusNode.setAttribute("aria-selected", "true");
  152. this._currentChild = page;
  153. }
  154. // make sure all tabs have the same length
  155. if(!this.isLeftToRight() && has("ie") && this._rectifyRtlTabList){
  156. this._rectifyRtlTabList();
  157. }
  158. },
  159. onRemoveChild: function(/*dijit._Widget*/ page){
  160. // summary:
  161. // Called whenever a page is removed from the container.
  162. // Remove the button corresponding to the page.
  163. // tags:
  164. // private
  165. if(this._currentChild === page){ this._currentChild = null; }
  166. // disconnect/unwatch connections/watches related to page being removed
  167. array.forEach(this.pane2connects[page.id], lang.hitch(this, "disconnect"));
  168. delete this.pane2connects[page.id];
  169. array.forEach(this.pane2watches[page.id], function(w){ w.unwatch(); });
  170. delete this.pane2watches[page.id];
  171. var button = this.pane2button[page.id];
  172. if(button){
  173. this.removeChild(button);
  174. delete this.pane2button[page.id];
  175. button.destroy();
  176. }
  177. delete page.controlButton;
  178. },
  179. onSelectChild: function(/*dijit._Widget*/ page){
  180. // summary:
  181. // Called when a page has been selected in the StackContainer, either by me or by another StackController
  182. // tags:
  183. // private
  184. if(!page){ return; }
  185. if(this._currentChild){
  186. var oldButton=this.pane2button[this._currentChild.id];
  187. oldButton.set('checked', false);
  188. oldButton.focusNode.setAttribute("aria-selected", "false");
  189. oldButton.focusNode.setAttribute("tabIndex", "-1");
  190. }
  191. var newButton=this.pane2button[page.id];
  192. newButton.set('checked', true);
  193. newButton.focusNode.setAttribute("aria-selected", "true");
  194. this._currentChild = page;
  195. newButton.focusNode.setAttribute("tabIndex", "0");
  196. var container = registry.byId(this.containerId);
  197. container.containerNode.setAttribute("aria-labelledby", newButton.id);
  198. },
  199. onButtonClick: function(/*dijit._Widget*/ page){
  200. // summary:
  201. // Called whenever one of my child buttons is pressed in an attempt to select a page
  202. // tags:
  203. // private
  204. if(this._currentChild.id === page.id) {
  205. //In case the user clicked the checked button, keep it in the checked state because it remains to be the selected stack page.
  206. var button=this.pane2button[page.id];
  207. button.set('checked', true);
  208. }
  209. var container = registry.byId(this.containerId);
  210. container.selectChild(page);
  211. },
  212. onCloseButtonClick: function(/*dijit._Widget*/ page){
  213. // summary:
  214. // Called whenever one of my child buttons [X] is pressed in an attempt to close a page
  215. // tags:
  216. // private
  217. var container = registry.byId(this.containerId);
  218. container.closeChild(page);
  219. if(this._currentChild){
  220. var b = this.pane2button[this._currentChild.id];
  221. if(b){
  222. focus.focus(b.focusNode || b.domNode);
  223. }
  224. }
  225. },
  226. // TODO: this is a bit redundant with forward, back api in StackContainer
  227. adjacent: function(/*Boolean*/ forward){
  228. // summary:
  229. // Helper for onkeypress to find next/previous button
  230. // tags:
  231. // private
  232. if(!this.isLeftToRight() && (!this.tabPosition || /top|bottom/.test(this.tabPosition))){ forward = !forward; }
  233. // find currently focused button in children array
  234. var children = this.getChildren();
  235. var current = array.indexOf(children, this.pane2button[this._currentChild.id]);
  236. // pick next button to focus on
  237. var offset = forward ? 1 : children.length - 1;
  238. return children[ (current + offset) % children.length ]; // dijit._Widget
  239. },
  240. onkeypress: function(/*Event*/ e){
  241. // summary:
  242. // Handle keystrokes on the page list, for advancing to next/previous button
  243. // and closing the current page if the page is closable.
  244. // tags:
  245. // private
  246. if(this.disabled || e.altKey ){ return; }
  247. var forward = null;
  248. if(e.ctrlKey || !e._djpage){
  249. switch(e.charOrCode){
  250. case keys.LEFT_ARROW:
  251. case keys.UP_ARROW:
  252. if(!e._djpage){ forward = false; }
  253. break;
  254. case keys.PAGE_UP:
  255. if(e.ctrlKey){ forward = false; }
  256. break;
  257. case keys.RIGHT_ARROW:
  258. case keys.DOWN_ARROW:
  259. if(!e._djpage){ forward = true; }
  260. break;
  261. case keys.PAGE_DOWN:
  262. if(e.ctrlKey){ forward = true; }
  263. break;
  264. case keys.HOME:
  265. case keys.END:
  266. var children = this.getChildren();
  267. if(children && children.length){
  268. children[e.charOrCode == keys.HOME ? 0 : children.length-1].onClick();
  269. }
  270. event.stop(e);
  271. break;
  272. case keys.DELETE:
  273. if(this._currentChild.closable){
  274. this.onCloseButtonClick(this._currentChild);
  275. }
  276. event.stop(e);
  277. break;
  278. default:
  279. if(e.ctrlKey){
  280. if(e.charOrCode === keys.TAB){
  281. this.adjacent(!e.shiftKey).onClick();
  282. event.stop(e);
  283. }else if(e.charOrCode == "w"){
  284. if(this._currentChild.closable){
  285. this.onCloseButtonClick(this._currentChild);
  286. }
  287. event.stop(e); // avoid browser tab closing.
  288. }
  289. }
  290. }
  291. // handle next/previous page navigation (left/right arrow, etc.)
  292. if(forward !== null){
  293. this.adjacent(forward).onClick();
  294. event.stop(e);
  295. }
  296. }
  297. },
  298. onContainerKeyPress: function(/*Object*/ info){
  299. // summary:
  300. // Called when there was a keypress on the container
  301. // tags:
  302. // private
  303. info.e._djpage = info.page;
  304. this.onkeypress(info.e);
  305. }
  306. });
  307. StackController.StackButton = StackButton; // for monkey patching
  308. return StackController;
  309. });