diff --git a/calendar/inc/class.calendar_ui.inc.php b/calendar/inc/class.calendar_ui.inc.php
index c35033c498..6860f64731 100644
--- a/calendar/inc/class.calendar_ui.inc.php
+++ b/calendar/inc/class.calendar_ui.inc.php
@@ -710,17 +710,17 @@ class calendar_ui
),
array(
'text' => lang('planner by category'),
- 'value' => '{"view":"planner", "menuaction":"calendar.calendar_uiviews.planner","sortby":"category"}',
+ 'value' => '{"view":"planner", "sortby":"category"}',
'selected' => $this->view == 'planner' && $this->sortby != 'user',
),
array(
'text' => lang('planner by user'),
- 'value' => '{"view":"planner", "menuaction":"calendar.calendar_uiviews.planner","sortby":"user"}',
+ 'value' => '{"view":"planner","sortby":"user"}',
'selected' => $this->view == 'planner' && $this->sortby == 'user',
),
array(
'text' => lang('yearly planner'),
- 'value' => '{"view":"planner", "menuaction":"calendar.calendar_uiviews.planner","sortby":"month"}',
+ 'value' => '{"view":"planner","sortby":"month"}',
'selected' => $this->view == 'planner' && $this->sortby == 'month',
),
array(
diff --git a/calendar/inc/class.calendar_uiviews.inc.php b/calendar/inc/class.calendar_uiviews.inc.php
index 2093c828be..195f9c06f5 100644
--- a/calendar/inc/class.calendar_uiviews.inc.php
+++ b/calendar/inc/class.calendar_uiviews.inc.php
@@ -211,8 +211,24 @@ class calendar_uiviews extends calendar_ui
/**
* Show the last view or the default one, if no last
*/
- function index()
+ function index($content)
{
+ if($content['merge'])
+ {
+ // View from sidebox is JSON encoded
+ $this->manage_states(array_merge($content,json_decode($content['view'],true)));
+ if($content['first'])
+ {
+ $this->first = egw_time::to($content['first'],'ts');
+ }
+ if($content['last'])
+ {
+ $this->last = egw_time::to($content['last'],'ts');
+ }
+ $_GET['merge'] = $content['merge'];
+ $this->merge();
+ return;
+ }
if (!$this->view) $this->view = 'week';
// handle views in other files
@@ -240,6 +256,11 @@ class calendar_uiviews extends calendar_ui
// Actually, this takes care of most of it...
$this->week();
+
+ $tmpl = new etemplate_new('calendar.planner');
+ // Get the actions
+ $tmpl->setElementAttribute('planner','actions',$this->get_actions());
+ $tmpl->exec('calendar_uiviews::index',array());
// List view in a separate file
$list_ui = new calendar_uilist();
diff --git a/calendar/js/app.js b/calendar/js/app.js
index 2cb7ac681b..4c16a8d8d8 100644
--- a/calendar/js/app.js
+++ b/calendar/js/app.js
@@ -12,6 +12,7 @@
/*egw:uses
/etemplate/js/etemplate2.js;
/calendar/js/et2_widget_timegrid.js;
+ /calendar/js/et2_widget_planner.js;
*/
/**
@@ -119,6 +120,10 @@ app.classes.calendar = AppJS.extend(
set_start_date: function(state) {
var d = state.date ? new Date(state.date) : new Date();
d.setUTCDate(1);
+ d.setUTCHours(0);
+ d.setUTCMinutes(0);
+ d.setUTCSeconds(0);
+ state.date = d.toJSON();
return app.calendar.date.start_of_week(d);
},
set_end_date: function(state) {
@@ -130,7 +135,76 @@ app.classes.calendar = AppJS.extend(
return week_start;
},
},
- listview: {etemplates: ['calendar.list']}
+
+ planner: {
+ etemplates: ['calendar.planner'],
+ set_group_by: function(state) {
+ return state.cat_id? state.cat_id : (state.sortby ? state.sortby : 0);
+ },
+ set_start_date: function(state) {
+ var d = state.date ? new Date(state.date) : new Date();
+ if(state.sortby && state.sortby === 'month')
+ {
+ d.setUTCDate(1);
+ }
+ else if (!state.planner_days)
+ {
+ if(d.getUTCDate() < 15)
+ {
+ d.setUTCDate(1);
+ return app.calendar.date.start_of_week(d);
+ }
+ else
+ {
+ return app.calendar.date.start_of_week(d);
+ }
+ }
+ return d;
+ },
+ set_end_date: function(state) {
+ var d = state.date ? new Date(state.date) : new Date();
+ if(state.sortby && state.sortby === 'month')
+ {
+ d.setUTCDate(0);
+ d.setUTCFullYear(d.getUTCFullYear() + 1);
+ }
+ else if (state.planner_days)
+ {
+ d.setUTCDate(d.getUTCDate() + parseInt(state.planner_days)-1);
+ }
+ else if (app.calendar.state.last)
+ {
+ d = new Date(app.calendar.state.last);
+ }
+ else if (!state.planner_days)
+ {
+ if (d.getUTCDate() < 15)
+ {
+ d.setUTCDate(0);
+ d.setUTCMonth(d.getUTCMonth()+1);
+ d = app.calendar.date.end_of_week(d);
+ }
+ else
+ {
+ d.setUTCMonth(d.getUTCMonth()+1);
+ d = app.calendar.date.end_of_week(d);
+ }
+ }
+ return d;
+ },
+ set_owner: function(state) {
+ return state.owner || 0;
+ }
+ },
+
+ listview: {
+ etemplates: ['calendar.list'],
+ set_start_date: function(state)
+ {
+ var d = state.date ? new Date(state.date) : new Date();
+ return d;
+ }
+ }
},
/**
@@ -162,6 +236,9 @@ app.classes.calendar = AppJS.extend(
//Drag_n_Drop (need to wait for DOM ready to init dnd)
jQuery(jQuery.proxy(this.drag_n_drop,this));
+
+ // Scroll
+ jQuery(jQuery.proxy(this._scroll,this));
},
/**
@@ -177,6 +254,7 @@ app.classes.calendar = AppJS.extend(
{
delete window.top.app.calendar;
}
+ jQuery(egw_getFramework().applications.calendar.tab.contentDiv).off();
},
/**
@@ -543,6 +621,55 @@ app.classes.calendar = AppJS.extend(
}
},
+ /**
+ * Bind scroll event
+ * When the user scrolls, we'll move enddate - startdate days
+ */
+ _scroll: function() {
+ // Bind only once, to the whole tab
+ jQuery(egw_getFramework().applications.calendar.tab.contentDiv)
+ .on('wheel','.et2_container:not(#calendar-list)',
+ function(e)
+ {
+ e.preventDefault();
+ var direction = e.originalEvent.deltaY > 0 ? 1 : -1;
+ var delta = 1;
+ var start = new Date(app.calendar.state.date);
+ var end = null;
+
+ // Get the view to calculate
+ if (app.calendar.views && app.calendar.state.view && app.calendar.views[app.calendar.state.view].set_end_date)
+ {
+ if(direction > 0)
+ {
+ start = app.calendar.views[app.calendar.state.view].set_end_date({date:start});
+ }
+ else
+ {
+ start = app.calendar.views[app.calendar.state.view].set_start_date({date:start});
+ }
+ start.setUTCDate(start.getUTCDate()+direction);
+ end = app.calendar.views[app.calendar.state.view].set_end_date({date:start});
+ }
+ // Calculate the current difference, and move
+ else if(app.calendar.state.first && app.calendar.state.last)
+ {
+ start = new Date(app.calendar.state.first);
+ end = new Date(app.calendar.state.last);
+ // Get the number of days
+ delta = (Math.round(Math.max(1,end - start)/(24*3600*1000)))*24*3600*1000
+ // Adjust
+ start = new Date(start.valueOf() + (delta * direction ));
+ end = new Date(end.valueOf() + (delta * direction));
+ }
+
+ app.calendar.update_state({date: start});
+
+ return false;
+ }
+ );
+ },
+
/**
* Function to help calendar resizable event, to fetch the right droppable cell
*
@@ -1390,8 +1517,8 @@ app.classes.calendar = AppJS.extend(
// Show the correct number of grids
- var grid_count = state.state.view == 'weekN' ? parseInt(this.egw.preference('multiple_weeks','calendar')) || 3 :
- state.state.view == 'month' ? 0 : // Calculate based on weeks in the month
+ var grid_count = state.state.view === 'weekN' ? parseInt(this.egw.preference('multiple_weeks','calendar')) || 3 :
+ state.state.view === 'month' ? 0 : // Calculate based on weeks in the month
state.state.owner.length > (this.egw.config('calview_no_consolidate','phpgwapi') || 5) ? 1 : state.state.owner.length;
var grid = this.views[this.state.view] ? this.views[this.state.view].etemplates[0].widgetContainer.getWidgetById('view') : false;
@@ -1401,11 +1528,13 @@ app.classes.calendar = AppJS.extend(
If the count is > 1, it's either because there are multiple date spans (weekN, month) and we need the correct span
per row, or there are multiple owners and we need the correct owner per row.
*/
- if(state.state.view !== 'listview' && (!grid || grid_count != grid._children.length || grid_count > 1))
+ if(grid && (grid_count !== grid._children.length || grid_count > 1))
{
// Need to redo the number of grids
var value = [];
- var date = state.state.first = view.set_start_date(state.state);
+ state.state.first = view.set_start_date(state.state).toJSON();
+ // We'll modify this one, so it needs to be a new object
+ var date = new Date(state.state.first);
// Determine the different end date
switch(state.state.view)
@@ -1427,7 +1556,7 @@ app.classes.calendar = AppJS.extend(
value.push(val);
date.setUTCHours(24*7);
}
- state.state.last=val.end_date;
+ state.state.last=val.end_date.toJSON();
break;
default:
var end = state.state.last = view.set_end_date(state.state);
@@ -1442,16 +1571,16 @@ app.classes.calendar = AppJS.extend(
}
break;
}
- if(view.etemplates[0].widgetContainer.getWidgetById('view'))
+ if(grid)
{
- view.etemplates[0].widgetContainer.getWidgetById('view').set_value(
+ grid.set_value(
{content: value}
);
}
}
else
{
- // Simple, easy case - just one timegrid.
+ // Simple, easy case - just one widget for the selected time span.
// Update existing view's special attribute filters, defined in the view list
for(var updater in view)
{
@@ -1658,8 +1787,18 @@ app.classes.calendar = AppJS.extend(
if(_selected[i].iface.getWidget() && _selected[i].iface.getWidget().instanceOf(et2_calendar_event))
{
is_widget = true;
- break;
}
+
+ // Also check classes, usually indicating permission
+ if(_action.data && _action.data.enableClass)
+ {
+ is_widget = is_widget && ($j( _selected[i].iface.getDOMNode()).hasClass(_action.data.enableClass))
+ }
+ if(_action.data && _action.data.disableClass)
+ {
+ is_widget = is_widget && !($j( _selected[i].iface.getDOMNode()).hasClass(_action.data.disableClass))
+ }
+
}
return is_widget;
},
diff --git a/calendar/js/et2_widget_daycol.js b/calendar/js/et2_widget_daycol.js
index f4bb641f18..014638a6ed 100644
--- a/calendar/js/et2_widget_daycol.js
+++ b/calendar/js/et2_widget_daycol.js
@@ -207,7 +207,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM],
this.div.attr("data-date", this.options.date);
// Set holiday and today classes
- this._day_class_holiday();
+ this.day_class_holiday();
// Update all the little boxes
this._draw();
@@ -228,7 +228,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM],
/**
* Applies class for today, and any holidays for current day
*/
- _day_class_holiday: function() {
+ day_class_holiday: function() {
// Remove all classes
this.title.removeClass()
// Except this one...
@@ -243,7 +243,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM],
);
// Holidays and birthdays
- var holidays = et2_calendar_daycol.get_holidays(this);
+ var holidays = et2_calendar_daycol.get_holidays(this,this.options.date.substring(0,4));
var holiday_list = [];
if(holidays && holidays[this.options.date])
{
@@ -295,7 +295,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM],
var events = _events || this.getArrayMgr('content').getEntry(this.options.date) || [];
// Sort events into minimally-overlapping columns
- var columns = this._event_columns(events);
+ var columns = this._spread_events(events);
for(var c = 0; c < columns.length; c++)
{
@@ -352,7 +352,7 @@ var et2_calendar_daycol = et2_valueWidget.extend([et2_IDetachedDOM],
* @param {Object[]} events
* @returns {Array[]} Events sorted into columns
*/
- _event_columns: function(events)
+ _spread_events: function(events)
{
var day_start = this.date.valueOf() / 1000;
var dst_check = new Date(this.date);
@@ -633,36 +633,36 @@ jQuery.extend(et2_calendar_daycol,
* Fetch and cache a list of the year's holidays
*
* @param {et2_calendar_timegrid} widget
+ * @param {string|numeric} year
* @returns {Array}
*/
- get_holidays: function(widget)
+ get_holidays: function(widget,year)
{
// Loaded in an iframe or something
if(!egw.window.et2_calendar_daycol) return {};
- var cache_id = widget.options.date.substring(0,4);
- var cache = egw.window.et2_calendar_daycol.holiday_cache[cache_id];
+ var cache = egw.window.et2_calendar_daycol.holiday_cache[year];
if (typeof cache == 'undefined')
{
// Fetch with json instead of jsonq because there may be more than
// one widget listening for the response by the time it gets back,
// and we can't do that when it's queued.
- egw.window.et2_calendar_daycol.holiday_cache[cache_id] = egw.json(
+ egw.window.et2_calendar_daycol.holiday_cache[year] = egw.json(
'calendar_timegrid_etemplate_widget::ajax_get_holidays',
- [cache_id]
+ [year]
).sendRequest();
}
- cache = egw.window.et2_calendar_daycol.holiday_cache[cache_id];
+ cache = egw.window.et2_calendar_daycol.holiday_cache[year];
if(typeof cache.done == 'function')
{
// pending, wait for it
cache.done(jQuery.proxy(function(response) {
- egw.window.et2_calendar_daycol.holiday_cache[this.cache_id] = response.response[0].data||undefined;
+ egw.window.et2_calendar_daycol.holiday_cache[this.year] = response.response[0].data||undefined;
egw.window.setTimeout(jQuery.proxy(function() {
- this.widget._day_class_holiday();
+ this.widget.day_class_holiday();
},this),1);
- },{widget:widget,cache_id:cache_id}));
+ },{widget:widget,year:year}));
return {};
}
else
diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js
index ffb0399c6e..ca063ba175 100644
--- a/calendar/js/et2_widget_event.js
+++ b/calendar/js/et2_widget_event.js
@@ -43,6 +43,7 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM],
// Main container
this.div = $j(document.createElement("div"))
.addClass("calendar_calEvent")
+ .addClass(this.options.class)
.css('width',this.options.width);
this.title = $j(document.createElement('div'))
.addClass("calendar_calEventHeader")
@@ -95,7 +96,7 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM],
event = jQuery.extend({},event);
var list = [event];
// Let parent format any missing data
- this._parent._event_columns(list);
+ this._parent._spread_events(list);
// Calculate vertical positioning
// TODO: Maybe move this somewhere common between here & parent?
@@ -176,8 +177,12 @@ var et2_calendar_event = et2_valueWidget.extend([et2_IDetachedDOM],
// Header
var title = !event.is_private ? event['title'] : egw.lang('private');
- var small_height = event['end_m']-event['start_m'] < 2*this._parent.display_settings.granularity ||
- event['end_m'] <= this._parent.display_settings.wd_start || event['start_m'] >= this._parent.display_settings.wd_end;
+ var small_height = true;
+ if(this._parent.display_settings)
+ {
+ small_height = event['end_m']-event['start_m'] < 2*this._parent.display_settings.granularity ||
+ event['end_m'] <= this._parent.display_settings.wd_start || event['start_m'] >= this._parent.display_settings.wd_end;
+ }
this.div.attr('data-title', title);
this.title.text(small_height ? title : this._get_timespan(event))
diff --git a/calendar/js/et2_widget_planner.js b/calendar/js/et2_widget_planner.js
new file mode 100644
index 0000000000..4678279490
--- /dev/null
+++ b/calendar/js/et2_widget_planner.js
@@ -0,0 +1,1430 @@
+/*
+ * Egroupware Calendar timegrid
+ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ * @package etemplate
+ * @subpackage api
+ * @link http://www.egroupware.org
+ * @author Nathan Gray
+ * @version $Id$
+ */
+
+
+"use strict";
+
+/*egw:uses
+ /etemplate/js/et2_core_valueWidget;
+ /calendar/js/et2_widget_planner_row.js;
+ /calendar/js/et2_widget_event.js;
+*/
+
+/**
+ * Class which implements the "calendar-planner" XET-Tag for displaying a longer
+ * ( > 10 days) span of time
+ *
+ * @augments et2_valueWidget
+ * @class
+ */
+var et2_calendar_planner = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizeable],
+{
+ createNamespace: true,
+
+ attributes: {
+ start_date: {
+ name: "Start date",
+ type: "any"
+ },
+ end_date: {
+ name: "End date",
+ type: "any"
+ },
+ group_by: {
+ name: "Group by",
+ type: "string", // or category ID
+ default: "0",
+ description: "Display planner by 'user', 'month', or the given category"
+ },
+ owner: {
+ name: "Owner",
+ type: "any", // Integer, or array of integers
+ default: 0,
+ description: "Account ID number of the calendar owner, if not the current user"
+ },
+ filter: {
+ name: "Filter",
+ type: "string",
+ default: '',
+ description: 'A filter that is used to select events. It is passed along when events are queried.'
+ },
+ value: {
+ type: "any",
+ description: "A list of events, optionally you can set start_date, end_date and group_by as keys and events will be fetched"
+ },
+ "onchange": {
+ "name": "onchange",
+ "type": "js",
+ "default": et2_no_init,
+ "description": "JS code which is executed when the date range changes."
+ },
+ "onevent_change": {
+ "name": "onevent_change",
+ "type": "js",
+ "default": et2_no_init,
+ "description": "JS code which is executed when an event changes."
+ }
+ },
+
+ /**
+ * Constructor
+ *
+ * @memberOf et2_calendar_planner
+ * @constructor
+ */
+ init: function() {
+ this._super.apply(this, arguments);
+
+ // Main container
+ this.div = $j(document.createElement("div"))
+ .addClass("calendar_plannerWidget");
+
+ // Header
+ this.gridHeader = $j(document.createElement("div"))
+ .addClass("calendar_plannerHeader")
+ .appendTo(this.div);
+ this.headerTitle = $j(document.createElement("div"))
+ .addClass("calendar_plannerHeaderTitle")
+ .appendTo(this.gridHeader);
+ this.headers = $j(document.createElement("div"))
+ .addClass("calendar_plannerHeaderRows")
+ .appendTo(this.gridHeader);
+
+ this.rows = $j(document.createElement("div"))
+ .appendTo(this.div);
+
+ // Used for its date calculations
+ this.date_helper = et2_createWidget('date-time',{},null);
+ this.date_helper.loadingFinished();
+
+ this.value = [];
+
+ // Update timer, to avoid redrawing twice when changing start & end date
+ this.update_timer = null;
+
+ this.setDOMNode(this.div[0]);
+ },
+
+ destroy: function() {
+ this._super.apply(this, arguments);
+ this.div.off();
+
+ // date_helper has no parent, so we must explicitly remove it
+ this.date_helper.destroy();
+ this.date_helper = null;
+
+ // Stop the invalidate timer
+ if(this.update_timer)
+ {
+ window.clearTimeout(this.update_timer);
+ }
+ },
+
+ doLoadingFinished: function() {
+ this._super.apply(this, arguments);
+
+ // Don't bother to draw anything if there's no date yet
+ if(this.options.start_date)
+ {
+ this._drawGrid();
+ }
+
+ // Actions may be set on a parent, so we need to explicitly get in here
+ // and get ours
+ this._link_actions(this.options.actions || this._parent.options.actions || []);
+
+ return true;
+ },
+
+ /**
+ * These handle the differences between the different group types.
+ * They provide the different titles, labels and grouping
+ */
+ groupers: {
+ // Group by user has one row for each user
+ user:
+ {
+ // Title in top left corner
+ title: function() { return this.egw().lang('User');},
+ // Column headers
+ headers: function() {
+ var start = new Date(this.options.start_date);
+ var end = new Date(this.options.end_date);
+ var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate());
+ var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate());
+ var day_count = Math.round((end_date - start_date) /(1000*3600*24))+1;
+ if(day_count >= 28)
+ {
+ this.headers.append(this._header_months(start, day_count));
+ }
+ if(day_count >= 5)
+ {
+ this.headers.append(this._header_weeks(start, day_count));
+ }
+ this.headers.append(this._header_days(start, day_count));
+ if(day_count <= 7)
+ {
+ this.headers.append(this._header_hours(start, day_count));
+ }
+ },
+ // Labels for the rows
+ row_labels: function() {
+ var labels = {};
+ var accounts = egw.accounts();
+ for(var i = 0; i < this.options.owner.length; i++)
+ {
+ var user = this.options.owner[i];
+ if(parseInt(user) === 0)
+ {
+ // 0 means current user
+ user = egw.user('account_id');
+ }
+ if (isNaN(user)) // resources
+ {
+ labels[user] = egw.link_title('resources',user.match(/\d+/)[0],function(name) {this[user] = name;},labels);
+ }
+ else if (user < 0) // groups
+ {
+ egw.accountData(user,'account_fullname',true,function(result) {
+ for(var id in result)
+ {
+ this[id] = result[id];
+ }
+ },labels);
+ }
+ else // users
+ {
+ user = parseInt(user)
+ for(var i = 0; i < accounts.length; i++)
+ {
+ if(accounts[i].value === user)
+ {
+ labels[user] = accounts[i].label;
+ break;
+ }
+ }
+ }
+ }
+
+ return labels;
+ },
+ // Group the events into the rows
+ group: function(labels, rows, event) {
+ // convert filter to allowed status
+ var status_to_show = ['U','A','T','D','G'];
+ switch(this.options.filter)
+ {
+ case 'unknown':
+ status_to_show = ['U','G']; break;
+ case 'accepted':
+ status_to_show = ['A']; break;
+ case 'tentative':
+ status_to_show = ['T']; break;
+ case 'rejected':
+ status_to_show = ['R']; break;
+ case 'delegated':
+ status_to_show = ['D']; break;
+ case 'all':
+ status_to_show = ['U','A','T','D','G','R']; break;
+ default:
+ status_to_show = ['U','A','T','D','G']; break;
+ }
+ for(var user in event.participants)
+ {
+ var participant = event.participants[user];
+ if(participant && typeof labels[user] !== 'undefined' && status_to_show.indexOf(participant.substr(0,1)) >= 0 ||
+ this.options.filter === 'owner' && event.owner === user)
+ {
+ if(typeof rows[user] === 'undefined')
+ {
+ rows[user] = [];
+ }
+ rows[user].push(event);
+ }
+ }
+ },
+ // Draw a single row
+ draw_row: function(sort_key, label, events) {
+ return this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date);
+ }
+ },
+
+ // Group by month has one row for each month
+ month:
+ {
+ title: function() { return this.egw().lang('Month');},
+ headers: function() {
+ this.headers.append(this._header_day_of_month());
+ },
+ row_labels: function() {
+ var labels = {};
+ var d = new Date(this.options.start_date);
+ d = new Date(d.valueOf() + d.getTimezoneOffset() * 60 * 1000);
+ for(var i = 0; i < 12; i++)
+ {
+ labels[d.getUTCFullYear() +'-'+d.getUTCMonth()] = egw.lang(date('F',d))+' '+d.getUTCFullYear();
+ d.setUTCMonth(d.getUTCMonth()+1);
+ }
+ return labels;
+ },
+ group: function(labels, rows,event) {
+ var start = new Date(event.start);
+ var key = start.getUTCFullYear() +'-'+start.getUTCMonth();
+ if(typeof rows[key] === 'undefined')
+ {
+ rows[key] = [];
+ }
+ rows[key].push(event);
+
+ // end in a different month?
+ var end = new Date(event.end);
+ var end_key = end.getUTCFullYear() +'-'+end.getUTCMonth();
+ while(key !== end_key)
+ {
+ var year = start.getUTCFullYear();
+ var month = start.getUTCMonth();
+ if (++month > 12)
+ {
+ ++year;
+ month = 1;
+ }
+ key = sprintf('%04d-%02d',year,month);
+ rows[key].push(event);
+ }
+ },
+ // Draw a single row, but split up the dates
+ draw_row: function(sort_key, label, events)
+ {
+ var key = sort_key.split('-');
+ this._drawRow(sort_key, label, events, new Date(key[0],key[1],1),new Date(key[0],parseInt(key[1])+1,0));
+ }
+ },
+ // Group by category has one row for each [sub]category
+ category:
+ {
+ title: function() { return this.egw().lang('Category');},
+ headers: function() {
+ var start = new Date(this.options.start_date);
+ var end = new Date(this.options.end_date);
+ var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate());
+ var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate());
+ var day_count = Math.round((end_date - start_date) /(1000*3600*24))+1;
+
+ if(day_count >= 28)
+ {
+ this.headers.append(this._header_months(start, day_count));
+ }
+
+ if(day_count >= 5)
+ {
+ this.headers.append(this._header_weeks(start, day_count));
+ }
+ this.headers.append(this._header_days(start, day_count));
+ if(day_count <= 7)
+ {
+ this.headers.append(this._header_hours(start, day_count));
+ }
+ },
+ row_labels: function() {
+ return {'': egw.lang('none')};
+ },
+ group: function(labels, rows, event) {
+ if(typeof rows[event.category] === 'undefined')
+ {
+ rows[event.category] = [];
+ }
+ rows[event.category].push(event);
+ if(typeof labels[event.category] === 'undefined')
+ {
+ var categories = et2_selectbox.cat_options({_type:'select-cat'}, {application: 'calendar'});
+ for(var i in categories )
+ {
+ if(parseInt(categories[i].value) === parseInt(event.category))
+ {
+ labels[event.category] = categories[i].label;
+ }
+ }
+ }
+ },
+ draw_row: function(sort_key, label, events) {
+ return this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date);
+ }
+ }
+ },
+
+ /**
+ * Something changed, and the planner needs to be re-drawn. We wait a bit to
+ * avoid re-drawing twice if start and end date both changed, then recreate.
+ *
+ * @param {boolean} trigger=false Trigger an event once things are done.
+ * Waiting until invalidate completes prevents 2 updates when changing the date range.
+ * @returns {undefined}
+ */
+ invalidate: function(trigger) {
+
+ // Wait a bit to see if anything else changes, then re-draw the days
+ if(this.update_timer === null)
+ {
+ this.update_timer = window.setTimeout(jQuery.proxy(function() {
+ this.widget.update_timer = null;
+
+ this.widget._fetch_data();
+ //this.widget._drawGrid();
+
+ // Update actions
+ if(this._actionManager)
+ {
+ this._link_actions(this._actionManager.children);
+ }
+
+ if(this.trigger)
+ {
+ this.widget.change();
+ }
+ },{widget:this,"trigger":trigger}),ET2_GRID_INVALIDATE_TIMEOUT);
+ }
+ },
+
+ detachFromDOM: function() {
+ // Remove the binding to the change handler
+ $j(this.div).off("change.et2_calendar_timegrid");
+
+ this._super.apply(this, arguments);
+ },
+
+ attachToDOM: function() {
+ this._super.apply(this, arguments);
+
+ // Add the binding for the event change handler
+ $j(this.div).on("change.et2_calendar_timegrid", '.calendar_calEvent', this, function(e) {
+ // Make sure function gets a reference to the widget
+ var args = Array.prototype.slice.call(arguments);
+ if(args.indexOf(this) == -1) args.push(this);
+
+ return e.data.event_change.apply(e.data, args);
+ });
+
+ // Add the binding for the change handler
+ $j(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function(e) {
+ return e.data.change.call(e.data, e, this);
+ });
+
+ },
+
+ getDOMNode: function(_sender)
+ {
+ if(_sender === this || !_sender)
+ {
+ return this.div[0];
+ }
+ if(_sender._parent === this)
+ {
+ return this.rows[0];
+ }
+ },
+
+ /**
+ * Creates all the DOM nodes for the planner grid
+ *
+ * Any existing nodes (& children) are removed, the headers & labels are
+ * determined according to the current group_by value, and then the rows
+ * are created.
+ *
+ * @method
+ * @private
+ *
+ */
+ _drawGrid: function()
+ {
+
+ this.div.css('height', this.options.height);
+
+ // Clear old events
+ var delete_index = this._children.length - 1;
+ while(this._children.length > 0 && delete_index >= 0)
+ {
+ this._children[delete_index].free();
+ this.removeChild(this._children[delete_index--]);
+ }
+
+ // Clear old rows
+ this.rows.empty();
+
+ var grouper = this.groupers[isNaN(this.options.group_by) ? this.options.group_by : 'category'];
+ if(!grouper) return;
+
+ // Headers
+ this.headers.empty();
+ this.headerTitle.text(grouper.title.apply(this));
+ grouper.headers.apply(this);
+
+ // Get the rows / labels
+ var labels = grouper.row_labels.call(this);
+
+ // Group the events
+ var events = {};
+ for(var i = 0; i < this.value.length; i++)
+ {
+ grouper.group.call(this, labels, events, this.value[i]);
+ }
+
+ // Draw the rows
+ for(var key in labels)
+ {
+ grouper.draw_row.call(this,key, labels[key], events[key] || []);
+ }
+
+ },
+
+ /**
+ * Draw a single row of the planner
+ *
+ * @param {string} key Index into the grouped labels & events
+ * @param {string} label
+ * @param {Array} events
+ * @param {Date} start
+ * @param {Date} end
+ */
+ _drawRow: function(key, label, events, start, end)
+ {
+ var row = et2_createWidget('calendar-planner_row',{
+ id: key,
+ label: label,
+ start_date: start,
+ end_date: end,
+ value: events
+ },this);
+
+
+ if(this.isInTree())
+ {
+ row.doLoadingFinished();
+ }
+
+ // Add actual events
+ row._update_events(events);
+ },
+
+
+ _header_day_of_month: function()
+ {
+ var day_width = 3.23; // 100.0 / 31;
+
+ // month scale with navigation
+ var content = '
';
+ var start = new Date(this.options.start_date);
+ start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
+ var end = new Date(this.options.end_date);
+ end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000);
+
+ var title = egw.lang(date('F',start))+' '+date('Y',start)+' - '+
+ egw.lang(date('F',end))+' '+date('Y',end);
+
+ // calculate date for navigation links
+ var time = new Date(start);
+ time.setUTCFullYear(time.getUTCFullYear()-1);
+ var last_year = date('Ymd',time);
+ time.setUTCMonth(time.getUTCMonth()+11);
+ var last_month = date('Ymd',time);
+ time.setUTCMonth(time.getUTCMonth()+2);
+ var next_month = date('Ymd',time);
+ time.setUTCMonth(time.getUTCMonth()+11);
+ var next_year = date('Ymd',time);
+
+ title = last_year + ' ' + last_month + ' ' + title + ' ' +next_month +' ' +next_year;
+/*
+ * TODO: implement these arrows
+ title = html::a_href(html::image('phpgwapi','first',lang('back one year'),$options=' alt="<<"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => $last_year,
+ )) + ' '+
+ html::a_href(html::image('phpgwapi','left',lang('back one month'),$options=' alt="<"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => $last_month,
+ )) + ' '+title;
+ title += ' '.html::a_href(html::image('phpgwapi','right',lang('forward one month'),$options=' alt=">>"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => $next_month,
+ ))+ ' '+
+ html::a_href(html::image('phpgwapi','last',lang('forward one year'),$options=' alt=">>"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => $next_year,
+ ));
+ */
+
+ content += '
'+
+ title+"
";
+ content += "
"; // end of plannerScale
+
+ // day of month scale
+ content +='';
+
+ for(var left = 0, i = 0; i < 31; left += day_width,++i)
+ {
+ content += '
'+
+ (1+i)+"
\n";
+ }
+ content += "
\n";
+
+ return content;
+ },
+
+ /**
+ * Make a header showing the months
+ * @param {Date} start
+ * @param {number} days
+ * @returns {string} HTML snippet
+ */
+ _header_months: function(start, days)
+ {
+ var content = '';
+ var days_in_month = 0;
+ var day_width = 100 / days;
+ for(var t = new Date(start),left = 0,i = 0; i < days; t.setUTCDate(t.getUTCDate() + days_in_month),left += days_in_month*day_width,i += days_in_month)
+ {
+ days_in_month = new Date(t.getUTCFullYear(),t.getUTCMonth()+1,0).getUTCDate();
+
+ if (i + days_in_month > days)
+ {
+ days_in_month = days - i;
+ }
+ if (days_in_month > 5)
+ {
+ var title = egw.lang(date('F',new Date(t.valueOf() + t.getTimezoneOffset() * 60 * 1000)))
+ }
+ if (days_in_month > 10)
+ {
+ title += ' '+t.getUTCFullYear();
+
+ // previous links
+ /*
+ $prev = $t_arr;
+ $prev['day'] = 1;
+ if ($prev['month']-- <= 1)
+ {
+ $prev['month'] = 12;
+ $prev['year']--;
+ }
+ if ($this->bo->date2ts($prev) < $start-20*DAY_s)
+ {
+ $prev['day'] = $this->day;
+ $full = $this->bo->date2string($prev);
+ if ($this->day >= 15) $prev = $t_arr; // we stay in the same month
+ $prev['day'] = $this->day < 15 ? 15 : 1;
+ $half = $this->bo->date2string($prev);
+ $title = html::a_href(html::image('phpgwapi','first',lang('back one month'),$options=' alt="<<"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => $full,
+ )) . ' '.
+ html::a_href(html::image('phpgwapi','left',lang('back half a month'),$options=' alt="<"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => $half,
+ )) . ' '.$title;
+ }
+ // next links
+ $next = $t_arr;
+ if ($next['month']++ >= 12)
+ {
+ $next['month'] = 1;
+ $next['year']++;
+ }
+ // dont show next scales, if there are more then 10 days in the next month or there is no next month
+ $days_in_next_month = (int) date('d',$end = $start+$days*DAY_s);
+ if ($days_in_next_month <= 10 || date('m',$end) == date('m',$t))
+ {
+ if ($this->day >= 15) $next = $t_arr; // we stay in the same month
+ $next['day'] = $this->day;
+ $full = $this->bo->date2string($next);
+ if ($this->day < 15) $next = $t_arr; // we stay in the same month
+ $next['day'] = $this->day < 15 ? 15 : 1;
+ $half = $this->bo->date2string($next);
+ $title .= ' '.html::a_href(html::image('phpgwapi','right',lang('forward half a month'),$options=' alt=">>"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => $half,
+ )). ' '.
+ html::a_href(html::image('phpgwapi','last',lang('forward one month'),$options=' alt=">>"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => $full,
+ ));
+ }
+ */
+ }
+ else
+ {
+ title = ' ';
+ }
+ content += '
'+
+ title+"
";
+ }
+ content += "
"; // end of plannerScale
+
+ return content;
+ },
+
+ /**
+ * Make a header showing the week numbers
+ *
+ * @param {Date} start
+ * @param {number} days
+ * @returns {string} HTML snippet
+ */
+ _header_weeks: function(start, days)
+ {
+ var week_width = 100 / days * (days <= 7 ? days : 7);
+
+ var content = '';
+ var state = ''
+
+ // we're not using UTC so date() formatting function works
+ var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
+ for(var left = 0,i = 0; i < days; t.setDate(t.getDate() + 7),left += week_width,i += 7)
+ {
+ var title = egw.lang('Week')+' '+date('W',t);
+
+ state = new Date(t.valueOf() - start.getTimezoneOffset() * 60 * 1000).toJSON();
+ /*
+ if (days > 7)
+ {
+ $title = html::a_href($title,array(
+ 'menuaction' => 'calendar.calendar_uiviews.planner',
+ 'planner_days' => 7,
+ 'date' => date('Ymd',$t),
+ ),false,' title="'.html::htmlspecialchars(lang('Weekview')).'"');
+ }
+ else
+ {
+ // prev. week
+ $title = html::a_href(html::image('phpgwapi','first',lang('previous'),$options=' alt="<<"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => date('Ymd',$t-7*DAY_s),
+ )) . '
'.$title;
+ // next week
+ $title .= ' '.html::a_href(html::image('phpgwapi','last',lang('next'),$options=' alt=">>"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => date('Ymd',$t+7*DAY_s),
+ ));
+ }
+ */
+ content += '
'+title+"
";
+ }
+ content += "
"; // end of plannerScale
+
+ return content;
+ },
+
+ /**
+ * Make a header for some days
+ *
+ * @param {Date} start
+ * @param {number} days
+ * @returns {string} HTML snippet
+ */
+ _header_days: function(start, days)
+ {
+ var day_width = 100 / days;
+ var content = '';
+
+ // we're not using UTC so date() formatting function works
+ var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
+ for(var left = 0,i = 0; i < days; t.setDate(t.getDate()+1),left += day_width,++i)
+ {
+ var holidays = [];
+ var day_class = this.day_class_holiday(t,holidays);
+ var title = '';
+ var state = '';
+
+ if (days <= 3)
+ {
+ title = egw.lang(date('l',t))+', '+date('j',t)+'. '+egw.lang(date('F',t));
+ }
+ else if (days <= 7)
+ {
+ title = egw.lang(date('l',t))+' '+date('j',t);
+ }
+ else
+ {
+ title = egw.lang(date('D',t)).substr(0,2)+'
'+date('j',t);
+ }
+ state = new Date(t.valueOf() - start.getTimezoneOffset() * 60 * 1000).toJSON();
+ if (days > 1)
+ {
+ /*
+ title = html::a_href($title,array(
+ 'menuaction' => 'calendar.calendar_uiviews.planner',
+ 'planner_days' => 1,
+ 'date' => date('Ymd',$t),
+ ),false,strpos($class,'calendar_calHoliday') !== false || strpos($class,'calendar_calBirthday') !== false ? '' : ' title="'.html::htmlspecialchars(lang('Dayview')).'"');
+ */
+ }
+ if (days < 5)
+ {
+ /*
+ if (!i) // prev. day only for the first day
+ {
+ title = html::a_href(html::image('phpgwapi','first',lang('previous'),$options=' alt="<<"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => date('Ymd',$start-DAY_s),
+ )) . ' '.$title;
+ }
+ if (i == days-1) // next day only for the last day
+ {
+ title += ' '.html::a_href(html::image('phpgwapi','last',lang('next'),$options=' alt=">>"'),array(
+ 'menuaction' => $this->view_menuaction,
+ 'date' => date('Ymd',$start+DAY_s),
+ ));
+ }
+ */
+ }
+ content += '
'+title+"
\n";
+ }
+ content += "
"; // end of plannerScale
+
+ return content;
+ },
+
+ /**
+ * Create a header with hours
+ *
+ * @param {Date} start
+ * @param {number} days
+ * @returns {string} HTML snippet for the header
+ */
+ _header_hours: function(start,days)
+ {
+ var divisors = [1,2,3,4,6,8,12];
+ var decr = 1;
+ for(var i = 0; i < divisors.length; i++) // numbers dividing 24 without rest
+ {
+ if (divisors[i] > days) break;
+ decr = divisors[i];
+ }
+ var hours = days * 24;
+ if (days === 1) // for a single day we calculate the hours of a days, to take into account daylight saving changes (23 or 25 hours)
+ {
+ var t = new Date(start.getUTCFullYear(),start.getUTCMonth(),start.getUTCDate());
+ var s = new Date(start);
+ s.setUTCHours(23);
+ s.setUTCMinutes(59);
+ s.setUTCSeconds(59);
+ hours = (s.getTime() - t.getTime()) / 3600000;
+ }
+ var cell_width = 100 / hours * decr;
+
+ var content = '';
+
+ // we're not using UTC so date() formatting function works
+ var t = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
+ for(var left = 0,i = 0; i < hours; left += cell_width,i += decr)
+ {
+ var title = date(egw.preference('timeformat','calendar') == 12 ? 'ha' : 'H',t);
+
+ content += '
'+title+"
";
+ t.setHours(t.getHours()+decr);
+ }
+ content += "
"; // end of plannerScale
+
+ return content;
+ },
+
+ /**
+ * Create a pagination button, and inserts it
+ *
+ */
+ _scroll_button: function()
+ {
+
+ },
+
+ /**
+ * Applies class for today, and any holidays for current day
+ *
+ * @param {Date} date
+ * @param {string[]} holiday_list Filled with a list of holidays for that day
+ *
+ * @return {string} CSS Classes for the day. calendar_calBirthday, calendar_calHoliday, calendar_calToday and calendar_weekend as appropriate
+ */
+ day_class_holiday: function(date,holiday_list) {
+
+ if(!date) return '';
+
+ var day_class = '';
+
+ // Holidays and birthdays
+ var holidays = et2_calendar_daycol.get_holidays(this,date.getUTCFullYear());
+
+ // Pass a number rather than the date object, to make sure it doesn't get changed
+ this.date_helper.set_value(date.getTime()/1000);
+ var date_key = ''+this.date_helper.get_year() + sprintf('%02d',this.date_helper.get_month()) + sprintf('%02d',this.date_helper.get_date());
+ if(holidays && holidays[date_key])
+ {
+ holidays = holidays[date_key];
+ for(var i = 0; i < holidays.length; i++)
+ {
+ if (typeof holidays[i]['birthyear'] !== 'undefined')
+ {
+ day_class += ' calendar_calBirthday';
+
+ holiday_list.push(holidays[i]['name']);
+ }
+ else
+ {
+ day_class += 'calendar_calHoliday';
+
+ holiday_list.push(holidays[i]['name']);
+ }
+ }
+ }
+ holidays = holiday_list.join(',');
+ var today = new Date();
+ if(date_key === ''+today.getUTCFullYear()+
+ sprintf("%02d",today.getUTCMonth()+1)+
+ sprintf("%02d",today.getUTCDate())
+ )
+ {
+ day_class += "calendar_calToday";
+ }
+ if(date.getUTCDay() == 0 || date.getUTCDay() == 6)
+ {
+ day_class += "calendar_weekend";
+ }
+ return day_class;
+ },
+
+ /**
+ * Link the actions to the DOM nodes / widget bits.
+ *
+ * @todo This currently does nothing
+ * @param {object} actions {ID: {attributes..}+} map of egw action information
+ */
+ _link_actions: function(actions)
+ {
+ // Get the parent? Might be a grid row, might not. Either way, it is
+ // just a container with no valid actions
+ var objectManager = egw_getAppObjectManager(true);
+ var parent = objectManager.getObjectById(this._parent.id);
+ if(!parent) return;
+
+ for(var i = 0; i < parent.children.length; i++)
+ {
+ var parent_finder = jQuery(this.div, parent.children[i].iface.doGetDOMNode());
+ if(parent_finder.length > 0)
+ {
+ parent = parent.children[i];
+ break;
+ }
+ }
+ },
+
+ /**
+ * Automatically add dnd support for linking
+ */
+ _init_links_dnd: function(mgr,actionLinks) {
+ var self = this;
+
+ var drop_action = mgr.getActionById('egw_link_drop');
+ var drag_action = mgr.getActionById('egw_link_drag');
+
+ // Check if this app supports linking
+ if(!egw.link_get_registry(this.dataStorePrefix || this.egw().appName, 'query') ||
+ egw.link_get_registry(this.dataStorePrefix || this.egw().appName, 'title'))
+ {
+ if(drop_action)
+ {
+ drop_action.remove();
+ if(actionLinks.indexOf(drop_action.id) >= 0)
+ {
+ actionLinks.splice(actionLinks.indexOf(drop_action.id),1);
+ }
+ }
+ if(drag_action)
+ {
+ drag_action.remove();
+ if(actionLinks.indexOf(drag_action.id) >= 0)
+ {
+ actionLinks.splice(actionLinks.indexOf(drag_action.id),1);
+ }
+ }
+ return;
+ }
+
+ // Don't re-add
+ if(drop_action == null)
+ {
+ // Create the drop action that links entries
+ drop_action = mgr.addAction('drop', 'egw_link_drop', egw.lang('Create link'), egw.image('link'), function(action, source, dropped) {
+ // Extract link IDs
+ var links = [];
+ var id = '';
+ for(var i = 0; i < source.length; i++)
+ {
+ if(!source[i].id) continue;
+ id = source[i].id.split('::');
+ links.push({app: id[0] == 'filemanager' ? 'link' : id[0], id: id[1]});
+ }
+ if(!links.length)
+ {
+ return;
+ }
+
+ // Link the entries
+ egw.json(self.egw().getAppName()+".etemplate_widget_link.ajax_link.etemplate",
+ dropped.id.split('::').concat([links]),
+ function(result) {
+ if(result)
+ {
+ this.egw().message('Linked');
+ }
+ },
+ self,
+ true,
+ self
+ ).sendRequest();
+
+ },true);
+ }
+ if(actionLinks.indexOf(drop_action.id) < 0)
+ {
+ actionLinks.push(drop_action.id);
+ }
+ // Accept other links, and files dragged from the filemanager
+ // This does not handle files dragged from the desktop. They are
+ // handled by et2_nextmatch, since it needs DOM stuff
+ if(drop_action.acceptedTypes.indexOf('link') == -1)
+ {
+ drop_action.acceptedTypes.push('link');
+ }
+
+ // Don't re-add
+ if(drag_action == null)
+ {
+ // Create drag action that allows linking
+ drag_action = mgr.addAction('drag', 'egw_link_drag', egw.lang('link'), 'link', function(action, selected) {
+ // Drag helper - list titles. Arbitrarily limited to 10.
+ var helper = $j(document.createElement("div"));
+ for(var i = 0; i < selected.length && i < 10; i++)
+ {
+ var id = selected[i].id.split('::');
+ var span = $j(document.createElement('span')).appendTo(helper);
+ egw.link_title(id[0],id[1], function(title) {
+ this.append(title);
+ this.append('
');
+ }, span);
+ }
+ // As we wanted to have a general defaul helper interface, we return null here and not using customize helper for links
+ // TODO: Need to decide if we need to create a customized helper interface for links anyway
+ //return helper;
+ return null;
+ },true);
+ }
+ if(actionLinks.indexOf(drag_action.id) < 0)
+ {
+ actionLinks.push(drag_action.id);
+ }
+ drag_action.set_dragType('link');
+ },
+
+ /**
+ * Get all action-links / id's of 1.-level actions from a given action object
+ *
+ * Here we are only interested in drop events.
+ *
+ * @param actions
+ * @returns {Array}
+ */
+ _get_action_links: function(actions)
+ {
+ var action_links = [];
+ // TODO: determine which actions are allowed without an action (empty actions)
+ for(var i in actions)
+ {
+ var action = actions[i];
+ if(action.type === 'drop')
+ {
+ action_links.push(typeof action.id !== 'undefined' ? action.id : i);
+ }
+ }
+ return action_links;
+ },
+
+ /**
+ * Use the egw.data system to get data from the calendar list for the
+ * selected time span.
+ *
+ */
+ _fetch_data: function()
+ {
+ this.egw().dataFetch(
+ this.getInstanceManager().etemplate_exec_id,
+ {start: 0, num_rows:0},
+ jQuery.extend({}, app.calendar.state,
+ {
+ get_rows: 'calendar.calendar_uilist.get_rows',
+ row_id:'row_id',
+ startdate:this.options.start_date,
+ enddate:this.options.end_date,
+ col_filter: {participant: this.options.owner},
+ filter:'custom'
+ }),
+ this.id,
+ function(data) {
+ console.log(data);
+ var events = [];
+ for(var i = 0; i < data.order.length && data.total; i++)
+ {
+ var record = this.egw().dataGetUIDdata(data.order[i]);
+ if(record && record.data)
+ {
+ events.push(record.data);
+ }
+ }
+ this.value = events;
+ this._drawGrid();
+ }, this,null
+ );
+ },
+
+ /**
+ * Provide specific data to be displayed.
+ * This is a way to set start and end dates, owner and event data in once call.
+ *
+ * @param {Object[]} events Array of events, indexed by date in Ymd format:
+ * {
+ * 20150501: [...],
+ * 20150502: [...]
+ * }
+ * Days should be in order.
+ *
+ */
+ set_value: function(events)
+ {
+ if(typeof events !== 'object') return false;
+
+ if(events.owner)
+ {
+ this.set_owner(events.owner);
+ delete events.owner;
+ }
+ if(events.start_date)
+ {
+ this.set_start_date(events.start_date);
+ delete events.start_date;
+ }
+ if(events.end_date)
+ {
+ this.set_end_date(events.end_date);
+ delete events.end_date;
+ }
+
+ this.value = events || [];
+ },
+
+ /**
+ * Change the start date
+ *
+ * @param {string|number|Date} new_date New starting date
+ * @returns {undefined}
+ */
+ set_start_date: function(new_date)
+ {
+ if(!new_date || new_date === null)
+ {
+ throw new Error('Invalid start date. ' + new_date.toString());
+ }
+
+ // Use date widget's existing functions to deal
+ if(typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8)
+ {
+ this.date_helper.set_value(new_date);
+ }
+ else if(typeof new_date === "string")
+ {
+ this.date_helper.set_year(new_date.substring(0,4));
+ this.date_helper.set_month(new_date.substring(4,6));
+ this.date_helper.set_date(new_date.substring(6,8));
+ }
+
+ var old_date = this.options.start_date;
+ this.options.start_date = this.date_helper.getValue();
+
+ if(old_date !== this.options.start_date && this.isAttached())
+ {
+ this.invalidate(true);
+ }
+ },
+
+ /**
+ * Change the end date
+ *
+ * @param {string|number|Date} new_date New end date
+ * @returns {undefined}
+ */
+ set_end_date: function(new_date)
+ {
+ if(!new_date || new_date === null)
+ {
+ throw new Error('Invalid end date. ' + new_date.toString());
+ }
+ // Use date widget's existing functions to deal
+ if(typeof new_date === "object" || typeof new_date === "string" && new_date.length > 8)
+ {
+ this.date_helper.set_value(new_date);
+ }
+ else if(typeof new_date === "string")
+ {
+ this.date_helper.set_year(new_date.substring(0,4));
+ this.date_helper.set_month(new_date.substring(4,6));
+ this.date_helper.set_date(new_date.substring(6,8));
+ }
+
+ this.date_helper.set_hours(23);
+ this.date_helper.set_minutes(59);
+ this.date_helper.date.setSeconds(59);
+ var old_date = this.options.end_date;
+ this.options.end_date = this.date_helper.getValue();
+
+ if(old_date !== this.options.end_date && this.isAttached())
+ {
+ this.invalidate(true);
+ }
+ },
+
+ /**
+ * Change how the planner is grouped
+ *
+ * @param {string|number} group_by 'user', 'month', or an integer category ID
+ * @returns {undefined}
+ */
+ set_group_by: function(group_by)
+ {
+ if(isNaN(group_by) && typeof this.groupers[group_by] === 'undefined')
+ {
+ throw new Error('Invalid group_by "'+group_by+'"');
+ }
+ var old = this.options.group_by;
+ this.options.group_by = ''+group_by;
+
+ if(old !== this.options.group_by && this.isAttached())
+ {
+ this.invalidate(true);
+ }
+ },
+
+ /**
+ * Set which users to display when filtering, and for rows when grouping by user.
+ *
+ * @param {number|number[]} _owner Account ID
+ */
+ set_owner: function(_owner)
+ {
+ var old = this.options.owner;
+ if(!jQuery.isArray(_owner))
+ {
+ if(typeof _owner === "string")
+ {
+ _owner = _owner.split(',');
+ }
+ else
+ {
+ _owner = [_owner];
+ }
+ }
+ else
+ {
+ _owner = jQuery.extend([],_owner);
+ }
+ this.options.owner = _owner;
+ if(old !== this.options.owner && this.isAttached())
+ {
+ this.invalidate(true);
+ }
+ },
+
+
+ /**
+ * Call change handler, if set
+ */
+ change: function(event) {
+ if (this.onchange)
+ {
+ if(typeof this.onchange == 'function')
+ {
+ // Make sure function gets a reference to the widget
+ var args = Array.prototype.slice.call(arguments);
+ if(args.indexOf(this) == -1) args.push(this);
+
+ return this.onchange.apply(this, args);
+ } else {
+ return (et2_compileLegacyJS(this.options.onchange, this, _node))();
+ }
+ }
+ },
+
+ /**
+ * Call event change handler, if set
+ */
+ event_change: function(event, dom_node) {
+ if (this.onevent_change)
+ {
+ var event_data = this._get_event_info(dom_node);
+ var event_widget = this.getWidgetById(event_data.id);
+ et2_calendar_event.recur_prompt(event_data, jQuery.proxy(function(button_id, event_data) {
+ // No need to continue
+ if(button_id === 'cancel') return false;
+
+ if(typeof this.onevent_change == 'function')
+ {
+ // Make sure function gets a reference to the widget
+ var args = Array.prototype.slice.call(arguments);
+
+ if(args.indexOf(event_widget) == -1) args.push(event_widget);
+
+ // Put button ID in event
+ event.button_id = button_id;
+
+ return this.onevent_change.apply(this, [event, event_widget, button_id]);
+ } else {
+ return (et2_compileLegacyJS(this.options.onevent_change, event_widget, dom_node))();
+ }
+ },this));
+ }
+ return false;
+ },
+
+ /**
+ * Click handler calling custom handler set via onclick attribute to this.onclick
+ *
+ * This also handles all its own actions, including navigation. If there is
+ * an event associated with the click, it will be found and passed to the
+ * onclick function.
+ *
+ * @param {Event} _ev
+ * @returns {boolean}
+ */
+ click: function(_ev)
+ {
+ var result = true;
+
+ // Is this click in the event stuff, or in the header?
+ if(this.gridHeader.has(_ev.target).length === 0 && !$j(_ev.target).hasClass('calendar_plannerRowHeader'))
+ {
+ // Event came from inside, maybe a calendar event
+ var event = this._get_event_info(_ev.originalEvent.target);
+ if(typeof this.onclick == 'function')
+ {
+ // Make sure function gets a reference to the widget, splice it in as 2. argument if not
+ var args = Array.prototype.slice.call(arguments);
+ if(args.indexOf(this) == -1) args.splice(1, 0, this);
+
+ result = this.onclick.apply(this, args);
+ }
+
+ if(event.id && result && !this.options.disabled && !this.options.readonly)
+ {
+ et2_calendar_event.recur_prompt(event);
+
+ return false;
+ }
+ return result;
+ }
+ else if (!jQuery.isEmptyObject(_ev.target.dataset))
+ {
+ // Click on a header, we can go there
+ debugger
+ _ev.data = jQuery.extend({},_ev.target.parentNode.dataset, _ev.target.dataset);
+ this.change(_ev);
+ }
+ else
+ {
+ // Default handler to open a new event at the selected time
+ // TODO: Determine date / time more accurately from position
+ this.egw().open(null, 'calendar', 'add', {
+ date: _ev.target.dataset.date || this.day_list[0],
+ hour: _ev.target.dataset.hour || this.options.day_start,
+ minute: _ev.target.dataset.minute || 0
+ } , '_blank');
+ return false;
+ }
+ },
+
+ _get_event_info: function(dom_node)
+ {
+ // Determine as much relevant info as can be found
+ var event_node = $j(dom_node).closest('[data-id]',this.div)[0];
+ var day_node = $j(event_node).closest('[data-date]',this.div)[0];
+
+ return jQuery.extend({
+ event_node: event_node,
+ day_node: day_node,
+ },
+ event_node ? event_node.dataset : {},
+ day_node ? day_node.dataset : {}
+ );
+ },
+
+
+ /**
+ * Get time from position
+ *
+ * @param {number} x
+ * @param {number} y
+ * @returns {DOMNode[]} time node(s) for the given position
+ */
+ _get_time_from_position: function(x,y) {
+
+ x = Math.round(x);
+ y = Math.round(y);
+ var nodes = $j('.calendar_calAddEvent[data-hour]',this.div).removeClass('drop-hover').filter(function() {
+ var offset = $j(this).offset();
+ var range={x:[offset.left,offset.left+$j(this).outerWidth()],y:[offset.top,offset.top+$j(this).outerHeight()]};
+
+ var i = (x >=range.x[0] && x <= range.x[1]) && (y >= range.y[0] && y <= range.y[1]);
+ return i;
+ }).addClass("drop-hover");
+
+ return nodes;
+ },
+
+ /**
+ * Code for implementing et2_IDetachedDOM
+ *
+ * @param {array} _attrs array to add further attributes to
+ */
+ getDetachedAttributes: function(_attrs) {
+ _attrs.push('start_date','end_date');
+ },
+
+ getDetachedNodes: function() {
+ return [this.getDOMNode()];
+ },
+
+ setDetachedAttributes: function(_nodes, _values) {
+ this.div = $j(_nodes[0]);
+
+ if(_values.start_date)
+ {
+ this.set_start_date(_values.start_date);
+ }
+ if(_values.end_date)
+ {
+ this.set_end_date(_values.end_date);
+ }
+ },
+
+ // Resizable interface
+ resize: function (_height)
+ {
+ this.options.height = _height;
+ this.div.css('height', this.options.height);
+ }
+});
+et2_register_widget(et2_calendar_planner, ["calendar-planner"]);
\ No newline at end of file
diff --git a/calendar/js/et2_widget_planner_row.js b/calendar/js/et2_widget_planner_row.js
new file mode 100644
index 0000000000..73b2df673e
--- /dev/null
+++ b/calendar/js/et2_widget_planner_row.js
@@ -0,0 +1,391 @@
+/*
+ * Egroupware
+ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ * @package
+ * @subpackage
+ * @link http://www.egroupware.org
+ * @author Nathan Gray
+ * @version $Id$
+ */
+
+
+
+/**
+ * Class for one row of a planner
+ *
+ * This widget is responsible for the label on the side
+ *
+ * @augments et2_valueWidget
+ */
+var et2_calendar_planner_row = et2_valueWidget.extend([et2_IDetachedDOM],
+{
+ attributes: {
+ start_date: {
+ name: "Start date",
+ type: "any"
+ },
+ end_date: {
+ name: "End date",
+ type: "any"
+ },
+ value: {
+ type: "any"
+ }
+ },
+
+ /**
+ * Constructor
+ *
+ * @memberOf et2_calendar_daycol
+ */
+ init: function() {
+ this._super.apply(this, arguments);
+
+ // Main container
+ this.div = $j(document.createElement("div"))
+ .addClass("calendar_plannerRowWidget")
+ .css('width',this.options.width);
+ this.title = $j(document.createElement('div'))
+ .addClass("calendar_plannerRowHeader")
+ .css('width', '15%')
+ .appendTo(this.div);
+ this.rows = $j(document.createElement('div'))
+ .addClass("calendar_eventRows")
+ .appendTo(this.div);
+
+ this.setDOMNode(this.div[0]);
+
+ // Used for its date calculations
+ this.date_helper = et2_createWidget('date-time',{},null);
+ this.date_helper.loadingFinished();
+ },
+
+ doLoadingFinished: function() {
+ this._super.apply(this, arguments);
+
+ this.set_label(this.options.label);
+ this._draw();
+ return true;
+ },
+
+ destroy: function() {
+ this._super.apply(this, arguments);
+
+ // date_helper has no parent, so we must explicitly remove it
+ this.date_helper.destroy();
+ this.date_helper = null;
+ },
+
+ getDOMNode: function(_sender)
+ {
+ if(_sender === this || !_sender)
+ {
+ return this.div[0];
+ }
+ if(_sender._parent === this)
+ {
+ return this.rows[0];
+ }
+ },
+
+ /**
+ * Draw the individual divs for weekends and events
+ */
+ _draw: function() {
+ // Remove any existing
+ this.rows.empty().nextAll().remove();
+
+ var days = 31;
+ var width = 85;
+ if (this._parent.options.group_by === 'month')
+ {
+ days = new Date(this.options.end_date.getUTCFullYear(),this.options.end_date.getUTCMonth()+1,0).getUTCDate();
+ if(days < 31)
+ {
+ width = 85*days/31;
+ this.rows.css('width',width+'%');
+ }
+ }
+
+ // mark weekends and other special days in yearly planner
+ if (this._parent.options.group_by == 'month')
+ {
+ this.rows.append(this._yearlyPlannerMarkDays(this.options.start_date, days));
+ }
+
+ if (this._parent.options.group_by === 'month' && days < 31)
+ {
+ // add a filler for non existing days in that month
+ this.rows.after('');
+ }
+ },
+
+ set_label: function(label)
+ {
+ this.options.label = label;
+ this.title.text(label);
+ if(this._parent.options.group_by === 'month')
+ {
+ this.title.attr('data-date', this.options.start_date.toJSON());
+ this.title.addClass('et2_clickable');
+ }
+ else
+ {
+ this.title.attr('data-date','');
+ this.title.removeClass('et2_clickable');
+ }
+ },
+
+ /**
+ * Mark special days (birthdays, holidays) on the planner
+ *
+ * @param {Date} start Start of the month
+ * @param {number} days How many days in the month
+ */
+ _yearlyPlannerMarkDays: function(start,days)
+ {
+ var day_width = 100/days;
+ var t = new Date(start);
+ var content = '';
+ for(var left = 0,i = 0; i < days;left += day_width,++i)
+ {
+ var holidays = [];
+ // TODO: implement this, pull / copy data from et2_widget_timegrid
+ var day_class = this._parent.day_class_holiday(t,holidays);
+
+ if (day_class) // no regular weekday
+ {
+ content += '';
+ }
+ t.setUTCDate(t.getUTCDate()+1)
+ }
+ return content;
+ },
+
+ /**
+ * Load the event data for this day and create event widgets for each.
+ *
+ * If event information is not provided, it will be pulled from the content array.
+ *
+ * @param {Object[]} [_events] Array of event information, one per event.
+ */
+ _update_events: function(_events)
+ {
+ // Remove all events
+ while(this._children.length)
+ {
+ this._children[this._children.length-1].free();
+ this.removeChild(this._children[this._children.length-1]);
+ }
+
+ var rows = this._spread_events(_events);
+ var row = $j('').appendTo(this.rows);
+ var height = rows.length * (parseInt(window.getComputedStyle(row[0]).getPropertyValue("height")) || 20);
+ row.remove();
+
+ for(var c = 0; c < rows.length; c++)
+ {
+ // Calculate vertical positioning
+ var top = c * (100.0 / rows.length);
+
+ for(var i = 0; i < rows[c].length; i++)
+ {
+ // Calculate horizontal positioning
+ var left = this._time_to_position(rows[c][i].start);
+ var width = this._time_to_position(rows[c][i].end)-left;
+
+ // Create event
+ var event = et2_createWidget('calendar-event',{
+ id:rows[c][i].app_id||rows[c][i].id,
+ class: 'calendar_plannerEvent'
+ },this);
+ if(this.isInTree())
+ {
+ event.doLoadingFinished();
+ }
+ event.set_value(rows[c][i]);
+
+ // TODO
+ event._link_actions(this._parent.options.actions||{});
+
+ // Position the event
+ event.div.css('top', top+'%');
+ event.div.css('height', (100/rows.length)+'%');
+ event.div.css('left', left.toFixed(1)+'%');
+ event.div.css('width', width.toFixed(1)+'%');
+ }
+ }
+ if(height)
+ {
+ this.div.height(height+'px');
+ }
+ },
+
+ /**
+ * Sort a day's events into non-overlapping rows
+ *
+ * @param {Object[]} events
+ * @returns {Array[]} Events sorted into rows
+ */
+ _spread_events: function(events)
+ {
+ // sorting the events in non-overlapping rows
+ var rows = [];
+ var row_end = [0];
+
+ var start = this.options.start_date;
+ var end = this.options.end_date;
+
+ for(var n = 0; n < events.length; n++)
+ {
+ var event = events[n];
+ if(typeof event.start !== 'object')
+ {
+ this.date_helper.set_value(event.start);
+ event.start = new Date(this.date_helper.getValue());
+ }
+ if(typeof event.end !== 'object')
+ {
+ this.date_helper.set_value(event.end);
+ event.end = new Date(this.date_helper.getValue());
+ }
+ if(typeof event['start_m'] === 'undefined')
+ {
+
+ var day_start = event.start.valueOf() / 1000;
+ var dst_check = new Date(event.start);
+ dst_check.setUTCHours(12);
+
+ // if daylight saving is switched on or off, correct $day_start
+ // gives correct times after 2am, times between 0am and 2am are wrong
+ var daylight_diff = day_start + 12*60*60 - (dst_check.valueOf()/1000);
+ if(daylight_diff)
+ {
+ day_start -= daylight_diff;
+ }
+
+ event['start_m'] = parseInt((event.start.valueOf()/1000 - day_start) / 60);
+ if (event['start_m'] < 0)
+ {
+ event['start_m'] = 0;
+ event['multiday'] = true;
+ }
+ event['end_m'] = parseInt((event.end.valueOf()/1000 - day_start) / 60);
+ if (event['end_m'] >= 24*60)
+ {
+ event['end_m'] = 24*60-1;
+ event['multiday'] = true;
+ }
+ }
+
+ var event_start = new Date(events[n].start).valueOf();
+ for(var row = 0; row_end[row] > event_start; ++row); // find a "free" row (no other event)
+ if(typeof rows[row] === 'undefined') rows[row] = [];
+ rows[row].push(events[n]);
+ row_end[row] = new Date(events[n]['end']).valueOf();
+ }
+ return rows;
+ },
+
+ /**
+ * Calculates the horizontal position based on the time given, as a percentage
+ * between the start and end times
+ *
+ * @param {int|Date|string} time in minutes from midnight, or a Date in string or object form
+ * @param {int|Date|string} start Earliest possible time (0%)
+ * @param {int|Date|string} end Latest possible time (100%)
+ * @return {float} position in percent
+ */
+ _time_to_position: function(time, start, end)
+ {
+ var pos = 0.0;
+
+ // Handle the different value types
+ start = this.options.start_date;
+ end = this.options.end_date;
+
+ if(typeof start === 'string')
+ {
+ start = new Date(start);
+ end = new Date(end);
+ }
+ var wd_start = 60 * (parseInt(egw.preference('workdaystarts','calendar')) || 9);
+ var wd_end = 60 * (parseInt(egw.preference('workdayends','calendar')) || 17);
+
+ var t = time;
+ if(typeof time === 'number' && time < 3600)
+ {
+ t = new Date(start.valueOf() + wd_start * 3600*1000);
+ }
+ else
+ {
+ t = new Date(time);
+ }
+
+ // Limits
+ if(t <= start) return 0; // We are left of our scale
+ if(t >= end) return 100; // We are right of our scale
+
+ // Basic scaling, doesn't consider working times
+ pos = (t - start) / (end - start);
+
+
+ // Month view
+ if(this._parent.options.group_by !== 'month')
+ {
+ // Daywise scaling
+ var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate());
+ var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate());
+ var t_date = new Date(t.getUTCFullYear(), t.getUTCMonth(),t.getUTCDate());
+
+ var days = Math.round((end_date - start_date) / (24 * 3600 * 1000))+1;
+ pos = 1 / days * Math.round((t_date - start_date) / (24*3600 * 1000));
+
+ var time_of_day = typeof t === 'object' ? 60 * t.getUTCHours() + t.getUTCMinutes() : t;
+
+ if (time_of_day >= wd_start)
+ {
+ var day_percentage = 0.1;
+ if (time_of_day > wd_end)
+ {
+ day_percentage = 1;
+ }
+ else
+ {
+ var wd_length = wd_end - wd_start;
+ if (wd_length <= 0) wd_length = 24*60;
+ day_percentage = (time_of_day-wd_start) / wd_length; // between 0 and 1
+ }
+ pos += day_percentage / days;
+ }
+
+ }
+ pos = 100 * pos;
+
+ return pos;
+ },
+
+ /**
+ * Code for implementing et2_IDetachedDOM
+ *
+ * @param {array} _attrs array to add further attributes to
+ */
+ getDetachedAttributes: function(_attrs) {
+
+ },
+
+ getDetachedNodes: function() {
+ return [this.getDOMNode()];
+ },
+
+ setDetachedAttributes: function(_nodes, _values) {
+
+ },
+
+});
+
+et2_register_widget(et2_calendar_planner_row, ["calendar-planner_row"]);
\ No newline at end of file
diff --git a/calendar/js/et2_widget_timegrid.js b/calendar/js/et2_widget_timegrid.js
index 8ffbb48a00..e72dd848b5 100644
--- a/calendar/js/et2_widget_timegrid.js
+++ b/calendar/js/et2_widget_timegrid.js
@@ -331,28 +331,6 @@ var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResiz
this.dropEnd = drag_helper.call($j('.calendar_calEventHeader',ui.helper)[0],event,ui.helper[0],0);
$j('.calendar_timeDemo',ui.helper).css('bottom','auto');
});
-
- // Bind scroll event
- // When the user scrolls, we'll move enddate - startdate days
- this.div.on('wheel',jQuery.proxy(function(e) {
- var direction = e.originalEvent.deltaY > 0 ? 1 : -1;
-
- this.date_helper.set_value(this.options.end_date || this.options.start_date);
- var end = this.date_helper.get_time();
-
- this.date_helper.set_value(this.options.start_date);
- var start = this.date_helper.get_time();
-
- var delta = 1000 * 60 * 60 * 24 + Math.max(0,end - start);
-
- // TODO - actually fetch new data
- this.set_start_date(new Date(start + (delta * direction )));
- this.set_end_date(new Date(end + (delta * direction)));
-
- e.preventDefault();
- return false;
- },this));
-
return true;
},
diff --git a/calendar/templates/default/app.css b/calendar/templates/default/app.css
index 27f384bc3b..f597148b02 100644
--- a/calendar/templates/default/app.css
+++ b/calendar/templates/default/app.css
@@ -473,6 +473,12 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
border: 1px solid gray;
padding-right: 3px;
}
+.calendar_plannerWidget:nth-child(odd) {
+ background-color: #ffffff;
+}
+.calendar_plannerWidget:nth-child(even) {
+ background-color: #f2f2f2;
+}
/* calendar_plannerHeader contains a calendar_plannerHeaderTitle and multiple calendar_plannerHeaderRows
*/
@@ -481,6 +487,7 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
top: 0px;
left: 0px;
width: 100%;
+ background-color: #e0e0e0;
}
/* calendar_plannerRowWidget contains a calendar_plannerRowHeader and multiple eventRowWidgets in an calendar_eventRows
@@ -490,6 +497,7 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
top: 0px;
left: 0px;
width: 100%;
+ min-height: 20px;
}
/* calendar_plannerScale represents a scale-row of the calendar_plannerHeader, containing multiple planner{Day|Week|Month}Scales
@@ -553,6 +561,8 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
top: 0px;
left: 15%; /* need to be identical for calendar_eventRows and calendar_plannerHeaderRows and match width of calendar_plannerRowHeader/calendar_plannerHeaderTitle */
width: 85%;
+ min-height: 20px;
+ height: 100%;
}
/**
@@ -576,6 +586,9 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
height: 100%;
z-index: 10;
}
+.calendar_eventRowsMarkedDay.calendar_weekend {
+ background-color: #e0e0e0;
+}
/* calendar_eventRowWidget contains non-overlapping events
*/
diff --git a/calendar/templates/default/planner.xet b/calendar/templates/default/planner.xet
new file mode 100644
index 0000000000..22ad247ae0
--- /dev/null
+++ b/calendar/templates/default/planner.xet
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/calendar/templates/default/sidebox.xet b/calendar/templates/default/sidebox.xet
index 65c9ae6786..d3779d2665 100644
--- a/calendar/templates/default/sidebox.xet
+++ b/calendar/templates/default/sidebox.xet
@@ -31,7 +31,7 @@ Egroupware
-
+
diff --git a/calendar/templates/pixelegg/app.css b/calendar/templates/pixelegg/app.css
index af7fcf4050..b386d01e7b 100755
--- a/calendar/templates/pixelegg/app.css
+++ b/calendar/templates/pixelegg/app.css
@@ -11,7 +11,7 @@
* @package calendar
* @version $Id$
*/
-/* $Id: app.css 52977 2015-06-23 12:32:55Z hnategh $ */
+/* $Id: app.css 52979 2015-06-23 14:17:18Z hnategh $ */
/*Media print classes*/
@media print {
.th td,
@@ -473,6 +473,12 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
border: 1px solid gray;
padding-right: 3px;
}
+.calendar_plannerWidget:nth-child(odd) {
+ background-color: #ffffff;
+}
+.calendar_plannerWidget:nth-child(even) {
+ background-color: #f2f2f2;
+}
/* calendar_plannerHeader contains a calendar_plannerHeaderTitle and multiple calendar_plannerHeaderRows
*/
.calendar_plannerHeader {
@@ -480,6 +486,7 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
top: 0px;
left: 0px;
width: 100%;
+ background-color: #e0e0e0;
}
/* calendar_plannerRowWidget contains a calendar_plannerRowHeader and multiple eventRowWidgets in an calendar_eventRows
*/
@@ -488,6 +495,7 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
top: 0px;
left: 0px;
width: 100%;
+ min-height: 20px;
}
/* calendar_plannerScale represents a scale-row of the calendar_plannerHeader, containing multiple planner{Day|Week|Month}Scales
*/
@@ -560,6 +568,8 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
left: 15%;
/* need to be identical for calendar_eventRows and calendar_plannerHeaderRows and match width of calendar_plannerRowHeader/calendar_plannerHeaderTitle */
width: 85%;
+ min-height: 20px;
+ height: 100%;
}
/**
* Filler for month with less then 31 days in yearly planner
@@ -581,6 +591,9 @@ e.g. the div with class calendar_calTimeGrid is generated by the timeGridWidget
height: 100%;
z-index: 10;
}
+.calendar_eventRowsMarkedDay.calendar_weekend {
+ background-color: #e0e0e0;
+}
/* calendar_eventRowWidget contains non-overlapping events
*/
.calendar_eventRowWidget {