| <!-- |
| Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
| Code distributed by Google as part of the polymer project is also |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
| --> |
| |
| <!-- |
| `core-collapse` creates a collapsible block of content. By default, the content |
| will be collapsed. Use `opened` or `toggle()` to show/hide the content. |
| |
| <button on-click="{{toggle}}">toggle collapse</button> |
| |
| <core-collapse id="collapse"> |
| Content goes here... |
| </core-collapse> |
| |
| ... |
| |
| toggle: function() { |
| this.$.collapse.toggle(); |
| } |
| |
| `core-collapse` adjusts the height/width of the collapsible element to show/hide |
| the content. So avoid putting padding/margin/border on the collapsible directly, |
| and instead put a div inside and style that. |
| |
| <style> |
| .collapse-content { |
| padding: 15px; |
| border: 1px solid #dedede; |
| } |
| </style> |
| |
| <core-collapse> |
| <div class="collapse-content"> |
| Content goes here... |
| </div> |
| </core-collapse> |
| |
| @group Polymer Core Elements |
| @element core-collapse |
| --> |
| |
| <link rel="import" href="../polymer/polymer.html"> |
| |
| <link rel="import" href="../core-resizable/core-resizable.html"> |
| |
| <link rel="stylesheet" href="core-collapse.css" shim-shadowdom> |
| |
| <polymer-element name="core-collapse" attributes="target horizontal opened duration fixedSize allowOverflow"> |
| <template> |
| |
| <content></content> |
| |
| </template> |
| <script> |
| |
| Polymer('core-collapse', Polymer.mixin({ |
| |
| /** |
| * Fired when the `core-collapse`'s `opened` property changes. |
| * |
| * @event core-collapse-open |
| */ |
| |
| /** |
| * Fired when the target element has been resized as a result of the opened |
| * state changing. |
| * |
| * @event core-resize |
| */ |
| |
| /** |
| * The target element that will be opened when the `core-collapse` is |
| * opened. If unspecified, the `core-collapse` itself is the target. |
| * |
| * @attribute target |
| * @type Object |
| * @default null |
| */ |
| target: null, |
| |
| /** |
| * If true, the orientation is horizontal; otherwise is vertical. |
| * |
| * @attribute horizontal |
| * @type boolean |
| * @default false |
| */ |
| horizontal: false, |
| |
| /** |
| * Set opened to true to show the collapse element and to false to hide it. |
| * |
| * @attribute opened |
| * @type boolean |
| * @default false |
| */ |
| opened: false, |
| |
| /** |
| * Collapsing/expanding animation duration in second. |
| * |
| * @attribute duration |
| * @type number |
| * @default 0.33 |
| */ |
| duration: 0.33, |
| |
| /** |
| * If true, the size of the target element is fixed and is set |
| * on the element. Otherwise it will try to |
| * use auto to determine the natural size to use |
| * for collapsing/expanding. |
| * |
| * @attribute fixedSize |
| * @type boolean |
| * @default false |
| */ |
| fixedSize: false, |
| |
| /** |
| * By default the collapsible element is set to overflow hidden. This helps |
| * avoid element bleeding outside the region and provides consistent overflow |
| * style across opened and closed states. Set this property to true to allow |
| * the collapsible element to overflow when it's opened. |
| * |
| * @attribute allowOverflow |
| * @type boolean |
| * @default false |
| */ |
| allowOverflow: false, |
| |
| created: function() { |
| this.transitionEndListener = this.transitionEnd.bind(this); |
| }, |
| |
| ready: function() { |
| this.target = this.target || this; |
| }, |
| |
| domReady: function() { |
| this.async(function() { |
| this.afterInitialUpdate = true; |
| }); |
| }, |
| |
| attached: function() { |
| this.resizerAttachedHandler(); |
| }, |
| |
| detached: function() { |
| if (this.target) { |
| this.removeListeners(this.target); |
| } |
| this.resizableDetachedHandler(); |
| }, |
| |
| targetChanged: function(old) { |
| if (old) { |
| this.removeListeners(old); |
| } |
| if (!this.target) { |
| return; |
| } |
| this.isTargetReady = !!this.target; |
| this.classList.toggle('core-collapse-closed', this.target !== this); |
| this.toggleOpenedStyle(false); |
| this.horizontalChanged(); |
| this.addListeners(this.target); |
| // set core-collapse-closed class initially to hide the target |
| this.toggleClosedClass(true); |
| this.update(); |
| }, |
| |
| addListeners: function(node) { |
| node.addEventListener('transitionend', this.transitionEndListener); |
| }, |
| |
| removeListeners: function(node) { |
| node.removeEventListener('transitionend', this.transitionEndListener); |
| }, |
| |
| horizontalChanged: function() { |
| this.dimension = this.horizontal ? 'width' : 'height'; |
| }, |
| |
| openedChanged: function() { |
| this.update(); |
| this.fire('core-collapse-open', this.opened); |
| }, |
| |
| /** |
| * Toggle the opened state. |
| * |
| * @method toggle |
| */ |
| toggle: function() { |
| this.opened = !this.opened; |
| }, |
| |
| setTransitionDuration: function(duration) { |
| var s = this.target.style; |
| s.transition = duration ? (this.dimension + ' ' + duration + 's') : null; |
| if (duration === 0) { |
| this.async('transitionEnd'); |
| } |
| }, |
| |
| transitionEnd: function() { |
| if (this.opened && !this.fixedSize) { |
| this.updateSize('auto', null); |
| } |
| this.setTransitionDuration(null); |
| this.toggleOpenedStyle(this.opened); |
| this.toggleClosedClass(!this.opened); |
| this.asyncFire('core-resize', null, this.target); |
| this.notifyResize(); |
| }, |
| |
| toggleClosedClass: function(closed) { |
| this.hasClosedClass = closed; |
| this.target.classList.toggle('core-collapse-closed', closed); |
| }, |
| |
| toggleOpenedStyle: function(opened) { |
| this.target.style.overflow = this.allowOverflow && opened ? '' : 'hidden'; |
| }, |
| |
| updateSize: function(size, duration, forceEnd) { |
| this.setTransitionDuration(duration); |
| this.calcSize(); |
| var s = this.target.style; |
| var nochange = s[this.dimension] === size; |
| s[this.dimension] = size; |
| // transitonEnd will not be called if the size has not changed |
| if (forceEnd && nochange) { |
| this.transitionEnd(); |
| } |
| }, |
| |
| update: function() { |
| if (!this.target) { |
| return; |
| } |
| if (!this.isTargetReady) { |
| this.targetChanged(); |
| } |
| this.horizontalChanged(); |
| this[this.opened ? 'show' : 'hide'](); |
| this.notifyResize(); |
| }, |
| |
| calcSize: function() { |
| return this.target.getBoundingClientRect()[this.dimension] + 'px'; |
| }, |
| |
| getComputedSize: function() { |
| return getComputedStyle(this.target)[this.dimension]; |
| }, |
| |
| show: function() { |
| this.toggleClosedClass(false); |
| // for initial update, skip the expanding animation to optimize |
| // performance e.g. skip calcSize |
| if (!this.afterInitialUpdate) { |
| this.transitionEnd(); |
| return; |
| } |
| if (!this.fixedSize) { |
| this.updateSize('auto', null); |
| var s = this.calcSize(); |
| if (s == '0px') { |
| this.transitionEnd(); |
| return; |
| } |
| this.updateSize(0, null); |
| } |
| this.async(function() { |
| this.updateSize(this.size || s, this.duration, true); |
| }); |
| }, |
| |
| hide: function() { |
| this.toggleOpenedStyle(false); |
| // don't need to do anything if it's already hidden |
| if (this.hasClosedClass && !this.fixedSize) { |
| return; |
| } |
| if (this.fixedSize) { |
| // save the size before hiding it |
| this.size = this.getComputedSize(); |
| } else { |
| this.updateSize(this.calcSize(), null); |
| } |
| this.async(function() { |
| this.updateSize(0, this.duration); |
| }); |
| } |
| |
| }, Polymer.CoreResizer)); |
| |
| </script> |
| </polymer-element> |