source: Dev/branches/rest-dojo-ui/client/dijit/layout/ScrollingTabController.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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