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