colorful-tones
5/12/2017 - 4:21 PM

Horizontal scrolling

Horizontal scrolling

//--------------------------------------------------------------
// HORIZONTAL SCROLL GRID
// 
// This technique takes previous, EXISTING laid out grid areas,
// using varying layout techniques, and uses a Desktop-first
// (i.e. max-width) approach to destroy those layouts, and 
// make the grid items instead be horizontally scrollable.
// We also use some Javascript to hide the scrollbar.
// Keep in mind that ANY grid that ONLY contains 3 items
// should have this technique ONLY applied on mobile.
//--------------------------------------------------------------

// Set up our local default variables
$hz-grid-gutter: 20px;
$hz-grid-item-width-tablet: 320px !default;
$hz-grid-item-width-mobile: 170px !default;
$hz-mobile-only: false !default;
$hz-tablet-only: false !default;

// This mixin accepts 4 parameters: 
// $number-of-items: 3 (default)
// $mobile-only: true or false (default)
// $tablet-width: default is set locally above
// $mobile-width: default is set locally above
// 
// Use Case.
// .horizontal-scroll-grid {
//     @include horizontal-scrolling-calculate-grid-items(3, true);
//     @include horizontal-scrolling-calculate-grid-items(5, $tablet-width: 210px);	
// }
@mixin horizontal-scrolling-calculate-grid-items($number-of-items: 3, $mobile-only: $hz-mobile-only, $tablet-only: $hz-tablet-only,  $tablet-width: $hz-grid-item-width-tablet, $mobile-width: $hz-grid-item-width-mobile) {
	
	// If we're on mobile only	
	@if $mobile-only {
		
		&.horizontal-scroll-grid-col-#{$number-of-items} {
			
			@include media( max-width ( $mq-horizontal-scroll-grid-mobile ) ) {
				@include margin-padding-reset;
				// Include our horiz scroll resets
				@include horizontal-scrolling-resets;

				overflow-x: scroll;
				
				.horizontal-scroll-grid-inner {
					width: calc( ( #{$mobile-width} * #{$number-of-items}.25 ) - 50px );
				} // .horizontal-scroll-grid-inner

				.column {
					margin-right: $hz-grid-gutter;
					width: $mobile-width - 20;
				} // .column
			}
			
		} // &.horizontal-scroll-grid-col-#{$number-of-items}
		
	} @elseif $tablet-only {
		
		&.horizontal-scroll-grid-col-#{$number-of-items} {
			
			@include media( min-width $phone-landscape max-width ( $mq-horizontal-scroll-grid-tablet ) ) {
				@include margin-padding-reset;
				// Include our horiz scroll resets
				@include horizontal-scrolling-resets;
				
				overflow-x: scroll;
				
				.horizontal-scroll-grid-inner {
					width: calc( ( #{$tablet-width} * #{$number-of-items}.25 ) - 80px );
				} // .horizontal-scroll-grid-inner

				.column {
					margin-right: $hz-grid-gutter;
					width: $tablet-width - 20;
				} // .column

			}
			
		} // &.horizontal-scroll-grid-col-#{$number-of-items}
		
	} @else {

		&.horizontal-scroll-grid-col-#{$number-of-items} {
			
			@include media( max-width ( $mq-horizontal-scroll-grid-tablet ) ) {
				@include margin-padding-reset;
				// Include our horiz scroll resets
				@include horizontal-scrolling-resets;
				
				overflow-x: scroll;
				
				.horizontal-scroll-grid-inner {
					width: calc( ( #{$tablet-width} * #{$number-of-items}.25 ) - 80px );
				} // .horizontal-scroll-grid-inner

				.column {
					margin-right: $hz-grid-gutter;
					width: $tablet-width - 20;
				} // .column

			}
			
			@include media( max-width ( $mq-horizontal-scroll-grid-mobile ) ) {
				
				.horizontal-scroll-grid-inner {
					width: calc( ( #{$mobile-width} * #{$number-of-items}.25 ) - 50px ) !important;
				} // .horizontal-scroll-grid-inner

				.column {
					margin-right: $hz-grid-gutter;
					width: $mobile-width - 20 !important;
				} // .column

			}
			
		} // &.horizontal-scroll-grid-col-#{$number-of-items}

	}

} // end @mixin horizontal-scrolling-calculate-grid-items

// This mixin does most of the destroying of preexisting grid layouts, as
// well as setting the baseline for new horizontal scrolling layout. It
// is included in the previous @mixin above.
@mixin horizontal-scrolling-resets() {
	
	.horizontal-scroll-grid-inner {
		display: block;
		padding-bottom: 0;
		padding-left: 4.5px;
		padding-top: 0;
		white-space: nowrap;
	} // .horizontal-scroll-grid-inner

	.column {
		display: inline-block;
		float: none;
		margin-bottom: 0;
		margin-left: -4.5px;
		padding-right: 0;
		vertical-align: top;
		white-space: normal;

		&:last-of-type {
			margin-right: 0;
		} // &:last-of-type

	} // .column
	
} // end @mixin horizontal-scrolling-resets()

// Here we do all our generation of class and horizontal scrolling magic!
.horizontal-scroll-enabled:not(.page-search-results) .horizontal-scroll-grid {
	
	@include horizontal-scrolling-calculate-grid-items(3, $mobile-only: true);
	@include horizontal-scrolling-calculate-grid-items(4);
	@include horizontal-scrolling-calculate-grid-items(5, $tablet-width: 210px);
	
} // .horizontal-scroll-grid

// Here we do all our generation of class and horizontal scrolling magic!
// On Search results we only apply horizontal scrolling paradigm for Tablet ONLY!
.page-search-results .horizontal-scroll-grid{
	
	@include horizontal-scrolling-calculate-grid-items(5, $mobile-only: false, $tablet-only: true, $tablet-width: 210px);	
	
} // .page-search-results .horizontal-scroll-grid

// The following wrapping element is added via JS for mobile and tablet ONLY.
// <div class="js-hidescrollbar"></div>
// See: horizontal-scroller.js:
.horizontal-scroll-enabled .js-hidescrollbar {
	margin-top: $gutter;
	overflow: hidden;
	
	// If immediately followed by a <footer> with a "See All" button
	// then let's add some breathing room.
	& + .section-footer .button,
	& + .section-footer-area .button, {
		margin-top: $gutter;
	} // + .section-footer .button

} // .horizontal-scroll-enabled .js-hidescrollbar
/**
 * This paradigm allows for horizontal scrolling on tablet and mobile.
 * Mostly the horizontal scrolling is accomplished with CSS. However, we
 * wanted to hide the scrollbars and do some height calculations on child
 * elements. Therefore, we enhanced it with some Javascript.
 * 
 * File horizontal-scroller.js
 * @author Damon Cook
 *
 */

window.WDS_HorizontalScroller = {};

( function ( window, $, that ) {
	
	// Set some sensible defaults.
	that.config = {
		mobile: 475,
		tablet: 1023,
		desktop: 1024,
		scrollBarHeight: 22,
		debounce: 250
	}

	// Constructor.
	that.init = function() {
		that.cache();

		if ( that.meetsRequirements() ) {
			that.bindEvents();
		}
	};

	// Cache all the things.
	that.cache = function() {
		that.$c = {
			window: $( window ),
			body: $( 'body' ),
			hScroll: $( '.horizontal-scroll-grid' ),
			hScrollSearchResults: 'page-search-results',
			hScrollBodyClass: 'horizontal-scroll-enabled',
			hScrollWrapperClass: 'js-hidescrollbar',
			hScrollMobileClass: 'horizontal-scroll-mobile',
			hScrollTabletClass: 'horizontal-scroll-tablet',
			hScrollGridItem: $( '.column' ),
			sectionsToAffectMobile: $( '.horizontal-scroll-grid-col-3' ),
			sectionsToAffectTablet: $( '.horizontal-scroll-grid-col-4, .horizontal-scroll-grid-col-5' ),
		};
	};

	// Do we meet the requirements?
	// In other words do we have .horizontal-scroll-grid on page?
	that.meetsRequirements = function() {
		return that.$c.hScroll.length;
	};
	
	// Combine all events.
	that.bindEvents = function() {
		// Watching resize can be taxing, therefore we use debounce.
		that.$c.window.on( 'load resize', that.debounce( that.sideScroller, that.config.debounce ) );
	}
	
	// This is where most of the watching and initializations lie.
	that.sideScroller = function() {
		
		// If we're on desktop...
		if ( that.screenGreaterThan( that.config.tablet ) ) {
			// Clean up anything we might have added on smaller screens.
			that.destroyMobile();
			that.destroyTablet();
			that.$c.body.removeClass( 'horizontal-scroll-enabled horizontal-scroll-tablet horizontal-scroll-mobile' );
			
			return;
		} else if ( that.$c.body.hasClass( that.$c.hScrollSearchResults ) && ! that.screenGreaterThan( that.config.mobile ) ) {
			// If we're on Search Results page and Tablet ONLY.
			// Clean up anything we might have added on smaller screens.
			that.destroyMobile();
			that.destroyTablet();
			that.$c.body.removeClass( 'horizontal-scroll-enabled horizontal-scroll-tablet horizontal-scroll-mobile' );
			
			return;
		} else {
			// Else, we're on mobile/tablet.
			that.addBodyClass();
			that.wrapSideScroller();
		}

	}
	
	// Cleanup any tablet stuff we did with resizing on smaller screens.
	that.destroyTablet = function() {
		
		// Find each horizontal scroll section and undo.
		that.$c.sectionsToAffectTablet.each( function() {

			if ( ! $( this ).parent().hasClass( that.$c.hScrollWrapperClass ) ) {
				return;
			}

			// Remove our extraneous wrapping <div class="js-hidescrollbar">
			$( this ).unwrap();
			// Remove the inline <div style="height: x;"> property.
			$( this ).css( 'height', '' );
		} );

	}
	
	// Cleanup any mobile stuff we did with resizing on smaller screens.
	that.destroyMobile = function() {
		
		// Find each horizontal scroll section and undo.
		that.$c.sectionsToAffectMobile.each( function() {
			
			if ( ! $( this ).parent().hasClass( that.$c.hScrollWrapperClass ) ) {
				return;
			}
			
			// Remove our extraneous wrapping <div class="js-hidescrollbar">
			$( this ).unwrap();
			// Remove the inline <div style="height: x;"> property.
			$( this ).css( 'height', '' );
		} );

	}
	
	// Wrap each found section in a new <div class="js=hidescrollbar"></div>
	// So we can then find the tallest child grid column, and use it to set the
	// height on a parent element, and then set a smaller height on our new <div>
	// to hide the previous <div>'s horizontal scrollbar.
	that.wrapSideScroller = function() {
		
		// If we have <body class="horizontal-scroll-enabled">
		if ( that.$c.body.hasClass( that.$c.hScrollBodyClass ) ) {
			
			// Cleanup any mobile specific stuff we did on resize.
			that.destroyMobile();
			
			// If we're on tablet...
			that.$c.sectionsToAffectTablet.each( function() {
				// If section doesn't already have the wrapper of <div class="js-hidescrollbar">
				if ( $( this ).parent().hasClass( that.$c.hScrollWrapperClass )  ) {
					return;
				}
				
				// Get the tallest .column in each section.
				var rowMax = that.rowHeight( $( this ) );
				
				// Set the parent element to the tallest height of its child .column minus the added scrollBarHeight
				$( this ).css( 'height', rowMax + that.config.scrollBarHeight );
				
				// Wrap another <div> around our entire section.
				$( this ).wrap( '<div class="js-hidescrollbar"></div>' );
				// Set the height on the new <div> to be less than its direct descendant.
				$( this ).parent().css( 'height', rowMax ); 
			} ); 
		} 
		
		// If we're on mobile...
		if ( that.$c.body.hasClass( that.$c.hScrollMobileClass ) ) {
			// If section doesn't already have the wrapper of <div class="js-hidescrollbar">
			that.$c.sectionsToAffectMobile.each( function() {
				if ( $( this ).parent().hasClass( that.$c.hScrollWrapperClass ) ) {
					return;
				}
				
				// Get the tallest .column in each section.
				var rowMax = that.rowHeight( $( this ) );
				
				// Set the parent element to the tallest height of its child .column minus the added scrollBarHeight
				$( this ).css( 'height', rowMax + that.config.scrollBarHeight );
				
				// Wrap another <div> around our entire section.
				$( this ).wrap( '<div class="js-hidescrollbar"></div>' );
				// Set the height on the new <div> to be less than its direct descendant.
				$( this ).parent().css( 'height', rowMax ); 
			} ); 
		}
	}
	
	// Add all our necessary classes to the <body>
	that.addBodyClass = function() {
		// If we're NOT on search results.
		if ( ! that.$c.body.hasClass( that.$c.hScrollSearchResults ) ) {
			// Add body class <body class="horizontal-scroll-enabled">
			that.$c.body.addClass( that.$c.hScrollBodyClass );
			
			// If we're on tablet...
			if ( that.screenGreaterThan( that.config.mobile ) ) {
				// Add body class <body class="horizontal-scroll-tablet">
				that.$c.body.addClass( that.$c.hScrollTabletClass );
				// Remove body class <body class="horizontal-scroll-mobile">
				that.$c.body.removeClass( that.$c.hScrollMobileClass );
			// Else, we're on mobile, but not on search results...
			} else {
				// Add body class <body class="horizontal-scroll-mobile">
				that.$c.body.addClass( that.$c.hScrollMobileClass );
				// Remove body class <body class="horizontal-scroll-tablet">
				that.$c.body.removeClass( that.$c.hScrollTabletClass );
			}
		// Else, we're on search results...
		} else {
			// If screen is tablet.
			if ( that.screenGreaterThan( that.config.mobile ) ) {
				// Add body class <body class="horizontal-scroll-enabled">
				that.$c.body.addClass( that.$c.hScrollBodyClass );
				// Add body class <body class="horizontal-scroll-tablet">
				that.$c.body.addClass( that.$c.hScrollTabletClass );
				// Remove body class <body class="horizontal-scroll-mobile">
				that.$c.body.removeClass( that.$c.hScrollMobileClass );
			// Else, we're on mobile, and want to cleanup...
			} else {
				// Remove body class <body class="horizontal-scroll-enabled">
				that.$c.body.removeClass( that.$c.hScrollBodyClass );
				// Remove body class <body class="horizontal-scroll-mobile">
				that.$c.body.removeClass( that.$c.hScrollMobileClass );
				// Remove body class <body class="horizontal-scroll-tablet">
				that.$c.body.removeClass( that.$c.hScrollTabletClass );
			}
		}
	}
	
	// Get the .outerHeight() of passed item.
	that.thisHeight = function() {
		return $( this ).outerHeight();
	}
	
	// When passed an element it'll find all child .column within,
	// and return $rowMax = the tallest column.
	that.rowHeight = function( $element ) {
		// get the height of the tallest item within each group
		var rowMax = Math.max.apply( Math, $element.find( that.$c.hScrollGridItem ).map( that.thisHeight ) );
		
		return rowMax;
	}
	
	// Calculate if screen size is larger than the passed breakpoint.
	that.screenGreaterThan = function( breakpoint ) {
		var windowWidth = window.innerWidth;

		if ( parseInt( windowWidth ) >= parseInt( breakpoint ) ) {
			return true;
		} else {
			return false;
		}
	};
	
	// Returns a function, that, as long as it continues to be invoked, will not
	// be triggered. The function will be called after it stops being called for
	// N milliseconds. If `immediate` is passed, trigger the function on the
	// leading edge, instead of the trailing.
	that.debounce = function debounce( func, wait, immediate ) {
		var timeout;
		
		return function() {
			var context = this, args = arguments;
			var later = function() {
				timeout = null;
				if ( !immediate ) func.apply( context, args );
			};
			var callNow = immediate && !timeout;
			clearTimeout( timeout );
			timeout = setTimeout( later, wait );
			if ( callNow ) func.apply( context, args );
		};
	};

	// Engage!
	$( that.init );

} )( window, jQuery, window.WDS_HorizontalScroller );