Render calendar folders as a searchable treelist widget

This commit is contained in:
Thomas Bruederli 2014-05-12 20:47:47 +02:00
parent d2d831b775
commit 00b1c7631b
7 changed files with 283 additions and 114 deletions

View file

@ -51,6 +51,7 @@ function rcube_calendar_ui(settings)
var ignore_click = false;
var event_defaults = { free_busy:'busy', alarms:'' };
var event_attendees = [];
var calendars_list;
var attendees_list;
var resources_list;
var resources_treelist;
@ -2658,15 +2659,10 @@ function rcube_calendar_ui(settings)
// mark the given calendar folder as selected
this.select_calendar = function(id)
{
var prefix = 'rcmlical';
$(rcmail.gui_objects.calendarslist).find('li.selected')
.removeClass('selected').addClass('unfocused');
$('#' + prefix + id, rcmail.gui_objects.calendarslist)
.removeClass('unfocused').addClass('selected');
calendars_list.select(id);
// trigger event hook
rcmail.triggerEvent('selectfolder', { folder:name, prefix:prefix });
rcmail.triggerEvent('selectfolder', { folder:id, prefix:'rcmlical' });
this.selected_calendar = id;
};
@ -2703,42 +2699,58 @@ function rcube_calendar_ui(settings)
event_sources.push(this.calendars[id]);
}
// init event handler on calendar list checkbox
if ((li = rcube_find_object('rcmlical' + id))) {
$('#'+li.id+' input').click(function(e){
var id = $(this).data('id');
if (me.calendars[id]) { // add or remove event source on click
var action;
if (this.checked) {
action = 'addEventSource';
me.calendars[id].active = true;
}
else {
action = 'removeEventSource';
me.calendars[id].active = false;
}
// add/remove event source
fc.fullCalendar(action, me.calendars[id]);
rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
}
}).data('id', id).get(0).checked = active;
$(li).click(function(e){
me.select_calendar($(this).data('id'));
rcmail.enable_command('calendar-edit', true);
rcmail.enable_command('calendar-remove', 'calendar-showurl', true);
})
.dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); })
.data('id', id);
}
// check active calendars
$('#rcmlical'+id+' > .calendar input').data('id', id).get(0).checked = active;
if (!cal.readonly && !this.selected_calendar) {
this.selected_calendar = id;
rcmail.enable_command('addevent', true);
}
}
// initialize treelist widget that controls the calendars list
calendars_list = new rcube_treelist_widget(rcmail.gui_objects.calendarslist, {
id_prefix: 'rcmlical',
selectable: true,
searchbox: '#calendarlistsearch'
});
calendars_list.addEventListener('select', function(node){
me.select_calendar(node.id);
rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
});
calendars_list.addEventListener('search', function(search){
console.log(search);
});
// init (delegate) event handler on calendar list checkboxes
$(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
var id = $(this).data('id');
if (me.calendars[id]) { // add or remove event source on click
var action;
if (this.checked) {
action = 'addEventSource';
me.calendars[id].active = true;
}
else {
action = 'removeEventSource';
me.calendars[id].active = false;
}
// add/remove event source
fc.fullCalendar(action, me.calendars[id]);
rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
e.stopPropagation();
}
});
// register dbl-click handler to open calendar edit dialog
$(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e){
var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
me.calendar_edit_dialog(me.calendars[id]);
});
// select default calendar
if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly)
this.selected_calendar = settings.default_calendar;

View file

@ -88,16 +88,16 @@ class kolab_driver extends calendar_driver
return $this->calendars;
}
/**
* Get a list of available calendars from this source
*
* @param bool $active Return only active calendars
* @param bool $personal Return only personal calendars
* @param object $tree Reference to hierarchical folder tree object
*
* @return array List of calendars
*/
public function list_calendars($active = false, $personal = false)
public function list_calendars($active = false, $personal = false, &$tree = null)
{
// attempt to create a default calendar for this user
if (!$this->has_writeable) {
@ -112,7 +112,7 @@ class kolab_driver extends calendar_driver
// include virtual folders for a full folder tree
if (!$active && !$personal && !$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
$folders = kolab_storage::folder_hierarchy($folders);
$folders = kolab_storage::folder_hierarchy($folders, $tree);
foreach ($folders as $id => $cal) {
$fullname = $cal->get_name();
@ -124,6 +124,7 @@ class kolab_driver extends calendar_driver
'id' => $cal->id,
'name' => $fullname,
'listname' => $listname,
'editname' => $cal->get_foldername(),
'virtual' => true,
'readonly' => true,
);
@ -1106,6 +1107,11 @@ class kolab_driver extends calendar_driver
*/
public function calendar_form($action, $calendar, $formfields)
{
// show default dialog for birthday calendar
if ($calendar['id'] == self::BIRTHDAY_CALENDAR_ID) {
return parent::calendar_form($action, $calendar, $formfields);
}
if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();

View file

@ -187,45 +187,108 @@ class calendar_ui
*/
function calendar_list($attrib = array())
{
$calendars = $this->cal->driver->list_calendars();
$html = '';
$jsenv = array();
$calendars = $this->cal->driver->list_calendars(false, false, $tree);
// walk folder tree
if (is_object($tree)) {
$html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
// append birthdays calendar which isn't part of $tree
if ($bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID]) {
$calendars = array(calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal);
}
else {
$calendars = array(); // clear array for flat listing
}
}
else {
// fall-back to flat folder listing
$attrib['class'] .= ' flat';
}
$li = '';
foreach ((array)$calendars as $id => $prop) {
if ($attrib['activeonly'] && !$prop['active'])
continue;
unset($prop['user_id']);
$prop['alarms'] = $this->cal->driver->alarms;
$prop['attendees'] = $this->cal->driver->attendees;
$prop['freebusy'] = $this->cal->driver->freebusy;
$prop['attachments'] = $this->cal->driver->attachments;
$prop['undelete'] = $this->cal->driver->undelete;
$prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
if (!$prop['virtual'])
$jsenv[$id] = $prop;
$html_id = html_identifier($id);
$class = 'cal-' . 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['readonly'])
$class .= ' readonly';
if ($prop['class_name'])
$class .= ' '.$prop['class_name'];
$li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' => $class),
($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
html::span('handle', ' ')) .
html::span(array('class' => 'calname', 'title' => $title), $prop['listname']));
$html .= html::tag('li', array('id' => 'rcmlical' . rcube_utils::html_identifier($id)),
$content = $this->calendar_list_item($id, $prop, $jsenv)
);
}
$this->rc->output->set_env('calendars', $jsenv);
$this->rc->output->add_gui_object('calendarslist', $attrib['id']);
return html::tag('ul', $attrib, $li, html::$common_attrib);
return html::tag('ul', $attrib, $html, html::$common_attrib);
}
/**
* Return html for a structured list <ul> for the mailbox tree
*/
public function list_tree_html(&$node, &$data, &$jsenv, $attrib)
{
$out = '';
foreach ($node->children as $folder) {
$id = $folder->id;
$prop = $data[$id];
$content = $this->calendar_list_item($id, $prop, $jsenv);
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' => 'rcmlical' . rcube_utils::html_identifier($id),
'class' => $prop['virtual'] ? 'virtual' : '',
),
$content);
}
}
return $out;
}
/**
* Helper method to build a calendar list item (HTML content and js data)
*/
protected function calendar_list_item($id, $prop, &$jsenv)
{
unset($prop['user_id']);
$prop['alarms'] = $this->cal->driver->alarms;
$prop['attendees'] = $this->cal->driver->attendees;
$prop['freebusy'] = $this->cal->driver->freebusy;
$prop['attachments'] = $this->cal->driver->attachments;
$prop['undelete'] = $this->cal->driver->undelete;
$prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
if (!$prop['virtual'])
$jsenv[$id] = $prop;
$class = 'calendar cal-' . asciiwords($id, true);
$title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
$is_collapsed = false; // TODO: determine this somehow?
if ($prop['virtual'])
$class = 'folder virtual';
else if ($prop['readonly'])
$class .= ' readonly';
if ($prop['class_name'])
$class .= ' '.$prop['class_name'];
$content = '';
if (!$attrib['activeonly'] || $prop['active']) {
$content = html::div($class,
($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
html::span('handle', ' ')) .
html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname'])
);
}
return $content;
}
/**

View file

@ -164,45 +164,64 @@ pre {
right: 0;
}
#calendarslist li {
margin: 0;
height: 20px;
padding: 6px 8px 2px;
display: block;
position: relative;
#calendars .scroller {
top: 68px;
}
#calendarslist li.virtual {
height: 12px;
#calendarslist li {
margin: 0;
position: relative;
}
#calendarslist li label {
display: block;
}
#calendarslist li div.folder,
#calendarslist li div.calendar {
position: relative;
height: 28px;
}
#calendarslist li div.virtual {
height: 22px;
}
#calendarslist li span.calname {
display: block;
padding: 0px 30px 2px 2px;
position: absolute;
top: 6px;
left: 26px;
right: 24px;
top: 7px;
left: 38px;
right: 22px;
cursor: default;
background: url(images/calendars.png) right 20px no-repeat;
padding-bottom: 2px;
padding-right: 30px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #004458;
}
#calendarslist li div.virtual > span.calname {
color: #aaa;
top: 4px;
left: 20px;
}
#calendarslist.flat li span.calname {
left: 24px;
}
#calendarslist li span.handle {
display: inline-block;
position: absolute;
top: 8px;
right: 6px;
padding: 0;
border-radius: 7px;
margin-right: 6px;
width: 10px;
height: 10px;
border-radius: 7px;
font-size: 0.8em;
border: 1px solid rgba(0, 0, 0, 0.5);
-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
@ -212,43 +231,65 @@ pre {
#calendarslist li input {
position: absolute;
top: 4px;
right: 5px;
top: 5px;
left: 18px;
}
#calendarslist li div.treetoggle {
top: 8px;
}
#calendarslist li.virtual div.treetoggle {
top: 6px;
}
#calendarslist.flat li input {
left: 4px;
}
#calendarslist ul li div.folder,
#calendarslist ul li div.calendar {
margin-left: 16px;
}
#calendarslist ul ul li div.folder,
#calendarslist ul ul li div.calendar {
margin-left: 32px;
}
#calendarslist ul ul ul li div.folder,
#calendarslist ul ul ul li div.calendar {
margin-left: 48px;
}
#calendarslist li.selected {
background-color: #c7e3ef;
}
#calendarslist li.selected span.calname {
#calendarslist li.selected > span.calname {
font-weight: bold;
}
#calendarslist li.readonly span.calname {
#calendarslist div.readonly span.calname {
background-position: right -20px;
}
#calendarslist li.other span.calname {
#calendarslist div.other span.calname {
background-position: right -38px;
}
#calendarslist li.other.readonly span.calname {
#calendarslist div.other.readonly span.calname {
background-position: right -56px;
}
#calendarslist li.shared span.calname {
#calendarslist div.shared span.calname {
background-position: right -74px;
}
#calendarslist li.shared.readonly span.calname {
#calendarslist div.shared.readonly span.calname {
background-position: right -92px;
}
#calendarslist li.virtual span.calname {
color: #aaa;
top: 2px;
}
#calfeedurl,
#caldavurl {
width: 98%;

View file

@ -23,8 +23,15 @@
<div id="calendars" class="uibox listbox" style="visibility:hidden">
<h2 class="boxtitle"><roundcube:label name="calendar.calendars" /></h2>
<div class="listsearchbox">
<div class="searchbox">
<input type="text" name="q" id="calendarlistsearch" />
<a class="iconbutton searchicon"></a>
<roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
</div>
</div>
<div class="scroller withfooter">
<roundcube:object name="plugin.calendar_list" id="calendarslist" class="listing" />
<roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing" />
</div>
<div class="boxfooter">
<roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('calendaroptionsmenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" />

View file

@ -756,43 +756,54 @@ class kolab_storage
* Check the folder tree and add the missing parents as virtual folders
*
* @param array $folders Folders list
* @param object $tree Reference to the root node of the folder tree
*
* @return array Folders list
* @return array Flat folders list
*/
public static function folder_hierarchy($folders)
public static function folder_hierarchy($folders, &$tree)
{
$_folders = array();
$existing = array_map(function($folder){ return $folder->get_name(); }, $folders);
$delim = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
$tree = new virtual_kolab_storage_folder('', '<root>', ''); // create tree root
$refs = array('' => $tree);
foreach ($folders as $idx => $folder) {
$path = explode($delim, $folder->name);
array_pop($path);
$folder->parent = join($delim, $path);
$folder->children = array(); // reset list
// skip top folders or ones with a custom displayname
if (count($path) <= 1 || kolab_storage::custom_displayname($folder->name)) {
if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
$tree->children[] = $folder;
}
else {
$parents = array();
$depth = $folder->get_namespace() == 'personal' ? 1 : 2;
while (count($path) > 1 && ($parent = join($delim, $path))) {
$name = kolab_storage::object_name($parent, $folder->get_namespace());
if (!in_array($name, $existing)) {
$parents[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace());
$existing[] = $name;
}
while (count($path) >= $depth && ($parent = join($delim, $path))) {
array_pop($path);
$name = kolab_storage::object_name($parent, $folder->get_namespace());
if (!$refs[$parent]) {
$refs[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace(), join($delim, $path));
$parents[] = $refs[$parent];
}
}
if (!empty($parents)) {
$parents = array_reverse(array_values($parents));
$parents = array_reverse($parents);
foreach ($parents as $parent) {
$parent_node = $refs[$parent->parent] ?: $tree;
$parent_node->children[] = $parent;
$_folders[] = $parent;
}
}
$parent_node = $refs[$folder->parent] ?: $tree;
$parent_node->children[] = $folder;
}
$refs[$folder->name] = $folder;
$_folders[] = $folder;
unset($folders[$idx]);
}
@ -1164,13 +1175,18 @@ class virtual_kolab_storage_folder
public $id;
public $name;
public $namespace;
public $parent = '';
public $children = array();
public $virtual = true;
protected $displayname;
public function __construct($realname, $name, $ns)
public function __construct($name, $dispname, $ns, $parent = '')
{
$this->id = kolab_storage::folder_id($realname);
$this->id = kolab_storage::folder_id($name);
$this->name = $name;
$this->namespace = $ns;
$this->parent = $parent;
$this->displayname = $dispname;
}
public function get_namespace()
@ -1181,6 +1197,12 @@ class virtual_kolab_storage_folder
public function get_name()
{
// this is already kolab_storage::object_name() result
return $this->name;
return $this->displayname;
}
public function get_foldername()
{
$parts = explode('/', $this->name);
return rcube_charset::convert(end($parts), 'UTF7-IMAP');
}
}

View file

@ -47,6 +47,12 @@ class kolab_storage_folder
* @var object
*/
public $cache;
/**
* List of direct child folders
* @var array
*/
public $children = array();
private $type_annotation;
private $namespace;
@ -217,6 +223,18 @@ class kolab_storage_folder
}
/**
* Getter for the top-end folder name (not the entire path)
*
* @return string Name of this folder
*/
public function get_foldername()
{
$parts = explode('/', $this->name);
return rcube_charset::convert(end($parts), 'UTF7-IMAP');
}
/**
* Get the color value stored in metadata
*