diff --git a/home/inc/class.home_link_portlet.inc.php b/home/inc/class.home_link_portlet.inc.php index ab48f42bda..5c0da52539 100644 --- a/home/inc/class.home_link_portlet.inc.php +++ b/home/inc/class.home_link_portlet.inc.php @@ -66,11 +66,11 @@ class home_link_portlet extends home_portlet /** * Get a fragment of HTML for display * - * @param content Array Values returned from a submit, if any - * @param context Settings for customizing 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 */ - public function get_content() + public function get_content($id = null) { return $this->title; } @@ -124,6 +124,7 @@ class home_link_portlet extends home_portlet ) ); $actions['view']['enabled'] = (bool)$this->context['entry']; + return $actions; } } diff --git a/home/inc/class.home_list_portlet.inc.php b/home/inc/class.home_list_portlet.inc.php new file mode 100644 index 0000000000..e7dc057e02 --- /dev/null +++ b/home/inc/class.home_list_portlet.inc.php @@ -0,0 +1,178 @@ +title = $context['title']; + } + // Add a new entry to the list + if($context['add']) + { + $context['list'][] = $context['add']; + unset($context['add']); + } + + $this->context = $context; + } + + /** + * Some descriptive information about the portlet, so that users can decide if + * they want it or not, and for inclusion in lists, hover text, etc. + * + * These should be already translated, no further translation will be done. + * + * @return Array with keys + * - displayName: Used in lists + * - title: Put in the portlet header + * - description: A short description of what this portlet does or displays + */ + public function get_description() + { + return array( + 'displayName'=> 'List of entries', + 'title'=> $this->title, + 'description'=> lang('Show a list of entries') + ); + } + + /** + * Get a fragment of HTML for 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) + { + $list = array(); + foreach($this->context['list'] as $link_id => $link) + { + $list[] = $link + array( + 'title' => egw_link::title($link['app'], $link['id']), + 'icon' => egw_link::get_registry($link['app'], 'icon') + ); + } + + // Find the portlet widget, and add a link-list to it + return ""; + } + + /** + * Return a list of settings to customize the portlet. + * + * Settings should be in the same style as for preferences. It is OK to return an empty array + * for no customizable settings. + * + * These should be already translated, no further translation will be done. + * + * @see preferences/inc/class.preferences_settings.inc.php + * @return Array of settings. Each setting should have the following keys: + * - name: Internal reference + * - type: Widget type for editing + * - label: Human name + * - help: Description of the setting, and what it does + * - default: Default value, for when it's not set yet + */ + public function get_properties() + { + return array( + array( + 'name' => 'title', + 'type' => 'textbox', + 'label' => lang('Title'), + ), + // Internal + array( + 'name' => 'list' + ) + ) + parent::get_properties(); + } + + /** + * Return a list of allowable actions for the portlet. + * + * These actions will be merged with the default porlet actions. + * We add an 'edit' action as default so double-clicking the widget + * opens the entry + */ + public function get_actions() + { + $actions = array( + 'add' => array( + 'icon' => 'add', + 'caption' => lang('add'), + 'hideOnDisabled' => false, + 'onExecute' => 'javaScript:app.home.add_link', + ), + 'add_drop' => array( + 'type' => 'drop', + 'caption' => lang('add'), + 'onExecute' => 'javaScript:app.home.add_link', + 'acceptedTypes' => array('file') + array_keys($GLOBALS['egw_info']['apps']), + ) + ); + return $actions; + } + + /** + * List portlet displays multiple entries, so it makes sense to accept multiple dropped entries + */ + public function accept_multiple() + { + return true; + } +} diff --git a/home/inc/class.home_portlet.inc.php b/home/inc/class.home_portlet.inc.php index 3d77eb397f..1ac535daa7 100644 --- a/home/inc/class.home_portlet.inc.php +++ b/home/inc/class.home_portlet.inc.php @@ -49,11 +49,11 @@ abstract class home_portlet /** * Get a fragment of HTML for display * - * @param content Array Values returned from a submit, if any - * @param context Settings for customizing 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 */ - public abstract function get_content(); + public abstract function get_content($id = null); /** * Return a list of settings to customize the portlet. @@ -71,7 +71,8 @@ abstract class home_portlet * - help: Description of the setting, and what it does * - default: Default value, for when it's not set yet */ - public function get_properties() { + public function get_properties() + { // Include the common attributes, or they won't get saved $properties = array(); foreach(self::$common_attributes as $prop) @@ -84,8 +85,18 @@ abstract class home_portlet /** * Return a list of allowable actions for the portlet. * - * These actions will be merged with the default porlet actions. Use the + * These actions will be merged with the default portlet actions. Use the * same id / key to override the default action. */ public abstract function get_actions(); + + /** + * If this portlet can accept, display, or otherwise handle multiple + * EgroupWare entries. Used for drag and drop processing. How the entries + * are handled are up to the portlet. + */ + public function accept_multiple() + { + return false; + } } diff --git a/home/inc/class.home_ui.inc.php b/home/inc/class.home_ui.inc.php index f19d8f6081..1daa07f4a1 100644 --- a/home/inc/class.home_ui.inc.php +++ b/home/inc/class.home_ui.inc.php @@ -65,31 +65,29 @@ class home_ui 'caption' => 'Add', 'onExecute' => 'javaScript:app.home.add', 'children' => $portlets - ), - 'drop_create' => array( - 'caption' => 'Add', - 'type' => 'drop', - 'acceptedTypes' => array('file') + array_keys($GLOBALS['egw_info']['apps']), - 'onExecute' => 'javaScript:app.home.add_from_drop', ) ); + // 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) { - // Home portlets + // Home portlets - uses link system, so all apps that support that are accepted if(!$children['children']) { - $children['onExecute'] = $actions['drop_create']['onExecute']; - $children['acceptedTypes'] = egw_link::app_list(); - $actions[$app] = $children; + $children['onExecute'] = $drop_execute; + $children['acceptedTypes'] = array_keys(egw_link::app_list()); + $children['type'] = 'drop'; + $actions["drop_$app"] = $children; } else { foreach($children as $portlet => $app_portlets) { - $app_portlets['onExecute'] = $actions['drop_create']['onExecute']; + $app_portlets['onExecute'] = $drop_execute; $app_portlet['acceptedTypes'] = $app; - $actions[$portlet] = $app_portlets; + $app_portlet['type'] = 'drop'; + $actions["drop_$portlet"] = $app_portlets; } } } @@ -117,7 +115,7 @@ class home_ui { $content = ''; $attrs = array(); - $this->get_portlet($context, $content, $attrs); + $this->get_portlet($id, $context, $content, $attrs); $portlets[$id] = $content; $attributes[$id] = $attrs; @@ -138,7 +136,7 @@ class home_ui * @param attributes Array Settings that can be customized on a per-portlet basis - will be set * @return home_portlet The portlet object that created the content */ - protected function get_portlet(&$context, &$content, &$attributes) + protected function get_portlet($id, &$context, &$content, &$attributes) { if(!$context['class']) $context['class'] = 'home_link_portlet'; @@ -146,14 +144,20 @@ class home_ui $portlet = new $classname($context); $desc = $portlet->get_description(); - $content = $portlet->get_content(); + $content = $portlet->get_content($id); - // Exclude common attributes changed through UI - $settings = $portlet->get_properties() + $context; + // 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]); } + $attributes = array( 'title' => $desc['title'], 'settings' => $settings, @@ -223,7 +227,8 @@ class home_ui 'id' => $portlet, 'caption' => $desc['displayName'], 'hint' => $desc['description'], - 'onExecute' => 'javaScript:app.home.add' + 'onExecute' => 'javaScript:app.home.add', + 'allowOnMultiple' => $instance->accept_multiple() ); } } @@ -259,12 +264,12 @@ class home_ui } else { -error_log(array2string($attributes)); -error_log(array2string($values)); // Get portlet settings, and merge new with old $content = ''; - $portlet = $this->get_portlet(array_merge((array)$attributes, $values), $content, $attributes); - $context = array('class' => get_class($portlet)); + $context = $values+(array)$portlets[$portlet_id]; //array('class'=>$attributes['class']); + $portlet = $this->get_portlet($portlet_id, $context,$content, $attributes); + + $context['class'] = get_class($portlet); foreach($portlet->get_properties() as $property) { if($values[$property['name']]) @@ -277,14 +282,12 @@ error_log(array2string($values)); } } + // Update client side $update = array('content' => $content, 'attributes' => $attributes); // New portlet? Flag going straight to edit mode - if(!array_key_exists($portlet_id,$portlets) && $attributes['settings']) - { - $update['edit_settings'] = true; - } + //$update['edit_settings'] = true; $response->data($update); // Store for preference update diff --git a/home/js/app.js b/home/js/app.js index ba088751b7..06d8b99574 100644 --- a/home/js/app.js +++ b/home/js/app.js @@ -1,10 +1,10 @@ /** - * EGroupware - Filemanager - Javascript UI + * EGroupware - Home - Javascript UI * * @link http://www.egroupware.org - * @package filemanager - * @author Ralf Becker - * @copyright (c) 2008-13 by Ralf Becker + * @package home + * @author Nathan Gray + * @copyright (c) 2013 Nathan Gray * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ @@ -14,7 +14,6 @@ /*egw:uses jquery.jquery; jquery.jquery-ui; - /phpgwapi/js/jquery/shapeshift/core/jquery.shapeshift.js; /phpgwapi/js/jquery/gridster/jquery.gridster.js; */ @@ -25,7 +24,7 @@ * * * Uses Gridster for the grid layout - * @see https://github.com/dustmoo/gridster.js + * @see http://gridster.net * @augments AppJS */ app.home = AppJS.extend( @@ -35,6 +34,19 @@ app.home = AppJS.extend( */ appname: "home", + /** + * Grid resolution. Must match et2_portlet GRID + */ + GRID: 50, + + /** + * Default size for new portlets + */ + DEFAULT: { + WIDTH: 2, + HEIGHT: 1 + }, + /** * Constructor * @@ -93,12 +105,12 @@ app.home = AppJS.extend( * Add a new portlet from the context menu */ add: function(action) { - var attrs = {id: this._create_id(), class: action.id}; + var attrs = {id: this._create_id()}; var portlet = et2_createWidget('portlet',attrs, this.portlet_container); portlet.loadingFinished(); // Get actual attributes & settings, since they're not available client side yet - portlet._process_edit(et2_dialog.OK_BUTTON, {}); + portlet._process_edit(et2_dialog.OK_BUTTON, {class: action.id}); // Set up sorting/grid of new portlet var $portlet_container = $j(this.portlet_container.getDOMNode()); @@ -111,7 +123,25 @@ app.home = AppJS.extend( * User dropped something on home. Add a new portlet */ add_from_drop: function(action,source,target_action) { - var attrs = {id: this._create_id(), class: action.id}; + + // Actions got confused drop vs popup + if(source[0].id == 'portlets') + { + return this.add(action); + } + + var $portlet_container = $j(this.portlet_container.getDOMNode()); + + // Basic portlet attributes + var attrs = {id: this._create_id()}; + + // Try to find where the drop was + if(action != null && action.ui && action.ui.position) + { + attrs.row = Math.round((action.ui.offset.top - $portlet_container.offset().top )/ this.GRID); + attrs.col = Math.max(0,Math.round((action.ui.offset.left - $portlet_container.offset().left) / this.GRID)-1); + } + var portlet = et2_createWidget('portlet',attrs, this.portlet_container); portlet.loadingFinished(); @@ -121,14 +151,14 @@ app.home = AppJS.extend( { if(source[i].id) drop_data.push(source[i].id); } - portlet._process_edit(et2_dialog.OK_BUTTON, {dropped_data: drop_data}); + portlet._process_edit(et2_dialog.OK_BUTTON, {dropped_data: drop_data, class: action.id.substr(5)}); // Set up sorting/grid of new portlet - var $portlet_container = $j(this.portlet_container.getDOMNode()); $portlet_container.data("gridster").add_widget( - portlet.getDOMNode() + portlet.getDOMNode(), + this.DEFAULT.WIDTH, this.DEFAULT.HEIGHT, + attrs.col, attrs.row ); - console.log(this,arguments); }, /** @@ -156,6 +186,14 @@ app.home = AppJS.extend( egw().open(widget.options.settings.entry, "", 'edit'); }, + /** + * For list_portlet - adds a new link + * This is needed here so action system can find it + */ + add_link: function(action,source,target_action) { + this.List.add_link(action, source, target_action); + }, + /** * Set up the drag / drop / re-order of portlets */ @@ -164,13 +202,11 @@ app.home = AppJS.extend( $portlet_container .addClass("home ui-helper-clearfix") .disableSelection() - /* Shapeshift - .shapeshift(); - */ /* Gridster */ .gridster({ widget_selector: 'div.et2_portlet', - widget_base_dimensions: [45, 45], + // Dimensions + margins = grid spacing + widget_base_dimensions: [this.GRID-5, this.GRID-5], widget_margins: [5,5], extra_rows: 1, extra_cols: 1, @@ -205,13 +241,13 @@ app.home = AppJS.extend( var widget = window.app.home.portlet_container.getWidgetById(changed[key].id); if(!widget || widget == window.app.home.portlet_container) continue; - egw().json("home.home_ui.ajax_set_properties",[widget.id, widget.options.settings,{ + egw().jsonq("home.home_ui.ajax_set_properties",[widget.id, widget.options.settings,{ row: changed[key].row, col: changed[key].col }], null, widget, true, widget - ).sendRequest(); + ); } } } @@ -224,8 +260,8 @@ app.home = AppJS.extend( .on("resizestop", function(event, ui) { $portlet_container.data("gridster").resize_widget( ui.element, - Math.round(ui.size.width / 50), - Math.round(ui.size.height / 50) + Math.round(ui.size.width / this.GRID), + Math.round(ui.size.height / this.GRID) ); }); }, @@ -243,5 +279,109 @@ app.home = AppJS.extend( } while(this.portlet_container.getWidgetById(id)); return id; + }, + + /** + * Functions for the list portlet + */ + 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) + { + var portlet = app.home.portlet_container.getWidgetById(id); + 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 + */ + add_link: function(action, source, target_action) { + // Actions got confused drop vs popup + if(source[0].id == 'portlets') + { + return this.add_link(action); + } + + // Get widget + var widget = null; + while(action.parent != null) + { + if(action.data && action.data.widget) + { + widget = action.data.widget; + break; + } + action = action.parent; + } + if(typeof source == undefined) + { + var link = et2_createWidget('link-entry', {label: egw.lang('Add')}, this.portlet_container); + var dialog = et2_dialog.show_dialog( + function(button_id) { + widget._process_edit(button_id,{list: widget.options.settings.list || {}, add: link.getValue()}); + link.destroy(); + }, + 'Add', + egw.lang('Add'), {}, + et2_dialog.BUTTONS_OK_CANCEL + ); + dialog.set_message(link.getDOMNode()); + } + else + { + // Drag'n'dropped something on the list - just send action IDs + var drop_data = []; + for(var i = 0; i < source.length; i++) + { + if(source[i].id) drop_data.push(source[i].id); + } + 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 + 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 || {}}); + } } }); diff --git a/home/templates/default/app.css b/home/templates/default/app.css index 2d7324a1c3..97da2d2cc6 100644 --- a/home/templates/default/app.css +++ b/home/templates/default/app.css @@ -17,6 +17,9 @@ .et2_portlet.ui-widget-content { overflow: hidden; } +.et2_portlet.ui-widget-content > div:last-of-type { + height: 100%; +} /* Shapeshift #portlets {