blowfish/node_modules/packery/js/packery.js
2023-01-29 22:30:24 +00:00

668 lines
18 KiB
JavaScript

/*!
* Packery v2.1.2
* Gapless, draggable grid layouts
*
* Licensed GPLv3 for open source use
* or Packery Commercial License for commercial use
*
* http://packery.metafizzy.co
* Copyright 2013-2018 Metafizzy
*/
( function( window, factory ) {
// universal module definition
/* jshint strict: false */ /* globals define, module, require */
if ( typeof define == 'function' && define.amd ) {
// AMD
define( [
'get-size/get-size',
'outlayer/outlayer',
'./rect',
'./packer',
'./item'
],
factory );
} else if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory(
require('get-size'),
require('outlayer'),
require('./rect'),
require('./packer'),
require('./item')
);
} else {
// browser global
window.Packery = factory(
window.getSize,
window.Outlayer,
window.Packery.Rect,
window.Packery.Packer,
window.Packery.Item
);
}
}( window, function factory( getSize, Outlayer, Rect, Packer, Item ) {
'use strict';
// ----- Rect ----- //
// allow for pixel rounding errors IE8-IE11 & Firefox; #227
Rect.prototype.canFit = function( rect ) {
return this.width >= rect.width - 1 && this.height >= rect.height - 1;
};
// -------------------------- Packery -------------------------- //
// create an Outlayer layout class
var Packery = Outlayer.create('packery');
Packery.Item = Item;
var proto = Packery.prototype;
proto._create = function() {
// call super
Outlayer.prototype._create.call( this );
// initial properties
this.packer = new Packer();
// packer for drop targets
this.shiftPacker = new Packer();
this.isEnabled = true;
this.dragItemCount = 0;
// create drag handlers
var _this = this;
this.handleDraggabilly = {
dragStart: function() {
_this.itemDragStart( this.element );
},
dragMove: function() {
_this.itemDragMove( this.element, this.position.x, this.position.y );
},
dragEnd: function() {
_this.itemDragEnd( this.element );
}
};
this.handleUIDraggable = {
start: function handleUIDraggableStart( event, ui ) {
// HTML5 may trigger dragstart, dismiss HTML5 dragging
if ( !ui ) {
return;
}
_this.itemDragStart( event.currentTarget );
},
drag: function handleUIDraggableDrag( event, ui ) {
if ( !ui ) {
return;
}
_this.itemDragMove( event.currentTarget, ui.position.left, ui.position.top );
},
stop: function handleUIDraggableStop( event, ui ) {
if ( !ui ) {
return;
}
_this.itemDragEnd( event.currentTarget );
}
};
};
// ----- init & layout ----- //
/**
* logic before any new layout
*/
proto._resetLayout = function() {
this.getSize();
this._getMeasurements();
// reset packer
var width, height, sortDirection;
// packer settings, if horizontal or vertical
if ( this._getOption('horizontal') ) {
width = Infinity;
height = this.size.innerHeight + this.gutter;
sortDirection = 'rightwardTopToBottom';
} else {
width = this.size.innerWidth + this.gutter;
height = Infinity;
sortDirection = 'downwardLeftToRight';
}
this.packer.width = this.shiftPacker.width = width;
this.packer.height = this.shiftPacker.height = height;
this.packer.sortDirection = this.shiftPacker.sortDirection = sortDirection;
this.packer.reset();
// layout
this.maxY = 0;
this.maxX = 0;
};
/**
* update columnWidth, rowHeight, & gutter
* @private
*/
proto._getMeasurements = function() {
this._getMeasurement( 'columnWidth', 'width' );
this._getMeasurement( 'rowHeight', 'height' );
this._getMeasurement( 'gutter', 'width' );
};
proto._getItemLayoutPosition = function( item ) {
this._setRectSize( item.element, item.rect );
if ( this.isShifting || this.dragItemCount > 0 ) {
var packMethod = this._getPackMethod();
this.packer[ packMethod ]( item.rect );
} else {
this.packer.pack( item.rect );
}
this._setMaxXY( item.rect );
return item.rect;
};
proto.shiftLayout = function() {
this.isShifting = true;
this.layout();
delete this.isShifting;
};
proto._getPackMethod = function() {
return this._getOption('horizontal') ? 'rowPack' : 'columnPack';
};
/**
* set max X and Y value, for size of container
* @param {Packery.Rect} rect
* @private
*/
proto._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
*/
proto._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 ) {
w = this._applyGridGutter( w, this.columnWidth );
h = this._applyGridGutter( h, this.rowHeight );
}
// rect must fit in packer
rect.width = Math.min( w, this.packer.width );
rect.height = Math.min( h, this.packer.height );
};
/**
* fits item to columnWidth/rowHeight and adds gutter
* @param {Number} measurement - item width or height
* @param {Number} gridSize - columnWidth or rowHeight
* @returns measurement
*/
proto._applyGridGutter = function( measurement, gridSize ) {
// just add gutter if no gridSize
if ( !gridSize ) {
return measurement + this.gutter;
}
gridSize += this.gutter;
// fit item to columnWidth/rowHeight
var remainder = measurement % gridSize;
var mathMethod = remainder && remainder < 1 ? 'round' : 'ceil';
measurement = Math[ mathMethod ]( measurement / gridSize ) * gridSize;
return measurement;
};
proto._getContainerSize = function() {
if ( this._getOption('horizontal') ) {
return {
width: this.maxX - this.gutter
};
} else {
return {
height: this.maxY - this.gutter
};
}
};
// -------------------------- stamp -------------------------- //
/**
* makes space for element
* @param {Element} elem
*/
proto._manageStamp = function( elem ) {
var item = this.getItem( elem );
var rect;
if ( item && item.isPlacing ) {
rect = item.rect;
} else {
var offset = this._getElementOffset( elem );
rect = new Rect({
x: this._getOption('originLeft') ? offset.left : offset.right,
y: this._getOption('originTop') ? 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;
}
proto.sortItemsByPosition = function() {
var sorter = this._getOption('horizontal') ? 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
*/
proto.fit = function( elem, x, y ) {
var item = this.getItem( elem );
if ( !item ) {
return;
}
// stamp item to get it out of layout
this.stamp( item.element );
// set placing flag
item.enablePlacing();
this.updateShiftTargets( item );
// 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
this.shift( item, x, y );
this._bindFitEvents( item );
item.moveTo( item.rect.x, item.rect.y );
// layout everything else
this.shiftLayout();
// return back to regularly scheduled programming
this.unstamp( item.element );
this.sortItemsByPosition();
item.disablePlacing();
};
/**
* emit event when item is fit and other items are laid out
* @param {Packery.Item} item
* @private
*/
proto._bindFitEvents = function( item ) {
var _this = this;
var ticks = 0;
function onLayout() {
ticks++;
if ( ticks != 2 ) {
return;
}
_this.dispatchEvent( 'fitComplete', null, [ item ] );
}
// when item is laid out
item.once( 'layout', onLayout );
// when all items are laid out
this.once( 'layoutComplete', onLayout );
};
// -------------------------- resize -------------------------- //
// debounced, layout on resize
proto.resize = function() {
// don't trigger if size did not change
// or if resize was unbound. See #285, outlayer#9
if ( !this.isResizeBound || !this.needsResizeLayout() ) {
return;
}
if ( this.options.shiftPercentResize ) {
this.resizeShiftPercentLayout();
} else {
this.layout();
}
};
/**
* check if layout is needed post layout
* @returns Boolean
*/
proto.needsResizeLayout = function() {
var size = getSize( this.element );
var innerSize = this._getOption('horizontal') ? 'innerHeight' : 'innerWidth';
return size[ innerSize ] != this.size[ innerSize ];
};
proto.resizeShiftPercentLayout = function() {
var items = this._getItemsForLayout( this.items );
var isHorizontal = this._getOption('horizontal');
var coord = isHorizontal ? 'y' : 'x';
var measure = isHorizontal ? 'height' : 'width';
var segmentName = isHorizontal ? 'rowHeight' : 'columnWidth';
var innerSize = isHorizontal ? 'innerHeight' : 'innerWidth';
// proportional re-align items
var previousSegment = this[ segmentName ];
previousSegment = previousSegment && previousSegment + this.gutter;
if ( previousSegment ) {
this._getMeasurements();
var currentSegment = this[ segmentName ] + this.gutter;
items.forEach( function( item ) {
var seg = Math.round( item.rect[ coord ] / previousSegment );
item.rect[ coord ] = seg * currentSegment;
});
} else {
var currentSize = getSize( this.element )[ innerSize ] + this.gutter;
var previousSize = this.packer[ measure ];
items.forEach( function( item ) {
item.rect[ coord ] = ( item.rect[ coord ] / previousSize ) * currentSize;
});
}
this.shiftLayout();
};
// -------------------------- drag -------------------------- //
/**
* handle an item drag start event
* @param {Element} elem
*/
proto.itemDragStart = function( elem ) {
if ( !this.isEnabled ) {
return;
}
this.stamp( elem );
// this.ignore( elem );
var item = this.getItem( elem );
if ( !item ) {
return;
}
item.enablePlacing();
item.showDropPlaceholder();
this.dragItemCount++;
this.updateShiftTargets( item );
};
proto.updateShiftTargets = function( dropItem ) {
this.shiftPacker.reset();
// pack stamps
this._getBoundingRect();
var isOriginLeft = this._getOption('originLeft');
var isOriginTop = this._getOption('originTop');
this.stamps.forEach( function( stamp ) {
// ignore dragged item
var item = this.getItem( stamp );
if ( item && item.isPlacing ) {
return;
}
var offset = this._getElementOffset( stamp );
var rect = new Rect({
x: isOriginLeft ? offset.left : offset.right,
y: isOriginTop ? offset.top : offset.bottom
});
this._setRectSize( stamp, rect );
// save its space in the packer
this.shiftPacker.placed( rect );
}, this );
// reset shiftTargets
var isHorizontal = this._getOption('horizontal');
var segmentName = isHorizontal ? 'rowHeight' : 'columnWidth';
var measure = isHorizontal ? 'height' : 'width';
this.shiftTargetKeys = [];
this.shiftTargets = [];
var boundsSize;
var segment = this[ segmentName ];
segment = segment && segment + this.gutter;
if ( segment ) {
var segmentSpan = Math.ceil( dropItem.rect[ measure ] / segment );
var segs = Math.floor( ( this.shiftPacker[ measure ] + this.gutter ) / segment );
boundsSize = ( segs - segmentSpan ) * segment;
// add targets on top
for ( var i=0; i < segs; i++ ) {
var initialX = isHorizontal ? 0 : i * segment;
var initialY = isHorizontal ? i * segment : 0;
this._addShiftTarget( initialX, initialY, boundsSize );
}
} else {
boundsSize = ( this.shiftPacker[ measure ] + this.gutter ) - dropItem.rect[ measure ];
this._addShiftTarget( 0, 0, boundsSize );
}
// pack each item to measure where shiftTargets are
var items = this._getItemsForLayout( this.items );
var packMethod = this._getPackMethod();
items.forEach( function( item ) {
var rect = item.rect;
this._setRectSize( item.element, rect );
this.shiftPacker[ packMethod ]( rect );
// add top left corner
this._addShiftTarget( rect.x, rect.y, boundsSize );
// add bottom left / top right corner
var cornerX = isHorizontal ? rect.x + rect.width : rect.x;
var cornerY = isHorizontal ? rect.y : rect.y + rect.height;
this._addShiftTarget( cornerX, cornerY, boundsSize );
if ( segment ) {
// add targets for each column on bottom / row on right
var segSpan = Math.round( rect[ measure ] / segment );
for ( var i=1; i < segSpan; i++ ) {
var segX = isHorizontal ? cornerX : rect.x + segment * i;
var segY = isHorizontal ? rect.y + segment * i : cornerY;
this._addShiftTarget( segX, segY, boundsSize );
}
}
}, this );
};
proto._addShiftTarget = function( x, y, boundsSize ) {
var checkCoord = this._getOption('horizontal') ? y : x;
if ( checkCoord !== 0 && checkCoord > boundsSize ) {
return;
}
// create string for a key, easier to keep track of what targets
var key = x + ',' + y;
var hasKey = this.shiftTargetKeys.indexOf( key ) != -1;
if ( hasKey ) {
return;
}
this.shiftTargetKeys.push( key );
this.shiftTargets.push({ x: x, y: y });
};
// -------------------------- drop -------------------------- //
proto.shift = function( item, x, y ) {
var shiftPosition;
var minDistance = Infinity;
var position = { x: x, y: y };
this.shiftTargets.forEach( function( target ) {
var distance = getDistance( target, position );
if ( distance < minDistance ) {
shiftPosition = target;
minDistance = distance;
}
});
item.rect.x = shiftPosition.x;
item.rect.y = shiftPosition.y;
};
function getDistance( a, b ) {
var dx = b.x - a.x;
var dy = b.y - a.y;
return Math.sqrt( dx * dx + dy * dy );
}
// -------------------------- drag move -------------------------- //
var DRAG_THROTTLE_TIME = 120;
/**
* handle an item drag move event
* @param {Element} elem
* @param {Number} x - horizontal change in position
* @param {Number} y - vertical change in position
*/
proto.itemDragMove = function( elem, x, y ) {
var item = this.isEnabled && this.getItem( elem );
if ( !item ) {
return;
}
x -= this.size.paddingLeft;
y -= this.size.paddingTop;
var _this = this;
function onDrag() {
_this.shift( item, x, y );
item.positionDropPlaceholder();
_this.layout();
}
// throttle
var now = new Date();
var isThrottled = this._itemDragTime && now - this._itemDragTime < DRAG_THROTTLE_TIME;
if ( isThrottled ) {
clearTimeout( this.dragTimeout );
this.dragTimeout = setTimeout( onDrag, DRAG_THROTTLE_TIME );
} else {
onDrag();
this._itemDragTime = now;
}
};
// -------------------------- drag end -------------------------- //
/**
* handle an item drag end event
* @param {Element} elem
*/
proto.itemDragEnd = function( elem ) {
var item = this.isEnabled && this.getItem( elem );
if ( !item ) {
return;
}
clearTimeout( this.dragTimeout );
item.element.classList.add('is-positioning-post-drag');
var completeCount = 0;
var _this = this;
function onDragEndLayoutComplete() {
completeCount++;
if ( completeCount != 2 ) {
return;
}
// reset drag item
item.element.classList.remove('is-positioning-post-drag');
item.hideDropPlaceholder();
_this.dispatchEvent( 'dragItemPositioned', null, [ item ] );
}
item.once( 'layout', onDragEndLayoutComplete );
this.once( 'layoutComplete', onDragEndLayoutComplete );
item.moveTo( item.rect.x, item.rect.y );
this.layout();
this.dragItemCount = Math.max( 0, this.dragItemCount - 1 );
this.sortItemsByPosition();
item.disablePlacing();
this.unstamp( item.element );
};
/**
* binds Draggabilly events
* @param {Draggabilly} draggie
*/
proto.bindDraggabillyEvents = function( draggie ) {
this._bindDraggabillyEvents( draggie, 'on' );
};
proto.unbindDraggabillyEvents = function( draggie ) {
this._bindDraggabillyEvents( draggie, 'off' );
};
proto._bindDraggabillyEvents = function( draggie, method ) {
var handlers = this.handleDraggabilly;
draggie[ method ]( 'dragStart', handlers.dragStart );
draggie[ method ]( 'dragMove', handlers.dragMove );
draggie[ method ]( 'dragEnd', handlers.dragEnd );
};
/**
* binds jQuery UI Draggable events
* @param {jQuery} $elems
*/
proto.bindUIDraggableEvents = function( $elems ) {
this._bindUIDraggableEvents( $elems, 'on' );
};
proto.unbindUIDraggableEvents = function( $elems ) {
this._bindUIDraggableEvents( $elems, 'off' );
};
proto._bindUIDraggableEvents = function( $elems, method ) {
var handlers = this.handleUIDraggable;
$elems
[ method ]( 'dragstart', handlers.start )
[ method ]( 'drag', handlers.drag )
[ method ]( 'dragstop', handlers.stop );
};
// ----- destroy ----- //
var _destroy = proto.destroy;
proto.destroy = function() {
_destroy.apply( this, arguments );
// disable flag; prevent drag events from triggering. #72
this.isEnabled = false;
};
// ----- ----- //
Packery.Rect = Rect;
Packery.Packer = Packer;
return Packery;
}));