Swipe.js 버그
휴~ 모바일 쇼핑몰 메인 화면에 swipe 기능을 넣기 위해서 Swipe.js 라는 플러그인을 사용했는데 이게 슬라이드가 2개 일 때 오동작을 하는 것을 알게 됐다. 항상 3개 이상의 슬라이드 있었기 때문에 인식하지 못했는데 그래서 오후에 이걸 수정해보고자 노력을 했지만 해결하지 못하고 퇴근을 했는데 혹시나 싶어 조금 전에 구글에서 검색을 해보니 플러그인 자체에 문제가 있었다. 약 한달 전에 이 부분이 수정됐다는 커밋도 확인을 했다. 그래서 바로 적용을 해봤는데 그래도 역시 약간의 문제가 있다. 2개 일때는 예외처리를 추가해서 플러그인 코드를 수정해야만 할 듯 하다.
플러그인 자체를 수정하는 것은 그다지 내키지 않지만 일단 문제가 있기 때문에 수정하는 게 맞을 것 같다. 이건 내일 출근해서 하는 걸로 하고 오늘은 그만 쉬어야겠다. ㅎㅎ
[2014-04-29 17:30 추가] 플러그인 자체의 코드 수정은 안하려고 했는데 swipe.js 만한 플러그인도 없고 해서.. 다른 플러그인을 적용하기 위해 오랜 시간 삽질을 해봤지만 터치감도나 전환이 자연스러운 것은 swipe.js가 제일 나은 것 같아서 결국은 아래 처럼 코드를 수정해서 사용하기로 했다.
/*
 * Swipe 2.0
 *
 * Brad Birdsall
 * Copyright 2013, MIT License
 * https://github.com/rmoszczynski/Swipe
 *
*/
function Swipe(container, options) {
  "use strict";
  // utilities
  var noop = function() {}; // simple no operation function
  var offloadFn = function(fn) { setTimeout(fn || noop, 0) }; // offload a functions execution
  // check browser capabilities
  var browser = {
    addEventListener: !!window.addEventListener,
    touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch,
    transitions: (function(temp) {
      var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition'];
      for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true;
      return false;
    })(document.createElement('swipe'))
  };
  // quit if no root element
  if (!container) return;
  var element = container.children[0];
  var slides, slidePos, width, length;
  options = options || {};
  var index = parseInt(options.startSlide, 10) || 0;
  var speed = options.speed || 300;
  options.continuous = options.continuous !== undefined ? options.continuous : true;
  function setup() {
    // cache slides
    slides = element.children;
    length = slides.length;
    // set continuous to false if only one slide
    if (slides.length <= 2) options.continuous = false;
    // create an array to store current positions of each slide
    slidePos = new Array(slides.length);
    // determine width of each slide
    width = container.getBoundingClientRect().width || container.offsetWidth;
    element.style.width = (slides.length * width) + 'px';
    // stack elements
    var pos = slides.length;
    while(pos--) {
      var slide = slides[pos];
      slide.style.width = width + 'px';
      slide.setAttribute('data-index', pos);
      if (browser.transitions) {
        slide.style.left = (pos * -width) + 'px';
        move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
      }
    }
    // reposition elements before and after index
    if (options.continuous && browser.transitions) {
      move(circle(index-1), -width, 0);
      move(circle(index+1), width, 0);
    }
    if (!browser.transitions) element.style.left = (index * -width) + 'px';
    container.style.visibility = 'visible';
  }
  function prev() {
    if (options.continuous) slide(index-1);
    else if (index) slide(index-1);
  }
  function next() {
    if (options.continuous) slide(index+1);
    else if (index < slides.length - 1) slide(index+1);
  }
  function circle(index) {
    // a simple positive modulo using slides.length
    return (slides.length + (index % slides.length)) % slides.length;
  }
  function slide(to, slideSpeed) {
    // do nothing if already on requested slide
    if (index == to) return;
    if (browser.transitions) {
      var direction = Math.abs(index-to) / (index-to); // 1: backward, -1: forward
      // get the actual position of the slide
      if (options.continuous) {
        var natural_direction = direction;
        direction = -slidePos[circle(to)] / width;
        // if going forward but to < index, use to = slides.length + to
        // if going backward but to > index, use to = -slides.length + to
        if (direction !== natural_direction) to =  -direction * slides.length + to;
      }
      var diff = Math.abs(index-to) - 1;
      // move all the slides between index and to in the right direction
      while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0);
      to = circle(to);
      move(index, width * direction, slideSpeed || speed);
      move(to, 0, slideSpeed || speed);
      if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place
    } else {
      to = circle(to);
      animate(index * -width, to * -width, slideSpeed || speed);
      //no fallback for a circular continuous if the browser does not accept transitions
    }
    index = to;
    offloadFn(options.callback && options.callback(index, slides[index]));
  }
  function move(index, dist, speed) {
    translate(index, dist, speed);
    slidePos[index] = dist;
  }
  function translate(index, dist, speed) {
    var slide = slides[index];
    var style = slide && slide.style;
    if (!style) return;
    style.webkitTransitionDuration =
    style.MozTransitionDuration =
    style.msTransitionDuration =
    style.OTransitionDuration =
    style.transitionDuration = speed + 'ms';
    style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
    style.msTransform =
    style.MozTransform =
    style.OTransform = 'translateX(' + dist + 'px)';
  }
  function animate(from, to, speed) {
    // if not an animation, just reposition
    if (!speed) {
      element.style.left = to + 'px';
      return;
    }
    var start = +new Date;
    var timer = setInterval(function() {
      var timeElap = +new Date - start;
      if (timeElap > speed) {
        element.style.left = to + 'px';
        if (delay) begin();
        options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
        clearInterval(timer);
        return;
      }
      element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px';
    }, 4);
  }
  // setup auto slideshow
  var delay = options.auto || 0;
  var interval;
  function begin() {
    interval = setTimeout(next, delay);
  }
  function stop() {
    delay = 0;
    clearTimeout(interval);
  }
  // setup initial vars
  var start = {};
  var delta = {};
  var isScrolling;
  // setup event capturing
  var events = {
    handleEvent: function(event) {
      switch (event.type) {
        case 'touchstart': this.start(event); break;
        case 'touchmove': this.move(event); break;
        case 'touchend': offloadFn(this.end(event)); break;
        case 'webkitTransitionEnd':
        case 'msTransitionEnd':
        case 'oTransitionEnd':
        case 'otransitionend':
        case 'transitionend': offloadFn(this.transitionEnd(event)); break;
        case 'resize': offloadFn(setup); break;
      }
      if (options.stopPropagation) event.stopPropagation();
    },
    start: function(event) {
      var touches = event.touches[0];
      // measure start values
      start = {
        // get initial touch coords
        x: touches.pageX,
        y: touches.pageY,
        // store time to determine touch duration
        time: +new Date
      };
      // used for testing first move event
      isScrolling = undefined;
      // reset delta and end measurements
      delta = {};
      // attach touchmove and touchend listeners
      element.addEventListener('touchmove', this, false);
      element.addEventListener('touchend', this, false);
    },
    move: function(event) {
      // ensure swiping with one touch and not pinching
      if ( event.touches.length > 1 || event.scale && event.scale !== 1) return
      if (options.disableScroll) event.preventDefault();
      var touches = event.touches[0];
      // measure change in x and y
      delta = {
        x: touches.pageX - start.x,
        y: touches.pageY - start.y
      }
      // determine if scrolling test has run - one time test
      if ( typeof isScrolling == 'undefined') {
        isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
      }
      // if user is not trying to scroll vertically
      if (!isScrolling) {
        // prevent native scrolling
        event.preventDefault();
        // stop slideshow
        stop();
        // increase resistance if first or last slide
        if (options.continuous) { // we don't add resistance at the end
          translate(circle(index-1), delta.x + slidePos[circle(index-1)], 0);
          translate(index, delta.x + slidePos[index], 0);
          translate(circle(index+1), delta.x + slidePos[circle(index+1)], 0);
        } else {
          delta.x =
            delta.x /
              ( (!index && delta.x > 0               // if first slide and sliding left
                || index == slides.length - 1        // or if last slide and sliding right
                && delta.x < 0                       // and if sliding at all
              ) ?
              ( Math.abs(delta.x) / width + 1 )      // determine resistance level
              : 1 );                                 // no resistance if false
          // translate 1:1
          translate(index-1, delta.x + slidePos[index-1], 0);
          translate(index, delta.x + slidePos[index], 0);
          translate(index+1, delta.x + slidePos[index+1], 0);
        }
      }
    },
    end: function(event) {
      // measure duration
      var duration = +new Date - start.time;
      // determine if slide attempt triggers next/prev slide
      var isValidSlide =
            Number(duration) < 250               // if slide duration is less than 250ms
            && Math.abs(delta.x) > 20            // and if slide amt is greater than 20px
            || Math.abs(delta.x) > width/2;      // or if slide amt is greater than half the width
      // determine if slide attempt is past start and end
      var isPastBounds =
            !index && delta.x > 0                            // if first slide and slide amt is greater than 0
            || index == slides.length - 1 && delta.x < 0;    // or if last slide and slide amt is less than 0
      if (options.continuous) isPastBounds = false;
      // determine direction of swipe (true:right, false:left)
      var direction = delta.x < 0;
      // if not scrolling vertically
      if (!isScrolling) {
        if (isValidSlide && !isPastBounds) {
          if (direction) {
            if (options.continuous) { // we need to get the next in this direction in place
              move(circle(index-1), -width, 0);
              move(circle(index+2), width, 0);
            } else {
              move(index-1, -width, 0);
            }
            move(index, slidePos[index]-width, speed);
            move(circle(index+1), slidePos[circle(index+1)]-width, speed);
            index = circle(index+1);
          } else {
            if (options.continuous) { // we need to get the next in this direction in place
              move(circle(index+1), width, 0);
              move(circle(index-2), -width, 0);
            } else {
              move(index+1, width, 0);
            }
            move(index, slidePos[index]+width, speed);
            move(circle(index-1), slidePos[circle(index-1)]+width, speed);
            index = circle(index-1);
          }
          options.callback && options.callback(index, slides[index]);
        } else {
          if (options.continuous) {
            move(circle(index-1), -width, speed);
            move(index, 0, speed);
            move(circle(index+1), width, speed);
          } else {
            move(index-1, -width, speed);
            move(index, 0, speed);
            move(index+1, width, speed);
          }
        }
      }
      // kill touchmove and touchend event listeners until touchstart called again
      element.removeEventListener('touchmove', events, false)
      element.removeEventListener('touchend', events, false)
    },
    transitionEnd: function(event) {
      if (parseInt(event.target.getAttribute('data-index'), 10) == index) {
        if (delay) begin();
        options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
      }
    }
  }
  // trigger setup
  setup();
  // start auto slideshow if applicable
  if (delay) begin();
  // add event listeners
  if (browser.addEventListener) {
    // set touchstart event on element
    if (browser.touch) element.addEventListener('touchstart', events, false);
    if (browser.transitions) {
      element.addEventListener('webkitTransitionEnd', events, false);
      element.addEventListener('msTransitionEnd', events, false);
      element.addEventListener('oTransitionEnd', events, false);
      element.addEventListener('otransitionend', events, false);
      element.addEventListener('transitionend', events, false);
    }
    // set resize event on window
    window.addEventListener('resize', events, false);
  } else {
    window.onresize = function () { setup() }; // to play nice with old IE
  }
  // expose the Swipe API
  return {
    setup: function() {
      setup();
    },
    slide: function(to, speed) {
      // cancel slideshow
      stop();
      slide(to, speed);
    },
    prev: function() {
      // cancel slideshow
      stop();
      prev();
    },
    next: function() {
      // cancel slideshow
      stop();
      next();
    },
    stop: function() {
      // cancel slideshow
      stop();
    },
    getPos: function() {
      // return current index position
      return index;
    },
    getNumSlides: function() {
      // return total number of slides
      return length;
    },
    kill: function() {
      // cancel slideshow
      stop();
      // reset element
      element.style.width = '';
      element.style.left = '';
      // reset slides
      var pos = slides.length;
      while(pos--) {
        var slide = slides[pos];
        slide.style.width = '';
        slide.style.left = '';
        if (browser.transitions) translate(pos, 0, 0);
      }
      // removed event listeners
      if (browser.addEventListener) {
        // remove current event listeners
        element.removeEventListener('touchstart', events, false);
        element.removeEventListener('webkitTransitionEnd', events, false);
        element.removeEventListener('msTransitionEnd', events, false);
        element.removeEventListener('oTransitionEnd', events, false);
        element.removeEventListener('otransitionend', events, false);
        element.removeEventListener('transitionend', events, false);
        window.removeEventListener('resize', events, false);
      }
      else {
        window.onresize = null;
      }
    }
  }
}
if ( window.jQuery || window.Zepto ) {
  (function($) {
    $.fn.Swipe = function(params) {
      return this.each(function() {
        $(this).data('Swipe', new Swipe($(this)[0], params));
      });
    }
  })( window.jQuery || window.Zepto )
}슬라이드가 2개 일 때 continuous 설정이 false 가 되도록 코드를 수정했다.
45라인에 <= 말고 < 로 해야 반복적으로 보여지는거 같던데요??
글쎄요.. 사용하는 환경이 다를 수 있으니까 코드도 달라져야 하는 거 아닐까요?
덕분에 간단하게 버그 대응했습니다
감사합니다^^
부족한 제 글이 도움이 됐다니 다행입니다. 좋은 하루 되세요.