From cbbb0bf39fd5861494fda559eacd2affd4851256 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Thu, 6 Nov 2014 21:40:03 +0000 Subject: [PATCH] Some WIP on getting home working again --- etemplate/js/et2_widget_portlet.js | 34 +- etemplate/templates/default/etemplate2.css | 3 - home/inc/class.home_legacy_portlet.inc.php | 10 +- home/inc/class.home_link_portlet.inc.php | 48 +- home/inc/class.home_list_portlet.inc.php | 63 +- home/inc/class.home_portlet.inc.php | 16 +- home/inc/class.home_ui.inc.php | 119 +- home/js/app.js | 233 +- home/templates/default/app.css | 23 +- home/templates/default/index.xet | 2 + home/templates/default/legacy.xet | 7 + home/templates/default/link.xet | 6 + home/templates/default/list.xet | 7 + home/templates/pixelegg/app.css | 5 +- home/templates/pixelegg/app.less | 10 +- phpgwapi/inc/class.egw_framework.inc.php | 2 - .../js/jquery/gridster/jquery.gridster.js | 2001 ++++++++++------- 17 files changed, 1546 insertions(+), 1043 deletions(-) create mode 100644 home/templates/default/legacy.xet create mode 100644 home/templates/default/link.xet create mode 100644 home/templates/default/list.xet diff --git a/etemplate/js/et2_widget_portlet.js b/etemplate/js/et2_widget_portlet.js index 6f9f1fe6ea..a5fc455b3a 100644 --- a/etemplate/js/et2_widget_portlet.js +++ b/etemplate/js/et2_widget_portlet.js @@ -43,12 +43,21 @@ var et2_portlet = et2_valueWidget.extend( "type": "string", "default": window.egw_webserverUrl+"/home/templates/default/edit.xet" }, + "color": { + "name": "Color", + "description": "Set the portlet color", + "type": "string", + "default": '' + }, "settings": { "name": "Customization settings", "description": "Array of customization settings, similar in structure to preference settings", "type": "any", "default": et2_no_init }, + "actions": { + default: {} + }, "width": { "default": 2, "ignore": true}, "height": { "default": 1, "type": "integer"}, "rows": {"ignore": true}, @@ -66,7 +75,7 @@ var et2_portlet = et2_valueWidget.extend( }, createNamespace: true, - GRID: 50, + GRID: 100, /** * These are the "normal" actions that every portlet is expected to have. @@ -126,10 +135,12 @@ var et2_portlet = et2_valueWidget.extend( } }); this.header = $j(document.createElement("div")) + .attr('id', this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + '_header') .addClass("ui-widget-header ui-corner-all") .appendTo(this.div) .html(this.options.title); this.content = $j(document.createElement("div")) + .attr('id', this.getInstanceManager().uniqueId+'_'+this.id.replace(/\./g, '-') + '_content') .appendTo(this.div); this.setDOMNode(this.div[0]); @@ -137,6 +148,15 @@ var et2_portlet = et2_valueWidget.extend( destroy: function() { + for(var i = 0; i < this._children.length; i++) + { + // Check for child is a different template and clear it, + // since it won't be cleared by destroy() + if(this._children[i]._inst != this._inst) + { + this._children[i]._inst.clear(); + } + } this._super.apply(this, arguments); }, @@ -233,7 +253,7 @@ var et2_portlet = et2_valueWidget.extend( buttons: et2_dialog.BUTTONS_OK_CANCEL },this); // Set seperately to avoid translation - dialog.set_title(this.egw().lang("Edit") + " " + this.options.title); + dialog.set_title(this.egw().lang("Edit") + " " + (this.options.title || '')); }, _process_edit: function(button_id, value) @@ -241,10 +261,14 @@ var et2_portlet = et2_valueWidget.extend( if(button_id != et2_dialog.OK_BUTTON) return; - // Save settings - server will reply with new content, if the portlet needs an update + // Save settings - server might reply with new content if the portlet needs an update, + // but ideally it doesn't this.div.addClass("loading"); this.egw().jsonq("home.home_ui.ajax_set_properties",[this.id, this.options.settings || {}, value], function(data) { + // This section not for us + if(!data || typeof data.attributes == 'undefined') return false; + this.div.removeClass("loading"); this.set_value(data.content); for(var key in data.attributes) @@ -337,10 +361,6 @@ var et2_portlet = et2_valueWidget.extend( { this.options.height = value; this.div.attr("data-sizey", value); - - // Explicitly set the height of the content, so it can properly scroll. Sometimes set_height() - // is called before everything is in the DOM though, so use a magic 18px in that case. - this.content.height(value * this.GRID - (this.header.outerHeight() > 0 ? this.header.outerHeight() : 18)); } }); diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index 93b882bea2..b6d9b701b2 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -1154,9 +1154,6 @@ div.message.floating { /** * et2_portlet */ -div.et2_portlet { - min-width: 100px; -} .et2_portlet .ui-widget-header { margin: 0em; padding-bottom: 4px; diff --git a/home/inc/class.home_legacy_portlet.inc.php b/home/inc/class.home_legacy_portlet.inc.php index 9012685986..5bf522c8d2 100644 --- a/home/inc/class.home_legacy_portlet.inc.php +++ b/home/inc/class.home_legacy_portlet.inc.php @@ -29,7 +29,7 @@ class home_legacy_portlet extends home_portlet */ protected $content = ''; - public function __construct(array &$context = array()) + public function __construct(array &$context = array(), &$need_reload = false) { $this->context = $context; @@ -90,9 +90,13 @@ class home_legacy_portlet extends home_portlet * unique, if needed. * @return string HTML fragment for display */ - public function get_content($id = null) + public function exec($id = null, etemplate_new &$etemplate = null) { - return $this->content; + $etemplate->read('home.legacy'); + + $etemplate->set_dom_id($id); + + $etemplate->exec('home.home_link_portlet.exec',array('legacy' => $this->content), array(),array(),array(),2); } /** diff --git a/home/inc/class.home_link_portlet.inc.php b/home/inc/class.home_link_portlet.inc.php index 5c0da52539..5df7eef421 100644 --- a/home/inc/class.home_link_portlet.inc.php +++ b/home/inc/class.home_link_portlet.inc.php @@ -24,19 +24,27 @@ class home_link_portlet extends home_portlet */ protected $title = 'Link'; + /** + * Base name for template + * @var string + */ + protected $template_name = 'home.link'; + /** * Construct the portlet * + * @param boolean $need_reload Flag to indicate that the portlet needs to be reloaded (exec will be called) */ - public function __construct(Array &$context = array()) + public function __construct(Array &$context = array(), &$need_reload = false) { // Process dropped data into something useable if($context['dropped_data']) { list($context['entry']['app'], $context['entry']['id']) = explode('::', $context['dropped_data'][0], 2); unset($context['dropped_data']); + $need_reload = true; } - if($context['entry']) + if($context['entry'] && is_array($context['entry'])); { $this->title = $context['entry']['title'] = egw_link::title($context['entry']['app'], $context['entry']['id']); } @@ -64,15 +72,33 @@ class home_link_portlet extends home_portlet } /** - * Get a fragment of HTML for display + * Generate display * * @param id String unique ID, provided to the portlet so it can make sure content is * unique, if needed. - * @return string HTML fragment for display */ - public function get_content($id = null) + public function exec($id = null, etemplate_new &$etemplate = null) { - return $this->title; + // Check for custom template for app + if($this->context && $this->context['entry'] && $this->context['entry']['app'] && + $etemplate->read($this->context['entry']['app'] . '.' . $this->template_name)) + { + // No action needed, custom template loaded as side-effect + } + else + { + $etemplate->read($this->template_name); + } + + $etemplate->set_dom_id($id); + + $content = $this->context; + if(!is_array($content['entry'])) + { + $content['entry'] = null; + } + + $etemplate->exec('home.home_link_portlet.exec',$content); } /** @@ -93,13 +119,15 @@ class home_link_portlet extends home_portlet */ public function get_properties() { - return array( - array( + $properties = parent::get_properties(); + + $properties[] = array( 'name' => 'entry', 'type' => 'link-entry', 'label' => lang('Entry'), - ) - ) + parent::get_properties(); + 'size' => '' + ); + return $properties; } /** diff --git a/home/inc/class.home_list_portlet.inc.php b/home/inc/class.home_list_portlet.inc.php index d3a8e0761f..fc44856def 100644 --- a/home/inc/class.home_list_portlet.inc.php +++ b/home/inc/class.home_list_portlet.inc.php @@ -36,7 +36,7 @@ class home_list_portlet extends home_portlet * Construct the portlet * */ - public function __construct(Array &$context = array()) + public function __construct(Array &$context = array(), &$need_reload = false) { if(!is_array($context['list'])) $context['list'] = array(); @@ -54,6 +54,7 @@ class home_list_portlet extends home_portlet } unset($add); unset($context['dropped_data']); + $need_reload = true; } if($context['title']) { @@ -89,37 +90,18 @@ class home_list_portlet extends home_portlet * unique, if needed. * @return string HTML fragment for display */ - public function get_content($id = null) + public function exec($id = null, etemplate_new &$etemplate = null) { - $list = array(); - foreach($this->context['list'] as $link_id => $link) + $etemplate->read('home.list'); + + $etemplate->set_dom_id($id); + $content = $this->context; + if(!is_array($content['list'])) { - $list[] = $link + array( - 'title' => egw_link::title($link['app'], $link['id']), - 'icon' => egw_link::get_registry($link['app'], 'icon') - ); + $content['list'] = Array(); } - // Find the portlet widget, and add a link-list to it - $script = 'app.home.List.set_content("'.$id.'", '.json_encode($list).');'; - if(egw_json_response::isJSONResponse()) - { - $response = egw_json_response::get(); - // This has to go last, after the template is loaded - $response->addBeforeSendDataCallback( - function($response, $script) { - // Bind to load event to make sure template is loaded first - $response->script('$j("#home-index").on("load", function() {'.$script.'});'); - } - ,$response, $script - ); - } else { - // Not a JSON Response? Probably an idots first load - $response = egw_json_response::get(); - // Bind to load event to make sure template is loaded first - $response->script('$j("#home-index").on("load", function() {'.$script.'});'); - } - return ''; + $etemplate->exec('home.home_list_portlet.exec',$content); } /** @@ -140,17 +122,18 @@ class home_list_portlet extends home_portlet */ public function get_properties() { - return array( - array( - 'name' => 'title', - 'type' => 'textbox', - 'label' => lang('Title'), - ), - // Internal - array( - 'name' => 'list' - ) - ) + parent::get_properties(); + $properties = parent::get_properties(); + + $properties[] = array( + 'name' => 'title', + 'type' => 'textbox', + 'label' => lang('Title'), + ); + // Internal - no type means it won't show in configure dialog + $properties[] = array( + 'name' => 'list' + ); + return $properties; } /** @@ -173,7 +156,7 @@ class home_list_portlet extends home_portlet 'type' => 'drop', 'caption' => lang('add'), 'onExecute' => 'javaScript:app.home.add_link', - 'acceptedTypes' => array('file') + array_keys($GLOBALS['egw_info']['apps']), + 'acceptedTypes' => array('file','link') + array_keys($GLOBALS['egw_info']['apps']), ) ); return $actions; diff --git a/home/inc/class.home_portlet.inc.php b/home/inc/class.home_portlet.inc.php index 1ac535daa7..b2abeae548 100644 --- a/home/inc/class.home_portlet.inc.php +++ b/home/inc/class.home_portlet.inc.php @@ -30,8 +30,9 @@ abstract class home_portlet * better to use get_properties(). * * @param context Array portlet settings such as size, as well as values for properties + * @param boolean $need_reload Flag to indicate that the portlet needs to be reloaded (exec will be called) */ - public abstract function __construct(Array &$context = array()); + public abstract function __construct(Array &$context = array(), &$need_reload = false); /** * Some descriptive information about the portlet, so that users can decide if @@ -47,13 +48,14 @@ abstract class home_portlet public abstract function get_description(); /** - * Get a fragment of HTML for display + * Generate the display for the portlet * * @param id String unique ID, provided to the portlet so it can make sure content is * unique, if needed. - * @return string HTML fragment for display + * @param etemplate etemplate_new Etemplate to generate content + * @param dom_id String ID of the etemplate targe DOM node. If passed, send -1 to etemplate->exec() */ - public abstract function get_content($id = null); + public abstract function exec($id = null, etemplate_new &$etemplate = null); /** * Return a list of settings to customize the portlet. @@ -79,6 +81,12 @@ abstract class home_portlet { $properties[$prop] = array('name' => $prop); } + + $properties[] = array( + 'name' => 'color', + 'type' => 'colorpicker', + 'label' => lang('Color'), + ); return $properties; } diff --git a/home/inc/class.home_ui.inc.php b/home/inc/class.home_ui.inc.php index 6f8cc37ca1..d87ade5a6b 100644 --- a/home/inc/class.home_ui.inc.php +++ b/home/inc/class.home_ui.inc.php @@ -34,22 +34,31 @@ class home_ui */ public function index($content = array()) { - // Disabled, for now - return; // CSS for Gridster grid layout egw_framework::includeCSS('/phpgwapi/js/jquery/gridster/jquery.gridster.css'); $template = new etemplate_new('home.index'); + // Get a list of portlets $content = array( 'portlets' => $this->get_user_portlets($template) ); - $template->setElementAttribute('portlets','actions',$this->get_actions()); + $template->setElementAttribute('home.index','actions',$this->get_actions()); //$template->setElementAttribute('portlets[1]','settings',$settings[1]); $GLOBALS['egw_info']['flags']['app_header'] = lang('home'); $GLOBALS['egw_info']['flags']['currentapp'] = 'home'; + $template->exec('home.home_ui.index', $content); + + // Now run the portlets themselves + foreach($content['portlets'] as $portlet => $p_data) + { + $id = $p_data['id']; + + if(!$id) continue; + $portlet = $this->get_portlet($id, $p_data, $content, $attrs, true); + } } /** @@ -65,8 +74,8 @@ class home_ui foreach($add_portlets as $id => &$add) { $add['id'] = 'add_' . $id; + $add['class'] = $id; } - error_log(array2string($add_portlets)); $actions = array( 'add' => array( 'type' => 'popup', @@ -78,13 +87,14 @@ class home_ui // Add all known portlets as drop actions too. If there are multiple matches, there will be a menu $drop_execute = 'javaScript:app.home.add_from_drop'; - foreach($portlets as $app => $children) + foreach($portlets as $app => &$children) { // Home portlets - uses link system, so all apps that support that are accepted if(!$children['children']) { + $children['class'] = $app; $children['onExecute'] = $drop_execute; - $children['acceptedTypes'] = array_keys(egw_link::app_list()); + $children['acceptedTypes'] = array('file','link'); $children['type'] = 'drop'; $actions["drop_$app"] = $children; } @@ -111,33 +121,45 @@ class home_ui */ protected function get_user_portlets(etemplate_new &$template) { - $portlets = array( - 'Just a hard-coded test', - ); - $attributes = array(); - $attributes[] = array( - 'title' => 'Has content', - ); + $portlets = array(); foreach((array)$GLOBALS['egw_info']['user']['preferences']['home']['portlets'] as $id => $context) { - error_log("Portlet: $id"); - error_log(array2string($context)); if(!$id || in_array($id, array_keys($GLOBALS['egw_info']['user']['apps']))) continue; - $content = ''; - $attrs = array(); - $this->get_portlet($id, $context, $content, $attrs); - $portlets[$id] = $content; - $attributes[$id] = $attrs; + + $classname = $context['class']; + $portlet = new $classname($context); + $desc = $portlet->get_description(); + $portlet_content = array( + 'id' => $id + ) + $desc + $context; + + + // Get settings + // Exclude common attributes changed through UI and settings lacking a type + $settings = $portlet->get_properties(); + foreach($settings as $key => $setting) + { + if(is_array($setting) && !array_key_exists('type',$setting)) unset($settings[$key]); + } + $settings += $context; + foreach(home_portlet::$common_attributes as $attr) + { + unset($settings[$attr]); + } + $portlet_content['settings'] = $settings; + + // Set actions + // Must be after settings so actions can take settings into account + $template->setElementAttribute("portlets[" . count($portlets) . "[$id]", 'actions', $portlet->get_actions()); + + $portlets[] = $portlet_content; } // Add in legacy HTML home bits - $this->get_legacy_portlets($template, $portlets, $attributes); - - foreach($portlets as $index => $portlet) - { - $template->setElementAttribute('portlets', $index, (array)$attributes[$index]); - } + // TODO: DOM IDs still collide + //$this->get_legacy_portlets($portlets, $attributes); + return $portlets; } @@ -148,18 +170,30 @@ class home_ui * These are specific values for the portlet's properties. * @param content String HTML fragment to be displayed - will be set by the portlet * @param attributes Array Settings that can be customized on a per-portlet basis - will be set + * @param full_exec Boolean If set, the portlet etemplates should use mode 2, if not use mode -1 * @return home_portlet The portlet object that created the content */ - protected function get_portlet($id, &$context, &$content, &$attributes) + protected function get_portlet($id, &$context, &$content, &$attributes, $full_exec = false) { if(!$context['class']) $context['class'] = 'home_link_portlet'; + // This should be set already, but just in case the execution path + // is different from normal... + if(egw_json_response::isJSONResponse()) + { + $GLOBALS['egw']->framework->response = egw_json_response::get(); + } + $classname = $context['class']; - $portlet = new $classname($context); + $portlet = new $classname($context, $full_exec); $desc = $portlet->get_description(); - $content = $portlet->get_content($id); + // Pre-set up etemplate so it only needs done once + $dom_id = 'home-index_'.$id.'_content'; + + $etemplate = new etemplate_new(); + // Exclude common attributes changed through UI and settings lacking a type $settings = $portlet->get_properties(); foreach($settings as $key => $setting) @@ -174,6 +208,7 @@ class home_ui $attributes = array( 'title' => $desc['title'], + 'color' => $settings['color'], 'settings' => $settings, 'actions' => $portlet->get_actions(), ); @@ -186,6 +221,15 @@ class home_ui $attributes[$name] = $context[$name]; } } + foreach($attributes as $attr => $value) + { + $etemplate->setElementAttribute($id, $attr, $value); + } + if($full_exec) + { + $content = $portlet->exec($id, $etemplate, $full_exec ? 2 : -1); + } + return $portlet; } @@ -195,7 +239,7 @@ class home_ui * wants some content, we make a portlet for that app using the home_legacy_portlet, * which fetches content from the home hook. */ - protected function get_legacy_portlets(&$etemplate, &$content, &$attributes) + protected function get_legacy_portlets(&$content, &$attributes) { $sorted_apps = array_keys($GLOBALS['egw_info']['user']['apps']); $portal_oldvarnames = array('mainscreen_showevents', 'homeShowEvents','homeShowLatest','mainscreen_showmail','mainscreen_showbirthdays','mainscreen_show_new_updated', 'homepage_display'); @@ -253,7 +297,7 @@ class home_ui foreach($files as $entry) { - if (!in_array($entry, array('.','..')) && substr($entry,-8) == '.inc.php' && strpos($entry,'portlet')) + if (!in_array($entry, array('.','..','class.home_legacy_portlet.inc.php')) && substr($entry,-8) == '.inc.php' && strpos($entry,'portlet')) { list(,$classname) = explode('.', $entry); if(class_exists($classname) && @@ -323,9 +367,8 @@ class home_ui else { // Get portlet settings, and merge new with old - $content = ''; $context = $values+(array)$portlets[$portlet_id]; //array('class'=>$attributes['class']); - + // Handle add IDs $classname =& $context['class']; if(strpos($classname,'add_') == 0 && !class_exists($classname)) @@ -333,8 +376,8 @@ class home_ui $add = true; $classname = substr($classname, 4); } - - $portlet = $this->get_portlet($portlet_id, $context,$content, $attributes); + $full_exec = false; + $portlet = $this->get_portlet($portlet_id, $context, $content, $attributes); $context['class'] = get_class($portlet); foreach($portlet->get_properties() as $property) @@ -349,17 +392,17 @@ class home_ui } } - // Update client side - $update = array('content' => $content, 'attributes' => $attributes); + $update = array('attributes' => $attributes); // New portlet? Flag going straight to edit mode if($add) { $update['edit_settings'] = true; } + // Send this back to the portlet widget $response->data($update); - + // Store for preference update $portlets[$portlet_id] = $context; } diff --git a/home/js/app.js b/home/js/app.js index 080b809e7d..7419c857d3 100644 --- a/home/js/app.js +++ b/home/js/app.js @@ -37,7 +37,7 @@ app.classes.home = AppJS.extend( /** * Grid resolution. Must match et2_portlet GRID */ - GRID: 50, + GRID: 100, /** * Default size for new portlets @@ -69,6 +69,13 @@ app.classes.home = AppJS.extend( // call parent this._super.apply(this, arguments); + + // Make sure all other sub-etemplates in portlets are done + var others = etemplate2.getByApplication(this.appname); + for(var i = 0; i < others.length; i++) + { + others[i].clear(); + } }, /** @@ -76,31 +83,71 @@ app.classes.home = AppJS.extend( * and ready. If you must store a reference to the et2 object, * make sure to clean it up in destroy(). * - * @param et2 etemplate2 Newly ready object + * @param {etemplate2} et2 Newly ready object + * @param {string} Template name */ - et2_ready: function(et2) + et2_ready: function(et2, name) { - // call parent - this._super.apply(this, arguments); - - this.portlet_container = this.et2.getWidgetById("portlets"); - - // Don't do twice - if(this.portlet_container._children.length > 0) return; - - // Add portlets - var content = this.et2.getArrayMgr("content").getEntry("portlets"); - var modifications = this.et2.getArrayMgr("modifications").getEntry("portlets"); - for(var key in content) + // Top level + if(name == 'home.index') { - //var attrs = jQuery.extend({id: key}, content[key], modifications[key]); - var attrs = {id: key}; - var portlet = et2_createWidget('portlet',attrs, this.portlet_container); - } - this.et2.loadingFinished(); + // call parent + this._super.apply(this, arguments); - // Set up sorting of portlets - this._do_ordering(); + this.et2.set_id('home.index'); + this.et2.set_actions(this.et2.getArrayMgr('modifications').getEntry('home.index')['actions']); + + this.portlet_container = this.et2.getWidgetById("portlets"); + + // Set up sorting of portlets + this._do_ordering(); + } + else if (et2.uniqueId) + { + // Handle bad timing - a sub-template was finished first + if(!this.portlet_container) + { + window.setTimeout(jQuery.proxy(this, function() {this.et2_ready(et2, name);}),200); + return; + } + + var portlet = this.portlet_container.getWidgetById(et2.uniqueId); + // Check for existing etemplate, this one loaded over it + // NOTE: Moving them around like this can cause problems with event handlers + var existing = etemplate2.getById(et2.uniqueId); + if(portlet && existing && existing.etemplate_exec_id != et2.etemplate_exec_id) + { + for(var i = 0; i < portlet._children.length; i++) + { + portlet._children[i]._inst.clear(); + } + portlet._children = []; + } + // It's in the right place for original load, but move it into portlet + var misplaced = $j(etemplate2.getById('home-index').DOMContainer).siblings('#'+et2.DOMContainer.id); + if(portlet) + { + portlet.addChild(et2.widgetContainer); + } + if(portlet && misplaced.length) + { + // etemplate->exec() always adds a new div, so if there's an extra one, move it + $j(et2.DOMContainer).remove(); + et2.DOMContainer = portlet.getDOMNode(et2); + et2.DOMContainer.id = et2.uniqueId; + } + } + }, + + /** + * Set top level actions + * + * @param {type} action + * @param {type} source + * @returns {undefined} + */ + set_actions: function() { + }, /** @@ -112,6 +159,8 @@ app.classes.home = AppJS.extend( var attrs = {id: this._create_id(), row: max_row + 1, col: 1}; var portlet = et2_createWidget('portlet',attrs, this.portlet_container); + // Override content ID so etemplate loads + portlet.content.attr('id', attrs.id); portlet.loadingFinished(); // Get actual attributes & settings, since they're not available client side yet @@ -150,6 +199,8 @@ app.classes.home = AppJS.extend( } var portlet = et2_createWidget('portlet',attrs, this.portlet_container); + // Override content ID so etemplate loads + portlet.content.attr('id', attrs.id); portlet.loadingFinished(); // Get actual attributes & settings, since they're not available client side yet @@ -158,7 +209,7 @@ app.classes.home = AppJS.extend( { if(source[i].id) drop_data.push(source[i].id); } - portlet._process_edit(et2_dialog.OK_BUTTON, {dropped_data: drop_data, class: action.id.substr(5)}); + portlet._process_edit(et2_dialog.OK_BUTTON, {dropped_data: drop_data, class: action.data.class || action.id.substr(5)}); // Set up sorting/grid of new portlet $portlet_container.data("gridster").add_widget( @@ -227,11 +278,11 @@ app.classes.home = AppJS.extend( */ serialize_params: function($w, grid) { return { - id: $w.attr("id"), + id: $w.children('.ui-widget-header').next().attr('id'), row: grid.row, col: grid.col, - width: grid.width, - height: grid.height + width: grid.size_x, + height: grid.size_y }; }, /** @@ -242,15 +293,18 @@ app.classes.home = AppJS.extend( stop: function(event,ui) { // Update widget(s) var changed = this.serialize_changed(); + + // Reset changed, or they keep accumulating + this.$changed = $j([]); + for (var key in changed) { if(!changed[key].id) continue; - // Changed ID is DOM id - var widget_id = changed[key].id.substr(window.app.home.et2.getInstanceManager().uniqueId.length + 1); - var widget = window.app.home.portlet_container.getWidgetById(widget_id); + // Changed ID is the ID + var widget = window.app.home.portlet_container.getWidgetById(changed[key].id); if(!widget || widget == window.app.home.portlet_container) continue; - egw().jsonq("home.home_ui.ajax_set_properties",[widget.id, widget.options.settings,{ + egw().jsonq("home.home_ui.ajax_set_properties",[changed[key].id, widget.options.settings,{ row: changed[key].row, col: changed[key].col }], @@ -263,6 +317,11 @@ app.classes.home = AppJS.extend( }); + // Bind window resize to re-layout gridster + $j(window).one("resize."+this.et2._inst.uniqueId, function() { + // Note this doesn't change the positions, just makes them invalid + $portlet_container.data('gridster').recalculate_faux_grid(); + }); // Bind resize to update gridster - this may happen _before_ the widget gets a // chance to update itself, so we can't use the widget $portlet_container @@ -295,48 +354,6 @@ app.classes.home = AppJS.extend( */ List: { - /** - * List uses mostly JS to generate its content, so we just do it on the JS side by - * returning a call to this function as the HTML content. - * - * @param id String The ID of the portlet - * @param list_values Array List of information passed to the link widget - */ - set_content: function(id, list_values) - { - try { - var portlet = app.home.portlet_container.getWidgetById(id); - } catch(e) { - egw.debug("log", "Tried to set home list content with no etemplate"); - return; - }; - if(portlet != null) - { - var list = portlet.getWidgetById(id+'-list'); - if(list) - { - // List was just rudely pulled from DOM by the call to HTML, put it back - portlet.content.append(list.getDOMNode()); - } - else - { - // Create widget - list = et2_createWidget('link-list', {id: id+'-list'}, portlet); - list.doLoadingFinished(); - // Abuse link list by overwriting delete handler - list._delete_link = app.home.List.delete_link; - } - list.set_value(list_values); - - // Disable link list context menu - $j('tr',list.list).unbind('contextmenu'); - - // Allow scroll bars - portlet.content.css('overflow', 'auto'); - } - }, - - /** * For list_portlet - opens a dialog to add a new entry to the list */ @@ -360,55 +377,79 @@ app.classes.home = AppJS.extend( } if(target_action == null) { - var link = et2_createWidget('link-entry', {label: this.egw.lang('Add')}, this.portlet_container); - var dialog = et2_dialog.show_dialog( - function(button_id) { + // use template base url from initial template, to continue using webdav, if that was loaded via webdav + var splitted = 'home.edit'.split('.'); + var path = app.home.portlet_container.getRoot()._inst.template_base_url + splitted.shift() + "/templates/default/" + + splitted.join('.')+ ".xet"; + var dialog = et2_createWidget("dialog",{ + callback: function(button_id, value) { if(button_id == et2_dialog.CANCEL_BUTTON) return; var new_list = widget.options.settings.list || []; - var add = link.getValue(); - link.destroy(); for(var i = 0; i < new_list.length; i++) { - if(new_list[i].app == add.app && new_list[i].id == add.id) + if(new_list[i].app == value.add.app && new_list[i].id == value.add.id) { - // Duplicate + // Duplicate - skip it return; } } - - new_list.push(add); + value.add.link_id = value.add.app + ':' + value.add.id; + // Update server side + new_list.push(value.add); widget._process_edit(button_id,{list: new_list}); + // Update client side + widget.getWidgetById('list').set_value(new_list); }, - 'Add', - this.egw.lang('Add'), {}, - et2_dialog.BUTTONS_OK_CANCEL - ); - dialog.set_message(link.getDOMNode()); + buttons: et2_dialog.BUTTONS_OK_CANCEL, + title: app.home.egw.lang('add'), + template:path, + value: { content: [{label: app.home.egw.lang('add'),type: 'link-entry',name: 'add',size:''}]} + }); } else { // Drag'n'dropped something on the list - just send action IDs - var drop_data = []; + var new_list = widget.options.settings.list || []; + var changed = false; + for(var i = 0; i < new_list.length; i++) + { + // Avoid duplicates + for(var j = 0; j < source.length; j++) + { + if(!source[j].id || new_list[i].app+"::"+new_list[i].id == source[j].id) + { + // Duplicate - skip it + source.splice(j,1); + } + } + } for(var i = 0; i < source.length; i++) { - if(source[i].id) drop_data.push(source[i].id); + var explode = source[i].id.split('::'); + new_list.push({app: explode[0],id: explode[1], link_id: explode.join(':')}); + changed = true; + } + widget.getWidgetById('list').set_value(new_list); + if(changed) + { + widget._process_edit(et2_dialog.OK_BUTTON,{ + list: new_list || {} + }); } - widget._process_edit(et2_dialog.BUTTONS_OK_CANCEL,{ - list: widget.options.settings.list || {}, - dropped_data: drop_data - }); } }, /** * Remove a link from the list */ - delete_link: function(undef, row) { - // Quick response + link_change: function(list, link_id, row) { + // Quick response client side row.slideUp(row.remove); + // Actual removal - this._parent.options.settings.list.splice(row.index(), 1); - this._parent._process_edit(et2_dialog.OK_BUTTON,{list: this._parent.options.settings.list || {}}); + var portlet = list._parent._parent; + portlet.options.settings.list.splice(row.index(), 1); + portlet._process_edit(et2_dialog.OK_BUTTON,{list: portlet.options.settings.list || {}}); } } }); diff --git a/home/templates/default/app.css b/home/templates/default/app.css index 97da2d2cc6..1ba1399620 100644 --- a/home/templates/default/app.css +++ b/home/templates/default/app.css @@ -2,6 +2,12 @@ * Home CSS */ +#home-index_home-index { + height:100%; +} +#home-index_portlets { + background-color: inherit; +} #portlets { border: 1px solid silver; width: 100%; @@ -20,20 +26,10 @@ .et2_portlet.ui-widget-content > div:last-of-type { height: 100%; } - -/* Shapeshift -#portlets { - position: relative; +.et2_portlet.ui-widget-content > div:last-of-type > div { + height: 100%; + background: linear-gradient(to bottom, rgba(255,255,255,.9) 10%,rgba(255,255,255,.75) 90%) /* W3C */ } -.home .et2_portlet { - position: absolute; -} -#portlets .ss-placeholder-child { - background: transparent; - border: 1px dashed silver; - position: absolute; -} -*/ /* Gridster */ #portlets { @@ -41,7 +37,6 @@ } .home .et2_portlet { position: absolute; - margin: 5px; } #portlets .preview-holder { margin: 5px; diff --git a/home/templates/default/index.xet b/home/templates/default/index.xet index 1dbe3ad75f..fe9fef2c6e 100644 --- a/home/templates/default/index.xet +++ b/home/templates/default/index.xet @@ -12,6 +12,8 @@ + + diff --git a/home/templates/default/legacy.xet b/home/templates/default/legacy.xet new file mode 100644 index 0000000000..baace61c7b --- /dev/null +++ b/home/templates/default/legacy.xet @@ -0,0 +1,7 @@ + + + + + diff --git a/home/templates/default/link.xet b/home/templates/default/link.xet new file mode 100644 index 0000000000..3c93690296 --- /dev/null +++ b/home/templates/default/link.xet @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/home/templates/default/list.xet b/home/templates/default/list.xet new file mode 100644 index 0000000000..158e0bc0f3 --- /dev/null +++ b/home/templates/default/list.xet @@ -0,0 +1,7 @@ + + + + + diff --git a/home/templates/pixelegg/app.css b/home/templates/pixelegg/app.css index f6443bde57..775f85728d 100755 --- a/home/templates/pixelegg/app.css +++ b/home/templates/pixelegg/app.css @@ -1,3 +1,4 @@ +@import "../default/app.css"; /** * EGroupware: CSS with less preprocessor * @@ -9,7 +10,3 @@ * @package home * @version $Id$ */ -#divMain img { - width: 16px; - height: 16px; -} diff --git a/home/templates/pixelegg/app.less b/home/templates/pixelegg/app.less index 95aa21cfbb..2d737676b7 100755 --- a/home/templates/pixelegg/app.less +++ b/home/templates/pixelegg/app.less @@ -13,12 +13,4 @@ @import (reference) "../../../pixelegg/less/def_buttons.less"; @import (reference) "../../../pixelegg/less/def_design_pattern_color_font_shadow.less"; - -// Dialog: Kalender Ressourcen - -#divMain { - - img {width: 16px; height: 16px;} - -} -// \ No newline at end of file +@import (reference) "../default/app.css"; \ No newline at end of file diff --git a/phpgwapi/inc/class.egw_framework.inc.php b/phpgwapi/inc/class.egw_framework.inc.php index e812ed3a67..0e982842d4 100644 --- a/phpgwapi/inc/class.egw_framework.inc.php +++ b/phpgwapi/inc/class.egw_framework.inc.php @@ -1165,8 +1165,6 @@ abstract class egw_framework $apps = array(); foreach($GLOBALS['egw_info']['user']['apps'] as $app => $data) { -// disable home until we get it working again -if ($app == 'home') continue; if (is_long($app)) { continue; diff --git a/phpgwapi/js/jquery/gridster/jquery.gridster.js b/phpgwapi/js/jquery/gridster/jquery.gridster.js index fb235dc064..b9ebb277bc 100644 --- a/phpgwapi/js/jquery/gridster/jquery.gridster.js +++ b/phpgwapi/js/jquery/gridster/jquery.gridster.js @@ -1,12 +1,16 @@ -/* - * jquery.coords - * https://github.com/ducksboard/gridster.js - * - * Copyright (c) 2012 ducksboard - * Licensed under the MIT licenses. - */ +/*! gridster.js - v0.5.6 - 2014-09-25 +* http://gridster.net/ +* Copyright (c) 2014 ducksboard; Licensed MIT */ -;(function($, window, document, undefined){ +;(function(root, factory) { + + if (typeof define === 'function' && define.amd) { + define('gridster-coords', ['jquery'], factory); + } else { + root.GridsterCoords = factory(root.$ || root.jQuery); + } + +}(this, function($) { /** * Creates objects with coordinates (x1, y1, x2, y2, cx, cy, width, height) * to simulate DOM elements on the screen. @@ -59,6 +63,9 @@ var d = this.data; + typeof d.left === 'undefined' && (d.left = d.x1); + typeof d.top === 'undefined' && (d.top = d.y1); + this.coords.x1 = d.left; this.coords.y1 = d.top; this.coords.x2 = d.left + d.width; @@ -93,6 +100,10 @@ return this.coords; }; + fn.destroy = function() { + this.el.removeData('coords'); + delete this.el; + }; //jQuery adapter $.fn.coords = function() { @@ -105,20 +116,24 @@ return ins; }; -}(jQuery, window, document)); + return Coords; -/* - * jquery.collision - * https://github.com/ducksboard/gridster.js - * - * Copyright (c) 2012 ducksboard - * Licensed under the MIT licenses. - */ +})); -;(function($, window, document, undefined){ +;(function(root, factory) { + + if (typeof define === 'function' && define.amd) { + define('gridster-collision', ['jquery', 'gridster-coords'], factory); + } else { + root.GridsterCollision = factory(root.$ || root.jQuery, + root.GridsterCoords); + } + +}(this, function($, Coords) { var defaults = { - colliders_context: document.body + colliders_context: document.body, + overlapping_region: 'C' // ,on_overlap: function(collider_data){}, // on_overlap_start : function(collider_data){}, // on_overlap_stop : function(collider_data){} @@ -136,6 +151,9 @@ * of HTMLElements or an Array of Coords instances. * @param {Object} [options] An Object with all options you want to * overwrite: + * @param {String} [options.overlapping_region] Determines when collision + * is valid, depending on the overlapped area. Values can be: 'N', 'S', + * 'W', 'E', 'C' or 'all'. Default is 'C'. * @param {Function} [options.on_overlap_start] Executes a function the first * time each `collider ` is overlapped. * @param {Function} [options.on_overlap_stop] Executes a function when a @@ -150,16 +168,12 @@ this.$element = el; this.last_colliders = []; this.last_colliders_coords = []; - if (typeof colliders === 'string' || colliders instanceof jQuery) { - this.$colliders = $(colliders, - this.options.colliders_context).not(this.$element); - }else{ - this.colliders = $(colliders); - } + this.set_colliders(colliders); this.init(); } + Collision.defaults = defaults; var fn = Collision.prototype; @@ -240,6 +254,7 @@ fn.find_collisions = function(player_data_coords){ var self = this; + var overlapping_region = this.options.overlapping_region; var colliders_coords = []; var colliders_data = []; var $colliders = (this.colliders || this.$colliders); @@ -263,7 +278,8 @@ player_coords, collider_coords); //todo: make this an option - if (region === 'C'){ + if (region === overlapping_region || overlapping_region === 'all') { + var area_coords = self.calculate_overlapped_area_coords( player_coords, collider_coords); var area = self.calculate_overlapped_area(area_coords); @@ -320,16 +336,59 @@ }; + fn.set_colliders = function(colliders) { + if (typeof colliders === 'string' || colliders instanceof $) { + this.$colliders = $(colliders, + this.options.colliders_context).not(this.$element); + }else{ + this.colliders = $(colliders); + } + }; + + //jQuery adapter $.fn.collision = function(collider, options) { return new Collision( this, collider, options ); }; + return Collision; -}(jQuery, window, document)); +})); ;(function(window, undefined) { - /* Debounce and throttle functions taken from underscore.js */ + + /* Delay, debounce and throttle functions taken from underscore.js + * + * Copyright (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and + * Investigative Reporters & Editors + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + + window.delay = function(func, wait) { + var args = Array.prototype.slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + window.debounce = function(func, wait, immediate) { var timeout; return function() { @@ -344,7 +403,6 @@ }; }; - window.throttle = function(func, wait) { var context, args, timeout, throttling, more, result; var whenDone = debounce( @@ -370,37 +428,46 @@ })(window); -/* - * jquery.draggable - * https://github.com/ducksboard/gridster.js - * - * Copyright (c) 2012 ducksboard - * Licensed under the MIT licenses. - */ +;(function(root, factory) { -;(function($, window, document, undefined){ + if (typeof define === 'function' && define.amd) { + define('gridster-draggable', ['jquery'], factory); + } else { + root.GridsterDraggable = factory(root.$ || root.jQuery); + } + +}(this, function($) { var defaults = { - items: '.gs_w', + items: 'li', distance: 1, limit: true, offset_left: 0, autoscroll: true, - ignore_dragging: ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'], - handle: null - // drag: function(e){}, - // start : function(e, ui){}, - // stop : function(e){} + ignore_dragging: ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'], // or function + handle: null, + container_width: 0, // 0 == auto + move_element: true, + helper: false, // or 'clone' + remove_helper: true + // drag: function(e) {}, + // start : function(e, ui) {}, + // stop : function(e) {} }; var $window = $(window); + var dir_map = { x : 'left', y : 'top' }; var isTouch = !!('ontouchstart' in window); - var pointer_events = { - start: isTouch ? 'touchstart' : 'mousedown.draggable', - move: isTouch ? 'touchmove' : 'mousemove.draggable', - end: isTouch ? 'touchend' : 'mouseup.draggable' + + var capitalize = function(str) { + return str.charAt(0).toUpperCase() + str.slice(1); }; + var idCounter = 0; + var uniqId = function() { + return ++idCounter + ''; + } + /** * Basic drag implementation for DOM elements inside a container. * Provide start/stop/drag callbacks. @@ -417,6 +484,9 @@ * the mouse must move before dragging should start. * @param {Boolean} [options.limit] Constrains dragging to the width of * the container + * @param {Object|Function} [options.ignore_dragging] Array of node names + * that sould not trigger dragging, by default is `['INPUT', 'TEXTAREA', + * 'SELECT', 'BUTTON']`. If a function is used return true to ignore dragging. * @param {offset_left} [options.offset_left] Offset added to the item * that is being dragged. * @param {Number} [options.drag] Executes a callback when the mouse is @@ -429,42 +499,56 @@ */ function Draggable(el, options) { this.options = $.extend({}, defaults, options); - this.$body = $(document.body); + this.$document = $(document); this.$container = $(el); this.$dragitems = $(this.options.items, this.$container); this.is_dragging = false; this.player_min_left = 0 + this.options.offset_left; + this.id = uniqId(); + this.ns = '.gridster-draggable-' + this.id; this.init(); } + Draggable.defaults = defaults; + var fn = Draggable.prototype; fn.init = function() { - this.calculate_positions(); - this.$container.css('position', 'relative'); + var pos = this.$container.css('position'); + this.calculate_dimensions(); + this.$container.css('position', pos === 'static' ? 'relative' : pos); this.disabled = false; this.events(); - this.on_window_resize = throttle($.proxy(this.calculate_positions, this), 200); - $(window).bind('resize', this.on_window_resize); + $(window).bind(this.nsEvent('resize'), + throttle($.proxy(this.calculate_dimensions, this), 200)); + }; + + fn.nsEvent = function(ev) { + return (ev || '') + this.ns; }; fn.events = function() { - this.proxied_on_select_start = $.proxy(this.on_select_start, this); - this.$container.on('selectstart', this.proxied_on_select_start); + this.pointer_events = { + start: this.nsEvent('touchstart') + ' ' + this.nsEvent('mousedown'), + move: this.nsEvent('touchmove') + ' ' + this.nsEvent('mousemove'), + end: this.nsEvent('touchend') + ' ' + this.nsEvent('mouseup'), + }; - this.proxied_drag_handler = $.proxy(this.drag_handler, this); - this.$container.on(pointer_events.start, this.options.items, this.proxied_drag_handler); + this.$container.on(this.nsEvent('selectstart'), + $.proxy(this.on_select_start, this)); - this.proxied_pointer_events_end = $.proxy(function(e) { + this.$container.on(this.pointer_events.start, this.options.items, + $.proxy(this.drag_handler, this)); + + this.$document.on(this.pointer_events.end, $.proxy(function(e) { this.is_dragging = false; if (this.disabled) { return; } - this.$body.off(pointer_events.move); + this.$document.off(this.pointer_events.move); if (this.drag_start) { this.on_dragstop(e); } - }, this); - this.$body.on(pointer_events.end, this.proxied_pointer_events_end); + }, this)); }; fn.get_actual_pos = function($el) { @@ -474,7 +558,7 @@ fn.get_mouse_pos = function(e) { - if (isTouch) { + if (e.originalEvent && e.originalEvent.touches) { var oe = e.originalEvent; e = oe.touches.length ? oe.touches[0] : oe.changedTouches[0]; } @@ -493,68 +577,112 @@ mouse_actual_pos.left - this.mouse_init_pos.left); var diff_y = Math.round(mouse_actual_pos.top - this.mouse_init_pos.top); - var left = Math.round(this.el_init_offset.left + diff_x - this.baseX); - var top = Math.round( - this.el_init_offset.top + diff_y - this.baseY + this.scrollOffset); + var left = Math.round(this.el_init_offset.left + + diff_x - this.baseX + $(window).scrollLeft() - this.win_offset_x); + var top = Math.round(this.el_init_offset.top + + diff_y - this.baseY + $(window).scrollTop() - this.win_offset_y); if (this.options.limit) { if (left > this.player_max_left) { left = this.player_max_left; - }else if(left < this.player_min_left) { + } else if(left < this.player_min_left) { left = this.player_min_left; } } return { - left: left, - top: top, - mouse_left: mouse_actual_pos.left, - mouse_top: mouse_actual_pos.top + position: { + left: left, + top: top + }, + pointer: { + left: mouse_actual_pos.left, + top: mouse_actual_pos.top, + diff_left: diff_x + ($(window).scrollLeft() - this.win_offset_x), + diff_top: diff_y + ($(window).scrollTop() - this.win_offset_y) + } }; }; - fn.manage_scroll = function(offset) { - /* scroll document */ - var nextScrollTop; - var scrollTop = $window.scrollTop(); - var min_window_y = scrollTop; - var max_window_y = min_window_y + this.window_height; + fn.get_drag_data = function(e) { + var offset = this.get_offset(e); + offset.$player = this.$player; + offset.$helper = this.helper ? this.$helper : this.$player; - var mouse_down_zone = max_window_y - 50; - var mouse_up_zone = min_window_y + 50; - - var abs_mouse_left = offset.mouse_left; - var abs_mouse_top = min_window_y + offset.mouse_top; - - var max_player_y = (this.doc_height - this.window_height + - this.player_height); - - if (abs_mouse_top >= mouse_down_zone) { - nextScrollTop = scrollTop + 30; - if (nextScrollTop < max_player_y) { - $window.scrollTop(nextScrollTop); - this.scrollOffset = this.scrollOffset + 30; - } - } - - if (abs_mouse_top <= mouse_up_zone) { - nextScrollTop = scrollTop - 30; - if (nextScrollTop > 0) { - $window.scrollTop(nextScrollTop); - this.scrollOffset = this.scrollOffset - 30; - } - } + return offset; }; - fn.calculate_positions = function(e) { + fn.set_limits = function(container_width) { + container_width || (container_width = this.$container.width()); + this.player_max_left = (container_width - this.player_width + + - this.options.offset_left); + + this.options.container_width = container_width; + + return this; + }; + + + fn.scroll_in = function(axis, data) { + var dir_prop = dir_map[axis]; + + var area_size = 50; + var scroll_inc = 30; + + var is_x = axis === 'x'; + var window_size = is_x ? this.window_width : this.window_height; + var doc_size = is_x ? $(document).width() : $(document).height(); + var player_size = is_x ? this.$player.width() : this.$player.height(); + + var next_scroll; + var scroll_offset = $window['scroll' + capitalize(dir_prop)](); + var min_window_pos = scroll_offset; + var max_window_pos = min_window_pos + window_size; + + var mouse_next_zone = max_window_pos - area_size; // down/right + var mouse_prev_zone = min_window_pos + area_size; // up/left + + var abs_mouse_pos = min_window_pos + data.pointer[dir_prop]; + + var max_player_pos = (doc_size - window_size + player_size); + + if (abs_mouse_pos >= mouse_next_zone) { + next_scroll = scroll_offset + scroll_inc; + if (next_scroll < max_player_pos) { + $window['scroll' + capitalize(dir_prop)](next_scroll); + this['scroll_offset_' + axis] += scroll_inc; + } + } + + if (abs_mouse_pos <= mouse_prev_zone) { + next_scroll = scroll_offset - scroll_inc; + if (next_scroll > 0) { + $window['scroll' + capitalize(dir_prop)](next_scroll); + this['scroll_offset_' + axis] -= scroll_inc; + } + } + + return this; + }; + + + fn.manage_scroll = function(data) { + this.scroll_in('x', data); + this.scroll_in('y', data); + }; + + + fn.calculate_dimensions = function(e) { this.window_height = $window.height(); + this.window_width = $window.width(); }; fn.drag_handler = function(e) { var node = e.target.nodeName; + // skip if drag is disabled, or click was not done with the mouse primary button if (this.disabled || e.which !== 1 && !isTouch) { return; } @@ -571,7 +699,7 @@ this.mouse_init_pos = this.get_mouse_pos(e); this.offsetY = this.mouse_init_pos.top - this.el_init_pos.top; - this.on_pointer_events_move = function(mme){ + this.$document.on(this.pointer_events.move, function(mme) { var mouse_actual_pos = self.get_mouse_pos(mme); var diff_x = Math.abs( mouse_actual_pos.left - self.mouse_init_pos.left); @@ -594,87 +722,82 @@ } return false; - }; + }); - this.$body.on(pointer_events.move, this.on_pointer_events_move); - - return false; + if (!isTouch) { return false; } }; fn.on_dragstart = function(e) { e.preventDefault(); - this.drag_start = true; - this.is_dragging = true; + + if (this.is_dragging) { return this; } + + this.drag_start = this.is_dragging = true; var offset = this.$container.offset(); this.baseX = Math.round(offset.left); this.baseY = Math.round(offset.top); - this.doc_height = $(document).height(); + this.initial_container_width = this.options.container_width || this.$container.width(); if (this.options.helper === 'clone') { this.$helper = this.$player.clone() .appendTo(this.$container).addClass('helper'); this.helper = true; - }else{ + } else { this.helper = false; } - this.scrollOffset = 0; + + this.win_offset_y = $(window).scrollTop(); + this.win_offset_x = $(window).scrollLeft(); + this.scroll_offset_y = 0; + this.scroll_offset_x = 0; this.el_init_offset = this.$player.offset(); this.player_width = this.$player.width(); this.player_height = this.$player.height(); - this.player_max_left = (this.$container.width() - this.player_width + - this.options.offset_left); + + this.set_limits(this.options.container_width); if (this.options.start) { - this.options.start.call(this.$player, e, { - helper: this.helper ? this.$helper : this.$player - }); + this.options.start.call(this.$player, e, this.get_drag_data(e)); } return false; }; fn.on_dragmove = function(e) { - var offset = this.get_offset(e); + var data = this.get_drag_data(e); - this.options.autoscroll && this.manage_scroll(offset); + this.options.autoscroll && this.manage_scroll(data); - (this.helper ? this.$helper : this.$player).css({ - 'position': 'absolute', - 'left' : offset.left, - 'top' : offset.top - }); + if (this.options.move_element) { + (this.helper ? this.$helper : this.$player).css({ + 'position': 'absolute', + 'left' : data.position.left, + 'top' : data.position.top + }); + } - var ui = { - 'position': { - 'left': offset.left, - 'top': offset.top - } - }; + var last_position = this.last_position || data.position; + data.prev_position = last_position; if (this.options.drag) { - this.options.drag.call(this.$player, e, ui); + this.options.drag.call(this.$player, e, data); } + + this.last_position = data.position; return false; }; fn.on_dragstop = function(e) { - var offset = this.get_offset(e); + var data = this.get_drag_data(e); this.drag_start = false; - var ui = { - 'position': { - 'left': offset.left, - 'top': offset.top - } - }; - if (this.options.stop) { - this.options.stop.call(this.$player, e, ui); + this.options.stop.call(this.$player, e, data); } - if (this.helper) { + if (this.helper && this.options.remove_helper) { this.$helper.remove(); } @@ -699,15 +822,12 @@ this.disabled = true; }; - - fn.destroy = function(){ + fn.destroy = function() { this.disable(); - this.$container.off('selectstart', this.proxied_on_select_start); - this.$container.off(pointer_events.start, this.proxied_drag_handler); - this.$body.off(pointer_events.end, this.proxied_pointer_events_end); - this.$body.off(pointer_events.move, this.on_pointer_events_move); - $(window).unbind('resize', this.on_window_resize); + this.$container.off(this.ns); + this.$document.off(this.ns); + $(window).off(this.ns); $.removeData(this.$container, 'drag'); }; @@ -717,47 +837,48 @@ return !$(event.target).is(this.options.handle); } - return $.inArray(event.target.nodeName, this.options.ignore_dragging) >= 0; + if ($.isFunction(this.options.ignore_dragging)) { + return this.options.ignore_dragging(event); + } + + return $(event.target).is(this.options.ignore_dragging.join(', ')); }; //jQuery adapter - $.fn.dragg = function ( options ) { - return this.each(function () { - if (!$.data(this, 'drag')) { - $.data(this, 'drag', new Draggable( this, options )); - } - }); + $.fn.drag = function ( options ) { + return new Draggable(this, options); }; + return Draggable; -}(jQuery, window, document)); +})); -/* - * jquery.gridster - * https://github.com/ducksboard/gridster.js - * - * Copyright (c) 2012 ducksboard - * Licensed under the MIT licenses. - */ -;(function($, window, document, undefined) { +;(function(root, factory) { + + if (typeof define === 'function' && define.amd) { + define(['jquery', 'gridster-draggable', 'gridster-collision'], factory); + } else { + root.Gridster = factory(root.$ || root.jQuery, root.GridsterDraggable, + root.GridsterCollision); + } + + }(this, function($, Draggable, Collision) { - //ToDo Max_cols and Max_size_x conflict.. need to unify var defaults = { namespace: '', widget_selector: 'li', - static_class: 'static', widget_margins: [10, 10], widget_base_dimensions: [400, 225], extra_rows: 0, extra_cols: 0, min_cols: 1, - max_cols: 60, + max_cols: Infinity, min_rows: 15, - max_rows: 15, - max_size_x: 6, + max_size_x: false, + autogrow_cols: false, autogenerate_stylesheet: true, avoid_overlapped_widgets: true, - shift_larger_widgets_down: true, + auto_init: true, serialize_params: function($w, wgd) { return { col: wgd.col, @@ -768,12 +889,20 @@ }, collision: {}, draggable: { + items: '.gs-w', distance: 4, - items: ".gs_w:not(.static)" + ignore_dragging: Draggable.defaults.ignore_dragging.slice(0) + }, + resize: { + enabled: false, + axes: ['both'], + handle_append_to: '', + handle_class: 'gs-resize-handle', + max_size: [Infinity, Infinity], + min_size: [1, 1] } }; - /** * @class Gridster * @uses Draggable @@ -795,6 +924,8 @@ * @param {Number} [options.extra_rows] Add more rows in addition to * those that have been calculated. * @param {Number} [options.min_cols] The minimum required columns. + * @param {Number} [options.max_cols] The maximum columns possible (set to null + * for no maximum). * @param {Number} [options.min_rows] The minimum required rows. * @param {Number} [options.max_size_x] The maximum number of columns * that a widget can span. @@ -807,6 +938,8 @@ * @param {Boolean} [options.avoid_overlapped_widgets] Avoid that widgets loaded * from the DOM can be overlapped. It is helpful if the positions were * bad stored in the database or if there was any conflict. + * @param {Boolean} [options.auto_init] Automatically call gridster init + * method or not when the plugin is instantiated. * @param {Function} [options.serialize_params] Return the data you want * for each widget in the serialization. Two arguments are passed: * `$w`: the jQuery wrapped HTMLElement, and `wgd`: the grid @@ -817,38 +950,158 @@ * @param {Object} [options.draggable] An Object with all options for * Draggable class you want to overwrite. See Draggable docs for more * info. + * @param {Object|Function} [options.draggable.ignore_dragging] Note that + * if you use a Function, and resize is enabled, you should ignore the + * resize handlers manually (options.resize.handle_class). + * @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]` + * @param {Array} [options.resize.min_size] Limit widget dimensions + * when resizing. Array values should be integers: + * `[min_cols_occupied, min_rows_occupied]` + * @param {Function} [options.resize.start] Function executed + * when resizing starts. + * @param {Function} [otions.resize.resize] Function executed + * during the resizing. + * @param {Function} [options.resize.stop] Function executed + * when resizing stops. * * @constructor */ function Gridster(el, options) { - 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.$changed = $([]); - this.w_queue = {}; - this.wrapper_width = this.$wrapper.width(); - this.min_widget_width = (this.options.widget_margins[0] * 2) + - this.options.widget_base_dimensions[0]; - this.min_widget_height = (this.options.widget_margins[1] * 2) + - this.options.widget_base_dimensions[1]; - this.init(); + 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.$changed = $([]); + this.wrapper_width = this.$wrapper.width(); + this.min_widget_width = (this.options.widget_margins[0] * 2) + + this.options.widget_base_dimensions[0]; + this.min_widget_height = (this.options.widget_margins[1] * 2) + + this.options.widget_base_dimensions[1]; + + this.generated_stylesheets = []; + this.$style_tags = $([]); + + this.options.auto_init && this.init(); } + Gridster.defaults = defaults; Gridster.generated_stylesheets = []; + + /** + * Sorts an Array of grid coords objects (representing the grid coords of + * each widget) in ascending way. + * + * @method sort_by_row_asc + * @param {Array} widgets Array of grid coords objects + * @return {Array} Returns the array sorted. + */ + Gridster.sort_by_row_asc = function(widgets) { + widgets = widgets.sort(function(a, b) { + if (!a.row) { + a = $(a).coords().grid; + b = $(b).coords().grid; + } + + if (a.row > b.row) { + return 1; + } + return -1; + }); + + return widgets; + }; + + + /** + * Sorts an Array of grid coords objects (representing the grid coords of + * each widget) placing first the empty cells upper left. + * + * @method sort_by_row_and_col_asc + * @param {Array} widgets Array of grid coords objects + * @return {Array} Returns the array sorted. + */ + Gridster.sort_by_row_and_col_asc = function(widgets) { + widgets = widgets.sort(function(a, b) { + if (a.row > b.row || a.row === b.row && a.col > b.col) { + return 1; + } + return -1; + }); + + return widgets; + }; + + + /** + * Sorts an Array of grid coords objects by column (representing the grid + * coords of each widget) in ascending way. + * + * @method sort_by_col_asc + * @param {Array} widgets Array of grid coords objects + * @return {Array} Returns the array sorted. + */ + Gridster.sort_by_col_asc = function(widgets) { + widgets = widgets.sort(function(a, b) { + if (a.col > b.col) { + return 1; + } + return -1; + }); + + return widgets; + }; + + + /** + * Sorts an Array of grid coords objects (representing the grid coords of + * each widget) in descending way. + * + * @method sort_by_row_desc + * @param {Array} widgets Array of grid coords objects + * @return {Array} Returns the array sorted. + */ + Gridster.sort_by_row_desc = function(widgets) { + widgets = widgets.sort(function(a, b) { + if (a.row + a.size_y < b.row + b.size_y) { + return 1; + } + return -1; + }); + return widgets; + }; + + + + /** Instance Methods **/ + 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.set_dom_grid_width(); this.$wrapper.addClass('ready'); this.draggable(); + this.options.resize.enabled && this.resizable(); - $(window).bind( - 'resize', throttle($.proxy(this.recalculate_faux_grid, this), 200)); + $(window).bind('resize.gridster', throttle( + $.proxy(this.recalculate_faux_grid, this), 200)); }; @@ -877,6 +1130,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. * @@ -887,20 +1167,24 @@ * @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. + * @param {Array} [min_size] min_size Minimum 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, min_size) { var pos; size_x || (size_x = 1); size_y || (size_y = 1); if (!col & !row) { pos = this.next_position(size_x, size_y); - }else{ + } else { pos = { col: col, - row: row + row: row, + size_x: size_x, + size_y: size_y }; this.empty_cells(col, row, size_x, size_y); @@ -911,54 +1195,129 @@ 'data-row': pos.row, 'data-sizex' : size_x, 'data-sizey' : size_y - }).addClass('gs_w').appendTo(this.$el).hide(); + }).addClass('gs-w').appendTo(this.$el).hide(); this.$widgets = this.$widgets.add($w); - this.$changed = this.$changed.add($w); this.register_widget($w); 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); + } + + if (min_size) { + this.set_widget_min_size($w, min_size); + } + + this.set_dom_grid_width(); this.set_dom_grid_height(); + this.drag_api.set_limits(this.cols * this.min_widget_width); + return $w.fadeIn(); }; + /** + * Change widget size limits. + * + * @method set_widget_min_size + * @param {HTMLElement|Number} $widget The jQuery wrapped HTMLElement + * representing the widget or an index representing the desired widget. + * @param {Array} min_size Minimum size (in units) for width and height. + * @return {HTMLElement} Returns instance of gridster Class. + */ + fn.set_widget_min_size = function($widget, min_size) { + $widget = typeof $widget === 'number' ? + this.$widgets.eq($widget) : $widget; + + if (!$widget.length) { return this; } + + var wgd = $widget.data('coords').grid; + wgd.min_size_x = min_size[0]; + wgd.min_size_y = min_size[1]; + + return this; + }; + /** - * Change the size of a widget. + * 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 * @param {HTMLElement} $widget The jQuery wrapped HTMLElement * representing the widget. * @param {Number} size_x The number of columns that will occupy the widget. + * 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 {Number} size_y The number of rows that will occupy the widget. + * @param {Function} [callback] Function executed when the widget is removed. * @return {HTMLElement} Returns $widget. */ - fn.resize_widget = function($widget, size_x, size_y) { + fn.resize_widget = function($widget, size_x, size_y, callback) { var wgd = $widget.coords().grid; - size_x || (size_x = wgd.size_x); - size_y || (size_y = wgd.size_y); - - if (size_x > this.cols) { - size_x = this.cols; - } - - var old_cells_occupied = this.get_cells_occupied(wgd); - var old_size_x = wgd.size_x; + var col = wgd.col; + var max_cols = this.options.max_cols; var old_size_y = wgd.size_y; var old_col = wgd.col; var new_col = old_col; - var wider = size_x > old_size_x; - var taller = size_y > old_size_y; - if (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); + size_x || (size_x = wgd.size_x); + size_y || (size_y = wgd.size_y); + + if (max_cols !== Infinity) { + size_x = Math.min(size_x, max_cols - col + 1); + } + + if (size_y > old_size_y) { + this.add_faux_rows(Math.max(size_y - old_size_y, 0)); + } + + var player_rcol = (col + size_x - 1); + if (player_rcol > this.cols) { + this.add_faux_cols(player_rcol - this.cols); } var new_grid_data = { @@ -968,7 +1327,35 @@ size_y: size_y }; - var new_cells_occupied = this.get_cells_occupied(new_grid_data); + this.mutate_widget_in_gridmap($widget, wgd, new_grid_data); + + this.set_dom_grid_height(); + this.set_dom_grid_width(); + + if (callback) { + callback.call(this, new_grid_data.size_x, new_grid_data.size_y); + } + + return $widget; + }; + + + /** + * 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; + + var old_cells_occupied = this.get_cells_occupied(wgd); + var new_cells_occupied = this.get_cells_occupied(new_wgd); var empty_cols = []; $.each(old_cells_occupied.cols, function(i, col) { @@ -1002,48 +1389,46 @@ if (occupied_cols.length) { var cols_to_empty = [ - new_col, wgd.row, size_x, Math.min(old_size_y, size_y), $widget + new_wgd.col, new_wgd.row, new_wgd.size_x, Math.min(old_size_y, new_wgd.size_y), $widget ]; this.empty_cells.apply(this, cols_to_empty); } if (occupied_rows.length) { - var rows_to_empty = [new_col, wgd.row, size_x, size_y, $widget]; + var rows_to_empty = [new_wgd.col, new_wgd.row, new_wgd.size_x, new_wgd.size_y, $widget]; this.empty_cells.apply(this, rows_to_empty); } - wgd.col = new_col; - wgd.size_x = size_x; - wgd.size_y = size_y; - this.add_to_gridmap(new_grid_data, $widget); + // not the same that wgd = new_wgd; + wgd.col = new_wgd.col; + wgd.row = new_wgd.row; + wgd.size_x = new_wgd.size_x; + wgd.size_y = new_wgd.size_y; + + this.add_to_gridmap(new_wgd, $widget); + + $widget.removeClass('player-revert'); //update coords instance attributes $widget.data('coords').update({ - width: (size_x * this.options.widget_base_dimensions[0] + - ((size_x - 1) * this.options.widget_margins[0]) * 2), - height: (size_y * this.options.widget_base_dimensions[1] + - ((size_y - 1) * this.options.widget_margins[1]) * 2) + width: (new_wgd.size_x * this.options.widget_base_dimensions[0] + + ((new_wgd.size_x - 1) * this.options.widget_margins[0]) * 2), + height: (new_wgd.size_y * this.options.widget_base_dimensions[1] + + ((new_wgd.size_y - 1) * this.options.widget_margins[1]) * 2) }); - if (size_y > old_size_y) { - this.add_faux_rows(size_y - old_size_y); - } - - if (size_x > old_size_x) { - this.add_faux_cols(size_x - old_size_x); - } - $widget.attr({ - 'data-col': new_col, - 'data-sizex': size_x, - 'data-sizey': size_y + 'data-col': new_wgd.col, + 'data-row': new_wgd.row, + 'data-sizex': new_wgd.size_x, + 'data-sizey': new_wgd.size_y }); if (empty_cols.length) { var cols_to_remove_holes = [ - empty_cols[0], wgd.row, + empty_cols[0], new_wgd.row, empty_cols.length, - Math.min(old_size_y, size_y), + Math.min(old_size_y, new_wgd.size_y), $widget ]; @@ -1052,14 +1437,17 @@ if (empty_rows.length) { var rows_to_remove_holes = [ - new_col, wgd.row, size_x, size_y, $widget + new_wgd.col, new_wgd.row, new_wgd.size_x, new_wgd.size_y, $widget ]; this.remove_empty_cells.apply(this, rows_to_remove_holes); } - return $widget; + this.move_widget_up($widget); + + return this; }; + /** * Move down widgets in cells represented by the arguments col, row, size_x, * size_y @@ -1084,7 +1472,7 @@ $nexts.not($exclude).each($.proxy(function(i, w) { var wgd = $(w).coords().grid; - if (!(wgd.row <= (row + size_y - 1))) { return; } + if ( !(wgd.row <= (row + size_y - 1))) { return; } var diff = (row + size_y) - wgd.row; this.move_widget_down($(w), diff); }, this)); @@ -1106,7 +1494,7 @@ * occupy. * @param {Number} size_y The number of rows that the group of cells * occupy. - * @param {HTMLElement} $exclude Exclude widgets from being moved. + * @param {HTMLElement} exclude Exclude widgets from being moved. * @return {Class} Returns the instance of the Gridster Class. */ fn.remove_empty_cells = function(col, row, size_x, size_y, exclude) { @@ -1117,12 +1505,9 @@ size_y: size_y }); - /* $nexts.not(exclude).each($.proxy(function(i, widget) { - console.log("from_remove") this.move_widget_up( $(widget), size_y ); }, this)); - */ this.set_dom_grid_height(); @@ -1167,18 +1552,11 @@ } if (valid_pos.length) { - return this.sort_by_row_and_col_asc(valid_pos)[0]; + return Gridster.sort_by_row_and_col_asc(valid_pos)[0]; } return false; }; - fn.remove_by_grid = function(col, row){ - var $w = this.is_widget(col, row); - if($w){ - this.remove_widget($w); - } - } - /** * Remove a widget from the grid. @@ -1191,7 +1569,7 @@ * @return {Class} Returns the instance of the Gridster Class. */ fn.remove_widget = function(el, silent, callback) { - var $el = el instanceof jQuery ? el : $(el); + var $el = el instanceof $ ? el : $(el); var wgd = $el.coords().grid; // if silent is a function assume it's a callback @@ -1222,6 +1600,8 @@ callback.call(this, el); } }, this)); + + return this; }; @@ -1253,17 +1633,14 @@ */ fn.serialize = function($widgets) { $widgets || ($widgets = this.$widgets); - var result = []; - $widgets.each($.proxy(function(i, widget) { - if(typeof($(widget).coords().grid) != "undefined"){ - result.push(this.options.serialize_params( - $(widget), $(widget).coords().grid ) ); - } - }, this)); - return result; + return $widgets.map($.proxy(function(i, widget) { + var $w = $(widget); + return this.options.serialize_params($w, $w.coords().grid); + }, this)).get(); }; + /** * Returns a serialized array of the widgets that have changed their * position. @@ -1278,49 +1655,74 @@ /** - * Creates the grid coords object representing the widget a add it to the + * Convert widgets from DOM elements to "widget grid data" Objects. + * + * @method dom_to_coords + * @param {HTMLElement} $widget The widget to be converted. + */ + fn.dom_to_coords = function($widget) { + return { + 'col': parseInt($widget.attr('data-col'), 10), + 'row': parseInt($widget.attr('data-row'), 10), + 'size_x': parseInt($widget.attr('data-sizex'), 10) || 1, + 'size_y': parseInt($widget.attr('data-sizey'), 10) || 1, + 'max_size_x': parseInt($widget.attr('data-max-sizex'), 10) || false, + 'max_size_y': parseInt($widget.attr('data-max-sizey'), 10) || false, + 'min_size_x': parseInt($widget.attr('data-min-sizex'), 10) || false, + 'min_size_y': parseInt($widget.attr('data-min-sizey'), 10) || false, + 'el': $widget + }; + }; + + + /** + * Creates the grid coords object representing the widget an add it to the * mapped array of positions. * * @method register_widget - * @return {Array} Returns the instance of the Gridster class. + * @param {HTMLElement|Object} $el jQuery wrapped HTMLElement representing + * the widget, or an "widget grid data" Object with (col, row, el ...). + * @return {Boolean} Returns true if the widget final position is different + * than the original. */ fn.register_widget = function($el) { + var isDOM = $el instanceof jQuery; + var wgd = isDOM ? this.dom_to_coords($el) : $el; + var posChanged = false; + isDOM || ($el = wgd.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), - 'el': $el - }; + var empty_upper_row = this.can_go_widget_up(wgd); + if (empty_upper_row) { + wgd.row = empty_upper_row; + $el.attr('data-row', empty_upper_row); + this.$el.trigger('gridster:positionchanged', [wgd]); + posChanged = true; + } if (this.options.avoid_overlapped_widgets && !this.can_move_to( {size_x: wgd.size_x, size_y: wgd.size_y}, wgd.col, wgd.row) ) { - /*if(!$el.hasClass('.disp_ad')){ - $el.remove(); - return false; - }*/ - 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, 'data-sizex': wgd.size_x, 'data-sizey': wgd.size_y }); + posChanged = true; } // 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); - return this; + this.options.resize.enabled && this.add_resize_handle($el); + + return posChanged; }; @@ -1370,13 +1772,13 @@ */ fn.add_to_gridmap = function(grid_data, value) { this.update_widget_position(grid_data, value || grid_data.el); - /*if (grid_data.el) { + + if (grid_data.el) { var $widgets = this.widgets_below(grid_data.el); $widgets.each($.proxy(function(i, widget) { - console.log("from_add_to_gridmap"); this.move_widget_up( $(widget)); }, this)); - } */ + } }; @@ -1391,13 +1793,16 @@ var self = this; var draggable_options = $.extend(true, {}, this.options.draggable, { offset_left: this.options.widget_margins[0], + offset_top: this.options.widget_margins[1], + container_width: this.cols * this.min_widget_width, + limit: true, start: function(event, ui) { self.$widgets.filter('.player-revert') .removeClass('player-revert'); self.$player = $(this); - self.$helper = self.options.draggable.helper === 'clone' ? - $(ui.helper) : self.$player; + self.$helper = $(ui.$helper); + self.helper = !self.$helper.is(self.$player); self.on_start_drag.call(self, event, ui); @@ -1413,7 +1818,59 @@ }, 60) }); - this.drag_api = this.$el.dragg(draggable_options).data('drag'); + this.drag_api = this.$el.drag(draggable_options); + return this; + }; + + + /** + * 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, + resize: true, + limit: this.options.autogrow_cols ? false : true, + 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(''); + + if ($.isArray(this.options.draggable.ignore_dragging)) { + this.options.draggable.ignore_dragging.push( + '.' + this.resize_handle_class); + } + return this; }; @@ -1422,20 +1879,29 @@ * This function is executed when the player begins to be dragged. * * @method on_start_drag - * @param {Event} The original browser event - * @param {Object} A prepared ui object. + * @param {Event} event The original browser event + * @param {Object} ui A prepared ui object with useful drag-related data */ fn.on_start_drag = function(event, ui) { - this.$helper.add(this.$player).add(this.$wrapper).addClass('dragging'); + this.highest_col = this.get_highest_occupied_cell().col; + this.$player.addClass('player'); this.player_grid_data = this.$player.coords().grid; this.placeholder_grid_data = $.extend({}, this.player_grid_data); - //set new grid height along the dragging period - this.$el.css('height', this.$el.height() + - (this.player_grid_data.size_y * this.min_widget_height)); + this.set_dom_grid_height(this.$el.height() + + (this.player_grid_data.size_y * this.min_widget_height)); + + this.set_dom_grid_width(this.cols); + + var pgd_sizex = this.player_grid_data.size_x; + var cols_diff = this.cols - this.highest_col; + + if (this.options.autogrow_cols && cols_diff <= pgd_sizex) { + this.add_faux_cols(Math.min(pgd_sizex - cols_diff, 1)); + } var colliders = this.faux_grid; var coords = this.$player.data('coords').coords; @@ -1448,12 +1914,11 @@ this.last_cols = []; this.last_rows = []; - // see jquery.collision.js this.collision_api = this.$helper.collision( colliders, this.options.collision); - this.$preview_holder = $('
  • ', { + this.$preview_holder = $('<' + this.$player.get(0).tagName + ' />', { 'class': 'preview-holder', 'data-row': this.$player.attr('data-row'), 'data-col': this.$player.attr('data-col'), @@ -1473,8 +1938,8 @@ * This function is executed when the player is being dragged. * * @method on_drag - * @param {Event} The original browser event - * @param {Object} A prepared ui object. + * @param {Event} event The original browser event + * @param {Object} ui A prepared ui object with useful drag-related data */ fn.on_drag = function(event, ui) { //break if dragstop has been fired @@ -1487,18 +1952,30 @@ top: ui.position.top + this.baseY }; + // auto grow cols + if (this.options.autogrow_cols) { + var prcol = this.placeholder_grid_data.col + + this.placeholder_grid_data.size_x - 1; + + // "- 1" due to adding at least 1 column in on_start_drag + if (prcol >= this.cols - 1 && this.options.max_cols >= this.cols + 1) { + this.add_faux_cols(1); + this.set_dom_grid_width(this.cols + 1); + this.drag_api.set_limits(this.container_width); + } + + this.collision_api.set_colliders(this.faux_grid); + } + this.colliders_data = this.collision_api.get_closest_colliders( abs_offset); this.on_overlapped_column_change( - this.on_start_overlapping_column, - this.on_stop_overlapping_column - ); + this.on_start_overlapping_column, this.on_stop_overlapping_column); this.on_overlapped_row_change( - this.on_start_overlapping_row, - this.on_stop_overlapping_row - ); + this.on_start_overlapping_row, this.on_stop_overlapping_row); + if (this.helper && this.$player) { this.$player.css({ @@ -1512,12 +1989,13 @@ } }; + /** * This function is executed when the player stops being dragged. * * @method on_stop_drag - * @param {Event} The original browser event - * @param {Object} A prepared ui object. + * @param {Event} event The original browser event + * @param {Object} ui A prepared ui object with useful drag-related data */ fn.on_stop_drag = function(event, ui) { this.$helper.add(this.$player).add(this.$wrapper) @@ -1525,7 +2003,8 @@ ui.position.left = ui.position.left + this.baseX; ui.position.top = ui.position.top + this.baseY; - this.colliders_data = this.collision_api.get_closest_colliders(ui.position); + this.colliders_data = this.collision_api.get_closest_colliders( + ui.position); this.on_overlapped_column_change( this.on_start_overlapping_column, @@ -1568,9 +2047,211 @@ this.player_grid_data = {}; this.cells_occupied_by_placeholder = {}; this.cells_occupied_by_player = {}; - this.w_queue = {}; this.set_dom_grid_height(); + this.set_dom_grid_width(); + + if (this.options.autogrow_cols) { + this.drag_api.set_limits(this.cols * this.min_widget_width); + } + }; + + + /** + * 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_initial_col = this.resize_coords.grid.col; + 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.options.max_cols - this.resize_initial_col + 1); + this.resize_max_size_y = this.resize_wgd.max_size_y || + this.options.resize.max_size[1]; + + this.resize_min_size_x = (this.resize_wgd.min_size_x || + this.options.resize.min_size[0] || 1); + this.resize_min_size_y = (this.resize_wgd.min_size_y || + this.options.resize.min_size[1] || 1); + + this.resize_initial_last_col = this.get_highest_occupied_cell().col; + + this.set_dom_grid_width(this.cols); + + 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'); + + if (this.options.resize.start) { + this.options.resize.start.call(this, event, ui, this.$resized_widget); + } + + this.$el.trigger('gridster:resizestart'); + }; + + + /** + * 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': '' + }); + + if (this.options.resize.stop) { + this.options.resize.stop.call(this, event, ui, this.$resized_widget); + } + + this.$el.trigger('gridster:resizestop'); + }, this), 300); + + this.set_dom_grid_width(); + + if (this.options.autogrow_cols) { + this.drag_api.set_limits(this.cols * this.min_widget_width); + } + }; + + + /** + * 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 margin_x = this.options.widget_margins[0]; + var margin_y = this.options.widget_margins[1]; + var max_size_x = this.resize_max_size_x; + var min_size_x = this.resize_min_size_x; + var max_size_y = this.resize_max_size_y; + var min_size_y = this.resize_min_size_y; + var autogrow = this.options.autogrow_cols; + var width; + var max_width = Infinity; + var max_height = Infinity; + + var inc_units_x = Math.ceil((rel_x / (wbd_x + margin_x * 2)) - 0.2); + var inc_units_y = Math.ceil((rel_y / (wbd_y + margin_y * 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); + + var max_cols = (this.container_width / this.min_widget_width) - + this.resize_initial_col + 1; + var limit_width = ((max_cols * this.min_widget_width) - margin_x * 2); + + size_x = Math.max(Math.min(size_x, max_size_x), min_size_x); + size_x = Math.min(max_cols, size_x); + width = (max_size_x * wbd_x) + ((size_x - 1) * margin_x * 2); + max_width = Math.min(width, limit_width); + min_width = (min_size_x * wbd_x) + ((size_x - 1) * margin_x * 2); + + size_y = Math.max(Math.min(size_y, max_size_y), min_size_y); + max_height = (max_size_y * wbd_y) + ((size_y - 1) * margin_y * 2); + min_height = (min_size_y * wbd_y) + ((size_y - 1) * margin_y * 2); + + if (this.resize_dir.right) { + size_y = this.resize_initial_sizey; + } else if (this.resize_dir.bottom) { + size_x = this.resize_initial_sizex; + } + + if (autogrow) { + var last_widget_col = this.resize_initial_col + size_x - 1; + if (autogrow && this.resize_initial_last_col <= last_widget_col) { + this.set_dom_grid_width(Math.max(last_widget_col + 1, this.cols)); + + if (this.cols < last_widget_col) { + this.add_faux_cols(last_widget_col - this.cols); + } + } + } + + var css_props = {}; + !this.resize_dir.bottom && (css_props.width = Math.max(Math.min( + this.resize_initial_width + rel_x, max_width), min_width)); + !this.resize_dir.right && (css_props.height = Math.max(Math.min( + this.resize_initial_height + rel_y, max_height), min_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); + this.set_dom_grid_width(this.cols); + + this.$resize_preview_holder.css({ + 'width': '', + 'height': '' + }).attr({ + 'data-row': this.$resized_widget.attr('data-row'), + 'data-sizex': size_x, + 'data-sizey': size_y + }); + } + + if (this.options.resize.resize) { + this.options.resize.resize.call(this, event, ui, this.$resized_widget); + } + + this.$el.trigger('gridster:resize'); + + this.resize_last_sizex = size_x; + this.resize_last_sizey = size_y; }; @@ -1587,7 +2268,7 @@ */ fn.on_overlapped_column_change = function(start_callback, stop_callback) { if (!this.colliders_data.length) { - return; + return this; } var cols = this.get_targeted_columns( this.colliders_data[0].el.data.col); @@ -1620,14 +2301,14 @@ * * @param {Function} start_callback Function executed when a new row begins * to be overlapped. The row is passed as first argument. - * @param {Function} stop_callback Function executed when a row stops being + * @param {Function} end_callback Function executed when a row stops being * overlapped. The row is passed as first argument. * @method on_overlapped_row_change * @return {Class} Returns the instance of the Gridster Class. */ fn.on_overlapped_row_change = function(start_callback, end_callback) { if (!this.colliders_data.length) { - return; + return this; } var rows = this.get_targeted_rows(this.colliders_data[0].el.data.row); var last_n_rows = this.last_rows.length; @@ -1653,22 +2334,20 @@ /** * Sets the current position of the player * - * @param {Function} start_callback Function executed when a new row begins - * to be overlapped. The row is passed as first argument. - * @param {Function} stop_callback Function executed when a row stops being - * overlapped. The row is passed as first argument. + * @param {Number} col + * @param {Number} row + * @param {Boolean} no_player * @method set_player - * @return {Class} Returns the instance of the Gridster Class. + * @return {object} */ fn.set_player = function(col, row, no_player) { var self = this; - var swap = false; if (!no_player) { this.empty_cells_player_occupies(); } var cell = !no_player ? self.colliders_data[0].el.data : {col: col}; var to_col = cell.col; - var to_row = cell.row || row; + var to_row = row || cell.row; this.player_grid_data = { col: to_col, @@ -1680,93 +2359,13 @@ this.cells_occupied_by_player = this.get_cells_occupied( this.player_grid_data); - //Added placeholder for more advanced movement. - this.cells_occupied_by_placeholder = this.get_cells_occupied( - this.placeholder_grid_data); - var $overlapped_widgets = this.get_widgets_overlapped( this.player_grid_data); - var player_size_y = this.player_grid_data.size_y; - var player_size_x = this.player_grid_data.size_x; - var placeholder_cells = this.cells_occupied_by_placeholder; - var $gr = this; + var constraints = this.widgets_constraints($overlapped_widgets); - - //Queue Swaps - $overlapped_widgets.each($.proxy(function(i, w){ - var $w = $(w); - var wgd = $w.coords().grid; - var outside_col = placeholder_cells.cols[0]+player_size_x-1; - var outside_row = placeholder_cells.rows[0]+player_size_y-1; - if ($w.hasClass($gr.options.static_class)){ - //next iteration - return true; - } - if(wgd.size_x <= player_size_x && wgd.size_y <= player_size_y){ - if(!$gr.is_swap_occupied(placeholder_cells.cols[0], wgd.row, wgd.size_x, wgd.size_y) && !$gr.is_player_in(placeholder_cells.cols[0], wgd.row) && !$gr.is_in_queue(placeholder_cells.cols[0], wgd.row, $w)){ - swap = $gr.queue_widget(placeholder_cells.cols[0], wgd.row, $w); - } - else if(!$gr.is_swap_occupied(outside_col, wgd.row, wgd.size_x, wgd.size_y) && !$gr.is_player_in(outside_col, wgd.row) && !$gr.is_in_queue(outside_col, wgd.row, $w)){ - swap = $gr.queue_widget(outside_col, wgd.row, $w); - } - else if(!$gr.is_swap_occupied(wgd.col, placeholder_cells.rows[0], wgd.size_x, wgd.size_y) && !$gr.is_player_in(wgd.col, placeholder_cells.rows[0]) && !$gr.is_in_queue(wgd.col, placeholder_cells.rows[0], $w)){ - swap = $gr.queue_widget(wgd.col, placeholder_cells.rows[0], $w); - } - else if(!$gr.is_swap_occupied(wgd.col, outside_row, wgd.size_x, wgd.size_y) && !$gr.is_player_in(wgd.col, outside_row) && !$gr.is_in_queue(wgd.col, outside_row, $w)){ - swap = $gr.queue_widget(wgd.col, outside_row, $w); - } - else if(!$gr.is_swap_occupied(placeholder_cells.cols[0],placeholder_cells.rows[0], wgd.size_x, wgd.size_y) && !$gr.is_player_in(placeholder_cells.cols[0],placeholder_cells.rows[0]) && !$gr.is_in_queue(placeholder_cells.cols[0],placeholder_cells.rows[0], $w)){ - swap = $gr.queue_widget(placeholder_cells.cols[0], placeholder_cells.rows[0], $w); - } else { - //in one last attempt we check for any other empty spaces - for (var c = 0; c < player_size_x; c++){ - for (var r = 0; r < player_size_y; r++){ - var colc = placeholder_cells.cols[0]+c; - var rowc = placeholder_cells.rows[0]+r; - if (!$gr.is_swap_occupied(colc,rowc, wgd.size_x, wgd.size_y) && !$gr.is_player_in(colc,rowc) && !$gr.is_in_queue(colc, rowc, $w)){ - swap = $gr.queue_widget(colc, rowc, $w); - c = player_size_x; - break; - } - } - } - - } - } else if ($gr.options.shift_larger_widgets_down && !swap) { - $overlapped_widgets.each($.proxy(function(i, w){ - var $w = $(w); - var wgd = $w.coords().grid; - - if($gr.can_go_down($w)){ - $gr.move_widget_down($w, $gr.player_grid_data.size_y); - $gr.set_placeholder(to_col, to_row); - } - })); - } - - $gr.clean_up_changed(); - })); - - - /* To show queued items in console - for(var key in this.w_queue){ - console.log("key " +key); - console.log(this.w_queue[key]); - } - */ - - //Move queued widgets - if(swap && this.can_placeholder_be_set(to_col, to_row, player_size_x, player_size_y)){ - for(var key in this.w_queue){ - var col = parseInt(key.split("_")[0]); - var row = parseInt(key.split("_")[1]); - if (this.w_queue[key] != "full"){ - this.new_move_widget_to(this.w_queue[key], col, row); - } - } - this.set_placeholder(to_col, to_row); - } + this.manage_movements(constraints.can_go_up, to_col, to_row); + this.manage_movements(constraints.can_not_go_up, to_col, to_row); /* if there is not widgets overlapping in the new player position, * update the new placeholder position. */ @@ -1775,13 +2374,9 @@ if (pp !== false) { to_row = pp; } - if(this.can_placeholder_be_set(to_col, to_row, player_size_x, player_size_y)){ - this.set_placeholder(to_col, to_row); - } + this.set_placeholder(to_col, to_row); } - this.w_queue = {}; - return { col: to_col, row: to_row @@ -1789,159 +2384,14 @@ }; - fn.is_swap_occupied = function(col, row, w_size_x, w_size_y) { - var occupied = false; - for (var c = 0; c < w_size_x; c++){ - for (var r = 0; r < w_size_y; r++){ - var colc = col + c; - var rowc = row + r; - var key = colc+"_"+rowc; - if(this.is_occupied(colc,rowc)){ - occupied = true; - } else if(key in this.w_queue){ - if(this.w_queue[key] == "full"){ - occupied = true; - continue; - } - $tw = this.w_queue[key]; - tgd = $tw.coords().grid; - //remove queued items if no longer under player. - if(!this.is_widget_under_player(tgd.col,tgd.row)){ - delete this.w_queue[key]; - } - } - if(rowc > parseInt(this.options.max_rows)){ - occupied = true; - } - if(colc > parseInt(this.options.max_cols)){ - occupied = true; - } - if (this.is_player_in(colc,rowc)){ - occupied = true; - } - } - } - - return occupied; - } - - fn.can_placeholder_be_set = function(col, row, player_size_x, player_size_y){ - var can_set = true; - for (var c = 0; c < player_size_x; c++){ - for (var r = 0; r < player_size_y; r++){ - var colc = col + c; - var rowc = row + r; - var key = colc+"_"+rowc; - var $tw = this.is_widget(colc, rowc); - //if this space is occupied and not queued for move. - if(rowc > parseInt(this.options.max_rows)){ - can_set = false; - } - if(colc > parseInt(this.options.max_cols)){ - can_set = false; - } - if(this.is_occupied(colc,rowc) && !this.is_widget_queued_and_can_move($tw)){ - can_set = false; - } - } - } - return can_set; - } - - fn.queue_widget = function(col, row, $widget){ - var $w = $widget - var wgd = $w.coords().grid; - var primary_key = col+"_"+row; - if (primary_key in this.w_queue){ - return false; - } - - this.w_queue[primary_key] = $w; - - for (var c = 0; c < wgd.size_x; c++){ - for (var r = 0; r < wgd.size_y; r++){ - var colc = col + c; - var rowc = row + r; - var key = colc+"_"+rowc; - if (key == primary_key){ - continue; - } - this.w_queue[key] = "full"; - } - } - - return true; - } - - fn.is_widget_queued_and_can_move = function($widget){ - var queued = false; - if ($widget === false){ - return false; - } - - for(var key in this.w_queue){ - if(this.w_queue[key] == "full"){ - continue; - } - if(this.w_queue[key].attr("data-col") == $widget.attr("data-col") && this.w_queue[key].attr("data-row") == $widget.attr("data-row")){ - queued = true; - //test whole space - var $w = this.w_queue[key]; - var dcol = parseInt(key.split("_")[0]); - var drow = parseInt(key.split("_")[1]); - var wgd = $w.coords().grid; - - for (var c = 0; c < wgd.size_x; c++){ - for (var r = 0; r < wgd.size_y; r++){ - var colc = dcol + c; - var rowc = drow + r; - if (this.is_player_in(colc,rowc)){ - queued = false; - } - - } - } - - } - } - - return queued - } - - fn.is_in_queue = function(col,row, $widget){ - var queued = false; - var key = col+"_"+row; - - if ((key in this.w_queue)){ - if (this.w_queue[key] == "full"){ - queued = true; - } else { - $tw = this.w_queue[key]; - tgd = $tw.coords().grid; - if(!this.is_widget_under_player(tgd.col,tgd.row)){ - delete this.w_queue[key] - queued = false; - } else if(this.w_queue[key].attr("data-col") == $widget.attr("data-col") && this.w_queue[key].attr("data-row") == $widget.attr("data-row")) { - delete this.w_queue[key] - queued = false; - } else { - queued = true; - } - } - } - - return queued; - } - - /** * See which of the widgets in the $widgets param collection can go to * a upper row and which not. * * @method widgets_contraints - * @param {HTMLElements} $widgets A jQuery wrapped collection of + * @param {jQuery} $widgets A jQuery wrapped collection of * HTMLElements. - * @return {Array} Returns a literal Object with two keys: `can_go_up` & + * @return {object} Returns a literal Object with two keys: `can_go_up` & * `can_not_go_up`. Each contains a set of HTMLElements. */ fn.widgets_constraints = function($widgets) { @@ -1956,7 +2406,7 @@ if (this.can_go_widget_up(wgd)) { $widgets_can_go_up = $widgets_can_go_up.add($w); wgd_can_go_up.push(wgd); - }else{ + } else { wgd_can_not_go_up.push(wgd); } }, this)); @@ -1964,104 +2414,18 @@ $widgets_can_not_go_up = $widgets.not($widgets_can_go_up); return { - can_go_up: this.sort_by_row_asc(wgd_can_go_up), - can_not_go_up: this.sort_by_row_desc(wgd_can_not_go_up) + can_go_up: Gridster.sort_by_row_asc(wgd_can_go_up), + can_not_go_up: Gridster.sort_by_row_desc(wgd_can_not_go_up) }; }; - /** - * Sorts an Array of grid coords objects (representing the grid coords of - * each widget) in ascending way. - * - * @method sort_by_row_asc - * @param {Array} widgets Array of grid coords objects - * @return {Array} Returns the array sorted. - */ - fn.sort_by_row_asc = function(widgets) { - widgets = widgets.sort(function(a, b) { - if (!a.row) { - a = $(a).coords().grid; - b = $(b).coords().grid; - } - - if (a.row > b.row) { - return 1; - } - return -1; - }); - - return widgets; - }; - - - /** - * Sorts an Array of grid coords objects (representing the grid coords of - * each widget) placing first the empty cells upper left. - * - * @method sort_by_row_and_col_asc - * @param {Array} widgets Array of grid coords objects - * @return {Array} Returns the array sorted. - */ - fn.sort_by_row_and_col_asc = function(widgets) { - widgets = widgets.sort(function(a, b) { - if (a.row > b.row || a.row === b.row && a.col > b.col) { - return 1; - } - return -1; - }); - - return widgets; - }; - - - /** - * Sorts an Array of grid coords objects by column (representing the grid - * coords of each widget) in ascending way. - * - * @method sort_by_col_asc - * @param {Array} widgets Array of grid coords objects - * @return {Array} Returns the array sorted. - */ - fn.sort_by_col_asc = function(widgets) { - widgets = widgets.sort(function(a, b) { - if (a.col > b.col) { - return 1; - } - return -1; - }); - - return widgets; - }; - - /** * Sorts an Array of grid coords objects (representing the grid coords of * each widget) in descending way. * - * @method sort_by_row_desc - * @param {Array} widgets Array of grid coords objects - * @return {Array} Returns the array sorted. - */ - fn.sort_by_row_desc = function(widgets) { - widgets = widgets.sort(function(a, b) { - if (a.row + a.size_y < b.row + b.size_y) { - return 1; - } - return -1; - }); - return widgets; - }; - - - /** - * Sorts an Array of grid coords objects (representing the grid coords of - * each widget) in descending way. - - * Depreciated. - * * @method manage_movements - * @param {HTMLElements} $widgets A jQuery collection of HTMLElements + * @param {jQuery} $widgets A jQuery collection of HTMLElements * representing the widgets you want to move. * @param {Number} to_col The column to which we want to move the widgets. * @param {Number} to_row The row to which we want to move the widgets. @@ -2091,11 +2455,9 @@ // so we need to move widget down to a position that dont // overlaps player var y = (to_row + this.player_grid_data.size_y) - wgd.row; - if (this.can_go_down($w)){ - console.log("In Move Down!") - this.move_widget_down($w, y); - this.set_placeholder(to_col, to_row); - } + + this.move_widget_down($w, y); + this.set_placeholder(to_col, to_row); } } }, this)); @@ -2171,13 +2533,15 @@ * @return {Boolean} Returns true or false. */ fn.is_empty = function(col, row) { - if (typeof this.gridmap[col] !== 'undefined' && - typeof this.gridmap[col][row] !== 'undefined' && - this.gridmap[col][row] === false - ) { - return true; - } - return false; + if (typeof this.gridmap[col] !== 'undefined') { + if(typeof this.gridmap[col][row] !== 'undefined' && + this.gridmap[col][row] === false + ) { + return true; + } + return false; + } + return true; }; @@ -2225,32 +2589,6 @@ return false; }; - /** - * Determines if widget is supposed to be static. - * @method is_static - * @param {Number} col The column to check. - * @param {Number} row The row to check. - * @return {Boolean} Returns true if widget exists and has static class, - * else returns false - */ - - fn.is_static = function(col, row) { - var cell = this.gridmap[col]; - if (!cell) { - return false; - } - - cell = cell[row]; - - if (cell) { - if(cell.hasClass(this.options.static_class)){ - return true; - } - } - - return false; - }; - /** * Determines if there is a widget in the cell represented by col/row @@ -2333,21 +2671,14 @@ if (moved_down || changed_column) { $nexts.each($.proxy(function(i, widget) { - //Make sure widget is at it's topmost position - $w = $(widget); - wgd = $w.coords().grid; - - var can_go_widget_up = this.can_go_widget_up(wgd); - - if (can_go_widget_up) { - this.move_widget_to($w, can_go_widget_up); - } - + this.move_widget_up( + $(widget), this.placeholder_grid_data.col - col + phgd.size_y); }, this)); } + var $widgets_under_ph = this.get_widgets_under_player( + this.cells_occupied_by_placeholder); - var $widgets_under_ph = this.get_widgets_under_player(this.cells_occupied_by_placeholder); if ($widgets_under_ph.length) { $widgets_under_ph.each($.proxy(function(i, widget) { var $w = $(widget); @@ -2389,7 +2720,7 @@ ) { upper_rows[tcol].push(r); min_row = r < min_row ? r : min_row; - }else{ + } else { break; } } @@ -2399,7 +2730,9 @@ return true; //break } - upper_rows[tcol].sort(); + upper_rows[tcol].sort(function(a, b) { + return a - b; + }); }); if (!result) { return false; } @@ -2454,7 +2787,9 @@ return true; //break } - upper_rows[tcol].sort(); + upper_rows[tcol].sort(function(a, b) { + return a - b; + }); }); if (!result) { return false; } @@ -2504,7 +2839,7 @@ if (valid_rows[0] !== p_top_row) { new_row = valid_rows[0] || false; } - }else{ + } else { if (valid_rows[0] !== p_top_row) { new_row = this.get_consecutive_numbers_index( valid_rows, size_y); @@ -2528,7 +2863,7 @@ break; } first = false; - }else{ + } else { result = []; first = true; } @@ -2544,7 +2879,7 @@ * Get widgets overlapping with the player. * * @method get_widgets_overlapped - * @return {HTMLElements} Returns a jQuery collection of HTMLElements. + * @return {jQuery} Returns a jQuery collection of HTMLElements. */ fn.get_widgets_overlapped = function() { var $w; @@ -2577,7 +2912,7 @@ * * @method on_start_overlapping_column * @param {Number} col The collided column. - * @return {HTMLElements} Returns a jQuery collection of HTMLElements. + * @return {jQuery} Returns a jQuery collection of HTMLElements. */ fn.on_start_overlapping_column = function(col) { this.set_player(col, false); @@ -2588,8 +2923,8 @@ * A callback executed when the player begins to collide with a row. * * @method on_start_overlapping_row - * @param {Number} col The collided row. - * @return {HTMLElements} Returns a jQuery collection of HTMLElements. + * @param {Number} row The collided row. + * @return {jQuery} Returns a jQuery collection of HTMLElements. */ fn.on_start_overlapping_row = function(row) { this.set_player(false, row); @@ -2601,17 +2936,16 @@ * * @method on_stop_overlapping_column * @param {Number} col The collided row. - * @return {HTMLElements} Returns a jQuery collection of HTMLElements. + * @return {jQuery} Returns a jQuery collection of HTMLElements. */ fn.on_stop_overlapping_column = function(col) { - //this.set_player(col, false); + this.set_player(col, false); + var self = this; - if(this.options.shift_larger_widgets_down){ - this.for_each_widget_below(col, this.cells_occupied_by_player.rows[0], - function(tcol, trow) { - self.move_widget_up(this, self.player_grid_data.size_y); - }); - } + this.for_each_widget_below(col, this.cells_occupied_by_player.rows[0], + function(tcol, trow) { + self.move_widget_up(this, self.player_grid_data.size_y); + }); }; @@ -2620,40 +2954,20 @@ * * @method on_stop_overlapping_row * @param {Number} row The collided row. - * @return {HTMLElements} Returns a jQuery collection of HTMLElements. + * @return {jQuery} Returns a jQuery collection of HTMLElements. */ fn.on_stop_overlapping_row = function(row) { - //this.set_player(false, row); + this.set_player(false, row); + var self = this; var cols = this.cells_occupied_by_player.cols; - if(this.options.shift_larger_widgets_down){ - for (var c = 0, cl = cols.length; c < cl; c++) { - this.for_each_widget_below(cols[c], row, function(tcol, trow) { - console.log("from_on_stop_overlapping_row"); - self.move_widget_up(this, self.player_grid_data.size_y); - }); - } + for (var c = 0, cl = cols.length; c < cl; c++) { + this.for_each_widget_below(cols[c], row, function(tcol, trow) { + self.move_widget_up(this, self.player_grid_data.size_y); + }); } }; - //Not yet part of api - DM. - fn.new_move_widget_to = function($widget, col, row){ - var self = this; - var widget_grid_data = $widget.coords().grid; - - this.remove_from_gridmap(widget_grid_data); - widget_grid_data.row = row; - widget_grid_data.col = col; - - this.add_to_gridmap(widget_grid_data); - $widget.attr('data-row', row); - $widget.attr('data-col', col); - this.update_widget_position(widget_grid_data, $widget); - this.$changed = this.$changed.add($widget); - - return this; - } - /** * Move a widget to a specific row. The cell or cells must be empty. @@ -2737,10 +3051,9 @@ moved.push($widget); - /* $next_widgets.each($.proxy(function(i, widget) { - console.log("from_within_move_widget_up"); + $next_widgets.each($.proxy(function(i, widget) { this.move_widget_up($(widget), y_units); - }, this)); */ + }, this)); } }); @@ -2751,16 +3064,20 @@ * Move down the specified widget and all below it. * * @method move_widget_down - * @param {HTMLElement} $widget The jQuery object representing the widget + * @param {jQuery} $widget The jQuery object representing the widget * you want to move. - * @param {Number} The number of cells that the widget has to move. + * @param {Number} y_units The number of cells that the widget has to move. * @return {Class} Returns the instance of the Gridster Class. */ fn.move_widget_down = function($widget, y_units) { - var el_grid_data = $widget.coords().grid; - var actual_row = el_grid_data.row; - var moved = []; - var y_diff = y_units; + var el_grid_data, actual_row, moved, y_diff; + + if (y_units <= 0) { return false; } + + el_grid_data = $widget.coords().grid; + actual_row = el_grid_data.row; + moved = []; + y_diff = y_units; if (!$widget) { return false; } @@ -2824,7 +3141,7 @@ !this.is_placeholder_in(tcol, r) ) { urc[tcol].push(r); - }else{ + } else { break; } } @@ -2889,7 +3206,7 @@ * * @method widgets_below * @param {HTMLElement} $el The jQuery wrapped HTMLElement. - * @return {HTMLElements} A jQuery collection of HTMLElements. + * @return {jQuery} A jQuery collection of HTMLElements. */ fn.widgets_below = function($el) { var el_grid_data = $.isPlainObject($el) ? $el : $el.coords().grid; @@ -2907,7 +3224,7 @@ }); }); - return this.sort_by_row_asc($nexts); + return Gridster.sort_by_row_asc($nexts); }; @@ -2939,23 +3256,6 @@ return this; }; - fn.can_go_down = function($el) { - var can_go_down = true; - var $gr = this; - - if ($el.hasClass(this.options.static_class)){ - can_go_down = false; - } - - this.widgets_below($el).each(function(){ - if ($(this).hasClass($gr.options.static_class)){ - can_go_down = false; - } - }) - - return can_go_down; - } - fn.can_go_up = function($el) { var el_grid_data = $el.coords().grid; @@ -2984,7 +3284,6 @@ }; - /** * Check if it's possible to move a widget to a specific col/row. It takes * into account the dimensions (`size_y` and `size_x` attrs. of the grid @@ -3076,7 +3375,7 @@ fn.get_cells_occupied = function(el_grid_data) { var cells = { cols: [], rows: []}; var i; - if (arguments[1] instanceof jQuery) { + if (arguments[1] instanceof $) { el_grid_data = arguments[1].coords().grid; } @@ -3152,15 +3451,6 @@ } }; - fn.clean_up_changed = function(){ - $gr = this; - $gr.$changed.each(function(){ - if($gr.options.shift_larger_widgets_down){ - $gr.move_widget_up($(this)); - } - }); - } - fn._traversing_widgets = function(type, direction, col, row, callback) { @@ -3169,7 +3459,7 @@ var cr, max; var action = type + '/' + direction; - if (arguments[2] instanceof jQuery) { + if (arguments[2] instanceof $) { var el_grid_data = arguments[2].coords().grid; col = el_grid_data.col; row = el_grid_data.row; @@ -3198,8 +3488,7 @@ ) { cr = callback.call(ga[col][trow], col, trow); matched.push(ga[col][trow]); - //break was causing problems, leaving for testing. - //if (cr) { break; } + if (cr) { break; } } } } @@ -3254,26 +3543,23 @@ fn.get_highest_occupied_cell = function() { var r; var gm = this.gridmap; - var rows = []; + var rl = gm[1].length; + var rows = [], cols = []; var row_in_col = []; for (var c = gm.length - 1; c >= 1; c--) { - for (r = gm[c].length - 1; r >= 1; r--) { + for (r = rl - 1; r >= 1; r--) { if (this.is_widget(c, r)) { rows.push(r); - row_in_col[r] = c; + cols.push(c); break; } } } - var highest_row = Math.max.apply(Math, rows); - - this.highest_occupied_cell = { - col: row_in_col[highest_row], - row: highest_row + return { + col: Math.max.apply(Math, cols), + row: Math.max.apply(Math, rows) }; - - return this.highest_occupied_cell; }; @@ -3309,9 +3595,34 @@ * @method set_dom_grid_height * @return {Object} Returns the instance of the Gridster class. */ - fn.set_dom_grid_height = function() { - var r = this.get_highest_occupied_cell().row; - this.$el.css('height', r * this.min_widget_height); + fn.set_dom_grid_height = function(height) { + if (typeof height === 'undefined') { + var r = this.get_highest_occupied_cell().row; + height = r * this.min_widget_height; + } + + this.container_height = height; + this.$el.css('height', this.container_height); + return this; + }; + + /** + * Set the current width of the parent grid. + * + * @method set_dom_grid_width + * @return {Object} Returns the instance of the Gridster class. + */ + fn.set_dom_grid_width = function(cols) { + if (typeof cols === 'undefined') { + cols = this.get_highest_occupied_cell().col; + } + + var max_cols = (this.options.autogrow_cols ? this.options.max_cols : + this.cols); + + cols = Math.min(max_cols, Math.max(cols, this.options.min_cols)); + this.container_width = cols * this.min_widget_width; + this.$el.css('width', this.container_width); return this; }; @@ -3326,7 +3637,7 @@ */ fn.generate_stylesheet = function(opts) { var styles = ''; - var max_size_x = this.options.max_size_x; + var max_size_x = this.options.max_size_x || this.cols; var max_rows = 0; var max_cols = 0; var i; @@ -3351,6 +3662,7 @@ return false; } + this.generated_stylesheets.push(serialized_opts); Gridster.generated_stylesheets.push(serialized_opts); /* generate CSS styles for cols */ @@ -3358,7 +3670,7 @@ styles += (opts.namespace + ' [data-col="'+ (i + 1) + '"] { left:' + ((i * opts.widget_base_dimensions[0]) + (i * opts.widget_margins[0]) + - ((i + 1) * opts.widget_margins[0])) + 'px;} '); + ((i + 1) * opts.widget_margins[0])) + 'px; }\n'); } /* generate CSS styles for rows */ @@ -3366,21 +3678,23 @@ styles += (opts.namespace + ' [data-row="' + (i + 1) + '"] { top:' + ((i * opts.widget_base_dimensions[1]) + (i * opts.widget_margins[1]) + - ((i + 1) * opts.widget_margins[1]) ) + 'px;} '); + ((i + 1) * opts.widget_margins[1]) ) + 'px; }\n'); } for (var y = 1; y <= opts.rows; y++) { styles += (opts.namespace + ' [data-sizey="' + y + '"] { height:' + (y * opts.widget_base_dimensions[1] + - (y - 1) * (opts.widget_margins[1] * 2)) + 'px;}'); + (y - 1) * (opts.widget_margins[1] * 2)) + 'px; }\n'); } for (var x = 1; x <= max_size_x; x++) { styles += (opts.namespace + ' [data-sizex="' + x + '"] { width:' + (x * opts.widget_base_dimensions[0] + - (x - 1) * (opts.widget_margins[0] * 2)) + 'px;}'); + (x - 1) * (opts.widget_margins[0] * 2)) + 'px; }\n'); } + this.remove_style_tags(); + return this.add_style_tag(styles); }; @@ -3393,18 +3707,39 @@ * @return {Object} Returns the instance of the Gridster class. */ fn.add_style_tag = function(css) { - var d = document; - var tag = d.createElement('style'); + var d = document; + var tag = d.createElement('style'); - d.getElementsByTagName('head')[0].appendChild(tag); - tag.setAttribute('type', 'text/css'); + d.getElementsByTagName('head')[0].appendChild(tag); + tag.setAttribute('type', 'text/css'); - if (tag.styleSheet) { - tag.styleSheet.cssText = css; - }else{ - tag.appendChild(document.createTextNode(css)); - } - return this; + if (tag.styleSheet) { + tag.styleSheet.cssText = css; + } else { + tag.appendChild(document.createTextNode(css)); + } + + this.$style_tags = this.$style_tags.add(tag); + + return this; + }; + + + /** + * Remove the style tag with the associated id from the head of the document + * + * @method remove_style_tag + * @return {Object} Returns the instance of the Gridster class. + */ + fn.remove_style_tags = function() { + var all_styles = Gridster.generated_stylesheets; + var ins_styles = this.generated_stylesheets; + + this.$style_tags.remove(); + + Gridster.generated_stylesheets = $.map(all_styles, function(s) { + if ($.inArray(s, ins_styles) === -1) { return s; } + }); }; @@ -3499,8 +3834,9 @@ fn.add_faux_cols = function(cols) { var actual_cols = this.cols; var max_cols = actual_cols + (cols || 1); + max_cols = Math.min(max_cols, this.options.max_cols); - for (var c = actual_cols; c < max_cols; c++) { + for (var c = actual_cols + 1; c <= max_cols; c++) { for (var r = this.rows; r >= 1; r--) { this.add_faux_cell(r, c); } @@ -3533,7 +3869,6 @@ left: this.baseX + (coords.data.col -1) * this.min_widget_width, top: this.baseY + (coords.data.row -1) * this.min_widget_height }); - }, this)); return this; @@ -3547,9 +3882,21 @@ * @return {Object} Returns the instance of the Gridster class. */ fn.get_widgets_from_DOM = function() { - this.$widgets.each($.proxy(function(i, widget) { - this.register_widget($(widget)); + var widgets_coords = this.$widgets.map($.proxy(function(i, widget) { + var $w = $(widget); + return this.dom_to_coords($w); }, this)); + + widgets_coords = Gridster.sort_by_row_and_col_asc(widgets_coords); + + var changes = $(widgets_coords).map($.proxy(function(i, wgd) { + return this.register_widget(wgd) || null; + }, this)); + + if (changes.length) { + this.$el.trigger('gridster:positionschanged'); + } + return this; }; @@ -3563,29 +3910,33 @@ */ fn.generate_grid_and_stylesheet = function() { var aw = this.$wrapper.width(); - var ah = this.$wrapper.height(); + var max_cols = this.options.max_cols; var cols = Math.floor(aw / this.min_widget_width) + this.options.extra_cols; var actual_cols = this.$widgets.map(function() { return $(this).attr('data-col'); - }); - actual_cols = Array.prototype.slice.call(actual_cols, 0); + }).get(); + //needed to pass tests with phantomjs actual_cols.length || (actual_cols = [0]); var min_cols = Math.max.apply(Math, actual_cols); + this.cols = Math.max(min_cols, cols, this.options.min_cols); + + if (max_cols !== Infinity && max_cols >= min_cols && max_cols < this.cols) { + this.cols = max_cols; + } + // get all rows that could be occupied by the current widgets var max_rows = this.options.extra_rows; this.$widgets.each(function(i, w) { max_rows += (+$(w).attr('data-sizey')); }); - this.cols = Math.max(min_cols, cols, this.options.min_cols); - //this.rows = Math.max(max_rows, this.options.min_rows); - this.rows = this.options.max_rows; + this.rows = Math.max(max_rows, this.options.min_rows); this.baseX = ($(window).width() - aw) / 2; this.baseY = this.$wrapper.offset().top; @@ -3597,16 +3948,40 @@ return this.generate_faux_grid(this.rows, this.cols); }; + /** + * Destroy this gridster by removing any sign of its presence, making it easy to avoid memory leaks + * + * @method destroy + * @param {Boolean} remove If true, remove gridster from DOM. + * @return {Object} Returns the instance of the Gridster class. + */ + fn.destroy = function(remove) { + this.$el.removeData('gridster'); + + // remove bound callback on window resize + $(window).unbind('.gridster'); + + if (this.drag_api) { + this.drag_api.destroy(); + } + + this.remove_style_tags(); + + remove && this.$el.remove(); + + return this; + }; + //jQuery adapter $.fn.gridster = function(options) { - return this.each(function() { - if (!$(this).data('gridster')) { - $(this).data('gridster', new Gridster( this, options )); - } - }); + return this.each(function() { + if (! $(this).data('gridster')) { + $(this).data('gridster', new Gridster( this, options )); + } + }); }; - $.Gridster = fn; + return Gridster; -}(jQuery, window, document)); +}));