UNPKG

grid-list

Version:

Drag and drop library for a two-dimensional resizable and responsive list of items

369 lines (315 loc) 11.5 kB
// It does not try to register in a CommonJS environment since jQuery is not // likely to run in those environments. (function (factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery', 'grid-list'], factory); } else { factory(jQuery, GridList); } }(function($, GridList) { var DraggableGridList = function(element, options, draggableOptions) { this.options = $.extend({}, this.defaults, options); this.draggableOptions = $.extend( {}, this.draggableDefaults, draggableOptions); this.$element = $(element); this._init(); this._bindEvents(); }; DraggableGridList.prototype = { defaults: { lanes: 5, direction: "horizontal", itemSelector: 'li[data-w]', widthHeightRatio: 1, dragAndDrop: true }, draggableDefaults: { zIndex: 2, scroll: false, containment: "parent" }, destroy: function() { this._unbindEvents(); }, resize: function(lanes) { if (lanes) { this.options.lanes = lanes; } this._createGridSnapshot(); this.gridList.resizeGrid(this.options.lanes); this._updateGridSnapshot(); this.reflow(); }, resizeItem: function(element, size) { /** * Resize an item. * * @param {Object} size * @param {Number} [size.w] * @param {Number} [size.h} */ this._createGridSnapshot(); this.gridList.resizeItem(this._getItemByElement(element), size); this._updateGridSnapshot(); this.render(); }, reflow: function() { this._calculateCellSize(); this.render(); }, render: function() { this._applySizeToItems(); this._applyPositionToItems(); }, _bindMethod: function(fn) { /** * Bind prototype method to instance scope (similar to CoffeeScript's fat * arrow) */ var that = this; return function() { return fn.apply(that, arguments); }; }, _init: function() { // Read items and their meta data. Ignore other list elements (like the // position highlight) this.$items = this.$element.children(this.options.itemSelector); this.items = this._generateItemsFromDOM(); this._widestItem = Math.max.apply( null, this.items.map(function(item) { return item.w; })); this._tallestItem = Math.max.apply( null, this.items.map(function(item) { return item.h; })); // Used to highlight a position an element will land on upon drop this.$positionHighlight = this.$element.find('.position-highlight').hide(); this._initGridList(); this.reflow(); if (this.options.dragAndDrop) { // Init Draggable JQuery UI plugin for each of the list items // http://api.jqueryui.com/draggable/ this.$items.draggable(this.draggableOptions); } }, _initGridList: function() { // Create instance of GridList (decoupled lib for handling the grid // positioning and sorting post-drag and dropping) this.gridList = new GridList(this.items, { lanes: this.options.lanes, direction: this.options.direction }); }, _bindEvents: function() { this._onStart = this._bindMethod(this._onStart); this._onDrag = this._bindMethod(this._onDrag); this._onStop = this._bindMethod(this._onStop); this.$items.on('dragstart', this._onStart); this.$items.on('drag', this._onDrag); this.$items.on('dragstop', this._onStop); }, _unbindEvents: function() { this.$items.off('dragstart', this._onStart); this.$items.off('drag', this._onDrag); this.$items.off('dragstop', this._onStop); }, _onStart: function(event, ui) { // Create a deep copy of the items; we use them to revert the item // positions after each drag change, making an entire drag operation less // distructable this._createGridSnapshot(); // Since dragging actually alters the grid, we need to establish the number // of cols (+1 extra) before the drag starts this._maxGridCols = this.gridList.grid.length; }, _onDrag: function(event, ui) { var item = this._getItemByElement(ui.helper), newPosition = this._snapItemPositionToGrid(item); if (this._dragPositionChanged(newPosition)) { this._previousDragPosition = newPosition; // Regenerate the grid with the positions from when the drag started GridList.cloneItems(this._items, this.items); this.gridList.generateGrid(); // Since the items list is a deep copy, we need to fetch the item // corresponding to this drag action again item = this._getItemByElement(ui.helper); this.gridList.moveItemToPosition(item, newPosition); // Visually update item positions and highlight shape this._applyPositionToItems(); this._highlightPositionForItem(item); } }, _onStop: function(event, ui) { this._updateGridSnapshot(); this._previousDragPosition = null; // HACK: jQuery.draggable removes this class after the dragstop callback, // and we need it removed before the drop, to re-enable CSS transitions $(ui.helper).removeClass('ui-draggable-dragging'); this._applyPositionToItems(); this._removePositionHighlight(); }, _generateItemsFromDOM: function() { /** * Generate the structure of items used by the GridList lib, using the DOM * data of the children of the targeted element. The items will have an * additional reference to the initial DOM element attached, in order to * trace back to it and re-render it once its properties are changed by the * GridList lib */ var _this = this, items = [], item; this.$items.each(function(i, element) { items.push({ $element: $(element), x: Number($(element).attr('data-x')), y: Number($(element).attr('data-y')), w: Number($(element).attr('data-w')), h: Number($(element).attr('data-h')), id: Number($(element).attr('data-id')) }); }); return items; }, _getItemByElement: function(element) { // XXX: this could be optimized by storing the item reference inside the // meta data of the DOM element for (var i = 0; i < this.items.length; i++) { if (this.items[i].$element.is(element)) { return this.items[i]; } } }, _calculateCellSize: function() { if (this.options.direction === "horizontal") { this._cellHeight = Math.floor(this.$element.height() / this.options.lanes); this._cellWidth = this._cellHeight * this.options.widthHeightRatio; } else { this._cellWidth = Math.floor(this.$element.width() / this.options.lanes); this._cellHeight = this._cellWidth / this.options.widthHeightRatio; } if (this.options.heightToFontSizeRatio) { this._fontSize = this._cellHeight * this.options.heightToFontSizeRatio; } }, _getItemWidth: function(item) { return item.w * this._cellWidth; }, _getItemHeight: function(item) { return item.h * this._cellHeight; }, _applySizeToItems: function() { for (var i = 0; i < this.items.length; i++) { this.items[i].$element.css({ width: this._getItemWidth(this.items[i]), height: this._getItemHeight(this.items[i]) }); } if (this.options.heightToFontSizeRatio) { this.$items.css('font-size', this._fontSize); } }, _applyPositionToItems: function() { // TODO: Implement group separators for (var i = 0; i < this.items.length; i++) { // Don't interfere with the positions of the dragged items if (this.items[i].move) { continue; } this.items[i].$element.css({ left: this.items[i].x * this._cellWidth, top: this.items[i].y * this._cellHeight }); } // Update the width of the entire grid container with enough room on the // right to allow dragging items to the end of the grid. if (this.options.direction === "horizontal") { this.$element.width( (this.gridList.grid.length + this._widestItem) * this._cellWidth); } else { this.$element.height( (this.gridList.grid.length + this._tallestItem) * this._cellHeight); } }, _dragPositionChanged: function(newPosition) { if (!this._previousDragPosition) { return true; } return (newPosition[0] != this._previousDragPosition[0] || newPosition[1] != this._previousDragPosition[1]); }, _snapItemPositionToGrid: function(item) { var position = item.$element.position(); position[0] -= this.$element.position().left; var col = Math.round(position.left / this._cellWidth), row = Math.round(position.top / this._cellHeight); // Keep item position within the grid and don't let the item create more // than one extra column col = Math.max(col, 0); row = Math.max(row, 0); if (this.options.direction === "horizontal") { col = Math.min(col, this._maxGridCols); row = Math.min(row, this.options.lanes - item.h); } else { col = Math.min(col, this.options.lanes - item.w); row = Math.min(row, this._maxGridCols); } return [col, row]; }, _highlightPositionForItem: function(item) { this.$positionHighlight.css({ width: this._getItemWidth(item), height: this._getItemHeight(item), left: item.x * this._cellWidth, top: item.y * this._cellHeight }).show(); if (this.options.heightToFontSizeRatio) { this.$positionHighlight.css('font-size', this._fontSize); } }, _removePositionHighlight: function() { this.$positionHighlight.hide(); }, _createGridSnapshot: function() { this._items = GridList.cloneItems(this.items); }, _updateGridSnapshot: function() { // Notify the user with the items that changed since the previous snapshot this._triggerOnChange(); GridList.cloneItems(this.items, this._items); }, _triggerOnChange: function() { if (typeof(this.options.onChange) != 'function') { return; } this.options.onChange.call( this, this.gridList.getChangedItems(this._items, '$element')); } }; $.fn.gridList = function(options, draggableOptions) { var instance, method, args; if (typeof(options) == 'string') { method = options; args = Array.prototype.slice.call(arguments, 1); } this.each(function() { instance = $(this).data('_gridList'); // The plugin call be called with no method on an existing GridList // instance to re-initialize it if (instance && !method) { instance.destroy(); instance = null; } if (!instance) { instance = new DraggableGridList(this, options, draggableOptions); $(this).data('_gridList', instance); } if (method) { instance[method].apply(instance, args); } }); // Maintain jQuery chain return this; }; }));