/**
 * MODIFIED BY LIQUID THEMES
 * Fresco - A Beautiful Responsive Lightbox - v2.3.2
 * (c) 2012-2021 Nick Stakenburg
 *
 * https://github.com/staaky/fresco
 *
 * @license: https://creativecommons.org/licenses/by/4.0
 */

// UMD wrapper
(function(root, factory) {
  if (typeof define === "function" && define.amd) {
    // AMD
    define(["jquery"], factory);
  } else if (typeof module === "object" && module.exports) {
    // Node/CommonJS
    module.exports = factory(require("jquery"));
  } else {
    // Browser global
    root.Fresco = factory(jQuery);
  }
})(this, function($) {


var Fresco = {};

$.extend(Fresco, {
  version: "2.3.2",
});

Fresco.Skins = {
  // the default skin
  fresco: {},
};

var Bounds = {
  viewport: function() {
    var dimensions = {
      width: $(window).width(),
    };

    // Mobile Safari has a bugged viewport height after scrolling
    // Firefox on Android also has problems with height
    if (Browser.MobileSafari || (Browser.Android && Browser.Gecko)) {
      var zoom = document.documentElement.clientWidth / window.innerWidth;
      dimensions.height = window.innerHeight * zoom;
    } else {
      // default
      dimensions.height = $(window).height();
    }

    return dimensions;
  },
};

var Browser = (function(uA) {
  function getVersion(identifier) {
    var version = new RegExp(identifier + "([\\d.]+)").exec(uA);
    return version ? parseFloat(version[1]) : true;
  }

  return {
    IE:
      !!(window.attachEvent && uA.indexOf("Opera") === -1) &&
      getVersion("MSIE "),
    Opera:
      uA.indexOf("Opera") > -1 &&
      ((!!window.opera && opera.version && parseFloat(opera.version())) ||
        7.55),
    WebKit: uA.indexOf("AppleWebKit/") > -1 && getVersion("AppleWebKit/"),
    Gecko:
      uA.indexOf("Gecko") > -1 &&
      uA.indexOf("KHTML") === -1 &&
      getVersion("rv:"),
    MobileSafari: !!uA.match(/Apple.*Mobile.*Safari/),
    Chrome: uA.indexOf("Chrome") > -1 && getVersion("Chrome/"),
    ChromeMobile: uA.indexOf("CrMo") > -1 && getVersion("CrMo/"),
    Android: uA.indexOf("Android") > -1 && getVersion("Android "),
    IEMobile: uA.indexOf("IEMobile") > -1 && getVersion("IEMobile/"),
  };
})(navigator.userAgent);

var _slice = Array.prototype.slice;

function baseToString(value) {
  if (typeof value === "string") {
    return value;
  }
  return value == null ? "" : value + "";
}

var _ = {
  isElement: function(object) {
    return object && object.nodeType === 1;
  },

  String: {
    capitalize: function(string) {
      string = baseToString(string);
      return string && string.charAt(0).toUpperCase() + string.slice(1);
    },
  },
};

//mousewheel
(function() {
  function wheel(event) {
    
    /**
     * Added by Liquid Themes
    */
    if ( ! event || ! event.originalEvent ) return;
    
    var realDelta;

    // normalize the delta
    if (event.originalEvent.wheelDelta)
      // IE & Opera
      realDelta = event.originalEvent.wheelDelta / 120;
    else if (event.originalEvent.detail)
      // W3C
      realDelta = -event.originalEvent.detail / 3;

    if (!realDelta) return;

    var customEvent = $.Event("fresco:mousewheel");
    $(event.target).trigger(customEvent, realDelta);

    if (customEvent.isPropagationStopped()) {
      event.stopPropagation();
    }
    if (customEvent.isDefaultPrevented()) {
      event.preventDefault();
    }
  }

  $(document.documentElement).on("mousewheel DOMMouseScroll", wheel);
})();

// Fit
var Fit = {
  within: function(bounds, dimensions) {
    var options = $.extend(
      {
        height: true,
        width: true,
      },
      arguments[2] || {}
    );

    var size = $.extend({}, dimensions),
      scale = 1,
      attempts = 5;

    var fit = { width: options.width, height: options.height };

    // adjust the bounds depending on what to fit (width/height)
    // start
    while (
      attempts > 0 &&
      ((fit.width && size.width > bounds.width) ||
        (fit.height && size.height > bounds.height))
    ) {
      // if both dimensions fall underneath a minimum, then don't event continue
      //if (size.width < 100 && size.height < 100) {
      var scaleX = 1,
        scaleY = 1;

      if (fit.width && size.width > bounds.width) {
        scaleX = bounds.width / size.width;
      }
      if (fit.height && size.height > bounds.height) {
        scaleY = bounds.height / size.height;
      }

      // we'll end up using the largest scaled down factor
      scale = Math.min(scaleX, scaleY);

      // adjust current size, based on original dimensions
      size = {
        width: dimensions.width * scale,
        height: dimensions.height * scale,
      };
      //}

      attempts--;
    }

    // make sure size is never pressed into negative
    size.width = Math.max(size.width, 0);
    size.height = Math.max(size.height, 0);

    return size;
  },
};

// we only uses some of the jQueryUI easing functions
// add those with a prefix to prevent conflicts
$.extend($.easing, {
  frescoEaseInCubic: function(x, t, b, c, d) {
    return c * (t /= d) * t * t + b;
  },

  frescoEaseInSine: function(x, t, b, c, d) {
    return -c * Math.cos((t / d) * (Math.PI / 2)) + c + b;
  },

  frescoEaseOutSine: function(x, t, b, c, d) {
    return c * Math.sin((t / d) * (Math.PI / 2)) + b;
  },
});

var Support = (function() {
  var testElement = document.createElement("div"),
    domPrefixes = "Webkit Moz O ms Khtml".split(" ");

  function prefixed(property) {
    return testAllProperties(property, "prefix");
  }

  function testProperties(properties, prefixed) {
    for (var i in properties) {
      if (testElement.style[properties[i]] !== undefined) {
        return prefixed === "prefix" ? properties[i] : true;
      }
    }
    return false;
  }

  function testAllProperties(property, prefixed) {
    var ucProperty = property.charAt(0).toUpperCase() + property.substr(1),
      properties = (
        property +
        " " +
        domPrefixes.join(ucProperty + " ") +
        ucProperty
      ).split(" ");

    return testProperties(properties, prefixed);
  }

  // feature detect
  return {
    canvas: (function() {
      var canvas = document.createElement("canvas");
      return !!(canvas.getContext && canvas.getContext("2d"));
    })(),

    css: {
      animation: testAllProperties("animation"),
      transform: testAllProperties("transform"),
      prefixed: prefixed,
    },

    svg:
      !!document.createElementNS &&
      !!document.createElementNS("http://www.w3.org/2000/svg", "svg")
        .createSVGRect,

    touch: (function() {
      try {
        return !!(
          "ontouchstart" in window ||
          (window.DocumentTouch && document instanceof DocumentTouch)
        ); // firefox on Android
      } catch (e) {
        return false;
      }
    })(),
  };
})();

// add mobile touch to support
Support.detectMobileTouch = function() {
  Support.mobileTouch =
    Support.touch &&
    (Browser.MobileSafari ||
      Browser.Android ||
      Browser.IEMobile ||
      Browser.ChromeMobile ||
      !/^(Win|Mac|Linux)/.test(navigator.platform)); // otherwise, assume anything not on Windows, Mac or Linux is a mobile device
  //Support.mobileTouch = true;
};
Support.detectMobileTouch();

/* ImageReady (standalone) - part of Voilà
 * http://voila.nickstakenburg.com
 * MIT License
 */
var ImageReady = function() {
  return this.initialize.apply(this, Array.prototype.slice.call(arguments));
};

$.extend(ImageReady.prototype, {
  supports: {
    naturalWidth: (function() {
      return "naturalWidth" in new Image();
    })(),
  },

  // NOTE: setTimeouts allow callbacks to be attached
  initialize: function(img, successCallback, errorCallback) {
    this.img = $(img)[0];
    this.successCallback = successCallback;
    this.errorCallback = errorCallback;
    this.isLoaded = false;

    this.options = $.extend(
      {
        method: "naturalWidth",
        pollFallbackAfter: 1000,
      },
      arguments[3] || {}
    );

    // a fallback is used when we're not polling for naturalWidth/Height
    // IE6-7 also use this to add support for naturalWidth/Height
    if (!this.supports.naturalWidth || this.options.method === "onload") {
      setTimeout(this.fallback.bind(this));
      return;
    }

    // can exit out right away if we have a naturalWidth
    if (this.img.complete && typeof this.img.naturalWidth !== "undefined") {
      setTimeout(
        function() {
          if (this.img.naturalWidth > 0) {
            this.success();
          } else {
            this.error();
          }
        }.bind(this)
      );
      return;
    }

    // we instantly bind to onerror so we catch right away
    $(this.img).bind(
      "error",
      function() {
        setTimeout(
          function() {
            this.error();
          }.bind(this)
        );
      }.bind(this)
    );

    this.intervals = [
      [1000, 10],
      [2 * 1000, 50],
      [4 * 1000, 100],
      [20 * 1000, 500],
    ];

    // for testing, 2sec delay
    // this.intervals = [[20 * 1000, 2000]];

    this._ipos = 0;
    this._time = 0;
    this._delay = this.intervals[this._ipos][1];

    // start polling
    this.poll();
  },

  poll: function() {
    this._polling = setTimeout(
      function() {
        if (this.img.naturalWidth > 0) {
          this.success();
          return;
        }

        // update time spend
        this._time += this._delay;

        // use a fallback after waiting
        if (
          this.options.pollFallbackAfter &&
          this._time >= this.options.pollFallbackAfter &&
          !this._usedPollFallback
        ) {
          this._usedPollFallback = true;
          this.fallback();
        }

        // next i within the interval
        if (this._time > this.intervals[this._ipos][0]) {
          // if there's no next interval, we asume
          // the image image errored out
          if (!this.intervals[this._ipos + 1]) {
            this.error();
            return;
          }

          this._ipos++;

          // update to the new bracket
          this._delay = this.intervals[this._ipos][1];
        }

        this.poll();
      }.bind(this),
      this._delay
    );
  },

  fallback: function() {
    var img = new Image();
    this._fallbackImg = img;

    img.onload = function() {
      img.onload = function() {};

      if (!this.supports.naturalWidth) {
        this.img.naturalWidth = img.width;
        this.img.naturalHeight = img.height;
      }

      this.success();
    }.bind(this);

    img.onerror = this.error.bind(this);

    img.src = this.img.src;
  },

  abort: function() {
    if (this._fallbackImg) {
      this._fallbackImg.onload = function() {};
    }

    if (this._polling) {
      clearTimeout(this._polling);
      this._polling = null;
    }
  },

  success: function() {
    if (this._calledSuccess) return;
    this._calledSuccess = true;

    this.isLoaded = true;
    this.successCallback(this);
  },

  error: function() {
    if (this._calledError) return;
    this._calledError = true;

    this.abort();
    if (this.errorCallback) this.errorCallback(this);
  },
});

function Timers() {
  return this.initialize.apply(this, _slice.call(arguments));
}
$.extend(Timers.prototype, {
  initialize: function() {
    this._timers = {};
  },

  set: function(name, handler, ms) {
    this._timers[name] = setTimeout(handler, ms);
  },

  get: function(name) {
    return this._timers[name];
  },

  clear: function(name) {
    if (name) {
      if (this._timers[name]) {
        clearTimeout(this._timers[name]);
        delete this._timers[name];
      }
    } else {
      this.clearAll();
    }
  },

  clearAll: function() {
    $.each(this._timers, function(i, timer) {
      clearTimeout(timer);
    });
    this._timers = {};
  },
});

// uses Types to scan a URI for info
function getURIData(url) {
  var result = { type: "image" };
  $.each(Types, function(i, type) {
    var data = type.data(url);
    if (data) {
      result = data;
      result.type = i;
      result.url = url;
    }
  });

  return result;
}

function detectExtension(url) {
  var ext = (url || "").replace(/\?.*/g, "").match(/\.([^.]{3,4})$/);
  return ext ? ext[1].toLowerCase() : null;
}

var Type = {
  isVideo: function(type) {
    return /^(youtube|vimeo)$/.test(type);
  },
};

var Types = {
  image: {
    extensions: "bmp gif jpeg jpg png webp",
    detect: function(url) {
      return $.inArray(detectExtension(url), this.extensions.split(" ")) > -1;
    },
    data: function(url) {
      if (!this.detect()) return false;

      return {
        extension: detectExtension(url),
      };
    },
  },

  vimeo: {
    detect: function(url) {
      var res = /(vimeo\.com)\/([a-zA-Z0-9-_]+)(?:\S+)?$/i.exec(url);
      if (res && res[2]) return res[2];

      return false;
    },
    data: function(url) {
      var id = this.detect(url);
      if (!id) return false;

      return {
        id: id,
      };
    },
  },

  youtube: {
    detect: function(url) {
      var res = /(youtube\.com|youtu\.be)\/watch\?(?=.*vi?=([a-zA-Z0-9-_]+))(?:\S+)?$/.exec(
        url
      );
      if (res && res[2]) return res[2];

      res = /(youtube\.com|youtu\.be)\/(vi?\/|u\/|embed\/)?([a-zA-Z0-9-_]+)(?:\S+)?$/i.exec(
        url
      );
      if (res && res[3]) return res[3];

      return false;
    },
    data: function(url) {
      var id = this.detect(url);
      if (!id) return false;

      return {
        id: id,
      };
    },
  },
};

var VimeoThumbnail = (function() {
  var VimeoThumbnail = function() {
    return this.initialize.apply(this, _slice.call(arguments));
  };
  $.extend(VimeoThumbnail.prototype, {
    initialize: function(url, successCallback, errorCallback) {
      this.url = url;
      this.successCallback = successCallback;
      this.errorCallback = errorCallback;

      this.load();
    },

    load: function() {
      // first try the cache
      var cache = Cache.get(this.url);

      if (cache) {
        return this.successCallback(cache.data.url);
      }

      var protocol =
          "http" +
          (window.location && window.location.protocol === "https:"
            ? "s"
            : "") +
          ":",
        video_id = getURIData(this.url).id;

      this._xhr = $.getJSON(
        protocol +
          "//vimeo.com/api/oembed.json?url=" +
          protocol +
          "//vimeo.com/" +
          video_id +
          "&callback=?",
        function(_data) {
          if (_data && _data.thumbnail_url) {
            var data = {
              url: _data.thumbnail_url,
            };

            Cache.set(this.url, data);

            this.successCallback(data.url);
          } else {
            this.errorCallback();
          }
        }.bind(this)
      );
    },

    abort: function() {
      if (this._xhr) {
        this._xhr.abort();
        this._xhr = null;
      }
    },
  });

  var Cache = {
    cache: [],

    get: function(url) {
      var entry = null;
      for (var i = 0; i < this.cache.length; i++) {
        if (this.cache[i] && this.cache[i].url === url) entry = this.cache[i];
      }
      return entry;
    },

    set: function(url, data) {
      this.remove(url);
      this.cache.push({ url: url, data: data });
    },

    remove: function(url) {
      for (var i = 0; i < this.cache.length; i++) {
        if (this.cache[i] && this.cache[i].url === url) {
          delete this.cache[i];
        }
      }
    },
  };

  return VimeoThumbnail;
})();

var VimeoReady = (function() {
  var VimeoReady = function() {
    return this.initialize.apply(this, _slice.call(arguments));
  };
  $.extend(VimeoReady.prototype, {
    initialize: function(url, callback) {
      this.url = url;
      this.callback = callback;

      this.load();
    },

    load: function() {
      // first try the cache
      var cache = Cache.get(this.url);

      if (cache) {
        return this.callback(cache.data);
      }

      var protocol =
          "http" +
          (window.location && window.location.protocol === "https:"
            ? "s"
            : "") +
          ":",
        video_id = getURIData(this.url).id;

      // NOTE: We're using a maxwidth/maxheight hack because of a regression in the oEmbed API
      // see: https://vimeo.com/forums/api/topic:283559
      this._xhr = $.getJSON(
        protocol +
          "//vimeo.com/api/oembed.json?url=" +
          protocol +
          "//vimeo.com/" +
          video_id +
          "&maxwidth=9999999&maxheight=9999999&callback=?",
        function(_data) {
          var data = {
            dimensions: {
              width: _data.width,
              height: _data.height,
            },
          };

          Cache.set(this.url, data);

          if (this.callback) this.callback(data);
        }.bind(this)
      );
    },

    abort: function() {
      if (this._xhr) {
        this._xhr.abort();
        this._xhr = null;
      }
    },
  });

  var Cache = {
    cache: [],

    get: function(url) {
      var entry = null;
      for (var i = 0; i < this.cache.length; i++) {
        if (this.cache[i] && this.cache[i].url === url) entry = this.cache[i];
      }
      return entry;
    },

    set: function(url, data) {
      this.remove(url);
      this.cache.push({ url: url, data: data });
    },

    remove: function(url) {
      for (var i = 0; i < this.cache.length; i++) {
        if (this.cache[i] && this.cache[i].url === url) {
          delete this.cache[i];
        }
      }
    },
  };

  return VimeoReady;
})();

var Options = {
  defaults: {
    effects: {
      content: { show: 0, hide: 0 },
      spinner: { show: 150, hide: 150 },
      window: { show: 440, hide: 300 },
      thumbnail: { show: 300, delay: 150 },
      thumbnails: { slide: 0 },
    },
    keyboard: {
      left: true,
      right: true,
      esc: true,
    },
    loadedMethod: "naturalWidth",
    loop: false,
    onClick: "previous-next",
    overflow: false,
    overlay: {
      close: true,
    },
    preload: [1, 2],
    position: true,
    skin: "fresco",
    spinner: true,
    spinnerDelay: 300,
    sync: true,
    thumbnails: "horizontal",
    ui: "outside",
    uiDelay: 3000,
    vimeo: {
      autoplay: 1,
      api: 1,
      title: 1,
      byline: 1,
      portrait: 0,
      loop: 0,
    },
    youtube: {
      autoplay: 1,
      controls: 1,
      //cc_load_policy: 0,
      enablejsapi: 1,
      hd: 1,
      iv_load_policy: 3,
      loop: 0,
      modestbranding: 1,
      rel: 0,
      vq: "hd1080", // force hd: http://stackoverflow.com/a/12467865
    },

    initialTypeOptions: {
      image: {},
      vimeo: {
        width: 1280,
      },
      // Youtube needs both dimensions, it doesn't support fetching video dimensions like Vimeo yet.
      // Star this ticket if you'd like to get support for it at some point:
      // https://code.google.com/p/gdata-issues/issues/detail?id=4329
      youtube: {
        width: 1280,
        height: 720,
      },
    },
  },

  create: function(opts, type, data) {
    opts = opts || {};
    data = data || {};

    opts.skin = opts.skin || this.defaults.skin;

    var selected = opts.skin
        ? $.extend(
            {},
            Fresco.Skins[opts.skin] || Fresco.Skins[this.defaults.skin]
          )
        : {},
      merged = $.extend(true, {}, this.defaults, selected);

    // merge initial type options
    if (merged.initialTypeOptions) {
      if (type && merged.initialTypeOptions[type]) {
        merged = $.extend(true, {}, merged.initialTypeOptions[type], merged);
      }
      // these aren't used further, so remove them
      delete merged.initialTypeOptions;
    }

    // safe options to work with
    var options = $.extend(true, {}, merged, opts);

    // touch should never use ui:inside
    if (Support.mobileTouch && options.ui === "inside") {
      options.ui = "outside";
    }

    // set all effect duration to 0 for effects: false
    // IE8 and below never use effects
    if (!options.effects || (Browser.IE && Browser.IE < 9)) {
      options.effects = {};
      $.each(this.defaults.effects, function(name, effect) {
        $.each((options.effects[name] = $.extend({}, effect)), function(
          option
        ) {
          options.effects[name][option] = 0;
        });
      });

      // disable the spinner when effects are disabled
      options.spinner = false;
    }

    // keyboard
    if (options.keyboard) {
      // when keyboard is true, enable all keys
      if (typeof options.keyboard === "boolean") {
        options.keyboard = {};
        $.each(this.defaults.keyboard, function(key, bool) {
          options.keyboard[key] = true;
        });
      }

      // disable left and right keys for video, because players like
      // youtube use these keys
      if (type === "vimeo" || type === "youtube") {
        $.extend(options.keyboard, { left: false, right: false });
      }
    }

    // overflow
    if (!options.overflow || Support.mobileTouch) {
      // false
      options.overflow = { x: false, y: false };
    } else {
      if (typeof options.overflow === "boolean") {
        // true
        options.overflow = { x: false, y: true };
      }
    }

    // vimeo & youtube always have no overlap
    if (type === "vimeo" || type === "youtube") {
      options.overlap = false;
    }

    // disabled thumbnails IE < 9 & touch based devices
    if ((Browser.IE && Browser.IE < 9) || Support.mobileTouch) {
      options.thumbnail = false;
      options.thumbnails = false;
    }

    // width/height are only used for youtube
    // convert it to maxWidth/Height for the other content
    // when no max values have been set
    if (type !== "youtube") {
      if (options.width && !options.maxWidth) {
        options.maxWidth = options.width;
      }
      if (options.height && !options.maxHeight) {
        options.maxHeight = options.height;
      }
    }

    // youtube thumbnails
    if (!options.thumbnail && typeof options.thumbnail !== "boolean") {
      // only continue if undefined, forced false stays false
      var thumbnail = false;

      switch (type) {
        case "youtube":
          var protocol =
            "http" +
            (window.location && window.location.protocol === "https:"
              ? "s"
              : "") +
            ":";

          thumbnail = protocol + "//img.youtube.com/vi/" + data.id + "/0.jpg";
          break;
        case "image":
        case "vimeo":
          thumbnail = true;
          break;
      }

      options.thumbnail = thumbnail;
    }

    return options;
  },
};

var Overlay = {
  initialize: function() {
    this.build();
    this.visible = false;
  },

  build: function() {
    this.element = $("<div>")
      .addClass("fr-overlay")
      .hide()
      .append($("<div>").addClass("fr-overlay-background"));

    this.element.on(
      "click",
      function() {
        var page = Pages.page;
        if (
          page &&
          page.view &&
          page.view.options.overlay &&
          !page.view.options.overlay.close
        ) {
          return;
        }
        Window.hide();
      }.bind(this)
    );

    if (Support.mobileTouch) {
      this.element.addClass("fr-mobile-touch");
    }

    // prevent mousewheel scroll
    this.element.on("fresco:mousewheel", function(event) {
      event.preventDefault();
    });
  },

  setSkin: function(skin) {
    if (this.skin) {
      this.element.removeClass("fr-overlay-skin-" + this.skin);
    }

    this.element.addClass("fr-overlay-skin-" + skin);
    this.skin = skin;
  },

  attach: function() {
    $(document.body).append(this.element);
  },

  detach: function() {
    this.element.detach();
  },

  show: function(callback, alternateDuration) {
    if (this.visible) {
      if (callback) callback();
      return;
    }

    this.visible = true;

    this.attach();
    this.max();

    var pDuration =
        (Pages.page && Pages.page.view.options.effects.window.show) || 0,
      duration =
        (typeof alternateDuration === "number"
          ? alternateDuration
          : pDuration) || 0;

    this.element.stop(true).fadeTo(duration, 1, callback);
  },

  hide: function(callback, alternateDuration) {
    if (!this.visible) {
      if (callback) callback();
      return;
    }

    var pDuration =
        (Pages.page && Pages.page.view.options.effects.window.hide) || 0,
      duration =
        (typeof alternateDuration === "number"
          ? alternateDuration
          : pDuration) || 0;

    this.element.stop(true).fadeOut(
      duration || 0,
      function() {
        this.detach();
        this.visible = false;
        if (callback) callback();
      }.bind(this)
    );
  },

  getScrollDimensions: function() {
    var dimensions = {};
    $.each(["width", "height"], function(i, d) {
      var D = d.substr(0, 1).toUpperCase() + d.substr(1),
        ddE = document.documentElement;

      dimensions[d] =
        (Browser.IE
          ? Math.max(ddE["offset" + D], ddE["scroll" + D])
          : Browser.WebKit
          ? document.body["scroll" + D]
          : ddE["scroll" + D]) || 0;
    });
    return dimensions;
  },

  max: function() {
    var scrollDimensions;
    if (Browser.MobileSafari && (Browser.WebKit && Browser.WebKit < 533.18)) {
      scrollDimensions = this.getScrollDimensions();
      this.element.css(scrollDimensions);
    }

    if (Browser.IE && Browser.IE < 9) {
      var viewport = Bounds.viewport();
      this.element.css({ height: viewport.height, width: viewport.width });
    }

    if (Support.mobileTouch && !scrollDimensions) {
      this.element.css({
        height: this.getScrollDimensions().height,
      });
    }
  },
};

var Window = {
  initialize: function() {
    this.queues = [];
    this.queues.hide = $({});

    this.pages = [];
    this._tracking = [];
    this._first = true;

    this.timers = new Timers();

    this.build();
    this.setSkin(Options.defaults.skin);
  },

  build: function() {
    // window
    this.element = $("<div>")
      .addClass("fr-window fr-measured")
      .hide() // start hidden

      .append(
        (this._box = $("<div>")
          .addClass("fr-box")
          .append((this._pages = $("<div>").addClass("fr-pages"))))
      )

      .append((this._thumbnails = $("<div>").addClass("fr-thumbnails")));

    Overlay.initialize();
    Pages.initialize(this._pages);
    Thumbnails.initialize(this._thumbnails);
    Spinner.initialize();
    UI.initialize();

    // support classes
    this.element.addClass(
      "fr" + (!Support.mobileTouch ? "-no" : "") + "-mobile-touch"
    );
    this.element.addClass("fr" + (!Support.svg ? "-no" : "") + "-svg");

    if (Browser.IE) {
      for (var i = 7; i <= 9; i++) {
        if (Browser.IE < i) {
          this.element.addClass("fr-ltIE" + i);
        }
      }
    }

    // prevent mousewheel scroll
    this.element.on("fresco:mousewheel", function(event) {
      event.preventDefault();
    });
  },

  attach: function() {
    if (this._attached) return;
    $(document.body).append(this.element);
    this._attached = true;
  },

  detach: function() {
    if (!this._attached) return;
    this.element.detach();
    this._attached = false;
  },

  setSkin: function(skin) {
    if (this._skin) {
      this.element.removeClass("fr-window-skin-" + this._skin);
    }
    this.element.addClass("fr-window-skin-" + skin);

    Overlay.setSkin(skin);

    this._skin = skin;
  },

  setShowingType: function(type) {
    if (this._showingType === type) return;

    if (this._showingType) {
      this.element.removeClass("fr-showing-type-" + this._showingType);
      if (Type.isVideo(this._showingType)) {
        this.element.removeClass("fr-showing-type-video");
      }
    }

    this.element.addClass("fr-showing-type-" + type);
    if (Type.isVideo(type)) {
      this.element.addClass("fr-showing-type-video");
    }

    this._showingType = type;
  },

  // Resize
  startObservingResize: function() {
    if (this._onWindowResizeHandler) return;
    $(window).on(
      "resize orientationchange",
      (this._onWindowResizeHandler = this._onWindowResize.bind(this))
    );
  },

  stopObservingResize: function() {
    if (this._onWindowResizeHandler) {
      $(window).off("resize orientationchange", this._onWindowResizeHandler);
      this._onWindowResizeHandler = null;
    }
  },

  _onScroll: function() {
    if (!Support.mobileTouch) return;
    // the timeout is a hack for iOS not responding
    this.timers.set("scroll", this.adjustToScroll.bind(this), 0);
  },

  _onWindowResize: function() {
    var page;
    if (!(page = Pages.page)) return;

    Thumbnails.fitToViewport();

    this.updateBoxDimensions();

    page.fitToBox();

    // update the UI to the current size
    UI.update();

    // instantly update previous/next
    UI.adjustPrevNext(null, 0);

    // reposition spinner
    Spinner.center();

    Overlay.max(); // IE7-8

    // show UI for touch on resize
    UI._onWindowResize();

    this._onScroll();
  },

  adjustToScroll: function() {
    if (!Support.mobileTouch) return;

    this.element.css({
      top: $(window).scrollTop(),
    });
  },

  getBoxDimensions: function() {
    return this._boxDimensions;
  },

  updateBoxDimensions: function() {
    var page;
    if (!(page = Pages.page)) return;

    var viewport = Bounds.viewport(),
      thumbnails = Thumbnails.getDimensions();

    var isHorizontal = Thumbnails._orientation === "horizontal";

    this._boxDimensions = {
      width: isHorizontal ? viewport.width : viewport.width - thumbnails.width,
      height: isHorizontal
        ? viewport.height - thumbnails.height
        : viewport.height,
    };

    // resize
    this._boxPosition = {
      top: 0,
      left: isHorizontal ? 0 : thumbnails.width,
    };

    this._box.css($.extend({}, this._boxDimensions, this._boxPosition));
  },

  show: function(callback, alternateDuration) {
    if (this.visible) {
      if (callback) callback();
      return;
    }

    this.visible = true;
    this.opening = true;

    this.attach();

    // clear timers that possible break toggling between show/hide()
    this.timers.clear("show-window");
    this.timers.clear("hide-overlay");

    // position the window at the top if mobile touch
    this.adjustToScroll();

    var duration =
      (typeof alternateDuration === "number"
        ? alternateDuration
        : Pages.page && Pages.page.view.options.effects.window.show) || 0;

    var fx = 2;

    // overlay
    Overlay[Pages.page && Pages.page.view.options.overlay ? "show" : "hide"](
      function() {
        if (callback && --fx < 1) callback();
      },
      duration
    );

    // window
    // using a timeout here removes a sharp visible edge of the window while fading in
    // because the fading happens on top of a solid area
    this.timers.set(
      "show-window",
      function() {
        this._show(
          function() {
            this.opening = false;
            if (callback && --fx < 1) callback();
          }.bind(this),
          duration
        );
      }.bind(this),
      duration > 1 ? Math.min(duration * 0.5, 50) : 1
    );
  },

  _show: function(callback, alternateDuration) {
    var duration =
      (typeof alternateDuration === "number"
        ? alternateDuration
        : Pages.page && Pages.page.view.options.effects.window.show) || 0;

    this.element.stop(true).fadeTo(duration, 1, callback);
  },

  hide: function(callback) {
    if (!this.view) return;

    var hideQueue = this.queues.hide;
    hideQueue.queue([]); // clear queue

    // clear timers that possible break toggling between show/hide()
    this.timers.clear("show-window");
    this.timers.clear("hide-overlay");

    var duration = Pages.page ? Pages.page.view.options.effects.window.hide : 0;

    hideQueue.queue(
      function(next_stop) {
        Pages.stop();

        // hide the spinner here so its effect ends early enough
        Spinner.hide();

        next_stop();
      }.bind(this)
    );

    hideQueue.queue(
      function(next_unbinds) {
        // ui
        UI.disable();
        UI.hide(null, duration);

        // keyboard
        Keyboard.disable();

        next_unbinds();
      }.bind(this)
    );

    hideQueue.queue(
      function(next_hidden) {
        var fx = 2;

        this._hide(function() {
          if (--fx < 1) next_hidden();
        }, duration);

        // using a timeout here removes a sharp visible edge of the window while fading out
        this.timers.set(
          "hide-overlay",
          function() {
            Overlay.hide(function() {
              if (--fx < 1) next_hidden();
            }, duration);
          }.bind(this),
          duration > 1 ? Math.min(duration * 0.5, 150) : 1
        );

        // after we initiate hide, the next show() should bring up the UI
        // we to this using a flag
        this._first = true;
      }.bind(this)
    );

    // callbacks after resize in a separate queue
    // so we can stop the hideQueue without stopping the resize
    hideQueue.queue(
      function(next_after_resize) {
        this._reset();

        // all of the below we cannot safely call safely
        this.stopObservingResize();
        //this.stopObservingScroll();

        Pages.removeAll();

        Thumbnails.clear();

        this.timers.clear();

        this._position = -1;

        // afterHide callback
        var afterHide = Pages.page && Pages.page.view.options.afterHide;
        if (typeof afterHide === "function") {
          afterHide.call(Fresco);
        }

        this.view = null;
        this.opening = false;
        this.closing = false;

        // remove from DOM
        this.detach();

        next_after_resize();
      }.bind(this)
    );

    if (typeof callback === "function") {
      hideQueue.queue(
        function(next_callback) {
          callback();
          next_callback();
        }.bind(this)
      );
    }
  },

  _hide: function(callback, alternateDuration) {
    var duration =
      (typeof alternateDuration === "number"
        ? alternateDuration
        : Pages.page && Pages.page.view.options.effects.window.hide) || 0;

    this.element.stop(true).fadeOut(duration, callback);
  },

  // Load
  load: function(views, position) {
    this.views = views;

    // dimension and visibility based code needs
    // the window attached
    this.attach();

    Thumbnails.load(views);

    Pages.load(views);

    this.startObservingResize();
    //this.startObservingScroll();

    if (position) {
      this.setPosition(position);
    }
  },

  // loading indicator
  /*
  startLoading: function() {
    if (!Spinner.supported) return;

    Spinner.show();
    Spinner.center();
  },

  stopLoading: function() {
    if (!Spinner.supported) return;

    // we only stop loading if there are no loading pages anymore
    var loadingCount = Pages.getLoadingCount();

    if (loadingCount < 1) {
      Spinner.hide();
    }
  },*/

  setPosition: function(position, callback) {
    this._position = position;

    // store the current view
    this.view = this.views[position - 1];

    // we need to make sure that a possible hide effect doesn't
    // trigger its callbacks, as that would cancel the showing/loading
    // of the page started below
    this.stopHideQueue();

    // store the page and show it
    this.page = Pages.show(
      position,
      function() {
        if (callback) callback();
      }.bind(this)
    );
  },

  // stop all callbacks possibly queued up into a hide animation
  // this allows the hide animation to finish as we start showing/loading
  // a new page, a callback could otherwise interrupt this
  stopHideQueue: function() {
    this.queues.hide.queue([]);
  },

  _reset: function() {
    this.visible = false;
    UI.hide(null, 0);
    UI.reset();
  },

  // Previous / Next
  mayPrevious: function() {
    return (
      (this.view &&
        this.view.options.loop &&
        this.views &&
        this.views.length > 1) ||
      this._position !== 1
    );
  },

  previous: function(force) {
    var mayPrevious = this.mayPrevious();

    if (force || mayPrevious) {
      this.setPosition(this.getSurroundingIndexes().previous);
    }
  },

  mayNext: function() {
    var hasViews = this.views && this.views.length > 1;

    return (
      (this.view && this.view.options.loop && hasViews) ||
      (hasViews && this.getSurroundingIndexes().next !== 1)
    );
  },

  next: function(force) {
    var mayNext = this.mayNext();

    if (force || mayNext) {
      this.setPosition(this.getSurroundingIndexes().next);
    }
  },

  // surrounding
  getSurroundingIndexes: function() {
    if (!this.views) return {};

    var pos = this._position,
      length = this.views.length;

    var previous = pos <= 1 ? length : pos - 1,
      next = pos >= length ? 1 : pos + 1;

    return {
      previous: previous,
      next: next,
    };
  },
};

//  Keyboard
//  keeps track of keyboard events when enabled
var Keyboard = {
  enabled: false,

  keyCode: {
    left: 37,
    right: 39,
    esc: 27,
  },

  // enable is passed the keyboard option of a page, which can be false
  // or contains multiple buttons to toggle
  enable: function(enabled) {
    this.disable();

    if (!enabled) return;

    $(document)
      .on("keydown", (this._onKeyDownHandler = this.onKeyDown.bind(this)))
      .on("keyup", (this._onKeyUpHandler = this.onKeyUp.bind(this)));

    this.enabled = enabled;
  },

  disable: function() {
    this.enabled = false;

    if (this._onKeyUpHandler) {
      $(document)
        .off("keyup", this._onKeyUpHandler)
        .off("keydown", this._onKeyDownHandler);
      this._onKeyUpHandler = this._onKeyDownHandler = null;
    }
  },

  onKeyDown: function(event) {
    if (!this.enabled) return;

    var key = this.getKeyByKeyCode(event.keyCode);

    if (!key || (key && this.enabled && !this.enabled[key])) return;

    event.preventDefault();
    event.stopPropagation();

    switch (key) {
      case "left":
        Window.previous();
        break;
      case "right":
        Window.next();
        break;
    }
  },

  onKeyUp: function(event) {
    if (!this.enabled) return;

    var key = this.getKeyByKeyCode(event.keyCode);

    if (!key || (key && this.enabled && !this.enabled[key])) return;

    switch (key) {
      case "esc":
        Window.hide();
        break;
    }
  },

  getKeyByKeyCode: function(keyCode) {
    for (var key in this.keyCode) {
      if (this.keyCode[key] === keyCode) return key;
    }
    return null;
  },
};

var Page = (function() {
  var _uid = 0,
    _loadedUrlCache = {},
    // a group of elements defining the 1px stroke, cloned later on
    _strokes = $("<div>")
      .addClass("fr-stroke fr-stroke-top fr-stroke-horizontal")
      .append($("<div>").addClass("fr-stroke-color"))
      .add(
        $("<div>")
          .addClass("fr-stroke fr-stroke-bottom fr-stroke-horizontal")
          .append($("<div>").addClass("fr-stroke-color"))
      )
      .add(
        $("<div>")
          .addClass("fr-stroke fr-stroke-left fr-stroke-vertical")
          .append($("<div>").addClass("fr-stroke-color"))
      )
      .add(
        $("<div>")
          .addClass("fr-stroke fr-stroke-right fr-stroke-vertical")
          .append($("<div>").addClass("fr-stroke-color"))
      );

  function Page() {
    return this.initialize.apply(this, _slice.call(arguments));
  }
  $.extend(Page.prototype, {
    initialize: function(view, position, total) {
      this.view = view;
      this.dimensions = { width: 0, height: 0 };
      this.uid = _uid++;

      // store position/total views for later use
      this._position = position;
      this._total = total;
      this._fullClick = false;

      this._visible = false;

      this.queues = {};
      this.queues.showhide = $({});
    },

    // create the page, this doesn't mean it's loaded
    // should happen instantly
    create: function() {
      if (this._created) return;

      Pages.element.append(
        (this.element = $("<div>")
          .addClass("fr-page")
          .append((this.container = $("<div>").addClass("fr-container")))
          .css({ opacity: 0 })
          .hide())
      );

      // check if we have a position
      var hasPosition = this.view.options.position && this._total > 1;
      if (hasPosition) {
        // mark it if so
        this.element.addClass("fr-has-position");
      }

      // info (caption/position)
      if (this.view.caption || hasPosition) {
        this.element.append(
          (this.info = $("<div>")
            .addClass("fr-info")
            .append($("<div>").addClass("fr-info-background"))
            .append(_strokes.clone(true))
            .append((this.infoPadder = $("<div>").addClass("fr-info-padder"))))
        );

        // insert position first because it floats right
        if (hasPosition) {
          this.element.addClass("fr-has-position");

          this.infoPadder.append(
            (this.pos = $("<div>")
              .addClass("fr-position")
              .append(
                $("<span>")
                  .addClass("fr-position-text")
                  .html(this._position + " / " + this._total)
              ))
          );
        }

        if (this.view.caption) {
          this.infoPadder.append(
            (this.caption = $("<div>")
              .addClass("fr-caption")
              .html(this.view.caption))
          );
        }
      }

      // background
      this.container
        .append(
          (this.background = $("<div>").addClass("fr-content-background"))
        )
        .append((this.content = $("<div>").addClass("fr-content")));

      // append images instantly
      if (this.view.type == "image") {
        this.content.append(
          (this.image = $("<img>")
            .addClass("fr-content-element")
            .attr({ src: this.view.url }))
        );

        this.content.append(_strokes.clone(true));
      }

      // ui:outside needs a position outside of the info bar
      if (hasPosition && this.view.options.ui == "outside") {
        this.container.append(
          (this.positionOutside = $("<div>")
            .addClass("fr-position-outside")
            .append($("<div>").addClass("fr-position-background"))
            .append(
              $("<span>")
                .addClass("fr-position-text")
                .html(this._position + " / " + this._total)
            ))
        );
      }

      // ui:inside has everything inside the content
      if (this.view.options.ui == "inside") {
        // buttons
        this.content
          // < previous
          .append(
            (this.previousInside = $("<div>")
              .addClass("fr-side fr-side-previous fr-toggle-ui")
              .append(
                $("<div>")
                  .addClass("fr-side-button")
                  .append($("<div>").addClass("fr-side-button-background"))
                  .append($("<div>").addClass("fr-side-button-icon"))
              ))
          )
          // > next
          .append(
            (this.nextInside = $("<div>")
              .addClass("fr-side fr-side-next fr-toggle-ui")
              .append(
                $("<div>")
                  .addClass("fr-side-button")
                  .append($("<div>").addClass("fr-side-button-background"))
                  .append($("<div>").addClass("fr-side-button-icon"))
              ))
          )

          // X close
          .append(
            (this.closeInside = $("<div>")
              .addClass("fr-close fr-toggle-ui")
              .append($("<div>").addClass("fr-close-background"))
              .append($("<div>").addClass("fr-close-icon")))
          );

        // info (only inserted when there is a caption)
        // if there is no caption we insert a separate position element below
        // but if 1 item in the group has a caption we insert the info bar
        if (this.view.caption || (hasPosition && this.view.grouped.caption)) {
          this.content.append(
            (this.infoInside = $("<div>")
              .addClass("fr-info fr-toggle-ui")
              .append($("<div>").addClass("fr-info-background"))
              .append(_strokes.clone(true))
              .append(
                (this.infoPadderInside = $("<div>").addClass("fr-info-padder"))
              ))
          );

          // insert position first because it floats right
          if (hasPosition) {
            this.infoPadderInside.append(
              (this.posInside = $("<div>")
                .addClass("fr-position")
                .append(
                  $("<span>")
                    .addClass("fr-position-text")
                    .html(this._position + " / " + this._total)
                ))
            );
          }

          if (this.view.caption) {
            this.infoPadderInside.append(
              (this.captionInside = $("<div>")
                .addClass("fr-caption")
                .html(this.view.caption))
            );
          }
        }

        // insert a separate position for when there's no caption
        // avoid adding it when the group has at least one caption,
        // the info bar is shown then
        if (!this.view.caption && hasPosition && !this.view.grouped.caption) {
          this.content.append(
            (this.positionInside = $("<div>")
              .addClass("fr-position-inside fr-toggle-ui")
              .append($("<div>").addClass("fr-position-background"))
              .append(
                $("<span>")
                  .addClass("fr-position-text")
                  .html(this._position + " / " + this._total)
              ))
          );
        }

        // disabled states on buttons
        var mayPrevious =
            (this.view.options.loop && this._total > 1) || this._position != 1,
          mayNext =
            (this.view.options.loop && this._total > 1) ||
            this._position < this._total;
        this.previousInside[(mayPrevious ? "remove" : "add") + "Class"](
          "fr-side-disabled"
        );
        this.nextInside[(mayNext ? "remove" : "add") + "Class"](
          "fr-side-disabled"
        );
      }

      // overlap (this affects padding)
      $.each(
        ["x", "y"],
        function(i, z) {
          if (this.view.options.overflow[z]) {
            this.element.addClass("fr-overflow-" + z);
          }
        }.bind(this)
      );

      // add the type
      this.element.addClass("fr-type-" + this.view.type);
      // add type-video
      if (Type.isVideo(this.view.type)) {
        this.element.addClass("fr-type-video");
      }

      // no sides
      if (this._total < 2) {
        this.element.addClass("fr-no-sides");
      }

      this._created = true;
    },

    //surrounding
    _getSurroundingPages: function() {
      var preload;
      if (!(preload = this.view.options.preload)) return [];

      var pages = [],
        begin = Math.max(1, this._position - preload[0]),
        end = Math.min(this._position + preload[1], this._total),
        pos = this._position;

      // add the pages after this one first for the preloading order
      for (var i = pos; i <= end; i++) {
        var page = Pages.pages[i - 1];
        if (page._position != pos) pages.push(page);
      }

      for (var i = pos; i >= begin; i--) {
        var page = Pages.pages[i - 1];
        if (page._position != pos) pages.push(page);
      }

      return pages;
    },

    preloadSurroundingImages: function() {
      var pages = this._getSurroundingPages();

      $.each(
        pages,
        function(i, page) {
          page.preload();
        }.bind(this)
      );
    },

    // preload is a non-abortable preloader,
    // so that it doesn't interfere with our regular load
    preload: function() {
      if (
        this.preloading ||
        this.preloaded ||
        this.view.type != "image" ||
        !this.view.options.preload ||
        this.loaded // page might be loaded before it's preloaded so also stop there
      ) {
        return;
      }

      // make sure the page is created
      this.create();

      this.preloading = true;

      this.preloadReady = new ImageReady(
        this.image[0],
        function(imageReady) {
          // mark this page as loaded, without hiding the spinner
          this.loaded = true;
          _loadedUrlCache[this.view.url] = true;

          this.preloading = false;
          this.preloaded = true;

          this.dimensions = {
            width: imageReady.img.naturalWidth,
            height: imageReady.img.naturalHeight,
          };
        }.bind(this),
        null,
        {
          // have the preload always use naturalWidth,
          // this avoid an extra new Image() request
          method: "naturalWidth",
        }
      );
    },

    // the purpose of load is to set dimensions
    // we use it to set dimensions even for content that doesn't load like youtube
    load: function(callback, isPreload) {
      // make sure the page is created
      this.create();
      // exit early if already loaded
      if (this.loaded) {
        if (callback) callback();
        return;
      }

      // abort possible previous (pre)load
      this.abort();

      // mark as loading
      this.loading = true;

      // start the spinner after waiting for some duration
      if (this.view.options.spinner) {
        // && !_loadedUrlCache[this.view.url]
        this._spinnerDelay = setTimeout(
          function() {
            Spinner.show();
          }.bind(this),
          this.view.options.spinnerDelay || 0
        );
      }

      switch (this.view.type) {
        case "image":
          // if we had an error before just go through
          if (this.error) {
            if (callback) callback();
            return;
          }

          this.imageReady = new ImageReady(
            this.image[0],
            function(imageReady) {
              // mark as loaded
              this._markAsLoaded();

              this.setDimensions({
                width: imageReady.img.naturalWidth,
                height: imageReady.img.naturalHeight,
              });

              if (callback) callback();
            }.bind(this),
            function() {
              // mark as loaded
              this._markAsLoaded();

              this.image.hide();
              this.content.prepend(
                (this.error = $("<div>")
                  .addClass("fr-error fr-content-element")
                  .append($("<div>").addClass("fr-error-icon")))
              );
              this.element.addClass("fr-has-error");

              this.setDimensions({
                width: this.error.outerWidth(),
                height: this.error.outerHeight(),
              });

              // allow resizing
              this.error.css({ width: "100%", height: "100%" });

              if (callback) callback();
            }.bind(this),
            {
              method: this.view.options.loadedMethod,
            }
          );

          break;

        case "vimeo":
          this.vimeoReady = new VimeoReady(
            this.view.url,
            function(data) {
              // mark as loaded
              this._markAsLoaded();

              this.setDimensions({
                width: data.dimensions.width,
                height: data.dimensions.height,
              });

              if (callback) callback();
            }.bind(this)
          );
          break;

        case "youtube":
          // mark as loaded
          this._markAsLoaded();

          this.setDimensions({
            width: this.view.options.width,
            height: this.view.options.height,
          });

          if (callback) callback();
          break;
      }
    },

    // sets dimensions taking maxWidth/Height into account
    setDimensions: function(dimensions) {
      this.dimensions = dimensions;

      if (this.view.options.maxWidth || this.view.options.maxHeight) {
        var opts = this.view.options,
          bounds = {
            width: opts.maxWidth ? opts.maxWidth : this.dimensions.width,
            height: opts.maxHeight ? opts.maxHeight : this.dimensions.height,
          };

        this.dimensions = Fit.within(bounds, this.dimensions);
      }
    },

    // helper for load()
    _markAsLoaded: function() {
      this._abortSpinnerDelay();

      this.loading = false;
      this.loaded = true;

      // mark url as loaded so we can avoid
      // showing the spinner again
      _loadedUrlCache[this.view.url] = true;

      Spinner.hide(null, null, this._position);
    },

    isVideo: function() {
      return Type.isVideo(this.view.type);
    },

    insertVideo: function(callback) {
      // don't insert a video twice
      // and stop if not a video
      if (this.playerIframe || !this.isVideo()) {
        if (callback) callback();
        return;
      }

      var protocol =
        "http" +
        (window.location && window.location.protocol === "https:" ? "s" : "") +
        ":";

      var playerVars = $.extend({}, this.view.options[this.view.type] || {}),
        queryString = $.param(playerVars),
        urls = {
          vimeo: protocol + "//player.vimeo.com/video/{id}?{queryString}",
          youtube: protocol + "//www.youtube.com/embed/{id}?{queryString}",
        },
        src = urls[this.view.type]
          .replace("{id}", this.view._data.id)
          .replace("{queryString}", queryString);

      this.content.prepend(
        (this.playerIframe = $(
          "<iframe webkitAllowFullScreen mozallowfullscreen allowFullScreen>"
        )
          .addClass("fr-content-element")
          .attr({
            src: src,
            height: this._contentDimensions.height,
            width: this._contentDimensions.width,
            frameborder: 0,
          }))
      );

      if (callback) callback();
    },

    raise: function() {
      // no need to raise if we're already the topmost element
      // this helps avoid unnecessary detaching of the element
      var lastChild = Pages.element[0].lastChild;
      if (lastChild && lastChild === this.element[0]) {
        return;
      }

      Pages.element.append(this.element);
    },

    show: function(callback) {
      var shq = this.queues.showhide;
      shq.queue([]); // clear queue

      shq.queue(
        function(next_stopped_inactive) {
          // hide the spinner only if it's visible, and when this page doesn't need loading
          var needsLoading =
            this.view.options.spinner && !_loadedUrlCache[this.view.url];
          if (Spinner._visible && !needsLoading) {
            Spinner.hide();
          }

          Pages.stopInactive();
          next_stopped_inactive();
        }.bind(this)
      );

      // update the mode is something we can do instantly
      shq.queue(
        function(next_updated_UI) {
          this.updateUI(); // first this page
          UI.set(this._ui); // then the window to match
          next_updated_UI();
        }.bind(this)
      );

      // keyboard, enabled here so escape can be pressed on load
      shq.queue(
        function(next_keyboard) {
          Keyboard.enable(this.view.options.keyboard);
          next_keyboard();
        }.bind(this)
      );

      // load
      shq.queue(
        function(next_loaded) {
          // skin spinner
          Spinner.setSkin(this.view.options.skin);

          // load
          this.load(
            function() {
              this.preloadSurroundingImages();
              next_loaded();
            }.bind(this)
          );
        }.bind(this)
      );

      shq.queue(
        function(next_utility) {
          this.raise();

          Window.setSkin(this.view.options.skin);
          UI.enable(); // enable ui controls

          this.fitToBox();

          // adjust to scroll
          Window.adjustToScroll();

          next_utility();
        }.bind(this)
      );

      // vimeo and youtube use this for insertion
      if (this.isVideo()) {
        shq.queue(
          function(next_video_inserted) {
            this.insertVideo(function() {
              next_video_inserted();
            });
          }.bind(this)
        );
      }

      // if we're not syncing, hide other visible pages before this one
      if (!this.view.options.sync) {
        shq.queue(
          function(next_synced) {
            Pages.hideInactive(next_synced);
          }.bind(this)
        );
      }

      shq.queue(
        function(next_shown) {
          var fx = 3,
            duration = this.view.options.effects.content.show;

          // set type on the window
          Window.setShowingType(this.view.type);

          // if this is the first thing we open, we should math the duration
          // of the page opening with that of the window
          if (!Window.visible) {
            duration = this.view.options.effects.window.show;

            if (typeof this.view.options.onShow === "function") {
              this.view.options.onShow.call(Fresco);
            }
          }

          // when syncing, hide other pages while showing this one
          if (this.view.options.sync) {
            fx++;
            Pages.hideInactive(function() {
              if (--fx < 1) next_shown();
            });
          }

          Window.show(function() {
            if (--fx < 1) next_shown();
          }, this.view.options.effects.window.show);

          this._show(function() {
            if (--fx < 1) next_shown();
          }, duration);

          UI.adjustPrevNext(
            function() {
              if (--fx < 1) next_shown();
            },
            Window._first ? 0 : duration
          );

          if (Window._first) {
            UI.show(null, 0);

            // don't show the UI the next time, it'll show up
            // when we set this flag again
            Window._first = false;
          } else {
            //if (this._ui == 'inside')
            UI.show(null, 0);
          }

          // call afterPosition right after starting the _show() but within
          // this queue step so triggers before the animation completes
          var afterPosition = this.view.options.afterPosition;
          if (typeof afterPosition === "function") {
            afterPosition.call(Fresco, this._position);
          }
        }.bind(this)
      );

      shq.queue(
        function(next_set_visible) {
          this._visible = true;

          if (callback) callback();

          next_set_visible();
        }.bind(this)
      );
    },

    _show: function(callback, alternateDuration) {
      var duration = !Window.visible
        ? 0
        : typeof alternateDuration === "number"
        ? alternateDuration
        : this.view.options.effects.content.show;

      this.element
        .stop(true)
        .show()
        .fadeTo(duration || 0, 1, callback);
    },

    hide: function(callback, alternateDuration) {
      if (!this.element) {
        if (callback) callback();
        return; // nothing to hide yet
      }

      this.removeVideo();

      // abort possible loading
      this.abort();

      var duration =
        typeof alternateDuration === "number"
          ? alternateDuration
          : this.view.options.effects.content.hide;

      // hide video instantly
      if (this.isVideo()) duration = 0;

      // stop, delay & effect
      this.element
        .stop(true)
        // we use alternative easing to minize background color showing through
        // a lowered opacity fade while images are trading places
        .fadeTo(
          duration,
          0,
          "frescoEaseInCubic",
          function() {
            this.element.hide();
            this._visible = false;
            Pages.removeTracking(this._position);
            if (callback) callback();
          }.bind(this)
        );
    },

    // stop everything
    stop: function() {
      var shq = this.queues.showhide;
      shq.queue([]); // clear queue

      // stop animations
      if (this.element) this.element.stop(true);

      // stop possible loading
      this.abort();
    },

    removeVideo: function() {
      if (this.playerIframe) {
        // this fixes a bug where sound keep playing after
        // removing the iframe in IE10+
        this.playerIframe[0].src = "//about:blank";

        this.playerIframe.remove();
        this.playerIframe = null;
      }
    },

    remove: function() {
      this.stop();
      this.removeVideo();
      if (this.element) this.element.remove();

      if (this._track) {
        Pages.removeTracking(this._position);
        this._track = false;
      }

      // only cancel preloading on remove
      if (this.preloadReady) {
        this.preloadReady.abort();
        this.preloadReady = null;
        this.preloading = null;
        this.preloaded = null;
      }

      this._visible = false;
      this.removed = true;
    },

    abort: function() {
      // we don't stop preloading when aborting
      if (this.imageReady) {
        this.imageReady.abort();
        this.imageReady = null;
      }

      if (this.vimeoReady) {
        this.vimeoReady.abort();
        this.vimeoReady = null;
      }

      this._abortSpinnerDelay();

      this.loading = false;
      //Window.stopLoading();
    },

    _abortSpinnerDelay: function() {
      if (this._spinnerDelay) {
        clearTimeout(this._spinnerDelay);
        this._spinnerDelay = null;
      }
    },

    _getInfoHeight: function(width) {
      // height is always 0 when no caption
      //if (!this.view.caption) return 0;
      // not measuring height? should this be based on visibility?
      var hasPosition = this.view.options.position && this._total > 1;

      switch (this._ui) {
        case "fullclick":
        case "inside":
          if (!(this.view.caption || hasPosition)) return 0;
          break;
        case "outside":
          if (!this.view.caption) return 0;
          break;
      }

      var info = this._ui === "inside" ? this.infoInside : this.info;

      // make sure width fits within the bounds if we use the outside ui
      if (this._ui === "outside") {
        width = Math.min(width, Window._boxDimensions.width);
      }

      // css('width') return an incorrected parsed value, we need the one on the style attribute
      var height,
        oldWidth = info[0].style.width; //.css('width');
      // always use 100% restore width for inside/fullclick
      if (this._ui === "inside" || this._ui === "fullclick") oldWidth = "100%";

      info.css({ width: width + "px" });

      height = parseFloat(info.outerHeight());

      info.css({ width: oldWidth });

      return height;
    },

    _whileVisible: function(fn, extraElements) {
      var shown = [],
        showElements = Window.element.add(this.element);

      if (extraElements) {
        showElements = showElements.add(extraElements);
      }

      // show hidden elements and track which elements got shown
      $.each(showElements, function(i, element) {
        var visible = $(element).is(":visible");

        if (!visible) {
          shown.push($(element).show());
        }
      });

      // need to measure assuming we have a caption, otherwise info.caption will flip between true/false
      // because this class could have been applied at some point
      // having a caption also means these classes are set a certain way, restore them later
      var has_nocap = this.element.hasClass("fr-no-caption");
      this.element.removeClass("fr-no-caption");
      var has_hascap = this.element.hasClass("fr-has-caption");
      this.element.addClass("fr-has-caption");

      // prevent fragments
      Window.element.css({ visibility: "hidden" });

      fn(); // run some code

      // restore visibility
      Window.element.css({ visibility: "visible" });

      // restore classes
      if (has_nocap) this.element.addClass("fr-no-caption");
      if (!has_hascap) this.element.removeClass("fr-has-caption");

      $.each(shown, function(i, element) {
        element.hide();
      });
    },

    // this keeps tracks of flags set in CSS to force certain options
    // like fullclick or no-overlap
    updateForced: function() {
      this.create(); // make sure the element is created

      this._fullClick = this.view.options.fullClick;
      this._noOverflow = false;
      if (parseInt(this.element.css("min-width")) > 0) this._fullClick = true;
      if (parseInt(this.element.css("min-height")) > 0) this._noOverflow = true;
    },

    updateUI: function() {
      // check if we have forced properties
      this.updateForced();

      var ui = this._fullClick ? "fullclick" : this.view.options.ui;

      if (this._ui) this.element.removeClass("fr-ui-" + this._ui);
      this.element.addClass("fr-ui-" + ui);

      this._ui = ui;
    },

    fitToBox: function() {
      // if we have no content, no need to update further
      if (!this.content) return;

      var page = this.element,
        bounds = $.extend({}, Window.getBoxDimensions()),
        dimensions = $.extend({}, this.dimensions),
        container = this.container;

      // need to know which mode we are in before continuing
      this.updateUI();

      var padding = {
        left: parseInt(container.css("padding-left")),
        top: parseInt(container.css("padding-top")),
      };

      // if the ui is outside and we're showing a position, it might be larger then left padding
      // so increase it if that's the case
      if (this._ui === "outside" && this._positionOutside) {
        var positionWidth = 0;

        this._whileVisible(
          function() {
            if (this._positionOutside.is(":visible")) {
              positionWidth = this._positionOutside.outerWidth(true);
            }
          }.bind(this)
        );

        if (positionWidth > padding.left) {
          padding.left = positionWidth;
        }
      }

      bounds.width -= 2 * padding.left;
      bounds.height -= 2 * padding.top;

      var fitOptions = {
        width: true,
        height: this._noOverflow ? true : !this.view.options.overflow.y,
      };

      var fitted = Fit.within(bounds, dimensions, fitOptions),
        contentDimensions = $.extend({}, fitted),
        content = this.content,
        infoHeight = 0,
        backgroundDimensions;

      var isInside = this._ui === "inside",
        info = isInside ? this.infoInside : this.info,
        caption = isInside ? this.captionInside : this.caption,
        pos = isInside ? this.posInside : this.pos;

      var infoCaption = !!caption;
      var extraShowElements;

      // set the max-height on the info window so the height calculation takes it into account
      //this.setInfoMaxHeight();

      switch (this._ui) {
        case "outside":
          var preScale = $.extend({}, contentDimensions);

          if (this.caption) {
            extraShowElements = this.caption;

            this._whileVisible(
              function() {
                var count = 0,
                  attempts = 2;

                while (count < attempts) {
                  infoHeight = this._getInfoHeight(contentDimensions.width);

                  var spaceBottom = bounds.height - contentDimensions.height;
                  if (spaceBottom < infoHeight) {
                    contentDimensions = Fit.within(
                      {
                        width: contentDimensions.width,
                        height: Math.max(
                          contentDimensions.height - (infoHeight - spaceBottom),
                          0
                        ), // prevents NaN
                      },
                      contentDimensions,
                      fitOptions
                    );
                  }

                  count++;
                }

                // grab info_height one final time
                infoHeight = this._getInfoHeight(contentDimensions.width);
                var infoShowLimit = 0.5; //this.view.options.infoShowLimit;

                if (
                  // too much overflow after resizing the info box to the content area
                  (!this.view.options.overflow.y &&
                    infoHeight + contentDimensions.height > bounds.height) ||
                  // caption disabled through css
                  (info && info.css("display") === "none") ||
                  // info height is >= 50% of image height
                  (infoShowLimit &&
                    infoHeight >= infoShowLimit * contentDimensions.height)
                ) {
                  // info is almost a square compared to the image width
                  infoCaption = false;
                  infoHeight = 0;
                  contentDimensions = preScale;
                }
              }.bind(this),
              extraShowElements
            );
          } // end caption

          if (info) {
            info.css({
              width: contentDimensions.width + "px",
            });
          }

          backgroundDimensions = {
            width: contentDimensions.width,
            height: contentDimensions.height + infoHeight,
          };

          break;

        case "inside":
          if (this.caption) {
            // store elements
            extraShowElements = caption;

            this._whileVisible(
              function() {
                infoHeight = this._getInfoHeight(contentDimensions.width);
                var infoShowLimit = 0.45;

                // hide caption if it is too large
                if (
                  infoShowLimit &&
                  infoHeight >= infoShowLimit * contentDimensions.height
                ) {
                  infoCaption = false;
                  infoHeight = 0;
                }
              }.bind(this),
              extraShowElements
            );
          }

          backgroundDimensions = contentDimensions;

          break;

        case "fullclick":
          var extraElements = [];

          if (caption) {
            extraElements.push(caption);
          }

          this._whileVisible(
            function() {
              // make the caption 100% width
              if (caption || pos) {
                info.css({ width: "100%" });
              }

              infoHeight = this._getInfoHeight(Window._boxDimensions.width);

              if (caption) {
                if (infoHeight > bounds.height * 0.5) {
                  infoCaption = false;

                  if (pos) {
                    var c_disp = this.caption.is(":visible");
                    this.caption.hide();
                    infoHeight = this._getInfoHeight(
                      Window._boxDimensions.width
                    );
                    if (c_disp) this.caption.show();
                  } else {
                    infoHeight = 0;
                  }
                }
              }

              contentDimensions = Fit.within(
                {
                  width: bounds.width,
                  height: Math.max(0, bounds.height - infoHeight),
                },
                contentDimensions,
                fitOptions
              );

              backgroundDimensions = contentDimensions;
            }.bind(this),
            extraElements
          );

          // always show the caption if we have a position
          // at some point to fix something, look into this.
          /*if (pos) {
          infoCaption = true;
        }*/

          // remove possible padding on content added by ui:outside
          this.content.css({ "padding-bottom": 0 });

          break;
      }

      // show/hide caption and mark its visibility with a class
      if (caption) {
        caption[infoCaption ? "show" : "hide"]();
      }
      this.element[(!infoCaption ? "add" : "remove") + "Class"](
        "fr-no-caption"
      );
      this.element[(!infoCaption ? "remove" : "add") + "Class"](
        "fr-has-caption"
      );

      this.content.css(contentDimensions);
      this.background.css(backgroundDimensions);

      // on iframes we force dimensions
      if (this.playerIframe) {
        this.playerIframe.attr(contentDimensions);
      }

      // Overlap, and tracking start/stop based on dimensions
      this.overlap = {
        y:
          backgroundDimensions.height +
          (this._ui === "fullclick" ? infoHeight : 0) -
          Window._boxDimensions.height,
        x: 0,
      };

      this._track =
        !this._noOverflow && this.view.options.overflow.y && this.overlap.y > 0;

      // store some values for later use in .position()
      this._infoHeight = infoHeight;
      this._padding = padding;
      this._contentDimensions = contentDimensions;
      this._backgroundDimensions = backgroundDimensions;

      Pages[(this._track ? "set" : "remove") + "Tracking"](this._position);

      this.position();
    },

    position: function() {
      // exit early if we have nothing to position
      if (!this.content) return;

      var contentDimensions = this._contentDimensions,
        backgroundDimensions = this._backgroundDimensions;

      // figure out top/left of the content
      var contentPosition = {
        top:
          Window._boxDimensions.height * 0.5 -
          backgroundDimensions.height * 0.5,
        left:
          Window._boxDimensions.width * 0.5 - backgroundDimensions.width * 0.5,
      };
      var infoPosition = {
        top: contentPosition.top + contentDimensions.height,
        left: contentPosition.left,
      };
      var containerBottom = 0;

      var info = this._ui === "inside" ? this.infoInside : this.info;

      switch (this._ui) {
        case "fullclick":
          contentPosition.top =
            (Window._boxDimensions.height - this._infoHeight) * 0.5 -
            backgroundDimensions.height * 0.5;

          infoPosition = {
            top: Window._boxDimensions.height - this._infoHeight,
            left: 0,
            bottom: "auto",
          };

          containerBottom = this._infoHeight;
          break;

        case "inside":
          // we have to do some resets when switching between inside and fullclick
          infoPosition = {
            top: "auto",
            left: 0,
            bottom: 0,
          };

          break;
      }

      // overlap requires further modification
      if (this.overlap.y > 0) {
        var xyp = Pages.getXYP();

        contentPosition.top = 0 - xyp.y * this.overlap.y;

        switch (this._ui) {
          case "outside":
          case "fullclick":
            infoPosition.top = Window._boxDimensions.height - this._infoHeight;
            break;

          case "inside":
            var bottom =
              contentPosition.top +
              contentDimensions.height -
              Window._boxDimensions.height;
            var top = -1 * contentPosition.top;

            // info
            infoPosition.bottom = bottom;

            // close X
            this.closeInside.css({
              top: top,
            });

            if (this._total > 1) {
              // < >
              var windowVisible = Window.element.is(":visible");
              if (!windowVisible) Window.element.show();

              var pRestoreStyle = this.previousInside.attr("style");
              this.previousInside.removeAttr("style");
              var pnMarginTop = parseInt(this.previousInside.css("margin-top")); // the original margin top
              this.previousInside.attr({ style: pRestoreStyle });

              if (!windowVisible) Window.element.hide();

              var buttons = this.previousInside.add(this.nextInside),
                center = this.overlap.y * 0.5;

              buttons.css({
                "margin-top": pnMarginTop + (top - center),
              });

              // position inside
              if (this.positionInside) {
                this.positionInside.css({ bottom: bottom });
              }
            }

            break;
        }
      } else {
        // we have to remove offset from the inner elements possible set by overlap
        if (this._ui === "inside") {
          this.element
            .find(".fr-info, .fr-side, .fr-close, .fr-position-inside")
            .removeAttr("style");
        }
      }

      if (info) info.css(infoPosition);
      this.container.css({ bottom: containerBottom });
      this.content.css(contentPosition);
      this.background.css(contentPosition);
    },
  });

  return Page;
})();

var Pages = {
  initialize: function(element) {
    this.element = element;
    this.pages = [];
    this.uid = 1;
    this._tracking = [];
  },

  load: function(views) {
    this.views = views;

    // remove all inactive page groups
    this.removeAll();

    // add pages for all these views
    $.each(
      views,
      function(i, view) {
        this.pages.push(new Page(view, i + 1, this.views.length));
      }.bind(this)
    );
  },

  show: function(position, callback) {
    var page = this.pages[position - 1];

    // never try to reload the exact same frame
    if (this.page && this.page.uid === page.uid) {
      return;
    }

    // update the page
    this.page = page;

    // reposition thumbnails
    Thumbnails.show(position);

    Window.updateBoxDimensions(); // these are based on Thumbnails, so after thumbnails

    page.show(
      function() {
        if (callback) callback();
      }.bind(this)
    );
  },

  // used by the API when opening
  // checks if the page is in the currently open group
  getPositionInActivePageGroup: function(element) {
    var position = 0;

    $.each(this.pages, function(i, page) {
      if (page.view.element && page.view.element === element) {
        position = i + 1;
      }
    });

    return position;
  },

  getLoadingCount: function() {
    // we only stop loading if all the frames we have are not loading anymore
    var count = 0;
    $.each(this.pages, function(i, page) {
      if (page.loading) count++;
    });
    return count;
  },

  // Window.hide will call thise when fully closed
  removeAll: function() {
    $.each(this.pages, function(i, page) {
      page.remove();
    });

    this.pages = [];
  },

  hideInactive: function(callback, alternateDuration) {
    var _pages = [];

    $.each(
      this.pages,
      function(i, page) {
        if (page.uid !== this.page.uid) {
          _pages.push(page);
        }
      }.bind(this)
    );

    var fx = 0 + _pages.length;

    if (fx < 1) {
      if (callback) callback();
    } else {
      $.each(_pages, function(i, page) {
        page.hide(function() {
          if (callback && --fx < 1) callback();
        }, alternateDuration);
      });
    }

    return _pages.length;
  },

  stopInactive: function() {
    $.each(
      this.pages,
      function(i, page) {
        if (page.uid !== this.page.uid /* && !page.preloading*/) {
          page.stop();
        }
      }.bind(this)
    );
  },

  stop: function() {
    $.each(this.pages, function(i, page) {
      page.stop();
    });
  },

  // Tracking
  /* Tracking x/y */
  handleTracking: function(event) {
    if (Browser.IE && Browser.IE < 9) {
      this.setXY({ x: event.pageX, y: event.pageY });
      this.updatePositions();
    } else {
      this._tracking_timer = setTimeout(
        function() {
          this.setXY({ x: event.pageX, y: event.pageY });
          this.updatePositions();
        }.bind(this),
        30
      );
    }
  },

  clearTrackingTimer: function() {
    if (this._tracking_timer) {
      clearTimeout(this._tracking_timer);
      this._tracking_timer = null;
    }
  },

  startTracking: function() {
    if (Support.mobileTouch || this._handleTracking) return;
    // we observe the document so that even when the page is
    // still loading and the window isn't in view we can still
    // shift x/y positions to get a correct position after load.
    $(document.documentElement).on(
      "mousemove",
      (this._handleTracking = this.handleTracking.bind(this))
    );
  },

  stopTracking: function() {
    if (Support.mobileTouch || !this._handleTracking) return;
    $(document.documentElement).off("mousemove", this._handleTracking);
    this._handleTracking = null;
    this.clearTrackingTimer();
  },

  setTracking: function(position) {
    if (!this.isTracking(position)) {
      this._tracking.push(this.pages[position - 1]);

      if (this._tracking.length === 1) this.startTracking();
    }
  },

  clearTracking: function() {
    this._tracking = [];
  },

  removeTracking: function(position) {
    this._tracking = $.grep(this._tracking, function(page) {
      return page._position !== position;
    });

    if (this._tracking.length < 1) this.stopTracking();
  },

  isTracking: function(position) {
    var found = false;
    $.each(this._tracking, function(i, page) {
      if (page._position === position) {
        found = true;
        return false;
      }
    });
    return found;
  },

  // the tracking itself
  // Pointer %
  setXY: function(xy) {
    this._xy = xy;
  },

  getXYP: function() {
    var page = Pages.page;
    var dimensions = $.extend({}, Window._boxDimensions);
    var xy = $.extend({}, this._xy);

    // subtract scroll
    xy.y -= $(window).scrollTop();

    if (
      page &&
      (page._ui === "outside" || page._ui === "fullclick") &&
      page._infoHeight > 0
    ) {
      dimensions.height -= page._infoHeight;
    }

    xy.y -= Window._boxPosition.top;

    // BUG: in Chrome, events can trigger past the range of the browser
    // window causing incorrect offset, when moving onto the developer tools
    // for example, haven't found a workaround for this yet.

    var xyp = {
      //x: Math.min(Math.max(xy.x / dimensions.width, 0), 1),
      x: 0,
      y: Math.min(Math.max(xy.y / dimensions.height, 0), 1),
    };

    // safety should be a percentage
    var safetyPX = 20,
      wh = { x: "width", y: "height" },
      safety = {};

    $.each(
      "y".split(" "),
      function(i, z) {
        // safety should be a percentage, so convert pixel to %
        safety[z] = Math.min(Math.max(safetyPX / dimensions[wh[z]], 0), 1);

        // now convert
        xyp[z] *= 1 + 2 * safety[z]; // increase the range by 2*%
        xyp[z] -= safety[z]; // shift back by %
        xyp[z] = Math.min(Math.max(xyp[z], 0), 1); // chop of the sides
      }.bind(this)
    );

    this.setXYP(xyp);

    return this._xyp;
  },

  setXYP: function(xyp) {
    this._xyp = xyp;
  },

  // update all page positions
  updatePositions: function() {
    if (this._tracking.length < 1) return;

    $.each(this._tracking, function(i, page) {
      page.position();
    });
  },
};

function View() {
  this.initialize.apply(this, _slice.call(arguments));
}
$.extend(View.prototype, {
  initialize: function(object) {
    var options = arguments[1] || {},
      data = {};

    // string -> element
    if (typeof object === "string") {
      // turn the string into an element
      object = { url: object };
    }

    // element -> object
    else if (object && object.nodeType === 1) {
      var element = $(object);

      object = {
        element: element[0],
        url: element.attr("href"),
        caption: element.attr("data-fresco-caption"),
        group: element.attr("data-fresco-group"),
        extension: element.attr("data-fresco-extension"),
        type: element.attr("data-fresco-type"),
        options:
          (element.attr("data-fresco-options") &&
            eval("({" + element.attr("data-fresco-options") + "})")) ||
          {},
      };
    }

    if (object) {
      // detect type if none is set
      if (!object.extension) {
        object.extension = detectExtension(object.url);
      }

      if (!object.type) {
        data = getURIData(object.url);
        object._data = data;
        object.type = data.type;
      }
    }

    if (!object._data) {
      object._data = getURIData(object.url);
    }

    if (object && object.options) {
      object.options = $.extend(
        true,
        $.extend({}, options),
        $.extend({}, object.options)
      );
    } else {
      object.options = $.extend({}, options);
    }
    // extend the options
    object.options = Options.create(object.options, object.type, object._data);

    // extend this with data
    $.extend(this, object);

    return this;
  },
});

// Spinner
// a pure CSS based spinner
var Spinner = {
  // mark as supported
  supported: Support.css.transform && Support.css.animation,

  initialize: function(element) {
    this.element = $("<div>")
      .addClass("fr-spinner")
      .hide();

    for (var i = 1; i <= 12; i++) {
      this.element.append($("<div>").addClass("fr-spin-" + i));
    }

    this.element.on(
      "click",
      function() {
        Window.hide();
      }.bind(this)
    );

    // prevent mousewheel scroll
    this.element.on("fresco:mousewheel", function(event) {
      event.preventDefault();
    });
  },

  setSkin: function(skin) {
    if (!this.supported) return;

    if (this._skin) {
      this.element.removeClass("fr-spinner-skin-" + this._skin);
    }

    // store dimensions, this avoids having to recalculate it every time we center()
    this.updateDimensions();

    this.element.addClass("fr-spinner-skin-" + skin);
    this._skin = skin;
  },

  updateDimensions: function() {
    // need to be attached to measure dimensions
    var attached = this._attached;
    if (!attached) this.attach();

    this._dimensions = {
      width: this.element.outerWidth(),
      height: this.element.outerHeight(),
    };

    if (!attached) this.detach();
  },

  attach: function() {
    if (this._attached) return;
    $(document.body).append(this.element);
    this._attached = true;
  },

  detach: function() {
    if (!this._attached) return;
    this.element.detach();
    this._attached = false;
  },

  show: function(callback, alternateDuration) {
    this._visible = true;

    this.attach();
    this.center();

    var pDuration =
        (Pages.page && Pages.page.view.options.effects.spinner.show) || 0,
      duration =
        (typeof alternateDuration === "number"
          ? alternateDuration
          : pDuration) || 0;

    this.element.stop(true).fadeTo(duration, 1, callback);
  },

  hide: function(callback, alternateDuration, position) {
    this._visible = false;

    var pDuration =
        (Pages.page && Pages.page.view.options.effects.spinner.hide) || 0,
      duration =
        (typeof alternateDuration === "number"
          ? alternateDuration
          : pDuration) || 0;

    this.element.stop(true).fadeOut(
      duration || 0,
      function() {
        this.detach();
        if (callback) callback();
      }.bind(this)
    );
  },

  // center on the current page
  center: function() {
    if (!this.supported) return;

    // make sure dimensions are set
    if (!this._dimensions) this.updateDimensions();

    // for ui:fullclick we can already figure out the info height
    // before the content is loaded and center accordingly
    var page = Pages.page,
      iH = 0;
    if (page && page._ui === "fullclick") {
      page._whileVisible(function() {
        iH = page._getInfoHeight(Window._boxDimensions.width);
      });
    }

    this.element.css({
      top:
        Window._boxPosition.top +
        Window._boxDimensions.height * 0.5 -
        this._dimensions.height * 0.5 -
        iH * 0.5,
      left:
        Window._boxPosition.left +
        Window._boxDimensions.width * 0.5 -
        this._dimensions.width * 0.5,
    });
  },
};

// API

// an unexposed object for internal use
var _Fresco = {
  _disabled: false,
  _fallback: true,

  initialize: function() {
    Window.initialize();
    if (!this._disabled) this.startDelegating();
  },

  // click delegation
  startDelegating: function() {
    if (this._delegateHandler) return;

    $(document.documentElement)
      .on(
        "click",
        ".fresco[href]",
        (this._delegateHandler = this.delegate.bind(this))
      )
      // observe document clicks for XY setting, this makes sure that
      // positioning is correct when opening overflow with the API
      .on("click", (this._setClickXYHandler = this.setClickXY.bind(this)));
  },

  stopDelegating: function() {
    if (!this._delegateHandler) return;

    $(document.documentElement)
      .off("click", ".fresco[href]", this._delegateHandler)
      .off("click", this._setClickXYHandler);

    this._setClickXYHandler = null;
    this._delegateHandler = null;
  },

  setClickXY: function(event) {
    Pages.setXY({
      x: event.pageX,
      y: event.pageY,
    });
  },

  delegate: function(event) {
    if (this._disabled) return;

    event.stopPropagation();
    event.preventDefault();

    var element = event.currentTarget;

    this.setClickXY(event);

    _Fresco.show(element);
  },

  show: function(object) {
    if (this._disabled) {
      this.showFallback.apply(_Fresco, _slice.call(arguments));
      return;
    }

    var options = arguments[1] || {},
      position = arguments[2];

    if (arguments[1] && typeof arguments[1] === "number") {
      position = arguments[1];
      options = {};
    }

    var views = [],
      object_type,
      isElement = _.isElement(object);

    switch ((object_type = typeof object)) {
      case "string":
      case "object":
        var view = new View(object, options),
          _dgo = "data-fresco-group-options",
          groupOptions = {};

        if (view.group) {
          // extend the entire group

          // if we have an element, look for other elements
          if (isElement) {
            var elements = $(
              '.fresco[data-fresco-group="' +
                $(object).attr("data-fresco-group") +
                '"]'
            );

            // find possible group options
            elements.filter("[" + _dgo + "]").each(function(i, element) {
              $.extend(
                groupOptions,
                eval("({" + ($(element).attr(_dgo) || "") + "})")
              );
            });

            elements.each(function(i, element) {
              // adjust the position if we find that the given object position
              if (!position && element === object) position = i + 1;
              views.push(
                new View(element, $.extend({}, groupOptions, options))
              );
            });
          }
        } else {
          if (isElement && $(object).is("[" + _dgo + "]")) {
            $.extend(
              groupOptions,
              eval("({" + ($(object).attr(_dgo) || "") + "})")
            );
            // reset the view with group options applied
            view = new View(object, $.extend({}, groupOptions, options));
          }

          views.push(view);
        }
        break;

      case "array":
        $.each(object, function(i, item) {
          var view = new View(item, options);
          views.push(view);
        });
        break;
    }

    // grouped settings
    var groupExtend = { grouped: { caption: false } },
      firstUI = views[0].options.ui;

    $.each(views, function(i, view) {
      // at least one view in the group has a caption
      if (view.caption) {
        groupExtend.grouped.caption = true;
      }

      // make sure all items in a group have the same ui
      if (i > 0 && view.options.ui !== firstUI) {
        view.options.ui = firstUI;
      }
    });
    // put the grouped settings on every view
    $.each(views, function(i, view) {
      view = $.extend(view, groupExtend);
    });

    // if we haven't found a position by now, load the first view
    if (!position || position < 1) {
      position = 1;
    }
    if (position > views.length) position = views.length;

    // if we've clicked an element, search for it in the currently open pagegroup
    var positionInAPG;
    if (
      isElement &&
      (positionInAPG = Pages.getPositionInActivePageGroup(object))
    ) {
      Window.setPosition(positionInAPG);
    } else {
      // otherwise start loading and open
      Window.load(views, position);
    }
  },

  showFallback: (function() {
    function getUrl(object) {
      var url,
        type = typeof object;

      if (type === "string") {
        url = object;
      } else if (type === "array" && object[0]) {
        url = getUrl(object[0]);
      } else if (_.isElement(object) && $(object).attr("href")) {
        url = $(object).attr("href");
      } else if (object.url) {
        url = object.url;
      } else {
        url = false;
      }

      return url;
    }

    return function(object) {
      if (!this._fallback) return;
      var url = getUrl(object);
      if (url) window.location.href = url;
    };
  })(),
};

$.extend(Fresco, {
  show: function(object) {
    _Fresco.show.apply(_Fresco, _slice.call(arguments));
    return this;
  },

  hide: function() {
    Window.hide();
    return this;
  },

  disable: function() {
    _Fresco.stopDelegating();
    _Fresco._disabled = true;
    return this;
  },

  enable: function() {
    _Fresco._disabled = false;
    _Fresco.startDelegating();
    return this;
  },

  fallback: function(fallback) {
    _Fresco._fallback = fallback;
    return this;
  },

  setDefaultSkin: function(skin) {
    Options.defaults.skin = skin;
    return this;
  },
});

// fallback for old browsers without full position:fixed support
if (
  // IE6
  (Browser.IE && Browser.IE < 7) ||
  // old Android
  // added a version check because Firefox on Android doesn't have a
  // version number above 4.2 anymore
  (typeof Browser.Android === "number" && Browser.Android < 3) ||
  // old WebKit
  (Browser.MobileSafari &&
    (typeof Browser.WebKit === "number" && Browser.WebKit < 533.18))
) {
  // we'll reset the show function
  _Fresco.show = _Fresco.showFallback;
}

// Thumbnails
var Thumbnails = {
  initialize: function(element) {
    this.element = element;

    this._thumbnails = [];
    this._orientation = "vertical";

    this._vars = {
      thumbnail: {},
      thumbnailFrame: {},
      thumbnails: {},
    };

    this.build();

    this.startObserving();
  },

  build: function() {
    this.element.append(
      (this.wrapper = $("<div>")
        .addClass("fr-thumbnails-wrapper")
        .append(
          (this._slider = $("<div>")
            .addClass("fr-thumbnails-slider")
            .append(
              (this._previous = $("<div>")
                .addClass("fr-thumbnails-side fr-thumbnails-side-previous")
                .append(
                  (this._previous_button = $("<div>")
                    .addClass("fr-thumbnails-side-button")
                    .append(
                      $("<div>").addClass(
                        "fr-thumbnails-side-button-background"
                      )
                    )
                    .append(
                      $("<div>").addClass("fr-thumbnails-side-button-icon")
                    ))
                ))
            )
            .append(
              (this._thumbs = $("<div>")
                .addClass("fr-thumbnails-thumbs")
                .append(
                  (this._slide = $("<div>").addClass("fr-thumbnails-slide"))
                ))
            )

            .append(
              (this._next = $("<div>")
                .addClass("fr-thumbnails-side fr-thumbnails-side-next")
                .append(
                  (this._next_button = $("<div>")
                    .addClass("fr-thumbnails-side-button")
                    .append(
                      $("<div>").addClass(
                        "fr-thumbnails-side-button-background"
                      )
                    )
                    .append(
                      $("<div>").addClass("fr-thumbnails-side-button-icon")
                    ))
                ))
            ))
        ))
    );
  },

  startObserving: function() {
    this._slider.delegate(
      ".fr-thumbnail",
      "click",
      function(event) {
        event.stopPropagation();

        var thumbnail = $(event.target).closest(".fr-thumbnail")[0];
        var position = thumbnail && $(thumbnail).data("fr-position");

        if (position) {
          this.setActive(position);
          Window.setPosition(position);
        }
      }.bind(this)
    );

    // prevent bubbling on slider click, so you can safely click next to a thumbnail
    this._slider.bind("click", function(event) {
      event.stopPropagation();
    });

    // previous / next
    this._previous.bind("click", this.previousPage.bind(this));
    this._next.bind("click", this.nextPage.bind(this));
  },

  load: function(views) {
    // first clear out any previous thumbnails
    this.clear();

    // set orientation
    // it's always horizontal unless 1 view has vertical
    var orientation = "horizontal",
      disabled = false;
    $.each(
      views,
      function(i, view) {
        if (view.options.thumbnails === "vertical") {
          orientation = "vertical";
        }
        if (!view.options.thumbnails) disabled = true;
      }.bind(this)
    );
    this.setOrientation(orientation);
    this._disabledGroup = disabled;

    $.each(
      views,
      function(i, view) {
        this._thumbnails.push(new Thumbnail(view, i + 1));
      }.bind(this)
    );

    this.fitToViewport();
  },

  clear: function() {
    $.each(this._thumbnails, function(i, thumbnail) {
      thumbnail.remove();
    });

    this._thumbnails = [];
    this._position = -1;
    this._page = -1;
  },

  setOrientation: function(orientation) {
    if (this._orientation) {
      Window.element.removeClass("fr-thumbnails-" + this._orientation);
    }
    Window.element.addClass("fr-thumbnails-" + orientation);

    this._orientation = orientation;
  },

  // disable / enable
  disable: function() {
    Window.element
      .removeClass("fr-thumbnails-enabled")
      .addClass("fr-thumbnails-disabled");
    this._disabled = true;
  },
  enable: function() {
    Window.element
      .removeClass("fr-thumbnails-disabled")
      .addClass("fr-thumbnails-enabled");
    this._disabled = false;
  },
  enabled: function() {
    return !this._disabled;
  },
  disabled: function() {
    return this._disabled;
  },

  // update current dimension variables
  updateVars: function() {
    var win = Window.element,
      vars = this._vars,
      orientation = this._orientation,
      isHorizontal = orientation === "horizontal",
      _top = isHorizontal ? "top" : "left",
      _left = isHorizontal ? "left" : "top",
      _mbottom = isHorizontal ? "bottom" : "left",
      _mtop = isHorizontal ? "top" : "right",
      _width = isHorizontal ? "width" : "height",
      _height = isHorizontal ? "height" : "width",
      _swapZ = { left: "right", right: "left", top: "bottom", bottom: "top" };

    this.element.removeClass("fr-thumbnails-measured");
    var w_vis = win.is(":visible");
    if (!w_vis) win.show();

    // we have to observe visibility without having the disabled class on the window
    if (this.disabled()) this.enable();

    // exit early if we're not showing thumbnails
    if (
      !this.element.is(":visible") ||
      this._thumbnails.length < 2 ||
      this._disabledGroup
    ) {
      this.disable();

      // store some variables so getDimensions at least has these
      $.extend(this._vars.thumbnails, { width: 0, height: 0 });

      // show the window again
      if (!w_vis) win.hide();
      this.element.addClass("fr-thumbnails-measured");

      // exit early
      return;
    } else {
      // otherwise enable and continue filling variables
      this.enable();
    }

    var previous = this._previous,
      next = this._next,
      viewport = Bounds.viewport();

    // NOTE: All dimensions are stored as if the orientation is horizontal.
    // base the height of the thumbnail on the height of the element, minus padding
    var height = this.element["inner" + _.String.capitalize(_height)](),
      paddingTop = parseInt(this._thumbs.css("padding-" + _top)) || 0,
      thumbnailHeight = Math.max(height - paddingTop * 2, 0),
      paddingLeft = parseInt(this._thumbs.css("padding-" + _left)) || 0,
      marginTotal =
        (parseInt(this.element.css("margin-" + _mbottom)) || 0) +
        (parseInt(this.element.css("margin-" + _mtop)) || 0);

    $.extend(vars.thumbnails, {
      height: height + marginTotal, // we store as z just to make dimensioning later easier
      width: viewport[isHorizontal ? "width" : "height"],
      paddingTop: paddingTop,
    });

    $.extend(vars.thumbnail, {
      height: thumbnailHeight,
      width: thumbnailHeight,
    });

    $.extend(vars.thumbnailFrame, {
      width: thumbnailHeight + paddingLeft * 2,
      height: height,
    });

    // previous/next
    vars.sides = {
      previous: {
        width: next["inner" + _.String.capitalize(_width)](),
        marginLeft: parseInt(previous.css("margin-" + _left)) || 0, // left
        marginRight: parseInt(previous.css("margin-" + _swapZ[_left])) || 0, // right
      },
      next: {
        width: next["inner" + _.String.capitalize(_width)](),
        marginLeft: parseInt(next.css("margin-" + _left)) || 0, // left
        marginRight: parseInt(next.css("margin-" + _swapZ[_left])) || 0, // right
      },
    };

    // how many pages and ipp
    // first try to fit all the thu
    var viewportWidth = viewport[_width], //$(window).width()
      thumbnailOuterWidth = vars.thumbnailFrame.width,
      thumbs = this._thumbnails.length;

    vars.thumbnails.width = viewportWidth;

    vars.sides.enabled = (thumbs * thumbnailOuterWidth) / viewportWidth > 1;

    // disable the sides if we have only 1 thumbnail
    var thumbsWidth = viewportWidth,
      vs = vars.sides,
      vsPrevious = vs.previous,
      vsNext = vs.next,
      sidesWidth =
        vsPrevious.marginLeft +
        vsPrevious.width +
        vsPrevious.marginRight +
        vsNext.marginLeft +
        vsNext.width +
        vsNext.marginRight;

    if (vars.sides.enabled) {
      thumbsWidth -= sidesWidth;
    }

    // lower the thumbnail width to a factor of a thumbnail
    thumbsWidth =
      Math.floor(thumbsWidth / thumbnailOuterWidth) * thumbnailOuterWidth;

    var totalThumbsWidth = thumbs * thumbnailOuterWidth;
    if (totalThumbsWidth < thumbsWidth) {
      thumbsWidth = totalThumbsWidth;
    }

    // now adust the wrapper to match that size
    var wrapperWidth = thumbsWidth + (vars.sides.enabled ? sidesWidth : 0);

    // items per page
    vars.ipp = Math.round(thumbsWidth / thumbnailOuterWidth); //Math.max(thumbsWidth / tw, 1); // at least one

    this._mode = "page";
    if (vars.ipp <= 1) {
      // recalculate width
      thumbsWidth = viewportWidth;
      wrapperWidth = viewportWidth;
      vars.sides.enabled = false;
      this._mode = "center";
    }

    // now find out the pages
    vars.pages = Math.ceil((thumbs * thumbnailOuterWidth) / thumbsWidth);

    vars.wrapper = {
      width: wrapperWidth + 1, // IE fix
      height: height,
    };

    vars.thumbs = {
      width: thumbsWidth,
      height: height,
    };

    vars.slide = {
      width: thumbs * thumbnailOuterWidth + 1, // IE fix
      height: height,
    };

    if (!w_vis) win.hide();
    this.element.addClass("fr-thumbnails-measured");
  },

  hide: function() {
    this.disable();
    this.thumbnails.hide();
    this._visible = false;
  },

  getDimensions: function() {
    var isHorizontal = this._orientation === "horizontal";

    return {
      width: isHorizontal
        ? this._vars.thumbnails.width
        : this._vars.thumbnails.height,
      height: isHorizontal
        ? this._vars.thumbnails.height
        : this._vars.thumbnails.width,
    };
  },

  // resize
  fitToViewport: function() {
    // make sure vars are set so we can use them
    this.updateVars();
    if (this.disabled()) return;

    var vars = $.extend({}, this._vars),
      isHorizontal = this._orientation === "horizontal";

    // individual thumbnails
    $.each(this._thumbnails, function(i, thumbnail) {
      thumbnail.resize();
    });

    // show hide sides
    this._previous[vars.sides.enabled ? "show" : "hide"]();
    this._next[vars.sides.enabled ? "show" : "hide"]();

    this._thumbs.css({
      width: vars.thumbs[isHorizontal ? "width" : "height"],
      height: vars.thumbs[isHorizontal ? "height" : "width"],
    });

    this._slide.css({
      width: vars.slide[isHorizontal ? "width" : "height"],
      height: vars.slide[isHorizontal ? "height" : "width"],
    });

    var wrapperCSS = {
      width: vars.wrapper[isHorizontal ? "width" : "height"],
      height: vars.wrapper[isHorizontal ? "height" : "width"],
    };
    wrapperCSS["margin-" + (isHorizontal ? "left" : "top")] =
      Math.round(-0.5 * vars.wrapper.width) + "px";
    wrapperCSS["margin-" + (!isHorizontal ? "left" : "top")] = 0;
    this.wrapper.css(wrapperCSS);

    // move to the correct position instantly
    if (this._position) {
      this.moveTo(this._position, true);
    }
  },

  moveToPage: function(page) {
    if (page < 1 || page > this._vars.pages || page === this._page) return;

    var position = this._vars.ipp * (page - 1) + 1;

    this.moveTo(position);
  },

  previousPage: function() {
    this.moveToPage(this._page - 1);
  },

  nextPage: function() {
    this.moveToPage(this._page + 1);
  },

  show: function(position) {
    // move instantly when the position wasn't set before
    var instant = this._position < 0;

    // make sure position is available
    if (position < 1) position = 1;
    var ic = this._thumbnails.length;
    if (position > ic) position = ic;

    // set it
    this._position = position;

    this.setActive(position);

    // don't move if we are using page mode and are on this page
    if (
      this._mode === "page" &&
      this._page === Math.ceil(position / this._vars.ipp)
    )
      return;

    this.moveTo(position, instant);
  },

  moveTo: function(position, instant) {
    this.updateVars();
    if (this.disabled()) return;

    var left,
      isHorizontal = this._orientation === "horizontal",
      vp_z = Bounds.viewport()[isHorizontal ? "width" : "height"],
      vp_center = vp_z * 0.5,
      t_width = this._vars.thumbnailFrame.width,
      page;

    if (this._mode === "page") {
      page = Math.ceil(position / this._vars.ipp);

      // set the page
      this._page = page;

      left = -1 * (t_width * (this._page - 1) * this._vars.ipp);

      // disabled states on buttons
      var disabled = "fr-thumbnails-side-button-disabled";
      this._previous_button[(page < 2 ? "add" : "remove") + "Class"](disabled);
      this._next_button[
        (page >= this._vars.pages ? "add" : "remove") + "Class"
      ](disabled);
    } else {
      // center
      left = vp_center + -1 * (t_width * (position - 1) + t_width * 0.5);
    }

    page = Pages.page;

    // now move there
    var resetCSS = {},
      animateCSS = {};
    resetCSS[!isHorizontal ? "left" : "top"] = 0; // zero out the other offset in case of switching orientation
    animateCSS[isHorizontal ? "left" : "top"] = left + "px";

    this._slide
      .stop(true)
      .css(resetCSS)
      .animate(
        animateCSS,
        instant
          ? 0
          : page
          ? page.view.options.effects.thumbnails.slide || 0
          : 0,
        function() {
          // load all thumbnails on this page
          this.loadCurrentPage();
        }.bind(this)
      );
  },

  loadCurrentPage: function() {
    var min, max;

    // load protection, don't load when there's no position or width on the thumbnail
    if (
      !this._position ||
      !this._vars.thumbnailFrame.width ||
      this._thumbnails.length < 1
    )
      return;

    if (this._mode === "page") {
      // load the thumbnail on the current page
      if (this._page < 1) return;

      min = (this._page - 1) * this._vars.ipp + 1;
      max = Math.min(min - 1 + this._vars.ipp, this._thumbnails.length);
    } else {
      // mode is 'center', load the thumbnails within the viewport
      var thumbnail_count = Math.ceil(
        this._vars.thumbnails.width / this._vars.thumbnailFrame.width
      );

      min = Math.max(
        Math.floor(Math.max(this._position - thumbnail_count * 0.5, 0)),
        1
      );
      max = Math.ceil(Math.min(this._position + thumbnail_count * 0.5));

      if (this._thumbnails.length < max) max = this._thumbnails.length;
    }

    // load the thumbnails
    for (var i = min; i <= max; i++) {
      this._thumbnails[i - 1].load();
    }
  },

  // only the active class
  setActive: function(position) {
    this._slide.find(".fr-thumbnail-active").removeClass("fr-thumbnail-active");

    var thumbnail = position && this._thumbnails[position - 1];
    if (thumbnail) thumbnail.activate();
  },

  refresh: function() {
    if (this._position) this.setPosition(this._position);
  },
};

// Thumbnail
function Thumbnail() {
  this.initialize.apply(this, _slice.call(arguments));
}
$.extend(Thumbnail.prototype, {
  initialize: function(view, position) {
    this.view = view;

    this._position = position;

    this.preBuild();
  },

  preBuild: function() {
    this.thumbnail = $("<div>")
      .addClass("fr-thumbnail")
      .data("fr-position", this._position);
  },

  build: function() {
    if (this.thumbnailFrame) return;

    var options = this.view.options;

    Thumbnails._slide.append(
      (this.thumbnailFrame = $("<div>")
        .addClass("fr-thumbnail-frame")
        .append(
          this.thumbnail.append(
            (this.thumbnailWrapper = $("<div>").addClass(
              "fr-thumbnail-wrapper"
            ))
          )
        ))
    );

    if (this.view.type === "image") {
      this.thumbnail.addClass("fr-load-thumbnail").data("thumbnail", {
        view: this.view,
        src: options.thumbnail || this.view.url,
      });
    }

    // icon
    var icon = options.thumbnail && options.thumbnail.icon;
    if (icon) {
      this.thumbnail.append(
        $("<div>").addClass("fr-thumbnail-icon fr-thumbnail-icon-" + icon)
      );
    }

    // overlay
    var overlay;
    this.thumbnail.append(
      (overlay = $("<div>")
        .addClass("fr-thumbnail-overlay")
        .append($("<div>").addClass("fr-thumbnail-overlay-background"))
        .append(
          (this.loading = $("<div>")
            .addClass("fr-thumbnail-loading")
            .append($("<div>").addClass("fr-thumbnail-loading-background"))
            .append(
              (this.spinner = $("<div>")
                .addClass("fr-thumbnail-spinner")
                .hide()
                .append($("<div>").addClass("fr-thumbnail-spinner-spin")))
            ))
        )
        .append($("<div>").addClass("fr-thumbnail-overlay-border")))
    );

    this.thumbnail.append($("<div>").addClass("fr-thumbnail-state"));

    this.resize();
  },

  remove: function() {
    if (this.thumbnailFrame) {
      this.thumbnailFrame.remove();
      this.thumbnailFrame = null;
      this.image = null;
    }

    if (this.ready) {
      this.ready.abort();
      this.ready = null;
    }

    if (this.vimeoThumbnail) {
      this.vimeoThumbnail.abort();
      this.vimeoThumbnail = null;
    }

    this._loading = false;
    this._removed = true;

    // clean up
    this.view = null;

    this._clearDelay();
  },

  load: function() {
    if (this._loaded || this._loading || this._removed) return;

    if (!this.thumbnailWrapper) this.build();

    this._loading = true;

    var thumbnail = this.view.options.thumbnail;
    var url =
      thumbnail && typeof thumbnail === "boolean"
        ? this.view.url
        : thumbnail || this.view.url;

    // store this now so we can modify it
    this._url = url;

    // vimeo needs an extra wrapper with a JSONP request
    if (url) {
      if (this.view.type === "vimeo") {
        if (url === thumbnail) {
          this._url = url;
          this._load(this._url);
        } else {
          switch (this.view.type) {
            case "vimeo":
              this.vimeoThumbnail = new VimeoThumbnail(
                this.view.url,
                function(url) {
                  this._url = url;
                  this._load(url);
                }.bind(this),
                function() {
                  this._error();
                }.bind(this)
              );
              break;
          }
        }
      } else {
        // youtube
        this._load(this._url);
      }
    }
  },

  activate: function() {
    this.thumbnail.addClass("fr-thumbnail-active");
  },

  _load: function(url) {
    this.thumbnailWrapper.prepend(
      (this.image = $("<img>")
        .addClass("fr-thumbnail-image")
        .attr({ src: url })
        .css({ opacity: 0.0001 }))
    );

    this.fadeInSpinner();

    this.ready = new ImageReady(
      this.image[0],
      function(imageready) {
        var img = imageready.img;

        // if the thumbnail has been removed before we finish,
        // or if the _loading has been cancelled,
        // just quit
        if (!this.thumbnailFrame || !this._loading) return;

        this._loaded = true;
        this._loading = false;

        // store dimensions, used by resize
        this._dimensions = {
          width: img.naturalWidth,
          height: img.naturalHeight,
        };

        // set dimensions after having loaded
        this.resize();

        // fadeout spinner
        this.show();
      }.bind(this),
      function() {
        this._error();
      }.bind(this),
      {
        method: this.view.options.loadedMethod,
      }
    );
  },

  // error callback
  _error: function() {
    this._loaded = true;
    this._loading = false;

    this.thumbnail.addClass("fr-thumbnail-error");
    if (this.image) {
      this.image.hide();
    }
    this.thumbnailWrapper.append($("<div>").addClass("fr-thumbnail-image"));
    this.show();
  },

  fadeInSpinner: function() {
    if (!Spinner.supported || !this.view.options.spinner) return;

    // clear possible delay
    this._clearDelay();

    var fx = this.view.options.effects.thumbnail;

    this._delay = setTimeout(
      function() {
        this.spinner.stop(true).fadeTo(fx.show || 0, 1);
      }.bind(this),
      this.view.options.spinnerDelay || 0
    );
  },

  // fades out the loading block
  // which could also contain a spinner
  show: function() {
    // clear possible delay
    this._clearDelay();

    var fx = this.view.options.effects.thumbnail;

    this.loading
      .stop(true)
      .delay(fx.delay)
      .fadeTo(fx.show, 0);
  },

  _clearDelay: function() {
    if (this._delay) {
      clearTimeout(this._delay);
      this._delay = null;
    }
  },

  // center image based on current dimensions
  resize: function() {
    if (!this.thumbnailFrame) return;

    var isHorizontal = Thumbnails._orientation === "horizontal";

    // frame
    this.thumbnailFrame.css({
      width: Thumbnails._vars.thumbnailFrame[isHorizontal ? "width" : "height"],
      height:
        Thumbnails._vars.thumbnailFrame[isHorizontal ? "height" : "width"],
    });

    // position frame
    this.thumbnailFrame.css({
      top: isHorizontal
        ? 0
        : Thumbnails._vars.thumbnailFrame.width * (this._position - 1),
      left: isHorizontal
        ? Thumbnails._vars.thumbnailFrame.width * (this._position - 1)
        : 0,
    });

    if (!this.thumbnailWrapper) return;

    // resize the wrapper
    var thumbnail = Thumbnails._vars.thumbnail;
    this.thumbnail.css({
      width: thumbnail.width,
      height: thumbnail.height,
      "margin-top": Math.round(-0.5 * thumbnail.height),
      "margin-left": Math.round(-0.5 * thumbnail.width),
      "margin-bottom": 0,
      "margin-right": 0,
    });

    // if there's no image, don't resize the rest
    if (!this._dimensions) return;

    var bounds = {
      width: thumbnail.width, //this.thumbnail.innerWidth(),
      height: thumbnail.height, //this.thumbnail.innerHeight()
    };

    var maxZ = Math.max(bounds.width, bounds.height);

    var dimensions;

    var image = $.extend({}, this._dimensions);

    if (image.width > bounds.width && image.height > bounds.height) {
      dimensions = Fit.within(bounds, image);

      // if the dimensions are smaller than bounds, increase them
      var scaleX = 1,
        scaleY = 1;
      if (dimensions.width < bounds.width) {
        scaleX = bounds.width / dimensions.width;
      }
      if (dimensions.height < bounds.height) {
        scaleY = bounds.height / dimensions.height;
      }

      var scale = Math.max(scaleX, scaleY);
      if (scale > 1) {
        dimensions.width *= scale;
        dimensions.height *= scale;
      }

      $.each("width height".split(" "), function(i, z) {
        dimensions[z] = Math.round(dimensions[z]); // .5 for anti-aliasing
      });
    } else {
      dimensions = Fit.within(
        this._dimensions,
        image.width < bounds.width || image.height < bounds.height
          ? { width: maxZ, height: maxZ }
          : bounds
      );
    }

    var x = Math.round(bounds.width * 0.5 - dimensions.width * 0.5),
      y = Math.round(bounds.height * 0.5 - dimensions.height * 0.5);

    this.image
      .removeAttr("style") // remove the opacity
      .css($.extend({}, dimensions, { top: y, left: x }));
  },
});

// UI Modes
var UI = {
  _modes: ["fullclick", "outside", "inside"],
  _ui: false,
  _validClickTargetSelector: [
    ".fr-content-element",
    ".fr-content",
    ".fr-content > .fr-stroke",
    ".fr-content > .fr-stroke .fr-stroke-color",
  ].join(", "),

  initialize: function(element) {
    // initialize the 3 different UI types
    $.each(
      this._modes,
      function(i, mode) {
        this[mode].initialize();
      }.bind(this)
    );

    // start with hidden ui
    Window.element.addClass("fr-ui-inside-hidden fr-ui-fullclick-hidden");
  },

  // set the ui mode, this only changes the class and with that visibility
  set: function(ui) {
    if (this._ui) {
      Window.element.removeClass("fr-window-ui-" + this._ui);
      Overlay.element.removeClass("fr-overlay-ui-" + this._ui);
    }
    Window.element.addClass("fr-window-ui-" + ui);
    Overlay.element.addClass("fr-overlay-ui-" + ui);

    // if we've switched ui's disable one and enable the other
    if (this._enabled && this._ui && this._ui !== ui) {
      this[this._ui].disable();
      this[ui].enable();

      // also show the UI when switched
      UI[ui].show();

      // stop timers on all other ui modes instantly
      //this.setActiveTimers(ui);
    }

    this._ui = ui;
  },

  // show ui for touch onresize
  _onWindowResize: function() {
    if (Support.mobileTouch) this.show();
  },

  // enable the currently set ui, disable all others
  enable: function() {
    $.each(
      this._modes,
      function(i, mode) {
        UI[mode][mode === this._ui ? "enable" : "disable"]();
      }.bind(this)
    );

    this._enabled = true;
  },

  disable: function() {
    $.each(
      this._modes,
      function(i, mode) {
        UI[mode].disable();
      }.bind(this)
    );

    this._enabled = false;
  },

  adjustPrevNext: function(callback, alternateDuration) {
    // only fullclick adjusts sides
    UI[this._ui].adjustPrevNext(callback, alternateDuration);
  },

  show: function(callback, alternateDuration) {
    UI[this._ui].show(callback, alternateDuration);

    //this.setActiveTimers(this._ui);
  },

  hide: function(callback, alternateDuration) {
    UI[this._ui].hide(callback, alternateDuration);
  },

  // called on Window.reset
  reset: function() {
    $.each(
      this._modes,
      function(i, mode) {
        UI[mode].reset();
      }.bind(this)
    );
  },

  update: function() {
    var page = Pages.page;
    if (!page) return;

    this.set(page._ui);
  },
};

UI.fullclick = {
  initialize: function() {
    this.build();

    this._scrollLeft = -1;
  },

  build: function() {
    Window._box
      .append(
        (this._previous = $("<div>")
          .addClass(
            "fr-side fr-side-previous fr-side-previous-fullclick fr-toggle-ui"
          )
          .append(
            $("<div>")
              .addClass("fr-side-button")
              .append($("<div>").addClass("fr-side-button-background"))
              .append($("<div>").addClass("fr-side-button-icon"))
          ))
      )
      .append(
        (this._next = $("<div>")
          .addClass("fr-side fr-side-next fr-side-next-fullclick fr-toggle-ui")
          .append(
            $("<div>")
              .addClass("fr-side-button")
              .append($("<div>").addClass("fr-side-button-background"))
              .append($("<div>").addClass("fr-side-button-icon"))
          ))
      )

      // close
      .append(
        (this._close = $("<div>")
          .addClass("fr-close fr-close-fullclick")
          .append($("<div>").addClass("fr-close-background"))
          .append($("<div>").addClass("fr-close-icon")))
      );

    // IE7 has a bug that causes multiple UI buttons from showing up
    // starting hidden fixes it
    if (Browser.IE && Browser.IE <= 7) {
      this._previous
        .add(this._next)
        .add(this._close)
        .hide();
    }

    // events
    this._close.on(
      "click",
      function(event) {
        event.preventDefault();
        Window.hide();
      }.bind(this)
    );

    this._previous.on(
      "click",
      function(event) {
        Window.previous();
        this._onMouseMove(event); // update cursor
      }.bind(this)
    );

    this._next.on(
      "click",
      function(event) {
        Window.next();
        this._onMouseMove(event); // update cursor
      }.bind(this)
    );
  },

  enable: function() {
    this.bind();
  },

  disable: function() {
    this.unbind();
  },

  reset: function() {
    Window.timers.clear("ui-fullclick");

    // clear cached mousemove
    this._x = -1;
    this._y = -1;

    this._scrollLeft = -1;

    this.resetPrevNext();

    // reset the classes by faking a mouseleave
    this._onMouseLeave();
  },

  resetPrevNext: function() {
    var buttons = this._previous.add(this._next);
    buttons.stop(true).removeAttr("style");
  },

  // events
  bind: function() {
    // no need to bind twice
    if (this._onMouseUpHandler) return;

    this.unbind();

    // clicks
    Window._pages.on(
      "mouseup",
      ".fr-container",
      (this._onMouseUpHandler = this._onMouseUp.bind(this))
    );

    // track <> only on desktop
    if (!Support.mobileTouch) {
      Window.element
        .on("mouseenter", (this._showHandler = this.show.bind(this)))
        .on("mouseleave", (this._hideHandler = this.hide.bind(this)));

      Window.element.on(
        "mousemove",
        (this._mousemoveHandler = function(event) {
          // Chrome has a bug that triggers mousemove events incorrectly
          // we have to work around this by comparing cursor positions
          // so only true mousemove events pass through:
          // https://code.google.com/p/chromium/issues/detail?id=420032
          var x = event.pageX,
            y = event.pageY;

          if (this._hoveringSideButton || (y === this._y && x === this._x)) {
            return;
          }

          // cache x/y
          this._x = x;
          this._y = y;

          this.show();
          this.startTimer();
        }.bind(this))
      );

      // delegate <> mousemove/click states
      Window._pages
        .on(
          "mousemove",
          ".fr-container",
          (this._onMouseMoveHandler = this._onMouseMove.bind(this))
        )
        .on(
          "mouseleave",
          ".fr-container",
          (this._onMouseLeaveHandler = this._onMouseLeave.bind(this))
        )
        .on(
          "mouseenter",
          ".fr-container",
          (this._onMouseEnterHandler = this._onMouseEnter.bind(this))
        );

      // delegate moving onto the <> buttons
      // keeping the mouse on them should keep the buttons visible
      Window.element
        .on(
          "mouseenter",
          ".fr-side",
          (this._onSideMouseEnterHandler = this._onSideMouseEnter.bind(this))
        )
        .on(
          "mouseleave",
          ".fr-side",
          (this._onSideMouseLeaveHandler = this._onSideMouseLeave.bind(this))
        );

      $(window).on(
        "scroll",
        (this._onScrollHandler = this._onScroll.bind(this))
      );
    }
  },

  unbind: function() {
    if (!this._onMouseUpHandler) return;

    // clicks
    Window._pages.off("mouseup", ".fr-container", this._onMouseUpHandler);
    this._onMouseUpHandler = null;

    if (this._showHandler) {
      Window.element
        .off("mouseenter", this._showHandler)
        .off("mouseleave", this._hideHandler)
        .off("mousemove", this._mousemoveHandler);

      Window._pages
        .off("mousemove", ".fr-container", this._onMouseMoveHandler)
        .off("mouseleave", ".fr-container", this._onMouseLeaveHandler)
        .off("mouseenter", ".fr-container", this._onMouseEnterHandler);

      Window.element
        .off("mouseenter", ".fr-side", this._onSideMouseEnterHandler)
        .off("mouseleave", ".fr-side", this._onSideMouseLeaveHandler);

      $(window).off("scroll", this._onScrollHandler);

      this._showHandler = null;
    }
  },

  adjustPrevNext: function(callback, alternateDuration) {
    var page = Pages.page;
    if (!page) {
      if (callback) callback();
      return;
    }

    // offset <>
    var windowVisible = Window.element.is(":visible");
    if (!windowVisible) Window.element.show();

    var pRestoreStyle = this._previous.attr("style");
    this._previous.removeAttr("style");
    var pnMarginTop = parseInt(this._previous.css("margin-top")); // the original margin top
    this._previous.attr({ style: pRestoreStyle });

    if (!windowVisible) Window.element.hide();

    // only the fullclick UI changes previous/next position based on info height
    var iH = page._infoHeight || 0;

    var buttons = this._previous.add(this._next),
      css = { "margin-top": pnMarginTop - iH * 0.5 };

    var duration =
      typeof alternateDuration === "number"
        ? alternateDuration
        : (Pages.page && Pages.page.view.options.effects.content.show) || 0;

    // adjust <> instantly when opening
    if (this.opening) duration = 0;

    buttons.stop(true).animate(css, duration, callback);

    // disabled states
    this._previous[(Window.mayPrevious() ? "remove" : "add") + "Class"](
      "fr-side-disabled"
    );
    this._next[(Window.mayNext() ? "remove" : "add") + "Class"](
      "fr-side-disabled"
    );

    // hide buttons for single content
    buttons[(page._total < 2 ? "add" : "remove") + "Class"]("fr-side-hidden");

    if (callback) callback();
  },

  _onScroll: function() {
    this._scrollLeft = $(window).scrollLeft();
  },

  _onMouseMove: function(event) {
    // no need for this on mobile-touch
    if (Support.mobileTouch) return;

    var side = this._getEventSide(event),
      Side = _.String.capitalize(side),
      mayClickHoveringSide = side ? Window["may" + Side]() : false;

    // prevent doing this on every mousemove
    if (
      side === this._hoveringSide &&
      mayClickHoveringSide === this._mayClickHoveringSide
    ) {
      return;
    }
    this._hoveringSide = side;
    this._mayClickHoveringSide = mayClickHoveringSide;

    Window._box[(mayClickHoveringSide ? "add" : "remove") + "Class"](
      "fr-hovering-clickable"
    );

    // previous
    switch (side) {
      case "previous":
        Window._box
          .addClass("fr-hovering-previous")
          .removeClass("fr-hovering-next");
        break;
      case "next":
        Window._box
          .addClass("fr-hovering-next")
          .removeClass("fr-hovering-previous");
        break;
    }
  },

  _onMouseLeave: function(event) {
    Window._box.removeClass(
      "fr-hovering-clickable fr-hovering-previous fr-hovering-next"
    );
    this._hoveringSide = false;
  },

  // click == mouseup
  // We use mouseup instead of click because it is more reliable.
  // Multiple clicks on images coming in and out of view can be
  // seen as triggering a selection, which doesn't trigger a click
  // event, but mouseup always fires.
  _onMouseUp: function(event) {
    // don't respond to middle/right clicks (2 & 3)
    if (event.which > 1) {
      return;
    }

    // close when needed
    // NOTE: we only do this for 1 item for fullclick
    // not for onClick:'close' since the arrow moves around
    // making it possible to accidentally close
    if (Pages.pages.length === 1) {
      Window.hide();
      return;
    }

    var side = this._getEventSide(event);
    Window[side]();

    // adjust cursor, doesn't work with effects
    // but _onMouseEnter is used to fix that
    this._onMouseMove(event);
  },

  _onMouseEnter: function(event) {
    // this solves clicking an area and not having an updating cursor
    // when not moving cursor after click. When an overlapping page comes into view
    // it'll trigger a mouseenter after the mouseout on the disappearing page
    // that would normally remove the clickable class
    this._onMouseMove(event);
  },

  _getEventSide: function(event) {
    var scrollLeft =
        this._scrollLeft > -1
          ? this._scrollLeft
          : (this._scrollLeft = $(window).scrollLeft()),
      left = event.pageX - Window._boxPosition.left - this._scrollLeft,
      width = Window._boxDimensions.width;

    return left < 0.5 * width ? "previous" : "next";
  },

  _onSideMouseEnter: function(event) {
    this._hoveringSideButton = true;
    this._hoveringSide = this._getEventSide(event);
    this._mayClickHoveringSide = Window[
      "may" + _.String.capitalize(this._hoveringSide)
    ]();
    this.clearTimer();
  },

  _onSideMouseLeave: function(event) {
    this._hoveringSideButton = false;
    this._hoveringSide = false;
    this._mayClickHoveringSide = false;
    this.startTimer();
  },

  show: function(callback) {
    if (this._visible) {
      // still clear a timer that could possible trigger hide
      // and start a new one
      this.startTimer();

      if (typeof callback === "function") callback();
      return;
    }

    this._visible = true;

    this.startTimer();

    Window.element
      .addClass("fr-visible-fullclick-ui")
      .removeClass("fr-hidden-fullclick-ui");

    if (Browser.IE && Browser.IE <= 7) {
      this._previous
        .add(this._next)
        .add(this._close)
        .show();
    }

    if (typeof callback === "function") callback();
  },

  hide: function(callback) {
    // never hide the ui for video
    var type = Pages.page && Pages.page.view.type;
    if (!this._visible || (type && (type === "youtube" || type === "vimeo"))) {
      if (typeof callback === "function") callback();
      return;
    }

    this._visible = false;

    Window.element
      .removeClass("fr-visible-fullclick-ui")
      .addClass("fr-hidden-fullclick-ui");

    if ($.type(callback) === "function") callback();
  },

  // UI Timer
  // not used on mobile-touch based devices
  clearTimer: function() {
    if (Support.mobileTouch) return;

    Window.timers.clear("ui-fullclick");
  },

  startTimer: function() {
    if (Support.mobileTouch) return;

    this.clearTimer();
    Window.timers.set(
      "ui-fullclick",
      function() {
        this.hide();
      }.bind(this),
      Window.view ? Window.view.options.uiDelay : 0
    );
  },
};

UI.inside = {
  initialize: function() {},

  enable: function() {
    this.bind();
  },

  disable: function() {
    this.unbind();
  },

  bind: function() {
    // no need to bind twice
    if (this._onMouseUpHandler) return;

    this.unbind();

    // clicks
    Window._pages.on(
      "mouseup",
      ".fr-content",
      (this._onMouseUpHandler = this._onMouseUp.bind(this))
    );

    // buttons
    Window._pages
      .on(
        "click",
        ".fr-content .fr-close",
        function(event) {
          event.preventDefault();
          Window.hide();
        }.bind(this)
      )
      .on(
        "click",
        ".fr-content .fr-side-previous",
        function(event) {
          Window.previous();
          this._onMouseMove(event); // update cursor
        }.bind(this)
      )
      .on(
        "click",
        ".fr-content .fr-side-next",
        function(event) {
          Window.next();
          this._onMouseMove(event); // update cursor
        }.bind(this)
      );

    // overlay
    Window.element.on(
      "click",
      ".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper",
      (this._delegateOverlayCloseHandler = this._delegateOverlayClose.bind(
        this
      ))
    );

    // track <> only on desktop
    if (!Support.mobileTouch) {
      Window.element
        .on(
          "mouseenter",
          ".fr-content",
          (this._showHandler = this.show.bind(this))
        )
        .on(
          "mouseleave",
          ".fr-content",
          (this._hideHandler = this.hide.bind(this))
        );

      Window.element.on(
        "mousemove",
        ".fr-content",
        (this._mousemoveHandler = function(event) {
          // Chrome has a bug that triggers mousemove events incorrectly
          // we have to work around this by comparing cursor positions
          // so only true mousemove events pass through:
          // https://code.google.com/p/chromium/issues/detail?id=420032
          var x = event.pageX,
            y = event.pageY;

          if (this._hoveringSideButton || (y === this._y && x === this._x)) {
            return;
          }

          // cache x/y
          this._x = x;
          this._y = y;

          this.show();
          this.startTimer();
        }.bind(this))
      );

      // block mousemove on caption and info
      Window._pages.on(
        "mousemove",
        ".fr-info, .fr-close",
        function(event) {
          event.stopPropagation();
          this._onMouseLeave(event);
        }.bind(this)
      );

      // hovering info shouldn't hide it
      // mousemove is used here since mouseenter would conflict with
      // mouseenter on the content triggering later. That would make it
      // impossible to enter the content on the info box and keep it
      // visible since the timer would start again on the 2nd mouseenter.
      Window._pages.on(
        "mousemove",
        ".fr-info",
        function() {
          this.clearTimer();
        }.bind(this)
      );

      // delegate <> mousemove/click states
      Window._pages
        .on(
          "mousemove",
          ".fr-content",
          (this._onMouseMoveHandler = this._onMouseMove.bind(this))
        )
        .on(
          "mouseleave",
          ".fr-content",
          (this._onMouseLeaveHandler = this._onMouseLeave.bind(this))
        )
        .on(
          "mouseenter",
          ".fr-content",
          (this._onMouseEnterHandler = this._onMouseEnter.bind(this))
        );

      // delegate moving onto the <> buttons
      // keeping the mouse on them should keep the buttons visible
      Window.element
        .on(
          "mouseenter",
          ".fr-side",
          (this._onSideMouseEnterHandler = this._onSideMouseEnter.bind(this))
        )
        .on(
          "mouseleave",
          ".fr-side",
          (this._onSideMouseLeaveHandler = this._onSideMouseLeave.bind(this))
        );

      $(window).on(
        "scroll",
        (this._onScrollHandler = this._onScroll.bind(this))
      );
    }
  },

  unbind: function() {
    if (!this._onMouseUpHandler) return;

    // clicks
    Window._pages.off("mouseup", ".fr-content", this._onMouseUpHandler);
    this._onMouseUpHandler = null;

    // buttons
    Window._pages
      .off("click", ".fr-content .fr-close")
      .off("click", ".fr-content .fr-side-previous")
      .off("click", ".fr-content .fr-side-next");

    // overlay
    Window.element.off(
      "click",
      ".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper",
      this._delegateOverlayCloseHandler
    );

    if (this._showHandler) {
      Window.element
        .off("mouseenter", ".fr-content", this._showHandler)
        .off("mouseleave", ".fr-content", this._hideHandler)
        .off("mousemove", ".fr-content", this._mousemoveHandler);

      // blocked mousemove
      Window._pages.off("mousemove", ".fr-info, .fr-close");

      // info
      Window._pages.off("mousemove", ".fr-info");

      Window._pages
        .off("mousemove", ".fr-content-element", this._onMouseMoveHandler)
        .off("mouseleave", ".fr-content", this._onMouseLeaveHandler)
        .off("mouseenter", ".fr-content", this._onMouseEnterHandler);

      Window.element
        .off("mouseenter", ".fr-side", this._onSideMouseEnterHandler)
        .off("mouseleave", ".fr-side", this._onSideMouseLeaveHandler);

      $(window).off("scroll", this._onScrollHandler);

      this._showHandler = null;
    }
  },

  reset: function() {
    Window.timers.clear("ui-fullclick");

    // clear cached mousemove
    this._x = -1;
    this._y = -1;

    this._scrollLeft = -1;
    this._hoveringSide = false;

    // reset the classes by faking a mouseleave
    this._onMouseLeave();
  },

  adjustPrevNext: function(callback) {
    if (callback) callback();
  },

  _onScroll: function() {
    this._scrollLeft = $(window).scrollLeft();
  },

  _delegateOverlayClose: function(event) {
    var page = Pages.page;
    if (page && page.view.options.overlay && !page.view.options.overlay.close)
      return;

    // we don't want to respond to clicks on children
    if (
      !$(event.target).is(
        ".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper"
      )
    )
      return;

    event.preventDefault();
    event.stopPropagation();
    Window.hide();
  },

  _onMouseMove: function(event) {
    // no need for this on mobile-touch
    if (Support.mobileTouch) return;

    var side = this._getEventSide(event),
      Side = _.String.capitalize(side),
      mayClickHoveringSide = side ? Window["may" + Side]() : false;

    // clear out side when onClick:'close'
    if (
      Pages.pages.length === 1 ||
      (Pages.page && Pages.page.view.options.onClick === "close")
    ) {
      side = false;
    }

    // prevent doing this on every mousemove
    if (
      side === this._hoveringSide &&
      mayClickHoveringSide === this._mayClickHoveringSide
    ) {
      return;
    }
    this._hoveringSide = side;
    this._mayClickHoveringSide = mayClickHoveringSide;

    if (side) {
      Window._box[(mayClickHoveringSide ? "add" : "remove") + "Class"](
        "fr-hovering-clickable"
      );

      // previous
      switch (side) {
        case "previous":
          Window._box
            .addClass("fr-hovering-previous")
            .removeClass("fr-hovering-next");
          break;
        case "next":
          Window._box
            .addClass("fr-hovering-next")
            .removeClass("fr-hovering-previous");
          break;
      }
    } else {
      Window._box.removeClass(
        "fr-hovering-clickable fr-hovering-previous fr-hovering-next"
      );
    }
  },

  _onMouseLeave: function(event) {
    Window._box.removeClass(
      "fr-hovering-clickable fr-hovering-previous fr-hovering-next"
    );
    this._hoveringSide = false;
  },

  // click == mouseup
  // We use mouseup instead of click because it is more reliable.
  // Multiple clicks on images coming in and out of view can be
  // seen as triggering a selection, which doesn't trigger a click
  // event, but mouseup always fires.
  _onMouseUp: function(event) {
    if (
      // don't respond to middle/right clicks (2 & 3)
      event.which > 1 ||
      // or invalid targets
      !$(event.target).is(UI._validClickTargetSelector)
    ) {
      return;
    }

    // close when needed
    if (
      Pages.pages.length === 1 ||
      (Pages.page && Pages.page.view.options.onClick === "close")
    ) {
      Window.hide();
      return;
    }

    var side = this._getEventSide(event);
    Window[side]();

    // adjust cursor, doesn't work with effects
    // but _onMouseEnter is used to fix that
    this._onMouseMove(event);
  },

  _onMouseEnter: function(event) {
    // this solves clicking an area and not having an updating cursor
    // when not moving cursor after click. When an overlapping page comes into view
    // it'll trigger a mouseenter after the mouseout on the disappearing page
    // that would normally remove the clickable class
    this._onMouseMove(event);
  },

  _getEventSide: function(event) {
    var scrollLeft =
        this._scrollLeft > -1
          ? this._scrollLeft
          : (this._scrollLeft = $(window).scrollLeft()),
      left = event.pageX - Window._boxPosition.left - this._scrollLeft,
      width = Window._boxDimensions.width;

    return left < 0.5 * width ? "previous" : "next";
  },

  _onSideMouseEnter: function(event) {
    this._hoveringSideButton = true;
    this._hoveringSide = this._getEventSide(event);
    this._mayClickHoveringSide = Window[
      "may" + _.String.capitalize(this._hoveringSide)
    ]();
    this.clearTimer();
  },

  _onSideMouseLeave: function(event) {
    this._hoveringSideButton = false;
    this._hoveringSide = false;
    this._mayClickHoveringSide = false;
    this.startTimer();
  },

  show: function(callback) {
    if (this._visible) {
      // still clear a timer that could possible trigger hide
      // and start a new one
      this.startTimer();

      if (typeof callback === "function") callback();
      return;
    }

    this._visible = true;
    //UI.fullclick.visible = true;

    this.startTimer();

    Window.element
      .addClass("fr-visible-inside-ui")
      .removeClass("fr-hidden-inside-ui");

    if (typeof callback === "function") callback();
  },

  hide: function(callback) {
    if (!this._visible) {
      if (typeof callback === "function") callback();
      return;
    }

    this._visible = false;
    //UI.fullclick.visible = false;

    Window.element
      .removeClass("fr-visible-inside-ui")
      .addClass("fr-hidden-inside-ui");

    if (typeof callback === "function") callback();
  },

  // timers
  // not used on mobile-touch based devices
  clearTimer: function() {
    if (Support.mobileTouch) return;

    Window.timers.clear("ui-inside");
  },

  startTimer: function() {
    if (Support.mobileTouch) return;

    this.clearTimer();
    Window.timers.set(
      "ui-inside",
      function() {
        this.hide();
      }.bind(this),
      Window.view ? Window.view.options.uiDelay : 0
    );
  },
};

UI.outside = {
  initialize: function() {
    this.build();

    this._scrollLeft = -1;
  },

  build: function() {
    Window._box
      .append(
        (this._previous = $("<div>")
          .addClass("fr-side fr-side-previous fr-side-previous-outside")
          .append(
            $("<div>")
              .addClass("fr-side-button")
              .append($("<div>").addClass("fr-side-button-background"))
              .append($("<div>").addClass("fr-side-button-icon"))
          ))
      )
      .append(
        (this._next = $("<div>")
          .addClass("fr-side fr-side-next fr-side-next-outside")
          .append(
            $("<div>")
              .addClass("fr-side-button")
              .append($("<div>").addClass("fr-side-button-background"))
              .append($("<div>").addClass("fr-side-button-icon"))
          ))
      )

      // close
      .append(
        (this._close = $("<div>")
          .addClass("fr-close fr-close-outside")
          .append($("<div>").addClass("fr-close-background"))
          .append($("<div>").addClass("fr-close-icon")))
      );

    // IE7 has a bug that causes multiple UI buttons from showing up
    // starting hidden fixes it
    if (Browser.IE && Browser.IE <= 7) {
      this._previous
        .add(this._next)
        .add(this._close)
        .hide();
    }

    // events
    this._close.on(
      "click",
      function(event) {
        event.preventDefault();
        Window.hide();
      }.bind(this)
    );

    this._previous.on(
      "click",
      function(event) {
        Window.previous();
        this._onMouseMove(event); // update cursor
      }.bind(this)
    );

    this._next.on(
      "click",
      function(event) {
        Window.next();
        this._onMouseMove(event); // update cursor
      }.bind(this)
    );
  },

  enable: function() {
    this.bind();
  },

  disable: function() {
    this.unbind();
  },

  reset: function() {
    Window.timers.clear("ui-outside");

    // clear cached mousemove
    this._x = -1;
    this._y = -1;

    this._scrollLeft = -1;

    // reset the classes by faking a mouseleave
    this._onMouseLeave();
  },

  // events
  bind: function() {
    // no need to bind twice
    if (this._onMouseUpHandler) return;

    this.unbind();

    // clicks
    Window.element.on(
      "mouseup",
      ".fr-content",
      (this._onMouseUpHandler = this._onMouseUp.bind(this))
    );

    // overlay
    Window.element.on(
      "click",
      ".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper",
      (this._delegateOverlayCloseHandler = this._delegateOverlayClose.bind(
        this
      ))
    );

    // track <> only on desktop
    if (!Support.mobileTouch) {
      // delegate <> mousemove/click states
      Window._pages
        .on(
          "mousemove",
          ".fr-content",
          (this._onMouseMoveHandler = this._onMouseMove.bind(this))
        )
        .on(
          "mouseleave",
          ".fr-content",
          (this._onMouseLeaveHandler = this._onMouseLeave.bind(this))
        )
        .on(
          "mouseenter",
          ".fr-content",
          (this._onMouseEnterHandler = this._onMouseEnter.bind(this))
        );

      // delegate moving onto the <> buttons
      // keeping the mouse on them should keep the buttons visible
      Window.element
        .on(
          "mouseenter",
          ".fr-side",
          (this._onSideMouseEnterHandler = this._onSideMouseEnter.bind(this))
        )
        .on(
          "mouseleave",
          ".fr-side",
          (this._onSideMouseLeaveHandler = this._onSideMouseLeave.bind(this))
        );

      $(window).on(
        "scroll",
        (this._onScrollHandler = this._onScroll.bind(this))
      );
    }
  },

  unbind: function() {
    if (!this._onMouseUpHandler) return;

    // clicks
    Window.element.off("mouseup", ".fr-content", this._onMouseUpHandler);
    this._onMouseUpHandler = null;

    // overlay
    Window.element.off(
      "click",
      ".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper",
      this._delegateOverlayCloseHandler
    );

    if (this._onMouseMoveHandler) {
      Window._pages
        .off("mousemove", ".fr-content", this._onMouseMoveHandler)
        .off("mouseleave", ".fr-content", this._onMouseLeaveHandler)
        .off("mouseenter", ".fr-content", this._onMouseEnterHandler);

      Window.element
        .off("mouseenter", ".fr-side", this._onSideMouseEnterHandler)
        .off("mouseleave", ".fr-side", this._onSideMouseLeaveHandler);

      $(window).off("scroll", this._onScrollHandler);

      this._onMouseMoveHandler = null;
    }
  },

  adjustPrevNext: function(callback, alternateDuration) {
    var page = Pages.page;
    if (!page) {
      if (callback) callback();
      return;
    }

    var buttons = this._previous.add(this._next);

    // disabled states
    this._previous[(Window.mayPrevious() ? "remove" : "add") + "Class"](
      "fr-side-disabled"
    );
    this._next[(Window.mayNext() ? "remove" : "add") + "Class"](
      "fr-side-disabled"
    );

    // hide buttons for single content
    buttons[(page._total < 2 ? "add" : "remove") + "Class"]("fr-side-hidden");

    if (callback) callback();
  },

  _onScroll: function() {
    this._scrollLeft = $(window).scrollLeft();
  },

  _delegateOverlayClose: function(event) {
    var page = Pages.page;
    if (page && page.view.options.overlay && !page.view.options.overlay.close)
      return;

    // we don't want to respond to clicks on children
    if (
      !$(event.target).is(
        ".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper"
      )
    )
      return;

    event.preventDefault();
    event.stopPropagation();
    Window.hide();
  },

  _onMouseMove: function(event) {
    // no need for this on mobile-touch
    if (Support.mobileTouch) return;

    var side = this._getEventSide(event),
      Side = _.String.capitalize(side),
      mayClickHoveringSide = side ? Window["may" + Side]() : false;

    // clear out side when onClick:'close'
    if (
      Pages.pages.length === 1 ||
      (Pages.page && Pages.page.view.options.onClick === "close")
    ) {
      side = false;
    }

    // prevent doing this on every mousemove
    if (
      side === this._hoveringSide &&
      mayClickHoveringSide === this._mayClickHoveringSide
    ) {
      return;
    }
    this._hoveringSide = side;
    this._mayClickHoveringSide = mayClickHoveringSide;

    if (side) {
      Window._box[(mayClickHoveringSide ? "add" : "remove") + "Class"](
        "fr-hovering-clickable"
      );

      // previous
      switch (side) {
        case "previous":
          Window._box
            .addClass("fr-hovering-previous")
            .removeClass("fr-hovering-next");
          break;
        case "next":
          Window._box
            .addClass("fr-hovering-next")
            .removeClass("fr-hovering-previous");
          break;
      }
    } else {
      Window._box.removeClass(
        "fr-hovering-clickable fr-hovering-previous fr-hovering-next"
      );
    }
  },

  _onMouseLeave: function(event) {
    Window._box.removeClass(
      "fr-hovering-clickable fr-hovering-previous fr-hovering-next"
    );
    this._hoveringSide = false;
  },

  // click == mouseup
  // We use mouseup instead of click because it is more reliable.
  // Multiple clicks on images coming in and out of view can be
  // seen as triggering a selection, which doesn't trigger a click
  // event, but mouseup always fires.
  _onMouseUp: function(event) {
    if (
      // don't respond to middle/right clicks (2 & 3)
      event.which > 1 ||
      // or invalid targets
      !$(event.target).is(UI._validClickTargetSelector)
    ) {
      return;
    }

    // close when needed
    if (
      Pages.pages.length === 1 ||
      (Pages.page && Pages.page.view.options.onClick === "close")
    ) {
      Window.hide();
      return;
    }

    var side = this._getEventSide(event);
    Window[side]();

    // adjust cursor, doesn't work with effects
    // but _onMouseEnter is used to fix that
    this._onMouseMove(event);
  },

  _onMouseEnter: function(event) {
    // this solves clicking an area and not having an updating cursor
    // when not moving cursor after click. When an overlapping page comes into view
    // it'll trigger a mouseenter after the mouseout on the disappearing page
    // that would normally remove the clickable class
    this._onMouseMove(event);
  },

  _getEventSide: function(event) {
    var scrollLeft =
        this._scrollLeft > -1
          ? this._scrollLeft
          : (this._scrollLeft = $(window).scrollLeft()),
      left = event.pageX - Window._boxPosition.left - this._scrollLeft,
      width = Window._boxDimensions.width;

    return left < 0.5 * width ? "previous" : "next";
  },

  show: function() {
    if (Browser.IE && Browser.IE <= 7) {
      this._previous
        .add(this._next)
        .add(this._close)
        .show();
    }
  },

  hide: function() {},

  _onSideMouseEnter: function(event) {
    this._hoveringSideButton = true;
    this._hoveringSide = this._getEventSide(event);
    this._mayClickHoveringSide = Window[
      "may" + _.String.capitalize(this._hoveringSide)
    ]();
  },

  _onSideMouseLeave: function(event) {
    this._hoveringSideButton = false;
    this._hoveringSide = false;
    this._mayClickHoveringSide = false;
  },

  clearTimer: function() {},
};

  // start
  $(function() {
    _Fresco.initialize();
  });

  return Fresco;
});