<commit_before>import Events from 'minivents'
import AnimationBus from 'animation-bus'
import PagerElement from './PagerElement'
import { modulo, clamp, sum, max, range } from './utils'

const TRANSFORM = require('get-prefix')('transform')
const isWindowDefined = (typeof window !== 'undefined')

// only require ResizeObserver polyfill if it isn't available and we aren't in a SSR environment
if (isWindowDefined && !window.ResizeObserver) {
  window.ResizeObserver = require('resize-observer-polyfill')
}

class Track extends PagerElement {
  getStyles(trackPosition) {
    const { x, y } = this.pager.getPositionValue(trackPosition)
    const trackSize = this.pager.getTrackSize()
    const style = {
      [TRANSFORM]: `translate3d(${x}px, ${y}px, 0)`
    }

    if (trackSize) {
      const { axis, viewsToShow } = this.pager.options
      const dimension = (axis === 'x') ? 'width' : 'height'

      style[dimension] = (viewsToShow === 'auto')
        ? trackSize
        : this.pager.views.length / viewsToShow * 100 + '%'
    }

    return style
  }
}

class View extends PagerElement {
  constructor({ index, ...restOptions }) {
    super(restOptions)
    this.index = index
    this.isCurrent = false
    this.inBounds = true
    this.setTarget()
    this.setOrigin()
  }

  setTarget() {
    const { align, viewsToShow } = this.pager.options
    let target = this.pager.getStartCoords(this.index)

    if (align) {
      target += this.pager.getAlignOffset(this)
    }

    this.target = target
  }

  setOrigin(trackPosition = this.pager.trackPosition) {
    this.origin = this.target - trackPosition
  }

  getStyles() {
    const { axis, viewsToShow, infinite } = this.pager.options
    const style = {}

    // we need to position views inline when using the x axis
    if (axis === 'x') {
      style.display = 'inline-block'
      style.verticalAlign = 'top'
    }

    // set width or height on view when viewsToShow is not auto
    if (viewsToShow !== 'auto') {
      style[axis === 'x' ? 'width' : 'height'] = 100 / this.pager.views.length + '%'
    }

    // make sure view stays in frame when using infinite option
    if (infinite && !this.inBounds) {
      style.position = 'relative'
      style[(axis === 'y') ? 'top' : 'left'] = this.getPosition()
    }

    // finally, apply any animations
    return {
      ...style,
      ...this.pager.animationBus.getStyles(this)
    }
  }
}

class Pager extends Events {
  constructor(options = {}) {
    super()

    this.options = {
      viewsToShow: 1,
      viewsToMove: 1,
      align: 0,
      contain: false,
      axis: 'x',
      autoSize: false,
      animations: [],
      infinite: false,
      instant: false,
      swipe: true,
      swipeThreshold: 0.5,
      flickTimeout: 300,
      accessibility: true,
      ...options
    }

    this.views = []
    this.currentIndex = 0
    this.currentView = null
    this.currentTween = 0
    this.trackPosition = 0
    this.isSwiping = false

    this.animationBus = new AnimationBus({
      animations: this.options.animations,
      origin: view => view.origin
    })

    if (isWindowDefined) {
      this.resizeObserver = new ResizeObserver(() => {
        this.hydrate()
      })
    }
  }

  setOptions(options) {
    const lastOptions = this.options

    // spread new options over the old ones
    this.options = {
      ...this.options,
      ...options
    }

    // merge animations into animation bus
    this.animationBus.animations = this.options.animations

    // fire a viewChange event with the new indicies if viewsToShow has changed
    if (lastOptions.viewsToShow !== this.options.viewsToShow) {
      this.emit('viewChange', this.getCurrentIndicies())
    }
  }

  hydrate = () => {
    this.frame.setSize()
    this.track.setSize()
    this.views.forEach(view => {
      view.setSize()
      view.setTarget()
    })
    this.setPositionValue()
    this.setViewStyles()
    this.emit('hydrated')
  }

  addFrame(node) {
    this.frame = new PagerElement({
      node,
      pager: this
    })
  }

  addTrack(node) {
    this.track = new Track({
      node,
      pager: this
    })
  }

  addView(node) {
    const index = this.views.length
    const view = new View({
      node,
      index,
      pager: this
    })

    // add view to collection
    this.views.push(view)

    // set this as the first view if there isn't one yet
    if (!this.currentView) {
      this.setCurrentView({
        index,
        suppressEvent: true
      })
    }

    // listen for width and height changes
    if (isWindowDefined) {
      this.resizeObserver.observe(node)
    }

    // fire an event
    this.emit('viewAdded')

    return view
  }

  removeView(view) {
    // filter out view
    this.views = this.views.filter(_view => view !== _view)

    // fire an event
    this.emit('viewRemoved')
  }

  prev() {
    this.setCurrentView({ direction: -1 })
  }

  next() {
    this.setCurrentView({ direction: 1 })
  }

  setCurrentView({
    direction = 0,
    index = this.currentIndex,
    suppressEvent = false
  }) {
    const { viewsToMove, infinite } = this.options
    const newIndex = index + (direction * viewsToMove)

    const previousIndex = this.currentIndex
    const currentIndex = infinite ? newIndex : clamp(newIndex, 0, this.views.length - 1)

    const previousView = this.getView(previousIndex)
    const currentView = this.getView(currentIndex)

    // set current index and view
    this.currentIndex = currentIndex
    this.currentView = currentView

    // swap current view flags
    previousView.isCurrent = false
    currentView.isCurrent = true

    // set the track position to the new view
    this.setPositionValue()

    if (!suppressEvent) {
      this.emit('viewChange', this.getCurrentIndicies())
    }
  }

  setPositionValue(trackPosition = this.currentView ? this.currentView.target : 0) {
    if (!this.isSwiping) {
      const { viewsToShow, autoSize, infinite, contain } = this.options
      const trackSize = this.getTrackSize()

      if (infinite) {
        // we offset by a track multiplier so infinite animation can take advantage of
        // physics by animating to a large value, the final value provided in getTransformValue
        // will return the proper wrapped value
        trackPosition -= (Math.floor(this.currentIndex / this.views.length) || 0) * trackSize
      }

      if (contain) {
        const trackEndOffset = ((viewsToShow === 'auto' && autoSize) || viewsToShow <= 1)
          ? 0 : this.getFrameSize({ autoSize: false })
        trackPosition = clamp(trackPosition, trackEndOffset - trackSize, 0)
      }
    }

    this.trackPosition = trackPosition
  }

  setViewStyles(trackPosition = 0) {
    const { infinite, align } = this.options
    const frameSize = this.getFrameSize()
    const trackSize = this.getTrackSize()
    const wrappedtrackPosition = modulo(trackPosition, -trackSize)

    this.views.reduce((lastPosition, view, index) => {
      const viewSize = view.getSize()
      const nextPosition = lastPosition + viewSize
      let position = lastPosition

      if (nextPosition + (viewSize * align) < Math.abs(wrappedtrackPosition)) {
        // shift views around so they are always visible in frame
        if (infinite) {
          position += trackSize - lastPosition
        }
        view.inBounds = false
      } else {
        view.inBounds = true
      }

      view.setPosition(position)
      view.setOrigin(trackPosition)

      return nextPosition
    }, 0)
  }

  getNumericViewsToShow() {
    return isNaN(this.options.viewsToShow) ? 1 : this.options.viewsToShow
  }

  getMaxDimensions(indices) {
    const { axis } = this.options
    const widths = []
    const heights = []

    indices.forEach(index => {
      const view = isNaN(index) ? index : this.getView(index)
      widths.push(view.getSize('width'))
      heights.push(view.getSize('height'))
    })

    return {
      width: (axis === 'x') ? sum(widths) : max(widths),
      height: (axis === 'y') ? sum(heights) : max(heights)
    }
  }

  getCurrentIndicies() {
    const { infinite, contain } = this.options
    const currentViews = []
    const viewsToShow = isNaN(this.options.viewsToShow) ? 1 : this.options.viewsToShow
    let minIndex = this.currentIndex
    let maxIndex = this.currentIndex + (viewsToShow - 1)

    if (contain) {
      // if containing, we need to clamp the start and end indexes so we only return what's in view
      minIndex = clamp(minIndex, 0, this.views.length - viewsToShow)
      maxIndex = clamp(maxIndex, 0, this.views.length - 1)
      for (let i = minIndex; i <= maxIndex; i++) {
        currentViews.push(i)
      }
    } else {
      for (let i = minIndex; i <= maxIndex; i++) {
        currentViews.push(infinite
          ? modulo(i, this.views.length)
          : clamp(i, 0, this.views.length - 1)
        )
      }
    }

    return currentViews
  }

  getFrameSize({ autoSize = this.options.autoSize, fullSize = false } = {}) {
    let dimensions = {
      width: 0,
      height: 0
    }

    if (this.views.length) {
      if (autoSize) {
        const currentViews = this.getCurrentIndicies()
        dimensions = this.getMaxDimensions(currentViews)
      } else if (this.frame) {
        dimensions = {
          width: this.frame.getSize('width'),
          height: this.frame.getSize('height')
        }
      }
    }

    if (fullSize) {
      return dimensions
    } else {
      return dimensions[this.options.axis === 'x' ? 'width' : 'height']
    }
  }

  getTrackSize(includeLastSlide = true) {
    const lastIndex = includeLastSlide ? this.views.length : this.views.length - 1
    let size = 0
    this.views.slice(0, lastIndex).forEach(view => {
      size += view.getSize()
    })
    return size
  }

  getView(index) {
    return this.views[modulo(index, this.views.length)]
  }

  // where the view should start
  getStartCoords(index) {
    let target = 0
    this.views.slice(0, index).forEach(view => {
      target -= view.getSize()
    })
    return target
  }

  // how much to offset the view defined by the align option
  getAlignOffset(view) {
    const frameSize = this.getFrameSize({ autoSize: false })
    const viewSize = view.getSize()
    return (frameSize - viewSize) * this.options.align
  }

  getPositionValue(trackPosition = this.trackPosition) {
    const { infinite, contain } = this.options
    const position = { x: 0, y: 0 }

    // store the current animated value so we can reference it later
    this.currentTween = trackPosition

    // wrap the track position if this is an infinite track
    if (infinite) {
      const trackSize = this.getTrackSize()
      trackPosition = modulo(trackPosition, -trackSize) || 0
    }

    // emit a "scroll" event so we can do things based on the progress of the track
    this.emit('scroll', {
      progress: trackPosition / this.getTrackSize(false),
      position: trackPosition
    })

    // set the proper transform axis based on our options
    position[this.options.axis] = trackPosition

    return position
  }

  resetViewIndex() {
    // reset back to a normal index
    this.setCurrentView({
      index: modulo(this.currentIndex, this.views.length),
      suppressEvent: true
    })
  }
}

export default Pager
<commit_msg>stop observing view after it has been removed<commit_after>import Events from 'minivents'
import AnimationBus from 'animation-bus'
import PagerElement from './PagerElement'
import { modulo, clamp, sum, max, range } from './utils'

const TRANSFORM = require('get-prefix')('transform')
const isWindowDefined = (typeof window !== 'undefined')

// only require ResizeObserver polyfill if it isn't available and we aren't in a SSR environment
if (isWindowDefined && !window.ResizeObserver) {
  window.ResizeObserver = require('resize-observer-polyfill')
}

class Track extends PagerElement {
  getStyles(trackPosition) {
    const { x, y } = this.pager.getPositionValue(trackPosition)
    const trackSize = this.pager.getTrackSize()
    const style = {
      [TRANSFORM]: `translate3d(${x}px, ${y}px, 0)`
    }

    if (trackSize) {
      const { axis, viewsToShow } = this.pager.options
      const dimension = (axis === 'x') ? 'width' : 'height'

      style[dimension] = (viewsToShow === 'auto')
        ? trackSize
        : this.pager.views.length / viewsToShow * 100 + '%'
    }

    return style
  }
}

class View extends PagerElement {
  constructor({ index, ...restOptions }) {
    super(restOptions)
    this.index = index
    this.isCurrent = false
    this.inBounds = true
    this.setTarget()
    this.setOrigin()
  }

  setTarget() {
    const { align, viewsToShow } = this.pager.options
    let target = this.pager.getStartCoords(this.index)

    if (align) {
      target += this.pager.getAlignOffset(this)
    }

    this.target = target
  }

  setOrigin(trackPosition = this.pager.trackPosition) {
    this.origin = this.target - trackPosition
  }

  getStyles() {
    const { axis, viewsToShow, infinite } = this.pager.options
    const style = {}

    // we need to position views inline when using the x axis
    if (axis === 'x') {
      style.display = 'inline-block'
      style.verticalAlign = 'top'
    }

    // set width or height on view when viewsToShow is not auto
    if (viewsToShow !== 'auto') {
      style[axis === 'x' ? 'width' : 'height'] = 100 / this.pager.views.length + '%'
    }

    // make sure view stays in frame when using infinite option
    if (infinite && !this.inBounds) {
      style.position = 'relative'
      style[(axis === 'y') ? 'top' : 'left'] = this.getPosition()
    }

    // finally, apply any animations
    return {
      ...style,
      ...this.pager.animationBus.getStyles(this)
    }
  }
}

class Pager extends Events {
  constructor(options = {}) {
    super()

    this.options = {
      viewsToShow: 1,
      viewsToMove: 1,
      align: 0,
      contain: false,
      axis: 'x',
      autoSize: false,
      animations: [],
      infinite: false,
      instant: false,
      swipe: true,
      swipeThreshold: 0.5,
      flickTimeout: 300,
      accessibility: true,
      ...options
    }

    this.views = []
    this.currentIndex = 0
    this.currentView = null
    this.currentTween = 0
    this.trackPosition = 0
    this.isSwiping = false

    this.animationBus = new AnimationBus({
      animations: this.options.animations,
      origin: view => view.origin
    })

    if (isWindowDefined) {
      this.resizeObserver = new ResizeObserver(() => {
        this.hydrate()
      })
    }
  }

  setOptions(options) {
    const lastOptions = this.options

    // spread new options over the old ones
    this.options = {
      ...this.options,
      ...options
    }

    // merge animations into animation bus
    this.animationBus.animations = this.options.animations

    // fire a viewChange event with the new indicies if viewsToShow has changed
    if (lastOptions.viewsToShow !== this.options.viewsToShow) {
      this.emit('viewChange', this.getCurrentIndicies())
    }
  }

  hydrate = () => {
    this.frame.setSize()
    this.track.setSize()
    this.views.forEach(view => {
      view.setSize()
      view.setTarget()
    })
    this.setPositionValue()
    this.setViewStyles()
    this.emit('hydrated')
  }

  addFrame(node) {
    this.frame = new PagerElement({
      node,
      pager: this
    })
  }

  addTrack(node) {
    this.track = new Track({
      node,
      pager: this
    })
  }

  addView(node) {
    const index = this.views.length
    const view = new View({
      node,
      index,
      pager: this
    })

    // add view to collection
    this.views.push(view)

    // set this as the first view if there isn't one yet
    if (!this.currentView) {
      this.setCurrentView({
        index,
        suppressEvent: true
      })
    }

    // listen for width and height changes
    if (isWindowDefined) {
      this.resizeObserver.observe(node)
    }

    // fire an event
    this.emit('viewAdded')

    return view
  }

  removeView(view) {
    // filter out view
    this.views = this.views.filter(_view => view !== _view)

    // stop observing node
    if (isWindowDefined) {
      this.resizeObserver.disconnect(view.node)
    }

    // fire an event
    this.emit('viewRemoved')
  }

  prev() {
    this.setCurrentView({ direction: -1 })
  }

  next() {
    this.setCurrentView({ direction: 1 })
  }

  setCurrentView({
    direction = 0,
    index = this.currentIndex,
    suppressEvent = false
  }) {
    const { viewsToMove, infinite } = this.options
    const newIndex = index + (direction * viewsToMove)

    const previousIndex = this.currentIndex
    const currentIndex = infinite ? newIndex : clamp(newIndex, 0, this.views.length - 1)

    const previousView = this.getView(previousIndex)
    const currentView = this.getView(currentIndex)

    // set current index and view
    this.currentIndex = currentIndex
    this.currentView = currentView

    // swap current view flags
    previousView.isCurrent = false
    currentView.isCurrent = true

    // set the track position to the new view
    this.setPositionValue()

    if (!suppressEvent) {
      this.emit('viewChange', this.getCurrentIndicies())
    }
  }

  setPositionValue(trackPosition = this.currentView ? this.currentView.target : 0) {
    if (!this.isSwiping) {
      const { viewsToShow, autoSize, infinite, contain } = this.options
      const trackSize = this.getTrackSize()

      if (infinite) {
        // we offset by a track multiplier so infinite animation can take advantage of
        // physics by animating to a large value, the final value provided in getTransformValue
        // will return the proper wrapped value
        trackPosition -= (Math.floor(this.currentIndex / this.views.length) || 0) * trackSize
      }

      if (contain) {
        const trackEndOffset = ((viewsToShow === 'auto' && autoSize) || viewsToShow <= 1)
          ? 0 : this.getFrameSize({ autoSize: false })
        trackPosition = clamp(trackPosition, trackEndOffset - trackSize, 0)
      }
    }

    this.trackPosition = trackPosition
  }

  setViewStyles(trackPosition = 0) {
    const { infinite, align } = this.options
    const frameSize = this.getFrameSize()
    const trackSize = this.getTrackSize()
    const wrappedtrackPosition = modulo(trackPosition, -trackSize)

    this.views.reduce((lastPosition, view, index) => {
      const viewSize = view.getSize()
      const nextPosition = lastPosition + viewSize
      let position = lastPosition

      if (nextPosition + (viewSize * align) < Math.abs(wrappedtrackPosition)) {
        // shift views around so they are always visible in frame
        if (infinite) {
          position += trackSize - lastPosition
        }
        view.inBounds = false
      } else {
        view.inBounds = true
      }

      view.setPosition(position)
      view.setOrigin(trackPosition)

      return nextPosition
    }, 0)
  }

  getNumericViewsToShow() {
    return isNaN(this.options.viewsToShow) ? 1 : this.options.viewsToShow
  }

  getMaxDimensions(indices) {
    const { axis } = this.options
    const widths = []
    const heights = []

    indices.forEach(index => {
      const view = isNaN(index) ? index : this.getView(index)
      widths.push(view.getSize('width'))
      heights.push(view.getSize('height'))
    })

    return {
      width: (axis === 'x') ? sum(widths) : max(widths),
      height: (axis === 'y') ? sum(heights) : max(heights)
    }
  }

  getCurrentIndicies() {
    const { infinite, contain } = this.options
    const currentViews = []
    const viewsToShow = isNaN(this.options.viewsToShow) ? 1 : this.options.viewsToShow
    let minIndex = this.currentIndex
    let maxIndex = this.currentIndex + (viewsToShow - 1)

    if (contain) {
      // if containing, we need to clamp the start and end indexes so we only return what's in view
      minIndex = clamp(minIndex, 0, this.views.length - viewsToShow)
      maxIndex = clamp(maxIndex, 0, this.views.length - 1)
      for (let i = minIndex; i <= maxIndex; i++) {
        currentViews.push(i)
      }
    } else {
      for (let i = minIndex; i <= maxIndex; i++) {
        currentViews.push(infinite
          ? modulo(i, this.views.length)
          : clamp(i, 0, this.views.length - 1)
        )
      }
    }

    return currentViews
  }

  getFrameSize({ autoSize = this.options.autoSize, fullSize = false } = {}) {
    let dimensions = {
      width: 0,
      height: 0
    }

    if (this.views.length) {
      if (autoSize) {
        const currentViews = this.getCurrentIndicies()
        dimensions = this.getMaxDimensions(currentViews)
      } else if (this.frame) {
        dimensions = {
          width: this.frame.getSize('width'),
          height: this.frame.getSize('height')
        }
      }
    }

    if (fullSize) {
      return dimensions
    } else {
      return dimensions[this.options.axis === 'x' ? 'width' : 'height']
    }
  }

  getTrackSize(includeLastSlide = true) {
    const lastIndex = includeLastSlide ? this.views.length : this.views.length - 1
    let size = 0
    this.views.slice(0, lastIndex).forEach(view => {
      size += view.getSize()
    })
    return size
  }

  getView(index) {
    return this.views[modulo(index, this.views.length)]
  }

  // where the view should start
  getStartCoords(index) {
    let target = 0
    this.views.slice(0, index).forEach(view => {
      target -= view.getSize()
    })
    return target
  }

  // how much to offset the view defined by the align option
  getAlignOffset(view) {
    const frameSize = this.getFrameSize({ autoSize: false })
    const viewSize = view.getSize()
    return (frameSize - viewSize) * this.options.align
  }

  getPositionValue(trackPosition = this.trackPosition) {
    const { infinite, contain } = this.options
    const position = { x: 0, y: 0 }

    // store the current animated value so we can reference it later
    this.currentTween = trackPosition

    // wrap the track position if this is an infinite track
    if (infinite) {
      const trackSize = this.getTrackSize()
      trackPosition = modulo(trackPosition, -trackSize) || 0
    }

    // emit a "scroll" event so we can do things based on the progress of the track
    this.emit('scroll', {
      progress: trackPosition / this.getTrackSize(false),
      position: trackPosition
    })

    // set the proper transform axis based on our options
    position[this.options.axis] = trackPosition

    return position
  }

  resetViewIndex() {
    // reset back to a normal index
    this.setCurrentView({
      index: modulo(this.currentIndex, this.views.length),
      suppressEvent: true
    })
  }
}

export default Pager
<|endoftext|>


<commit_before>/* @flow */

import * as React from 'react';
import { StyleSheet, Keyboard } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, { Easing } from 'react-native-reanimated';

import type {
  Layout,
  NavigationState,
  Route,
  Listener,
  PagerCommonProps,
} from './types';

type Props<T: Route> = {|
  ...PagerCommonProps,
  onIndexChange: (index: number) => mixed,
  navigationState: NavigationState<T>,
  layout: Layout,
  // Clip unfocused views to improve memory usage
  // Don't enable this on iOS where this is buggy and views don't re-appear
  removeClippedSubviews?: boolean,
  children: (props: {|
    // Animated value which represents the state of current index
    // It can include fractional digits as it represents the intermediate value
    position: Animated.Node<number>,
    // Function to actually render the content of the pager
    // The parent component takes care of rendering
    render: (children: React.Node) => React.Node,
    // Add a listener to listen for position updates
    addListener: (type: 'position', listener: Listener) => void,
    // Remove a position listener
    removeListener: (type: 'position', listener: Listener) => void,
    // Callback to call when switching the tab
    // The tab switch animation is performed even if the index in state is unchanged
    jumpTo: (key: string) => void,
  |}) => React.Node,
|};

const {
  Clock,
  Value,
  onChange,
  abs,
  add,
  and,
  block,
  call,
  clockRunning,
  cond,
  divide,
  eq,
  event,
  greaterThan,
  max,
  min,
  multiply,
  neq,
  or,
  round,
  set,
  spring,
  startClock,
  stopClock,
  sub,
  timing,
} = Animated;

const TRUE = 1;
const FALSE = 0;
const NOOP = 0;
const UNSET = -1;

const DIRECTION_LEFT = 1;
const DIRECTION_RIGHT = -1;

const SWIPE_DISTANCE_MINIMUM = 20;
const SWIPE_DISTANCE_MULTIPLIER = 1 / 1.75;

const SPRING_CONFIG = {
  damping: 30,
  mass: 1,
  stiffness: 200,
  overshootClamping: true,
  restSpeedThreshold: 0.001,
  restDisplacementThreshold: 0.001,
};

const TIMING_CONFIG = {
  duration: 250,
  easing: Easing.out(Easing.cubic),
};

export default class Pager<T: Route> extends React.Component<Props<T>> {
  static defaultProps = {
    swipeVelocityThreshold: 1200,
  };

  componentDidUpdate(prevProps: Props<T>) {
    const { index } = this.props.navigationState;

    if (
      // Check for index in state to avoid unintended transition if component updates during swipe
      (index !== prevProps.navigationState.index &&
        index !== this._currentIndexValue) ||
      // Check if the user updated the index correctly after an update
      (typeof this._pendingIndexValue === 'number' &&
        index !== this._pendingIndexValue)
    ) {
      // Index in user's state is different from the index being tracked
      this._jumpToIndex(index);
    }

    // Reset the pending index
    this._pendingIndexValue = undefined;

    // Update our mappings of animated nodes when props change
    if (
      prevProps.navigationState.routes.length !==
      this.props.navigationState.routes.length
    ) {
      this._routesLength.setValue(this.props.navigationState.routes.length);
    }

    if (prevProps.layout.width !== this.props.layout.width) {
      this._position.setValue(-index * this.props.layout.width);
      this._layoutWidth.setValue(this.props.layout.width);
    }

    if (this.props.swipeDistanceThreshold != null) {
      if (
        prevProps.swipeDistanceThreshold !== this.props.swipeDistanceThreshold
      ) {
        this._swipeDistanceThreshold.setValue(
          this.props.swipeDistanceThreshold
        );
      }
    } else {
      if (prevProps.layout.width !== this.props.layout.width) {
        this._swipeDistanceThreshold.setValue(
          this.props.layout.width * SWIPE_DISTANCE_MULTIPLIER
        );
      }
    }

    if (
      prevProps.swipeVelocityThreshold !== this.props.swipeVelocityThreshold
    ) {
      this._swipeVelocityThreshold.setValue(this.props.swipeVelocityThreshold);
    }
  }

  // Clock used for tab transition animations
  _clock = new Clock();

  // Current state of the gesture
  _velocityX = new Value(0);
  _gestureX = new Value(0);
  _gestureState = new Value(State.UNDETERMINED);
  _offsetX = new Value(0);

  // Current position of the page (translateX value)
  _position = new Value(
    // Intial value is based on the index and page width
    this.props.navigationState.index * this.props.layout.width * DIRECTION_RIGHT
  );

  // Initial index of the tabs
  _index = new Value(this.props.navigationState.index);

  // Next index of the tabs, updated for navigation from outside (tab press, state update)
  _nextIndex = new Value(UNSET);

  // Whether the user is currently dragging the screen
  _isSwiping = new Value(FALSE);

  // Whether the update was due to swipe gesture
  // This controls whether the transition will use a spring or timing animation
  // Remember to set it before transition needs to occur
  _isSwipeGesture = new Value(FALSE);

  // Mappings to some prop values
  // We use them in animation calculations, so we need live animated nodes
  _routesLength = new Value(this.props.navigationState.routes.length);
  _layoutWidth = new Value(this.props.layout.width);

  // Threshold values to determine when to trigger a swipe gesture
  _swipeDistanceThreshold = new Value(this.props.swipeDistanceThreshold || 180);
  _swipeVelocityThreshold = new Value(this.props.swipeVelocityThreshold);

  // Whether we need to add a listener for position change
  // To avoid unnecessary traffic through the bridge, don't add listeners unless needed
  _isListening = new Value(FALSE);

  // The current index change caused by the pager's animation
  // The pager is used as a controlled component
  // We need to keep track of the index to determine when to trigger animation
  // The state will change at various points, we should only respond when we are out of sync
  // This will ensure smoother animation and avoid weird glitches
  _currentIndexValue = this.props.navigationState.index;

  // The pending index value as result of state update caused by swipe gesture
  // We need to set it when state changes from inside this component
  // It also needs to be reset right after componentDidUpdate fires
  _pendingIndexValue: ?number = undefined;

  // Listeners for the animated value
  _positionListeners: Listener[] = [];

  _jumpToIndex = (index: number) => {
    // If the index changed, we need to trigger a tab switch
    this._isSwipeGesture.setValue(FALSE);
    this._nextIndex.setValue(index);
  };

  _jumpTo = (key: string) => {
    const { navigationState } = this.props;

    const index = navigationState.routes.findIndex(route => route.key === key);

    // A tab switch might occur when we're in the middle of a transition
    // In that case, the index might be same as before
    // So we conditionally make the pager to update the position
    if (navigationState.index === index) {
      this._jumpToIndex(index);
    } else {
      this.props.onIndexChange(index);
    }
  };

  _addListener = (type: string, listener: Listener) => {
    if (type !== 'position') {
      return;
    }

    this._positionListeners.push(listener);
    this._isListening.setValue(TRUE);
  };

  _removeListener = (type: string, listener: Listener) => {
    if (type !== 'position') {
      return;
    }

    const index = this._positionListeners.indexOf(listener);

    if (index > -1) {
      this._positionListeners.splice(index, 1);
    }

    if (this._positionListeners.length === 0) {
      this._isListening.setValue(FALSE);
    }
  };

  _handlePositionChange = ([translateX]: [number]) => {
    // The position value is calculated based on the translate value
    // If we don't have the layout yet, we should return the current index
    const value = this.props.layout.width
      ? Math.abs(translateX / this.props.layout.width)
      : this.props.navigationState.index;

    this._positionListeners.forEach(listener => listener(value));
  };

  _transitionTo = (index: Animated.Node<number>) => {
    const toValue = new Value(0);
    const frameTime = new Value(0);

    const state = {
      position: this._position,
      time: new Value(0),
      finished: new Value(FALSE),
    };

    return block([
      cond(clockRunning(this._clock), NOOP, [
        // Animation wasn't running before
        // Set the initial values and start the clock
        set(toValue, multiply(index, this._layoutWidth, DIRECTION_RIGHT)),
        set(frameTime, 0),
        set(state.time, 0),
        set(state.finished, FALSE),
        set(this._index, index),
        startClock(this._clock),
      ]),
      cond(
        this._isSwipeGesture,
        // Animate the values with a spring for swipe
        spring(
          this._clock,
          { ...state, velocity: this._velocityX },
          { ...SPRING_CONFIG, toValue }
        ),
        // Otherwise use a timing animation for faster switching
        timing(
          this._clock,
          { ...state, frameTime },
          { ...TIMING_CONFIG, toValue }
        )
      ),
      cond(state.finished, [
        // Reset values
        set(this._isSwipeGesture, FALSE),
        set(this._gestureX, 0),
        set(this._velocityX, 0),
        // When the animation finishes, stop the clock
        stopClock(this._clock),
        call([this._index], ([value]) => {
          // If the index changed, and previous animation has finished, update state
          this.props.onIndexChange(value);

          // Without this check, the pager can go to an infinite update <-> animate loop for sync updates
          if (value !== this.props.navigationState.index) {
            this._pendingIndexValue = value;

            // Force componentDidUpdate to fire, whether user does a setState or not
            // This allows us to detect when the user drops the update and revert back
            // It's necessary to make sure that the state stays in sync
            this.forceUpdate();
          }
        }),
      ]),
    ]);
  };

  _handleGestureEvent = event([
    {
      nativeEvent: {
        translationX: this._gestureX,
        velocityX: this._velocityX,
        state: this._gestureState,
      },
    },
  ]);

  _translateX = block([
    call([this._index], ([value]) => {
      this._currentIndexValue = value;
    }),
    // Conditionally listen for changes in the position value
    // The position value can changes a lot in short time
    // So we only add a listener when necessary to avoid extra overhead
    cond(
      this._isListening,
      onChange(
        this._position,
        call([this._position], this._handlePositionChange)
      )
    ),
    onChange(
      this._isSwiping,
      // Listen to updates for this value only when it changes
      // Without `onChange`, this will fire even if the value didn't change
      // We don't want to call the listeners if the value didn't change
      call([this._isSwiping], ([value]) => {
        const { keyboardDismissMode, onSwipeStart, onSwipeEnd } = this.props;

        if (value === TRUE) {
          onSwipeStart && onSwipeStart();

          if (keyboardDismissMode === 'on-drag') {
            Keyboard.dismiss();
          }
        } else {
          onSwipeEnd && onSwipeEnd();
        }
      })
    ),
    onChange(
      this._nextIndex,
      cond(neq(this._nextIndex, UNSET), [
        // Stop any running animations
        cond(clockRunning(this._clock), stopClock(this._clock)),
        // Update the index to trigger the transition
        set(this._index, this._nextIndex),
        set(this._nextIndex, UNSET),
      ])
    ),
    cond(
      eq(this._gestureState, State.ACTIVE),
      [
        cond(this._isSwiping, NOOP, [
          // We weren't dragging before, set it to true
          set(this._isSwiping, TRUE),
          set(this._isSwipeGesture, TRUE),
          // Also update the drag offset to the last position
          set(this._offsetX, this._position),
        ]),
        // Update position with previous offset + gesture distance
        set(this._position, add(this._offsetX, this._gestureX)),
        // Stop animations while we're dragging
        stopClock(this._clock),
      ],
      [
        set(this._isSwiping, FALSE),
        this._transitionTo(
          cond(
            and(
              greaterThan(abs(this._gestureX), SWIPE_DISTANCE_MINIMUM),
              or(
                greaterThan(abs(this._gestureX), this._swipeDistanceThreshold),
                greaterThan(abs(this._velocityX), this._swipeVelocityThreshold)
              )
            ),
            // For swipe gesture, to calculate the index, determine direction and add to index
            // When the user swipes towards the left, we transition to the next tab
            // When the user swipes towards the right, we transition to the previous tab
            round(
              min(
                max(
                  0,
                  sub(
                    this._index,
                    cond(
                      greaterThan(
                        // Gesture can be positive, or negative
                        // Get absolute for comparision
                        abs(this._gestureX),
                        this._swipeDistanceThreshold
                      ),
                      // If gesture value exceeded the threshold, calculate direction from distance travelled
                      cond(
                        greaterThan(this._gestureX, 0),
                        DIRECTION_LEFT,
                        DIRECTION_RIGHT
                      ),
                      // Otherwise calculate direction from the gesture velocity
                      cond(
                        greaterThan(this._velocityX, 0),
                        DIRECTION_LEFT,
                        DIRECTION_RIGHT
                      )
                    )
                  )
                ),
                sub(this._routesLength, 1)
              )
            ),
            // Index didn't change/changed due to state update
            this._index
          )
        ),
      ]
    ),
    this._position,
  ]);

  render() {
    const {
      layout,
      navigationState,
      swipeEnabled,
      children,
      removeClippedSubviews,
    } = this.props;

    // Make sure that the translation doesn't exceed the bounds to prevent overscrolling
    const translateX = min(
      max(
        multiply(
          this._layoutWidth,
          sub(this._routesLength, 1),
          DIRECTION_RIGHT
        ),
        this._translateX
      ),
      0
    );

    const position = cond(
      this._layoutWidth,
      divide(abs(translateX), this._layoutWidth),
      this._index
    );

    return children({
      position,
      addListener: this._addListener,
      removeListener: this._removeListener,
      jumpTo: this._jumpTo,
      render: children => (
        <PanGestureHandler
          enabled={layout.width !== 0 && swipeEnabled}
          onGestureEvent={this._handleGestureEvent}
          onHandlerStateChange={this._handleGestureEvent}
          activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
          failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
        >
          <Animated.View
            removeClippedSubviews={removeClippedSubviews}
            style={[
              styles.container,
              layout.width
                ? {
                    width: layout.width * navigationState.routes.length,
                    transform: [{ translateX }],
                  }
                : null,
            ]}
          >
            {children}
          </Animated.View>
        </PanGestureHandler>
      ),
    });
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
  },
});
<commit_msg>feat: add RTL support for Slider (#740)<commit_after>/* @flow */

import * as React from 'react';
import { StyleSheet, Keyboard, I18nManager } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, { Easing } from 'react-native-reanimated';

import type {
  Layout,
  NavigationState,
  Route,
  Listener,
  PagerCommonProps,
} from './types';

type Props<T: Route> = {|
  ...PagerCommonProps,
  onIndexChange: (index: number) => mixed,
  navigationState: NavigationState<T>,
  layout: Layout,
  // Clip unfocused views to improve memory usage
  // Don't enable this on iOS where this is buggy and views don't re-appear
  removeClippedSubviews?: boolean,
  children: (props: {|
    // Animated value which represents the state of current index
    // It can include fractional digits as it represents the intermediate value
    position: Animated.Node<number>,
    // Function to actually render the content of the pager
    // The parent component takes care of rendering
    render: (children: React.Node) => React.Node,
    // Add a listener to listen for position updates
    addListener: (type: 'position', listener: Listener) => void,
    // Remove a position listener
    removeListener: (type: 'position', listener: Listener) => void,
    // Callback to call when switching the tab
    // The tab switch animation is performed even if the index in state is unchanged
    jumpTo: (key: string) => void,
  |}) => React.Node,
|};

const {
  Clock,
  Value,
  onChange,
  abs,
  add,
  and,
  block,
  call,
  clockRunning,
  cond,
  divide,
  eq,
  event,
  greaterThan,
  max,
  min,
  multiply,
  neq,
  or,
  not,
  round,
  set,
  spring,
  startClock,
  stopClock,
  sub,
  timing,
} = Animated;

const TRUE = 1;
const FALSE = 0;
const NOOP = 0;
const UNSET = -1;

const DIRECTION_LEFT = 1;
const DIRECTION_RIGHT = -1;

const SWIPE_DISTANCE_MINIMUM = 20;
const SWIPE_DISTANCE_MULTIPLIER = 1 / 1.75;

const SPRING_CONFIG = {
  damping: 30,
  mass: 1,
  stiffness: 200,
  overshootClamping: true,
  restSpeedThreshold: 0.001,
  restDisplacementThreshold: 0.001,
};

const TIMING_CONFIG = {
  duration: 250,
  easing: Easing.out(Easing.cubic),
};

export default class Pager<T: Route> extends React.Component<Props<T>> {
  static defaultProps = {
    swipeVelocityThreshold: 1200,
  };

  componentDidUpdate(prevProps: Props<T>) {
    const { index } = this.props.navigationState;

    if (
      // Check for index in state to avoid unintended transition if component updates during swipe
      (index !== prevProps.navigationState.index &&
        index !== this._currentIndexValue) ||
      // Check if the user updated the index correctly after an update
      (typeof this._pendingIndexValue === 'number' &&
        index !== this._pendingIndexValue)
    ) {
      // Index in user's state is different from the index being tracked
      this._jumpToIndex(index);
    }

    // Reset the pending index
    this._pendingIndexValue = undefined;

    // Update our mappings of animated nodes when props change
    if (
      prevProps.navigationState.routes.length !==
      this.props.navigationState.routes.length
    ) {
      this._routesLength.setValue(this.props.navigationState.routes.length);
    }

    if (prevProps.layout.width !== this.props.layout.width) {
      this._position.setValue(-index * this.props.layout.width);
      this._layoutWidth.setValue(this.props.layout.width);
    }

    if (this.props.swipeDistanceThreshold != null) {
      if (
        prevProps.swipeDistanceThreshold !== this.props.swipeDistanceThreshold
      ) {
        this._swipeDistanceThreshold.setValue(
          this.props.swipeDistanceThreshold
        );
      }
    } else {
      if (prevProps.layout.width !== this.props.layout.width) {
        this._swipeDistanceThreshold.setValue(
          this.props.layout.width * SWIPE_DISTANCE_MULTIPLIER
        );
      }
    }

    if (
      prevProps.swipeVelocityThreshold !== this.props.swipeVelocityThreshold
    ) {
      this._swipeVelocityThreshold.setValue(this.props.swipeVelocityThreshold);
    }
  }

  // Clock used for tab transition animations
  _clock = new Clock();

  // Current state of the gesture
  _velocityX = new Value(0);
  _gestureX = new Value(0);
  _gestureState = new Value(State.UNDETERMINED);
  _offsetX = new Value(0);

  // Current position of the page (translateX value)
  _position = new Value(
    // Initial value is based on the index and page width
    this.props.navigationState.index * this.props.layout.width * DIRECTION_RIGHT
  );

  // Initial index of the tabs
  _index = new Value(this.props.navigationState.index);

  // Next index of the tabs, updated for navigation from outside (tab press, state update)
  _nextIndex = new Value(UNSET);

  // Whether the user is currently dragging the screen
  _isSwiping = new Value(FALSE);

  // Whether the update was due to swipe gesture
  // This controls whether the transition will use a spring or timing animation
  // Remember to set it before transition needs to occur
  _isSwipeGesture = new Value(FALSE);

  // Mappings to some prop values
  // We use them in animation calculations, so we need live animated nodes
  _routesLength = new Value(this.props.navigationState.routes.length);
  _layoutWidth = new Value(this.props.layout.width);

  // Threshold values to determine when to trigger a swipe gesture
  _swipeDistanceThreshold = new Value(this.props.swipeDistanceThreshold || 180);
  _swipeVelocityThreshold = new Value(this.props.swipeVelocityThreshold);

  // The reason for using this value instead of simply passing `this._velocity`
  // into a spring animation is that we need to reverse it if we're using RTL mode.
  // Also, it's not possible to pass multiplied value there, because
  // value passed to STATE of spring (the first argument) has to be Animated.Value
  // and it's not allowed to pass other nodes there. The result of multiplying is not an
  // Animated.Value. So this value is being updated on each start of spring animation.
  _initialVelocityForSpring = new Value(0);

  // Whether we need to add a listener for position change
  // To avoid unnecessary traffic through the bridge, don't add listeners unless needed
  _isListening = new Value(FALSE);

  // The current index change caused by the pager's animation
  // The pager is used as a controlled component
  // We need to keep track of the index to determine when to trigger animation
  // The state will change at various points, we should only respond when we are out of sync
  // This will ensure smoother animation and avoid weird glitches
  _currentIndexValue = this.props.navigationState.index;

  // The pending index value as result of state update caused by swipe gesture
  // We need to set it when state changes from inside this component
  // It also needs to be reset right after componentDidUpdate fires
  _pendingIndexValue: ?number = undefined;

  // Listeners for the animated value
  _positionListeners: Listener[] = [];

  _jumpToIndex = (index: number) => {
    // If the index changed, we need to trigger a tab switch
    this._isSwipeGesture.setValue(FALSE);
    this._nextIndex.setValue(index);
  };

  _jumpTo = (key: string) => {
    const { navigationState } = this.props;

    const index = navigationState.routes.findIndex(route => route.key === key);

    // A tab switch might occur when we're in the middle of a transition
    // In that case, the index might be same as before
    // So we conditionally make the pager to update the position
    if (navigationState.index === index) {
      this._jumpToIndex(index);
    } else {
      this.props.onIndexChange(index);
    }
  };

  _addListener = (type: string, listener: Listener) => {
    if (type !== 'position') {
      return;
    }

    this._positionListeners.push(listener);
    this._isListening.setValue(TRUE);
  };

  _removeListener = (type: string, listener: Listener) => {
    if (type !== 'position') {
      return;
    }

    const index = this._positionListeners.indexOf(listener);

    if (index > -1) {
      this._positionListeners.splice(index, 1);
    }

    if (this._positionListeners.length === 0) {
      this._isListening.setValue(FALSE);
    }
  };

  _handlePositionChange = ([translateX]: [number]) => {
    // The position value is calculated based on the translate value
    // If we don't have the layout yet, we should return the current index
    const value = this.props.layout.width
      ? Math.abs(translateX / this.props.layout.width)
      : this.props.navigationState.index;

    this._positionListeners.forEach(listener => listener(value));
  };

  _transitionTo = (index: Animated.Node<number>) => {
    const toValue = new Value(0);
    const frameTime = new Value(0);

    const state = {
      position: this._position,
      time: new Value(0),
      finished: new Value(FALSE),
    };

    return block([
      cond(clockRunning(this._clock), NOOP, [
        // Animation wasn't running before
        // Set the initial values and start the clock
        set(toValue, multiply(index, this._layoutWidth, DIRECTION_RIGHT)),
        set(frameTime, 0),
        set(state.time, 0),
        set(state.finished, FALSE),
        set(this._index, index),
        startClock(this._clock),
      ]),
      cond(
        this._isSwipeGesture,
        // Animate the values with a spring for swipe
        [
          cond(
            not(clockRunning(this._clock)),
            I18nManager.isRTL
              ? set(
                  this._initialVelocityForSpring,
                  multiply(-1, this._velocityX)
                )
              : set(this._initialVelocityForSpring, this._velocityX)
          ),
          spring(
            this._clock,
            { ...state, velocity: this._initialVelocityForSpring },
            { ...SPRING_CONFIG, toValue }
          ),
        ],
        // Otherwise use a timing animation for faster switching
        timing(
          this._clock,
          { ...state, frameTime },
          { ...TIMING_CONFIG, toValue }
        )
      ),
      cond(state.finished, [
        // Reset values
        set(this._isSwipeGesture, FALSE),
        set(this._gestureX, 0),
        set(this._velocityX, 0),
        // When the animation finishes, stop the clock
        stopClock(this._clock),
        call([this._index], ([value]) => {
          // If the index changed, and previous animation has finished, update state
          this.props.onIndexChange(value);

          // Without this check, the pager can go to an infinite update <-> animate loop for sync updates
          if (value !== this.props.navigationState.index) {
            this._pendingIndexValue = value;

            // Force componentDidUpdate to fire, whether user does a setState or not
            // This allows us to detect when the user drops the update and revert back
            // It's necessary to make sure that the state stays in sync
            this.forceUpdate();
          }
        }),
      ]),
    ]);
  };

  _handleGestureEvent = event([
    {
      nativeEvent: {
        translationX: this._gestureX,
        velocityX: this._velocityX,
        state: this._gestureState,
      },
    },
  ]);

  _translateX = block([
    call([this._index], ([value]) => {
      this._currentIndexValue = value;
    }),
    // Conditionally listen for changes in the position value
    // The position value can changes a lot in short time
    // So we only add a listener when necessary to avoid extra overhead
    cond(
      this._isListening,
      onChange(
        this._position,
        call([this._position], this._handlePositionChange)
      )
    ),
    onChange(
      this._isSwiping,
      // Listen to updates for this value only when it changes
      // Without `onChange`, this will fire even if the value didn't change
      // We don't want to call the listeners if the value didn't change
      call([this._isSwiping], ([value]) => {
        const { keyboardDismissMode, onSwipeStart, onSwipeEnd } = this.props;

        if (value === TRUE) {
          onSwipeStart && onSwipeStart();

          if (keyboardDismissMode === 'on-drag') {
            Keyboard.dismiss();
          }
        } else {
          onSwipeEnd && onSwipeEnd();
        }
      })
    ),
    onChange(
      this._nextIndex,
      cond(neq(this._nextIndex, UNSET), [
        // Stop any running animations
        cond(clockRunning(this._clock), stopClock(this._clock)),
        // Update the index to trigger the transition
        set(this._index, this._nextIndex),
        set(this._nextIndex, UNSET),
      ])
    ),
    cond(
      eq(this._gestureState, State.ACTIVE),
      [
        cond(this._isSwiping, NOOP, [
          // We weren't dragging before, set it to true
          set(this._isSwiping, TRUE),
          set(this._isSwipeGesture, TRUE),
          // Also update the drag offset to the last position
          set(this._offsetX, this._position),
        ]),
        // Update position with previous offset + gesture distance
        set(
          this._position,
          I18nManager.isRTL
            ? sub(this._offsetX, this._gestureX)
            : add(this._offsetX, this._gestureX)
        ),
        // Stop animations while we're dragging
        stopClock(this._clock),
      ],
      [
        set(this._isSwiping, FALSE),
        this._transitionTo(
          cond(
            and(
              greaterThan(abs(this._gestureX), SWIPE_DISTANCE_MINIMUM),
              or(
                greaterThan(abs(this._gestureX), this._swipeDistanceThreshold),
                greaterThan(abs(this._velocityX), this._swipeVelocityThreshold)
              )
            ),
            // For swipe gesture, to calculate the index, determine direction and add to index
            // When the user swipes towards the left, we transition to the next tab
            // When the user swipes towards the right, we transition to the previous tab
            round(
              min(
                max(
                  0,
                  sub(
                    this._index,
                    cond(
                      greaterThan(
                        // Gesture can be positive, or negative
                        // Get absolute for comparision
                        abs(this._gestureX),
                        this._swipeDistanceThreshold
                      ),
                      // If gesture value exceeded the threshold, calculate direction from distance travelled
                      cond(
                        greaterThan(this._gestureX, 0),
                        I18nManager.isRTL ? DIRECTION_RIGHT : DIRECTION_LEFT,
                        I18nManager.isRTL ? DIRECTION_LEFT : DIRECTION_RIGHT
                      ),
                      // Otherwise calculate direction from the gesture velocity
                      cond(
                        greaterThan(this._velocityX, 0),
                        I18nManager.isRTL ? DIRECTION_RIGHT : DIRECTION_LEFT,
                        I18nManager.isRTL ? DIRECTION_LEFT : DIRECTION_RIGHT
                      )
                    )
                  )
                ),
                sub(this._routesLength, 1)
              )
            ),
            // Index didn't change/changed due to state update
            this._index
          )
        ),
      ]
    ),
    this._position,
  ]);

  render() {
    const {
      layout,
      navigationState,
      swipeEnabled,
      children,
      removeClippedSubviews,
    } = this.props;

    // Make sure that the translation doesn't exceed the bounds to prevent overscrolling
    const translateX = min(
      max(
        multiply(
          this._layoutWidth,
          sub(this._routesLength, 1),
          DIRECTION_RIGHT
        ),
        this._translateX
      ),
      0
    );

    const position = cond(
      this._layoutWidth,
      divide(abs(translateX), this._layoutWidth),
      this._index
    );

    return children({
      position,
      addListener: this._addListener,
      removeListener: this._removeListener,
      jumpTo: this._jumpTo,
      render: children => (
        <PanGestureHandler
          enabled={layout.width !== 0 && swipeEnabled}
          onGestureEvent={this._handleGestureEvent}
          onHandlerStateChange={this._handleGestureEvent}
          activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
          failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
        >
          <Animated.View
            removeClippedSubviews={removeClippedSubviews}
            style={[
              styles.container,
              layout.width
                ? {
                    width: layout.width * navigationState.routes.length,
                    transform: [
                      {
                        translateX: I18nManager.isRTL
                          ? multiply(translateX, -1)
                          : translateX,
                      },
                    ],
                  }
                : null,
            ]}
          >
            {children}
          </Animated.View>
        </PanGestureHandler>
      ),
    });
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
  },
});
<|endoftext|>


<commit_before>(function (exports) {
  "use strict";

  Number.Range = function (min, max) {
    max = max || (min ? 0 : 100);
    min = min || 0;

    this.min = Math.min(min, max);
    this.max = Math.max(min, max);
  };

  Number.Range.implements({
    limit: {
      value: function (value) {
        if (value > this.max) {
          return this.max;
        } else if (value < this.min) {
          return this.min;
        }
        return value;
      }
    }
  });

}(typeof exports === 'undefined' ? window : exports));
<commit_msg>simplier Range.prototype.limit<commit_after>(function (exports) {
  "use strict";

  Number.Range = function (min, max) {
    max = max || (min ? 0 : 100);
    min = min || 0;

    this.min = Math.min(min, max);
    this.max = Math.max(min, max);
  };

  Number.Range.implements({
    limit: {
      value: function (value) {
        return Math.max(this.min, Math.min(value, this.max));
      }
    }
  });

}(typeof exports === 'undefined' ? window : exports));
<|endoftext|>


<commit_before>/**
 * Slidy main file.
 */

const Emitter = require('tiny-emitter');
const Hammer = require('hammerjs');

import bind from 'lodash/bind';
import debounce from 'lodash/debounce';
import forEach from 'lodash/forEach';
import * as detect from './utils/detect';
import { Controls } from './Controls';
import { Nav } from './Nav';
import { Pagination } from './Pagination';
import { Queue } from './Queue';

/**
 * Slidy main class.
 *
 * @export
 * @class Slidy
 */
export class Slidy {
  /**
   * Creates an instance of Slidy.
   * @param   {HTMLElement|string} el             Slider container (element or selector)
   * @param   {Object}             opts           Configuration settings
   * @param   {*}                  [context=null] Optional context, available through `this.context`
   * @param   {Object}             [data=null]    Additional optional data
   * @returns {undefined}
   */
  constructor(el, opts, context = null, data = null) {
    /* eslint-disable no-param-reassign */
    // Check and get element(s).
    if (typeof el === 'string') {
      el = document.querySelectorAll(el);
    }
    /* eslint-enable no-param-reassign */

    if (!el || el.length === 0) {
      console.error('Slidy: no element matching!');

      return;
    }

    if (el.length > 1) {
      console.warn('Slidy: multiple elements matching!');
    }

    this._el = el[0] || el;

    // Check and get options.
    this._opts = Object.assign({
      auto: false, // Boolean: start slider automaticaly
      click: true, // Boolean: enable click on slider
      controls: false, // Boolean: create prev/next buttons
      debounce: 100, // Integer: debounce delay on resize
      height: 'auto', // Mixed: integer (px) or 'auto'
      index: 0, // Integer: initial index
      interval: 2000, // Integer: time between 2 transitions
      loop: true, // Boolean: enable/disable loop
      namespace: 'slidy', // String: custom namespace
      nav: false, // Mixed: create navigation (number, thumb, custom)
      pagination: false, // Mixed: create pagination (1[separator]10)
      pause: true, // Boolean: pause on hover
      queue: 1, // Integer: queue max items
      resize: true, // Boolean: enable resize event and callback
      reverse: false, // Boolean: reverse directions / controls
      swipe: false, // Boolean: enable swipe
      tap: false, // Boolean: enable tap
      touch: false, // Boolean: enable BOTH tap/swipe (deprecated)
      transition: null, // Function: transition which returns a resolved promise
      zerofill: false, // Mixed(Boolean | number): '1' -> '01' for navigation/pagination
    }, opts);

    if (this._opts.transition === null) {
      console.error('Slidy: you should define a transition!');

      return;
    }

    if (this._opts.touch) {
      this._opts.swipe = true;
      this._opts.tap = true;
    }

    this._context = context;
    this._data = data;

    this._debounceDelay = this._opts.debounce;
    this._currentIndex = this._opts.index;
    this._newIndex = this._currentIndex;
    this._oldIndex = null;
    this._items = Array.prototype.map.call(this._el.children, item => item); // #TODO use Array.from ?
    this._length = this._items.length;
    this._hasPause = false;

    this._dispatcher = new Emitter();

    // Binding
    this.start = this.start.bind(this);
    this.slideNext = this.slideNext.bind(this);

    // Public events.
    this._dispatcher.on('beforeInit', () => {
      this.beforeInit();
    });
    this._dispatcher.on('afterInit', () => {
      this.afterInit();
    });
    if (this._opts.resize) {
      this._dispatcher.on('afterResize', () => {
        this.afterResize();
      });
    }
    this._dispatcher.on('beforeSlide', direction => {
      this.beforeSlide(direction);
    });
    this._dispatcher.on('afterSlide', direction => {
      this.afterSlide(direction);
    });

    this.init();
    this.bind();
  }

  /**
   * Getters/setters.
   */
  get outer() {
    return this._outer;
  }

  get el() {
    return this._el;
  }

  get items() {
    return this._items;
  }

  get context() {
    return this._context;
  }

  get data() {
    return this._data;
  }

  get currentIndex() {
    return this._currentIndex;
  }

  set currentIndex(i) {
    this._currentIndex = i;
  }

  get newIndex() {
    return this._newIndex;
  }

  set newIndex(i) {
    this._newIndex = i;
  }

  get oldIndex() {
    return this._oldIndex;
  }

  set oldIndex(i) {
    this._oldIndex = i;
  }

  get options() {
    return this._opts;
  }

  get dispatcher() {
    return this._dispatcher;
  }

  get namespace() {
    return this._opts.namespace;
  }

  /**
   * Init component.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  init() {
    this._dispatcher.emit('beforeInit');

    // Set height.
    // To get the most 'correct' auto-height,
    // do it before applying anything…
    if (this._opts.height === 'auto') {
      this.reset();
    } else {
      this._el.style.height = `${this._opts.height}px`;
    }

    // Add HTML wrapper.
    this._outer = document.createElement('div');
    this._el.before(this._outer);
    this._outer.append(this._el);

    // Add CSS classes.
    this._outer.classList.add(`${this.namespace}-outer`);
    this._el.classList.add(this.namespace);
    forEach(this._items, slide => {
      slide.classList.add(`${this.namespace}__item`);
    });

    // Set active class on currentIndex.
    this._items[this._currentIndex].classList.add('is-active');

    // Accessibility
    this._el.setAttribute('role', 'slider');
    this._el.setAttribute('aria-valuemin', 1);
    this._el.setAttribute('aria-valuemax', this._items.length);
    this._el.setAttribute('aria-valuenow', this._currentIndex + 1);

    if (this._opts.click) {
      this._el.style.cursor = 'pointer';
    }

    this._queue = new Queue(this, this._opts.transition);

    // Add controls.
    if (this._opts.controls) {
      this._controls = new Controls(this, {
        loop: this._opts.loop,
        controls: this._opts.controls,
      });
    }

    // Add nav.
    if (this._opts.nav) {
      this._nav = new Nav(this);
    }

    // Add pagination.
    if (this._opts.pagination) {
      this._pagination = new Pagination(this);
    }

    // Start auto mode.
    if (this._opts.auto) {
      this.t2 = setTimeout(this.start, this._opts.interval);
    }

    this._dispatcher.emit('afterInit');
  }

  /**
   * Bind event handlers.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  bind() {
    this.onEnter = this.enter.bind(this);
    this.onLeave = this.leave.bind(this);
    this.onClick = this.click.bind(this);
    this.onTap = this.tap.bind(this);
    this.onSwipe = this.swipe.bind(this);

    if (this._opts.resize) {
      this.onResize = bind(debounce(this.resize, this._debounceDelay), this);

      window.addEventListener('resize', this.onResize);
    }

    if (this._opts.pause && this._opts.auto) {
      this._outer.addEventListener('mouseenter', this.onEnter);
      this._hasPause = true;
    }

    if (this._opts.click && !detect.touchevents()) {
      this._el.addEventListener('click', this.onClick);
    }

    if ((this._opts.tap || this._opts.swipe) && detect.touchevents()) {
      const options = {
        recognizers: [],
      };

      this._mc = new Hammer.Manager(this._el, options);
    }

    if (this._opts.tap && detect.touchevents()) {
      const tap = new Hammer.Tap();

      this._mc.add(tap);
      this._mc.on('tap', this.onTap);
    }

    if (this._opts.swipe && detect.touchevents()) {
      const swipe = new Hammer.Swipe({ direction: Hammer.DIRECTION_HORIZONTAL });

      this._mc.add(swipe);
      this._mc.on('swipeleft swiperight', this.onSwipe);
    }
  }

  /**
   * API.
   */

  /**
   * Navigate to previous slide.
   *
   * @param {boolean} force force reverse
   * @returns {undefined}
   * @memberof Slidy
   */
  slidePrev(force = false) {
    if (this._opts.reverse && !force) {
      this.slideNext(true);

      return;
    }

    let newIndex = this._currentIndex - 1;

    if (newIndex < 0) {
      if (!this._opts.loop) {
        return;
      }
      newIndex = this._length - 1;
    }
    this.slide('prev');
  }

  /**
   * Navigate to next slide.
   *
   * @param {boolean} force force reverse
   * @returns {undefined}
   * @memberof Slidy
   */
  slideNext(force = false) {
    if (this._opts.reverse && !force) {
      this.slidePrev(true);

      return;
    }

    let newIndex = this._currentIndex + 1;

    if (newIndex === this._length) {
      if (!this._opts.loop) {
        return;
      }
      newIndex = 0;
    }
    this.slide('next');
  }

  /**
   * Navigate to slide by index.
   *
   * @param {number} index slide index
   * @returns {undefined}
   * @memberof Slidy
   */
  slideTo(index) {
    this.slide('to', index);
  }

  /**
   * Add move to the queue.
   *
   * @param {string} move prev|next|to
   * @param {number} [index=null] slide index
   * @returns {undefined}
   * @memberof Slidy
   */
  slide(move, index = null) {
    if (this._opts.auto) {
      clearInterval(this._t1);
      this._t1 = setInterval(this.slideNext, this._opts.interval);
    }

    if (this._queue) {
      this._queue.add(move, index);
    } else {
      // Prevent 'persistent' auto
      this.stop();
    }
  }

  /**
   * Height calculation if auto.
   * Called on 'init' and 'resize'.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  reset() {
    if (this._opts.height === 'auto') {
      // Reset inline height.
      this._el.style.height = '';

      // Check if items have height
      // if not, check first node
      // then remove 0 height, sort DESC, get the highest height.
      const getMinHeight = function getMinHeight(arr) {
        return arr
          .filter(item => item > 0)
          .sort((a, b) => b - a)
          .slice(0, 1);
      };

      const heights = [];
      const hasNoHeight = this._items[0].offsetHeight === 0;

      forEach(this._items, item => {
        if (hasNoHeight && item.hasChildNodes()) {
          heights.push(item.firstElementChild.offsetHeight);
        } else {
          heights.push(item.offsetHeight);
        }
      });
      this._el.style.height = `${getMinHeight(heights)}px`;
    }
  }

  /**
   * Start autoplay.
   * Enabled via "auto" and used by "pause" options.
   *
   * @param {number} delay delay before slideNext
   * @param {boolean} auto autoloop or not
   * @returns {undefined}
   * @memberof Slidy
   */
  start(delay = this._opts.interval, auto = this._opts.auto) {
    this._t2 = setTimeout(() => {
      this.slideNext();
      if (!this._hasPause && this._opts.pause) {
        this._outer.addEventListener('mouseenter', this.onEnter);
      }
      if (auto) {
        clearInterval(this._t1);
        this._t1 = setInterval(this.slideNext, this._opts.interval);
      }
    }, delay);
  }

  /**
   * Pause autoplay.
   * Used by "pause" options.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  stop() {
    if (this._hasPause) {
      this._outer.removeEventListener('mouseenter', this.onEnter);
    }
    clearTimeout(this._t2);
    clearInterval(this._t1);
  }

  /**
   * Destroy component.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  destroy() {
    this._destroyed = true;

    // Remove interval.
    this.stop();

    // Empty queue.
    if (this._queue) {
      this._queue.empty();
      delete this._queue;
    }

    // Remove listeners.
    if (this._opts.resize) {
      window.removeEventListener('resize', this.onResize);
    }
    this._el.removeEventListener('mouseenter', this.onEnter);
    this._el.removeEventListener('mouseleave', this.onLeave);
    this._el.removeEventListener('click', this.onClick);

    // Remove Hammer.manager.
    if (this._mc) {
      this._mc.destroy();
      delete this._mc;
    }

    // Remove controls.
    if (this._controls) {
      this._controls.destroy();
      delete this._controls;
    }

    // Remove nav.
    if (this._nav) {
      this._nav.destroy();
      delete this._nav;
    }

    // Remove pagination.
    if (this._pagination) {
      this._pagination.destroy();
      delete this._pagination;
    }

    // Remove HTML wrapper.
    this._outer.before(this._el);
    if (this._outer.parentNode) {
      this._outer.parentNode.removeChild(this._outer);
    }

    // Remove CSS classes.
    this._el.classList.remove(this.namespace);
    forEach(this._items, slide => {
      slide.classList.remove(`${this.namespace}__item`);
      slide.removeAttribute('style');
      forEach(slide.children, child => {
        child.removeAttribute('style');
      });
    });

    this._el.removeAttribute('style');
  }

  /**
   * Events.
   */

  /**
   * RWD reset.
   * Mainly for height…
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  resize() {
    if (!this._destroyed) {
      this.reset();
      this._dispatcher.emit('afterResize');
    }
  }

  /**
   * Click on slider to go to the next slide.
   * Enabled via "click" option.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  click() {
    this.slideNext();
  }

  /**
   * Same as click but for touch devices.
   * Enabled via "touch" option.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  tap() {
    this.slideNext();
  }

  /**
   * Complement gesture for "tap".
   * Enabled via "touch" option.
   *
   * @param {TouchEvent} e touch event
   * @returns {undefined}
   * @memberof Slidy
   */
  swipe(e) {
    if (e.direction === Hammer.DIRECTION_LEFT) {
      this.slideNext();
    }
    if (e.direction === Hammer.DIRECTION_RIGHT) {
      this.slidePrev();
    }
  }

  /**
   * Play/pause on hover.
   * Enabled via "auto" + "pause" options.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  enter() {
    this._outer.removeEventListener('mouseenter', this.onEnter);
    this.stop();
    this._outer.addEventListener('mouseleave', this.onLeave);
  }

  leave() {
    this._outer.removeEventListener('mouseleave', this.onLeave);
    this.start();
    this._outer.addEventListener('mouseenter', this.onEnter);
  }

  /**
   * Callbacks.
   */

  beforeInit() {
    if (this._opts.beforeInit) {
      this._opts.beforeInit.call(this, this._el);
    }
  }

  afterInit() {
    if (this._opts.afterInit) {
      this._opts.afterInit.call(this, this._el);
    }
  }

  afterResize() {
    if (this._opts.afterResize) {
      this._opts.afterResize.call(this, this._el);
    }
  }

  beforeSlide(direction) {
    if (this._opts.beforeSlide) {
      this._opts.beforeSlide.call(
        this,
        this.currentIndex,
        this.newIndex,
        direction
      );
    }
  }

  afterSlide(direction) {
    // Accessibility
    this._el.setAttribute('aria-valuenow', this.currentIndex + 1);

    if (this._opts.afterSlide) {
      this._opts.afterSlide.call(
        this,
        this.currentIndex,
        this.oldIndex,
        direction
      );
    }
  }
}
<commit_msg>Fix initial delay when using auto mode<commit_after>/**
 * Slidy main file.
 */

const Emitter = require('tiny-emitter');
const Hammer = require('hammerjs');

import bind from 'lodash/bind';
import debounce from 'lodash/debounce';
import forEach from 'lodash/forEach';
import * as detect from './utils/detect';
import { Controls } from './Controls';
import { Nav } from './Nav';
import { Pagination } from './Pagination';
import { Queue } from './Queue';

/**
 * Slidy main class.
 *
 * @export
 * @class Slidy
 */
export class Slidy {
  /**
   * Creates an instance of Slidy.
   * @param   {HTMLElement|string} el             Slider container (element or selector)
   * @param   {Object}             opts           Configuration settings
   * @param   {*}                  [context=null] Optional context, available through `this.context`
   * @param   {Object}             [data=null]    Additional optional data
   * @returns {undefined}
   */
  constructor(el, opts, context = null, data = null) {
    /* eslint-disable no-param-reassign */
    // Check and get element(s).
    if (typeof el === 'string') {
      el = document.querySelectorAll(el);
    }
    /* eslint-enable no-param-reassign */

    if (!el || el.length === 0) {
      console.error('Slidy: no element matching!');

      return;
    }

    if (el.length > 1) {
      console.warn('Slidy: multiple elements matching!');
    }

    this._el = el[0] || el;

    // Check and get options.
    this._opts = Object.assign({
      auto: false, // Boolean: start slider automaticaly
      click: true, // Boolean: enable click on slider
      controls: false, // Boolean: create prev/next buttons
      debounce: 100, // Integer: debounce delay on resize
      height: 'auto', // Mixed: integer (px) or 'auto'
      index: 0, // Integer: initial index
      interval: 2000, // Integer: time between 2 transitions
      loop: true, // Boolean: enable/disable loop
      namespace: 'slidy', // String: custom namespace
      nav: false, // Mixed: create navigation (number, thumb, custom)
      pagination: false, // Mixed: create pagination (1[separator]10)
      pause: true, // Boolean: pause on hover
      queue: 1, // Integer: queue max items
      resize: true, // Boolean: enable resize event and callback
      reverse: false, // Boolean: reverse directions / controls
      swipe: false, // Boolean: enable swipe
      tap: false, // Boolean: enable tap
      touch: false, // Boolean: enable BOTH tap/swipe (deprecated)
      transition: null, // Function: transition which returns a resolved promise
      zerofill: false, // Mixed(Boolean | number): '1' -> '01' for navigation/pagination
    }, opts);

    if (this._opts.transition === null) {
      console.error('Slidy: you should define a transition!');

      return;
    }

    if (this._opts.touch) {
      this._opts.swipe = true;
      this._opts.tap = true;
    }

    this._context = context;
    this._data = data;

    this._debounceDelay = this._opts.debounce;
    this._currentIndex = this._opts.index;
    this._newIndex = this._currentIndex;
    this._oldIndex = null;
    this._items = Array.prototype.map.call(this._el.children, item => item); // #TODO use Array.from ?
    this._length = this._items.length;
    this._hasPause = false;

    this._dispatcher = new Emitter();

    // Binding
    this.start = this.start.bind(this);
    this.slideNext = this.slideNext.bind(this);

    // Public events.
    this._dispatcher.on('beforeInit', () => {
      this.beforeInit();
    });
    this._dispatcher.on('afterInit', () => {
      this.afterInit();
    });
    if (this._opts.resize) {
      this._dispatcher.on('afterResize', () => {
        this.afterResize();
      });
    }
    this._dispatcher.on('beforeSlide', direction => {
      this.beforeSlide(direction);
    });
    this._dispatcher.on('afterSlide', direction => {
      this.afterSlide(direction);
    });

    this.init();
    this.bind();
  }

  /**
   * Getters/setters.
   */
  get outer() {
    return this._outer;
  }

  get el() {
    return this._el;
  }

  get items() {
    return this._items;
  }

  get context() {
    return this._context;
  }

  get data() {
    return this._data;
  }

  get currentIndex() {
    return this._currentIndex;
  }

  set currentIndex(i) {
    this._currentIndex = i;
  }

  get newIndex() {
    return this._newIndex;
  }

  set newIndex(i) {
    this._newIndex = i;
  }

  get oldIndex() {
    return this._oldIndex;
  }

  set oldIndex(i) {
    this._oldIndex = i;
  }

  get options() {
    return this._opts;
  }

  get dispatcher() {
    return this._dispatcher;
  }

  get namespace() {
    return this._opts.namespace;
  }

  /**
   * Init component.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  init() {
    this._dispatcher.emit('beforeInit');

    // Set height.
    // To get the most 'correct' auto-height,
    // do it before applying anything…
    if (this._opts.height === 'auto') {
      this.reset();
    } else {
      this._el.style.height = `${this._opts.height}px`;
    }

    // Add HTML wrapper.
    this._outer = document.createElement('div');
    this._el.before(this._outer);
    this._outer.append(this._el);

    // Add CSS classes.
    this._outer.classList.add(`${this.namespace}-outer`);
    this._el.classList.add(this.namespace);
    forEach(this._items, slide => {
      slide.classList.add(`${this.namespace}__item`);
    });

    // Set active class on currentIndex.
    this._items[this._currentIndex].classList.add('is-active');

    // Accessibility
    this._el.setAttribute('role', 'slider');
    this._el.setAttribute('aria-valuemin', 1);
    this._el.setAttribute('aria-valuemax', this._items.length);
    this._el.setAttribute('aria-valuenow', this._currentIndex + 1);

    if (this._opts.click) {
      this._el.style.cursor = 'pointer';
    }

    this._queue = new Queue(this, this._opts.transition);

    // Add controls.
    if (this._opts.controls) {
      this._controls = new Controls(this, {
        loop: this._opts.loop,
        controls: this._opts.controls,
      });
    }

    // Add nav.
    if (this._opts.nav) {
      this._nav = new Nav(this);
    }

    // Add pagination.
    if (this._opts.pagination) {
      this._pagination = new Pagination(this);
    }

    // Start auto mode.
    if (this._opts.auto) {
      this.start();
    }

    this._dispatcher.emit('afterInit');
  }

  /**
   * Bind event handlers.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  bind() {
    this.onEnter = this.enter.bind(this);
    this.onLeave = this.leave.bind(this);
    this.onClick = this.click.bind(this);
    this.onTap = this.tap.bind(this);
    this.onSwipe = this.swipe.bind(this);

    if (this._opts.resize) {
      this.onResize = bind(debounce(this.resize, this._debounceDelay), this);

      window.addEventListener('resize', this.onResize);
    }

    if (this._opts.pause && this._opts.auto) {
      this._outer.addEventListener('mouseenter', this.onEnter);
      this._hasPause = true;
    }

    if (this._opts.click && !detect.touchevents()) {
      this._el.addEventListener('click', this.onClick);
    }

    if ((this._opts.tap || this._opts.swipe) && detect.touchevents()) {
      const options = {
        recognizers: [],
      };

      this._mc = new Hammer.Manager(this._el, options);
    }

    if (this._opts.tap && detect.touchevents()) {
      const tap = new Hammer.Tap();

      this._mc.add(tap);
      this._mc.on('tap', this.onTap);
    }

    if (this._opts.swipe && detect.touchevents()) {
      const swipe = new Hammer.Swipe({ direction: Hammer.DIRECTION_HORIZONTAL });

      this._mc.add(swipe);
      this._mc.on('swipeleft swiperight', this.onSwipe);
    }
  }

  /**
   * API.
   */

  /**
   * Navigate to previous slide.
   *
   * @param {boolean} force force reverse
   * @returns {undefined}
   * @memberof Slidy
   */
  slidePrev(force = false) {
    if (this._opts.reverse && !force) {
      this.slideNext(true);

      return;
    }

    let newIndex = this._currentIndex - 1;

    if (newIndex < 0) {
      if (!this._opts.loop) {
        return;
      }
      newIndex = this._length - 1;
    }
    this.slide('prev');
  }

  /**
   * Navigate to next slide.
   *
   * @param {boolean} force force reverse
   * @returns {undefined}
   * @memberof Slidy
   */
  slideNext(force = false) {
    if (this._opts.reverse && !force) {
      this.slidePrev(true);

      return;
    }

    let newIndex = this._currentIndex + 1;

    if (newIndex === this._length) {
      if (!this._opts.loop) {
        return;
      }
      newIndex = 0;
    }
    this.slide('next');
  }

  /**
   * Navigate to slide by index.
   *
   * @param {number} index slide index
   * @returns {undefined}
   * @memberof Slidy
   */
  slideTo(index) {
    this.slide('to', index);
  }

  /**
   * Add move to the queue.
   *
   * @param {string} move prev|next|to
   * @param {number} [index=null] slide index
   * @returns {undefined}
   * @memberof Slidy
   */
  slide(move, index = null) {
    if (this._opts.auto) {
      clearInterval(this._t1);
      this._t1 = setInterval(this.slideNext, this._opts.interval);
    }

    if (this._queue) {
      this._queue.add(move, index);
    } else {
      // Prevent 'persistent' auto
      this.stop();
    }
  }

  /**
   * Height calculation if auto.
   * Called on 'init' and 'resize'.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  reset() {
    if (this._opts.height === 'auto') {
      // Reset inline height.
      this._el.style.height = '';

      // Check if items have height
      // if not, check first node
      // then remove 0 height, sort DESC, get the highest height.
      const getMinHeight = function getMinHeight(arr) {
        return arr
          .filter(item => item > 0)
          .sort((a, b) => b - a)
          .slice(0, 1);
      };

      const heights = [];
      const hasNoHeight = this._items[0].offsetHeight === 0;

      forEach(this._items, item => {
        if (hasNoHeight && item.hasChildNodes()) {
          heights.push(item.firstElementChild.offsetHeight);
        } else {
          heights.push(item.offsetHeight);
        }
      });
      this._el.style.height = `${getMinHeight(heights)}px`;
    }
  }

  /**
   * Start autoplay.
   * Enabled via "auto" and used by "pause" options.
   *
   * @param {number} delay delay before slideNext
   * @param {boolean} auto autoloop or not
   * @returns {undefined}
   * @memberof Slidy
   */
  start(delay = this._opts.interval, auto = this._opts.auto) {
    this._t2 = setTimeout(() => {
      this.slideNext();
      if (!this._hasPause && this._opts.pause) {
        this._outer.addEventListener('mouseenter', this.onEnter);
      }
      if (auto) {
        clearInterval(this._t1);
        this._t1 = setInterval(this.slideNext, this._opts.interval);
      }
    }, delay);
  }

  /**
   * Pause autoplay.
   * Used by "pause" options.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  stop() {
    if (this._hasPause) {
      this._outer.removeEventListener('mouseenter', this.onEnter);
    }
    clearTimeout(this._t2);
    clearInterval(this._t1);
  }

  /**
   * Destroy component.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  destroy() {
    this._destroyed = true;

    // Remove interval.
    this.stop();

    // Empty queue.
    if (this._queue) {
      this._queue.empty();
      delete this._queue;
    }

    // Remove listeners.
    if (this._opts.resize) {
      window.removeEventListener('resize', this.onResize);
    }
    this._el.removeEventListener('mouseenter', this.onEnter);
    this._el.removeEventListener('mouseleave', this.onLeave);
    this._el.removeEventListener('click', this.onClick);

    // Remove Hammer.manager.
    if (this._mc) {
      this._mc.destroy();
      delete this._mc;
    }

    // Remove controls.
    if (this._controls) {
      this._controls.destroy();
      delete this._controls;
    }

    // Remove nav.
    if (this._nav) {
      this._nav.destroy();
      delete this._nav;
    }

    // Remove pagination.
    if (this._pagination) {
      this._pagination.destroy();
      delete this._pagination;
    }

    // Remove HTML wrapper.
    this._outer.before(this._el);
    if (this._outer.parentNode) {
      this._outer.parentNode.removeChild(this._outer);
    }

    // Remove CSS classes.
    this._el.classList.remove(this.namespace);
    forEach(this._items, slide => {
      slide.classList.remove(`${this.namespace}__item`);
      slide.removeAttribute('style');
      forEach(slide.children, child => {
        child.removeAttribute('style');
      });
    });

    this._el.removeAttribute('style');
  }

  /**
   * Events.
   */

  /**
   * RWD reset.
   * Mainly for height…
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  resize() {
    if (!this._destroyed) {
      this.reset();
      this._dispatcher.emit('afterResize');
    }
  }

  /**
   * Click on slider to go to the next slide.
   * Enabled via "click" option.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  click() {
    this.slideNext();
  }

  /**
   * Same as click but for touch devices.
   * Enabled via "touch" option.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  tap() {
    this.slideNext();
  }

  /**
   * Complement gesture for "tap".
   * Enabled via "touch" option.
   *
   * @param {TouchEvent} e touch event
   * @returns {undefined}
   * @memberof Slidy
   */
  swipe(e) {
    if (e.direction === Hammer.DIRECTION_LEFT) {
      this.slideNext();
    }
    if (e.direction === Hammer.DIRECTION_RIGHT) {
      this.slidePrev();
    }
  }

  /**
   * Play/pause on hover.
   * Enabled via "auto" + "pause" options.
   *
   * @returns {undefined}
   * @memberof Slidy
   */
  enter() {
    this._outer.removeEventListener('mouseenter', this.onEnter);
    this.stop();
    this._outer.addEventListener('mouseleave', this.onLeave);
  }

  leave() {
    this._outer.removeEventListener('mouseleave', this.onLeave);
    this.start();
    this._outer.addEventListener('mouseenter', this.onEnter);
  }

  /**
   * Callbacks.
   */

  beforeInit() {
    if (this._opts.beforeInit) {
      this._opts.beforeInit.call(this, this._el);
    }
  }

  afterInit() {
    if (this._opts.afterInit) {
      this._opts.afterInit.call(this, this._el);
    }
  }

  afterResize() {
    if (this._opts.afterResize) {
      this._opts.afterResize.call(this, this._el);
    }
  }

  beforeSlide(direction) {
    if (this._opts.beforeSlide) {
      this._opts.beforeSlide.call(
        this,
        this.currentIndex,
        this.newIndex,
        direction
      );
    }
  }

  afterSlide(direction) {
    // Accessibility
    this._el.setAttribute('aria-valuenow', this.currentIndex + 1);

    if (this._opts.afterSlide) {
      this._opts.afterSlide.call(
        this,
        this.currentIndex,
        this.oldIndex,
        direction
      );
    }
  }
}
<|endoftext|>


<commit_before>/**
 * @author mrdoob / http://mrdoob.com/
 */

var Stats = function () {

	var mode = 0;

	var container = document.createElement( 'div' );
	container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000';
	container.addEventListener( 'click', function ( event ) {

		event.preventDefault();
		showPanel( ++ mode % container.children.length );

	}, false );

	//

	function addPanel( panel ) {

		container.appendChild( panel.dom );
		return panel;

	}

	function showPanel( id ) {

		for ( var i = 0; i < container.children.length; i ++ ) {

			container.children[ i ].style.display = i === id ? 'block' : 'none';

		}

		mode = id;

	}

	//

	var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0;

	var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) );
	var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) );

	if ( self.performance && self.performance.memory ) {

		var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) );

	}

	showPanel( 0 );

	return {

		REVISION: 16,

		dom: container,

		addPanel: addPanel,
		showPanel: showPanel,

		begin: function () {

			beginTime = ( performance || Date ).now();

		},

		end: function () {

			frames ++;

			var time = ( performance || Date ).now();

			msPanel.update( time - beginTime, 200 );

			if ( time > prevTime + 1000 ) {

				fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 );

				prevTime = time;
				frames = 0;

				if ( memPanel ) {

					var memory = performance.memory;
					memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 );

				}

			}

			return time;

		},

		update: function () {

			beginTime = this.end();

		},

		// Backwards Compatibility

		domElement: container,
		setMode: showPanel

	};

};

Stats.Panel = function ( name, fg, bg ) {

	var min = Infinity, max = 0, round = Math.round;
	var PR = round( window.devicePixelRatio || 1 );

	var WIDTH = 80 * PR, HEIGHT = 48 * PR,
			TEXT_X = 3 * PR, TEXT_Y = 2 * PR,
			GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR,
			GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR;

	var canvas = document.createElement( 'canvas' );
	canvas.width = WIDTH;
	canvas.height = HEIGHT;
	canvas.style.cssText = 'width:80px;height:48px';

	var context = canvas.getContext( '2d' );
	context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif';
	context.textBaseline = 'top';

	context.fillStyle = bg;
	context.fillRect( 0, 0, WIDTH, HEIGHT );

	context.fillStyle = fg;
	context.fillText( name, TEXT_X, TEXT_Y );
	context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );

	context.fillStyle = bg;
	context.globalAlpha = 0.9;
	context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );

	return {

		dom: canvas,

		update: function ( value, maxValue ) {

			min = Math.min( min, value );
			max = Math.max( max, value );

			context.fillStyle = bg;
			context.globalAlpha = 1;
			context.fillRect( 0, 0, WIDTH, GRAPH_Y );
			context.fillStyle = fg;
			context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y );

			context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT );

			context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT );

			context.fillStyle = bg;
			context.globalAlpha = 0.9;
			context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) );

		}

	};

};

export { Stats as default };
<commit_msg>1 second is inclusive in fps (#79)<commit_after>/**
 * @author mrdoob / http://mrdoob.com/
 */

var Stats = function () {

	var mode = 0;

	var container = document.createElement( 'div' );
	container.style.cssText = 'position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000';
	container.addEventListener( 'click', function ( event ) {

		event.preventDefault();
		showPanel( ++ mode % container.children.length );

	}, false );

	//

	function addPanel( panel ) {

		container.appendChild( panel.dom );
		return panel;

	}

	function showPanel( id ) {

		for ( var i = 0; i < container.children.length; i ++ ) {

			container.children[ i ].style.display = i === id ? 'block' : 'none';

		}

		mode = id;

	}

	//

	var beginTime = ( performance || Date ).now(), prevTime = beginTime, frames = 0;

	var fpsPanel = addPanel( new Stats.Panel( 'FPS', '#0ff', '#002' ) );
	var msPanel = addPanel( new Stats.Panel( 'MS', '#0f0', '#020' ) );

	if ( self.performance && self.performance.memory ) {

		var memPanel = addPanel( new Stats.Panel( 'MB', '#f08', '#201' ) );

	}

	showPanel( 0 );

	return {

		REVISION: 16,

		dom: container,

		addPanel: addPanel,
		showPanel: showPanel,

		begin: function () {

			beginTime = ( performance || Date ).now();

		},

		end: function () {

			frames ++;

			var time = ( performance || Date ).now();

			msPanel.update( time - beginTime, 200 );

			if ( time >= prevTime + 1000 ) {

				fpsPanel.update( ( frames * 1000 ) / ( time - prevTime ), 100 );

				prevTime = time;
				frames = 0;

				if ( memPanel ) {

					var memory = performance.memory;
					memPanel.update( memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576 );

				}

			}

			return time;

		},

		update: function () {

			beginTime = this.end();

		},

		// Backwards Compatibility

		domElement: container,
		setMode: showPanel

	};

};

Stats.Panel = function ( name, fg, bg ) {

	var min = Infinity, max = 0, round = Math.round;
	var PR = round( window.devicePixelRatio || 1 );

	var WIDTH = 80 * PR, HEIGHT = 48 * PR,
			TEXT_X = 3 * PR, TEXT_Y = 2 * PR,
			GRAPH_X = 3 * PR, GRAPH_Y = 15 * PR,
			GRAPH_WIDTH = 74 * PR, GRAPH_HEIGHT = 30 * PR;

	var canvas = document.createElement( 'canvas' );
	canvas.width = WIDTH;
	canvas.height = HEIGHT;
	canvas.style.cssText = 'width:80px;height:48px';

	var context = canvas.getContext( '2d' );
	context.font = 'bold ' + ( 9 * PR ) + 'px Helvetica,Arial,sans-serif';
	context.textBaseline = 'top';

	context.fillStyle = bg;
	context.fillRect( 0, 0, WIDTH, HEIGHT );

	context.fillStyle = fg;
	context.fillText( name, TEXT_X, TEXT_Y );
	context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );

	context.fillStyle = bg;
	context.globalAlpha = 0.9;
	context.fillRect( GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT );

	return {

		dom: canvas,

		update: function ( value, maxValue ) {

			min = Math.min( min, value );
			max = Math.max( max, value );

			context.fillStyle = bg;
			context.globalAlpha = 1;
			context.fillRect( 0, 0, WIDTH, GRAPH_Y );
			context.fillStyle = fg;
			context.fillText( round( value ) + ' ' + name + ' (' + round( min ) + '-' + round( max ) + ')', TEXT_X, TEXT_Y );

			context.drawImage( canvas, GRAPH_X + PR, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT, GRAPH_X, GRAPH_Y, GRAPH_WIDTH - PR, GRAPH_HEIGHT );

			context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT );

			context.fillStyle = bg;
			context.globalAlpha = 0.9;
			context.fillRect( GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, round( ( 1 - ( value / maxValue ) ) * GRAPH_HEIGHT ) );

		}

	};

};

export { Stats as default };
<|endoftext|>