/*! * 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; }));