From 8a094443cba1e34bcd43634fe008a1f4536818f8 Mon Sep 17 00:00:00 2001 From: vieron Date: Fri, 25 Oct 2013 22:29:58 +0200 Subject: [PATCH] feat(gridster): drag-and-drop widget resizing --- src/jquery.gridster.css | 60 +++++++- src/jquery.gridster.js | 320 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 367 insertions(+), 13 deletions(-) diff --git a/src/jquery.gridster.css b/src/jquery.gridster.css index c38a9c5a..6d01764d 100644 --- a/src/jquery.gridster.css +++ b/src/jquery.gridster.css @@ -47,7 +47,8 @@ transition: left .3s, top .3s!important; } -.gridster .dragging { +.gridster .dragging, +.gridster .resizing { z-index: 10!important; -webkit-transition: all 0s !important; -moz-transition: all 0s !important; @@ -55,7 +56,62 @@ transition: all 0s !important; } + +.gs-resize-handle { + position: absolute; + z-index: 1; +} + +.gs-resize-handle-both { + width: 20px; + height: 20px; + bottom: -8px; + right: -8px; + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); + background-position: top left; + background-repeat: no-repeat; + cursor: se-resize; + z-index: 20; +} + +.gs-resize-handle-x { + top: 0; + bottom: 13px; + right: -5px; + width: 10px; + cursor: e-resize; +} + +.gs-resize-handle-y { + left: 0; + right: 13px; + bottom: -5px; + height: 10px; + cursor: s-resize; +} + +.gs-w:hover .gs-resize-handle, +.resizing .gs-resize-handle { + opacity: 1; +} + +.gs-resize-handle, +.gs-w.dragging .gs-resize-handle { + opacity: 0; +} + +.gs-resize-disabled .gs-resize-handle { + display: none!important; +} + +[data-max-sizex="1"] .gs-resize-handle-x, +[data-max-sizey="1"] .gs-resize-handle-y, +[data-max-sizey="1"][data-max-sizex="1"] .gs-resize-handle { + display: none !important; +} + /* Uncomment this if you set helper : "clone" in draggable options */ /*.gridster .player { opacity:0; -}*/ +} +*/ \ No newline at end of file diff --git a/src/jquery.gridster.js b/src/jquery.gridster.js index 8e8653c0..8821dfcf 100644 --- a/src/jquery.gridster.js +++ b/src/jquery.gridster.js @@ -32,6 +32,13 @@ draggable: { items: '.gs-w', distance: 4 + }, + resize: { + enabled: false, + axes: ['x', 'y', 'both'], + handle_append_to: '', + handle_class: 'gs-resize-handle', + max_size: [Infinity, Infinity] } }; @@ -80,6 +87,19 @@ * @param {Object} [options.draggable] An Object with all options for * Draggable class you want to overwrite. See Draggable docs for more * info. + * @param {Object} [options.resize] An Object with resize config + * options. + * @param {Boolean} [options.resize.enabled] Set to true to enable + * resizing. + * @param {Array} [options.resize.axes] Axes in which widgets can be + * resized. Possible values: ['x', 'y', 'both']. + * @param {String} [options.resize.handle_append_to] Set a valid CSS + * selector to append resize handles to. + * @param {String} [options.resize.handle_class] CSS class name used + * by resize handles. + * @param {Array} [options.resize.max_size] Limit widget dimensions + * when resizing. Array values should be integers: + * `[max_cols_occupied, max_rows_occupied]` * * @constructor */ @@ -87,7 +107,8 @@ this.options = $.extend(true, defaults, options); this.$el = $(el); this.$wrapper = this.$el.parent(); - this.$widgets = this.$el.children(this.options.widget_selector).addClass('gs-w'); + this.$widgets = this.$el.children( + this.options.widget_selector).addClass('gs-w'); this.widgets = []; this.$changed = $([]); this.wrapper_width = this.$wrapper.width(); @@ -106,11 +127,13 @@ var fn = Gridster.prototype; fn.init = function() { + this.options.resize.enabled && this.setup_resize(); this.generate_grid_and_stylesheet(); this.get_widgets_from_DOM(); this.set_dom_grid_height(); this.$wrapper.addClass('ready'); this.draggable(); + this.options.resize.enabled && this.resizable(); $(window).bind('resize.gridster', throttle( $.proxy(this.recalculate_faux_grid, this), 200)); @@ -142,6 +165,33 @@ }; + + /** + * Disables drag-and-drop widget resizing. + * + * @method disable + * @return {Class} Returns instance of gridster Class. + */ + fn.disable_resize = function() { + this.$el.addClass('gs-resize-disabled'); + this.resize_api.disable(); + return this; + }; + + + /** + * Enables drag-and-drop widget resizing. + * + * @method enable + * @return {Class} Returns instance of gridster Class. + */ + fn.enable_resize = function() { + this.$el.removeClass('gs-resize-disabled'); + this.resize_api.enable(); + return this; + }; + + /** * Add a new widget to the grid. * @@ -152,10 +202,11 @@ * @param {Number} [size_y] The nÂș of columns the widget occupies vertically. * @param {Number} [col] The column the widget should start in. * @param {Number} [row] The row the widget should start in. + * @param {Array} [max_size] max_size Maximun size (in units) for width and height. * @return {HTMLElement} Returns the jQuery wrapped HTMLElement representing. * the widget that was just created. */ - fn.add_widget = function(html, size_x, size_y, col, row) { + fn.add_widget = function(html, size_x, size_y, col, row, max_size) { var pos; size_x || (size_x = 1); size_y || (size_y = 1); @@ -185,14 +236,56 @@ this.add_faux_rows(pos.size_y); //this.add_faux_cols(pos.size_x); + if (max_size) { + this.set_widget_max_size($w, max_size); + } + this.set_dom_grid_height(); return $w.fadeIn(); }; + /** + * Change widget size limits. + * + * @method set_widget_max_size + * @param {HTMLElement|Number} $widget The jQuery wrapped HTMLElement + * representing the widget or an index representing the desired widget. + * @param {Array} max_size Maximun size (in units) for width and height. + * @return {HTMLElement} Returns instance of gridster Class. + */ + fn.set_widget_max_size = function($widget, max_size) { + $widget = typeof $widget === 'number' ? + this.$widgets.eq($widget) : $widget; - /** + if (!$widget.length) { return this; } + + var wgd = $widget.data('coords').grid; + wgd.max_size_x = max_size[0]; + wgd.max_size_y = max_size[1]; + + return this; + }; + + + /** + * Append the resize handle into a widget. + * + * @method add_resize_handle + * @param {HTMLElement} $widget The jQuery wrapped HTMLElement + * representing the widget. + * @return {HTMLElement} Returns instance of gridster Class. + */ + fn.add_resize_handle = function($w) { + var append_to = this.options.resize.handle_append_to; + $(this.resize_handle_tpl).appendTo( append_to ? $(append_to, $w) : $w); + + return this; + }; + + + /** * Change the size of a widget. Width is limited to the current grid width. * * @method resize_widget @@ -200,11 +293,16 @@ * representing the widget. * @param {Number} size_x The number of columns that will occupy the widget. * @param {Number} size_y The number of rows that will occupy the widget. - * @param {Function} callback Function executed when the widget is removed. + * @param {Boolean} [reposition] Set to false to not move the widget to + * the left if there is insufficient space on the right. + * By default size_x is limited to the space available from + * the column where the widget begins, until the last column to the right. + * @param {Function} [callback] Function executed when the widget is removed. * @return {HTMLElement} Returns $widget. */ - fn.resize_widget = function($widget, size_x, size_y, callback) { + fn.resize_widget = function($widget, size_x, size_y, reposition, callback) { var wgd = $widget.coords().grid; + reposition !== false && (reposition = true); size_x || (size_x = wgd.size_x); size_y || (size_y = wgd.size_y); @@ -216,7 +314,7 @@ var old_col = wgd.col; var new_col = old_col; - if (old_col + size_x - 1 > this.cols) { + if (reposition && old_col + size_x - 1 > this.cols) { var diff = old_col + (size_x - 1) - this.cols; var c = old_col - diff; new_col = Math.max(1, c); @@ -245,8 +343,17 @@ }; + /** + * Mutate widget dimensions and position in the grid map. + * + * @method mutate_widget_in_gridmap + * @param {HTMLElement} $widget The jQuery wrapped HTMLElement + * representing the widget to mutate. + * @param {Object} wgd Current widget grid data (col, row, size_x, size_y). + * @param {Object} new_wgd New widget grid data. + * @return {HTMLElement} Returns instance of gridster Class. + */ fn.mutate_widget_in_gridmap = function($widget, wgd, new_wgd) { - var old_size_x = wgd.size_x; var old_size_y = wgd.size_y; @@ -303,6 +410,8 @@ this.add_to_gridmap(new_wgd, $widget); + $widget.removeClass('player-revert'); + //update coords instance attributes $widget.data('coords').update({ width: (new_wgd.size_x * this.options.widget_base_dimensions[0] + @@ -558,12 +667,13 @@ * @return {Array} Returns the instance of the Gridster class. */ fn.register_widget = function($el) { - var wgd = { 'col': parseInt($el.attr('data-col'), 10), 'row': parseInt($el.attr('data-row'), 10), 'size_x': parseInt($el.attr('data-sizex'), 10), 'size_y': parseInt($el.attr('data-sizey'), 10), + 'max_size_x': parseInt($el.attr('data-max-sizex'), 10) || false, + 'max_size_y': parseInt($el.attr('data-max-sizey'), 10) || false, 'el': $el }; @@ -571,8 +681,7 @@ !this.can_move_to( {size_x: wgd.size_x, size_y: wgd.size_y}, wgd.col, wgd.row) ) { - wgd = this.next_position(wgd.size_x, wgd.size_y); - wgd.el = $el; + $.extend(wgd, this.next_position(wgd.size_x, wgd.size_y)); $el.attr({ 'data-col': wgd.col, 'data-row': wgd.row, @@ -583,12 +692,13 @@ // attach Coord object to player data-coord attribute $el.data('coords', $el.coords()); - // Extend Coord object with grid position info $el.data('coords').grid = wgd; this.add_to_gridmap(wgd, $el); + this.options.resize.enabled && this.add_resize_handle($el); + return this; }; @@ -661,6 +771,8 @@ var draggable_options = $.extend(true, {}, this.options.draggable, { offset_left: this.options.widget_margins[0], container_width: this.container_width, + ignore_dragging: ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', + '.' + this.options.resize.handle_class], start: function(event, ui) { self.$widgets.filter('.player-revert') .removeClass('player-revert'); @@ -688,6 +800,50 @@ }; + /** + * Bind resize events to get resize working. + * + * @method resizable + * @return {Class} Returns instance of gridster Class. + */ + fn.resizable = function() { + this.resize_api = this.$el.drag({ + items: '.' + this.options.resize.handle_class, + offset_left: this.options.widget_margins[0], + container_width: this.container_width, + move_element: false, + start: $.proxy(this.on_start_resize, this), + stop: $.proxy(function(event, ui) { + delay($.proxy(function() { + this.on_stop_resize(event, ui); + }, this), 120); + }, this), + drag: throttle($.proxy(this.on_resize, this), 60) + }); + + return this; + }; + + + /** + * Setup things required for resizing. Like build templates for drag handles. + * + * @method setup_resize + * @return {Class} Returns instance of gridster Class. + */ + fn.setup_resize = function() { + this.resize_handle_class = this.options.resize.handle_class; + var axes = this.options.resize.axes; + var handle_tpl = ''; + + this.resize_handle_tpl = $.map(axes, function(type) { + return handle_tpl.replace('{type}', type); + }).join(''); + return this; + }; + + /** * This function is executed when the player begins to be dragged. * @@ -842,6 +998,148 @@ }; + + /** + * This function is executed every time a widget starts to be resized. + * + * @method on_start_resize + * @param {Event} event The original browser event + * @param {Object} ui A prepared ui object with useful drag-related data + */ + fn.on_start_resize = function(event, ui) { + this.$resized_widget = ui.$player.closest('.gs-w'); + this.resize_coords = this.$resized_widget.coords(); + this.resize_wgd = this.resize_coords.grid; + this.resize_initial_width = this.resize_coords.coords.width; + this.resize_initial_height = this.resize_coords.coords.height; + this.resize_initial_sizex = this.resize_coords.grid.size_x; + this.resize_initial_sizey = this.resize_coords.grid.size_y; + this.resize_last_sizex = this.resize_initial_sizex; + this.resize_last_sizey = this.resize_initial_sizey; + this.resize_max_size_x = Math.min(this.resize_wgd.max_size_x || + this.options.resize.max_size[0], this.cols - this.resize_wgd.col + 1); + this.resize_max_size_y = this.resize_wgd.max_size_y || + this.options.resize.max_size[1]; + + this.resize_dir = { + right: ui.$player.is('.' + this.resize_handle_class + '-x'), + bottom: ui.$player.is('.' + this.resize_handle_class + '-y') + }; + + this.$resized_widget.css({ + 'min-width': this.options.widget_base_dimensions[0], + 'min-height': this.options.widget_base_dimensions[1] + }); + + var nodeName = this.$resized_widget.get(0).tagName; + this.$resize_preview_holder = $('<' + nodeName + ' />', { + 'class': 'preview-holder resize-preview-holder', + 'data-row': this.$resized_widget.attr('data-row'), + 'data-col': this.$resized_widget.attr('data-col'), + 'css': { + 'width': this.resize_initial_width, + 'height': this.resize_initial_height + } + }).appendTo(this.$el); + + this.$resized_widget.addClass('resizing'); + }; + + + /** + * This function is executed every time a widget stops being resized. + * + * @method on_stop_resize + * @param {Event} event The original browser event + * @param {Object} ui A prepared ui object with useful drag-related data + */ + fn.on_stop_resize = function(event, ui) { + this.$resized_widget + .removeClass('resizing') + .css({ + 'width': '', + 'height': '' + }); + + delay($.proxy(function() { + this.$resize_preview_holder + .remove() + .css({ + 'min-width': '', + 'min-height': '' + }); + }, this), 300); + }; + + /** + * This function is executed when a widget is being resized. + * + * @method on_resize + * @param {Event} event The original browser event + * @param {Object} ui A prepared ui object with useful drag-related data + */ + fn.on_resize = function(event, ui) { + var rel_x = (ui.pointer.diff_left); + var rel_y = (ui.pointer.diff_top); + var wbd_x = this.options.widget_base_dimensions[0]; + var wbd_y = this.options.widget_base_dimensions[1]; + var max_width = Infinity; + var max_height = Infinity; + + var inc_units_x = Math.ceil((rel_x / + (this.options.widget_base_dimensions[0] + + this.options.widget_margins[0] * 2)) - 0.2); + + var inc_units_y = Math.ceil((rel_y / + (this.options.widget_base_dimensions[1] + + this.options.widget_margins[1] * 2)) - 0.2); + + var size_x = Math.max(1, this.resize_initial_sizex + inc_units_x); + var size_y = Math.max(1, this.resize_initial_sizey + inc_units_y); + + size_x = Math.min(size_x, this.resize_max_size_x); + max_width = (this.resize_max_size_x * wbd_x) + + ((size_x - 1) * this.options.widget_margins[0] * 2); + + size_y = Math.min(size_y, this.resize_max_size_y); + max_height = (this.resize_max_size_y * wbd_y) + + ((size_y - 1) * this.options.widget_margins[1] * 2); + + + if (this.resize_dir.right) { + size_y = this.resize_initial_sizey; + } else if (this.resize_dir.bottom) { + size_x = this.resize_initial_sizex; + } + + var css_props = {}; + !this.resize_dir.bottom && (css_props.width = Math.min( + this.resize_initial_width + rel_x, max_width)); + !this.resize_dir.right && (css_props.height = Math.min( + this.resize_initial_height + rel_y, max_height)); + + this.$resized_widget.css(css_props); + + if (size_x !== this.resize_last_sizex || + size_y !== this.resize_last_sizey) { + + this.resize_widget(this.$resized_widget, size_x, size_y, false); + + this.$resize_preview_holder.css({ + 'width': '', + 'height': '' + }).attr({ + 'data-row': this.$resized_widget.attr('data-row'), + 'data-sizex': size_x, + 'data-sizey': size_y + }); + } + + this.resize_last_sizex = size_x; + this.resize_last_sizey = size_y; + }; + + /** * Executes the callbacks passed as arguments when a column begins to be * overlapped or stops being overlapped.