ScrollingTabController.js 19 KB

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