var ProductDetail = {
/**
* Base Product Detail
*
* TODO
* - Add to cart
* - Push state variation change
*
* Usage:
* $(function() {
* mySettings = {animate: false};
* myProductDetail = _.extend(ProductDetail, {});
* myProductDetail.init(mySettings);
* });
*/
settings: {
animate: true,
animationSpeed: 300,
alternates: '.alternates li',
activeClass: 'active',
arrows: false,
arrowButton: '[data-product-thumbs-nav]',
arrowInfinitely: true,
next: '[data-product-thumbs-nav="next"]',
prev: '[data-product-thumbs-nav="prev"]',
productImage: '.product-image',
container: '#product-detail',
onchange: $.noop,
},
init: function (settings) {
settings = settings || {};
this.settings = _.extend(this.settings, settings);
this.bindUI();
},
bindUI: function () {
var self = this;
$('body').on('click', self.settings.alternates + ' a:not(.product-video)', function (e) {
e.preventDefault();
self.changeAlternate($(this));
});
// Next/prev arrows
if (self.settings.arrows) {
self.arrows();
}
},
arrows: function () {
var self = this;
$('body').on('click', self.settings.arrowButton, function (e) {
e.preventDefault();
self.navigate($(this).is(self.settings.prev) ? 'prev' : 'next');
});
},
changeAlternate: function (alternate) {
var self = this,
scope = alternate.closest(this.settings.container),
parent = alternate.closest('li'),
alternates = scope.find(this.settings.alternates),
productImage = scope.find(this.settings.productImage),
productImageContainer = $(productImage).parent();
if (parent.hasClass(self.settings.activeClass)) return;
alternates.removeClass(self.settings.activeClass);
parent.addClass(self.settings.activeClass);
if (productImageContainer.data('zoom-image-id')) {
productImageContainer.attr('data-zoom-image-id', parent.index() + 1);
}
this.loadImage(productImage, alternate.attr('href'));
this.settings.onchange(alternate, productImage);
},
loadImage: function (img, image) {
var self = this;
if (self.settings.animate) {
img.animate({opacity: 0}, self.settings.animationSpeed);
}
var loadImage = $('').attr('src', image).on('load', function () {
img.queue(function () {
img.attr('src', image);
$(this).dequeue();
});
if (self.settings.animate) {
img.animate({opacity: 1}, self.settings.animationSpeed);
}
});
},
navigate: function (direction) {
var self = this,
active = $(self.settings.alternates + '.' + self.settings.activeClass),
item = direction === 'prev' ? active.prev() : active.next();
if (item.length > 0) {
self.changeAlternate(item.find('a'));
} else {
if (self.settings.arrowInfinitely) {
var loopDirection = direction === 'prev' ? ':last-of-type' : ':first-of-type',
loopItem = $(self.settings.alternates + loopDirection);
self.changeAlternate(loopItem.find('a'));
}
}
}
};
var ProductTile = {
/**
* Base Product Tile
*
* Handles changing between swatches on product tile
*
* TODO
* - Improve slide animation
*
* Usage:
* $(function() {
* mySettings = {animate: false};
* myProductSwatches = _.extend(ProductSwatches, {});
* myProductSwatches.init(mySettings);
* });
*/
settings: {
activeClass: 'active',
animate: true,
animation: 'slide', // slide or fade
animationSpeed: 300,
arrows: '.image button',
productList: '.products > li',
productLink: '.product-link',
productTile: '.product-tile',
productImage: '.image img',
productPrice: '.price',
productNew: '.new',
productColor: '.color',
quickview: '.quickview',
slideScope: 'a',
swatches: '.swatches li',
onchange: $.noop,
beforechange: $.noop,
afterchange: $.noop
},
init: function (settings) {
settings = settings || {};
this.settings = _.extend(this.settings, settings);
this.bindUI();
},
bindUI: function () {
var self = this;
// Swatches
$('body').on('click', [self.settings.productList, self.settings.swatches].join(' '), function (e) {
e.preventDefault();
self.changeSwatch($(this).find('a'));
});
// Next/prev arrows
$('body').on('click', [self.settings.productList, self.settings.arrows].join(' '), function (e) {
e.preventDefault();
self.navigate($(this), $(this).hasClass('prev') ? 'prev' : 'next');
});
// Touch swipe
if ($.fn.swipe) {
$(self.settings.productList).swipe({
swipeLeft: function (event, direction, distance, duration, fingerCount) {
$(this).closest(self.settings.productTile).find(self.settings.arrows).filter('.next').trigger('click');
},
swipeRight: function (event, direction, distance, duration, fingerCount) {
$(this).closest(self.settings.productTile).find(self.settings.arrows).filter('.prev').trigger('click');
},
excludedElements: []
});
}
},
changeSwatch: function (swatch) {
var scope = swatch.closest(this.settings.productTile),
swatches = scope.find(this.settings.swatches),
parent = swatch.closest('li'),
productImage = scope.find(this.settings.productImage),
productLink = scope.find(this.settings.productLink),
productPrice = scope.find(this.settings.productPrice),
productNew = scope.find(this.settings.productNew),
quickvewLink = scope.find(this.settings.quickview),
productColor = scope.find(this.settings.productColor),
active = swatches.filter('.' + this.settings.activeClass),
direction = 'next';
if (scope.hasClass('animating')) return;
if (parent.hasClass(this.settings.activeClass)) return;
// If active is greater than swatch, go left
direction = (swatches.index(parent) + 1) != swatches.length && swatches.index(active) > swatches.index(parent) ? 'prev' : direction;
// Set active
swatches.removeClass(this.settings.activeClass);
parent.addClass(this.settings.activeClass);
// Product url
productLink.attr('href', swatch.attr('href'));
quickvewLink.attr('href', swatch.attr('href'));
// Pricing
if (parent.data('sale-price')) {
productPrice.html('' + parent.data('normal-price') + '' + parent.data('sale-price'));
} else {
productPrice.html(parent.data('price'));
}
// Colors
if (parent.data('color')) {
productColor.html(parent.data('color'));
}
// New
productNew.hide();
if (parent.data('new') == 'True') {
productNew.show();
}
// Load new product image
this.loadImage(productImage, parent.data('image'), direction);
},
loadImage: function (img, image, direction) {
var self = this,
tile = img.closest('li');
// Translate direction to css attribute
direction = direction == 'prev' ? 'right' : 'left';
// beforechange callback
self.settings.beforechange(tile, direction);
// Setup for animation
if (self.settings.animate) {
// Fade: Fade out
if (self.settings.animation == 'fade') img.animate({opacity: 0}, 300);
// Slide: Get the slider ready
if (self.settings.animation == 'slide') {
var scope = img.closest(self.settings.slideScope),
clone = img.clone(),
height = img.outerHeight(),
width = img.width(),
options = {'position': 'absolute', 'left': 'auto', 'right': 'auto'};
scope.closest('li').addClass('animating');
scope.css({'position': 'relative', 'height': height, 'overflow': 'hidden'});
options[direction] = width + 'px';
clone.css(options);
img.css({'position': 'absolute', 'left': 'auto', 'right': 'auto'}).after(clone);
}
}
// Preload the image and animate
var loadImage = $('').attr('src', image).on('load', function () {
if (self.settings.animate && self.settings.animation == 'slide') {
img.queue(function () {
// Set the clone with new source
clone.attr('src', image);
// Slide image out and remove it
options = {};
options[direction] = '-' + width + 'px';
img.animate(options, self.settings.animationSpeed, 'swing', function () {
$(this).remove();
scope.closest('li').removeClass('animating');
scope.css({'height': 'auto'});
clone.css({'position': 'relative'});
});
// Slide clone in making it the new guy
options = {};
options[direction] = 0;
clone.animate(options, self.settings.animationSpeed, 'swing').promise().done(function () {
// afterchange callback
self.settings.afterchange(tile);
});
$(this).dequeue();
});
} else {
img.queue(function () {
img.attr('src', image);
$(this).dequeue();
});
if (self.settings.animate) {
// img.animate({opacity: 1}, self.settings.animationSpeed);
img.animate({opacity: 1}, self.settings.animationSpeed).promise().done(function () {
img.attr('style', '');
// afterchange callback
self.settings.afterchange(tile);
});
}
}
// onchange callback
self.settings.onchange(tile);
});
},
navigate: function (arrow, direction) {
var scope = arrow.closest(this.settings.productTile),
swatches = scope.find(this.settings.swatches),
activeColor = swatches.filter('.active');
// Find next/prev swatch
if (activeColor[direction]().length > 0) {
return activeColor[direction]().find('a').trigger('click');
}
// Wrap to first/last swatch
return swatches.filter(':' + (direction == 'prev' ? 'last' : 'first')).find('a').trigger('click');
}
};
var Quickview = {
settings: {
htmlClass: 'quickview-active',
close: '.close',
container: '#quickview',
nextNavigation: '#quickview .next',
prevNavigation: '#quickview .previous',
activeItemClass: 'js-quickview-active',
quickviewItems: '.product-tile',
quickviewLink: '[data-action="quickview"]',
iframe: '' +
'' +
'' +
'
' +
'' +
'' +
'
' +
'',
iframeContent: '.product-detail',
iframeDynamicSize: false,
onload: $.noop,
onclose: $.noop,
beforeload: $.noop,
beforeclose: $.noop
},
active: false,
init: function (settings) {
settings = settings || {};
this.settings = _.extend(this.settings, settings);
this.bindUI();
parent.Quickview.triggerParent();
},
bindUI: function () {
var self = this;
// Create quickview container
// Enabling quickview
$('body').on('click', self.settings.quickviewLink, function (e) {
// Call the beforeload method
self.open(this, e);
return false;
});
// Arrow through the items on wall
$('body').on('click', self.settings.prevNavigation, $.proxy(self.prev, self));
$('body').on('click', self.settings.nextNavigation, $.proxy(self.next, self));
$(document).keydown(function (e) {
if (!self.active) {
return true;
}
switch (e.which) {
case 37:
self.prev();
break;
case 39:
self.next();
break;
case 27:
self.close();
break;
}
if ($.inArray(e.which, [27, 37, 39]) >= 0) {
return false;
}
});
},
created: false,
create: function (el) {
var self = this;
if (self.created) {
return;
}
self.created = true;
var container = $(self.settings.iframe);
$('body').append(container);
var iframe = container.find('iframe');
this.iframe = iframe;
self.iframe = iframe;
this.container = container;
self.container = container;
// Watch for the iframe to call Quickview.triggerParent() which calls this custom event
$('body').on('sidecart:quickview:loaded', function () {
self.settings.onload(iframe);
// Optionally set the iframe size based on its content
if (self.settings.iframeDynamicSize) {
var content = self.iframe.contents().find(self.settings.iframeContent),
width = content.outerWidth(true),
height = content.outerHeight(true);
$(self.iframe).width(width);
$(self.iframe).height(height);
container.removeClass('loading');
} else {
container.removeClass('loading');
}
});
// Closing quickview
container.find(self.settings.close).on('click', function (e) {
self.close(iframe, e);
});
},
open: function (el, e) {
// Add the markup
this.create(el);
// Trigger the beforeload
this.settings.beforeload(el, e);
// Set the quickview as open
this.active = true;
// Add an active class
$(el).closest(this.settings.quickviewItems).addClass(this.settings.activeItemClass).siblings().removeClass(this.settings.activeItemClass);
// Load quickview URL
this.container.addClass('loading');
// Generate condensed view url
url = $(el).closest('a').attr('href');
var condensed = url.indexOf('?') > -1 ? '&condensed=true' : '?condensed=true';
url = [url, condensed].join('');
this.iframe.attr('src', url);
$('html').addClass(this.settings.htmlClass);
},
next: function () {
var nextItem = $('.' + this.settings.activeItemClass).nextAll(this.settings.quickviewItems).find(this.settings.quickviewLink).closest(this.settings.quickviewItems).first();
if (nextItem.length < 1) nextItem = $(this.settings.quickviewItems).find(this.settings.quickviewLink).closest(this.settings.quickviewItems).first();
nextItem.find(this.settings.quickviewLink).trigger('click');
},
prev: function () {
prevItem = $('.' + this.settings.activeItemClass).prevAll(this.settings.quickviewItems).find(this.settings.quickviewLink).closest(this.settings.quickviewItems).last();
if (prevItem.length < 1) prevItem = $(this.settings.quickviewItems).find(this.settings.quickviewLink).closest(this.settings.quickviewItems).last();
prevItem.find(this.settings.quickviewLink).trigger('click');
},
close: function (el, e) {
var self = this;
self.settings.beforeclose(el, e);
$('html').removeClass(this.settings.htmlClass);
$(el).attr('src', 'about:blank');
self.active = false;
self.settings.onclose(el, e);
},
triggerParent: function () {
$('body').trigger('sidecart:quickview:loaded');
}
};
var Zoom = {
/**
* Zoom Modal
* @param {String} trigger - The element (any css selector will work) that will act as trigger to open zoom modal
* @param {Boolean} draggable - Will image be draggable? Defaults to true (relies on jquery-ui and jquery.ui.touch-punch for touch support)
* @param {Boolean} flexSync - Is flexslider being used on product detail hero images and we want the sliders to be synced up? Defaults to false
* @param {Boolean} videos - Is there going to be videos? Defaults to false
* @param {String} extraClass - Do we need an extra class that will be added to overlay after a small delay? Defaults to false - EG: This can be used to hide the menu (thumbs/title) after a few seconds and show it on hover
* @param {Number} extraClassDelay - How long should we wait to add the extraClass? Defaults to 5000
*/
init: function (trigger, draggable, flexSync, videos, extraClass, extraClassDelay) {
this.template = $('[data-zoom-template]');
this.build();
this.draggable = (typeof draggable !== "undefined") ? draggable : true;
this.flexSync = (typeof flexSync !== "undefined") ? flexSync : false;
this.trigger = $(trigger);
this.container = $('body').find('[data-zoom]');
this.extraClass = (typeof extraClass !== "undefined") ? extraClass : false;
this.extraClassDelay = (typeof extraClassDelay !== "undefined") ? extraClassDelay : 5000; // add class after 5 seconds by default
this.slides = this.container.find('[data-zoom-slides]');
this.slideItem = this.container.find('[data-zoom-slides] > li');
this.closeBtn = this.container.find('.zoom-close');
this.next = this.container.find('[data-zoom-next]');
this.prev = this.container.find('[data-zoom-prev]');
this.media = this.container.find('[data-zoom-media]');
this.clickEvent = 'click touchstart';
this.videos = (typeof videos !== "undefined") ? true : false;
this.videoOverlay = this.container.find('[data-zoom-video-overlay]');
this.video = this.container.find('.video iframe');
this.bindControls();
},
// Build the markup from script template
build: function () {
$('body').append(this.template.html());
},
// Bind Controls
bindControls: function () {
var self = this;
// --- ACIVATE OVERLAY --- //
// Activate zoom when image is clicked from product detail
self.trigger.on('click', function (event) {
// Open the zoom overlay
self.open();
// Trigger element
var btn = $(this);
// Image or video?
var type = btn.data('zoom-trigger');
// Which slide is active?
var active = btn.attr('data-zoom-' + type + '-id');
// Zoom slider
var slider = self.media;
// Zoom slider active item
var activeSlide = slider.find('li[data-zoom-slide="' + type + '"][data-zoom-' + type + '-id="' + active + '"]').index();
// Sync up the Zoom slider
slider.flexslider(activeSlide);
// If its a video, autoplay it
if (self.videos && type === "video") {
// Check which video trigger was clicked and sync it up
var videoId = btn.data('zoom-video-id');
var videoIndex = self.media.find('[data-zoom-video-id="' + videoId + '"]').index();
// Autoplay the video
var activeVideo = self.media.find('li').eq(videoIndex);
activeVideo.addClass('video-playing').find('iframe').attr('src', self.media.find('li').eq(videoIndex).find('iframe').attr('src') + '&autoplay=1');
}
event.preventDefault();
});
// --- DRAGGABLE --- //
if (this.draggable) {
// Make heros draggable
self.slides.draggable();
}
// --- CLOSE --- //
// Close overlay with escape key
$(document).keydown(function (e) {
if (e.keyCode == 27) {
self.close();
}
});
// Close overlay with [X] close button
self.closeBtn.on(self.clickEvent, function (e) {
self.close();
e.preventDefault();
});
// --- FLEXSLIDER --- //
// Initialize flexslider
this.media.flexslider({
controlsContainer: '[data-zoom-thumbs]',
controlNav: true,
manualControls: "[data-zoom-thumb]",
directionNav: false,
slideshow: false,
touch: self.draggable ? false : true, // keep touch off if image is draggable
start: function (slider) {
// Enable the nav buttons if theres multiple slides
if (slider.find('.slides > li + li').length > 0) {
$('.zoom-overlay .flex-direction-nav').removeClass('flex-disabled');
}
},
before: function (slider) {
// Center hero if it was dragged
slider.find('.slides').animate({'left': 0, 'top': 0});
// Stop video if its active
self.stopVideo();
var activeSlide = slider.animatingTo;
// flexSync up the main product-detail image slider
if (self.flexSync) {
if (slider.slides.eq(slider.animatingTo).data('zoom-slide') !== "video") {
$(self.flexSync).flexslider(activeSlide);
}
}
// Update active slide and type
var slide = slider.slides.eq(activeSlide);
var type = slide.data('zoom-slide'); // image or video
var active = slide.data('zoom-' + type + '-id');
newActiveSlide = slider.find('li[data-zoom-slide="' + type + '"][data-zoom-' + type + '-id="' + active + '"]').index();
slider.flexslider(newActiveSlide);
// Setting data-sync-with-zoom on alternates, will sync up the main product detail hero after changing alternates in zoom (without flexslider on hero)
if ($('[data-sync-with-zoom]').length > 0) ProductDetail.changeAlternate($('[data-sync-with-zoom]').eq(active - 1).find('a'));
}
});
// --- FLEXSLIDER ARROWS --- //
// Custom zoom next button
self.next.on(self.clickEvent, function (e) {
self.media.flexslider("next");
e.preventDefault();
});
// Custom zoom prev button
self.prev.on(self.clickEvent, function (e) {
self.media.flexslider("prev");
e.preventDefault();
});
// --- VIDEOS --- //
if (self.videos) {
// So we can easily reset back after killing iframes from playing (videos are "stopped" by removing the src of iframe)
self.video.each(function () {
var vid = $(this),
url = vid.attr('src');
vid.data('video', url);
});
// Activate the video
// Clicks on the invisible overlay on top of video set a class on li and hide the overlay
self.videoOverlay.on('click', function () { // this overlay is hidden on touch screens
var container = $(this).closest('[data-zoom-slide="video"]'),
videoIframe = container.find('iframe');
// Set a data attribute to show this li has a video playing in it (so we know to kill it when the slide is changed or overlays closed)
container.addClass('video-playing');
// Add autoplay to video
videoIframe.attr('src', videoIframe.data('video') + '&autoplay=1');
});
}
},
// --- ZOOM OPEN --- //
open: function () {
var self = this;
$('html').addClass('zoom-active');
// Add an extra class to overlay after a delay (for hiding the thumbs and info)
if (this.extraClass) {
setTimeout(function () {
self.container.addClass(self.extraClass);
}, self.extraClassDelay);
}
},
// --- ZOOM CLOSE --- //
close: function () {
var self = this;
// Hide the overlay
$('html').removeClass('zoom-active');
// Stop video if its playing
this.stopVideo();
// Remove extra class on overlay
if (this.extraClass) {
setTimeout(function () {
self.container.removeClass(self.extraClass);
}, 300);
}
},
// --- STOP VIDEOS --- //
// Stop videos that are playing in overlay
stopVideo: function () {
var video = $('[data-zoom-slide="video"]');
video.each(function () {
var vid = $(this);
// Kill the video by swapping out its src
if (vid.hasClass('video-playing') || $('html').hasClass('touch')) {
var videoIframe = vid.find('iframe');
vid.removeClass('video-playing');
// setTimeout so we dont see the video flash while transitioning to next/prev slide
setTimeout(function () {
videoIframe.attr('src', videoIframe.data('video'));
}, 300);
}
});
}
};