 /**
 * jQuery jPages v0.7
 * Client side pagination with jQuery
 * http://luis-almeida.github.com/jPages
 *
 * Licensed under the MIT license.
 * Copyright 2012 Luís Almeida
 * https://github.com/luis-almeida
 */

;(function($, window, document, undefined) {

  var name = "jPages",
      instance = null,
      defaults = {
        containerID: "",
        first: false,
        previous: "← previous",
        next: "next →",
        last: false,
        links: "numeric", // blank || title
        startPage: 1,
        perPage: 10,
        midRange: 5,
        startRange: 1,
        endRange: 1,
        keyBrowse: false,
        scrollBrowse: false,
        pause: 0,
        clickStop: false,
        delay: 50,
        direction: "forward", // backwards || auto || random ||
        animation: "", // http://daneden.me/animate/ - any entrance animations
        fallback: 400,
        minHeight: true,
        callback: undefined // function( pages, items ) { }
      };


  function Plugin(element, options) {
    this.options = $.extend({}, defaults, options);

    this._container = $("#" + this.options.containerID);
    if (!this._container.length) return;

    this.jQwindow = $(window);
    this.jQdocument = $(document);

    this._holder = $(element);
    this._nav = {};

    this._first = $(this.options.first);
    this._previous = $(this.options.previous);
    this._next = $(this.options.next);
    this._last = $(this.options.last);

    /* only visible items! */
    this._items = this._container.children(":visible");
    this._itemsShowing = $([]);
    this._itemsHiding = $([]);

    this._numPages = Math.ceil(this._items.length / this.options.perPage);
    this._currentPageNum = this.options.startPage;

    this._clicked = false;
    this._cssAnimSupport = this.getCSSAnimationSupport();

    this.init();
  }

  Plugin.prototype = {

    constructor : Plugin,

    getCSSAnimationSupport : function() {
      var animation = false,
          animationstring = 'animation',
          keyframeprefix = '',
          domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
          pfx = '',
          elm = this._container.get(0);

      if (elm.style.animationName) animation = true;

      if (animation === false) {
        for (var i = 0; i < domPrefixes.length; i++) {
          if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) {
            pfx = domPrefixes[i];
            animationstring = pfx + 'Animation';
            keyframeprefix = '-' + pfx.toLowerCase() + '-';
            animation = true;
            break;
          }
        }
      }

      return animation;
    },

    init : function() {
      this.setStyles();
      this.setNav();
      this.paginate(this._currentPageNum);
      this.setMinHeight();
    },

    setStyles : function() {
      var requiredStyles = "<style>" +
      ".jp-invisible { visibility: hidden !important; } " +
      ".jp-hidden { display: none !important; }" +
      "</style>";

      $(requiredStyles).appendTo("head");

      if (this._cssAnimSupport && this.options.animation.length)
        this._items.addClass("animated jp-hidden");
      else this._items.hide();

    },

    setNav : function() {
      var navhtml = this.writeNav();

      this._holder.each(this.bind(function(index, element) {
        var holder = $(element);
        holder.html(navhtml);
        this.cacheNavElements(holder, index);
        this.bindNavHandlers(index);
        this.disableNavSelection(element);
      }, this));

      if (this.options.keyBrowse) this.bindNavKeyBrowse();
      if (this.options.scrollBrowse) this.bindNavScrollBrowse();
    },

    writeNav : function() {
      var i = 1, navhtml;
      navhtml = this.writeBtn("first") + this.writeBtn("previous");

      for (; i <= this._numPages; i++) {
        if (i === 1 && this.options.startRange === 0) navhtml += "<span>...</span>";
        if (i > this.options.startRange && i <= this._numPages - this.options.endRange)
          navhtml += "<a href='#' class='jp-hidden'>";
        else
          navhtml += "<a>";

        switch (this.options.links) {
          case "numeric":
            navhtml += i;
            break;
          case "blank":
            break;
          case "title":
            var title = this._items.eq(i - 1).attr("data-title");
            navhtml += title !== undefined ? title : "";
            break;
        }

        navhtml += "</a>";
        if (i === this.options.startRange || i === this._numPages - this.options.endRange)
          navhtml += "<span>...</span>";
      }
      navhtml += this.writeBtn("next") + this.writeBtn("last") + "</div>";
      return navhtml;
    },

    writeBtn : function(which) {

      return this.options[which] !== false && !$(this["_" + which]).length ?
      "<a class='jp-" + which + "'>" + this.options[which] + "</a>" : "";

    },

    cacheNavElements : function(holder, index) {
      this._nav[index] = {};
      this._nav[index].holder = holder;
      this._nav[index].first = this._first.length ? this._first : this._nav[index].holder.find("a.jp-first");
      this._nav[index].previous = this._previous.length ? this._previous : this._nav[index].holder.find("a.jp-previous");
      this._nav[index].next = this._next.length ? this._next : this._nav[index].holder.find("a.jp-next");
      this._nav[index].last = this._last.length ? this._last : this._nav[index].holder.find("a.jp-last");
      this._nav[index].fstBreak = this._nav[index].holder.find("span:first");
      this._nav[index].lstBreak = this._nav[index].holder.find("span:last");
      this._nav[index].pages = this._nav[index].holder.find("a").not(".jp-first, .jp-previous, .jp-next, .jp-last");
      this._nav[index].permPages =
        this._nav[index].pages.slice(0, this.options.startRange)
          .add(this._nav[index].pages.slice(this._numPages - this.options.endRange, this._numPages));
      this._nav[index].pagesShowing = $([]);
      this._nav[index].currentPage = $([]);
    },

    bindNavHandlers : function(index) {
      var nav = this._nav[index];

      // default nav
      nav.holder.bind("click.jPages", this.bind(function(evt) {
        var newPage = this.getNewPage(nav, $(evt.target));
        if (this.validNewPage(newPage)) {
          this._clicked = true;
          this.paginate(newPage);
        }
        evt.preventDefault();
      }, this));

      // custom first
      if (this._first.length) {
        this._first.bind("click.jPages", this.bind(function() {
          if (this.validNewPage(1)) {
            this._clicked = true;
            this.paginate(1);
          }
        }, this));
      }

      // custom previous
      if (this._previous.length) {
        this._previous.bind("click.jPages", this.bind(function() {
          var newPage = this._currentPageNum - 1;
          if (this.validNewPage(newPage)) {
            this._clicked = true;
            this.paginate(newPage);
          }
        }, this));
      }

      // custom next
      if (this._next.length) {
        this._next.bind("click.jPages", this.bind(function() {
          var newPage = this._currentPageNum + 1;
          if (this.validNewPage(newPage)) {
            this._clicked = true;
            this.paginate(newPage);
          }
        }, this));
      }

      // custom last
      if (this._last.length) {
        this._last.bind("click.jPages", this.bind(function() {
          if (this.validNewPage(this._numPages)) {
            this._clicked = true;
            this.paginate(this._numPages);
          }
        }, this));
      }

    },

    disableNavSelection : function(element) {
      if (typeof element.onselectstart != "undefined")
        element.onselectstart = function() {
          return false;
        };
      else if (typeof element.style.MozUserSelect != "undefined")
        element.style.MozUserSelect = "none";
      else
        element.onmousedown = function() {
          return false;
        };
    },

    bindNavKeyBrowse : function() {
      this.jQdocument.bind("keydown.jPages", this.bind(function(evt) {
        var target = evt.target.nodeName.toLowerCase();
        if (this.elemScrolledIntoView() && target !== "input" && target != "textarea") {
          var newPage = this._currentPageNum;

          if (evt.which == 37) newPage = this._currentPageNum - 1;
          if (evt.which == 39) newPage = this._currentPageNum + 1;

          if (this.validNewPage(newPage)) {
            this._clicked = true;
            this.paginate(newPage);
          }
        }
      }, this));
    },

    elemScrolledIntoView : function() {
      var docViewTop, docViewBottom, elemTop, elemBottom;
      docViewTop = this.jQwindow.scrollTop();
      docViewBottom = docViewTop + this.jQwindow.height();
      elemTop = this._container.offset().top;
      elemBottom = elemTop + this._container.height();
      return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom));

      // comment above and uncomment below if you want keyBrowse to happen
      // only when container is completely visible in the page
      /*return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom) &&
                (elemBottom <= docViewBottom) &&  (elemTop >= docViewTop) );*/
    },

    bindNavScrollBrowse : function() {
      this._container.bind("mousewheel.jPages DOMMouseScroll.jPages", this.bind(function(evt) {
        var newPage = (evt.originalEvent.wheelDelta || -evt.originalEvent.detail) > 0 ?
        (this._currentPageNum - 1) : (this._currentPageNum + 1);
        if (this.validNewPage(newPage)) {
          this._clicked = true;
          this.paginate(newPage);
        }
        evt.preventDefault();
        return false;
      }, this));
    },

    getNewPage : function(nav, target) {
      if (target.is(nav.currentPage)) return this._currentPageNum;
      if (target.is(nav.pages)) return nav.pages.index(target) + 1;
      if (target.is(nav.first)) return 1;
      if (target.is(nav.last)) return this._numPages;
      if (target.is(nav.previous)) return nav.pages.index(nav.currentPage);
      if (target.is(nav.next)) return nav.pages.index(nav.currentPage) + 2;
    },

    validNewPage : function(newPage) {
      return newPage !== this._currentPageNum && newPage > 0 && newPage <= this._numPages;
    },

    paginate : function(page) {
      var itemRange, pageInterval;
      itemRange = this.updateItems(page);
      pageInterval = this.updatePages(page);
      this._currentPageNum = page;
      if ($.isFunction(this.options.callback))
        this.callback(page, itemRange, pageInterval);

      this.updatePause();
    },

    updateItems : function(page) {
      var range = this.getItemRange(page);
      this._itemsHiding = this._itemsShowing;
      this._itemsShowing = this._items.slice(range.start, range.end);
      if (this._cssAnimSupport && this.options.animation.length) this.cssAnimations(page);
      else this.jQAnimations(page);
      return range;
    },

    getItemRange : function(page) {
      var range = {};
      range.start = (page - 1) * this.options.perPage;
      range.end = range.start + this.options.perPage;
      if (range.end > this._items.length) range.end = this._items.length;
      return range;
    },

    cssAnimations : function(page) {
      clearInterval(this._delay);

      this._itemsHiding
        .removeClass(this.options.animation + " jp-invisible")
        .addClass("jp-hidden");

      this._itemsShowing
        .removeClass("jp-hidden")
        .addClass("jp-invisible");

      this._itemsOriented = this.getDirectedItems(page);
      this._index = 0;

      this._delay = setInterval(this.bind(function() {
        if (this._index === this._itemsOriented.length) clearInterval(this._delay);
        else {
          this._itemsOriented
          .eq(this._index)
          .removeClass("jp-invisible")
          .addClass(this.options.animation);
        }
        this._index = this._index + 1;
      }, this), this.options.delay);
    },

    jQAnimations : function(page) {
      clearInterval(this._delay);
      this._itemsHiding.addClass("jp-hidden");
      this._itemsShowing.fadeTo(0, 0).removeClass("jp-hidden");
      this._itemsOriented = this.getDirectedItems(page);
      this._index = 0;
      this._delay = setInterval(this.bind(function() {
        if (this._index === this._itemsOriented.length) clearInterval(this._delay);
        else {
          this._itemsOriented
          .eq(this._index)
          .fadeTo(this.options.fallback, 1);
        }
        this._index = this._index + 1;
      }, this), this.options.delay);
    },

    getDirectedItems : function(page) {
      var itemsToShow;

      switch (this.options.direction) {
        case "backwards":
          itemsToShow = $(this._itemsShowing.get().reverse());
          break;
        case "random":
          itemsToShow = $(this._itemsShowing.get().sort(function() {
            return (Math.round(Math.random()) - 0.5);
          }));
          break;
        case "auto":
          itemsToShow = page >= this._currentPageNum ?
          this._itemsShowing : $(this._itemsShowing.get().reverse());
          break;
        default:
          itemsToShow = this._itemsShowing;
      }

      return itemsToShow;
    },

    updatePages : function(page) {
      var interval, index, nav;
      interval = this.getInterval(page);
      for (index in this._nav) {
        if (this._nav.hasOwnProperty(index)) {
          nav = this._nav[index];
          this.updateBtns(nav, page);
          this.updateCurrentPage(nav, page);
          this.updatePagesShowing(nav, interval);
          this.updateBreaks(nav, interval);
        }
      }
      return interval;
    },

    getInterval : function(page) {
      var neHalf, upperLimit, start, end;
      neHalf = Math.ceil(this.options.midRange / 2);
      upperLimit = this._numPages - this.options.midRange;
      start = page > neHalf ? Math.max(Math.min(page - neHalf, upperLimit), 0) : 0;
      end = page > neHalf ?
        Math.min(page + neHalf - (this.options.midRange % 2 > 0 ? 1 : 0), this._numPages) :
        Math.min(this.options.midRange, this._numPages);
      return {start: start,end: end};
    },

    updateBtns : function(nav, page) {
      if (page === 1) {
        nav.first.addClass("jp-disabled");
        nav.previous.addClass("jp-disabled");
      }
      if (page === this._numPages) {
        nav.next.addClass("jp-disabled");
        nav.last.addClass("jp-disabled");
      }
      if (this._currentPageNum === 1 && page > 1) {
        nav.first.removeClass("jp-disabled");
        nav.previous.removeClass("jp-disabled");
      }
      if (this._currentPageNum === this._numPages && page < this._numPages) {
        nav.next.removeClass("jp-disabled");
        nav.last.removeClass("jp-disabled");
      }
    },

    updateCurrentPage : function(nav, page) {
      nav.currentPage.removeClass("jp-current");
      nav.currentPage = nav.pages.eq(page - 1).addClass("jp-current");
    },

    updatePagesShowing : function(nav, interval) {
      var newRange = nav.pages.slice(interval.start, interval.end).not(nav.permPages);
      nav.pagesShowing.not(newRange).addClass("jp-hidden");
      newRange.not(nav.pagesShowing).removeClass("jp-hidden");
      nav.pagesShowing = newRange;
    },

    updateBreaks : function(nav, interval) {
      if (
        interval.start > this.options.startRange ||
        (this.options.startRange === 0 && interval.start > 0)
      ) nav.fstBreak.removeClass("jp-hidden");
      else nav.fstBreak.addClass("jp-hidden");

      if (interval.end < this._numPages - this.options.endRange) nav.lstBreak.removeClass("jp-hidden");
      else nav.lstBreak.addClass("jp-hidden");
    },

    callback : function(page, itemRange, pageInterval) {
      var pages = {
            current: page,
            interval: pageInterval,
            count: this._numPages
          },
          items = {
            showing: this._itemsShowing,
            oncoming: this._items.slice(itemRange.start + this.options.perPage, itemRange.end + this.options.perPage),
            range: itemRange,
            count: this._items.length
          };

      pages.interval.start = pages.interval.start + 1;
      items.range.start = items.range.start + 1;
      this.options.callback(pages, items);
    },

    updatePause : function() {
      if (this.options.pause && this._numPages > 1) {
        clearTimeout(this._pause);
        if (this.options.clickStop && this._clicked) return;
        else {
          this._pause = setTimeout(this.bind(function() {
            this.paginate(this._currentPageNum !== this._numPages ? this._currentPageNum + 1 : 1);
          }, this), this.options.pause);
        }
      }
    },

    setMinHeight : function() {
      if (this.options.minHeight && !this._container.is("table, tbody")) {
        setTimeout(this.bind(function() {
          this._container.css({ "min-height": this._container.css("height") });
        }, this), 1000);
      }
    },

    bind : function(fn, me) {
      return function() {
        return fn.apply(me, arguments);
      };
    },

    destroy : function() {
      this.jQdocument.unbind("keydown.jPages");
      this._container.unbind("mousewheel.jPages DOMMouseScroll.jPages");

      if (this.options.minHeight) this._container.css("min-height", "");
      if (this._cssAnimSupport && this.options.animation.length)
        this._items.removeClass("animated jp-hidden jp-invisible " + this.options.animation);
      else this._items.removeClass("jp-hidden").fadeTo(0, 1);
      this._holder.unbind("click.jPages").empty();
    }

  };

  $.fn[name] = function(arg) {
    var type = $.type(arg);

    if (type === "object") {
      if (this.length && !$.data(this, name)) {
        instance = new Plugin(this, arg);
        this.each(function() {
          $.data(this, name, instance);
        });
      }
      return this;
    }

    if (type === "string" && arg === "destroy") {
      instance.destroy();
      this.each(function() {
        $.removeData(this, name);
      });
      return this;
    }

    if (type === 'number' && arg % 1 === 0) {
      if (instance.validNewPage(arg)) instance.paginate(arg);
      return this;
    }

    return this;
  };

})(jQuery, window, document);
