1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9-->
10
11<link rel="import" href="../polymer/polymer.html">
12<link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html">
13
14<!--
15`iron-collapse` creates a collapsible block of content.  By default, the content
16will be collapsed.  Use `opened` or `toggle()` to show/hide the content.
17
18    <button on-click="toggle">toggle collapse</button>
19
20    <iron-collapse id="collapse">
21      <div>Content goes here...</div>
22    </iron-collapse>
23
24    ...
25
26    toggle: function() {
27      this.$.collapse.toggle();
28    }
29
30`iron-collapse` adjusts the max-height/max-width of the collapsible element to show/hide
31the content.  So avoid putting padding/margin/border on the collapsible directly,
32and instead put a div inside and style that.
33
34    <style>
35      .collapse-content {
36        padding: 15px;
37        border: 1px solid #dedede;
38      }
39    </style>
40
41    <iron-collapse>
42      <div class="collapse-content">
43        <div>Content goes here...</div>
44      </div>
45    </iron-collapse>
46
47### Styling
48
49The following custom properties and mixins are available for styling:
50
51Custom property | Description | Default
52----------------|-------------|----------
53`--iron-collapse-transition-duration` | Animation transition duration | `300ms`
54
55@group Iron Elements
56@hero hero.svg
57@demo demo/index.html
58@element iron-collapse
59-->
60
61<dom-module id="iron-collapse">
62
63  <template>
64
65    <style>
66      :host {
67        display: block;
68        transition-duration: var(--iron-collapse-transition-duration, 300ms);
69        overflow: visible;
70      }
71
72      :host(.iron-collapse-closed) {
73        display: none;
74      }
75
76      :host(:not(.iron-collapse-opened)) {
77        overflow: hidden;
78      }
79    </style>
80
81    <content></content>
82
83  </template>
84
85</dom-module>
86
87<script>
88
89  Polymer({
90
91    is: 'iron-collapse',
92
93    behaviors: [
94      Polymer.IronResizableBehavior
95    ],
96
97    properties: {
98
99      /**
100       * If true, the orientation is horizontal; otherwise is vertical.
101       *
102       * @attribute horizontal
103       */
104      horizontal: {
105        type: Boolean,
106        value: false,
107        observer: '_horizontalChanged'
108      },
109
110      /**
111       * Set opened to true to show the collapse element and to false to hide it.
112       *
113       * @attribute opened
114       */
115      opened: {
116        type: Boolean,
117        value: false,
118        notify: true,
119        observer: '_openedChanged'
120      },
121
122      /**
123       * When true, the element is transitioning its opened state. When false,
124       * the element has finished opening/closing.
125       *
126       * @attribute transitioning
127       */
128      transitioning: {
129        type: Boolean,
130        notify: true,
131        readOnly: true
132      },
133
134      /**
135       * Set noAnimation to true to disable animations.
136       *
137       * @attribute noAnimation
138       */
139      noAnimation: {
140        type: Boolean
141      },
142
143      /**
144       * Stores the desired size of the collapse body.
145       * @private
146       */
147      _desiredSize: {
148        type: String,
149        value: ''
150      }
151    },
152
153    get dimension() {
154      return this.horizontal ? 'width' : 'height';
155    },
156
157    /**
158     * `maxWidth` or `maxHeight`.
159     * @private
160     */
161    get _dimensionMax() {
162      return this.horizontal ? 'maxWidth' : 'maxHeight';
163    },
164
165    /**
166     * `max-width` or `max-height`.
167     * @private
168     */
169    get _dimensionMaxCss() {
170      return this.horizontal ? 'max-width' : 'max-height';
171    },
172
173    hostAttributes: {
174      role: 'group',
175      'aria-hidden': 'true',
176      'aria-expanded': 'false'
177    },
178
179    listeners: {
180      transitionend: '_onTransitionEnd'
181    },
182
183    /**
184     * Toggle the opened state.
185     *
186     * @method toggle
187     */
188    toggle: function() {
189      this.opened = !this.opened;
190    },
191
192    show: function() {
193      this.opened = true;
194    },
195
196    hide: function() {
197      this.opened = false;
198    },
199
200    /**
201     * Updates the size of the element.
202     * @param {string} size The new value for `maxWidth`/`maxHeight` as css property value, usually `auto` or `0px`.
203     * @param {boolean=} animated if `true` updates the size with an animation, otherwise without.
204     */
205    updateSize: function(size, animated) {
206      // Consider 'auto' as '', to take full size.
207      size = size === 'auto' ? '' : size;
208
209      var willAnimate = animated && !this.noAnimation &&
210                        this.isAttached && this._desiredSize !== size;
211
212      this._desiredSize = size;
213
214      this._updateTransition(false);
215      // If we can animate, must do some prep work.
216      if (willAnimate) {
217        // Animation will start at the current size.
218        var startSize = this._calcSize();
219        // For `auto` we must calculate what is the final size for the animation.
220        // After the transition is done, _transitionEnd will set the size back to `auto`.
221        if (size === '') {
222          this.style[this._dimensionMax] = '';
223          size = this._calcSize();
224        }
225        // Go to startSize without animation.
226        this.style[this._dimensionMax] = startSize;
227        // Force layout to ensure transition will go. Set scrollTop to itself
228        // so that compilers won't remove it.
229        this.scrollTop = this.scrollTop;
230        // Enable animation.
231        this._updateTransition(true);
232        // If final size is the same as startSize it will not animate.
233        willAnimate = (size !== startSize);
234      }
235      // Set the final size.
236      this.style[this._dimensionMax] = size;
237      // If it won't animate, call transitionEnd to set correct classes.
238      if (!willAnimate) {
239        this._transitionEnd();
240      }
241    },
242
243    /**
244     * enableTransition() is deprecated, but left over so it doesn't break existing code.
245     * Please use `noAnimation` property instead.
246     *
247     * @method enableTransition
248     * @deprecated since version 1.0.4
249     */
250    enableTransition: function(enabled) {
251      Polymer.Base._warn('`enableTransition()` is deprecated, use `noAnimation` instead.');
252      this.noAnimation = !enabled;
253    },
254
255    _updateTransition: function(enabled) {
256      this.style.transitionDuration = (enabled && !this.noAnimation) ? '' : '0s';
257    },
258
259    _horizontalChanged: function() {
260      this.style.transitionProperty = this._dimensionMaxCss;
261      var otherDimension = this._dimensionMax === 'maxWidth' ? 'maxHeight' : 'maxWidth';
262      this.style[otherDimension] = '';
263      this.updateSize(this.opened ? 'auto' : '0px', false);
264    },
265
266    _openedChanged: function() {
267      this.setAttribute('aria-expanded', this.opened);
268      this.setAttribute('aria-hidden', !this.opened);
269
270      this._setTransitioning(true);
271      this.toggleClass('iron-collapse-closed', false);
272      this.toggleClass('iron-collapse-opened', false);
273      this.updateSize(this.opened ? 'auto' : '0px', true);
274
275      // Focus the current collapse.
276      if (this.opened) {
277        this.focus();
278      }
279    },
280
281    _transitionEnd: function() {
282      this.style[this._dimensionMax] = this._desiredSize;
283      this.toggleClass('iron-collapse-closed', !this.opened);
284      this.toggleClass('iron-collapse-opened', this.opened);
285      this._updateTransition(false);
286      this.notifyResize();
287      this._setTransitioning(false);
288    },
289
290    _onTransitionEnd: function(event) {
291      if (Polymer.dom(event).rootTarget === this) {
292        this._transitionEnd();
293      }
294    },
295
296    _calcSize: function() {
297      return this.getBoundingClientRect()[this.dimension] + 'px';
298    }
299
300  });
301
302</script>
303