From 80ebe6f4451cf490c4fcaa4bf64c95ae219f002a Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Fri, 3 Jun 2011 18:23:17 +0200 Subject: [PATCH 1/4] Added generator for random event data; improved event display (with lots of events); save hidden calendars in user prefs --- plugins/calendar/calendar.js | 59 +++++++++++++++++---- plugins/calendar/calendar.php | 52 ++++++++++++++++++ plugins/calendar/lib/calendar_ui.php | 3 +- plugins/calendar/skins/default/calendar.css | 21 ++++++++ 4 files changed, 123 insertions(+), 12 deletions(-) diff --git a/plugins/calendar/calendar.js b/plugins/calendar/calendar.js index 42643573..005a6d86 100644 --- a/plugins/calendar/calendar.js +++ b/plugins/calendar/calendar.js @@ -30,7 +30,8 @@ function rcube_calendar(settings) this.alarm_ids = []; this.alarm_dialog = null; this.snooze_popup = null; - this.dismiss_link = null + this.dismiss_link = null; + this.eventcount = []; /*** private vars ***/ @@ -600,9 +601,11 @@ function rcube_calendar(settings) }; + /*** startup code ***/ + // create list of event sources AKA calendars this.calendars = {}; - var li, cal, event_sources = []; + var li, cal, active, event_sources = []; for (var id in rcmail.env.calendars) { cal = rcmail.env.calendars[id]; this.calendars[id] = $.extend({ @@ -611,17 +614,29 @@ function rcube_calendar(settings) className: 'fc-event-cal-'+id, id: id }, cal); - event_sources.push(this.calendars[id]); + + if ((active = ($.inArray(String(id), settings.hidden_calendars) < 0))) + event_sources.push(this.calendars[id]); // init event handler on calendar list checkbox if ((li = rcmail.get_folder_li(id, 'rcmlical'))) { $('#'+li.id+' input').click(function(e){ var id = $(this).data('id'); if (me.calendars[id]) { // add or remove event source on click - var action = this.checked ? 'addEventSource' : 'removeEventSource'; + var action; + if (this.checked) { + action = 'addEventSource'; + settings.hidden_calendars = $.map(settings.hidden_calendars, function(v){ return v == id ? null : v; }); + } + else { + action = 'removeEventSource'; + settings.hidden_calendars.push(id); + } $('#calendar').fullCalendar(action, me.calendars[id]); + rcmail.save_pref({ name:'hidden_calendars', value:settings.hidden_calendars.join(',') }); } - }).data('id', id); + }).data('id', id).get(0).checked = active; + $(li).click(function(e){ var id = $(this).data('id'); rcmail.select_folder(id, me.selected_calendar, 'rcmlical'); @@ -643,7 +658,7 @@ function rcube_calendar(settings) right: 'agendaDay,agendaWeek,month' }, aspectRatio: 1, - height: $(window).height() - 95, + height: $(window).height() - 96, eventSources: event_sources, monthNames : settings['months'], monthNamesShort : settings['months_short'], @@ -679,7 +694,23 @@ function rcube_calendar(settings) }, // event rendering eventRender: function(event, element, view) { - if(view.name != "month") { + element.attr('title', event.title); + if (view.name == 'month') { +/* attempt to limit the number of events displayed + (could also be used to init fish-eye-view) + var max = 4; // to be derrived from window size + var sday = event.start.getMonth()*12 + event.start.getDate(); + var eday = event.end.getMonth()*12 + event.end.getDate(); + if (!me.eventcount[sday]) me.eventcount[sday] = 1; + else me.eventcount[sday]++; + if (!me.eventcount[eday]) me.eventcount[eday] = 1; + else if (eday != sday) me.eventcount[eday]++; + + if (me.eventcount[sday] > max || me.eventcount[eday] > max) + return false; +*/ + } + else { if (event.location) { element.find('div.fc-event-title').after('
@ ' + Q(event.location) + '
'); } @@ -713,7 +744,7 @@ function rcube_calendar(settings) day_clicked_ts = now; }, // callback when a specific event is clicked - eventClick : function(event) { + eventClick: function(event) { event_show_dialog(event); }, // callback when an event was dragged and finally dropped @@ -734,7 +765,7 @@ function rcube_calendar(settings) rcmail.http_post('plugin.event', { action:'move', e:data }); }, // callback for event resizing - eventResize : function(event, delta) { + eventResize: function(event, delta) { // send resize request to server var data = { id: event.id, @@ -745,6 +776,13 @@ function rcube_calendar(settings) recurring_edit_confirm(data, 'resize'); else rcmail.http_post('plugin.event', { action:'resize', e:data }); + }, + viewDisplay: function(view) { + me.eventcount = []; + window.setTimeout(function(){ $('div.fc-content').css('overflow', view.name == 'month' ? 'auto' : 'hidden') }, 10); + }, + windowResize: function(view) { + me.eventcount = []; } }); @@ -912,10 +950,9 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // let's go var cal = new rcube_calendar(rcmail.env.calendar_settings); - cal.init_ui(); $(window).resize(function() { - $('#calendar').fullCalendar('option', 'height', $(window).height() - 95); + $('#calendar').fullCalendar('option', 'height', $(window).height() - 96); }).resize(); // show toolbar diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index c973d171..539e44c4 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -75,6 +75,7 @@ class calendar extends rcube_plugin $this->register_action('plugin.load_events', array($this, 'load_events')); $this->register_action('plugin.event', array($this, 'event')); $this->register_action('plugin.export_events', array($this, 'export_events')); + $this->register_action('plugin.randomdata', array($this, 'generate_randomdata')); $this->add_hook('keep_alive', array($this, 'keep_alive')); // set user's timezone @@ -472,6 +473,9 @@ class calendar extends rcube_plugin $this->rc->gettext('nov'), $this->rc->gettext('dec') ); $settings['today'] = rcube_label('today'); + + // user prefs + $settings['hidden_calendars'] = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); return $settings; } @@ -694,5 +698,53 @@ class calendar extends rcube_plugin 'u' => 'c', )); } + + /** + * TEMPORARY: generate random event data for testing + * Create events by opening http:///?_task=calendar&_action=plugin.randomdata&_num=500 + */ + public function generate_randomdata() + { + $cats = array_keys($this->driver->list_categories()); + $cals = $this->driver->list_calendars(); + $num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100; + + while ($count++ < $num) { + $start = round((time() + rand(-2600, 2600) * 1000) / 300) * 300; + $duration = round(rand(30, 360) / 30) * 30 * 60; + $allday = rand(0,20) > 18; + $alarm = rand(-30,12) * 5; + $fb = rand(0,2); + + if (date('G', $start) > 23) + $start -= 3600; + + if ($allday) { + $start = strtotime(date('Y-m-d 00:00:00', $start)); + $duration = 86399; + } + + $title = ''; + $len = rand(4, 40); + $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"; + for ($i = 0; $i < $len; $i++) + $title .= $chars[rand(0,strlen($chars)-1)]; + + $this->driver->new_event(array( + 'uid' => $this->generate_uid(), + 'start' => $start, + 'end' => $start + $duration, + 'allday' => $allday, + 'title' => $title, + 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'), + 'categories' => $cats[array_rand($cats)], + 'calendar' => array_rand($cals), + 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '', + 'priority' => 1, + )); + } + + $this->rc->output->redirect(''); + } } diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 8ccccc48..1f7348fc 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -126,6 +126,7 @@ class calendar_ui function calendar_list($attrib = array()) { $calendars = $this->calendar->driver->list_calendars(); + $hidden = explode(',', $this->rc->config->get('hidden_calendars', '')); $li = ''; foreach ((array)$calendars as $id => $prop) { @@ -137,7 +138,7 @@ class calendar_ui $html_id = html_identifier($id); $li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' =>'cal-' . asciiwords($id, true)), - html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => true), '') . html::span(null, Q($prop['name']))); + html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => !in_array($id, $hidden)), '') . html::span(null, Q($prop['name']))); } $this->rc->output->set_env('calendars', $jsenv); diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 4400c3a1..17215174 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -434,10 +434,31 @@ a.alarm-action-snooze:after { /* fullcalendar style overrides */ +.fc-content { + position: absolute !important; + top: 37px; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; +} + .fc-event-title { font-weight: bold; } +.fc-event-hori .fc-event-title { + font-weight: normal; + white-space: nowrap; +} + +.fc-event-hori .fc-event-time { + white-space: nowrap; + font-weight: normal; + font-size: 10px; + padding-right: 0.6em; +} + .fc-event-cateories { font-style:italic; } From 4d532e9a27e08460e3ffec59e04c2cb0b7aa1630 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Sat, 4 Jun 2011 15:52:12 -0400 Subject: [PATCH 2/4] Show events in clients timezone; only refetch events from modified source; maintain rc-specific patch for fullcalendar --- plugins/calendar/calendar.js | 10 ++++-- plugins/calendar/calendar.php | 22 +++++++------ .../drivers/database/database_driver.php | 2 +- plugins/calendar/lib/fullcalendar-rc.patch | 32 +++++++++++++++++++ plugins/calendar/lib/js/fullcalendar.js | 11 ++++--- plugins/calendar/skins/default/calendar.css | 2 +- 6 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 plugins/calendar/lib/fullcalendar-rc.patch diff --git a/plugins/calendar/calendar.js b/plugins/calendar/calendar.js index 005a6d86..199a373b 100644 --- a/plugins/calendar/calendar.js +++ b/plugins/calendar/calendar.js @@ -298,6 +298,7 @@ function rcube_calendar(settings) // post data to server var data = { + calendar: event.calendar, start: start.getTime()/1000, end: end.getTime()/1000, allday: allday.checked?1:0, @@ -309,7 +310,7 @@ function rcube_calendar(settings) priority: priority.val(), sensitivity: sensitivity.val(), recurrence: '', - alarms:'', + alarms: '' }; // serialize alarm settings @@ -658,6 +659,7 @@ function rcube_calendar(settings) right: 'agendaDay,agendaWeek,month' }, aspectRatio: 1, + ignoreTimezone: false, // will translate event dates to the client's timezone height: $(window).height() - 96, eventSources: event_sources, monthNames : settings['months'], @@ -755,6 +757,7 @@ function rcube_calendar(settings) // send move request to server var data = { id: event.id, + calendar: event.calendar, start: event.start.getTime()/1000, end: event.end.getTime()/1000, allday: allDay?1:0 @@ -769,8 +772,9 @@ function rcube_calendar(settings) // send resize request to server var data = { id: event.id, + calendar: event.calendar, start: event.start.getTime()/1000, - end: event.end.getTime()/1000, + end: event.end.getTime()/1000 }; if (event.recurrence) recurring_edit_confirm(data, 'resize'); @@ -945,7 +949,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // register callback commands rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); }); - rcmail.addEventListener('plugin.reload_calendar', function(){ $('#calendar').fullCalendar('refetchEvents'); }); + rcmail.addEventListener('plugin.reload_calendar', function(p){ $('#calendar').fullCalendar('refetchEvents', cal.calendars[p.source]); }); // let's go diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 539e44c4..40f00f0c 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -50,7 +50,7 @@ class calendar extends rcube_plugin } // load localizations - $this->add_texts('localization/', true); + $this->add_texts('localization/', $this->rc->action != 'plugin.event'); // load Calendar user interface which includes jquery-ui $this->require_plugin('jqueryui'); @@ -181,7 +181,7 @@ class calendar extends rcube_plugin 'title' => html::label($field_id, Q($this->gettext('default_view'))), 'content' => $select->show($this->rc->config->get('calendar_default_view', "agendaWeek")), ); - +/* $field_id = 'rcmfd_time_format'; $choices = array('HH:mm', 'H:mm', 'h:mmt'); $select = new html_select(array('name' => '_time_format', 'id' => $field_id)); @@ -190,7 +190,7 @@ class calendar extends rcube_plugin 'title' => html::label($field_id, Q($this->gettext('time_format'))), 'content' => $select->show($this->rc->config->get('calendar_time_format', "HH:mm")), ); - +*/ $field_id = 'rcmfd_timeslot'; $choices = array('1', '2', '3', '4', '6'); $select = new html_select(array('name' => '_timeslots', 'id' => $field_id)); @@ -217,8 +217,8 @@ class calendar extends rcube_plugin $field_id = 'rcmfd_alarm'; $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id)); $select_type->add($this->gettext('none'), ''); - $select_type->add($this->gettext('alarmdisplayoption'), 'DISPLAY'); - $select_type->add($this->gettext('alarmemailoption'), 'EMAIL'); + foreach ($this->driver->alarm_types as $type) + $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type); $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3)); $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset')); @@ -378,7 +378,7 @@ class calendar extends rcube_plugin $this->rc->output->show_message('calendar.errorsaving', 'error'); if ($success && $reload) - $this->rc->output->command('plugin.reload_calendar', array()); + $this->rc->output->command('plugin.reload_calendar', array('source' => $event['calendar'])); } /** @@ -504,7 +504,8 @@ class calendar extends rcube_plugin function fromGMT($datetime, $user_tz = true) { $tz = $user_tz ? $this->gmt_offset : date('Z'); - return strtotime($datetime) + $tz; + $ts = is_numeric($datetime) ? $datetime : strtotime($datetime); + return $ts + $tz; } /** @@ -725,17 +726,18 @@ class calendar extends rcube_plugin } $title = ''; - $len = rand(4, 40); + $len = rand(2, 12); + $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise."); $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"; for ($i = 0; $i < $len; $i++) - $title .= $chars[rand(0,strlen($chars)-1)]; + $title .= $words[rand(0,count($words)-1)] . " "; $this->driver->new_event(array( 'uid' => $this->generate_uid(), 'start' => $start, 'end' => $start + $duration, 'allday' => $allday, - 'title' => $title, + 'title' => rtrim($title), 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'), 'categories' => $cats[array_rand($cats)], 'calendar' => array_rand($cals), diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index 350304e4..ef807747 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -508,7 +508,7 @@ class database_driver extends calendar_driver $success = $this->rc->db->affected_rows($query); if ($success && $update_master) $this->_update_event($master, true); -console($savemode, $master['id'], $success); + return $success; } diff --git a/plugins/calendar/lib/fullcalendar-rc.patch b/plugins/calendar/lib/fullcalendar-rc.patch new file mode 100644 index 00000000..8b5337d1 --- /dev/null +++ b/plugins/calendar/lib/fullcalendar-rc.patch @@ -0,0 +1,32 @@ +--- js/fullcalendar.js.orig 2011-06-04 15:45:44.000000000 -0400 ++++ js/fullcalendar.js 2011-06-04 15:46:38.000000000 -0400 +@@ -500,8 +500,8 @@ + } + + +- function refetchEvents() { +- fetchEvents(currentView.visStart, currentView.visEnd); // will call reportEvents ++ function refetchEvents(source) { ++ fetchEvents(currentView.visStart, currentView.visEnd, source); // will call reportEvents + } + + +@@ -897,7 +897,7 @@ + } + + +- function fetchEvents(start, end) { ++ function fetchEvents(start, end, src) { + rangeStart = start; + rangeEnd = end; + cache = []; +@@ -905,7 +905,8 @@ + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i Date: Sun, 5 Jun 2011 19:08:47 -0600 Subject: [PATCH 3/4] Implement calendar operations (create/edit/remove) --- plugins/calendar/TODO | 2 +- plugins/calendar/calendar.js | 107 ++++++++++++++++-- plugins/calendar/calendar.php | 53 +++++++-- plugins/calendar/drivers/calendar_driver.php | 18 +++ .../drivers/database/database_driver.php | 76 ++++++++++++- plugins/calendar/lib/calendar_ui.php | 2 + plugins/calendar/lib/fullcalendar-rc.patch | 11 +- plugins/calendar/lib/js/fullcalendar.js | 2 +- .../calendar/lib/js/jquery.miniColors.min.js | 17 +++ plugins/calendar/localization/de_DE.inc | 2 +- plugins/calendar/localization/en_US.inc | 5 + plugins/calendar/skins/default/calendar.css | 11 +- .../skins/default/images/minicolors-all.png | Bin 0 -> 13370 bytes .../default/images/minicolors-handles.gif | Bin 0 -> 421 bytes .../skins/default/jquery.miniColors.css | 65 +++++++++++ .../skins/default/templates/calendar.html | 30 ++++- 16 files changed, 366 insertions(+), 35 deletions(-) create mode 100644 plugins/calendar/lib/js/jquery.miniColors.min.js create mode 100644 plugins/calendar/skins/default/images/minicolors-all.png create mode 100644 plugins/calendar/skins/default/images/minicolors-handles.gif create mode 100644 plugins/calendar/skins/default/jquery.miniColors.css diff --git a/plugins/calendar/TODO b/plugins/calendar/TODO index 2775202b..6646ce80 100644 --- a/plugins/calendar/TODO +++ b/plugins/calendar/TODO @@ -15,7 +15,7 @@ - List (Agenda) view - Individual days selection + Show list of calendars in a (hideable) drawer - - View: 3.1: Folder list + + View: 3.1: Folder list - View: 3.2: Add / Remove / Rename / Share Folders + View: 3.6: Combined calendar view (Turn calendars on/off) + View: 3.7: Small month overview calendar diff --git a/plugins/calendar/calendar.js b/plugins/calendar/calendar.js index 199a373b..929aa7a4 100644 --- a/plugins/calendar/calendar.js +++ b/plugins/calendar/calendar.js @@ -36,6 +36,7 @@ function rcube_calendar(settings) /*** private vars ***/ var me = this; + var fcselector = '#calendar'; var day_clicked = day_clicked_ts = 0; var ignore_click = false; @@ -158,14 +159,15 @@ function rcube_calendar(settings) minWidth: 320, width: 420 }).show(); - +/* + // add link for "more options" drop-down $('') .attr('href', '#') .html('More Options') .addClass('dropdown-link') .click(function(){ return false; }) .insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first()); - +*/ }; // bring up the event dialog (jquery-ui popup) @@ -461,7 +463,7 @@ function rcube_calendar(settings) ], close: function(){ $dialog.dialog("destroy").hide(); - $('#calendar').fullCalendar('refetchEvents'); + $(fcselector).fullCalendar('refetchEvents'); } }).show(); @@ -475,7 +477,7 @@ function rcube_calendar(settings) this.add_event = function() { if (this.selected_calendar) { var now = new Date(); - var date = $('#calendar').fullCalendar('getDate') || now; + var date = $(fcselector).fullCalendar('getDate') || now; date.setHours(now.getHours()+1); date.setMinutes(0); var end = new Date(date.getTime()); @@ -601,6 +603,82 @@ function rcube_calendar(settings) this.dismiss_link = null; }; + // opens a jquery UI dialog with event properties (or empty for creating a new calendar) + this.calendar_edit_dialog = function(calendar) + { + // close show dialog first + var $dialog = $("#calendarform").dialog('close'); + + if (!calendar) + calendar = { name:'', color:'cc0000' }; + + // reset form first + $('#calendarform > form').get(0).reset(); + + var name = $('#calendar-name').val(calendar.name); + var color = $('#calendar-color').val(calendar.color).miniColors('value', calendar.color); + + // dialog buttons + var buttons = {}; + + buttons[rcmail.gettext('save', 'calendar')] = function() { + // TODO: do some input validation + if (!name.val() || name.val().length < 2) { + alert(rcmail.gettext('invalidcalendarproperties', 'calendar')); + name.select(); + return; + } + + // post data to server + var data = { + name: name.val(), + color: color.val().replace(/^#/, '') + }; + if (calendar.id) + data.id = calendar.id; + + rcmail.http_post('plugin.calendar', { action:(calendar.id ? 'edit' : 'new'), c:data }); + $dialog.dialog("close"); + }; + + buttons[rcmail.gettext('cancel', 'calendar')] = function() { + $dialog.dialog("close"); + }; + + // open jquery UI dialog + $dialog.dialog({ + modal: true, + resizable: true, + title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'), + close: function() { + $dialog.dialog("destroy").hide(); + }, + buttons: buttons, + minWidth: 400, + width: 420 + }).show(); + + name.select(); + }; + + this.calendar_remove = function(calendar) + { + if (confirm(rcmail.gettext('deletecalendarconfirm', 'calendar'))) { + rcmail.http_post('plugin.calendar', { action:'remove', c:{ id:calendar.id } }); + return true; + } + return false; + }; + + this.calendar_destroy_source = function(id) + { + if (this.calendars[id]) { + $(fcselector).fullCalendar('removeEventSource', this.calendars[id]); + $(rcmail.get_folder_li(id, 'rcmlical')).remove(); + $('#edit-calendar option[value="'+id+'"]').remove(); + delete this.calendars[id]; + } + }; /*** startup code ***/ @@ -633,7 +711,7 @@ function rcube_calendar(settings) action = 'removeEventSource'; settings.hidden_calendars.push(id); } - $('#calendar').fullCalendar(action, me.calendars[id]); + $(fcselector).fullCalendar(action, me.calendars[id]); rcmail.save_pref({ name:'hidden_calendars', value:settings.hidden_calendars.join(',') }); } }).data('id', id).get(0).checked = active; @@ -641,6 +719,7 @@ function rcube_calendar(settings) $(li).click(function(e){ var id = $(this).data('id'); rcmail.select_folder(id, me.selected_calendar, 'rcmlical'); + rcmail.enable_command('plugin.calendar-edit','plugin.calendar-remove', true); me.selected_calendar = id; }).data('id', id); } @@ -652,7 +731,7 @@ function rcube_calendar(settings) } // initalize the fullCalendar plugin - $('#calendar').fullCalendar({ + $(fcselector).fullCalendar({ header: { left: 'prev,next today', center: 'title', @@ -802,7 +881,7 @@ function rcube_calendar(settings) var diff = (kw - base_kw) * 7 * 86400000; // select monday of the chosen calendar week var date = new Date(base_date.getTime() - day_off * 86400000 + diff); - $('#calendar').fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek'); + $(fcselector).fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek'); $("#datepicker").datepicker('setDate', date); window.setTimeout(init_week_events, 10); }).css('cursor', 'pointer'); @@ -817,7 +896,7 @@ function rcube_calendar(settings) onSelect: function(dateText, inst) { ignore_click = true; var d = $("#datepicker").datepicker('getDate'); //parse_datetime('0:0', dateText); - $('#calendar').fullCalendar('gotoDate', d).fullCalendar('select', d, d, true); + $(fcselector).fullCalendar('gotoDate', d).fullCalendar('select', d, d, true); window.setTimeout(init_week_events, 10); }, onChangeMonthYear: function(year, month, inst) { @@ -826,14 +905,14 @@ function rcube_calendar(settings) d.setYear(year); d.setMonth(month - 1); $("#datepicker").data('year', year).data('month', month); - //$('#calendar').fullCalendar('gotoDate', d).fullCalendar('setDate', d); + //$(fcselector).fullCalendar('gotoDate', d).fullCalendar('setDate', d); }, })); window.setTimeout(init_week_events, 10); // react on fullcalendar buttons var fullcalendar_update = function() { - var d = $('#calendar').fullCalendar('getDate'); + var d = $(fcselector).fullCalendar('getDate'); $("#datepicker").datepicker('setDate', d); window.setTimeout(init_week_events, 10); }; @@ -930,6 +1009,8 @@ function rcube_calendar(settings) $('#recurrence-form-'+freq+', #recurrence-form-until').show(); }); $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) }); + + $('#calendar-color').miniColors(); // hide event dialog when clicking somewhere into document $(document).bind('mousedown', dialog_check); @@ -942,6 +1023,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // configure toobar buttons rcmail.register_command('plugin.addevent', function(){ cal.add_event(); }, true); + + // configure list operations + rcmail.register_command('plugin.calendar-create', function(){ cal.calendar_edit_dialog(null); }, true); + rcmail.register_command('plugin.calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false); + rcmail.register_command('plugin.calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false); // export events rcmail.register_command('plugin.export', function(){ rcmail.goto_url('plugin.export_events', { source:cal.selected_calendar }); }, true); @@ -950,6 +1036,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // register callback commands rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); }); rcmail.addEventListener('plugin.reload_calendar', function(p){ $('#calendar').fullCalendar('refetchEvents', cal.calendars[p.source]); }); + rcmail.addEventListener('plugin.calendar_destroy_source', function(p){ cal.calendar_destroy_source(p.id); }); // let's go diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 40f00f0c..bec70d15 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -50,7 +50,7 @@ class calendar extends rcube_plugin } // load localizations - $this->add_texts('localization/', $this->rc->action != 'plugin.event'); + $this->add_texts('localization/', !$this->rc->action || $this->rc->task != 'calendar'); // load Calendar user interface which includes jquery-ui $this->require_plugin('jqueryui'); @@ -71,9 +71,9 @@ class calendar extends rcube_plugin // register calendar actions $this->register_action('index', array($this, 'calendar_view')); - $this->register_action('plugin.calendar', array($this, 'calendar_view')); + $this->register_action('plugin.event', array($this, 'event_action')); + $this->register_action('plugin.calendar', array($this, 'calendar_action')); $this->register_action('plugin.load_events', array($this, 'load_events')); - $this->register_action('plugin.event', array($this, 'event')); $this->register_action('plugin.export_events', array($this, 'export_events')); $this->register_action('plugin.randomdata', array($this, 'generate_randomdata')); $this->add_hook('keep_alive', array($this, 'keep_alive')); @@ -247,7 +247,7 @@ class calendar extends rcube_plugin $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name); $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category'))); $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30)); - $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => $field_class, 'size' => 6)); + $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6)); $categories_list .= html::div(null, $category_name->show($name) . ' ' . $category_color->show($color) . ' ' . $category_remove->show()); } @@ -261,16 +261,22 @@ class calendar extends rcube_plugin $p['blocks']['categories']['options']['categories'] = array( 'content' => $new_category->show('') . ' ' . $add_category->show(), ); - + $this->rc->output->add_script('function rcube_calendar_add_category(){ var name = $("#rcmfd_new_category").val(); if (name.length) { var input = $("").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name); - var color = $("").attr("type", "text").attr("name", "_colors[]").attr("size", 6).val("000000"); + var color = $("").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000"); var button = $("").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() }); $("
").append(input).append(" ").append(color).append(" ").append(button).appendTo("#calendarcategories"); + color.miniColors(); } }'); + + // include color picker + $this->include_script('lib/js/jquery.miniColors.min.js'); + $this->include_stylesheet('skins/' .$this->rc->config->get('skin') . '/jquery.miniColors.css'); + $this->rc->output->add_script('$("input.colors").miniColors()', 'docready'); } } @@ -334,10 +340,43 @@ class calendar extends rcube_plugin return $p; } + /** + * Dispatcher for calendar actions initiated by the client + */ + function calendar_action() + { + $action = get_input_value('action', RCUBE_INPUT_POST); + $cal = get_input_value('c', RCUBE_INPUT_POST); + $success = $reload = false; + + switch ($action) { + case "new": + $success = $this->driver->create_calendar($cal); + $reload = true; + break; + case "edit": + $success = $this->driver->edit_calendar($cal); + $reload = true; + break; + case "remove": + if ($success = $this->driver->remove_calendar($cal)) + $this->rc->output->command('plugin.calendar_destroy_source', array('id' => $cal['id'])); + break; + } + + if ($success) + $this->rc->output->show_message('successfullysaved', 'confirmation'); + else + $this->rc->output->show_message('calendar.errorsaving', 'error'); + + if ($success && $reload) + $this->rc->output->redirect(''); + } + /** * Dispatcher for event actions initiated by the client */ - function event() + function event_action() { $action = get_input_value('action', RCUBE_INPUT_POST); $event = get_input_value('e', RCUBE_INPUT_POST); diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index be50e300..b0687728 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -81,6 +81,24 @@ abstract class calendar_driver */ abstract function create_calendar($prop); + /** + * Update properties of an existing calendar + * + * @param array Hash array with calendar properties + * id: Calendar Identifier + * name: Calendar name + * color: The color of the calendar + * @return boolean True on success, Fales on failure + */ + abstract function edit_calendar($prop); + + /** + * Delete the given calendar with all its contents + * + * @return boolean True on success, Fales on failure + */ + abstract function remove_calendar($prop); + /** * Add a single event to the database * diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index ef807747..6394849a 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -75,7 +75,7 @@ class database_driver extends calendar_driver if (!empty($this->rc->user->ID)) { $calendar_ids = array(); $result = $this->rc->db->query( - "SELECT * FROM " . $this->db_calendars . " + "SELECT *, calendar_id AS id FROM " . $this->db_calendars . " WHERE user_id=?", $this->rc->user->ID ); @@ -121,10 +121,59 @@ class database_driver extends calendar_driver ); if ($result) - return $this->rc->db->insert_id($this->$sequence_calendars); + return $this->rc->db->insert_id($this->sequence_calendars); return false; } + + /** + * Update properties of an existing calendar + * + * @see calendar_driver::edit_calendar() + */ + public function edit_calendar($prop) + { + $query = $this->rc->db->query( + "UPDATE " . $this->db_calendars . " + SET name=?, color=? + WHERE calendar_id=? + AND user_id=?", + $prop['name'], + $prop['color'], + $prop['id'], + $this->rc->user->ID + ); + + return $this->rc->db->affected_rows($query); + } + + /** + * Delete the given calendar with all its contents + * + * @see calendar_driver::remove_calendar() + */ + public function remove_calendar($prop) + { + if (!$this->calendars[$prop['id']]) + return false; + + // delete all events of this calendar + $query = $this->rc->db->query( + "DELETE FROM " . $this->db_events . " + WHERE calendar_id=?", + $prop['id'] + ); + + // TODO: also delete linked attachments + + $query = $this->rc->db->query( + "DELETE FROM " . $this->db_calendars . " + WHERE calendar_id=?", + $prop['id'] + ); + + return $this->rc->db->affected_rows($query); + } /** * Add a single event to the database @@ -699,8 +748,15 @@ class database_driver extends calendar_driver */ public function remove_category($name) { - // TBD. alter events accordingly - return false; + $query = $this->rc->db->query( + "UPDATE " . $this->db_events . " + SET categories='' + WHERE categories=? + AND calendar_id IN (" . $this->calendar_ids . ")", + $name + ); + + return $this->rc->db->affected_rows($query); } /** @@ -708,8 +764,16 @@ class database_driver extends calendar_driver */ public function replace_category($oldname, $name, $color) { - // TBD. alter events accordingly - return false; + $query = $this->rc->db->query( + "UPDATE " . $this->db_events . " + SET categories=? + WHERE categories=? + AND calendar_id IN (" . $this->calendar_ids . ")", + $name, + $oldname + ); + + return $this->rc->db->affected_rows($query); } } diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 1f7348fc..27914e58 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -54,6 +54,7 @@ class calendar_ui { $skin = $this->rc->config->get('skin'); $this->calendar->include_stylesheet('skins/' . $skin . '/fullcalendar.css'); + $this->calendar->include_stylesheet('skins/' . $skin . '/jquery.miniColors.css'); } /** @@ -62,6 +63,7 @@ class calendar_ui public function addJS() { $this->calendar->include_script('lib/js/fullcalendar.js'); + $this->calendar->include_script('lib/js/jquery.miniColors.min.js'); $this->calendar->include_script('calendar.js'); } diff --git a/plugins/calendar/lib/fullcalendar-rc.patch b/plugins/calendar/lib/fullcalendar-rc.patch index 8b5337d1..3a7543d1 100644 --- a/plugins/calendar/lib/fullcalendar-rc.patch +++ b/plugins/calendar/lib/fullcalendar-rc.patch @@ -1,5 +1,5 @@ ---- js/fullcalendar.js.orig 2011-06-04 15:45:44.000000000 -0400 -+++ js/fullcalendar.js 2011-06-04 15:46:38.000000000 -0400 +--- js/fullcalendar.js.orig 2011-06-04 13:45:44.000000000 -0600 ++++ js/fullcalendar.js 2011-06-05 18:58:59.000000000 -0600 @@ -500,8 +500,8 @@ } @@ -11,7 +11,7 @@ } -@@ -897,7 +897,7 @@ +@@ -897,15 +897,16 @@ } @@ -20,9 +20,10 @@ rangeStart = start; rangeEnd = end; cache = []; -@@ -905,7 +905,8 @@ + var fetchID = ++currentFetchID; var len = sources.length; - pendingSourceCnt = len; +- pendingSourceCnt = len; ++ pendingSourceCnt = typeof src == 'undefined' ? len : 1; for (var i=0; i');trigger.insertAfter(input);input.addClass('miniColors').attr('maxlength',7).attr('autocomplete','off');input.data('trigger',trigger);input.data('hsb',hsb);if(o.change)input.data('change',o.change);if(o.readonly)input.attr('readonly',true);if(o.disabled)disable(input);trigger.bind('click.miniColors',function(event){event.preventDefault();input.trigger('focus');});input.bind('focus.miniColors',function(event){show(input);});input.bind('blur.miniColors',function(event){var hex=cleanHex(input.val());input.val(hex?'#'+hex:'');});input.bind('keydown.miniColors',function(event){if(event.keyCode===9)hide(input);});input.bind('keyup.miniColors',function(event){var filteredHex=input.val().replace(/[^A-F0-9#]/ig,'');input.val(filteredHex);if(!setColorFromInput(input)){input.data('trigger').css('backgroundColor','#FFF');}});input.bind('paste.miniColors',function(event){setTimeout(function(){input.trigger('keyup');},5);});};var destroy=function(input){hide();input=$(input);input.data('trigger').remove();input.removeAttr('autocomplete');input.removeData('trigger');input.removeData('selector');input.removeData('hsb');input.removeData('huePicker');input.removeData('colorPicker');input.removeData('mousebutton');input.removeData('moving');input.unbind('click.miniColors');input.unbind('focus.miniColors');input.unbind('blur.miniColors');input.unbind('keyup.miniColors');input.unbind('keydown.miniColors');input.unbind('paste.miniColors');$(document).unbind('mousedown.miniColors');$(document).unbind('mousemove.miniColors');};var enable=function(input){input.attr('disabled',false);input.data('trigger').css('opacity',1);};var disable=function(input){hide(input);input.attr('disabled',true);input.data('trigger').css('opacity',.5);};var show=function(input){if(input.attr('disabled'))return false;hide();var selector=$('
');selector.append('
');selector.append('
');selector.css({top:input.is(':visible')?input.offset().top+input.outerHeight():input.data('trigger').offset().top+input.data('trigger').outerHeight(),left:input.is(':visible')?input.offset().left:input.data('trigger').offset().left,display:'none'}).addClass(input.attr('class'));var hsb=input.data('hsb');selector.find('.miniColors-colors').css('backgroundColor','#'+hsb2hex({h:hsb.h,s:100,b:100}));var colorPosition=input.data('colorPosition');if(!colorPosition)colorPosition=getColorPositionFromHSB(hsb);selector.find('.miniColors-colorPicker').css('top',colorPosition.y+'px').css('left',colorPosition.x+'px');var huePosition=input.data('huePosition');if(!huePosition)huePosition=getHuePositionFromHSB(hsb);selector.find('.miniColors-huePicker').css('top',huePosition.y+'px');input.data('selector',selector);input.data('huePicker',selector.find('.miniColors-huePicker'));input.data('colorPicker',selector.find('.miniColors-colorPicker'));input.data('mousebutton',0);$('BODY').append(selector);selector.fadeIn(100);selector.bind('selectstart',function(){return false;});$(document).bind('mousedown.miniColors',function(event){input.data('mousebutton',1);if($(event.target).parents().andSelf().hasClass('miniColors-colors')){event.preventDefault();input.data('moving','colors');moveColor(input,event);} +if($(event.target).parents().andSelf().hasClass('miniColors-hues')){event.preventDefault();input.data('moving','hues');moveHue(input,event);} +if($(event.target).parents().andSelf().hasClass('miniColors-selector')){event.preventDefault();return;} +if($(event.target).parents().andSelf().hasClass('miniColors'))return;hide(input);});$(document).bind('mouseup.miniColors',function(event){input.data('mousebutton',0);input.removeData('moving');});$(document).bind('mousemove.miniColors',function(event){if(input.data('mousebutton')===1){if(input.data('moving')==='colors')moveColor(input,event);if(input.data('moving')==='hues')moveHue(input,event);}});};var hide=function(input){if(!input)input='.miniColors';$(input).each(function(){var selector=$(this).data('selector');$(this).removeData('selector');$(selector).fadeOut(100,function(){$(this).remove();});});$(document).unbind('mousedown.miniColors');$(document).unbind('mousemove.miniColors');};var moveColor=function(input,event){var colorPicker=input.data('colorPicker');colorPicker.hide();var position={x:event.clientX-input.data('selector').find('.miniColors-colors').offset().left+$(document).scrollLeft()-5,y:event.clientY-input.data('selector').find('.miniColors-colors').offset().top+$(document).scrollTop()-5};if(position.x<=-5)position.x=-5;if(position.x>=144)position.x=144;if(position.y<=-5)position.y=-5;if(position.y>=144)position.y=144;input.data('colorPosition',position);colorPicker.css('left',position.x).css('top',position.y).show();var s=Math.round((position.x+5)*.67);if(s<0)s=0;if(s>100)s=100;var b=100-Math.round((position.y+5)*.67);if(b<0)b=0;if(b>100)b=100;var hsb=input.data('hsb');hsb.s=s;hsb.b=b;setColor(input,hsb,true);};var moveHue=function(input,event){var huePicker=input.data('huePicker');huePicker.hide();var position={y:event.clientY-input.data('selector').find('.miniColors-colors').offset().top+$(document).scrollTop()-1};if(position.y<=-1)position.y=-1;if(position.y>=149)position.y=149;input.data('huePosition',position);huePicker.css('top',position.y).show();var h=Math.round((150-position.y-1)*2.4);if(h<0)h=0;if(h>360)h=360;var hsb=input.data('hsb');hsb.h=h;setColor(input,hsb,true);};var setColor=function(input,hsb,updateInputValue){input.data('hsb',hsb);var hex=hsb2hex(hsb);if(updateInputValue)input.val('#'+hex);input.data('trigger').css('backgroundColor','#'+hex);if(input.data('selector'))input.data('selector').find('.miniColors-colors').css('backgroundColor','#'+hsb2hex({h:hsb.h,s:100,b:100}));if(input.data('change')){input.data('change').call(input,'#'+hex,hsb2rgb(hsb));}};var setColorFromInput=function(input){var hex=cleanHex(input.val());if(!hex)return false;var hsb=hex2hsb(hex);var currentHSB=input.data('hsb');if(hsb.h===currentHSB.h&&hsb.s===currentHSB.s&&hsb.b===currentHSB.b)return true;var colorPosition=getColorPositionFromHSB(hsb);var colorPicker=$(input.data('colorPicker'));colorPicker.css('top',colorPosition.y+'px').css('left',colorPosition.x+'px');var huePosition=getHuePositionFromHSB(hsb);var huePicker=$(input.data('huePicker'));huePicker.css('top',huePosition.y+'px');setColor(input,hsb,false);return true;};var getColorPositionFromHSB=function(hsb){var x=Math.ceil(hsb.s/.67);if(x<0)x=0;if(x>150)x=150;var y=150-Math.ceil(hsb.b/.67);if(y<0)y=0;if(y>150)y=150;return{x:x-5,y:y-5};} +var getHuePositionFromHSB=function(hsb){var y=150-(hsb.h/2.4);if(y<0)h=0;if(y>150)h=150;return{y:y-1};} +var cleanHex=function(hex){hex=hex.replace(/[^A-Fa-f0-9]/,'');if(hex.length==3){hex=hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];} +return hex.length===6?hex:null;};var hsb2rgb=function(hsb){var rgb={};var h=Math.round(hsb.h);var s=Math.round(hsb.s*255/100);var v=Math.round(hsb.b*255/100);if(s==0){rgb.r=rgb.g=rgb.b=v;}else{var t1=v;var t2=(255-s)*v/255;var t3=(t1-t2)*(h%60)/60;if(h==360)h=0;if(h<60){rgb.r=t1;rgb.b=t2;rgb.g=t2+t3;} +else if(h<120){rgb.g=t1;rgb.b=t2;rgb.r=t1-t3;} +else if(h<180){rgb.g=t1;rgb.r=t2;rgb.b=t2+t3;} +else if(h<240){rgb.b=t1;rgb.r=t2;rgb.g=t1-t3;} +else if(h<300){rgb.b=t1;rgb.g=t2;rgb.r=t2+t3;} +else if(h<360){rgb.r=t1;rgb.g=t2;rgb.b=t1-t3;} +else{rgb.r=0;rgb.g=0;rgb.b=0;}} +return{r:Math.round(rgb.r),g:Math.round(rgb.g),b:Math.round(rgb.b)};};var rgb2hex=function(rgb){var hex=[rgb.r.toString(16),rgb.g.toString(16),rgb.b.toString(16)];$.each(hex,function(nr,val){if(val.length==1)hex[nr]='0'+val;});return hex.join('');};var hex2rgb=function(hex){var hex=parseInt(((hex.indexOf('#')>-1)?hex.substring(1):hex),16);return{r:hex>>16,g:(hex&0x00FF00)>>8,b:(hex&0x0000FF)};};var rgb2hsb=function(rgb){var hsb={h:0,s:0,b:0};var min=Math.min(rgb.r,rgb.g,rgb.b);var max=Math.max(rgb.r,rgb.g,rgb.b);var delta=max-min;hsb.b=max;hsb.s=max!=0?255*delta/max:0;if(hsb.s!=0){if(rgb.r==max){hsb.h=(rgb.g-rgb.b)/delta;}else if(rgb.g==max){hsb.h=2+(rgb.b-rgb.r)/delta;}else{hsb.h=4+(rgb.r-rgb.g)/delta;}}else{hsb.h=-1;} +hsb.h*=60;if(hsb.h<0){hsb.h+=360;} +hsb.s*=100/255;hsb.b*=100/255;return hsb;};var hex2hsb=function(hex){var hsb=rgb2hsb(hex2rgb(hex));if(hsb.s===0)hsb.h=360;return hsb;};var hsb2hex=function(hsb){return rgb2hex(hsb2rgb(hsb));};switch(o){case'readonly':$(this).each(function(){$(this).attr('readonly',data);});return $(this);break;case'disabled':$(this).each(function(){if(data){disable($(this));}else{enable($(this));}});return $(this);case'value':$(this).each(function(){$(this).val(data).trigger('keyup');});return $(this);break;case'destroy':$(this).each(function(){destroy($(this));});return $(this);default:if(!o)o={};$(this).each(function(){if($(this)[0].tagName.toLowerCase()!=='input')return;if($(this).data('trigger'))return;create($(this),o,data);});return $(this);}}});})(jQuery); \ No newline at end of file diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc index f9d0881d..c1a6e0d4 100644 --- a/plugins/calendar/localization/de_DE.inc +++ b/plugins/calendar/localization/de_DE.inc @@ -17,7 +17,7 @@ $labels = array(); // config $labels['default_view'] = 'Ansicht'; $labels['time_format'] = 'Zeitformatierung'; -$labels['timeslots'] = 'Zeitfenster pro Stunde'; +$labels['timeslots'] = 'Zeitraster pro Stunde'; $labels['first_day'] = 'Erster Wochentag'; // calendar diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 8872f798..f44f5317 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -17,6 +17,9 @@ $labels['calendars'] = 'Calendars'; $labels['category'] = 'Category'; $labels['categories'] = 'Categories'; $labels['createcalendar'] = 'Create new calendar'; +$labels['editcalendar'] = 'Edit calendar properties'; +$labels['name'] = 'Name'; +$labels['color'] = 'Color'; $labels['day'] = 'Day'; $labels['week'] = 'Week'; $labels['month'] = 'Month'; @@ -87,9 +90,11 @@ $labels['tabattachments'] = 'Attachments'; // messages $labels['deleteventconfirm'] = "Do you really want to delete this event?"; +$labels['deletecalendarconfirm'] = "Do you really want to delete this calendar with all its events?"; $labels['errorsaving'] = "Failed to save changes"; $labels['operationfailed'] = "The requested operation failed"; $labels['invalideventdates'] = "Invalid dates entered! Please check your input."; +$labels['invalidcalendarproperties'] = "Invalid calendar properties! Please set a valid name."; // recurrence form $labels['repeat'] = 'Repeat'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 975b9d63..56846e93 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -207,7 +207,8 @@ pre { } #eventshow, -#eventedit { +#eventedit, +#calendarform { display: none; } @@ -219,6 +220,10 @@ pre { text-align: center; } +a.miniColors-trigger { + margin-top: -3px; +} + /* jQuery UI overrides */ #eventshow h1 { @@ -256,6 +261,7 @@ pre { border-radius: 0; } +div.form-section, #eventshow div.event-section, #eventtabs div.event-section { margin-top: 0.2em; @@ -287,7 +293,8 @@ pre { } #eventshow label, -#eventedit label { +#eventedit label, +.form-section label { display: inline-block; min-width: 7em; padding-right: 0.5em; diff --git a/plugins/calendar/skins/default/images/minicolors-all.png b/plugins/calendar/skins/default/images/minicolors-all.png new file mode 100644 index 0000000000000000000000000000000000000000..001ed888c232598a57e83ea746ecab7b6772674d GIT binary patch literal 13370 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 P6*7I245

- - + +
@@ -27,6 +27,13 @@
+
+
    +
  • +
  • +
+
+

Event Title

Location
@@ -163,9 +170,24 @@
+ +
+
+
+ + +
+
+ + +
+
+
+
+
@@ -183,6 +205,10 @@