Add new folder navigation to tasks module (#3047)

This commit is contained in:
Thomas Bruederli 2014-05-21 13:04:18 +02:00
parent 184730b347
commit f4f5a30e0a
13 changed files with 633 additions and 159 deletions

View file

@ -119,8 +119,8 @@ class calendar_ui
$this->rc->output->include_script('treelist.js');
// include kolab folderlist widget if available
if (is_readable($this->cal->home . '/lib/js/folderlist.js')) {
$this->cal->include_script('lib/js/folderlist.js');
if (is_readable($this->cal->api->dir . 'libkolab/js/folderlist.js')) {
$this->cal->api->include_script('libkolab/js/folderlist.js');
}
jqueryui::miniColors();

View file

@ -1 +0,0 @@
../../../libkolab/js/folderlist.js

View file

@ -149,6 +149,24 @@ abstract class kolab_storage_folder_api
return rcube_charset::convert(end($parts), 'UTF7-IMAP');
}
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
$path = explode('/', $this->name);
array_pop($path);
// don't list top-level namespace folder
if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
$path = array();
}
return join('/', $path);
}
/**
* Get the color value stored in metadata

View file

@ -199,6 +199,18 @@ class tasklist_database_driver extends tasklist_driver
return false;
}
/**
* Search for shared or otherwise not listed tasklists the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of tasklists
*/
public function search_lists($query, $source)
{
return array();
}
/**
* Get number of tasks matching the given filter
*

View file

@ -45,12 +45,15 @@ class tasklist_kolab_driver extends tasklist_driver
$this->rc = $plugin->rc;
$this->plugin = $plugin;
$this->_read_lists();
if (kolab_storage::$version == '2.0') {
$this->alarm_absolute = false;
}
// tasklist use fully encoded identifiers
kolab_storage::$encode_ids = true;
$this->_read_lists();
$this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
}
@ -83,87 +86,171 @@ class tasklist_kolab_driver extends tasklist_driver
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$prefs = $this->rc->config->get('kolab_tasklists', array());
$listnames = array();
// include virtual folders for a full folder tree
if (!$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
$folders = kolab_storage::folder_hierarchy($folders);
foreach ($folders as $folder) {
$utf7name = $folder->name;
$tasklist = $this->folder_props($folder, $delim, $prefs);
$path_imap = explode($delim, $utf7name);
$editname = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP'); // pop off raw name part
$path_imap = join($delim, $path_imap);
$fullname = $folder->get_name();
$listname = kolab_storage::folder_displayname($fullname, $listnames);
// special handling for virtual folders
if ($folder->virtual) {
$list_id = kolab_storage::folder_id($utf7name);
$this->lists[$list_id] = array(
'id' => $list_id,
'name' => $fullname,
'listname' => $listname,
'virtual' => true,
'editable' => false,
);
continue;
}
if ($folder->get_namespace() == 'personal') {
$norename = false;
$readonly = false;
$alarms = true;
}
else {
$alarms = false;
$readonly = true;
if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$readonly = false;
}
$info = $folder->get_folder_info();
$norename = $readonly || $info['norename'] || $info['protected'];
}
$list_id = kolab_storage::folder_id($utf7name);
$tasklist = array(
'id' => $list_id,
'name' => $fullname,
'listname' => $listname,
'editname' => $editname,
'color' => $folder->get_color('0000CC'),
'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
'editable' => !$readionly,
'norename' => $norename,
'active' => $folder->is_active(),
'parentfolder' => $path_imap,
'default' => $folder->default,
'children' => true, // TODO: determine if that folder indeed has child folders
'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
);
$this->lists[$tasklist['id']] = $tasklist;
$this->folders[$tasklist['id']] = $folder;
$this->folders[$folder->name] = $folder;
}
}
/**
* Derive list properties from the given kolab_storage_folder object
*/
protected function folder_props($folder, $delim, $prefs)
{
if ($folder->get_namespace() == 'personal') {
$norename = false;
$readonly = false;
$alarms = true;
}
else {
$alarms = false;
$readonly = true;
if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$readonly = false;
}
$info = $folder->get_folder_info();
$norename = $readonly || $info['norename'] || $info['protected'];
}
$list_id = $folder->id; #kolab_storage::folder_id($folder->name);
$old_id = kolab_storage::folder_id($folder->name, false);
if (!isset($prefs[$list_id]['showalarms']) && isset($prefs[$old_id]['showalarms'])) {
$prefs[$list_id]['showalarms'] = $prefs[$old_id]['showalarms'];
}
return array(
'id' => $list_id,
'name' => $folder->get_name(),
'listname' => $folder->get_foldername(),
'editname' => $folder->get_foldername(),
'color' => $folder->get_color('0000CC'),
'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
'editable' => !$readonly,
'norename' => $norename,
'active' => $folder->is_active(),
'parentfolder' => $folder->get_parent(),
'default' => $folder->default,
'virtual' => $folder->virtual,
'children' => true, // TODO: determine if that folder indeed has child folders
'subscribed' => (bool)$folder->is_subscribed(),
'group' => $folder->get_namespace(),
'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
);
}
/**
* Get a list of available task lists from this source
*/
public function get_lists()
public function get_lists(&$tree = null)
{
// attempt to create a default list for this user
if (empty($this->lists)) {
if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true)))
$prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true);
if ($this->create_list($prop))
$this->_read_lists(true);
}
return $this->lists;
$folders = array();
foreach ($this->lists as $id => $list) {
if (!empty($this->folders[$id])) {
$folders[] = $this->folders[$id];
}
}
// include virtual folders for a full folder tree
if (!is_null($tree)) {
$folders = kolab_storage::folder_hierarchy($folders, $tree);
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$prefs = $this->rc->config->get('kolab_tasklists', array());
$lists = array();
foreach ($folders as $folder) {
$list_id = $folder->id; #kolab_storage::folder_id($folder->name);
$imap_path = explode($delim, $folder->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !$this->folders[$parent_id]);
// restore "real" parent ID
if ($parent_id && !$this->folders[$parent_id]) {
$parent_id = kolab_storage::folder_id($folder->get_parent());
}
$fullname = $folder->get_name();
$listname = $folder->get_foldername();
// special handling for virtual folders
if ($folder instanceof kolab_storage_folder_user) {
$lists[$list_id] = array(
'id' => $list_id,
'name' => $folder->get_name(),
'listname' => $listname,
'title' => $folder->get_owner(),
'virtual' => true,
'editable' => false,
'group' => 'other virtual',
'class' => 'user',
'parent' => $parent_id,
);
}
else if ($folder->virtual) {
$lists[$list_id] = array(
'id' => $list_id,
'name' => kolab_storage::object_name($fullname),
'listname' => $listname,
'virtual' => true,
'editable' => false,
'group' => $folder->get_namespace(),
'class' => 'folder',
'parent' => $parent_id,
);
}
else {
if (!$this->lists[$list_id]) {
$this->lists[$list_id] = $this->folder_props($folder, $delim, $prefs);
$this->folders[$list_id] = $folder;
}
$this->lists[$list_id]['parent'] = $parent_id;
$lists[$list_id] = $this->lists[$list_id];
}
}
return $lists;
}
/**
* Get the kolab_calendar instance for the given calendar ID
*
* @param string List identifier (encoded imap folder name)
* @return object kolab_storage_folder Object nor null if list doesn't exist
*/
protected function get_folder($id)
{
// create list and folder instance if necesary
if (!$this->lists[$id]) {
$folder = kolab_storage::get_folder(kolab_storage::id_decode($id));
if ($folder->type) {
$this->folders[$id] = $folder;
$this->lists[$id] = $this->folder_props($folder, $this->rc->get_storage()->get_hierarchy_delimiter(), $this->rc->config->get('kolab_tasklists', array()));
}
}
return $this->folders[$id];
}
/**
* Create a new list assigned to the current user
*
@ -215,7 +302,7 @@ class tasklist_kolab_driver extends tasklist_driver
*/
public function edit_list(&$prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
$prop['oldname'] = $folder->name;
$prop['type'] = 'task';
$newfolder = kolab_storage::folder_update($prop);
@ -254,12 +341,18 @@ class tasklist_kolab_driver extends tasklist_driver
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
* permanent: True if list is to be subscribed permanently
* @return boolean True on success, Fales on failure
*/
public function subscribe_list($prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
return $folder->activate($prop['active']);
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
$ret = false;
if (isset($prop['permanent']))
$ret |= $folder->subscribe(intval($prop['permanent']));
if (isset($prop['active']))
$ret |= $folder->activate(intval($prop['active']));
return $ret;
}
return false;
}
@ -273,7 +366,7 @@ class tasklist_kolab_driver extends tasklist_driver
*/
public function remove_list($prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
if (kolab_storage::folder_delete($folder->name))
return true;
else
@ -283,6 +376,63 @@ class tasklist_kolab_driver extends tasklist_driver
return false;
}
/**
* Search for shared or otherwise not listed tasklists the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of tasklists
*/
public function search_lists($query, $source)
{
if (!kolab_storage::setup()) {
return array();
}
$this->search_more_results = false;
$this->lists = $this->folders = array();
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
}
}
// search other user's namespace via LDAP
else if ($source == 'users') {
$limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
$folders = array();
// search for tasks folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) {
$folders[] = new kolab_storage_folder($foldername, 'task');
}
if (count($folders)) {
$userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
$this->folders[$userfolder->id] = $userfolder;
$this->lists[$userfolder->id] = $this->folder_props($userfolder, $delim, array());
foreach ($folders as $folder) {
$this->folders[$folder->id] = $folder;
$this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
$count++;
}
}
if ($count >= $limit) {
$this->search_more_results = true;
break;
}
}
}
return $this->get_lists();
}
/**
* Get number of tasks matching the given filter
*
@ -303,7 +453,9 @@ class tasklist_kolab_driver extends tasklist_driver
$counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
foreach ($lists as $list_id) {
$folder = $this->folders[$list_id];
if (!$folder = $this->get_folder($list_id)) {
continue;
}
foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) {
$rec = $this->_to_rcube_task($record);
@ -367,7 +519,9 @@ class tasklist_kolab_driver extends tasklist_driver
}
foreach ($lists as $list_id) {
$folder = $this->folders[$list_id];
if (!$folder = $this->get_folder($list_id)) {
continue;
}
foreach ($folder->select($query) as $record) {
$task = $this->_to_rcube_task($record);
$task['list'] = $list_id;
@ -391,11 +545,11 @@ class tasklist_kolab_driver extends tasklist_driver
{
$id = is_array($prop) ? ($prop['uid'] ?: $prop['id']) : $prop;
$list_id = is_array($prop) ? $prop['list'] : null;
$folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders;
$folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->folders;
// find task in the available folders
foreach ($folders as $list_id => $folder) {
if (is_numeric($list_id))
if (is_numeric($list_id) || !$folder)
continue;
if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
$this->tasks[$id] = $this->_to_rcube_task($object);
@ -424,7 +578,7 @@ class tasklist_kolab_driver extends tasklist_driver
$childs = array();
$list_id = $prop['list'];
$task_ids = array($prop['id']);
$folder = $this->folders[$list_id];
$folder = $this->get_folder($list_id);
// query for childs (recursively)
while ($folder && !empty($task_ids)) {
@ -484,7 +638,7 @@ class tasklist_kolab_driver extends tasklist_driver
if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
continue;
$folder = $this->folders[$lid];
$folder = $this->get_folder($lid);
foreach ($folder->select($query) as $record) {
if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-)
continue;
@ -756,11 +910,11 @@ class tasklist_kolab_driver extends tasklist_driver
public function edit_task($task)
{
$list_id = $task['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
// moved from another folder
if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
if (!$fromfolder->move($task['id'], $folder->name))
return false;
@ -809,11 +963,11 @@ class tasklist_kolab_driver extends tasklist_driver
public function move_task($task)
{
$list_id = $task['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
// execute move command
if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
return $fromfolder->move($task['id'], $folder->name);
}
@ -831,7 +985,7 @@ class tasklist_kolab_driver extends tasklist_driver
public function delete_task($task, $force = true)
{
$list_id = $task['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
if (!$list_id || !($folder = $this->get_folder($list_id)))
return false;
return $folder->delete($task['id']);
@ -892,7 +1046,7 @@ class tasklist_kolab_driver extends tasklist_driver
*/
public function get_attachment_body($id, $task)
{
if ($storage = $this->folders[$task['list']]) {
if ($storage = $this->get_folder($task['list'])) {
return $storage->get_attachment($task['id'], $id);
}
@ -905,7 +1059,7 @@ class tasklist_kolab_driver extends tasklist_driver
public function tasklist_edit_form($action, $list, $fieldprop)
{
if ($list['id'] && ($list = $this->lists[$list['id']])) {
$folder_name = $this->folders[$list['id']]->name; // UTF7
$folder_name = $this->get_folder($list['id'])->name; // UTF7
}
else {
$folder_name = '';

View file

@ -125,6 +125,15 @@ abstract class tasklist_driver
*/
abstract function remove_list($prop);
/**
* Search for shared or otherwise not listed tasklists the user has access
*
* @param string Search string
* @param string Section/source to search
* @return array List of tasklists
*/
abstract function search_lists($query, $source);
/**
* Get number of tasks matching the given filter
*

View file

@ -5,6 +5,9 @@ $labels['navtitle'] = 'Tasks';
$labels['lists'] = 'Tasklists';
$labels['list'] = 'Tasklist';
$labels['tags'] = 'Tags';
$labels['tasklistsubscribe'] = 'List permanently';
$labels['listsearchresults'] = 'Available Tasklists';
$labels['findlists'] = 'Find tasklists...';
$labels['newtask'] = 'New Task';
$labels['createnewtask'] = 'Create new Task (e.g. Saturday, Mow the lawn)';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -74,6 +74,32 @@ body.attachmentwin #topnav .topright {
bottom: 0px;
}
#tasklistsbox .boxtitle a.iconbutton.search {
position: absolute;
top: 8px;
right: 8px;
width: 16px;
cursor: pointer;
background-position: -2px -317px;
}
#tasklistsbox .listsearchbox {
display: none;
}
#tasklistsbox .listsearchbox.expanded {
display: block;
}
#tasklistsbox .scroller {
top: 34px;
}
#tasklistsbox .listsearchbox.expanded + .scroller {
top: 68px;
}
#taskselector {
margin: -4px 40px 0 0;
padding: 0;
@ -225,32 +251,48 @@ body.attachmentwin #topnav .topright {
display: none;
}
#tasklists li {
#tasklistsbox .treelist li {
margin: 0;
display: block;
position: relative;
}
#tasklistsbox .treelist li div.tasklist {
margin: 0;
height: 20px;
padding: 6px 8px 2px 6px;
display: block;
position: relative;
white-space: nowrap;
}
#tasklists li.virtual {
height: 12px;
#tasklistsbox .treelist li.virtual > div.tasklist {
height: 14px;
}
#tasklists li label {
#tasklistsbox .treelist ul li > div.tasklist {
margin-left: 16px;
}
#tasklistsbox .treelist ul ul li > div.tasklist {
margin-left: 32px;
}
#tasklistsbox .treelist ul ul ul li > div.tasklist {
margin-left: 48px;
}
#tasklistsbox .treelist li label {
display: block;
}
#tasklists li span.listname {
#tasklistsbox .treelist li span.listname {
display: block;
position: absolute;
top: 7px;
left: 26px;
right: 26px;
left: 38px;
right: 40px;
cursor: default;
padding-bottom: 2px;
padding-right: 30px;
padding: 0px 30px 2px 2px;
color: #004458;
overflow: hidden;
text-overflow: ellipsis;
@ -258,8 +300,11 @@ body.attachmentwin #topnav .topright {
background: url(sprites.png) right 20px no-repeat;
}
#tasklists li span.handle {
#tasklistsbox .treelist li span.quickview {
display: inline-block;
position: absolute;
top: 6px;
right: 20px;
width: 16px;
height: 16px;
margin-right: 4px;
@ -267,52 +312,76 @@ body.attachmentwin #topnav .topright {
cursor: pointer;
}
#tasklists li:hover span.handle {
#tasklistsbox .treelist li a.subscribed {
display: inline-block;
position: absolute;
top: 6px;
right: 5px;
height: 16px;
width: 16px;
padding: 0;
background: url(sprites.png) -100px 0 no-repeat;
overflow: hidden;
text-indent: -5000px;
cursor: pointer;
}
#tasklistsbox .treelist div:hover > a.subscribed {
background-position: -2px -215px;
}
#tasklistsbox .treelist div.subscribed a.subscribed {
background-position: -20px -215px;
}
#tasklistsbox .treelist li div:hover > span.quickview {
background-position: -20px -101px;
}
#tasklists li.focusview span.handle {
#tasklistsbox .treelist li div.focusview > span.quickview {
background-position: -2px -101px;
}
#tasklists li.selected span.listname {
#tasklistsbox .searchresults .treelist li span.quickview {
display: none;
}
#tasklistsbox .treelist li.selected > div > span.listname {
font-weight: bold;
}
#tasklists li.readonly span.listname {
#tasklistsbox .treelist .readonly > span.listname {
background-position: right -142px;
}
#tasklists li.other span.listname {
#tasklistsbox .treelist .user > span.listname {
background-position: right -160px;
}
#tasklists li.other.readonly span.listname {
background-position: right -178px;
}
#tasklists li.shared span.listname {
background-position: right -196px;
}
#tasklists li.shared.readonly span.listname {
background-position: right -214px;
}
#tasklists li.virtual span.listname {
#tasklistsbox .treelist .virtual > span.listname {
color: #aaa;
top: 2px;
top: 4px;
left: 20px;
right: 5px;
}
#tasklists li.virtual span.handle {
background: none;
cursor: default;
#tasklistsbox .treelist.flat li span.calname {
left: 24px;
right: 22px;
}
#tasklists li input {
#tasklistsbox .treelist li input {
position: absolute;
top: 5px;
right: 5px;
left: 18px;
}
#tasklistsbox .treelist li .treetoggle {
top: 8px;
}
#tasklistsbox .treelist li.virtual > .treetoggle {
top: 6px;
}
#mainview-right {

View file

@ -24,9 +24,18 @@
</div>
<div id="tasklistsbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="tasklist.lists" /></h2>
<h2 class="boxtitle"><roundcube:label name="tasklist.lists" />
<a class="iconbutton search" title="<roundcube:label name='tasklist.findlists' />"></a>
</h2>
<div class="listsearchbox">
<div class="searchbox">
<input type="text" name="q" id="tasklistsearch" placeholder="<roundcube:label name='tasklist.findlists' />" />
<a class="iconbutton searchicon"></a>
<roundcube:button command="reset-listsearch" id="tasklistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
</div>
</div>
<div class="scroller withfooter">
<roundcube:object name="plugin.tasklists" id="tasklists" class="listing" />
<roundcube:object name="plugin.tasklists" id="tasklists" class="treelist listing" />
</div>
<div class="boxfooter">
<roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="UI.show_popup('tasklistoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" />
@ -171,6 +180,30 @@ $(document).ready(function(e){
orientation:'v', relative:true, start:240, min:180, size:12 }).init();
new rcube_splitter({ id:'taskviewsplitterv', p1:'#tagsbox', p2:'#tasklistsbox',
orientation:'h', relative:true, start:242, min:120, size:12, offset:4 }).init();
// animation to unfold list search box
$('#tasklistsbox .boxtitle a.search').click(function(e){
var box = $('#tasklistsbox .listsearchbox'),
dir = box.is(':visible') ? -1 : 1;
box.slideToggle({
duration: 160,
progress: function(animation, progress) {
if (dir < 0) progress = 1 - progress;
$('#tasklistsbox .scroller').css('top', (34 + 34 * progress) + 'px');
},
complete: function() {
box.toggleClass('expanded');
if (box.is(':visible')) {
box.find('input[type=text]').focus();
}
else {
$('#tasklistsearch-reset').click();
}
// TODO: save state in localStorage
}
});
});
});
</script>

View file

@ -79,6 +79,7 @@ function rcube_tasklist_ui(settings)
var scroll_speed = 20;
var scroll_sensitivity = 40;
var scroll_timer;
var tasklists_widget;
var me = this;
// general datepicker settings
@ -127,18 +128,80 @@ function rcube_tasklist_ui(settings)
{
// initialize task list selectors
for (var id in me.tasklists) {
if ((li = rcmail.get_folder_li(id, 'rcmlitasklist'))) {
init_tasklist_li(li, id);
}
if (me.tasklists[id].editable && (!me.selected_list || (me.tasklists[id].active && !me.tasklists[me.selected_list].active))) {
me.selected_list = id;
break;
}
}
// initialize treelist widget that controls the tasklists list
var widget_class = window.kolab_folderlist || rcube_treelist_widget;
tasklists_widget = new widget_class(rcmail.gui_objects.tasklistslist, {
id_prefix: 'rcmlitasklist',
selectable: true,
save_state: true,
searchbox: '#tasklistsearch',
search_action: 'tasks/tasklist',
search_sources: [ 'folders', 'users' ],
search_title: rcmail.gettext('listsearchresults','tasklist')
});
tasklists_widget.addEventListener('select', function(node) {
var id = $(this).data('id');
rcmail.enable_command('list-edit', 'list-remove', 'list-import', me.tasklists[node.id].editable);
me.selected_list = node.id;
});
tasklists_widget.addEventListener('subscribe', function(p) {
var list;
if ((list = me.tasklists[p.id])) {
list.subscribed = p.subscribed || false;
rcmail.http_post('tasklist', { action:'subscribe', l:{ id:p.id, active:list.active?1:0, permanent:list.subscribed?1:0 } });
}
});
tasklists_widget.addEventListener('insert-item', function(p) {
var list = p.data;
if (list && list.id && !list.virtual) {
me.tasklists[list.id] = list;
var prop = { id:p.id, active:list.active?1:0 };
if (list.subscribed) prop.permanent = 1;
rcmail.http_post('tasklist', { action:'subscribe', l:prop });
list_tasks();
}
});
// init (delegate) event handler on tasklist checkboxes
tasklists_widget.container.on('click', 'input[type=checkbox]', function(e){
var list, id = this.value;
if ((list = me.tasklists[id])) {
list.active = this.checked;
fetch_counts();
if (!this.checked) remove_tasks(id);
else list_tasks(null);
rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:list.active?1:0 } });
// disable focusview
if (!this.checked && focusview == id) {
set_focusview(null);
}
}
e.stopPropagation();
});
// handler for clicks on quickview buttons
tasklists_widget.container.on('click', '.quickview', function(e){
var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, '');
set_focusview(focusview == id ? null : id)
e.stopPropagation();
});
// register dbl-click handler to open calendar edit dialog
tasklists_widget.container.on('dblclick', ':not(.virtual) > .tasklist', function(e){
var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, '');
list_edit_dialog(id);
});
if (me.selected_list) {
rcmail.enable_command('addtask', true);
$(rcmail.get_folder_li(me.selected_list, 'rcmlitasklist')).click();
tasklists_widget.select(me.selected_list);
}
// register server callbacks
@ -1972,7 +2035,7 @@ function rcube_tasklist_ui(settings)
me.selected_list = id;
// click on handle icon toggles focusview
if (e.target.className == 'handle') {
if (e.target.className == 'quickview') {
set_focusview(focusview == id ? null : id)
}
// disable focusview when selecting another list
@ -1994,13 +2057,15 @@ function rcube_tasklist_ui(settings)
function set_focusview(id)
{
if (focusview && focusview != id)
$(rcmail.get_folder_li(focusview, 'rcmlitasklist')).removeClass('focusview');
$(tasklists_widget.get_item(focusview)).find('.tasklist').first().removeClass('focusview');
focusview = id;
var li = $(tasklists_widget.get_item(id)).find('.tasklist').first();
// activate list if necessary
if (focusview && !me.tasklists[id].active) {
$('input', rcmail.get_folder_li(id, 'rcmlitasklist')).get(0).checked = true;
li.find('input[type=checkbox]').get(0).checked = true;
me.tasklists[id].active = true;
fetch_counts();
}
@ -2009,7 +2074,7 @@ function rcube_tasklist_ui(settings)
list_tasks(null);
if (focusview) {
$(rcmail.get_folder_li(focusview, 'rcmlitasklist')).addClass('focusview');
li.addClass('focusview');
}
}

View file

@ -52,6 +52,7 @@ class tasklist extends rcube_plugin
public $driver;
public $timezone;
public $ui;
public $home; // declare public to be used in other classes
private $collapsed_tasks = array();
@ -134,8 +135,7 @@ class tasklist extends rcube_plugin
}
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
require_once($this->home . '/tasklist_ui.php');
$this->ui = new tasklist_ui($this);
$this->load_ui();
$this->ui->init();
}
@ -144,6 +144,16 @@ class tasklist extends rcube_plugin
$this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
}
/**
*
*/
private function load_ui()
{
if (!$this->ui) {
require_once($this->home . '/tasklist_ui.php');
$this->ui = new tasklist_ui($this);
}
}
/**
* Helper method to load the backend driver according to local config
@ -619,6 +629,30 @@ class tasklist extends rcube_plugin
if (($success = $this->driver->remove_list($list)))
$this->rc->output->command('plugin.destroy_tasklist', $list);
break;
case 'search':
$this->load_ui();
$results = array();
foreach ((array)$this->driver->search_lists(get_input_value('q', RCUBE_INPUT_GPC), get_input_value('source', RCUBE_INPUT_GPC)) as $id => $prop) {
$editname = $prop['editname'];
unset($prop['editname']); // force full name to be displayed
$prop['active'] = false;
// let the UI generate HTML and CSS representation for this calendar
$html = $this->ui->tasklist_list_item($id, $prop, $jsenv);
$prop += (array)$jsenv[$id];
$prop['editname'] = $editname;
$prop['html'] = $html;
$results[] = $prop;
}
// report more results available
if ($this->driver->search_more_results) {
$this->rc->output->show_message('autocompletemore', 'info');
}
$this->rc->output->command('multi_thread_http_response', $results, get_input_value('_reqid', RCUBE_INPUT_GPC));
return;
}
if ($success)
@ -875,6 +909,13 @@ class tasklist extends rcube_plugin
{
$this->ui->init();
$this->ui->init_templates();
// set autocompletion env
$this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
$this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
$this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
$this->rc->output->add_label('autocompletechars', 'autocompletemore');
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
}

View file

@ -81,6 +81,12 @@ class tasklist_ui
$this->plugin->include_script('jquery.tagedit.js');
$this->plugin->include_script('tasklist.js');
$this->rc->output->include_script('treelist.js');
// include kolab folderlist widget if available
if (is_readable($this->plugin->api->dir . 'libkolab/js/folderlist.js')) {
$this->plugin->api->include_script('libkolab/js/folderlist.js');
}
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tagedit.css');
}
@ -88,45 +94,110 @@ class tasklist_ui
/**
*
*/
function tasklists($attrib = array())
public function tasklists($attrib = array())
{
$lists = $this->plugin->driver->get_lists();
$tree = true;
$jsenv = array();
$lists = $this->plugin->driver->get_lists($tree);
$li = '';
foreach ((array)$lists as $id => $prop) {
if ($attrib['activeonly'] && !$prop['active'])
continue;
// walk folder tree
if (is_object($tree)) {
$html = $this->list_tree_html($tree, $lists, $jsenv, $attrib);
}
else {
// fall-back to flat folder listing
$attrib['class'] .= ' flat';
$html = '';
foreach ((array)$lists as $id => $prop) {
if ($attrib['activeonly'] && !$prop['active'])
continue;
$html .= html::tag('li', array(
'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
'class' => $prop['group'],
),
$this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly'])
);
}
}
$this->rc->output->set_env('tasklists', $jsenv);
$this->rc->output->add_gui_object('tasklistslist', $attrib['id']);
return html::tag('ul', $attrib, $html, html::$common_attrib);
}
/**
* Return html for a structured list <ul> for the folder tree
*/
public function list_tree_html($node, $data, &$jsenv, $attrib)
{
$out = '';
foreach ($node->children as $folder) {
$id = $folder->id;
$prop = $data[$id];
$is_collapsed = false; // TODO: determine this somehow?
$content = $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly']);
if (!empty($folder->children)) {
$content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
$this->list_tree_html($folder, $data, $jsenv, $attrib));
}
if (strlen($content)) {
$out .= html::tag('li', array(
'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
),
$content);
}
}
return $out;
}
/**
* Helper method to build a tasklist item (HTML content and js data)
*/
public function tasklist_list_item($id, $prop, &$jsenv, $activeonly = false)
{
// enrich list properties with settings from the driver
if (!$prop['virtual']) {
unset($prop['user_id']);
$prop['alarms'] = $this->plugin->driver->alarms;
$prop['undelete'] = $this->plugin->driver->undelete;
$prop['sortable'] = $this->plugin->driver->sortable;
$prop['attachments'] = $this->plugin->driver->attachments;
if (!$prop['virtual'])
$jsenv[$id] = $prop;
$html_id = html_identifier($id);
$class = 'tasks-' . asciiwords($id, true);
$title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
if ($prop['virtual'])
$class .= ' virtual';
else if (!$prop['editable'])
$class .= ' readonly';
if ($prop['class_name'])
$class .= ' '.$prop['class_name'];
$li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active']))) .
html::span(array('class' => 'handle', 'title' => $this->plugin->gettext('focusview')), '&nbsp;') .
html::span(array('class' => 'listname', 'title' => $title), $prop['listname']));
$jsenv[$id] = $prop;
}
$this->rc->output->set_env('tasklists', $jsenv);
$this->rc->output->add_gui_object('folderlist', $attrib['id']);
$classes = array('tasklist');
$title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
return html::tag('ul', $attrib, $li, html::$common_attrib);
if ($prop['virtual'])
$classes[] = 'virtual';
else if (!$prop['editable'])
$classes[] = 'readonly';
if ($prop['subscribed'])
$classes[] = 'subscribed';
if ($prop['class'])
$classes[] = $prop['class'];
if (!$activeonly || $prop['active']) {
return html::div(join(' ', $classes),
html::span(array('class' => 'listname', 'title' => $title), $prop['listname']) .
($prop['virtual'] ? '' :
html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), '&nbsp;') .
(isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe')), ' ') : '')
)
);
}
return '';
}