// ==============================================
// Pointer abstraction
// ==============================================
/**
* This class provides an easy and abstracted mechanism to determine the
* best pointer behavior to use -- that is, is the user currently interacting
* with their device in a touch manner, or using a mouse.
*
* Since devices may use either touch or mouse or both, there is no way to
* know the user's preferred pointer type until they interact with the site.
*
* To accommodate this, this class provides a method and two events
* to determine the user's preferred pointer type.
*
* - getPointer() returns the last used pointer type, or, if the user has
* not yet interacted with the site, falls back to a Modernizr test.
*
* - The mouse-detected event is triggered on the window object when the user
* is using a mouse pointer input, or has switched from touch to mouse input.
* It can be observed in this manner: $j(window).on('mouse-detected', function(event) { // custom code });
*
* - The touch-detected event is triggered on the window object when the user
* is using touch pointer input, or has switched from mouse to touch input.
* It can be observed in this manner: $j(window).on('touch-detected', function(event) { // custom code });
*/
var PointerManager = {
MOUSE_POINTER_TYPE: 'mouse',
TOUCH_POINTER_TYPE: 'touch',
POINTER_EVENT_TIMEOUT_MS: 500,
standardTouch: false,
touchDetectionEvent: null,
lastTouchType: null,
pointerTimeout: null,
pointerEventLock: false,
getPointerEventsSupported: function() {
return this.standardTouch;
},
getPointerEventsInputTypes: function() {
if (window.navigator.pointerEnabled) { //IE 11+
//return string values from http://msdn.microsoft.com/en-us/library/windows/apps/hh466130.aspx
return {
MOUSE: 'mouse',
TOUCH: 'touch',
PEN: 'pen'
};
} else if (window.navigator.msPointerEnabled) { //IE 10
//return numeric values from http://msdn.microsoft.com/en-us/library/windows/apps/hh466130.aspx
return {
MOUSE: 0x00000004,
TOUCH: 0x00000002,
PEN: 0x00000003
};
} else { //other browsers don't support pointer events
return {}; //return empty object
}
},
/**
* If called before init(), get best guess of input pointer type
* using Modernizr test.
* If called after init(), get current pointer in use.
*/
getPointer: function() {
// On iOS devices, always default to touch, as this.lastTouchType will intermittently return 'mouse' if
// multiple touches are triggered in rapid succession in Safari on iOS
if(Modernizr.ios) {
return this.TOUCH_POINTER_TYPE;
}
if(this.lastTouchType) {
return this.lastTouchType;
}
return Modernizr.touch ? this.TOUCH_POINTER_TYPE : this.MOUSE_POINTER_TYPE;
},
setPointerEventLock: function() {
this.pointerEventLock = true;
},
clearPointerEventLock: function() {
this.pointerEventLock = false;
},
setPointerEventLockTimeout: function() {
var that = this;
if(this.pointerTimeout) {
clearTimeout(this.pointerTimeout);
}
this.setPointerEventLock();
this.pointerTimeout = setTimeout(function() { that.clearPointerEventLock(); }, this.POINTER_EVENT_TIMEOUT_MS);
},
triggerMouseEvent: function(originalEvent) {
if(this.lastTouchType == this.MOUSE_POINTER_TYPE) {
return; //prevent duplicate events
}
this.lastTouchType = this.MOUSE_POINTER_TYPE;
$j(window).trigger('mouse-detected', originalEvent);
},
triggerTouchEvent: function(originalEvent) {
if(this.lastTouchType == this.TOUCH_POINTER_TYPE) {
return; //prevent duplicate events
}
this.lastTouchType = this.TOUCH_POINTER_TYPE;
$j(window).trigger('touch-detected', originalEvent);
},
initEnv: function() {
if (window.navigator.pointerEnabled) {
this.standardTouch = true;
this.touchDetectionEvent = 'pointermove';
} else if (window.navigator.msPointerEnabled) {
this.standardTouch = true;
this.touchDetectionEvent = 'MSPointerMove';
} else {
this.touchDetectionEvent = 'touchstart';
}
},
wirePointerDetection: function() {
var that = this;
if(this.standardTouch) { //standard-based touch events. Wire only one event.
//detect pointer event
$j(window).on(this.touchDetectionEvent, function(e) {
switch(e.originalEvent.pointerType) {
case that.getPointerEventsInputTypes().MOUSE:
that.triggerMouseEvent(e);
break;
case that.getPointerEventsInputTypes().TOUCH:
case that.getPointerEventsInputTypes().PEN:
// intentionally group pen and touch together
that.triggerTouchEvent(e);
break;
}
});
} else { //non-standard touch events. Wire touch and mouse competing events.
//detect first touch
$j(window).on(this.touchDetectionEvent, function(e) {
if(that.pointerEventLock) {
return;
}
that.setPointerEventLockTimeout();
that.triggerTouchEvent(e);
});
//detect mouse usage
$j(document).on('mouseover', function(e) {
if(that.pointerEventLock) {
return;
}
that.setPointerEventLockTimeout();
that.triggerMouseEvent(e);
});
}
},
init: function() {
this.initEnv();
this.wirePointerDetection();
}
};
// ==============================================
// jQuery Init
// ==============================================
// Use $j(document).ready() because Magento executes Prototype inline
$j(document).ready(function () {
// ==============================================
// Basic variables
// ==============================================
var breakpointMedium = 768;
var isResponsive = $j('body').hasClass('responsive');
// ==============================================
// UI Pattern - ToggleSingle
// ==============================================
// Use this plugin to toggle the visibility of content based on a toggle link/element.
// This pattern differs from the accordion functionality in the Toggle pattern in that each toggle group acts
// independently of the others. It is named so as not to be confused with the Toggle pattern below
//
// This plugin requires a specific markup structure. The plugin expects a set of elements that it
// will use as the toggle link. It then hides all immediately following siblings and toggles the sibling's
// visibility when the toggle link is clicked.
//
// Example markup:
//
//
Trigger
//
Content that should show when
//
//
// JS: jQuery('.block-title').toggleSingle();
//
// Options:
// destruct: defaults to false, but if true, the plugin will remove itself, display content, and remove event handlers
jQuery.fn.toggleSingle = function (options) {
// passing destruct: true allows
var settings = $j.extend({
destruct: false
}, options);
return this.each(function () {
if (!settings.destruct) {
$j(this).on('click', function () {
$j(this)
.toggleClass('active')
.next()
.toggleClass('no-display');
});
// Hide the content
$this = $j(this);
if (!$this.hasClass('active'))
{
$this.next().addClass('no-display');
}
//$j(this).next().addClass('no-display');
} else {
// Remove event handler so that the toggle link can no longer be used
$j(this).off('click');
// Remove all classes that were added by this plugin
$j(this)
.removeClass('active')
.next()
.removeClass('no-display');
}
});
}
// ==============================================
// UI Pattern - Toggle Content (tabs and accordions in one setup)
// ==============================================
$j('.toggle-content').each(function () {
var wrapper = jQuery(this);
var hasTabs = wrapper.hasClass('tabs');
var hasAccordion = wrapper.hasClass('accordion');
var startOpen = wrapper.hasClass('open');
var dl = wrapper.children('dl:first');
var dts = dl.children('dt');
var panes = dl.children('dd');
var groups = new Array(dts, panes);
//Create a ul for tabs if necessary.
if (hasTabs) {
var ul = jQuery('');
dts.each(function () {
var dt = jQuery(this);
var li = jQuery('');
li.html(dt.html());
ul.append(li);
});
ul.insertBefore(dl);
var lis = ul.children();
groups.push(lis);
}
//Add "last" classes.
var i;
for (i = 0; i < groups.length; i++) {
groups[i].filter(':last').addClass('last');
}
function toggleClasses(clickedItem, group) {
var index = group.index(clickedItem);
var i;
for (i = 0; i < groups.length; i++) {
groups[i].removeClass('current');
groups[i].eq(index).addClass('current');
}
}
//Toggle on tab (dt) click.
dts.on('click', function (e) {
//They clicked the current dt to close it. Restore the wrapper to unclicked state.
if (jQuery(this).hasClass('current') && wrapper.hasClass('accordion-open')) {
wrapper.removeClass('accordion-open');
} else {
//They're clicking something new. Reflect the explicit user interaction.
wrapper.addClass('accordion-open');
}
toggleClasses(jQuery(this), dts);
});
//Toggle on tab (li) click.
if (hasTabs) {
lis.on('click', function (e) {
toggleClasses(jQuery(this), lis);
});
//Open the first tab.
lis.eq(0).trigger('click');
}
//Open the first accordion if desired.
if (startOpen) {
dts.eq(0).trigger('click');
}
});
// ==============================================
// Layered Navigation Block
// ==============================================
// On product list pages, we want to show the layered nav/category menu immediately above the product list
if (isResponsive)
{
if ($j('.block-layered-nav').length && $j('.category-products').length)
{
enquire.register('screen and (max-width: ' + (breakpointMedium - 1) + 'px)', {
match: function () {
$j('.block-layered-nav').insertBefore($j('.category-products'))
},
unmatch: function () {
// Move layered nav back to left column
$j('.block-layered-nav').insertAfter($j('#layered-nav-marker'))
}
});
}
}
// ==============================================
// Blocks collapsing (on smaller viewports)
// ==============================================
if (isResponsive)
{
enquire.register('(max-width: ' + (breakpointMedium - 1) + 'px)', {
setup: function () {
this.toggleElements = $j(
'.sidebar .block:not(.block-layered-nav) .block-title, ' +
'.block-layered-nav .block-subtitle--filter, ' +
//'.block-layered-nav .block-title, ' + //Currently this element is hidden in mobile view
'.mobile-collapsible .block-title'
);
},
match: function () {
this.toggleElements.toggleSingle();
},
unmatch: function () {
this.toggleElements.toggleSingle({destruct: true});
}
});
}
// ==============================================
// Blocks collapsing on all viewports
// ==============================================
//Exclude elements with ".mobile-collapsible" for backward compatibility
$j('.collapsible:not(.mobile-collapsible) .block-title').toggleSingle();
}); //end: on document ready
// ==============================================
// PDP - image zoom - needs to be available outside document.ready scope
// ==============================================
var ProductMediaManager = {
IMAGE_ZOOM_THRESHOLD: 20,
imageWrapper: null,
destroyZoom: function() {
//Custom modification. Code not needed if elevateZoom plugin not being used.
//$j('.zoomContainer').remove();
//$j('.product-image-gallery .gallery-image').removeData('elevateZoom');
},
createZoom: function(image) {
// Destroy since zoom shouldn't be enabled under certain conditions
ProductMediaManager.destroyZoom();
//Custom modification
//To use this part, required: PointerManager, Modernizr.mq, bp variable
/*
if(
// Don't use zoom on devices where touch has been used
PointerManager.getPointer() == PointerManager.TOUCH_POINTER_TYPE
// Don't use zoom when screen is small, or else zoom window shows outside body
|| Modernizr.mq("screen and (max-width:" + bp.medium + "px)")
) {
return; // zoom not enabled
}
*/
if(image.length <= 0) { //no image found
return;
}
if(image[0].naturalWidth && image[0].naturalHeight) {
var widthDiff = image[0].naturalWidth - image.width() - ProductMediaManager.IMAGE_ZOOM_THRESHOLD;
var heightDiff = image[0].naturalHeight - image.height() - ProductMediaManager.IMAGE_ZOOM_THRESHOLD;
if(widthDiff < 0 && heightDiff < 0) {
//image not big enough
image.parents('.product-image').removeClass('zoom-available');
return;
} else {
image.parents('.product-image').addClass('zoom-available');
}
}
//Custom modification. Code not needed if elevateZoom plugin not being used.
//image.elevateZoom();
},
swapImage: function(targetImage) {
targetImage = $j(targetImage);
targetImage.addClass('gallery-image');
//Custom modification. Code not needed if elevateZoom plugin not being used.
//ProductMediaManager.destroyZoom();
var imageGallery = $j('.product-image-gallery');
if(targetImage[0].complete) { //image already loaded -- swap immediately
imageGallery.find('.gallery-image').removeClass('visible');
//move target image to correct place, in case it's necessary
imageGallery.append(targetImage);
//reveal new image
targetImage.addClass('visible');
//Custom modification. Code not needed if elevateZoom plugin not being used.
//wire zoom on new image
//ProductMediaManager.createZoom(targetImage);
//Custom modification
//Trigger event to know when image was changed
$j(document).trigger('product-media-manager-image-updated', {img: targetImage} );
} else { //need to wait for image to load
//add spinner
imageGallery.addClass('loading');
//move target image to correct place, in case it's necessary
imageGallery.append(targetImage);
//wait until image is loaded
imagesLoaded(targetImage, function() {
//remove spinner
imageGallery.removeClass('loading');
//hide old image
imageGallery.find('.gallery-image').removeClass('visible');
//reveal new image
targetImage.addClass('visible');
//Custom modification. Code not needed if elevateZoom plugin not being used.
//wire zoom on new image
//ProductMediaManager.createZoom(targetImage);
//Custom modification
//Trigger event to know when image was changed
$j(document).trigger('product-media-manager-image-updated', {img: targetImage} );
});
}
},
wireThumbnails: function() {
//Custom modification. Code not needed if elevateZoom plugin not being used.
/*
//trigger image change event on thumbnail click
$j('.product-image-thumbs .thumb-link').click(function(e) {
e.preventDefault();
var jlink = $j(this);
var target = $j('#image-' + jlink.data('image-index'));
ProductMediaManager.swapImage(target);
});
*/
},
initZoom: function() {
//Custom modification. Code not needed if elevateZoom plugin not being used.
//ProductMediaManager.createZoom($j(".gallery-image.visible")); //set zoom on first image
},
init: function() {
//Custom modification. Code not needed if elevateZoom plugin not being used.
/*
ProductMediaManager.imageWrapper = $j('.product-img-box');
// Re-initialize zoom on viewport size change since resizing causes problems with zoom and since smaller
// viewport sizes shouldn't have zoom
$j(window).on('delayed-resize', function(e, resizeEvent) {
ProductMediaManager.initZoom();
});
ProductMediaManager.initZoom();
ProductMediaManager.wireThumbnails();
*/
$j(document).trigger('product-media-loaded', ProductMediaManager);
}
};
$j(document).ready(function() {
ProductMediaManager.init();
});