diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 116b490e..2a8a8d2b 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -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; diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 28eb8ba5..974a3d3d 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -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(); diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 984ce03b..117b9242 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -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; } /** diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index f47032fe..1ffaea8b 100644 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -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%; diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html index debe7ec4..0842cf03 100644 --- a/plugins/calendar/skins/larry/templates/calendar.html +++ b/plugins/calendar/skins/larry/templates/calendar.html @@ -23,8 +23,15 @@