// LICENSE: BSD-3-Clause // http://git.io/vZkLP var LTGrid = (function ($) { 'use strict' /* exported Grid */ /** * A collection of rect objects */ var Grid = (function () { /** * @param {Array} rects array of Rect objects */ function Grid(rects) { this.rects = rects || [] } /** * Return all the rects that intersect with a given rect * * @param {Rect} rect * @return {Array} */ Grid.prototype.getIntersectingRects = function (rect) { return this.rects.filter(function (item) { return rect !== item && rect.intersect(item) }) } /** * Reduce all the vertical whitespace between rects * * @return {Grid} self */ Grid.prototype.compact = function () { var rectsCopy = this.rects.slice(0) var self = this rectsCopy .sort(function (a, b) { return a.y - b.y }) .forEach(function (item) { do { item.y -= 1 } while (item.y >= 0 && self.getIntersectingRects(item).length === 0) item.y += 1 }) return this } /** * The maximum height of the rects in the grid * * @return {Number} */ Grid.prototype.height = function () { var hights = this.rects.map(function (item) { return item.bottom() }) return hights.length ? Math.max.apply(null, hights) : 0 } /** * Move a rect inside the grid, or update its size * If there is overlap move rects downards * * @param {Rect} rect * @param {Object} params An object with optional keys x, y, w, h to modify the rect * @return {Grid} self */ Grid.prototype.updateNoOverlap = function (rect, params) { var self = this this.update(rect, params) this.getIntersectingRects(rect) .forEach(function (item) { self.updateNoOverlap(item, { x: item.x, y: rect.bottom() }) }) return this } /** * Move a rect inside the grid, or update its size * * @param {Rect} rect * @param {Object} params An object with optional keys x, y, w, h to modify the rect * @return {Grid} self */ Grid.prototype.update = function (rect, params) { rect.x = ('x' in params) ? params.x : rect.x rect.y = ('y' in params) ? params.y : rect.y rect.w = ('w' in params) ? params.w : rect.w rect.h = ('h' in params) ? params.h : rect.h return this } return Grid })() /* exported LTData */ var LTData = (function ($) { var NAME = 'ltData' var LTData = { NAME: NAME } LTData.getRects = function (rects) { return $.map(rects, function (rect) { return new Rect(rect.x, rect.y, rect.w, rect.h) }) } /** * Getter / setter for rects data * * @param {string} size xs, sm, md or lg * @param {Array} rects Array of Rect objects */ $.fn[NAME] = function (size, rects) { var ltGrid = this[LTGrid.NAME]().data(LTGrid.DATA_KEY) if (undefined !== rects) { ltGrid .grid(size, new Grid(LTData.getRects(rects))) .update() return this } return ltGrid.grid(size).rects } return LTData })(jQuery) /* exported LTGrid */ var LTGrid = (function ($) { /** * ------------------------------------------------------------------------ * Constants * ------------------------------------------------------------------------ */ var NAME = 'ltGrid' var DATA_KEY = 'lt.grid' var EVENT_KEY = '.' + DATA_KEY var Event = { UPDATE : 'update', START : 'dragstart' + EVENT_KEY + ' touchstart' + EVENT_KEY, OVER : 'dragover' + EVENT_KEY + ' touchmove' + EVENT_KEY, END : 'dragend' + EVENT_KEY + ' touchcancel' + EVENT_KEY, DROP : 'drop' + EVENT_KEY + ' touchend' + EVENT_KEY, LEAVE : 'dragleave.' + EVENT_KEY } var Selector = { GRID : '[data-arrange="lt-grid"]', WIDGET : '[data-arrange="lt-grid"] .lt' } var Default = { resize: true, overlap: false, compact: true, params: { lg: { gap: 1, maxWidth: Number.MAX_VALUE, cols: 4, aspect: 2 / 3 }, md: { gap: 2, maxWidth: 1200, cols: 3, aspect: 2 / 3 }, sm: { gap: 3, maxWidth: 992, cols: 2, aspect: 2 / 3 }, xs: { gap: 4, maxWidth: 768, cols: 1, aspect: 2 / 3 } } } /** * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ /** * @param {jQuery} element * @param {Object} options */ function LTGrid(element, options) { this.$element = $(element) this._options = this._getOptions(options || {}) this.$mask = undefined this.$ghost = undefined } // getters LTGrid.Default = Default LTGrid.NAME = NAME LTGrid.DATA_KEY = DATA_KEY LTGrid.EVENT_KEY = EVENT_KEY LTGrid.prototype.options = function () { return this._options } // public /** * Update an option directly * * @param {String} name * @param {mixed} value */ LTGrid.prototype.option = function (name, value) { this._options[name] = value } /** * Get the current screen size * @return {String} xs, sm, md or lg */ LTGrid.prototype.size = function () { var currentSize var windowWidth = this._windowWidth() for (var size in this._options.params) { if (windowWidth < this._options.params[size].maxWidth) { currentSize = size } } return currentSize } /** * Compact the grid for current size */ LTGrid.prototype.compact = function () { var size = this.size() var grid = this.grid(size) this.grid(size, grid.compact()) } /** * Resize container to match items */ LTGrid.prototype.resize = function () { var size = this.size() var rect = new Rect(0, 0, 0, this.grid(size).height()) var modifiedClass = rect.setCss(this.$element.attr('class'), size) this.$element.attr('class', modifiedClass) } /** * Clear artefacts like mask and ghost and update */ LTGrid.prototype.end = function () { this._removeMask() this._removeGhost() this.update() } /** * Call resize and compact if allowed */ LTGrid.prototype.update = function () { if (this._options.compact) { this.compact() } if (this._options.resize) { this.resize() } this.$element.trigger(Event.UPDATE) } /** * Setter / getter of a Grid object for this Layout Grid * * @param {String} size xs, sm, md or lg * @param {Grid} grid * @return {Grid} */ LTGrid.prototype.grid = function (size, grid) { var $items = this.$element.children('[draggable]') if (undefined !== grid) { grid.rects.forEach(function (rect, index) { $items.eq(index)[LTRect.NAME](size, rect) }) return this } return new Grid($.map($items.toArray(), function (item) { return $(item)[LTRect.NAME](size) })) } /** * Move a widget within the grid, repositioning other elements * so there is no overlapping * * @param {jQuery} $widget * @param {Object} params An object with optional keys x, y, w, h to modify the rect */ LTGrid.prototype.reposition = function ($widget, params) { var size = this.size() var rect = $widget[LTRect.NAME](size) var grid = this.grid(size) if (this._options.overlap) { grid.update(rect, params) } else { grid.updateNoOverlap(rect, params) } this.grid(size, grid) this.update() } // private // ------------------------------------------------------------------------ /** * Move the ghost element of a widget inside the grid. * Pass a mouse x and y coords, relative to the grid * * @param {jQuery} $widget * @param {Number} mouseX * @param {Number} mouseY */ LTGrid.prototype._moveGhost = function ($widget, mouseX, mouseY) { var size = this.size() var $ghost = this._getGhost($widget) var rect = $ghost[LTRect.NAME](size) var gap = this._options.params[size].gap var cols = this._options.params[size].cols rect.x = Math.floor(mouseX / (this._itemWidth(size) + gap)) rect.y = Math.floor(mouseY / (this._itemHeight(size) + gap)) rect.x = Math.min(Math.max(0, rect.x), cols - rect.w) $ghost[LTRect.NAME](size, rect) } /** * Return a ghost item for a widget, cache ghost * * @param {jQuery} $widget * @return {jQuery} */ LTGrid.prototype._getGhost = function ($widget) { if (undefined === this.$ghost) { this.$ghost = $('
') this.$element.append(this.$ghost) } return this.$ghost } /** * Remove the ghost element for this grid * * @param {jQuery} $widget */ LTGrid.prototype._removeGhost = function () { if (this.$ghost) { this.$ghost.remove() this.$ghost = undefined } } /** * Move the widget to its corresponding ghost position * * @param {jQuery} $widget */ LTGrid.prototype._moveToGhost = function ($widget) { var size = this.size() var $parent = $widget.parent() var $ghost = this._getGhost($widget) var pos = $ghost[LTRect.NAME](size) this.$element.append($widget) this.reposition($widget, { x: pos.x, y: pos.y }) $parent.add(this.$element)[NAME]('update') } /** * Get the mask of the grid. Create one if there is none. * * @return {jQuery} */ LTGrid.prototype._getMask = function () { if (undefined === this.$mask) { this.$mask = $('
') this.$element.append(this.$mask) } return this.$mask } /** * Remove the mask */ LTGrid.prototype._removeMask = function () { if (undefined !== this.$mask) { this.$mask.remove() this.$mask = undefined } } /** * The width of a single grid count, in pixels * * @param {String} size xs, sm, md or lg * @return {Number} */ LTGrid.prototype._itemWidth = function (size) { var cols = this._options.params[size].cols var gap = this._options.params[size].gap return (this.$element.width() - (cols - 1) * gap) / cols } /** * The height of a single grid count, in pixels * * @param {String} size xs, sm, md or lg * @return {Number} */ LTGrid.prototype._itemHeight = function (size) { var aspect = this._options.params[size].aspect return this._itemWidth(size) * aspect } /** * Return the current window width * * @return {Number} */ LTGrid.prototype._windowWidth = function () { return $(window).width() } LTGrid.prototype._getOptions = function (options) { return $.extend(true, {}, Default, options) } // static LTGrid._jQueryInterface = function (config, a1, a2, a3) { return this.each(function () { var $this = $(this) var data = $this.data(DATA_KEY) var _config = $.extend( true, {}, Default, $this.data(), typeof config === 'object' && config ) if (!data) { data = new LTGrid(this, _config) $this.data(DATA_KEY, data) } if (typeof config === 'string') { data[config](a1, a2, a3) } }) } /** * ------------------------------------------------------------------------ * Data Api implementation * ------------------------------------------------------------------------ */ $(document) .on(Event.START, Selector.WIDGET, function (event) { Store.set(event.originalEvent, this) }) .on(Event.OVER, Selector.GRID, function (event) { var original = event.originalEvent var $widget = $(Store.get(original)) if ($widget.length) { var pos = original.touches ? original.touches[0] : original var $this = $(this) var mouseX = pos.pageX - $this.offset().left var mouseY = pos.pageY - $this.offset().top var grid = $this[NAME]().data(DATA_KEY) event.preventDefault() grid._getMask() grid._moveGhost($widget, mouseX, mouseY) } }) .on(Event.END, Selector.GRID, function () { $(this)[NAME]('end') }) .on(Event.LEAVE, Selector.GRID, function (event) { event.preventDefault() if ($(event.target).data('lt-grid') === 'mask') { $(this)[NAME]('end') } }) .on(Event.DROP, Selector.GRID, function (event) { var $widget = $(Store.get(event.originalEvent)) if ($widget.length) { var $this = $(this) var grid = $this[NAME]().data(DATA_KEY) event.preventDefault() grid._moveToGhost($widget) grid.end() } }) /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ */ $.fn[NAME] = LTGrid._jQueryInterface $.fn[NAME].LTGrid = LTGrid return LTGrid })(jQuery) /* exported LTGridOnly */ var LTGridOnly = (function ($) { /** * ------------------------------------------------------------------------ * Constants * ------------------------------------------------------------------------ */ var NAME = 'ltGridOnly' var DATA_KEY = 'lt.grid-only' var EVENT_KEY = '.' + DATA_KEY var Event = { CLICK : 'click' + EVENT_KEY } var Css = { xs : 'lt-only-xs', sm : 'lt-only-sm', md : 'lt-only-md', lg : 'lt-only-lg' } var Selector = { TOGGLE : '[data-toggle="lt-grid-only"]' } /** * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ /** * @param {jQuery} element * @param {Object} options */ function LTGridOnly(element, options) { this.$element = $(element) this.$target = $(options.target)[LTGrid.NAME]() this.ltGrid = this.$target.data(LTGrid.DATA_KEY) } // getters LTGridOnly.NAME = NAME LTGridOnly.DATA_KEY = DATA_KEY LTGridOnly.EVENT_KEY = EVENT_KEY // public /** * Save original params for later use */ LTGridOnly.prototype.originalParams = function () { if (undefined === this.ltGrid.options().originalParams) { this.ltGrid.options().originalParams = this.ltGrid.options().params } return this.ltGrid.options().originalParams } /** * Compact the grid for current size * @param {String} size */ LTGridOnly.prototype.set = function (size) { var original = this.originalParams() var onlyClasses = $.map( Css, function (name) { return name } ) this.$target.removeClass(onlyClasses.join(' ')) if (size) { var params = {} params[size] = original[size] params[size].maxWidth = Number.MAX_VALUE this.$target.addClass(Css[size]) this.ltGrid.option('params', params) } else { this.ltGrid.option('params', original) } } // static LTGridOnly._jQueryInterface = function (config, a1) { return this.each(function () { var $this = $(this) var data = $this.data(DATA_KEY) var _config = $.extend( true, {}, $this.data(), typeof config === 'object' && config ) if (!data) { data = new LTGridOnly(this, _config) $this.data(DATA_KEY, data) } if (typeof config === 'string') { data[config](a1) } }) } /** * ------------------------------------------------------------------------ * Data Api implementation * ------------------------------------------------------------------------ */ $(document) .on(Event.CLICK, Selector.TOGGLE, function (event) { var $item = $(event.target) $item[LTGridOnly.NAME]('set', $item.data('only')) }) /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ */ $.fn[NAME] = LTGridOnly._jQueryInterface $.fn[NAME].LTGridOnly = LTGridOnly return LTGridOnly })(jQuery) /* exported LTRect */ var LTRect = (function ($) { var NAME = 'ltRect' var DATA_KEY = 'lt.rect' var LTRect = { NAME: NAME, DATA_KEY: DATA_KEY } /** * Getter / setter for div element's rect. * Uses its css classes to laod the initial rect for a given size, * then caches in data. * Each screen size has its own rect * * @param {string} size xs, sm, md or lg * @param {Rect} newRect a Rect object to set * @return {jQuery} */ $.fn[NAME] = function (size, newRect) { if (undefined === newRect) { if (undefined === this.data(DATA_KEY + size)) { this.data(DATA_KEY + size, (new Rect()).loadCss(this.attr('class'), size)) } return this.data(DATA_KEY + size) } this.data(DATA_KEY + size, newRect) this.attr('class', newRect.setCss(this.attr('class'), size)) return this } return LTRect })(jQuery) /* exported LTSize */ var LTSize = (function ($) { var NAME = 'ltSize' var LTSize = { NAME: NAME } /** * Get the current size of the grid */ $.fn[NAME] = function () { return this[LTGrid.NAME]().data(LTGrid.DATA_KEY).size() } return LTSize })(jQuery) /* exported Rect */ /** * Object that represents a rectangle with many supporting methods */ var Rect = (function () { var paramNames = ['x', 'y', 'w', 'h'] /** * @param {Number} x default 0 * @param {Number} y default 0 * @param {Number} w width, default 1 * @param {Number} h height, default 1 */ function Rect(x, y, w, h) { this.x = x || 0 this.y = y || 0 this.w = w || 1 this.h = h || 1 } /** * @return {Number} */ Rect.prototype.bottom = function () { return this.y + this.h } /** * @return {Number} */ Rect.prototype.right = function () { return this.x + this.w } /** * Check if this rect is intersecting with another rect * * @param {Rect} rect * @return {Boolean} */ Rect.prototype.intersect = function (rect) { return this.x < rect.right() && this.right() > rect.x && this.y < rect.bottom() && this.bottom() > rect.y } /** * Modify a "css classes" string * with the pos and size of this rect, * for a specific screen size * * @param {String} classes html classes * @param {String} size xs, sm, md or lg * @return {String} */ Rect.prototype.setCss = function (classes, size) { var self = this paramNames.forEach(function (name) { classes = classes.replace(new RegExp('lt-' + size + '-' + name + '-(\\d+)'), 'lt-' + size + '-' + name + '-' + self[name]) }) return classes } /** * Load data from "css classes", for a specific screen size * * @param {String} classes html classes * @param {String} size xs, sm, md or lg */ Rect.prototype.loadCss = function (classes, size) { var self = this paramNames.forEach(function (name) { var match = classes.match(new RegExp('lt-' + size + '-' + name + '-(\\d+)')) if (match) { self[name] = parseInt(match[1], 10) } }) return this } return Rect })() /* exported Store */ /** * A class to store / retrieve element inside of dataTransfer object of an event * Fall back to a static variable if dataTransfer is not available */ var Store = (function () { var Store = {} /** * Genrate a time based random number * * @return {Number} */ Store.getRandomNumber = function () { return Math.round(new Date().getTime() + (Math.random() * 100)) } /** * Make sure the item has an id to quickly find it * Do not override existing ids * * @param {Element} item * @return {String} */ Store.getId = function (item) { if (!item.id) { item.id = 'lt-' + this.getRandomNumber() } return item.id } /** * Clear internal storage variable */ Store.clear = function () { this.item = null } /** * Save the element * * @param {Event} event * @param {Element} item */ Store.set = function (event, item) { this.item = JSON.stringify({ LTWidget: this.getId(item) }) if (event.dataTransfer) { event.dataTransfer.setData('text', this.item) } } /** * Retrieve stored element * * @param {Event} event * @return {Element} */ Store.get = function (event) { var dataString = (event.dataTransfer && event.dataTransfer.getData('text')) || this.item if (dataString) { var data = JSON.parse(dataString) return document.getElementById(data.LTWidget) } } return Store })() LTGrid.Rect = Rect LTGrid.Grid = Grid return LTGrid })(jQuery) //# sourceMappingURL=layout-grid.js.map