Refactor. Fix 🐛 calculating widths.

Refactor the getItemPosition method.
Fix `_getOuterWidth` for Firefox because offsetWidth/Height return
integers for Firefox. Now it uses `getBoundingClientRect` to determine
the width.
Up the column threshold from `0.03` to `0.3` because Safari (desktop &
mobile) return integers for the client rect and offsetWidth.
Add a note about the native Android browser with 4.1.0 and 4.1.1 where
it incorrectly thinks it supports unprefixed transitions.
pull/56/head
Glen Cheney 10 years ago
parent e6cb28bd1e
commit dcb16daf28

@ -47,10 +47,15 @@ function dashify( prop ) {
var TRANSITION = Modernizr.prefixed('transition');
var TRANSITION_DELAY = Modernizr.prefixed('transitionDelay');
var TRANSITION_DURATION = Modernizr.prefixed('transitionDuration');
// Note(glen): Stock Android 4.1.x browser will fail here because it wrongly
// says it supports non-prefixed transitions.
// https://github.com/Modernizr/Modernizr/issues/897
var TRANSITIONEND = {
'WebkitTransition' : 'webkitTransitionEnd',
'transition' : 'transitionend'
}[ TRANSITION ];
var TRANSFORM = Modernizr.prefixed('transform');
var CSS_TRANSFORM = dashify(TRANSFORM);
@ -58,6 +63,7 @@ var CSS_TRANSFORM = dashify(TRANSFORM);
var CAN_TRANSITION_TRANSFORMS = Modernizr.csstransforms && Modernizr.csstransitions;
var HAS_TRANSFORMS_3D = Modernizr.csstransforms3d;
var SHUFFLE = 'shuffle';
var COLUMN_THRESHOLD = 0.3;
// Configurable. You can change these constants to fit your application.
// The default scale and concealed scale, however, have to be different values.
@ -112,6 +118,14 @@ function defer(fn, context, wait) {
return setTimeout( $.proxy( fn, context ), wait || 0 );
}
function arrayMax( array ) {
return Math.max.apply( Math, array );
}
function arrayMin( array ) {
return Math.min.apply( Math, array );
}
// Used for unique instance variables
var id = 0;
@ -190,37 +204,40 @@ Shuffle._getItemTransformString = function(x, y, scale) {
};
Shuffle._getPreciseDimension = function( element, style ) {
var dimension;
if ( window.getComputedStyle ) {
dimension = window.getComputedStyle( element, null )[ style ];
} else {
dimension = $( element ).css( style );
}
return parseFloat( dimension );
/**
* Retrieve the computed style for an element, parsed as a float. This should
* not be used for width or height values because jQuery mangles them and they
* are not precise enough.
* @param {Element} element Element to get style for.
* @param {string} style Style property.
* @return {number} The parsed computed value or zero if that fails because IE
* will return 'auto' when the element doesn't have margins instead of
* the computed style.
* @private
*/
Shuffle._getNumberStyle = function( element, style ) {
return parseFloat( $( element ).css( style ) ) || 0;
};
/**
* Returns the outer width of an element, optionally including its margins.
* the client rect is used instead of `offsetWidth` because Firefox returns an
* integer value instead of a double.
* @param {Element} element The element.
* @param {boolean} [includeMargins] Whether to include margins. Default is false.
* @return {number} The width.
*/
Shuffle._getOuterWidth = function( element, includeMargins ) {
var width = element.offsetWidth;
var rect = element.getBoundingClientRect();
var width = rect.right - rect.left;
// Use jQuery here because it uses getComputedStyle internally and is
// cross-browser. Using the style property of the element will only work
// if there are inline styles.
if (includeMargins) {
var styles = $(element).css(['marginLeft', 'marginRight']);
// Defaults to zero if parsing fails because IE will return 'auto' when
// the element doesn't have margins instead of the computed style.
var marginLeft = parseFloat(styles.marginLeft) || 0;
var marginRight = parseFloat(styles.marginRight) || 0;
if ( includeMargins ) {
var marginLeft = Shuffle._getNumberStyle( element, 'marginLeft');
var marginRight = Shuffle._getNumberStyle( element, 'marginRight');
width += marginLeft + marginRight;
}
@ -237,10 +254,9 @@ Shuffle._getOuterWidth = function( element, includeMargins ) {
Shuffle._getOuterHeight = function( element, includeMargins ) {
var height = element.offsetHeight;
if (includeMargins) {
var styles = $(element).css(['marginTop', 'marginBottom']);
var marginTop = parseFloat(styles.marginTop) || 0;
var marginBottom = parseFloat(styles.marginBottom) || 0;
if ( includeMargins ) {
var marginTop = Shuffle._getNumberStyle( element, 'marginTop');
var marginBottom = Shuffle._getNumberStyle( element, 'marginBottom');
height += marginTop + marginBottom;
}
@ -550,12 +566,12 @@ Shuffle.prototype._getConcealedItems = function() {
/**
* Returns the column size, based on column width and sizer options.
* @param {number} gutterSize Size of the gutters.
* @param {number} containerWidth Size of the parent container.
* @param {number} gutterSize Size of the gutters.
* @return {number}
* @private
*/
Shuffle.prototype._getColumnSize = function( gutterSize, containerWidth ) {
Shuffle.prototype._getColumnSize = function( containerWidth, gutterSize ) {
var size;
// If the columnWidth property is a function, then the grid is fluid
@ -564,7 +580,7 @@ Shuffle.prototype._getColumnSize = function( gutterSize, containerWidth ) {
// columnWidth option isn't a function, are they using a sizing element?
} else if ( this.useSizer ) {
size = Shuffle._getPreciseDimension(this.sizer, 'width');
size = Shuffle._getOuterWidth(this.sizer);
// if not, how about the explicitly set option?
} else if ( this.columnWidth ) {
@ -599,7 +615,7 @@ Shuffle.prototype._getGutterSize = function( containerWidth ) {
if ( $.isFunction( this.gutterWidth ) ) {
size = this.gutterWidth(containerWidth);
} else if ( this.useSizer ) {
size = Shuffle._getPreciseDimension(this.sizer, 'marginLeft');
size = Shuffle._getNumberStyle(this.sizer, 'marginLeft');
} else {
size = this.gutterWidth;
}
@ -615,11 +631,11 @@ Shuffle.prototype._getGutterSize = function( containerWidth ) {
Shuffle.prototype._setColumns = function( theContainerWidth ) {
var containerWidth = theContainerWidth || Shuffle._getOuterWidth( this.element );
var gutter = this._getGutterSize( containerWidth );
var columnWidth = this._getColumnSize( gutter, containerWidth );
var columnWidth = this._getColumnSize( containerWidth, gutter );
var calculatedColumns = (containerWidth + gutter) / columnWidth;
// Widths given from getComputedStyle are not precise enough...
if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < 0.03 ) {
if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < COLUMN_THRESHOLD ) {
// e.g. calculatedColumns = 11.998876
calculatedColumns = Math.round( calculatedColumns );
}
@ -642,7 +658,7 @@ Shuffle.prototype._setContainerSize = function() {
* @private
*/
Shuffle.prototype._getContainerSize = function() {
return Math.max.apply( Math, this.colYs );
return arrayMax( this.colYs );
};
/**
@ -653,6 +669,18 @@ Shuffle.prototype._fire = function( name, args ) {
};
/**
* Zeros out the y columns array, which is used to determine item placement.
* @private
*/
Shuffle.prototype._resetCols = function() {
var i = this.cols;
this.colYs = [];
while (i--) {
this.colYs.push( 0 );
}
};
/**
* Loops through each item that should be shown and calculates the x, y position.
* @param {Array.<Element>} items Array of items that will be shown/layed out in order in their array.
@ -661,42 +689,7 @@ Shuffle.prototype._fire = function( name, args ) {
*/
Shuffle.prototype._layout = function( items, isOnlyPosition ) {
each(items, function( item ) {
var $item = $(item);
var itemData = $item.data();
var currPos = itemData.position;
var currScale = itemData.scale;
var pos = this._getItemPosition( $item );
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
if ( pos.x === currPos.x && pos.y === currPos.y && currScale === DEFAULT_SCALE ) {
return;
}
var transitionObj = {
$item: $item,
x: pos.x,
y: pos.y,
scale: DEFAULT_SCALE,
opacity: isOnlyPosition ? 0 : 1,
skipTransition: !!isOnlyPosition,
callfront: function() {
if ( !isOnlyPosition ) {
$item.css( 'visibility', 'visible' );
}
},
callback: function() {
if ( isOnlyPosition ) {
$item.css( 'visibility', 'hidden' );
}
}
};
this.styleQueue.push( transitionObj );
this._layoutItem( item, isOnlyPosition );
}, this);
// `_layout` always happens after `_shrink`, so it's safe to process the style
@ -707,88 +700,135 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
this._setContainerSize();
};
/**
* Zeros out the y columns array, which is used to determine item placement.
* @private
*/
Shuffle.prototype._resetCols = function() {
var i = this.cols;
this.colYs = [];
while (i--) {
this.colYs.push( 0 );
Shuffle.prototype._layoutItem = function( item, isOnlyPosition ) {
var $item = $(item);
var itemData = $item.data();
var currPos = itemData.position;
var currScale = itemData.scale;
var itemSize = {
width: Shuffle._getOuterWidth( item, true ),
height: Shuffle._getOuterHeight( item, true )
};
var pos = this._getItemPosition( itemSize );
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
if ( pos.x === currPos.x && pos.y === currPos.y && currScale === DEFAULT_SCALE ) {
return;
}
var transitionObj = {
$item: $item,
x: pos.x,
y: pos.y,
scale: DEFAULT_SCALE,
opacity: isOnlyPosition ? 0 : 1,
skipTransition: !!isOnlyPosition,
callfront: function() {
if ( !isOnlyPosition ) {
$item.css( 'visibility', 'visible' );
}
},
callback: function() {
if ( isOnlyPosition ) {
$item.css( 'visibility', 'hidden' );
}
}
};
this.styleQueue.push( transitionObj );
};
Shuffle.prototype._getItemPosition = function( $item ) {
var itemWidth = Shuffle._getOuterWidth( $item[0], true );
var columnSpan = itemWidth / this.colWidth;
Shuffle.prototype._getItemPosition = function( itemSize ) {
var columnSpan = itemSize.width / this.colWidth;
// If the difference between the rounded column span number and the
// calculated column span number is really small, round the number to
// make it fit.
if ( Math.abs(Math.round(columnSpan) - columnSpan) < 0.03 ) {
if ( Math.abs(Math.round(columnSpan) - columnSpan) < COLUMN_THRESHOLD ) {
// e.g. columnSpan = 4.0089945390298745
columnSpan = Math.round( columnSpan );
}
// How many columns does this item span. Ensure it's not more than the
// amount of columns in the whole layout.
var colSpan = Math.min( Math.ceil(columnSpan), this.cols );
// Ensure the column span is not more than the amount of columns in the whole layout.
columnSpan = Math.min( Math.ceil(columnSpan), this.cols );
var setY = this._getColumnSet( columnSpan, this.cols );
// Finds the index of the smallest number in the set.
var shortColumnIndex = this._getShortColumn( setY, this.buffer );
// Position the item
var position = {
x: Math.round( (this.colWidth * shortColumnIndex) + this.offset.left ),
y: Math.round( setY[shortColumnIndex] + this.offset.top )
};
// Update the columns array with the new values for each column.
// e.g. before the update the columns could be [250, 0, 0, 0] for an item
// which spans 2 columns. After it would be [250, itemHeight, itemHeight, 0].
var setHeight = setY[shortColumnIndex] + itemSize.height;
var setSpan = this.cols + 1 - setY.length;
for ( var i = 0; i < setSpan; i++ ) {
this.colYs[ shortColumnIndex + i ] = setHeight;
}
return position;
};
/**
* Retrieves the column set to use for placement.
* @param {number} columnSpan The number of columns this current item spans.
* @param {number} columns The total columns in the grid.
* @return {Array.<number>} An array of numbers represeting the column set.
* @private
*/
Shuffle.prototype._getColumnSet = function( columnSpan, columns ) {
// The item spans only one column.
if ( colSpan === 1 ) {
return this._placeItem( $item, this.colYs );
if ( columnSpan === 1 ) {
return this.colYs;
// The item spans more than one column, figure out how many different
// places it could fit horizontally
} else {
var groupCount = this.cols + 1 - colSpan;
var groupCount = columns + 1 - columnSpan;
var groupY = [];
var groupColY;
var i;
// for each group potential horizontal position
for ( i = 0; i < groupCount; i++ ) {
for ( var i = 0; i < groupCount; i++ ) {
// make an array of colY values for that one group
groupColY = this.colYs.slice( i, i + colSpan );
groupColY = this.colYs.slice( i, i + columnSpan );
// and get the max value of the array
groupY[i] = Math.max.apply( Math, groupColY );
groupY[i] = arrayMax( groupColY );
}
return this._placeItem( $item, groupY );
return groupY;
}
};
// TODO: Cleanup and combine with _getItemPosition.
Shuffle.prototype._placeItem = function( $item, setY ) {
// get the minimum Y value from the columns
var minimumY = Math.min.apply( Math, setY );
var shortCol = 0;
// Find index of short column, the first from the left where this item will go
// if ( setY[i] === minimumY ) requires items' height to be exact every time.
// The buffer value is very useful when the height is a percentage of the width
for (var i = 0, len = setY.length; i < len; i++) {
if ( setY[i] >= minimumY - this.buffer && setY[i] <= minimumY + this.buffer ) {
shortCol = i;
break;
/**
* Find index of short column, the first from the left where this item will go.
*
* @param {Array.<number>} positions The array to search for the smallest number.
* @param {number} buffer Optional buffer which is very useful when the height
* is a percentage of the width.
* @return {number} Index of the short column.
* @private
*/
Shuffle.prototype._getShortColumn = function( positions, buffer ) {
var minPosition = arrayMin( positions );
for (var i = 0, len = positions.length; i < len; i++) {
if ( positions[i] >= minPosition - buffer && positions[i] <= minPosition + buffer ) {
return i;
}
}
// Position the item
var position = {
x: Math.round( (this.colWidth * shortCol) + this.offset.left ),
y: Math.round( minimumY + this.offset.top )
};
// Apply setHeight to necessary columns
var setHeight = minimumY + Shuffle._getOuterHeight( $item[0], true ),
setSpan = this.cols + 1 - len;
for ( i = 0; i < setSpan; i++ ) {
this.colYs[ shortCol + i ] = setHeight;
}
return position;
return 0;
};
/**

File diff suppressed because one or more lines are too long

@ -15,7 +15,7 @@ window.Modernizr=function(a,b,c){function z(a){j.cssText=a}function A(a,b){retur
if (typeof define === 'function' && define.amd) {
define(['jquery', 'modernizr'], factory);
} else {
factory(window.$, window.Modernizr);
factory(window.jQuery, window.Modernizr);
}
})(function($, Modernizr, undefined) {
@ -53,10 +53,15 @@ function dashify( prop ) {
var TRANSITION = Modernizr.prefixed('transition');
var TRANSITION_DELAY = Modernizr.prefixed('transitionDelay');
var TRANSITION_DURATION = Modernizr.prefixed('transitionDuration');
// Note(glen): Stock Android 4.1.x browser will fail here because it wrongly
// says it supports non-prefixed transitions.
// https://github.com/Modernizr/Modernizr/issues/897
var TRANSITIONEND = {
'WebkitTransition' : 'webkitTransitionEnd',
'transition' : 'transitionend'
}[ TRANSITION ];
var TRANSFORM = Modernizr.prefixed('transform');
var CSS_TRANSFORM = dashify(TRANSFORM);
@ -64,6 +69,7 @@ var CSS_TRANSFORM = dashify(TRANSFORM);
var CAN_TRANSITION_TRANSFORMS = Modernizr.csstransforms && Modernizr.csstransitions;
var HAS_TRANSFORMS_3D = Modernizr.csstransforms3d;
var SHUFFLE = 'shuffle';
var COLUMN_THRESHOLD = 0.3;
// Configurable. You can change these constants to fit your application.
// The default scale and concealed scale, however, have to be different values.
@ -118,6 +124,14 @@ function defer(fn, context, wait) {
return setTimeout( $.proxy( fn, context ), wait || 0 );
}
function arrayMax( array ) {
return Math.max.apply( Math, array );
}
function arrayMin( array ) {
return Math.min.apply( Math, array );
}
// Used for unique instance variables
var id = 0;
@ -196,37 +210,40 @@ Shuffle._getItemTransformString = function(x, y, scale) {
};
Shuffle._getPreciseDimension = function( element, style ) {
var dimension;
if ( window.getComputedStyle ) {
dimension = window.getComputedStyle( element, null )[ style ];
} else {
dimension = $( element ).css( style );
}
return parseFloat( dimension );
/**
* Retrieve the computed style for an element, parsed as a float. This should
* not be used for width or height values because jQuery mangles them and they
* are not precise enough.
* @param {Element} element Element to get style for.
* @param {string} style Style property.
* @return {number} The parsed computed value or zero if that fails because IE
* will return 'auto' when the element doesn't have margins instead of
* the computed style.
* @private
*/
Shuffle._getNumberStyle = function( element, style ) {
return parseFloat( $( element ).css( style ) ) || 0;
};
/**
* Returns the outer width of an element, optionally including its margins.
* the client rect is used instead of `offsetWidth` because Firefox returns an
* integer value instead of a double.
* @param {Element} element The element.
* @param {boolean} [includeMargins] Whether to include margins. Default is false.
* @return {number} The width.
*/
Shuffle._getOuterWidth = function( element, includeMargins ) {
var width = element.offsetWidth;
var rect = element.getBoundingClientRect();
var width = rect.right - rect.left;
// Use jQuery here because it uses getComputedStyle internally and is
// cross-browser. Using the style property of the element will only work
// if there are inline styles.
if (includeMargins) {
var styles = $(element).css(['marginLeft', 'marginRight']);
// Defaults to zero if parsing fails because IE will return 'auto' when
// the element doesn't have margins instead of the computed style.
var marginLeft = parseFloat(styles.marginLeft) || 0;
var marginRight = parseFloat(styles.marginRight) || 0;
if ( includeMargins ) {
var marginLeft = Shuffle._getNumberStyle( element, 'marginLeft');
var marginRight = Shuffle._getNumberStyle( element, 'marginRight');
width += marginLeft + marginRight;
}
@ -243,10 +260,9 @@ Shuffle._getOuterWidth = function( element, includeMargins ) {
Shuffle._getOuterHeight = function( element, includeMargins ) {
var height = element.offsetHeight;
if (includeMargins) {
var styles = $(element).css(['marginTop', 'marginBottom']);
var marginTop = parseFloat(styles.marginTop) || 0;
var marginBottom = parseFloat(styles.marginBottom) || 0;
if ( includeMargins ) {
var marginTop = Shuffle._getNumberStyle( element, 'marginTop');
var marginBottom = Shuffle._getNumberStyle( element, 'marginBottom');
height += marginTop + marginBottom;
}
@ -556,12 +572,12 @@ Shuffle.prototype._getConcealedItems = function() {
/**
* Returns the column size, based on column width and sizer options.
* @param {number} gutterSize Size of the gutters.
* @param {number} containerWidth Size of the parent container.
* @param {number} gutterSize Size of the gutters.
* @return {number}
* @private
*/
Shuffle.prototype._getColumnSize = function( gutterSize, containerWidth ) {
Shuffle.prototype._getColumnSize = function( containerWidth, gutterSize ) {
var size;
// If the columnWidth property is a function, then the grid is fluid
@ -570,7 +586,7 @@ Shuffle.prototype._getColumnSize = function( gutterSize, containerWidth ) {
// columnWidth option isn't a function, are they using a sizing element?
} else if ( this.useSizer ) {
size = Shuffle._getPreciseDimension(this.sizer, 'width');
size = Shuffle._getOuterWidth(this.sizer);
// if not, how about the explicitly set option?
} else if ( this.columnWidth ) {
@ -605,7 +621,7 @@ Shuffle.prototype._getGutterSize = function( containerWidth ) {
if ( $.isFunction( this.gutterWidth ) ) {
size = this.gutterWidth(containerWidth);
} else if ( this.useSizer ) {
size = Shuffle._getPreciseDimension(this.sizer, 'marginLeft');
size = Shuffle._getNumberStyle(this.sizer, 'marginLeft');
} else {
size = this.gutterWidth;
}
@ -621,11 +637,11 @@ Shuffle.prototype._getGutterSize = function( containerWidth ) {
Shuffle.prototype._setColumns = function( theContainerWidth ) {
var containerWidth = theContainerWidth || Shuffle._getOuterWidth( this.element );
var gutter = this._getGutterSize( containerWidth );
var columnWidth = this._getColumnSize( gutter, containerWidth );
var columnWidth = this._getColumnSize( containerWidth, gutter );
var calculatedColumns = (containerWidth + gutter) / columnWidth;
// Widths given from getComputedStyle are not precise enough...
if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < 0.03 ) {
if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < COLUMN_THRESHOLD ) {
// e.g. calculatedColumns = 11.998876
calculatedColumns = Math.round( calculatedColumns );
}
@ -648,7 +664,7 @@ Shuffle.prototype._setContainerSize = function() {
* @private
*/
Shuffle.prototype._getContainerSize = function() {
return Math.max.apply( Math, this.colYs );
return arrayMax( this.colYs );
};
/**
@ -659,6 +675,18 @@ Shuffle.prototype._fire = function( name, args ) {
};
/**
* Zeros out the y columns array, which is used to determine item placement.
* @private
*/
Shuffle.prototype._resetCols = function() {
var i = this.cols;
this.colYs = [];
while (i--) {
this.colYs.push( 0 );
}
};
/**
* Loops through each item that should be shown and calculates the x, y position.
* @param {Array.<Element>} items Array of items that will be shown/layed out in order in their array.
@ -667,42 +695,7 @@ Shuffle.prototype._fire = function( name, args ) {
*/
Shuffle.prototype._layout = function( items, isOnlyPosition ) {
each(items, function( item ) {
var $item = $(item);
var itemData = $item.data();
var currPos = itemData.position;
var currScale = itemData.scale;
var pos = this._getItemPosition( $item );
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
if ( pos.x === currPos.x && pos.y === currPos.y && currScale === DEFAULT_SCALE ) {
return;
}
var transitionObj = {
$item: $item,
x: pos.x,
y: pos.y,
scale: DEFAULT_SCALE,
opacity: isOnlyPosition ? 0 : 1,
skipTransition: !!isOnlyPosition,
callfront: function() {
if ( !isOnlyPosition ) {
$item.css( 'visibility', 'visible' );
}
},
callback: function() {
if ( isOnlyPosition ) {
$item.css( 'visibility', 'hidden' );
}
}
};
this.styleQueue.push( transitionObj );
this._layoutItem( item, isOnlyPosition );
}, this);
// `_layout` always happens after `_shrink`, so it's safe to process the style
@ -713,88 +706,135 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
this._setContainerSize();
};
/**
* Zeros out the y columns array, which is used to determine item placement.
* @private
*/
Shuffle.prototype._resetCols = function() {
var i = this.cols;
this.colYs = [];
while (i--) {
this.colYs.push( 0 );
Shuffle.prototype._layoutItem = function( item, isOnlyPosition ) {
var $item = $(item);
var itemData = $item.data();
var currPos = itemData.position;
var currScale = itemData.scale;
var itemSize = {
width: Shuffle._getOuterWidth( item, true ),
height: Shuffle._getOuterHeight( item, true )
};
var pos = this._getItemPosition( itemSize );
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
if ( pos.x === currPos.x && pos.y === currPos.y && currScale === DEFAULT_SCALE ) {
return;
}
var transitionObj = {
$item: $item,
x: pos.x,
y: pos.y,
scale: DEFAULT_SCALE,
opacity: isOnlyPosition ? 0 : 1,
skipTransition: !!isOnlyPosition,
callfront: function() {
if ( !isOnlyPosition ) {
$item.css( 'visibility', 'visible' );
}
},
callback: function() {
if ( isOnlyPosition ) {
$item.css( 'visibility', 'hidden' );
}
}
};
this.styleQueue.push( transitionObj );
};
Shuffle.prototype._getItemPosition = function( $item ) {
var itemWidth = Shuffle._getOuterWidth( $item[0], true );
var columnSpan = itemWidth / this.colWidth;
Shuffle.prototype._getItemPosition = function( itemSize ) {
var columnSpan = itemSize.width / this.colWidth;
// If the difference between the rounded column span number and the
// calculated column span number is really small, round the number to
// make it fit.
if ( Math.abs(Math.round(columnSpan) - columnSpan) < 0.03 ) {
if ( Math.abs(Math.round(columnSpan) - columnSpan) < COLUMN_THRESHOLD ) {
// e.g. columnSpan = 4.0089945390298745
columnSpan = Math.round( columnSpan );
}
// How many columns does this item span. Ensure it's not more than the
// amount of columns in the whole layout.
var colSpan = Math.min( Math.ceil(columnSpan), this.cols );
// Ensure the column span is not more than the amount of columns in the whole layout.
columnSpan = Math.min( Math.ceil(columnSpan), this.cols );
var setY = this._getColumnSet( columnSpan, this.cols );
// Finds the index of the smallest number in the set.
var shortColumnIndex = this._getShortColumn( setY, this.buffer );
// Position the item
var position = {
x: Math.round( (this.colWidth * shortColumnIndex) + this.offset.left ),
y: Math.round( setY[shortColumnIndex] + this.offset.top )
};
// Update the columns array with the new values for each column.
// e.g. before the update the columns could be [250, 0, 0, 0] for an item
// which spans 2 columns. After it would be [250, itemHeight, itemHeight, 0].
var setHeight = setY[shortColumnIndex] + itemSize.height;
var setSpan = this.cols + 1 - setY.length;
for ( var i = 0; i < setSpan; i++ ) {
this.colYs[ shortColumnIndex + i ] = setHeight;
}
return position;
};
/**
* Retrieves the column set to use for placement.
* @param {number} columnSpan The number of columns this current item spans.
* @param {number} columns The total columns in the grid.
* @return {Array.<number>} An array of numbers represeting the column set.
* @private
*/
Shuffle.prototype._getColumnSet = function( columnSpan, columns ) {
// The item spans only one column.
if ( colSpan === 1 ) {
return this._placeItem( $item, this.colYs );
if ( columnSpan === 1 ) {
return this.colYs;
// The item spans more than one column, figure out how many different
// places it could fit horizontally
} else {
var groupCount = this.cols + 1 - colSpan;
var groupCount = columns + 1 - columnSpan;
var groupY = [];
var groupColY;
var i;
// for each group potential horizontal position
for ( i = 0; i < groupCount; i++ ) {
for ( var i = 0; i < groupCount; i++ ) {
// make an array of colY values for that one group
groupColY = this.colYs.slice( i, i + colSpan );
groupColY = this.colYs.slice( i, i + columnSpan );
// and get the max value of the array
groupY[i] = Math.max.apply( Math, groupColY );
groupY[i] = arrayMax( groupColY );
}
return this._placeItem( $item, groupY );
return groupY;
}
};
// TODO: Cleanup and combine with _getItemPosition.
Shuffle.prototype._placeItem = function( $item, setY ) {
// get the minimum Y value from the columns
var minimumY = Math.min.apply( Math, setY );
var shortCol = 0;
// Find index of short column, the first from the left where this item will go
// if ( setY[i] === minimumY ) requires items' height to be exact every time.
// The buffer value is very useful when the height is a percentage of the width
for (var i = 0, len = setY.length; i < len; i++) {
if ( setY[i] >= minimumY - this.buffer && setY[i] <= minimumY + this.buffer ) {
shortCol = i;
break;
/**
* Find index of short column, the first from the left where this item will go.
*
* @param {Array.<number>} positions The array to search for the smallest number.
* @param {number} buffer Optional buffer which is very useful when the height
* is a percentage of the width.
* @return {number} Index of the short column.
* @private
*/
Shuffle.prototype._getShortColumn = function( positions, buffer ) {
var minPosition = arrayMin( positions );
for (var i = 0, len = positions.length; i < len; i++) {
if ( positions[i] >= minPosition - buffer && positions[i] <= minPosition + buffer ) {
return i;
}
}
// Position the item
var position = {
x: Math.round( (this.colWidth * shortCol) + this.offset.left ),
y: Math.round( minimumY + this.offset.top )
};
// Apply setHeight to necessary columns
var setHeight = minimumY + Shuffle._getOuterHeight( $item[0], true ),
setSpan = this.cols + 1 - len;
for ( i = 0; i < setSpan; i++ ) {
this.colYs[ shortCol + i ] = setHeight;
}
return position;
return 0;
};
/**

File diff suppressed because one or more lines are too long

@ -30,10 +30,15 @@ function dashify( prop ) {
var TRANSITION = Modernizr.prefixed('transition');
var TRANSITION_DELAY = Modernizr.prefixed('transitionDelay');
var TRANSITION_DURATION = Modernizr.prefixed('transitionDuration');
// Note(glen): Stock Android 4.1.x browser will fail here because it wrongly
// says it supports non-prefixed transitions.
// https://github.com/Modernizr/Modernizr/issues/897
var TRANSITIONEND = {
'WebkitTransition' : 'webkitTransitionEnd',
'transition' : 'transitionend'
}[ TRANSITION ];
var TRANSFORM = Modernizr.prefixed('transform');
var CSS_TRANSFORM = dashify(TRANSFORM);
@ -41,6 +46,7 @@ var CSS_TRANSFORM = dashify(TRANSFORM);
var CAN_TRANSITION_TRANSFORMS = Modernizr.csstransforms && Modernizr.csstransitions;
var HAS_TRANSFORMS_3D = Modernizr.csstransforms3d;
var SHUFFLE = 'shuffle';
var COLUMN_THRESHOLD = 0.3;
// Configurable. You can change these constants to fit your application.
// The default scale and concealed scale, however, have to be different values.
@ -95,6 +101,14 @@ function defer(fn, context, wait) {
return setTimeout( $.proxy( fn, context ), wait || 0 );
}
function arrayMax( array ) {
return Math.max.apply( Math, array );
}
function arrayMin( array ) {
return Math.min.apply( Math, array );
}
// Used for unique instance variables
var id = 0;
@ -173,37 +187,40 @@ Shuffle._getItemTransformString = function(x, y, scale) {
};
Shuffle._getPreciseDimension = function( element, style ) {
var dimension;
if ( window.getComputedStyle ) {
dimension = window.getComputedStyle( element, null )[ style ];
} else {
dimension = $( element ).css( style );
}
return parseFloat( dimension );
/**
* Retrieve the computed style for an element, parsed as a float. This should
* not be used for width or height values because jQuery mangles them and they
* are not precise enough.
* @param {Element} element Element to get style for.
* @param {string} style Style property.
* @return {number} The parsed computed value or zero if that fails because IE
* will return 'auto' when the element doesn't have margins instead of
* the computed style.
* @private
*/
Shuffle._getNumberStyle = function( element, style ) {
return parseFloat( $( element ).css( style ) ) || 0;
};
/**
* Returns the outer width of an element, optionally including its margins.
* the client rect is used instead of `offsetWidth` because Firefox returns an
* integer value instead of a double.
* @param {Element} element The element.
* @param {boolean} [includeMargins] Whether to include margins. Default is false.
* @return {number} The width.
*/
Shuffle._getOuterWidth = function( element, includeMargins ) {
var width = element.offsetWidth;
var rect = element.getBoundingClientRect();
var width = rect.right - rect.left;
// Use jQuery here because it uses getComputedStyle internally and is
// cross-browser. Using the style property of the element will only work
// if there are inline styles.
if (includeMargins) {
var styles = $(element).css(['marginLeft', 'marginRight']);
// Defaults to zero if parsing fails because IE will return 'auto' when
// the element doesn't have margins instead of the computed style.
var marginLeft = parseFloat(styles.marginLeft) || 0;
var marginRight = parseFloat(styles.marginRight) || 0;
if ( includeMargins ) {
var marginLeft = Shuffle._getNumberStyle( element, 'marginLeft');
var marginRight = Shuffle._getNumberStyle( element, 'marginRight');
width += marginLeft + marginRight;
}
@ -220,10 +237,9 @@ Shuffle._getOuterWidth = function( element, includeMargins ) {
Shuffle._getOuterHeight = function( element, includeMargins ) {
var height = element.offsetHeight;
if (includeMargins) {
var styles = $(element).css(['marginTop', 'marginBottom']);
var marginTop = parseFloat(styles.marginTop) || 0;
var marginBottom = parseFloat(styles.marginBottom) || 0;
if ( includeMargins ) {
var marginTop = Shuffle._getNumberStyle( element, 'marginTop');
var marginBottom = Shuffle._getNumberStyle( element, 'marginBottom');
height += marginTop + marginBottom;
}
@ -533,12 +549,12 @@ Shuffle.prototype._getConcealedItems = function() {
/**
* Returns the column size, based on column width and sizer options.
* @param {number} gutterSize Size of the gutters.
* @param {number} containerWidth Size of the parent container.
* @param {number} gutterSize Size of the gutters.
* @return {number}
* @private
*/
Shuffle.prototype._getColumnSize = function( gutterSize, containerWidth ) {
Shuffle.prototype._getColumnSize = function( containerWidth, gutterSize ) {
var size;
// If the columnWidth property is a function, then the grid is fluid
@ -547,7 +563,7 @@ Shuffle.prototype._getColumnSize = function( gutterSize, containerWidth ) {
// columnWidth option isn't a function, are they using a sizing element?
} else if ( this.useSizer ) {
size = Shuffle._getPreciseDimension(this.sizer, 'width');
size = Shuffle._getOuterWidth(this.sizer);
// if not, how about the explicitly set option?
} else if ( this.columnWidth ) {
@ -582,7 +598,7 @@ Shuffle.prototype._getGutterSize = function( containerWidth ) {
if ( $.isFunction( this.gutterWidth ) ) {
size = this.gutterWidth(containerWidth);
} else if ( this.useSizer ) {
size = Shuffle._getPreciseDimension(this.sizer, 'marginLeft');
size = Shuffle._getNumberStyle(this.sizer, 'marginLeft');
} else {
size = this.gutterWidth;
}
@ -598,11 +614,11 @@ Shuffle.prototype._getGutterSize = function( containerWidth ) {
Shuffle.prototype._setColumns = function( theContainerWidth ) {
var containerWidth = theContainerWidth || Shuffle._getOuterWidth( this.element );
var gutter = this._getGutterSize( containerWidth );
var columnWidth = this._getColumnSize( gutter, containerWidth );
var columnWidth = this._getColumnSize( containerWidth, gutter );
var calculatedColumns = (containerWidth + gutter) / columnWidth;
// Widths given from getComputedStyle are not precise enough...
if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < 0.03 ) {
if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < COLUMN_THRESHOLD ) {
// e.g. calculatedColumns = 11.998876
calculatedColumns = Math.round( calculatedColumns );
}
@ -625,7 +641,7 @@ Shuffle.prototype._setContainerSize = function() {
* @private
*/
Shuffle.prototype._getContainerSize = function() {
return Math.max.apply( Math, this.colYs );
return arrayMax( this.colYs );
};
/**
@ -636,6 +652,18 @@ Shuffle.prototype._fire = function( name, args ) {
};
/**
* Zeros out the y columns array, which is used to determine item placement.
* @private
*/
Shuffle.prototype._resetCols = function() {
var i = this.cols;
this.colYs = [];
while (i--) {
this.colYs.push( 0 );
}
};
/**
* Loops through each item that should be shown and calculates the x, y position.
* @param {Array.<Element>} items Array of items that will be shown/layed out in order in their array.
@ -644,42 +672,7 @@ Shuffle.prototype._fire = function( name, args ) {
*/
Shuffle.prototype._layout = function( items, isOnlyPosition ) {
each(items, function( item ) {
var $item = $(item);
var itemData = $item.data();
var currPos = itemData.position;
var currScale = itemData.scale;
var pos = this._getItemPosition( $item );
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
if ( pos.x === currPos.x && pos.y === currPos.y && currScale === DEFAULT_SCALE ) {
return;
}
var transitionObj = {
$item: $item,
x: pos.x,
y: pos.y,
scale: DEFAULT_SCALE,
opacity: isOnlyPosition ? 0 : 1,
skipTransition: !!isOnlyPosition,
callfront: function() {
if ( !isOnlyPosition ) {
$item.css( 'visibility', 'visible' );
}
},
callback: function() {
if ( isOnlyPosition ) {
$item.css( 'visibility', 'hidden' );
}
}
};
this.styleQueue.push( transitionObj );
this._layoutItem( item, isOnlyPosition );
}, this);
// `_layout` always happens after `_shrink`, so it's safe to process the style
@ -690,88 +683,135 @@ Shuffle.prototype._layout = function( items, isOnlyPosition ) {
this._setContainerSize();
};
/**
* Zeros out the y columns array, which is used to determine item placement.
* @private
*/
Shuffle.prototype._resetCols = function() {
var i = this.cols;
this.colYs = [];
while (i--) {
this.colYs.push( 0 );
Shuffle.prototype._layoutItem = function( item, isOnlyPosition ) {
var $item = $(item);
var itemData = $item.data();
var currPos = itemData.position;
var currScale = itemData.scale;
var itemSize = {
width: Shuffle._getOuterWidth( item, true ),
height: Shuffle._getOuterHeight( item, true )
};
var pos = this._getItemPosition( itemSize );
// Save data for shrink
$item.data( 'position', pos );
$item.data( 'scale', DEFAULT_SCALE );
// If the item will not change its position, do not add it to the render
// queue. Transitions don't fire when setting a property to the same value.
if ( pos.x === currPos.x && pos.y === currPos.y && currScale === DEFAULT_SCALE ) {
return;
}
var transitionObj = {
$item: $item,
x: pos.x,
y: pos.y,
scale: DEFAULT_SCALE,
opacity: isOnlyPosition ? 0 : 1,
skipTransition: !!isOnlyPosition,
callfront: function() {
if ( !isOnlyPosition ) {
$item.css( 'visibility', 'visible' );
}
},
callback: function() {
if ( isOnlyPosition ) {
$item.css( 'visibility', 'hidden' );
}
}
};
this.styleQueue.push( transitionObj );
};
Shuffle.prototype._getItemPosition = function( $item ) {
var itemWidth = Shuffle._getOuterWidth( $item[0], true );
var columnSpan = itemWidth / this.colWidth;
Shuffle.prototype._getItemPosition = function( itemSize ) {
var columnSpan = itemSize.width / this.colWidth;
// If the difference between the rounded column span number and the
// calculated column span number is really small, round the number to
// make it fit.
if ( Math.abs(Math.round(columnSpan) - columnSpan) < 0.03 ) {
if ( Math.abs(Math.round(columnSpan) - columnSpan) < COLUMN_THRESHOLD ) {
// e.g. columnSpan = 4.0089945390298745
columnSpan = Math.round( columnSpan );
}
// How many columns does this item span. Ensure it's not more than the
// amount of columns in the whole layout.
var colSpan = Math.min( Math.ceil(columnSpan), this.cols );
// Ensure the column span is not more than the amount of columns in the whole layout.
columnSpan = Math.min( Math.ceil(columnSpan), this.cols );
var setY = this._getColumnSet( columnSpan, this.cols );
// Finds the index of the smallest number in the set.
var shortColumnIndex = this._getShortColumn( setY, this.buffer );
// Position the item
var position = {
x: Math.round( (this.colWidth * shortColumnIndex) + this.offset.left ),
y: Math.round( setY[shortColumnIndex] + this.offset.top )
};
// Update the columns array with the new values for each column.
// e.g. before the update the columns could be [250, 0, 0, 0] for an item
// which spans 2 columns. After it would be [250, itemHeight, itemHeight, 0].
var setHeight = setY[shortColumnIndex] + itemSize.height;
var setSpan = this.cols + 1 - setY.length;
for ( var i = 0; i < setSpan; i++ ) {
this.colYs[ shortColumnIndex + i ] = setHeight;
}
return position;
};
/**
* Retrieves the column set to use for placement.
* @param {number} columnSpan The number of columns this current item spans.
* @param {number} columns The total columns in the grid.
* @return {Array.<number>} An array of numbers represeting the column set.
* @private
*/
Shuffle.prototype._getColumnSet = function( columnSpan, columns ) {
// The item spans only one column.
if ( colSpan === 1 ) {
return this._placeItem( $item, this.colYs );
if ( columnSpan === 1 ) {
return this.colYs;
// The item spans more than one column, figure out how many different
// places it could fit horizontally
} else {
var groupCount = this.cols + 1 - colSpan;
var groupCount = columns + 1 - columnSpan;
var groupY = [];
var groupColY;
var i;
// for each group potential horizontal position
for ( i = 0; i < groupCount; i++ ) {
for ( var i = 0; i < groupCount; i++ ) {
// make an array of colY values for that one group
groupColY = this.colYs.slice( i, i + colSpan );
groupColY = this.colYs.slice( i, i + columnSpan );
// and get the max value of the array
groupY[i] = Math.max.apply( Math, groupColY );
groupY[i] = arrayMax( groupColY );
}
return this._placeItem( $item, groupY );
return groupY;
}
};
// TODO: Cleanup and combine with _getItemPosition.
Shuffle.prototype._placeItem = function( $item, setY ) {
// get the minimum Y value from the columns
var minimumY = Math.min.apply( Math, setY );
var shortCol = 0;
// Find index of short column, the first from the left where this item will go
// if ( setY[i] === minimumY ) requires items' height to be exact every time.
// The buffer value is very useful when the height is a percentage of the width
for (var i = 0, len = setY.length; i < len; i++) {
if ( setY[i] >= minimumY - this.buffer && setY[i] <= minimumY + this.buffer ) {
shortCol = i;
break;
/**
* Find index of short column, the first from the left where this item will go.
*
* @param {Array.<number>} positions The array to search for the smallest number.
* @param {number} buffer Optional buffer which is very useful when the height
* is a percentage of the width.
* @return {number} Index of the short column.
* @private
*/
Shuffle.prototype._getShortColumn = function( positions, buffer ) {
var minPosition = arrayMin( positions );
for (var i = 0, len = positions.length; i < len; i++) {
if ( positions[i] >= minPosition - buffer && positions[i] <= minPosition + buffer ) {
return i;
}
}
// Position the item
var position = {
x: Math.round( (this.colWidth * shortCol) + this.offset.left ),
y: Math.round( minimumY + this.offset.top )
};
// Apply setHeight to necessary columns
var setHeight = minimumY + Shuffle._getOuterHeight( $item[0], true ),
setSpan = this.cols + 1 - len;
for ( i = 0; i < setSpan; i++ ) {
this.colYs[ shortCol + i ] = setHeight;
}
return position;
return 0;
};
/**

@ -112,7 +112,7 @@ describe('Shuffle.js', function() {
var shuffle = $shuffle.data('shuffle');
expect(shuffle._getGutterSize(1000)).toBe(50);
expect(shuffle._getColumnSize(50, 1000)).toBe(350);
expect(shuffle._getColumnSize(1000, 50)).toBe(350);
expect(shuffle.colWidth).toBe(350);
expect(shuffle.cols).toBe(3);
expect(shuffle.colYs).toEqual([600, 450, 450]);

@ -3,11 +3,3 @@
## Improvements
* More JSDoc
* Create an `Item` class for shuffle items so they can handle storing values and more.
* Horizontal layout
## Things I don't like and would like to fix
* Less jQuery dependency
## Reasons Zepto doesn't work
* `.data()` - although I think it can be included?
* `.css(['style1', 'style2', 'style3'])` isn't supported.

Loading…
Cancel
Save