packery
Version:
bin-packing layout library
483 lines (413 loc) • 12 kB
JavaScript
/*!
* Packery v1.3.0
* bin-packing layout library
* http://packery.metafizzy.co
*
* Commercial use requires one-time purchase of a commercial license
* http://packery.metafizzy.co/license.html
*
* Non-commercial use is licensed under the GPL v3 License
*
* Copyright 2014 Metafizzy
*/
( function( window ) {
'use strict';
// -------------------------- Packery -------------------------- //
// used for AMD definition and requires
function packeryDefinition( classie, getSize, Outlayer, Rect, Packer, Item ) {
// create an Outlayer layout class
var Packery = Outlayer.create('packery');
Packery.Item = Item;
Packery.prototype._create = function() {
// call super
Outlayer.prototype._create.call( this );
// initial properties
this.packer = new Packer();
// Left over from v1.0
this.stamp( this.options.stamped );
// create drag handlers
var _this = this;
this.handleDraggabilly = {
dragStart: function( draggie ) {
_this.itemDragStart( draggie.element );
},
dragMove: function( draggie ) {
_this.itemDragMove( draggie.element, draggie.position.x, draggie.position.y );
},
dragEnd: function( draggie ) {
_this.itemDragEnd( draggie.element );
}
};
this.handleUIDraggable = {
start: function handleUIDraggableStart( event ) {
_this.itemDragStart( event.currentTarget );
},
drag: function handleUIDraggableDrag( event, ui ) {
_this.itemDragMove( event.currentTarget, ui.position.left, ui.position.top );
},
stop: function handleUIDraggableStop( event ) {
_this.itemDragEnd( event.currentTarget );
}
};
};
// ----- init & layout ----- //
/**
* logic before any new layout
*/
Packery.prototype._resetLayout = function() {
this.getSize();
this._getMeasurements();
// reset packer
var packer = this.packer;
// packer settings, if horizontal or vertical
if ( this.options.isHorizontal ) {
packer.width = Number.POSITIVE_INFINITY;
packer.height = this.size.innerHeight + this.gutter;
packer.sortDirection = 'rightwardTopToBottom';
} else {
packer.width = this.size.innerWidth + this.gutter;
packer.height = Number.POSITIVE_INFINITY;
packer.sortDirection = 'downwardLeftToRight';
}
packer.reset();
// layout
this.maxY = 0;
this.maxX = 0;
};
/**
* update columnWidth, rowHeight, & gutter
* @private
*/
Packery.prototype._getMeasurements = function() {
this._getMeasurement( 'columnWidth', 'width' );
this._getMeasurement( 'rowHeight', 'height' );
this._getMeasurement( 'gutter', 'width' );
};
Packery.prototype._getItemLayoutPosition = function( item ) {
this._packItem( item );
return item.rect;
};
/**
* layout item in packer
* @param {Packery.Item} item
*/
Packery.prototype._packItem = function( item ) {
this._setRectSize( item.element, item.rect );
// pack the rect in the packer
this.packer.pack( item.rect );
this._setMaxXY( item.rect );
};
/**
* set max X and Y value, for size of container
* @param {Packery.Rect} rect
* @private
*/
Packery.prototype._setMaxXY = function( rect ) {
this.maxX = Math.max( rect.x + rect.width, this.maxX );
this.maxY = Math.max( rect.y + rect.height, this.maxY );
};
/**
* set the width and height of a rect, applying columnWidth and rowHeight
* @param {Element} elem
* @param {Packery.Rect} rect
*/
Packery.prototype._setRectSize = function( elem, rect ) {
var size = getSize( elem );
var w = size.outerWidth;
var h = size.outerHeight;
// size for columnWidth and rowHeight, if available
// only check if size is non-zero, #177
if ( w || h ) {
var colW = this.columnWidth + this.gutter;
var rowH = this.rowHeight + this.gutter;
w = this.columnWidth ? Math.ceil( w / colW ) * colW : w + this.gutter;
h = this.rowHeight ? Math.ceil( h / rowH ) * rowH : h + this.gutter;
}
// rect must fit in packer
rect.width = Math.min( w, this.packer.width );
rect.height = Math.min( h, this.packer.height );
};
Packery.prototype._getContainerSize = function() {
if ( this.options.isHorizontal ) {
return {
width: this.maxX - this.gutter
};
} else {
return {
height: this.maxY - this.gutter
};
}
};
// -------------------------- stamp -------------------------- //
/**
* makes space for element
* @param {Element} elem
*/
Packery.prototype._manageStamp = function( elem ) {
var item = this.getItem( elem );
var rect;
if ( item && item.isPlacing ) {
rect = item.placeRect;
} else {
var offset = this._getElementOffset( elem );
rect = new Rect({
x: this.options.isOriginLeft ? offset.left : offset.right,
y: this.options.isOriginTop ? offset.top : offset.bottom
});
}
this._setRectSize( elem, rect );
// save its space in the packer
this.packer.placed( rect );
this._setMaxXY( rect );
};
// -------------------------- methods -------------------------- //
function verticalSorter( a, b ) {
return a.position.y - b.position.y || a.position.x - b.position.x;
}
function horizontalSorter( a, b ) {
return a.position.x - b.position.x || a.position.y - b.position.y;
}
Packery.prototype.sortItemsByPosition = function() {
var sorter = this.options.isHorizontal ? horizontalSorter : verticalSorter;
this.items.sort( sorter );
};
/**
* Fit item element in its current position
* Packery will position elements around it
* useful for expanding elements
*
* @param {Element} elem
* @param {Number} x - horizontal destination position, optional
* @param {Number} y - vertical destination position, optional
*/
Packery.prototype.fit = function( elem, x, y ) {
var item = this.getItem( elem );
if ( !item ) {
return;
}
// prepare internal properties
this._getMeasurements();
// stamp item to get it out of layout
this.stamp( item.element );
// required for positionPlaceRect
item.getSize();
// set placing flag
item.isPlacing = true;
// fall back to current position for fitting
x = x === undefined ? item.rect.x: x;
y = y === undefined ? item.rect.y: y;
// position it best at its destination
item.positionPlaceRect( x, y, true );
this._bindFitEvents( item );
item.moveTo( item.placeRect.x, item.placeRect.y );
// layout everything else
this.layout();
// return back to regularly scheduled programming
this.unstamp( item.element );
this.sortItemsByPosition();
// un set placing flag, back to normal
item.isPlacing = false;
// copy place rect position
item.copyPlaceRectPosition();
};
/**
* emit event when item is fit and other items are laid out
* @param {Packery.Item} item
* @private
*/
Packery.prototype._bindFitEvents = function( item ) {
var _this = this;
var ticks = 0;
function tick() {
ticks++;
if ( ticks !== 2 ) {
return;
}
_this.emitEvent( 'fitComplete', [ _this, item ] );
}
// when item is laid out
item.on( 'layout', function() {
tick();
return true;
});
// when all items are laid out
this.on( 'layoutComplete', function() {
tick();
return true;
});
};
// -------------------------- resize -------------------------- //
// debounced, layout on resize
Packery.prototype.resize = function() {
// don't trigger if size did not change
var size = getSize( this.element );
// check that this.size and size are there
// IE8 triggers resize on body size change, so they might not be
var hasSizes = this.size && size;
var innerSize = this.options.isHorizontal ? 'innerHeight' : 'innerWidth';
if ( hasSizes && size[ innerSize ] === this.size[ innerSize ] ) {
return;
}
this.layout();
};
// -------------------------- drag -------------------------- //
/**
* handle an item drag start event
* @param {Element} elem
*/
Packery.prototype.itemDragStart = function( elem ) {
this.stamp( elem );
var item = this.getItem( elem );
if ( item ) {
item.dragStart();
}
};
/**
* handle an item drag move event
* @param {Element} elem
* @param {Number} x - horizontal change in position
* @param {Number} y - vertical change in position
*/
Packery.prototype.itemDragMove = function( elem, x, y ) {
var item = this.getItem( elem );
if ( item ) {
item.dragMove( x, y );
}
// debounce
var _this = this;
// debounce triggering layout
function delayed() {
_this.layout();
delete _this.dragTimeout;
}
this.clearDragTimeout();
this.dragTimeout = setTimeout( delayed, 40 );
};
Packery.prototype.clearDragTimeout = function() {
if ( this.dragTimeout ) {
clearTimeout( this.dragTimeout );
}
};
/**
* handle an item drag end event
* @param {Element} elem
*/
Packery.prototype.itemDragEnd = function( elem ) {
var item = this.getItem( elem );
var itemDidDrag;
if ( item ) {
itemDidDrag = item.didDrag;
item.dragStop();
}
// if elem didn't move, or if it doesn't need positioning
// unignore and unstamp and call it a day
if ( !item || ( !itemDidDrag && !item.needsPositioning ) ) {
this.unstamp( elem );
return;
}
// procced with dragged item
classie.add( item.element, 'is-positioning-post-drag' );
// save this var, as it could get reset in dragStart
var onLayoutComplete = this._getDragEndLayoutComplete( elem, item );
if ( item.needsPositioning ) {
item.on( 'layout', onLayoutComplete );
item.moveTo( item.placeRect.x, item.placeRect.y );
} else if ( item ) {
// item didn't need placement
item.copyPlaceRectPosition();
}
this.clearDragTimeout();
this.on( 'layoutComplete', onLayoutComplete );
this.layout();
};
/**
* get drag end callback
* @param {Element} elem
* @param {Packery.Item} item
* @returns {Function} onLayoutComplete
*/
Packery.prototype._getDragEndLayoutComplete = function( elem, item ) {
var itemNeedsPositioning = item && item.needsPositioning;
var completeCount = 0;
var asyncCount = itemNeedsPositioning ? 2 : 1;
var _this = this;
return function onLayoutComplete() {
completeCount++;
// don't proceed if not complete
if ( completeCount !== asyncCount ) {
return true;
}
// reset item
if ( item ) {
classie.remove( item.element, 'is-positioning-post-drag' );
item.isPlacing = false;
item.copyPlaceRectPosition();
}
_this.unstamp( elem );
// only sort when item moved
_this.sortItemsByPosition();
// emit item drag event now that everything is done
if ( itemNeedsPositioning ) {
_this.emitEvent( 'dragItemPositioned', [ _this, item ] );
}
// listen once
return true;
};
};
/**
* binds Draggabilly events
* @param {Draggabilly} draggie
*/
Packery.prototype.bindDraggabillyEvents = function( draggie ) {
draggie.on( 'dragStart', this.handleDraggabilly.dragStart );
draggie.on( 'dragMove', this.handleDraggabilly.dragMove );
draggie.on( 'dragEnd', this.handleDraggabilly.dragEnd );
};
/**
* binds jQuery UI Draggable events
* @param {jQuery} $elems
*/
Packery.prototype.bindUIDraggableEvents = function( $elems ) {
$elems
.on( 'dragstart', this.handleUIDraggable.start )
.on( 'drag', this.handleUIDraggable.drag )
.on( 'dragstop', this.handleUIDraggable.stop );
};
Packery.Rect = Rect;
Packery.Packer = Packer;
return Packery;
}
// -------------------------- transport -------------------------- //
if ( typeof define === 'function' && define.amd ) {
// AMD
define( [
'classie/classie',
'get-size/get-size',
'outlayer/outlayer',
'./rect',
'./packer',
'./item'
],
packeryDefinition );
} else if ( typeof exports === 'object' ) {
// CommonJS
module.exports = packeryDefinition(
require('desandro-classie'),
require('get-size'),
require('outlayer'),
require('./rect'),
require('./packer'),
require('./item')
);
} else {
// browser global
window.Packery = packeryDefinition(
window.classie,
window.getSize,
window.Outlayer,
window.Packery.Rect,
window.Packery.Packer,
window.Packery.Item
);
}
})( window );