ScrollingTabController.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  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.ScrollingTabController"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dijit.layout.ScrollingTabController"] = true;
  8. dojo.provide("dijit.layout.ScrollingTabController");
  9. dojo.require("dijit.layout.TabController");
  10. dojo.require("dijit.Menu");
  11. dojo.require("dijit.form.Button");
  12. dojo.require("dijit._HasDropDown");
  13. dojo.declare("dijit.layout.ScrollingTabController",
  14. dijit.layout.TabController,
  15. {
  16. // summary:
  17. // Set of tabs with left/right arrow keys and a menu to switch between tabs not
  18. // all fitting on a single row.
  19. // Works only for horizontal tabs (either above or below the content, not to the left
  20. // or right).
  21. // tags:
  22. // private
  23. templateString: dojo.cache("dijit.layout", "templates/ScrollingTabController.html", "<div class=\"dijitTabListContainer-${tabPosition}\" style=\"visibility:hidden\">\n\t<div dojoType=\"dijit.layout._ScrollingTabControllerMenuButton\"\n\t\t\tclass=\"tabStripButton-${tabPosition}\"\n\t\t\tid=\"${id}_menuBtn\" containerId=\"${containerId}\" iconClass=\"dijitTabStripMenuIcon\"\n\t\t\tdropDownPosition=\"below-alt, above-alt\"\n\t\t\tdojoAttachPoint=\"_menuBtn\" showLabel=\"false\">&#9660;</div>\n\t<div dojoType=\"dijit.layout._ScrollingTabControllerButton\"\n\t\t\tclass=\"tabStripButton-${tabPosition}\"\n\t\t\tid=\"${id}_leftBtn\" iconClass=\"dijitTabStripSlideLeftIcon\"\n\t\t\tdojoAttachPoint=\"_leftBtn\" dojoAttachEvent=\"onClick: doSlideLeft\" showLabel=\"false\">&#9664;</div>\n\t<div dojoType=\"dijit.layout._ScrollingTabControllerButton\"\n\t\t\tclass=\"tabStripButton-${tabPosition}\"\n\t\t\tid=\"${id}_rightBtn\" iconClass=\"dijitTabStripSlideRightIcon\"\n\t\t\tdojoAttachPoint=\"_rightBtn\" dojoAttachEvent=\"onClick: doSlideRight\" showLabel=\"false\">&#9654;</div>\n\t<div class='dijitTabListWrapper' dojoAttachPoint='tablistWrapper'>\n\t\t<div role='tablist' dojoAttachEvent='onkeypress:onkeypress'\n\t\t\t\tdojoAttachPoint='containerNode' class='nowrapTabStrip'></div>\n\t</div>\n</div>\n"),
  24. // useMenu: [const] Boolean
  25. // True if a menu should be used to select tabs when they are too
  26. // wide to fit the TabContainer, false otherwise.
  27. useMenu: true,
  28. // useSlider: [const] Boolean
  29. // True if a slider should be used to select tabs when they are too
  30. // wide to fit the TabContainer, false otherwise.
  31. useSlider: true,
  32. // tabStripClass: [const] String
  33. // The css class to apply to the tab strip, if it is visible.
  34. tabStripClass: "",
  35. widgetsInTemplate: true,
  36. // _minScroll: Number
  37. // The distance in pixels from the edge of the tab strip which,
  38. // if a scroll animation is less than, forces the scroll to
  39. // go all the way to the left/right.
  40. _minScroll: 5,
  41. attributeMap: dojo.delegate(dijit._Widget.prototype.attributeMap, {
  42. "class": "containerNode"
  43. }),
  44. buildRendering: function(){
  45. this.inherited(arguments);
  46. var n = this.domNode;
  47. this.scrollNode = this.tablistWrapper;
  48. this._initButtons();
  49. if(!this.tabStripClass){
  50. this.tabStripClass = "dijitTabContainer" +
  51. this.tabPosition.charAt(0).toUpperCase() +
  52. this.tabPosition.substr(1).replace(/-.*/, "") +
  53. "None";
  54. dojo.addClass(n, "tabStrip-disabled")
  55. }
  56. dojo.addClass(this.tablistWrapper, this.tabStripClass);
  57. },
  58. onStartup: function(){
  59. this.inherited(arguments);
  60. // Do not show the TabController until the related
  61. // StackController has added it's children. This gives
  62. // a less visually jumpy instantiation.
  63. dojo.style(this.domNode, "visibility", "visible");
  64. this._postStartup = true;
  65. },
  66. onAddChild: function(page, insertIndex){
  67. this.inherited(arguments);
  68. // changes to the tab button label or iconClass will have changed the width of the
  69. // buttons, so do a resize
  70. dojo.forEach(["label", "iconClass"], function(attr){
  71. this.pane2watches[page.id].push(
  72. this.pane2button[page.id].watch(attr, dojo.hitch(this, function(name, oldValue, newValue){
  73. if(this._postStartup && this._dim){
  74. this.resize(this._dim);
  75. }
  76. }))
  77. );
  78. }, this);
  79. // Increment the width of the wrapper when a tab is added
  80. // This makes sure that the buttons never wrap.
  81. // The value 200 is chosen as it should be bigger than most
  82. // Tab button widths.
  83. dojo.style(this.containerNode, "width",
  84. (dojo.style(this.containerNode, "width") + 200) + "px");
  85. },
  86. onRemoveChild: function(page, insertIndex){
  87. // null out _selectedTab because we are about to delete that dom node
  88. var button = this.pane2button[page.id];
  89. if(this._selectedTab === button.domNode){
  90. this._selectedTab = null;
  91. }
  92. this.inherited(arguments);
  93. },
  94. _initButtons: function(){
  95. // summary:
  96. // Creates the buttons used to scroll to view tabs that
  97. // may not be visible if the TabContainer is too narrow.
  98. // Make a list of the buttons to display when the tab labels become
  99. // wider than the TabContainer, and hide the other buttons.
  100. // Also gets the total width of the displayed buttons.
  101. this._btnWidth = 0;
  102. this._buttons = dojo.query("> .tabStripButton", this.domNode).filter(function(btn){
  103. if((this.useMenu && btn == this._menuBtn.domNode) ||
  104. (this.useSlider && (btn == this._rightBtn.domNode || btn == this._leftBtn.domNode))){
  105. this._btnWidth += dojo._getMarginSize(btn).w;
  106. return true;
  107. }else{
  108. dojo.style(btn, "display", "none");
  109. return false;
  110. }
  111. }, this);
  112. },
  113. _getTabsWidth: function(){
  114. var children = this.getChildren();
  115. if(children.length){
  116. var leftTab = children[this.isLeftToRight() ? 0 : children.length - 1].domNode,
  117. rightTab = children[this.isLeftToRight() ? children.length - 1 : 0].domNode;
  118. return rightTab.offsetLeft + dojo.style(rightTab, "width") - leftTab.offsetLeft;
  119. }else{
  120. return 0;
  121. }
  122. },
  123. _enableBtn: function(width){
  124. // summary:
  125. // Determines if the tabs are wider than the width of the TabContainer, and
  126. // thus that we need to display left/right/menu navigation buttons.
  127. var tabsWidth = this._getTabsWidth();
  128. width = width || dojo.style(this.scrollNode, "width");
  129. return tabsWidth > 0 && width < tabsWidth;
  130. },
  131. resize: function(dim){
  132. // summary:
  133. // Hides or displays the buttons used to scroll the tab list and launch the menu
  134. // that selects tabs.
  135. if(this.domNode.offsetWidth == 0){
  136. return;
  137. }
  138. // Save the dimensions to be used when a child is renamed.
  139. this._dim = dim;
  140. // Set my height to be my natural height (tall enough for one row of tab labels),
  141. // and my content-box width based on margin-box width specified in dim parameter.
  142. // But first reset scrollNode.height in case it was set by layoutChildren() call
  143. // in a previous run of this method.
  144. this.scrollNode.style.height = "auto";
  145. this._contentBox = dijit.layout.marginBox2contentBox(this.domNode, {h: 0, w: dim.w});
  146. this._contentBox.h = this.scrollNode.offsetHeight;
  147. dojo.contentBox(this.domNode, this._contentBox);
  148. // Show/hide the left/right/menu navigation buttons depending on whether or not they
  149. // are needed.
  150. var enable = this._enableBtn(this._contentBox.w);
  151. this._buttons.style("display", enable ? "" : "none");
  152. // Position and size the navigation buttons and the tablist
  153. this._leftBtn.layoutAlign = "left";
  154. this._rightBtn.layoutAlign = "right";
  155. this._menuBtn.layoutAlign = this.isLeftToRight() ? "right" : "left";
  156. dijit.layout.layoutChildren(this.domNode, this._contentBox,
  157. [this._menuBtn, this._leftBtn, this._rightBtn, {domNode: this.scrollNode, layoutAlign: "client"}]);
  158. // set proper scroll so that selected tab is visible
  159. if(this._selectedTab){
  160. if(this._anim && this._anim.status() == "playing"){
  161. this._anim.stop();
  162. }
  163. var w = this.scrollNode,
  164. sl = this._convertToScrollLeft(this._getScrollForSelectedTab());
  165. w.scrollLeft = sl;
  166. }
  167. // Enable/disabled left right buttons depending on whether or not user can scroll to left or right
  168. this._setButtonClass(this._getScroll());
  169. this._postResize = true;
  170. // Return my size so layoutChildren() can use it.
  171. // Also avoids IE9 layout glitch on browser resize when scroll buttons present
  172. return {h: this._contentBox.h, w: dim.w};
  173. },
  174. _getScroll: function(){
  175. // summary:
  176. // Returns the current scroll of the tabs where 0 means
  177. // "scrolled all the way to the left" and some positive number, based on #
  178. // of pixels of possible scroll (ex: 1000) means "scrolled all the way to the right"
  179. var sl = (this.isLeftToRight() || dojo.isIE < 8 || (dojo.isIE && dojo.isQuirks) || dojo.isWebKit) ? this.scrollNode.scrollLeft :
  180. dojo.style(this.containerNode, "width") - dojo.style(this.scrollNode, "width")
  181. + (dojo.isIE == 8 ? -1 : 1) * this.scrollNode.scrollLeft;
  182. return sl;
  183. },
  184. _convertToScrollLeft: function(val){
  185. // summary:
  186. // Given a scroll value where 0 means "scrolled all the way to the left"
  187. // and some positive number, based on # of pixels of possible scroll (ex: 1000)
  188. // means "scrolled all the way to the right", return value to set this.scrollNode.scrollLeft
  189. // to achieve that scroll.
  190. //
  191. // This method is to adjust for RTL funniness in various browsers and versions.
  192. if(this.isLeftToRight() || dojo.isIE < 8 || (dojo.isIE && dojo.isQuirks) || dojo.isWebKit){
  193. return val;
  194. }else{
  195. var maxScroll = dojo.style(this.containerNode, "width") - dojo.style(this.scrollNode, "width");
  196. return (dojo.isIE == 8 ? -1 : 1) * (val - maxScroll);
  197. }
  198. },
  199. onSelectChild: function(/*dijit._Widget*/ page){
  200. // summary:
  201. // Smoothly scrolls to a tab when it is selected.
  202. var tab = this.pane2button[page.id];
  203. if(!tab || !page){return;}
  204. // Scroll to the selected tab, except on startup, when scrolling is handled in resize()
  205. var node = tab.domNode;
  206. if(this._postResize && node != this._selectedTab){
  207. this._selectedTab = node;
  208. var sl = this._getScroll();
  209. if(sl > node.offsetLeft ||
  210. sl + dojo.style(this.scrollNode, "width") <
  211. node.offsetLeft + dojo.style(node, "width")){
  212. this.createSmoothScroll().play();
  213. }
  214. }
  215. this.inherited(arguments);
  216. },
  217. _getScrollBounds: function(){
  218. // summary:
  219. // Returns the minimum and maximum scroll setting to show the leftmost and rightmost
  220. // tabs (respectively)
  221. var children = this.getChildren(),
  222. scrollNodeWidth = dojo.style(this.scrollNode, "width"), // about 500px
  223. containerWidth = dojo.style(this.containerNode, "width"), // 50,000px
  224. maxPossibleScroll = containerWidth - scrollNodeWidth, // scrolling until right edge of containerNode visible
  225. tabsWidth = this._getTabsWidth();
  226. if(children.length && tabsWidth > scrollNodeWidth){
  227. // Scrolling should happen
  228. return {
  229. min: this.isLeftToRight() ? 0 : children[children.length-1].domNode.offsetLeft,
  230. max: this.isLeftToRight() ?
  231. (children[children.length-1].domNode.offsetLeft + dojo.style(children[children.length-1].domNode, "width")) - scrollNodeWidth :
  232. maxPossibleScroll
  233. };
  234. }else{
  235. // No scrolling needed, all tabs visible, we stay either scrolled to far left or far right (depending on dir)
  236. var onlyScrollPosition = this.isLeftToRight() ? 0 : maxPossibleScroll;
  237. return {
  238. min: onlyScrollPosition,
  239. max: onlyScrollPosition
  240. };
  241. }
  242. },
  243. _getScrollForSelectedTab: function(){
  244. // summary:
  245. // Returns the scroll value setting so that the selected tab
  246. // will appear in the center
  247. var w = this.scrollNode,
  248. n = this._selectedTab,
  249. scrollNodeWidth = dojo.style(this.scrollNode, "width"),
  250. scrollBounds = this._getScrollBounds();
  251. // TODO: scroll minimal amount (to either right or left) so that
  252. // selected tab is fully visible, and just return if it's already visible?
  253. var pos = (n.offsetLeft + dojo.style(n, "width")/2) - scrollNodeWidth/2;
  254. pos = Math.min(Math.max(pos, scrollBounds.min), scrollBounds.max);
  255. // TODO:
  256. // If scrolling close to the left side or right side, scroll
  257. // all the way to the left or right. See this._minScroll.
  258. // (But need to make sure that doesn't scroll the tab out of view...)
  259. return pos;
  260. },
  261. createSmoothScroll: function(x){
  262. // summary:
  263. // Creates a dojo._Animation object that smoothly scrolls the tab list
  264. // either to a fixed horizontal pixel value, or to the selected tab.
  265. // description:
  266. // If an number argument is passed to the function, that horizontal
  267. // pixel position is scrolled to. Otherwise the currently selected
  268. // tab is scrolled to.
  269. // x: Integer?
  270. // An optional pixel value to scroll to, indicating distance from left.
  271. // Calculate position to scroll to
  272. if(arguments.length > 0){
  273. // position specified by caller, just make sure it's within bounds
  274. var scrollBounds = this._getScrollBounds();
  275. x = Math.min(Math.max(x, scrollBounds.min), scrollBounds.max);
  276. }else{
  277. // scroll to center the current tab
  278. x = this._getScrollForSelectedTab();
  279. }
  280. if(this._anim && this._anim.status() == "playing"){
  281. this._anim.stop();
  282. }
  283. var self = this,
  284. w = this.scrollNode,
  285. anim = new dojo._Animation({
  286. beforeBegin: function(){
  287. if(this.curve){ delete this.curve; }
  288. var oldS = w.scrollLeft,
  289. newS = self._convertToScrollLeft(x);
  290. anim.curve = new dojo._Line(oldS, newS);
  291. },
  292. onAnimate: function(val){
  293. w.scrollLeft = val;
  294. }
  295. });
  296. this._anim = anim;
  297. // Disable/enable left/right buttons according to new scroll position
  298. this._setButtonClass(x);
  299. return anim; // dojo._Animation
  300. },
  301. _getBtnNode: function(/*Event*/ e){
  302. // summary:
  303. // Gets a button DOM node from a mouse click event.
  304. // e:
  305. // The mouse click event.
  306. var n = e.target;
  307. while(n && !dojo.hasClass(n, "tabStripButton")){
  308. n = n.parentNode;
  309. }
  310. return n;
  311. },
  312. doSlideRight: function(/*Event*/ e){
  313. // summary:
  314. // Scrolls the menu to the right.
  315. // e:
  316. // The mouse click event.
  317. this.doSlide(1, this._getBtnNode(e));
  318. },
  319. doSlideLeft: function(/*Event*/ e){
  320. // summary:
  321. // Scrolls the menu to the left.
  322. // e:
  323. // The mouse click event.
  324. this.doSlide(-1,this._getBtnNode(e));
  325. },
  326. doSlide: function(/*Number*/ direction, /*DomNode*/ node){
  327. // summary:
  328. // Scrolls the tab list to the left or right by 75% of the widget width.
  329. // direction:
  330. // If the direction is 1, the widget scrolls to the right, if it is
  331. // -1, it scrolls to the left.
  332. if(node && dojo.hasClass(node, "dijitTabDisabled")){return;}
  333. var sWidth = dojo.style(this.scrollNode, "width");
  334. var d = (sWidth * 0.75) * direction;
  335. var to = this._getScroll() + d;
  336. this._setButtonClass(to);
  337. this.createSmoothScroll(to).play();
  338. },
  339. _setButtonClass: function(/*Number*/ scroll){
  340. // summary:
  341. // Disables the left scroll button if the tabs are scrolled all the way to the left,
  342. // or the right scroll button in the opposite case.
  343. // scroll: Integer
  344. // amount of horizontal scroll
  345. var scrollBounds = this._getScrollBounds();
  346. this._leftBtn.set("disabled", scroll <= scrollBounds.min);
  347. this._rightBtn.set("disabled", scroll >= scrollBounds.max);
  348. }
  349. });
  350. dojo.declare("dijit.layout._ScrollingTabControllerButtonMixin", null, {
  351. baseClass: "dijitTab tabStripButton",
  352. templateString: dojo.cache("dijit.layout", "templates/_ScrollingTabControllerButton.html", "<div dojoAttachEvent=\"onclick:_onButtonClick\">\n\t<div role=\"presentation\" class=\"dijitTabInnerDiv\" dojoattachpoint=\"innerDiv,focusNode\">\n\t\t<div role=\"presentation\" class=\"dijitTabContent dijitButtonContents\" dojoattachpoint=\"tabContent\">\n\t\t\t<img role=\"presentation\" alt=\"\" src=\"${_blankGif}\" class=\"dijitTabStripIcon\" dojoAttachPoint=\"iconNode\"/>\n\t\t\t<span dojoAttachPoint=\"containerNode,titleNode\" class=\"dijitButtonText\"></span>\n\t\t</div>\n\t</div>\n</div>\n"),
  353. // Override inherited tabIndex: 0 from dijit.form.Button, because user shouldn't be
  354. // able to tab to the left/right/menu buttons
  355. tabIndex: "",
  356. // Similarly, override FormWidget.isFocusable() because clicking a button shouldn't focus it
  357. // either (this override avoids focus() call in FormWidget.js)
  358. isFocusable: function(){ return false; }
  359. });
  360. dojo.declare("dijit.layout._ScrollingTabControllerButton",
  361. [dijit.form.Button, dijit.layout._ScrollingTabControllerButtonMixin]);
  362. dojo.declare(
  363. "dijit.layout._ScrollingTabControllerMenuButton",
  364. [dijit.form.Button, dijit._HasDropDown, dijit.layout._ScrollingTabControllerButtonMixin],
  365. {
  366. // id of the TabContainer itself
  367. containerId: "",
  368. // -1 so user can't tab into the button, but so that button can still be focused programatically.
  369. // Because need to move focus to the button (or somewhere) before the menu is hidden or IE6 will crash.
  370. tabIndex: "-1",
  371. isLoaded: function(){
  372. // recreate menu every time, in case the TabContainer's list of children (or their icons/labels) have changed
  373. return false;
  374. },
  375. loadDropDown: function(callback){
  376. this.dropDown = new dijit.Menu({
  377. id: this.containerId + "_menu",
  378. dir: this.dir,
  379. lang: this.lang
  380. });
  381. var container = dijit.byId(this.containerId);
  382. dojo.forEach(container.getChildren(), function(page){
  383. var menuItem = new dijit.MenuItem({
  384. id: page.id + "_stcMi",
  385. label: page.title,
  386. iconClass: page.iconClass,
  387. dir: page.dir,
  388. lang: page.lang,
  389. onClick: function(){
  390. container.selectChild(page);
  391. }
  392. });
  393. this.dropDown.addChild(menuItem);
  394. }, this);
  395. callback();
  396. },
  397. closeDropDown: function(/*Boolean*/ focus){
  398. this.inherited(arguments);
  399. if(this.dropDown){
  400. this.dropDown.destroyRecursive();
  401. delete this.dropDown;
  402. }
  403. }
  404. });
  405. }