iron-swipeable-container.html 8.06 KB
<!--
@license
Copyright (c) 2015 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
-->

<link rel="import" href="../polymer/polymer.html">

<!--
`<iron-swipeable-container>` is a container that allows any of its nested
children (native or custom elements) to be swiped away. By default it supports
a curved or horizontal transition, but the transition duration and properties
can be customized.

Example:

    <iron-swipeable-container>
      <div>I can be swiped</div>
      <paper-card heading="Me too!"></paper-card>
    </iron-swipeable-container>

To disable swiping on individual children, you must give them the `.disable-swipe`
class. Alternatively, to disable swiping on the whole container, you can use its
`disable-swipe` attribute:

    <iron-swipeable-container>
      <div class="disable-swipe">I cannot be swiped be swiped</div>
      <paper-card heading="But I can!"></paper-card>
    </iron-swipeable-container>

    <iron-swipeable-container disable-swipe>
      <div>I cannot be swiped</div>
      <paper-card heading="Me neither :("></paper-card>
    </iron-swipeable-container>

It is a good idea to disable text selection on any of the children that you
want to be swiped:

    .swipe {
      -moz-user-select: none;
      -ms-user-select: none;
      -webkit-user-select: none;
      user-select: none;
      cursor: default;
    }

@group Iron Elements
@element iron-swipeable-container
@demo demo/index.html
-->

<dom-module id="iron-swipeable-container">
  <template>
    <style>
      :host {
        display: block;
      }
    </style>
    <content id="content"></content>
  </template>
</dom-module>

<script>

  Polymer({
    is: 'iron-swipeable-container',

    /**
     * Fired when a child element is swiped away.
     *
     * @event iron-swipe
     */

    properties: {
      /**
       * The style in which to swipe the card. Currently supported
       * options are `curve | horizontal`. If left unspecified, the default
       * is assumed to be `horizontal`.
       */
      swipeStyle: {
       type: String,
       value: 'horizontal'
      },

      /**
       * If true, then the container will not allow swiping.
       */
      disabled: {
        type: Boolean,
        value: false
      },

      /**
       * The ratio of the width of the element that the translation animation
       * should happen over. For example, if the `widthRatio` is 3, the
       * animation will take place on a distance 3 times the width of the
       * element being swiped.
       */
      widthRatio: {
       type: Number,
       value: 3
      },

      /**
       * The ratio of the total animation distance after which the opacity
       * transformation begins. For example, if the `widthRatio` is 1 and
       * the `opacityRate` is 0.5, then the element needs to travel half its
       * width before its opacity starts decreasing.
       */
      opacityRate: {
       type: Number,
       value: 0.2
      },

      /**
       * The CSS transition applied while swiping.
       */
      transition: {
        type: String,
        value: '300ms cubic-bezier(0.4, 0.0, 0.2, 1)'
      }
    },

    ready: function() {
      this._transitionProperty = 'opacity, transform';
      this._swipeComplete = false;
      this._direction = '';
    },

    attached: function() {
      this._nodeObserver = Polymer.dom(this.$.content).observeNodes(
          function(mutations) {
            for (var i = 0; i < mutations.addedNodes.length; i++) {
              this._addListeners(mutations.addedNodes[i]);
            }
            for (var i = 0; i < mutations.removedNodes.length; i++) {
              this._removeListeners(mutations.removedNodes[i]);
            }
          }.bind(this));
    },

    _addListeners: function(node) {
      if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.COMMENT_NODE)
        return;
      // Set up the animation.
      node.style.transitionProperty = this._transitionProperty;
      node.style.transition = this.transition;

      this.listen(node, 'track', '_onTrack');
      this.listen(node, 'transitionend', '_onTransitionEnd');
    },

    _removeListeners: function(node) {
      if (node.nodeType === Node.TEXT_NODE)
        return;
      this.unlisten(node, 'track', '_onTrack');
      this.unlisten(node, 'transitionend', '_onTransitionEnd');
    },

    detached: function() {
      if (this._nodeObserver) {
        Polymer.dom(this.$.content).unobserveNodes(this._nodeObserver);
        this._nodeObserver = null;
      }
    },

    _onTrack: function(event) {
      if (this.disabled)
        return;

      var target = event.currentTarget;
      if (target.classList.contains('disable-swipe'))
        return;

      var track = event.detail;
      if (track.state === 'start') {
        this._trackStart(track, target);
      } else if (track.state === 'track') {
        this._trackMove(track, target);
      } else if (track.state === 'end') {
        this._trackEnd(track, target);
      }
    },

    _trackStart: function(event, target) {
      // Save the width of the element, so that we don't trigger a style
      // recalc every time we need it.
      this._nodeWidth = target.offsetWidth;
      target.style.transition = 'none';
    },

    _trackMove: function(event, target) {
      this._animate(event.dx, target);
    },

    _trackEnd: function(event, target) {
      // The element is swiped away if it's moved halfway its total width.
      this._swipeComplete = Math.abs(event.dx) > this._nodeWidth / 2;
      this._direction = event.dx > 0;
      this._swipeEnd(target);
    },

    _animate: function(x, target) {
      var direction = x > 0 ? 1 : -1;

      // This is the total distance the animation will take place over.
      var totalDistance = this._nodeWidth * this.widthRatio;

      // Opacity distance overflow. `this._nodeWidth * this.opacityRate` is the
      // total distance the element needs to travel to become completely
      // transparent, and `x` is how much the element has already travelled.
      var opaqueDistance = Math.max(0, Math.abs(x) - this._nodeWidth * this.opacityRate);
      var opacity = Math.max(0, (totalDistance - opaqueDistance) / totalDistance);
      target.style.opacity = opacity;

      var translate, rotate;

      if (this.swipeStyle === 'horizontal') {
        translate = 'translate3d(' + x + 'px,' + 0 + 'px,0)';
        rotate = '';
      } else {  // Default is assumed to be `curve`.
        // Assume the element will be completely transparent at 90 degrees, so
        // figure out the rotation and vertical displacement needed to
        // achieve that.
        var y = totalDistance - Math.sqrt(totalDistance * totalDistance - opaqueDistance * opaqueDistance);
        var deg = (1 - opacity) * direction * 90;

        translate = 'translate3d(' + x + 'px,' + y + 'px,0)';
        rotate = ' rotate(' + deg + 'deg)';
      }

      this.transform(translate + rotate, target);
    },

    _swipeEnd: function(target) {
      // Restore the original transition;
      target.style.transition = this.transition;

      if (this._swipeComplete) {
        // If the element is ready to be swiped away, then translate it to the full
        // transparency distance.
        var totalDistance = this._nodeWidth * this.widthRatio;
        this._animate(this._direction ? totalDistance : -totalDistance, target);
      } else {
        this._animate(0, target);
      }
    },

    _onTransitionEnd: function(event) {
      var target = event.currentTarget;

      if (this._swipeComplete && event.propertyName === 'opacity') {
        Polymer.dom(this).removeChild(target);
        this.fire('iron-swipe',
            { direction: this._direction > 0 ? 'right' : 'left',
              target:target
            });
      }
    }

  });
</script>