- Use a queue to buffer property updates

- Better support for dropping application entries on Home portlet area
- Add a portlet that shows a list of entries, supports dropping into the list
This commit is contained in:
Nathan Gray 2013-05-29 19:25:12 +00:00
parent fdfae8dd92
commit 5fea4ed9a5
6 changed files with 391 additions and 55 deletions

View File

@ -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;
}
}

View File

@ -0,0 +1,178 @@
<?php
/**
* EGroupware - Home - A simple portlet for displaying a list of entries
*
* @link www.egroupware.org
* @author Nathan Gray
* @copyright (c) 2013 by Nathan Gray
* @package home
* @subpackage portlet
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id$
*/
/**
* The home_list_portlet uses the link system and its associated link-list widget
* to display a list of entries. This is a simple static list that the user can manually
* add to and remove from.
*
* Any application that supports the link system should be able to be added into the list.
*/
class home_list_portlet extends home_portlet
{
/**
* Context for this portlet
*/
protected $context = array();
/**
* Title of entry
*/
protected $title = 'List';
/**
* Construct the portlet
*
*/
public function __construct(Array &$context = array())
{
if(!is_array($context['list'])) $context['list'] = array();
// Process dropped data (Should be GUIDs) into something useable
if($context['dropped_data'])
{
foreach((Array)$context['dropped_data'] as $dropped)
{
$add = array();
list($add['app'], $add['id']) = explode('::', $dropped, 2);
if($add['app'] && $add['id'])
{
$context['list'][] = $add;
}
}
unset($add);
unset($context['dropped_data']);
}
if($context['title'])
{
$this->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 "<script>app.home.List.set_content('$id', ".json_encode($list).")</script>";
}
/**
* 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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -1,10 +1,10 @@
/**
* EGroupware - Filemanager - Javascript UI
* EGroupware - Home - Javascript UI
*
* @link http://www.egroupware.org
* @package filemanager
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @copyright (c) 2008-13 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
* @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 || {}});
}
}
});

View File

@ -17,6 +17,9 @@
.et2_portlet.ui-widget-content {
overflow: hidden;
}
.et2_portlet.ui-widget-content > div:last-of-type {
height: 100%;
}
/* Shapeshift
#portlets {