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); } }); } };