/*
* 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_daycol.js;
/calendar/js/et2_widget_event.js;
*/
/**
* Class which implements the "calendar-timegrid" XET-Tag for displaying a span of days
*
* This widget is responsible for the times on the side
*
* @augments et2_DOMWidget
*/
var et2_calendar_timegrid = et2_valueWidget.extend([et2_IDetachedDOM, et2_IResizeable],
{
createNamespace: true,
attributes: {
start_date: {
name: "Start date",
type: "any"
},
end_date: {
name: "End date",
type: "any"
},
value: {
type: "any",
description: "An array of events, indexed by date (Ymd format)."
},
day_start: {
name: "Day start time",
type: "string",
default: parseInt(egw.preference('workdaystarts','calendar')) || 9,
description: "Work day start time. If unset, this will default to the current user's preference"
},
day_end: {
name: "Day end time",
type: "string",
default: parseInt(egw.preference('workdayends','calendar')) || 17,
description: "Work day end time. If unset, this will default to the current user's preference"
},
show_weekend: {
name: "Weekends",
type: "boolean",
default: egw.preference('days_in_weekview','calendar') != 5,
description: "Display weekends. The date range should still include them for proper scrolling, but they just won't be shown."
},
granularity: {
name: "Granularity",
type: "integer",
default: parseInt(egw.preference('interval','calendar')) || 30,
description: "How many minutes per row"
},
extra_rows: {
name: "Extra rows",
type: "integer",
default: 2,
description: "Extra rows above and below the workday"
},
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"
},
height: {
"default": '100%'
}
},
/**
* Constructor
*
* @memberOf et2_calendar_timegrid
*/
init: function() {
this._super.apply(this, arguments);
// Main container
this.div = $j(document.createElement("div"))
.addClass("calendar_calTimeGrid");
// Contains times / rows
this.gridHeader = $j(document.createElement("div"))
.addClass("calendar_calGridHeader")
.appendTo(this.div);
// Contains days / columns
this.days = $j(document.createElement("div"))
.addClass("calendar_calDayCols")
.appendTo(this.div);
// Used for its date calculations
this.date_helper = et2_createWidget('date',{},null);
this.date_helper.loadingFinished();
// Used for owners
this.owner = et2_createWidget('select-account_ro',{},this);
// List of dates in Ymd
// The first one should be start_date, last should be end_date
this.day_list = [];
this.day_widgets = [];
// 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;
},
doLoadingFinished: function() {
this._super.apply(this, arguments);
this._drawGrid();
// 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);
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 + (end - start);// / (1000 * 60 * 60 * 24));
// 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))
// Bind context event to create actionobjects as needed
// TODO: Do it like this, or the normal way?
.on('contextmenu', jQuery.proxy(function(e) {
if(this.days.has(e.target).length)
{
var event = this._get_event_info(e.originalEvent.target);
this._link_event(event);
}
},this));
return true;
},
/**
* Something changed, and the days need to be re-drawn. We wait a bit to
* avoid re-drawing twice if start and end date both changed, then recreate
* the days.
* The whole grid is not regenerated because times aren't expected to change,
* just the days.
*
* @returns {undefined}
*/
invalidate: function() {
// Reset the list of days
this.day_list = [];
// 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.update_timer = null;
this._drawDays();
},this),ET2_GRID_INVALIDATE_TIMEOUT);
}
},
getDOMNode: function(_sender) {
if(_sender === this || !_sender)
{
return this.div[0];
}
else if (_sender.instanceOf(et2_calendar_daycol))
{
return this.days[0];
}
else if (_sender)
{
return this.gridHeader[0];
}
},
_drawGrid: function() {
this.div.css('height', this.options.height)
.empty();
// Draw in the horizontal - the times
this._drawTimes();
// Draw in the vertical - the days
this.div.append(this.days);
this._drawDays();
},
/**
* Creates the DOM nodes for the times in the left column, and the horizontal
* lines (mostly via CSS) that span the whole time span.
*/
_drawTimes: function() {
var wd_start = 60*this.options.day_start;
var wd_end = 60*this.options.day_end;
var granularity = this.options.granularity;
var totalDisplayMinutes = wd_end - wd_start;
var rowsToDisplay = (totalDisplayMinutes/granularity)+2+2*this.options.extra_rows;
var rowHeight = (100/rowsToDisplay).toFixed(1);
// ensure a minimum height of each row
if (this.options.height < (rowsToDisplay+1) * 12)
{
this.options.height = (rowsToDisplay+1) * 12;
}
this.gridHeader
.css('height', rowHeight+'%')
.text(this.options.label)
.appendTo(this.div);
// the hour rows
var show = {
5 : [0,15,30,45],
10 : [0,30],
15 : [0,30],
45 : [0,15,30,45]
};
var html = '';
for(var t = wd_start,i = 1+this.options.extra_rows; t <= wd_end; t += granularity,++i)
{
html += '
';
// show time for full hours, always for 45min interval and at least on every 3 row
var time = jQuery.datepicker.formatTime(
egw.preference("timeformat") == 12 ? "h:mmtt" : "HH:mm",
{
hour: t / 60,
minute: t % 60,
seconds: 0,
timezone: 0
},
{"ampm": (egw.preference("timeformat") == "12")}
);
var time_label = (typeof show[granularity] === 'undefined' ? t % 60 === 0 : show[granularity].indexOf(t % 60) !== -1) ? time : '';
html += '
'+time_label+"
\n";
}
this.div.append(html);
},
/**
* Set up the needed day widgets to correctly display the selected date
* range. First we calculate the needed dates, then we create any needed
* widgets. Existing widgets are recycled rather than discarded.
*/
_drawDays: function() {
// If day list is still empty, recalculate it from start & end date
if(this.day_list.length === 0)
{
this.day_list = this._calculate_day_list(this.options.start_date, this.options.end_date, this.options.show_weekend);
}
// Create any needed widgets - otherwise, we'll just recycle
// Add any needed day widgets (now showing more days)
while(this.day_list.length > this.day_widgets.length)
{
var day = et2_createWidget('calendar-daycol',{
owner: this.options.owner
},this);
if(this.isInTree())
{
day.doLoadingFinished();
}
this.day_widgets.push(day);
}
// Remove any extra day widgets (now showing less)
var delete_index = this.day_widgets.length - 1;
while(this.day_widgets.length > this.day_list.length)
{
// If we're going down to an existing one, just keep it for cool CSS animation
while(this.day_list.indexOf(this.day_widgets[delete_index].options.date) > -1)
{
delete_index--;
}
this.day_widgets[delete_index].set_width('0px');
this.day_widgets[delete_index].free();
this.day_widgets.splice(delete_index--,1);
}
// Create / update day widgets with dates and data, if available
for(var i = 0; i < this.day_list.length; i++)
{
day = this.day_widgets[i];
// Set the date, and pass any data we have
day.set_date(this.day_list[i], this.value[this.day_list[i]] || false);
day.set_id(this.day_list[i]);
day.set_width((100/this.day_list.length).toFixed(2) + '%');
// Position
$j(day.getDOMNode()).css('left', ((100/this.day_list.length).toFixed(2) * i) + '%');
}
// Update actions
if(this._actionManager)
{
this._link_actions(this._actionManager.children);
}
// TODO: Figure out how to do this with detached nodes
/*
var nodes = this.day_col.getDetachedNodes();
var supportedAttrs = [];
this.day_col.getDetachedAttributes(supportedAttrs);
supportedAttrs.push("id");
for(var i = 0; i < day_count; i++)
{
this.day_col.setDetachedAttributes(nodes.clone(),)
}
*/
},
/**
* Calculate a list of days between start and end date, skipping weekends if
* desired.
*
* @param {Date|string} start_date Date that et2_date widget can understand
* @param {Date|string} end_date Date that et2_date widget can understand
* @param {boolean} show_weekend If not showing weekend, Saturday and Sunday
* will not be in the returned list.
*
* @returns {string[]} List of days in Ymd format
*/
_calculate_day_list: function(start_date, end_date, show_weekend) {
var day_list = [];
this.date_helper.set_value(end_date);
var end = this.date_helper.date.getTime();
var i = 1;
this.date_helper.set_value(start_date);
do
{
if(show_weekend || !show_weekend && [0,6].indexOf(this.date_helper.date.getUTCDay()) === -1)
{
day_list.push(''+this.date_helper.get_year() + sprintf('%02d',this.date_helper.get_month()) + sprintf('%02d',this.date_helper.get_date()));
}
this.date_helper.set_date(this.date_helper.get_date()+1);
}
// Limit it to 14 days to avoid infinite loops in case something is mis-set,
// though the limit is more based on how wide the screen is
while(end >= this.date_helper.date.getTime() && i <= 14)
return day_list;
},
/**
* Link the actions to the DOM nodes / widget bits.
*
* @param {object} actions {ID: {attributes..}+} map of egw action information
*/
_link_actions: function(actions)
{
this._super.apply(this, arguments);
// Get the top level element for the tree
var objectManager = egw_getAppObjectManager(true);
var widget_object = objectManager.getObjectById(this.id);
// Time grid is just a container
widget_object.flags = EGW_AO_FLAG_IS_CONTAINER;
},
/**
* Bind a single event as needed to the action system.
*
* @param {Object} event
*/
_link_event: function(event)
{
if(!event || !event.app_id) return;
// Go over the widget & add links - this is where we decide which actions are
// 'allowed' for this widget at this time
var objectManager = egw_getObjectManager(this.id,false);
if(objectManager == null)
{
// No actions set up
return;
}
var obj = null;
debugger;
if(!(obj = objectManager.getObjectById(event.app_id)))
{
obj = objectManager.addObject(event.app_id, new et2_action_object_impl(this,event.event_node));
obj.data = event;
obj.updateActionLinks(objectManager.actionLinks)
}
objectManager.setAllSelected(false);
obj.setSelected(true);
objectManager.updateSelectedChildren(obj,true)
},
/**
* 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;
}
this.value = events;
var day_list = Object.keys(events);
this.set_start_date(day_list[0]);
this.set_end_date(day_list[day_list.length-1]);
// Reset and calculate instead of just use the keys so we can get the weekend preference
this.day_list = [];
},
/**
* Change the start date
*
* @param {string|number|Date} new_date New starting date
* @returns {undefined}
*/
set_start_date: function(new_date)
{
// 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();
}
},
/**
* Change the end date
*
* @param {string|number|Date} new_date New end date
* @returns {undefined}
*/
set_end_date: function(new_date)
{
// 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.end_date;
this.options.end_date = this.date_helper.getValue();
if(old_date !== this.options.end_date && this.isAttached())
{
this.invalidate();
}
},
get_granularity: function()
{
// get option, or user's preference
if(typeof this.options.granularity === 'undefined')
{
this.options.granularity = egw.preference('interval','calendar') || 30;
}
return parseInt(this.options.granularity);
},
/**
* 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.days.has(_ev.target).length)
{
// 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)
{
this._edit_event(event);
return false;
}
return result;
}
else
{
// Default handler to open a new event at the selected time
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 : {}
);
},
_edit_event: function(event)
{
if(event.recur_type)
{
var edit_id = event.id;
var edit_date = event.start;
var that = this;
var buttons = [
{text: this.egw().lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true},
{text: this.egw().lang("Edit series"), id:"series"},
{text: this.egw().lang("Cancel"), id:"cancel"}
];
et2_dialog.show_dialog(function(_button_id)
{
switch(_button_id)
{
case 'exception':
that.egw().open(edit_id, 'calendar', 'edit', {date:edit_date,exception: '1'});
break;
case 'series':
that.egw().open(edit_id, 'calendar', 'edit', {date:edit_date});
break;
case 'cancel':
default:
break;
}
},this.egw().lang("Do you want to edit this event as an exception or the whole series?"),
this.egw().lang("This event is part of a series"), {}, buttons, et2_dialog.WARNING_MESSAGE);
}
else
{
this.egw().open(event.id, event.app||'calendar','edit');
}
},
/**
* Set which user owns this. Owner is passed along to the individual
* days.
*
* @param {number} _owner Account ID
* @returns {undefined}
*/
set_owner: function(_owner)
{
// Let select-account widget handle value validation
this.owner.set_value(_owner);
this.options.owner = _owner;//this.owner.getValue();
for (var i = this._children.length - 1; i >= 0; i--)
{
if(typeof this._children[i].set_owner === 'function')
{
this._children[i].set_owner(this.options.owner);
}
}
},
/**
* 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_timegrid, ["calendar-timegrid"]);