UNPKG

packrat-ui

Version:

realtime bin-packing layout library

604 lines (497 loc) 15.3 kB
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ var Rect = require('./rect'); function Item(el, layout) { this.el = el; this.$el = jQuery(el); this.id = this.$el.attr('id'); // things have to ids for now this.layout = layout; this.rect = new Rect(); this.placeRect = new Rect(); // we will use this at some point... this.updateRect(); } Item.prototype.updateRect = function() { var pos = this.layout.$el.position(); this.rect.x = this.$el.position().left - pos.left; this.rect.y = this.$el.position().top - pos.top; this.rect.width = this.$el.width(); this.rect.height = this.$el.height(); this.placeRect.width = this.$el.width(); this.placeRect.height = this.$el.height(); }; Item.prototype.updateDOM = function() { var pos = this.layout.$el.position(); this.$el.css({ left: pos.left + this.rect.x, top: pos.top + this.rect.y }); }; Item.prototype.dragMove = function(x, y) { var pos = this.layout.$el.position(); x = x - pos.left; y = y - pos.top; this.placeRect.x = x; this.placeRect.y = y; this.rect.x = x; this.rect.y = y; }; module.exports = Item; },{"./rect":4}],2:[function(require,module,exports){ /** * bin-packing layout library - Credit goes to desandro * * Licensed GPLv3 for open source use * or Packery Commercial License for commercial use * * Copyright 2015 Metafizzy */ var Rect = require('./rect'); /** * @param {Number} width * @param {Number} height * @param {String} sortDirection * topLeft for vertical, leftTop for horizontal */ function Packer( width, height, sortDirection ) { this.width = width || 0; this.height = height || 0; this.sortDirection = sortDirection || 'downwardLeftToRight'; this.reset(); } Packer.prototype.reset = function() { this.spaces = []; this.newSpaces = []; var initialSpace = new Rect({ x: 0, y: 0, width: this.width, height: this.height }); this.spaces.push( initialSpace ); // set sorter this.sorter = sorters[ this.sortDirection ] || sorters.downwardLeftToRight; }; // change x and y of rect to fit with in Packer's available spaces Packer.prototype.pack = function( rect ) { for ( var i=0, len = this.spaces.length; i < len; i++ ) { var space = this.spaces[i]; if ( space.canFit( rect ) ) { this.placeInSpace( rect, space ); break; } } }; Packer.prototype.placeInSpace = function( rect, space ) { // place rect in space rect.x = space.x; rect.y = space.y; this.placed( rect ); }; // update spaces with placed rect Packer.prototype.placed = function( rect ) { // update spaces var revisedSpaces = []; for ( var i=0, len = this.spaces.length; i < len; i++ ) { var space = this.spaces[i]; var newSpaces = space.getMaximalFreeRects( rect ); // add either the original space or the new spaces to the revised spaces if ( newSpaces ) { revisedSpaces.push.apply( revisedSpaces, newSpaces ); } else { revisedSpaces.push( space ); } } this.spaces = revisedSpaces; this.mergeSortSpaces(); }; Packer.prototype.mergeSortSpaces = function() { // remove redundant spaces Packer.mergeRects( this.spaces ); this.spaces.sort( this.sorter ); }; // add a space back Packer.prototype.addSpace = function( rect ) { this.spaces.push( rect ); this.mergeSortSpaces(); }; // -------------------------- utility functions -------------------------- // /** * Remove redundant rectangle from array of rectangles * @param {Array} rects: an array of Rects * @returns {Array} rects: an array of Rects **/ Packer.mergeRects = function( rects ) { for ( var i=0, len = rects.length; i < len; i++ ) { var rect = rects[i]; // skip over this rect if it was already removed if ( !rect ) { continue; } // clone rects we're testing, remove this rect var compareRects = rects.slice(0); // do not compare with self compareRects.splice( i, 1 ); // compare this rect with others var removedCount = 0; for ( var j=0, jLen = compareRects.length; j < jLen; j++ ) { var compareRect = compareRects[j]; // if this rect contains another, // remove that rect from test collection var indexAdjust = i > j ? 0 : 1; if ( rect.contains( compareRect ) ) { // console.log( 'current test rects:' + testRects.length, testRects ); // console.log( i, j, indexAdjust, rect, compareRect ); rects.splice( j + indexAdjust - removedCount, 1 ); removedCount++; } } } return rects; }; // -------------------------- sorters -------------------------- // // functions for sorting rects in order var sorters = { // top down, then left to right downwardLeftToRight: function( a, b ) { return a.y - b.y || a.x - b.x; }, // left to right, then top down rightwardTopToBottom: function( a, b ) { return a.x - b.x || a.y - b.y; } }; module.exports = Packer; },{"./rect":4}],3:[function(require,module,exports){ require('./vendor/jquery-bridget'); var Packer = require('./packer'); var Rect = require('./rect'); var Item = require('./item'); var Packrat = function(el, options) { this.el = el; this.$el = jQuery(el); this.options = options; this._init(); }; Packrat.prototype._init = function() { var self = this; this.packer = new Packer(this.$el.width(), this.$el.height()); this.collection = {}; this.order = []; this.stamps = []; this.$el.droppable({ accept: this.options.accept, hoverClass: 'hovered', drop: function(event, ui) { self.attachElement(ui.draggable); self.layout(); }, out: function(event, ui) { self.detachElement(ui.draggable); }, over: function(event, ui) { self.attachElement(ui.draggable); } }); }; Packrat.prototype.isAttached = function(element) { var id = element.attr('id'); return (id in this.collection); }; Packrat.prototype.attachElement = function(element) { if (this.isAttached(element)) { return; } var self = this; var item = new Item(element, this); this.collection[element.attr('id')] = item; this.order.push(item); element.data('packrat.item', item); item.events = { dragstart: function(event, ui) { self.itemDragStart(item, ui); }, drag: function(event, ui) { self.itemDragMove(item, ui.position.left, ui.position.right); }, dragstop: function(event, ui) { self.itemDragStop(item, ui); } }; element.on('dragstart', item.events.dragstart); element.on('drag', item.events.drag); element.on('dragstop', item.events.dragstop); item.unbindEvents = function() { element.off('dragstart', item.events.dragstart); element.off('drag', item.events.drag); element.off('dragstop', item.events.dragstop); }; }; Packrat.prototype.itemDragStart = function(item) { item.isPlacing = true; this.packer.addSpace(item.rect); }; Packrat.prototype.itemDragMove = function(item, x, y) { item.dragMove(x, y); this.order.sort(function(a, b) { return a.rect.x - b.rect.x || a.rect.y - b.rect.y; }); this.layout(); }; Packrat.prototype.itemDragStop = function(item) { item.isPlacing = false; item.updateDOM(); }; Packrat.prototype.beforeLayout = function() { this.packer.reset(); }; Packrat.prototype.afterLayout = function() { }; Packrat.prototype.layout = function() { this.beforeLayout(); this.order.forEach(function(item) { if (item.isPlacing) { this.packer.pack(item.rect); } else { this.packer.pack(item.rect); item.updateDOM(); } }.bind(this)); this.afterLayout(); }; Packrat.prototype.getItem = function(element) { return this.collection[element.attr('id')]; }; Packrat.prototype.detachElement = function(element) { var item = element.data('packrat.item'); this.packer.addSpace(item.rect); item.unbindEvents(); for(var i=0; i<this.order.length; i++) { if (this.order[i] === item) { this.order.splice(i, 1); break; } } delete this.collection[element.attr('id')]; }; $.bridget('packrat', Packrat); module.exports = Packrat; },{"./item":1,"./packer":2,"./rect":4,"./vendor/jquery-bridget":5}],4:[function(require,module,exports){ /** * Simple Rect constructor - By desandro * * Licensed GPLv3 for open source use * or Packery Commercial License for commercial use * * Copyright 2015 Metafizzy */ function Rect( props ) { // extend properties from defaults for ( var prop in Rect.defaults ) { this[ prop ] = Rect.defaults[ prop ]; } for ( prop in props ) { this[ prop ] = props[ prop ]; } } Rect.defaults = { x: 0, y: 0, width: 0, height: 0 }; /** * Determines whether or not this rectangle wholly encloses another rectangle or point. * @param {Rect} rect * @returns {Boolean} **/ Rect.prototype.contains = function( rect ) { // points don't have width or height var otherWidth = rect.width || 0; var otherHeight = rect.height || 0; return this.x <= rect.x && this.y <= rect.y && this.x + this.width >= rect.x + otherWidth && this.y + this.height >= rect.y + otherHeight; }; /** * Determines whether or not the rectangle intersects with another. * @param {Rect} rect * @returns {Boolean} **/ Rect.prototype.overlaps = function( rect ) { var thisRight = this.x + this.width; var thisBottom = this.y + this.height; var rectRight = rect.x + rect.width; var rectBottom = rect.y + rect.height; // http://stackoverflow.com/a/306332 return this.x < rectRight && thisRight > rect.x && this.y < rectBottom && thisBottom > rect.y; }; /** * @param {Rect} rect - the overlapping rect * @returns {Array} freeRects - rects representing the area around the rect **/ Rect.prototype.getMaximalFreeRects = function( rect ) { // if no intersection, return false if ( !this.overlaps( rect ) ) { return false; } var freeRects = []; var freeRect; var thisRight = this.x + this.width; var thisBottom = this.y + this.height; var rectRight = rect.x + rect.width; var rectBottom = rect.y + rect.height; // top if ( this.y < rect.y ) { freeRect = new Rect({ x: this.x, y: this.y, width: this.width, height: rect.y - this.y }); freeRects.push( freeRect ); } // right if ( thisRight > rectRight ) { freeRect = new Rect({ x: rectRight, y: this.y, width: thisRight - rectRight, height: this.height }); freeRects.push( freeRect ); } // bottom if ( thisBottom > rectBottom ) { freeRect = new Rect({ x: this.x, y: rectBottom, width: this.width, height: thisBottom - rectBottom }); freeRects.push( freeRect ); } // left if ( this.x < rect.x ) { freeRect = new Rect({ x: this.x, y: this.y, width: rect.x - this.x, height: this.height }); freeRects.push( freeRect ); } return freeRects; }; Rect.prototype.canFit = function( rect ) { return this.width >= rect.width && this.height >= rect.height; }; module.exports = Rect; },{}],5:[function(require,module,exports){ /** * Bridget makes jQuery widgets * v1.1.0 * MIT license */ (function(window) { 'use strict'; // -------------------------- utils -------------------------- // var slice = Array.prototype.slice; function noop() {} // -------------------------- definition -------------------------- // function defineBridget( $ ) { // bail if no jQuery if ( !$ ) { return; } // -------------------------- addOptionMethod -------------------------- // /** * adds option method -> $().plugin('option', {...}) * @param {Function} PluginClass - constructor class */ function addOptionMethod( PluginClass ) { // don't overwrite original option method if ( PluginClass.prototype.option ) { return; } // option setter PluginClass.prototype.option = function( opts ) { // bail out if not an object if ( !$.isPlainObject( opts ) ){ return; } this.options = $.extend( true, this.options, opts ); }; } // -------------------------- plugin bridge -------------------------- // // helper function for logging errors // $.error breaks jQuery chaining var logError = typeof console === 'undefined' ? noop : function( message ) { console.error( message ); }; /** * jQuery plugin bridge, access methods like $elem.plugin('method') * @param {String} namespace - plugin name * @param {Function} PluginClass - constructor class */ function bridge( namespace, PluginClass ) { // add to jQuery fn namespace $.fn[ namespace ] = function( options ) { if ( typeof options === 'string' ) { // call plugin method when first argument is a string // get arguments for method var args = slice.call( arguments, 1 ); for ( var i=0, len = this.length; i < len; i++ ) { var elem = this[i]; var instance = $.data( elem, namespace ); if ( !instance ) { logError( "cannot call methods on " + namespace + " prior to initialization; " + "attempted to call '" + options + "'" ); continue; } if ( !$.isFunction( instance[options] ) || options.charAt(0) === '_' ) { logError( "no such method '" + options + "' for " + namespace + " instance" ); continue; } // trigger method with arguments var returnValue = instance[ options ].apply( instance, args ); // break look and return first value if provided if ( returnValue !== undefined ) { return returnValue; } } // return this if no return value return this; } else { return this.each( function() { var instance = $.data( this, namespace ); if ( instance ) { // apply options & init instance.option( options ); instance._init(); } else { // initialize new instance instance = new PluginClass( this, options ); $.data( this, namespace, instance ); } }); } }; } // -------------------------- bridget -------------------------- // /** * converts a Prototypical class into a proper jQuery plugin * the class must have a ._init method * @param {String} namespace - plugin name, used in $().pluginName * @param {Function} PluginClass - constructor class */ $.bridget = function( namespace, PluginClass ) { addOptionMethod( PluginClass ); bridge( namespace, PluginClass ); }; } defineBridget( window.jQuery || window.$ ); })(window); },{}]},{},[3]);