source: Dev/trunk/src/client/dijit/layout/AccordionContainer.js

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 18.3 KB
Line 
1define([
2        "require",
3        "dojo/_base/array", // array.forEach array.map
4        "dojo/_base/declare", // declare
5        "dojo/_base/fx", // fx.Animation
6        "dojo/dom", // dom.setSelectable
7        "dojo/dom-attr", // domAttr.attr
8        "dojo/dom-class", // domClass.remove
9        "dojo/dom-construct", // domConstruct.place
10        "dojo/dom-geometry",
11        "dojo/keys", // keys
12        "dojo/_base/lang", // lang.getObject lang.hitch
13        "dojo/sniff", // has("ie") has("dijit-legacy-requires")
14        "dojo/topic", // publish
15        "../focus", // focus.focus()
16        "../_base/manager", // manager.defaultDuration
17        "dojo/ready",
18        "../_Widget",
19        "../_Container",
20        "../_TemplatedMixin",
21        "../_CssStateMixin",
22        "./StackContainer",
23        "./ContentPane",
24        "dojo/text!./templates/AccordionButton.html",
25        "../a11yclick" // AccordionButton template uses ondijitclick; not for keyboard, but for responsive touch.
26], function(require, array, declare, fx, dom, domAttr, domClass, domConstruct, domGeometry, keys, lang, has, topic,
27                        focus, manager, ready, _Widget, _Container, _TemplatedMixin, _CssStateMixin, StackContainer, ContentPane, template){
28
29        // module:
30        //              dijit/layout/AccordionContainer
31
32
33        // Design notes:
34        //
35        // An AccordionContainer is a StackContainer, but each child (typically ContentPane)
36        // is wrapped in a _AccordionInnerContainer.   This is hidden from the caller.
37        //
38        // The resulting markup will look like:
39        //
40        //      <div class=dijitAccordionContainer>
41        //              <div class=dijitAccordionInnerContainer>        (one pane)
42        //                              <div class=dijitAccordionTitle>         (title bar) ... </div>
43        //                              <div class=dijtAccordionChildWrapper>   (content pane) </div>
44        //              </div>
45        //      </div>
46        //
47        // Normally the dijtAccordionChildWrapper is hidden for all but one child (the shown
48        // child), so the space for the content pane is all the title bars + the one dijtAccordionChildWrapper,
49        // which on claro has a 1px border plus a 2px bottom margin.
50        //
51        // During animation there are two dijtAccordionChildWrapper's shown, so we need
52        // to compensate for that.
53
54        var AccordionButton = declare("dijit.layout._AccordionButton", [_Widget, _TemplatedMixin, _CssStateMixin], {
55                // summary:
56                //              The title bar to click to open up an accordion pane.
57                //              Internal widget used by AccordionContainer.
58                // tags:
59                //              private
60
61                templateString: template,
62
63                // label: String
64                //              Title of the pane
65                label: "",
66                _setLabelAttr: {node: "titleTextNode", type: "innerHTML" },
67
68                // title: String
69                //              Tooltip that appears on hover
70                title: "",
71                _setTitleAttr: {node: "titleTextNode", type: "attribute", attribute: "title"},
72
73                // iconClassAttr: String
74                //              CSS class for icon to left of label
75                iconClassAttr: "",
76                _setIconClassAttr: { node: "iconNode", type: "class" },
77
78                baseClass: "dijitAccordionTitle",
79
80                getParent: function(){
81                        // summary:
82                        //              Returns the AccordionContainer parent.
83                        // tags:
84                        //              private
85                        return this.parent;
86                },
87
88                buildRendering: function(){
89                        this.inherited(arguments);
90                        var titleTextNodeId = this.id.replace(' ', '_');
91                        domAttr.set(this.titleTextNode, "id", titleTextNodeId + "_title");
92                        this.focusNode.setAttribute("aria-labelledby", domAttr.get(this.titleTextNode, "id"));
93                        dom.setSelectable(this.domNode, false);
94                },
95
96                getTitleHeight: function(){
97                        // summary:
98                        //              Returns the height of the title dom node.
99                        return domGeometry.getMarginSize(this.domNode).h;       // Integer
100                },
101
102                // TODO: maybe the parent should set these methods directly rather than forcing the code
103                // into the button widget?
104                _onTitleClick: function(){
105                        // summary:
106                        //              Callback when someone clicks my title.
107                        var parent = this.getParent();
108                        parent.selectChild(this.contentWidget, true);
109                        focus.focus(this.focusNode);
110                },
111
112                _onTitleKeyDown: function(/*Event*/ evt){
113                        return this.getParent()._onKeyDown(evt, this.contentWidget);
114                },
115
116                _setSelectedAttr: function(/*Boolean*/ isSelected){
117                        this._set("selected", isSelected);
118                        this.focusNode.setAttribute("aria-expanded", isSelected ? "true" : "false");
119                        this.focusNode.setAttribute("aria-selected", isSelected ? "true" : "false");
120                        this.focusNode.setAttribute("tabIndex", isSelected ? "0" : "-1");
121                }
122        });
123
124        if(has("dojo-bidi")){
125                AccordionButton.extend({
126                        _setLabelAttr: function(label){
127                                this._set("label", label);
128                                domAttr.set(this.titleTextNode, "innerHTML", label);
129                                this.applyTextDir(this.titleTextNode);
130                        },
131
132                        _setTitleAttr: function(title){
133                                this._set("title", title);
134                                domAttr.set(this.titleTextNode, "title", title);
135                                this.applyTextDir(this.titleTextNode);
136                        }
137                });
138        }
139
140        var AccordionInnerContainer = declare("dijit.layout._AccordionInnerContainer" + (has("dojo-bidi") ? "_NoBidi" : ""), [_Widget, _CssStateMixin], {
141                // summary:
142                //              Internal widget placed as direct child of AccordionContainer.containerNode.
143                //              When other widgets are added as children to an AccordionContainer they are wrapped in
144                //              this widget.
145
146                /*=====
147                 // buttonWidget: Function|String
148                 //             Class to use to instantiate title
149                 //             (Wish we didn't have a separate widget for just the title but maintaining it
150                 //             for backwards compatibility, is it worth it?)
151                 buttonWidget: null,
152                 =====*/
153
154                /*=====
155                 // contentWidget: dijit/_WidgetBase
156                 //             Pointer to the real child widget
157                 contentWidget: null,
158                 =====*/
159
160                baseClass: "dijitAccordionInnerContainer",
161
162                // tell nested layout widget that we will take care of sizing
163                isLayoutContainer: true,
164
165                buildRendering: function(){
166                        // Builds a template like:
167                        //      <div class=dijitAccordionInnerContainer>
168                        //              Button
169                        //              <div class=dijitAccordionChildWrapper>
170                        //                      ContentPane
171                        //              </div>
172                        //      </div>
173
174                        // Create wrapper div, placed where the child is now
175                        this.domNode = domConstruct.place("<div class='" + this.baseClass +
176                                "' role='presentation'>", this.contentWidget.domNode, "after");
177
178                        // wrapper div's first child is the button widget (ie, the title bar)
179                        var child = this.contentWidget,
180                                cls = lang.isString(this.buttonWidget) ? lang.getObject(this.buttonWidget) : this.buttonWidget;
181                        this.button = child._buttonWidget = (new cls({
182                                contentWidget: child,
183                                label: child.title,
184                                title: child.tooltip,
185                                dir: child.dir,
186                                lang: child.lang,
187                                textDir: child.textDir || this.textDir,
188                                iconClass: child.iconClass,
189                                id: child.id + "_button",
190                                parent: this.parent
191                        })).placeAt(this.domNode);
192
193                        // and then the actual content widget (changing it from prior-sibling to last-child),
194                        // wrapped by a <div class=dijitAccordionChildWrapper>
195                        this.containerNode = domConstruct.place("<div class='dijitAccordionChildWrapper' role='tabpanel' style='display:none'>", this.domNode);
196                        this.containerNode.setAttribute("aria-labelledby", this.button.id);
197
198                        domConstruct.place(this.contentWidget.domNode, this.containerNode);
199                },
200
201                postCreate: function(){
202                        this.inherited(arguments);
203
204                        // Map changes in content widget's title etc. to changes in the button
205                        var button = this.button,
206                                cw = this.contentWidget;
207                        this._contentWidgetWatches = [
208                                cw.watch('title', lang.hitch(this, function(name, oldValue, newValue){
209                                        button.set("label", newValue);
210                                })),
211                                cw.watch('tooltip', lang.hitch(this, function(name, oldValue, newValue){
212                                        button.set("title", newValue);
213                                })),
214                                cw.watch('iconClass', lang.hitch(this, function(name, oldValue, newValue){
215                                        button.set("iconClass", newValue);
216                                }))
217                        ];
218                },
219
220                _setSelectedAttr: function(/*Boolean*/ isSelected){
221                        this._set("selected", isSelected);
222                        this.button.set("selected", isSelected);
223                        if(isSelected){
224                                var cw = this.contentWidget;
225                                if(cw.onSelected){
226                                        cw.onSelected();
227                                }
228                        }
229                },
230
231                startup: function(){
232                        // Called by _Container.addChild()
233                        this.contentWidget.startup();
234                },
235
236                destroy: function(){
237                        this.button.destroyRecursive();
238
239                        array.forEach(this._contentWidgetWatches || [], function(w){
240                                w.unwatch();
241                        });
242
243                        delete this.contentWidget._buttonWidget;
244                        delete this.contentWidget._wrapperWidget;
245
246                        this.inherited(arguments);
247                },
248
249                destroyDescendants: function(/*Boolean*/ preserveDom){
250                        // since getChildren isn't working for me, have to code this manually
251                        this.contentWidget.destroyRecursive(preserveDom);
252                }
253        });
254
255        if(has("dojo-bidi")){
256                AccordionInnerContainer = declare("dijit.layout._AccordionInnerContainer", AccordionInnerContainer, {
257                        postCreate: function(){
258                                this.inherited(arguments);
259
260                                // Map changes in content widget's textdir to changes in the button
261                                var button = this.button;
262                                this._contentWidgetWatches.push(
263                                        this.contentWidget.watch("textDir", function(name, oldValue, newValue){
264                                                button.set("textDir", newValue);
265                                        })
266                                );
267                        }
268                });
269        }
270
271        var AccordionContainer = declare("dijit.layout.AccordionContainer", StackContainer, {
272                // summary:
273                //              Holds a set of panes where every pane's title is visible, but only one pane's content is visible at a time,
274                //              and switching between panes is visualized by sliding the other panes up/down.
275                // example:
276                //      |       <div data-dojo-type="dijit/layout/AccordionContainer">
277                //      |               <div data-dojo-type="dijit/layout/ContentPane" title="pane 1">
278                //      |               </div>
279                //      |               <div data-dojo-type="dijit/layout/ContentPane" title="pane 2">
280                //      |                       <p>This is some text</p>
281                //      |               </div>
282                //      |       </div>
283
284                // duration: Integer
285                //              Amount of time (in ms) it takes to slide panes
286                duration: manager.defaultDuration,
287
288                // buttonWidget: [const] String
289                //              The name of the widget used to display the title of each pane
290                buttonWidget: AccordionButton,
291
292                /*=====
293                 // _verticalSpace: Number
294                 //             Pixels of space available for the open pane
295                 //             (my content box size minus the cumulative size of all the title bars)
296                 _verticalSpace: 0,
297                 =====*/
298                baseClass: "dijitAccordionContainer",
299
300                buildRendering: function(){
301                        this.inherited(arguments);
302                        this.domNode.style.overflow = "hidden";         // TODO: put this in dijit.css
303                        this.domNode.setAttribute("role", "tablist");
304                },
305
306                startup: function(){
307                        if(this._started){
308                                return;
309                        }
310                        this.inherited(arguments);
311                        if(this.selectedChildWidget){
312                                this.selectedChildWidget._wrapperWidget.set("selected", true);
313                        }
314                },
315
316                layout: function(){
317                        // Implement _LayoutWidget.layout() virtual method.
318                        // Set the height of the open pane based on what room remains.
319
320                        var openPane = this.selectedChildWidget;
321
322                        if(!openPane){
323                                return;
324                        }
325
326                        // space taken up by title, plus wrapper div (with border/margin) for open pane
327                        var wrapperDomNode = openPane._wrapperWidget.domNode,
328                                wrapperDomNodeMargin = domGeometry.getMarginExtents(wrapperDomNode),
329                                wrapperDomNodePadBorder = domGeometry.getPadBorderExtents(wrapperDomNode),
330                                wrapperContainerNode = openPane._wrapperWidget.containerNode,
331                                wrapperContainerNodeMargin = domGeometry.getMarginExtents(wrapperContainerNode),
332                                wrapperContainerNodePadBorder = domGeometry.getPadBorderExtents(wrapperContainerNode),
333                                mySize = this._contentBox;
334
335                        // get cumulative height of all the unselected title bars
336                        var totalCollapsedHeight = 0;
337                        array.forEach(this.getChildren(), function(child){
338                                if(child != openPane){
339                                        // Using domGeometry.getMarginSize() rather than domGeometry.position() since claro has 1px bottom margin
340                                        // to separate accordion panes.  Not sure that works perfectly, it's probably putting a 1px
341                                        // margin below the bottom pane (even though we don't want one).
342                                        totalCollapsedHeight += domGeometry.getMarginSize(child._wrapperWidget.domNode).h;
343                                }
344                        });
345                        this._verticalSpace = mySize.h - totalCollapsedHeight - wrapperDomNodeMargin.h
346                                - wrapperDomNodePadBorder.h - wrapperContainerNodeMargin.h - wrapperContainerNodePadBorder.h
347                                - openPane._buttonWidget.getTitleHeight();
348
349                        // Memo size to make displayed child
350                        this._containerContentBox = {
351                                h: this._verticalSpace,
352                                w: this._contentBox.w - wrapperDomNodeMargin.w - wrapperDomNodePadBorder.w
353                                        - wrapperContainerNodeMargin.w - wrapperContainerNodePadBorder.w
354                        };
355
356                        if(openPane){
357                                openPane.resize(this._containerContentBox);
358                        }
359                },
360
361                _setupChild: function(child){
362                        // Overrides _LayoutWidget._setupChild().
363                        // Put wrapper widget around the child widget, showing title
364
365                        child._wrapperWidget = AccordionInnerContainer({
366                                contentWidget: child,
367                                buttonWidget: this.buttonWidget,
368                                id: child.id + "_wrapper",
369                                dir: child.dir,
370                                lang: child.lang,
371                                textDir: child.textDir || this.textDir,
372                                parent: this
373                        });
374
375                        this.inherited(arguments);
376
377                        // Since we are wrapping children in AccordionInnerContainer, replace the default
378                        // wrapper that we created in StackContainer.
379                        domConstruct.place(child.domNode, child._wrapper, "replace");
380                },
381
382                removeChild: function(child){
383                        // Overrides _LayoutWidget.removeChild().
384
385                        // Destroy wrapper widget first, before StackContainer.getChildren() call.
386                        // Replace wrapper widget with true child widget (ContentPane etc.).
387                        // This step only happens if the AccordionContainer has been started; otherwise there's no wrapper.
388                        // (TODO: since StackContainer destroys child._wrapper, maybe it can do this step too?)
389                        if(child._wrapperWidget){
390                                domConstruct.place(child.domNode, child._wrapperWidget.domNode, "after");
391                                child._wrapperWidget.destroy();
392                                delete child._wrapperWidget;
393                        }
394
395                        domClass.remove(child.domNode, "dijitHidden");
396
397                        this.inherited(arguments);
398                },
399
400                getChildren: function(){
401                        // Overrides _Container.getChildren() to return content panes rather than internal AccordionInnerContainer panes
402                        return array.map(this.inherited(arguments), function(child){
403                                return child.declaredClass == "dijit.layout._AccordionInnerContainer" ? child.contentWidget : child;
404                        }, this);
405                },
406
407                destroy: function(){
408                        if(this._animation){
409                                this._animation.stop();
410                        }
411                        array.forEach(this.getChildren(), function(child){
412                                // If AccordionContainer has been started, then each child has a wrapper widget which
413                                // also needs to be destroyed.
414                                if(child._wrapperWidget){
415                                        child._wrapperWidget.destroy();
416                                }else{
417                                        child.destroyRecursive();
418                                }
419                        });
420                        this.inherited(arguments);
421                },
422
423                _showChild: function(child){
424                        // Override StackContainer._showChild() to set visibility of _wrapperWidget.containerNode
425                        child._wrapperWidget.containerNode.style.display = "block";
426                        return this.inherited(arguments);
427                },
428
429                _hideChild: function(child){
430                        // Override StackContainer._showChild() to set visibility of _wrapperWidget.containerNode
431                        child._wrapperWidget.containerNode.style.display = "none";
432                        this.inherited(arguments);
433                },
434
435                _transition: function(/*dijit/_WidgetBase?*/ newWidget, /*dijit/_WidgetBase?*/ oldWidget, /*Boolean*/ animate){
436                        // Overrides StackContainer._transition() to provide sliding of title bars etc.
437
438                        if(has("ie") < 8){
439                                // workaround animation bugs by not animating; not worth supporting animation for IE6 & 7
440                                animate = false;
441                        }
442
443                        if(this._animation){
444                                // there's an in-progress animation.  speedily end it so we can do the newly requested one
445                                this._animation.stop(true);
446                                delete this._animation;
447                        }
448
449                        var self = this;
450
451                        if(newWidget){
452                                newWidget._wrapperWidget.set("selected", true);
453
454                                var d = this._showChild(newWidget);     // prepare widget to be slid in
455
456                                // Size the new widget, in case this is the first time it's being shown,
457                                // or I have been resized since the last time it was shown.
458                                // Note that page must be visible for resizing to work.
459                                if(this.doLayout && newWidget.resize){
460                                        newWidget.resize(this._containerContentBox);
461                                }
462                        }
463
464                        if(oldWidget){
465                                oldWidget._wrapperWidget.set("selected", false);
466                                if(!animate){
467                                        this._hideChild(oldWidget);
468                                }
469                        }
470
471                        if(animate){
472                                var newContents = newWidget._wrapperWidget.containerNode,
473                                        oldContents = oldWidget._wrapperWidget.containerNode;
474
475                                // During the animation we will be showing two dijitAccordionChildWrapper nodes at once,
476                                // which on claro takes up 4px extra space (compared to stable AccordionContainer).
477                                // Have to compensate for that by immediately shrinking the pane being closed.
478                                var wrapperContainerNode = newWidget._wrapperWidget.containerNode,
479                                        wrapperContainerNodeMargin = domGeometry.getMarginExtents(wrapperContainerNode),
480                                        wrapperContainerNodePadBorder = domGeometry.getPadBorderExtents(wrapperContainerNode),
481                                        animationHeightOverhead = wrapperContainerNodeMargin.h + wrapperContainerNodePadBorder.h;
482
483                                oldContents.style.height = (self._verticalSpace - animationHeightOverhead) + "px";
484
485                                this._animation = new fx.Animation({
486                                        node: newContents,
487                                        duration: this.duration,
488                                        curve: [1, this._verticalSpace - animationHeightOverhead - 1],
489                                        onAnimate: function(value){
490                                                value = Math.floor(value);      // avoid fractional values
491                                                newContents.style.height = value + "px";
492                                                oldContents.style.height = (self._verticalSpace - animationHeightOverhead - value) + "px";
493                                        },
494                                        onEnd: function(){
495                                                delete self._animation;
496                                                newContents.style.height = "auto";
497                                                oldWidget._wrapperWidget.containerNode.style.display = "none";
498                                                oldContents.style.height = "auto";
499                                                self._hideChild(oldWidget);
500                                        }
501                                });
502                                this._animation.onStop = this._animation.onEnd;
503                                this._animation.play();
504                        }
505
506                        return d;       // If child has an href, promise that fires when the widget has finished loading
507                },
508
509                // note: we are treating the container as controller here
510                _onKeyDown: function(/*Event*/ e, /*dijit/_WidgetBase*/ fromTitle){
511                        // summary:
512                        //              Handle keydown events
513                        // description:
514                        //              This is called from a handler on AccordionContainer.domNode
515                        //              (setup in StackContainer), and is also called directly from
516                        //              the click handler for accordion labels
517                        if(this.disabled || e.altKey || !(fromTitle || e.ctrlKey)){
518                                return;
519                        }
520                        var c = e.keyCode;
521                        if((fromTitle && (c == keys.LEFT_ARROW || c == keys.UP_ARROW)) ||
522                                (e.ctrlKey && c == keys.PAGE_UP)){
523                                this._adjacent(false)._buttonWidget._onTitleClick();
524                                e.stopPropagation();
525                                e.preventDefault();
526                        }else if((fromTitle && (c == keys.RIGHT_ARROW || c == keys.DOWN_ARROW)) ||
527                                (e.ctrlKey && (c == keys.PAGE_DOWN || c == keys.TAB))){
528                                this._adjacent(true)._buttonWidget._onTitleClick();
529                                e.stopPropagation();
530                                e.preventDefault();
531                        }
532                }
533        });
534
535        // Back compat w/1.6, remove for 2.0
536        if(has("dijit-legacy-requires")){
537                ready(0, function(){
538                        var requires = ["dijit/layout/AccordionPane"];
539                        require(requires);      // use indirection so modules not rolled into a build
540                });
541        }
542
543        // For monkey patching
544        AccordionContainer._InnerContainer = AccordionInnerContainer;
545        AccordionContainer._Button = AccordionButton;
546
547        return AccordionContainer;
548});
Note: See TracBrowser for help on using the repository browser.