mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 14:41:29 +01:00
5e3c67a5cf
classes are now uppercase and in their own files. lowercase classes are deprecated. Interfaces are now actual interfaces that should be implemented instead of creating and returning an ai Object every time
2676 lines
75 KiB
TypeScript
2676 lines
75 KiB
TypeScript
/*
|
|
* Egroupware Calendar timegrid
|
|
*
|
|
* @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package calendar
|
|
* @subpackage etemplate
|
|
* @link https://www.egroupware.org
|
|
* @author Nathan Gray
|
|
*/
|
|
|
|
|
|
/*egw:uses
|
|
/calendar/js/et2_widget_view.js;
|
|
/calendar/js/et2_widget_planner_row.js;
|
|
/calendar/js/et2_widget_event.js;
|
|
*/
|
|
|
|
import {et2_createWidget, et2_register_widget, WidgetConfig} from "../../api/js/etemplate/et2_core_widget";
|
|
import {ClassWithAttributes} from "../../api/js/etemplate/et2_core_inheritance";
|
|
import {et2_calendar_view} from "./et2_widget_view";
|
|
import {et2_action_object_impl} from "../../api/js/etemplate/et2_core_DOMWidget";
|
|
import {et2_calendar_event} from "./et2_widget_event";
|
|
import {et2_calendar_planner_row} from "./et2_widget_planner_row";
|
|
import {egw} from "../../api/js/jsapi/egw_global";
|
|
import {egw_getObjectManager, egwActionObject} from "../../api/js/egw_action/egw_action";
|
|
import {
|
|
EGW_AI_DRAG_ENTER,
|
|
EGW_AI_DRAG_OUT,
|
|
EGW_AO_FLAG_IS_CONTAINER
|
|
} from "../../api/js/egw_action/egw_action_constants";
|
|
import {et2_IDetachedDOM, et2_IPrint, et2_IResizeable} from "../../api/js/etemplate/et2_core_interfaces";
|
|
import {et2_compileLegacyJS} from "../../api/js/etemplate/et2_core_legacyJSFunctions";
|
|
import {et2_no_init} from "../../api/js/etemplate/et2_core_common";
|
|
import {CalendarApp} from "./app";
|
|
import {sprintf} from "../../api/js/egw_action/egw_action_common";
|
|
import {et2_dataview_grid} from "../../api/js/etemplate/et2_dataview_view_grid";
|
|
import {formatDate, formatTime} from "../../api/js/etemplate/Et2Date/Et2Date";
|
|
import interact from "@interactjs/interactjs/index";
|
|
import type {InteractEvent} from "@interactjs/core/InteractEvent";
|
|
import {StaticOptions} from "../../api/js/etemplate/Et2Select/StaticOptions";
|
|
import {SelectOption} from "../../api/js/etemplate/Et2Select/FindSelectOptions";
|
|
|
|
/**
|
|
* Class which implements the "calendar-planner" XET-Tag for displaying a longer
|
|
* ( > 10 days) span of time. Events can be grouped into rows by either user,
|
|
* category, or month. Their horizontal position and size in the row is determined
|
|
* by their start date and duration relative to the displayed date range.
|
|
*
|
|
* @augments et2_calendar_view
|
|
*/
|
|
export class et2_calendar_planner extends et2_calendar_view implements et2_IDetachedDOM, et2_IResizeable, et2_IPrint
|
|
{
|
|
static readonly _attributes: any = {
|
|
group_by: {
|
|
name: "Group by",
|
|
type: "string", // or category ID
|
|
default: "0",
|
|
description: "Display planner by 'user', 'month', or the given category"
|
|
},
|
|
filter: {
|
|
name: "Filter",
|
|
type: "string",
|
|
default: '',
|
|
description: 'A filter that is used to select events. It is passed along when events are queried.'
|
|
},
|
|
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."
|
|
},
|
|
hide_empty: {
|
|
name: "Hide empty rows",
|
|
type: "boolean",
|
|
default: false,
|
|
description: "Hide rows with no events."
|
|
},
|
|
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."
|
|
}
|
|
};
|
|
|
|
public static readonly DEFERRED_ROW_TIME: number = 100;
|
|
|
|
private gridHeader: JQuery;
|
|
private headerTitle: JQuery;
|
|
private headers: JQuery;
|
|
private rows: JQuery;
|
|
private grid: JQuery;
|
|
private vertical_bar: JQuery;
|
|
|
|
private doInvalidate: boolean;
|
|
|
|
private registeredCallbacks: any[];
|
|
private cache: {};
|
|
private _deferred_row_updates: {};
|
|
private grouper: any;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
|
|
{
|
|
// Call the inherited constructor
|
|
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_planner._attributes, _child || {}));
|
|
|
|
|
|
// Main container
|
|
this.div = jQuery(document.createElement("div"))
|
|
.addClass("calendar_plannerWidget");
|
|
|
|
// Header
|
|
this.gridHeader = jQuery(document.createElement("div"))
|
|
.addClass("calendar_plannerHeader")
|
|
.appendTo(this.div);
|
|
this.headerTitle = jQuery(document.createElement("div"))
|
|
.addClass("calendar_plannerHeaderTitle")
|
|
.appendTo(this.gridHeader);
|
|
this.headers = jQuery(document.createElement("div"))
|
|
.addClass("calendar_plannerHeaderRows")
|
|
.appendTo(this.gridHeader);
|
|
|
|
this.rows = jQuery(document.createElement("div"))
|
|
.addClass("calendar_plannerRows")
|
|
.appendTo(this.div);
|
|
this.grid = jQuery(document.createElement("div"))
|
|
.addClass("calendar_plannerGrid")
|
|
.appendTo(this.div);
|
|
|
|
this.vertical_bar = jQuery(document.createElement("div"))
|
|
.addClass('verticalBar')
|
|
.appendTo(this.div);
|
|
|
|
this.value = [];
|
|
|
|
// Update timer, to avoid redrawing twice when changing start & end date
|
|
this.update_timer = null;
|
|
this.doInvalidate = true;
|
|
|
|
this.setDOMNode(this.div[0]);
|
|
|
|
this.registeredCallbacks = [];
|
|
this.cache = {};
|
|
this._deferred_row_updates = {};
|
|
}
|
|
|
|
destroy( )
|
|
{
|
|
super.destroy();
|
|
|
|
this.div.off();
|
|
|
|
for(var i = 0; i < this.registeredCallbacks.length; i++)
|
|
{
|
|
egw.dataUnregisterUID(this.registeredCallbacks[i],null,this);
|
|
}
|
|
}
|
|
|
|
doLoadingFinished( )
|
|
{
|
|
super.doLoadingFinished();
|
|
|
|
// Don't bother to draw anything if there's no date yet
|
|
if(this.options.start_date)
|
|
{
|
|
this._drawGrid();
|
|
}
|
|
|
|
// Automatically bind drag and resize for every event using jQuery directly
|
|
// - no action system -
|
|
var planner = this;
|
|
|
|
this.cache = {};
|
|
this._deferred_row_updates = {};
|
|
|
|
/**
|
|
* If user puts the mouse over an event, then we'll set up resizing so
|
|
* they can adjust the length. Should be a little better on resources
|
|
* than binding it for every calendar event.
|
|
*/
|
|
this.div.on('mouseover', '.calendar_calEvent:not(.ui-resizable):not(.rowNoEdit)', function()
|
|
{
|
|
// Load the event
|
|
planner._get_event_info(this);
|
|
var that = this;
|
|
|
|
//Resizable event handler
|
|
interact(this).resizable
|
|
({
|
|
invert: "reposition",
|
|
edges: {right: true},
|
|
startAxis: "x",
|
|
lockAxis: "x",
|
|
containment: 'parent',
|
|
|
|
|
|
/**
|
|
* If dragging to resize an event, abort drag to create
|
|
*
|
|
* @param {InteractEvent} event
|
|
*/
|
|
onstart: function(event : InteractEvent)
|
|
{
|
|
if(planner.drag_create.start)
|
|
{
|
|
// Abort drag to create, we're dragging to resize
|
|
planner._drag_create_end({});
|
|
}
|
|
event.target.classList.add("resizing");
|
|
},
|
|
|
|
/**
|
|
* Triggered at the end of resizing the calEvent.
|
|
*
|
|
* @param {InteractEvent} event
|
|
*/
|
|
onend: function(event : InteractEvent)
|
|
{
|
|
interact(this).unset();
|
|
var e = new jQuery.Event('change');
|
|
e.originalEvent = event;
|
|
e.data = {duration: 0};
|
|
var event_data = planner._get_event_info(this);
|
|
var event_widget = planner.getWidgetById(event_data.widget_id);
|
|
var sT = event_widget.options.value.start_m;
|
|
if(typeof this.dropEnd != 'undefined')
|
|
{
|
|
var eT = parseInt(this.dropEnd.getUTCHours() * 60) + parseInt(this.dropEnd.getUTCMinutes());
|
|
e.data.duration = ((eT - sT) / 60) * 3600;
|
|
|
|
if(event_widget)
|
|
{
|
|
event_widget.options.value.end_m = eT;
|
|
event_widget.options.value.duration = e.data.duration;
|
|
}
|
|
|
|
// Leave the helper there until the update is done
|
|
var loading = event_data.event_node;
|
|
|
|
// and add a loading icon so user knows something is happening
|
|
jQuery('.calendar_timeDemo', loading).after('<div class="loading"></div>');
|
|
|
|
jQuery(this).trigger(e);
|
|
|
|
// Remove loading, done or not
|
|
loading.remove();
|
|
}
|
|
// Clear the helper, re-draw
|
|
if(event_widget)
|
|
{
|
|
(<et2_calendar_planner_row>event_widget.getParent()).position_event(event_widget);
|
|
}
|
|
}.bind(this),
|
|
|
|
/**
|
|
* Triggered during the resize, on the drag of the resize handler
|
|
*
|
|
* @param {InteractEvent} event
|
|
*/
|
|
onmove: function(event : InteractEvent)
|
|
{
|
|
event.target.style.width = event.rect.width + "px";
|
|
let position;
|
|
if(planner.options.group_by == 'month')
|
|
{
|
|
position = {left: event.clientX, top: event.clientY};
|
|
}
|
|
else
|
|
{
|
|
let offset = parseInt(getComputedStyle(event.target).left) - event.rect.left;
|
|
position = {top: event.rect.top, left: event.rect.right + offset};
|
|
}
|
|
planner._drag_helper(this, position, event.rect.height);
|
|
}.bind(this)
|
|
});
|
|
});
|
|
this.div
|
|
.on('mousemove', function(event)
|
|
{
|
|
// Ignore headers
|
|
if(planner.headers.has(event.target).length !== 0)
|
|
{
|
|
planner.vertical_bar.hide();
|
|
return;
|
|
}
|
|
// Position bar by mouse
|
|
planner.vertical_bar.css("left", (event.clientX - planner.grid.offset().left + 120) + "px");
|
|
planner.vertical_bar.css('top', '0px');
|
|
|
|
// Get time at mouse
|
|
if(jQuery(event.target).closest('.calendar_eventRows').length == 0)
|
|
{
|
|
// "Invalid" times, from space after the last planner row, or header
|
|
var time = planner._get_time_from_position(event.pageX - planner.grid.offset().left, 10);
|
|
}
|
|
else if(planner.options.group_by == 'month')
|
|
{
|
|
var time = planner._get_time_from_position(event.clientX, event.clientY);
|
|
}
|
|
else
|
|
{
|
|
var time = planner._get_time_from_position(event.offsetX, event.offsetY);
|
|
}
|
|
// Passing to formatter, cancel out timezone
|
|
if(time)
|
|
{
|
|
const formatDate = new Date(time.valueOf() + time.getTimezoneOffset() * 60 * 1000);
|
|
planner.vertical_bar
|
|
.html('<span>'+date(egw.preference('timeformat','calendar') == 12 ? 'h:ia' : 'H:i',formatDate)+'</span>')
|
|
.show();
|
|
|
|
if(planner.drag_create.event && planner.drag_create.parent && planner.drag_create.end)
|
|
{
|
|
|
|
planner.drag_create.end.date = time.toJSON();
|
|
planner._drag_update_event();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No (valid) time, just hide
|
|
planner.vertical_bar.hide();
|
|
}
|
|
})
|
|
.on('mousedown', jQuery.proxy(this._mouse_down, this))
|
|
.on('mouseup', jQuery.proxy(this._mouse_up, this));
|
|
|
|
// 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.getParent().options.actions || []);
|
|
|
|
// Customize and override some draggable settings
|
|
this.div
|
|
.on('dragstart', '.calendar_calEvent', function(event)
|
|
{
|
|
// Cancel drag to create, we're dragging an existing event
|
|
planner._drag_create_end();
|
|
});
|
|
return true;
|
|
}
|
|
|
|
_createNamespace() {
|
|
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() : string
|
|
{
|
|
return this.egw().lang('User');
|
|
},
|
|
// Column headers
|
|
headers: async 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 >= 6)
|
|
{
|
|
this.headers.append(this._header_months(start, day_count));
|
|
}
|
|
if(day_count < 120)
|
|
{
|
|
var weeks = this._header_weeks(start, day_count);
|
|
this.headers.append(weeks);
|
|
this.grid.append(weeks);
|
|
}
|
|
if(day_count < 60)
|
|
{
|
|
var days = await this._header_days(start, day_count);
|
|
this.headers.append(days);
|
|
this.grid.append(days);
|
|
}
|
|
if(day_count <= 7)
|
|
{
|
|
var hours = this._header_hours(start, day_count);
|
|
this.headers.append(hours);
|
|
this.grid.append(hours);
|
|
}
|
|
},
|
|
// Labels for the rows
|
|
row_labels: function() {
|
|
let labels = [];
|
|
let already_added = [];
|
|
let options = [];
|
|
let resource = null;
|
|
let owner = null;
|
|
if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner'))
|
|
{
|
|
owner = app.calendar.sidebox_et2.getWidgetById('owner')
|
|
}
|
|
else
|
|
{
|
|
owner = this.getArrayMgr("sel_options").getRoot().getEntry('owner');
|
|
}
|
|
options = owner.select_options;
|
|
for(var i = 0; i < this.options.owner.length; i++)
|
|
{
|
|
var user = this.options.owner[i];
|
|
// Handle grouped resources like mailing lists - pull it from sidebox owner
|
|
// and expand to their contents
|
|
if(options && options.find &&
|
|
((resource = options.find(function(element) {return element.value == user;}) || {}) || isNaN(user)))
|
|
{
|
|
if(resource && resource.resources)
|
|
{
|
|
for(var j = 0; j < resource.resources.length; j++)
|
|
{
|
|
var id = resource.resources[j];
|
|
if(already_added.indexOf('' + id) < 0)
|
|
{
|
|
labels.push({
|
|
id: id,
|
|
label: this._get_owner_name(id)||'',
|
|
data: {participants:id,owner:id}
|
|
});
|
|
already_added.push(''+id);
|
|
}
|
|
}
|
|
}
|
|
else if (user < 0)
|
|
{
|
|
// Group, but no users found. Need those.
|
|
egw.accountData(parseInt(user),'account_fullname',true,function(result) {
|
|
this.invalidate();
|
|
},this);
|
|
}
|
|
else if(already_added.indexOf(''+user) < 0 && (isNaN(user) || parseInt(user) >= 0))
|
|
{
|
|
labels.push({
|
|
id: user,
|
|
label: this._get_owner_name(user),
|
|
data: {participants:user,owner:user}
|
|
});
|
|
already_added.push(''+user);
|
|
}
|
|
}
|
|
else if (user < 0) // groups
|
|
{
|
|
egw.accountData(parseInt(user),'account_fullname',true,function(result) {
|
|
for(var id in result)
|
|
{
|
|
if(already_added.indexOf(''+id) < 0)
|
|
{
|
|
this.push({id: id, label: result[id]||'', data: {participants:id,owner:id}});
|
|
already_added.push(''+id);
|
|
}
|
|
}
|
|
},labels);
|
|
}
|
|
else // users
|
|
{
|
|
if(already_added.indexOf(user) < 0)
|
|
{
|
|
var label = this._get_owner_name(user)||'';
|
|
labels.push({id: user, label: label, data: {participants:user,owner:''}});
|
|
already_added.push(''+user);
|
|
}
|
|
}
|
|
}
|
|
|
|
return labels.sort(function(a,b) {
|
|
return a.label.localeCompare(b.label);
|
|
});
|
|
},
|
|
// 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;
|
|
}
|
|
var participants = event.participants;
|
|
var add_row = function(user, participant) {
|
|
var label_index = false;
|
|
for(var i = 0; i < labels.length; i++)
|
|
{
|
|
if(labels[i].id == user)
|
|
{
|
|
label_index = i;
|
|
break;
|
|
}
|
|
}
|
|
if(participant && label_index !== false && status_to_show.indexOf(participant.substr(0,1)) >= 0 ||
|
|
!participant && label_index !== false ||
|
|
this.options.filter === 'owner' && event.owner === user)
|
|
{
|
|
if(typeof rows[label_index] === 'undefined')
|
|
{
|
|
rows[label_index] = [];
|
|
}
|
|
rows[label_index].push(event);
|
|
}
|
|
};
|
|
for(let user in participants)
|
|
{
|
|
var participant = participants[user];
|
|
|
|
if(parseInt(user) < 0) // groups
|
|
{
|
|
let owner = null;
|
|
let options = [];
|
|
if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner'))
|
|
{
|
|
owner = app.calendar.sidebox_et2.getWidgetById('owner')
|
|
}
|
|
else
|
|
{
|
|
owner = this.getArrayMgr("sel_options").getRoot().getEntry('owner');
|
|
}
|
|
options = owner.select_options.find((o) => o.value == user).resources || [];
|
|
|
|
for(let i = 0; i < options.length; i++)
|
|
{
|
|
if(!participants[options[i]])
|
|
{
|
|
add_row.call(this, options[i], participant);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
add_row.call(this, user, participant);
|
|
}
|
|
},
|
|
// Draw a single row
|
|
draw_row: function(sort_key, label, events) {
|
|
var row = this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date);
|
|
if(this.options.hide_empty && !events.length)
|
|
{
|
|
row.set_disabled(true);
|
|
}
|
|
// Highlight current user, sort_key is account_id
|
|
if(sort_key === egw.user('account_id'))
|
|
{
|
|
row.set_class('current_user')
|
|
}
|
|
// Set account_id so event.owner_check can use it
|
|
row.options.owner = sort_key;
|
|
|
|
// Since the daywise cache is by user, we can tap in here
|
|
var t = new Date(this.options.start_date);
|
|
var end = new Date(this.options.end_date);
|
|
do
|
|
{
|
|
var cache_id = CalendarApp._daywise_cache_id(t, sort_key);
|
|
egw.dataRegisterUID(cache_id, row._data_callback, row);
|
|
|
|
t.setUTCDate(t.getUTCDate() + 1);
|
|
}
|
|
while(t < end);
|
|
return row;
|
|
}
|
|
},
|
|
|
|
// 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++)
|
|
{
|
|
// Not using UTC because we corrected for timezone offset
|
|
labels.push({id:sprintf('%04d-%02d', d.getFullYear(), d.getMonth()), label:this.egw().lang(date('F',d))+' '+d.getFullYear()});
|
|
d.setMonth(d.getMonth()+1);
|
|
}
|
|
return labels;
|
|
},
|
|
group: function(labels, rows,event) {
|
|
// Yearly planner does not show infologs
|
|
if(event && event.app && event.app == 'infolog') return;
|
|
|
|
var start = new Date(event.start);
|
|
start = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
|
|
var key = sprintf('%04d-%02d', start.getFullYear(), start.getMonth());
|
|
var label_index : number|boolean = false;
|
|
for(var i = 0; i < labels.length; i++)
|
|
{
|
|
if(labels[i].id == key)
|
|
{
|
|
label_index = i;
|
|
break;
|
|
}
|
|
}
|
|
if(label_index)
|
|
{
|
|
if(typeof rows[label_index] === 'undefined')
|
|
{
|
|
rows[label_index] = [];
|
|
}
|
|
rows[label_index].push(event);
|
|
}
|
|
|
|
// end in a different month?
|
|
var end = new Date(event.end);
|
|
end = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000);
|
|
var end_key = sprintf('%04d-%02d',end.getFullYear(),end.getMonth());
|
|
var year = start.getFullYear();
|
|
var month = start.getMonth();
|
|
key = sprintf('%04d-%02d',year,month);
|
|
|
|
do
|
|
{
|
|
var end_label_index = typeof label_index == "boolean" ? 0 : label_index;
|
|
|
|
for(let i = end_label_index; i < labels.length; i++)
|
|
{
|
|
if(labels[i].id == key)
|
|
{
|
|
end_label_index = i;
|
|
if(typeof rows[end_label_index] === 'undefined')
|
|
{
|
|
rows[end_label_index] = [];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if(end_label_index != label_index)
|
|
{
|
|
rows[end_label_index].push(event);
|
|
}
|
|
if (++month > 11)
|
|
{
|
|
++year;
|
|
month = 0;
|
|
}
|
|
key = sprintf('%04d-%02d',year,month);
|
|
} while(key <= end_key)
|
|
},
|
|
// Draw a single row, but split up the dates
|
|
draw_row: function(sort_key, label, events)
|
|
{
|
|
var key = sort_key.split('-');
|
|
var start = new Date(key[0]+"-"+sprintf("%02d",parseInt(key[1])+1)+"-01T00:00:00Z");
|
|
// Use some care to avoid issues with timezones and daylight savings
|
|
var end = new Date(start);
|
|
end.setUTCMonth(start.getUTCMonth() + 1);
|
|
end.setUTCDate(1);
|
|
end.setUTCHours(0);
|
|
end.setUTCMinutes(0);
|
|
end = new Date(end.valueOf() - 1000);
|
|
end.setUTCMonth(start.getUTCMonth());
|
|
this._drawRow(sort_key, label, events, start, end);
|
|
}
|
|
},
|
|
// Group by category has one row for each [sub]category
|
|
category:
|
|
{
|
|
title: function() { return this.egw().lang('Category');},
|
|
headers: async 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 >= 6)
|
|
{
|
|
this.headers.append(this._header_months(start, day_count));
|
|
}
|
|
if(day_count < 120)
|
|
{
|
|
var weeks = this._header_weeks(start, day_count);
|
|
this.headers.append(weeks);
|
|
this.grid.append(weeks);
|
|
}
|
|
if(day_count < 60)
|
|
{
|
|
var days = await this._header_days(start, day_count);
|
|
this.headers.append(days);
|
|
this.grid.append(days);
|
|
}
|
|
if(day_count <= 7)
|
|
{
|
|
var hours = this._header_hours(start, day_count);
|
|
this.headers.append(hours);
|
|
this.grid.append(hours);
|
|
}
|
|
},
|
|
row_labels: function()
|
|
{
|
|
var im = this.getInstanceManager();
|
|
var labels = [];
|
|
var categories = <SelectOption[]>StaticOptions.cached_server_side(this, "cat", ',,,calendar', false);
|
|
if(!categories || categories.length == 0)
|
|
{
|
|
// No categories at all? Probably loading before sidebox is done. Ask directly and wait for them, rather than firing
|
|
// 50 different requests.
|
|
egw.json(
|
|
'EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options',
|
|
['select-cat', ',,,calendar'],
|
|
function(data)
|
|
{
|
|
categories = data;
|
|
}
|
|
).sendRequest(false);
|
|
}
|
|
|
|
let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar;
|
|
if(!app_calendar.state.cat_id ||
|
|
app_calendar.state.cat_id.toString() === '' ||
|
|
app_calendar.state.cat_id.toString() == '0'
|
|
)
|
|
{
|
|
app_calendar.state.cat_id = '';
|
|
labels.push({id: '', value: '', label: egw.lang('none'), main: '', data: {}});
|
|
labels = labels.concat(categories);
|
|
}
|
|
else
|
|
{
|
|
var cat_id = app_calendar.state.cat_id;
|
|
if(typeof cat_id == 'string')
|
|
{
|
|
cat_id = cat_id.split(',');
|
|
}
|
|
for(var i = 0; i < cat_id.length; i++)
|
|
{
|
|
// Find label for that category
|
|
let cat = null;
|
|
for(let j = 0; j < categories.length; j++)
|
|
{
|
|
if(categories[j].value == cat_id[i])
|
|
{
|
|
cat = categories[j];
|
|
categories[j].id = categories[j].value;
|
|
labels.push(categories[j]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Get its children immediately
|
|
if(cat && cat.children === "")
|
|
{
|
|
continue;
|
|
}
|
|
else if(!cat || !cat.children || categories.filter(o => cat.children.find(c => o.value == c)).length != cat.children.length)
|
|
{
|
|
egw.json(
|
|
'EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options',
|
|
['select-cat', ',,,calendar,' + cat_id[i]],
|
|
function(data)
|
|
{
|
|
labels = labels.concat(data);
|
|
}
|
|
).sendRequest(false);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
for(var i = labels.length -1; i >= 0; i--)
|
|
{
|
|
labels[i].id = labels[i].value;
|
|
labels[i].data = {
|
|
cat_id: labels[i].id,
|
|
main: labels[i].value==labels[i].main
|
|
};
|
|
if(labels[i].children && labels[i].children.length)
|
|
{
|
|
labels[i].data.has_children = true;
|
|
}
|
|
}
|
|
return labels;
|
|
},
|
|
group: function(labels, rows, event) {
|
|
var cats = event.category;
|
|
let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar;
|
|
if(typeof event.category === 'string')
|
|
{
|
|
cats = cats.split(',');
|
|
}
|
|
for(var cat = 0; cat < cats.length; cat++)
|
|
{
|
|
var label_index = false;
|
|
var category = cats[cat] ? parseInt(cats[cat],10) : false;
|
|
if(category == 0 || !category) category = '';
|
|
for(var i = 0; i < labels.length; i++)
|
|
{
|
|
if(labels[i].id == category)
|
|
{
|
|
// If there's no cat filter, only show the top level
|
|
if(!app_calendar.state.cat_id)
|
|
{
|
|
for(var j = 0; j < labels.length; j++)
|
|
{
|
|
if(labels[j].id == labels[i].main)
|
|
{
|
|
label_index = j;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
label_index = i;
|
|
break;
|
|
}
|
|
}
|
|
if(label_index !== false && typeof rows[label_index] === 'undefined')
|
|
{
|
|
rows[label_index] = [];
|
|
}
|
|
if(label_index !== false && rows[label_index].indexOf(event) === -1)
|
|
{
|
|
rows[label_index].push(event);
|
|
}
|
|
}
|
|
},
|
|
draw_row: function(sort_key, label, events) {
|
|
var row = this._drawRow(sort_key, label,events,this.options.start_date, this.options.end_date);
|
|
if(this.options.hide_empty && !events.length)
|
|
{
|
|
row.set_disabled(true);
|
|
}
|
|
return row;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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( trigger?)
|
|
{
|
|
|
|
// Busy
|
|
if(!this.doInvalidate) return;
|
|
|
|
// Not yet ready
|
|
if(!this.options.start_date || !this.options.end_date) return;
|
|
|
|
// Wait a bit to see if anything else changes, then re-draw the days
|
|
if(this.update_timer !== null)
|
|
{
|
|
window.clearTimeout(this.update_timer);
|
|
}
|
|
this.update_timer = window.setTimeout(jQuery.proxy(function() {
|
|
this.widget.doInvalidate = false;
|
|
|
|
// Show AJAX loader
|
|
this.widget.loader.show();
|
|
|
|
this.widget.cache = {};
|
|
this._deferred_row_updates = {};
|
|
|
|
this.widget._fetch_data();
|
|
|
|
this.widget._drawGrid();
|
|
|
|
if(this.trigger)
|
|
{
|
|
this.widget.change();
|
|
}
|
|
this.widget.update_timer = null;
|
|
this.widget.doInvalidate = true;
|
|
|
|
this.widget._updateNow();
|
|
window.setTimeout(jQuery.proxy(function() {if(this.loader) this.loader.hide();},this.widget),500);
|
|
},{widget:this,"trigger":trigger}),et2_dataview_grid.ET2_GRID_INVALIDATE_TIMEOUT);
|
|
}
|
|
|
|
detachFromDOM()
|
|
{
|
|
// Remove the binding to the change handler
|
|
jQuery(this.div).off("change.et2_calendar_timegrid");
|
|
|
|
return super.detachFromDOM();
|
|
}
|
|
|
|
attachToDOM( )
|
|
{
|
|
let result = super.attachToDOM();
|
|
|
|
// Add the binding for the event change handler
|
|
jQuery(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
|
|
jQuery(this.div).on("change.et2_calendar_timegrid", '*:not(.calendar_calEvent)', this, function(e) {
|
|
return e.data.change.call(e.data, e, this);
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
getDOMNode(_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
|
|
*
|
|
*/
|
|
async _drawGrid()
|
|
{
|
|
|
|
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].destroy();
|
|
this.removeChild(this._children[delete_index--]);
|
|
}
|
|
|
|
// Clear old rows
|
|
this.rows.empty()
|
|
.append(this.grid);
|
|
this.grid.empty();
|
|
|
|
var grouper = this.grouper;
|
|
if(!grouper) return;
|
|
|
|
// Headers
|
|
this.headers.empty();
|
|
this.headerTitle.text(grouper.title.apply(this));
|
|
await grouper.headers.apply(this);
|
|
this.grid.find('*').contents().filter(function(){
|
|
return this.nodeType === 3;
|
|
}).remove();
|
|
|
|
// 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]);
|
|
}
|
|
|
|
// Set height for rows
|
|
this.rows.height(this.div.height() - this.headers.outerHeight());
|
|
|
|
// Draw the rows
|
|
let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar;
|
|
for(var key in labels)
|
|
{
|
|
if (!labels.hasOwnProperty(key)) continue;
|
|
|
|
// Skip sub-categories (events are merged into top level)
|
|
if(this.options.group_by == 'category' &&
|
|
(!app_calendar.state.cat_id || app_calendar.state.cat_id == '') &&
|
|
labels[key].id != labels[key].main
|
|
)
|
|
{
|
|
continue;
|
|
}
|
|
var row = grouper.draw_row.call(this,labels[key].id, labels[key].label, events[key] || []);
|
|
|
|
// Add extra data for clicking on row
|
|
if(row)
|
|
{
|
|
for(var extra in labels[key].data)
|
|
{
|
|
row.getDOMNode().dataset[extra] = labels[key].data[extra];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adjust header if there's a scrollbar
|
|
if(this.rows.children().last().length)
|
|
{
|
|
this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().first().width()) + 'px');
|
|
}
|
|
// Add actual events
|
|
for(var key in this._deferred_row_updates)
|
|
{
|
|
window.clearTimeout(key);
|
|
}
|
|
window.setTimeout(jQuery.proxy(function() {
|
|
this._deferred_row_update();
|
|
}, this ),et2_calendar_planner.DEFERRED_ROW_TIME);
|
|
this.value = [];
|
|
}
|
|
|
|
/**
|
|
* 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(key, label, events, start, end)
|
|
{
|
|
let row = et2_createWidget('calendar-planner_row',{
|
|
id: 'planner_row_'+key,
|
|
label: label,
|
|
start_date: start,
|
|
end_date: end,
|
|
value: events,
|
|
readonly: this.options.readonly
|
|
},this);
|
|
|
|
|
|
if(this.isInTree())
|
|
{
|
|
row.doLoadingFinished();
|
|
}
|
|
|
|
return row;
|
|
}
|
|
|
|
|
|
_header_day_of_month()
|
|
{
|
|
let day_width = 3.23; // 100.0 / 31;
|
|
|
|
// month scale with navigation
|
|
var content = '<div class="calendar_plannerScale">';
|
|
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 = this.egw().lang(date('F',start))+' '+date('Y',start)+' - '+
|
|
this.egw().lang(date('F',end))+' '+date('Y',end);
|
|
|
|
content += '<div class="calendar_plannerMonthScale th et2_link" style="left: 0; width: 100%;">'+
|
|
title+"</div>";
|
|
content += "</div>"; // end of plannerScale
|
|
|
|
// day of month scale
|
|
content +='<div class="calendar_plannerScale">';
|
|
|
|
for(var left = 0, i = 0; i < 31; left += day_width,++i)
|
|
{
|
|
content += '<div class="calendar_plannerDayOfMonthScale " style="left: '+left+'%; width: '+day_width+'%;">'+
|
|
(1+i)+"</div>\n";
|
|
}
|
|
content += "</div>\n";
|
|
|
|
return content;
|
|
}
|
|
/**
|
|
* Update the 'now' line
|
|
* @private
|
|
*/
|
|
public _updateNow()
|
|
{
|
|
let now = super._updateNow();
|
|
if(now === false || this.grouper == this.groupers.month)
|
|
{
|
|
this.now_div.hide();
|
|
return false;
|
|
}
|
|
|
|
let row = null;
|
|
for(let i = 0; i < this._children.length && row == null; i++)
|
|
{
|
|
if(this._children[i].instanceOf(et2_calendar_planner_row))
|
|
{
|
|
row = this._children[i];
|
|
}
|
|
}
|
|
if(!row)
|
|
{
|
|
this.now_div.hide();
|
|
return false;
|
|
}
|
|
this.now_div.appendTo(this.grid)
|
|
.show()
|
|
.css('left', row._time_to_position(now) + '%');
|
|
}
|
|
|
|
/**
|
|
* Make a header showing the months
|
|
* @param {Date} start
|
|
* @param {number} days
|
|
* @returns {string} HTML snippet
|
|
*/
|
|
_header_months(start, days)
|
|
{
|
|
var content = '<div class="calendar_plannerScale">';
|
|
var days_in_month = 0;
|
|
var day_width = 100 / days;
|
|
var end = new Date(start);
|
|
end.setUTCDate(end.getUTCDate()+days);
|
|
var t = new Date(start.valueOf());
|
|
for(var left = 0,i = 0; i < days;t.setUTCDate(1),t.setUTCMonth(t.getUTCMonth()+1),left += days_in_month*day_width,i += days_in_month)
|
|
{
|
|
var u = new Date(t.getUTCFullYear(),t.getUTCMonth()+1,0,-t.getTimezoneOffset()/60);
|
|
days_in_month = 1+ ((u-t) / (24*3600*1000));
|
|
|
|
var first = new Date(t.getUTCFullYear(),t.getUTCMonth(),1,-t.getTimezoneOffset()/60);
|
|
if(days_in_month <= 0) break;
|
|
|
|
if (i + days_in_month > days)
|
|
{
|
|
days_in_month = days - i;
|
|
}
|
|
var title = this.egw().lang(date('F',new Date(t.valueOf() + t.getTimezoneOffset() * 60 * 1000)));
|
|
if (days_in_month > 10)
|
|
{
|
|
title += '</span> <span class="et2_clickable et2_link" data-sortby="month">'+t.getUTCFullYear();
|
|
}
|
|
else if (days_in_month < 5)
|
|
{
|
|
title = ' ';
|
|
}
|
|
content += '<div class="calendar_plannerMonthScale" data-date="'+first.toJSON()+
|
|
'" style="left: '+left+'%; width: '+(day_width*days_in_month)+'%;"><span'+
|
|
' data-planner_view="month" class="et2_clickable et2_link">'+
|
|
title+"</span></div>";
|
|
}
|
|
content += "</div>"; // end of plannerScale
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Make a header showing the week numbers
|
|
*
|
|
* @param {Date} start
|
|
* @param {number} days
|
|
* @returns {string} HTML snippet
|
|
*/
|
|
_header_weeks(start, days)
|
|
{
|
|
|
|
var content = '<div class="calendar_plannerScale" data-planner_view="week">';
|
|
var state = '';
|
|
|
|
// we're not using UTC so date() formatting function works
|
|
var t = new Date(start.valueOf());
|
|
|
|
// Make sure we're lining up on the week
|
|
let app_calendar = this.getInstanceManager().app_obj.calendar || app.calendar;
|
|
var week_end = app_calendar.date.end_of_week(start);
|
|
var days_in_week = Math.floor(((week_end-start ) / (24*3600*1000))+1);
|
|
var week_width = 100 / days * (days <= 7 ? days : days_in_week);
|
|
for(var left = 0,i = 0; i < days; t.setUTCDate(t.getUTCDate() + 7),left += week_width)
|
|
{
|
|
// Avoid overflow at the end
|
|
if(days - i < 7)
|
|
{
|
|
days_in_week = days-i;
|
|
}
|
|
var usertime = new Date(t.valueOf());
|
|
if(start.getTimezoneOffset() < 0)
|
|
{
|
|
// Gets the right week # east of GMT. West does not need it(?)
|
|
usertime.setUTCMinutes(usertime.getUTCMinutes() - start.getTimezoneOffset());
|
|
}
|
|
|
|
week_width = 100 / days * Math.min(days, days_in_week);
|
|
|
|
var title = this.egw().lang('Week')+' '+app_calendar.date.week_number(usertime);
|
|
|
|
if(start.getTimezoneOffset() > 0)
|
|
{
|
|
// Gets the right week start west of GMT
|
|
usertime.setUTCMinutes(usertime.getUTCMinutes() +start.getTimezoneOffset());
|
|
}
|
|
state = app_calendar.date.start_of_week(usertime);
|
|
state.setUTCHours(0);
|
|
state.setUTCMinutes(0);
|
|
state = state.toJSON();
|
|
|
|
if(days_in_week > 1 || days == 1)
|
|
{
|
|
content += '<div class="calendar_plannerWeekScale et2_clickable et2_link" data-date=\'' + state + '\' style="left: '+left+'%; width: '+week_width+'%;">'+title+"</div>";
|
|
}
|
|
i+= days_in_week;
|
|
if(days_in_week != 7)
|
|
{
|
|
t.setUTCDate(t.getUTCDate() - (7 - days_in_week));
|
|
days_in_week = 7;
|
|
}
|
|
}
|
|
content += "</div>"; // end of plannerScale
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Make a header for some days
|
|
*
|
|
* @param {Date} start
|
|
* @param {number} days
|
|
* @returns {string} HTML snippet
|
|
*/
|
|
async _header_days(start, days)
|
|
{
|
|
var day_width = 100 / days;
|
|
var content = '<div class="calendar_plannerScale'+(days > 3 ? 'Day' : '')+'" data-planner_view="day" >';
|
|
|
|
// 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)
|
|
{
|
|
if(!this.options.show_weekend && [0,6].indexOf(t.getDay()) !== -1 ) continue;
|
|
var holidays = [];
|
|
var tempDate = new Date(t);
|
|
tempDate.setMinutes(tempDate.getMinutes()-tempDate.getTimezoneOffset());
|
|
var title = '';
|
|
let state = new Date(t.valueOf() - t.getTimezoneOffset() * 60 * 1000);
|
|
var day_class = await this.day_class_holiday(state,holidays, days);
|
|
|
|
if (days <= 3)
|
|
{
|
|
title = this.egw().lang(date('l',t))+', '+date('j',t)+'. '+this.egw().lang(date('F',t));
|
|
}
|
|
else if (days <= 7)
|
|
{
|
|
title = this.egw().lang(date('l',t))+' '+date('j',t);
|
|
}
|
|
else
|
|
{
|
|
title = this.egw().lang(date('D',t)).substr(0,2)+'<br />'+date('j',t);
|
|
}
|
|
|
|
content += '<div class="calendar_plannerDayScale et2_clickable et2_link '+ day_class+
|
|
'" data-date=\'' + state.toJSON() +'\''+
|
|
(holidays ? ' title="'+holidays.join(',')+'"' : '')+'>'+title+"</div>\n";
|
|
}
|
|
content += "</div>"; // 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(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(),-start.getTimezoneOffset()/60);
|
|
var s = new Date(start);
|
|
s.setUTCHours(23);
|
|
s.setUTCMinutes(59);
|
|
s.setUTCSeconds(59);
|
|
hours = Math.ceil((s.getTime() - t.getTime()) / 3600000);
|
|
}
|
|
var cell_width = 100 / hours * decr;
|
|
|
|
var content = '<div class="calendar_plannerScale" data-planner_view="day">';
|
|
|
|
// 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)
|
|
{
|
|
if(!this.options.show_weekend && [0,6].indexOf(t.getDay()) !== -1 ) continue;
|
|
var title = date(egw.preference('timeformat','calendar') == 12 ? 'ha' : 'H',t);
|
|
|
|
content += '<div class="calendar_plannerHourScale et2_link" data-date="' + t.toJSON() +'" style="left: '+left+'%; width: '+(cell_width)+'%;">'+title+"</div>";
|
|
t.setHours(t.getHours()+decr);
|
|
}
|
|
content += "</div>"; // end of plannerScale
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @param {integer} days Number of days shown in the day header
|
|
*
|
|
* @return {string} CSS Classes for the day. calendar_calBirthday, calendar_calHoliday, calendar_calToday and calendar_weekend as appropriate
|
|
*/
|
|
async day_class_holiday(date, holiday_list, days?)
|
|
{
|
|
if(!date)
|
|
{
|
|
return '';
|
|
}
|
|
|
|
// Holidays and birthdays
|
|
const fetched = await this.egw().holidays(date.getUTCFullYear());
|
|
var day_class = '';
|
|
|
|
// Pass a string rather than the date object, to make sure it doesn't get changed
|
|
let date_key = formatDate(this.date_helper(date.toJSON()), {dateFormat: "Ymd"});
|
|
if(fetched && fetched[date_key])
|
|
{
|
|
const dates = fetched[date_key];
|
|
for(var i = 0; i < dates.length; i++)
|
|
{
|
|
if(typeof dates[i]['birthyear'] !== 'undefined')
|
|
{
|
|
day_class += ' calendar_calBirthday ';
|
|
if(typeof days == 'undefined' || days <= 21)
|
|
{
|
|
day_class += ' calendar_calBirthdayIcon ';
|
|
}
|
|
|
|
holiday_list.push(dates[i]['name']);
|
|
}
|
|
else
|
|
{
|
|
day_class += 'calendar_calHoliday ';
|
|
|
|
holiday_list.push(dates[i]['name']);
|
|
}
|
|
}
|
|
}
|
|
var today = new Date();
|
|
if(date_key === ''+today.getFullYear()+
|
|
sprintf("%02d",today.getMonth()+1)+
|
|
sprintf("%02d",today.getDate())
|
|
)
|
|
{
|
|
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(actions)
|
|
{
|
|
if(!this._actionObject)
|
|
{
|
|
// Get the parent? Might be a grid row, might not. Either way, it is
|
|
// just a container with no valid actions
|
|
var objectManager = egw_getObjectManager(this.getInstanceManager().app,true,1);
|
|
objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager;
|
|
var parent = objectManager.getObjectById(this.id,3) || objectManager.getObjectById(this._parent.id,3) || objectManager;
|
|
if(!parent)
|
|
{
|
|
debugger;
|
|
egw.debug('error','No parent objectManager found');
|
|
return;
|
|
}
|
|
|
|
for(var i = 0; i < parent.children.length; i++)
|
|
{
|
|
var parent_finder = jQuery('#'+this.div.id, parent.children[i].iface.doGetDOMNode());
|
|
if(parent_finder.length > 0)
|
|
{
|
|
parent = parent.children[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This binds into the egw action system. Most user interactions (drag to move, resize)
|
|
// are handled internally using jQuery directly.
|
|
var widget_object = this._actionObject || parent.getObjectById(this.id);
|
|
var aoi = new et2_action_object_impl(this,this.getDOMNode(this)).getAOI();
|
|
|
|
/**
|
|
* Determine if we allow a dropped event to use the invite/change actions,
|
|
* and enable or disable them appropriately
|
|
*
|
|
* @param {egwAction} action
|
|
* @param {et2_calendar_event} event The event widget being dragged
|
|
* @param {egwActionObject} target Planner action object
|
|
*/
|
|
var _invite_enabled = function(action, event, target)
|
|
{
|
|
var event = event.iface.getWidget();
|
|
var planner = target.iface.getWidget() || false;
|
|
//debugger;
|
|
if(event === planner || !event || !planner ||
|
|
!event.options || !event.options.value.participants || !planner.options.owner
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
var owner_match = false;
|
|
var own_row = false;
|
|
|
|
for(var id in event.options.value.participants)
|
|
{
|
|
planner.iterateOver(function(row) {
|
|
// Check scroll section or header section
|
|
if(row.div.hasClass('drop-hover') || row.div.has(':hover'))
|
|
{
|
|
owner_match = owner_match || row.node.dataset[planner.options.group_by] === ''+id;
|
|
own_row = (row === event.getParent());
|
|
}
|
|
}, this, et2_calendar_planner_row);
|
|
|
|
}
|
|
var enabled = !owner_match &&
|
|
// Not inside its own row
|
|
!own_row;
|
|
|
|
widget_object.getActionLink('invite').enabled = enabled;
|
|
widget_object.getActionLink('change_participant').enabled = enabled;
|
|
|
|
// If invite or change participant are enabled, drag is not
|
|
widget_object.getActionLink('egw_link_drop').enabled = !enabled;
|
|
};
|
|
|
|
aoi.doTriggerEvent = function(_event, _data)
|
|
{
|
|
|
|
// Determine target node
|
|
var event = _data.event || false;
|
|
if(!event)
|
|
{
|
|
return;
|
|
}
|
|
if(_data.ui.draggable.classList.contains('rowNoEdit'))
|
|
{
|
|
return;
|
|
}
|
|
|
|
/*
|
|
We have to handle the drop in the normal event stream instead of waiting
|
|
for the egwAction system so we can get the helper, and destination
|
|
*/
|
|
if(event.type === 'drop')
|
|
{
|
|
this.getWidget()._event_drop.call(jQuery('.calendar_d-n-d_timeCounter', _data.ui.draggable)[0], this.getWidget(), event, _data.ui);
|
|
}
|
|
var drag_listener = function(event)
|
|
{
|
|
let style = getComputedStyle(_data.ui.helper);
|
|
aoi.getWidget()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', _data.ui.draggable)[0], {
|
|
top: parseInt(style.top),
|
|
left: event.clientX - jQuery(this).parent().offset().left
|
|
}, 0);
|
|
};
|
|
var time = jQuery('.calendar_d-n-d_timeCounter', _data.ui.draggable);
|
|
switch(_event)
|
|
{
|
|
// Triggered once, when something is dragged into the timegrid's div
|
|
case EGW_AI_DRAG_ENTER:
|
|
// Listen to the drag and update the helper with the time
|
|
// This part lets us drag between different timegrids
|
|
jQuery(_data.ui.draggable).on('drag.et2_timegrid' + widget_object.id, drag_listener);
|
|
jQuery(_data.ui.draggable).on('dragend.et2_timegrid' + widget_object.id, function()
|
|
{
|
|
jQuery(_data.ui.draggable).off('drag.et2_timegrid' + widget_object.id);
|
|
});
|
|
if(time.length)
|
|
{
|
|
// The out will trigger after the over, so we count
|
|
time.data('count', time.data('count') + 1);
|
|
}
|
|
else
|
|
{
|
|
jQuery(_data.ui.draggable).prepend('<div class="calendar_d-n-d_timeCounter" data-count="1"><span></span></div>');
|
|
}
|
|
|
|
break;
|
|
|
|
// Triggered once, when something is dragged out of the timegrid
|
|
case EGW_AI_DRAG_OUT:
|
|
// Stop listening
|
|
jQuery(_data.ui.draggable).off('drag.et2_timegrid' + widget_object.id);
|
|
// Remove any highlighted time squares
|
|
jQuery('[data-date]',this.doGetDOMNode()).removeClass("ui-state-active");
|
|
|
|
// Out triggers after the over, count to not accidentally remove
|
|
time.data('count',time.data('count')-1);
|
|
if(time.length && time.data('count') <= 0)
|
|
{
|
|
time.remove();
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
if (widget_object == null) {
|
|
// Add a new container to the object manager which will hold the widget
|
|
// objects
|
|
widget_object = parent.insertObject(false, new egwActionObject(
|
|
this.id, parent, aoi,
|
|
this._actionManager || parent.manager.getActionById(this.id) || parent.manager
|
|
),EGW_AO_FLAG_IS_CONTAINER);
|
|
}
|
|
else
|
|
{
|
|
widget_object.setAOI(aoi);
|
|
}
|
|
// Go over the widget & add links - this is where we decide which actions are
|
|
// 'allowed' for this widget at this time
|
|
var action_links = this._get_action_links(actions);
|
|
|
|
this._init_links_dnd(widget_object.manager, action_links);
|
|
|
|
widget_object.updateActionLinks(action_links);
|
|
this._actionObject = widget_object;
|
|
}
|
|
|
|
/**
|
|
* Automatically add dnd support for linking
|
|
*
|
|
* @param {type} mgr
|
|
* @param {type} actionLinks
|
|
*/
|
|
_init_links_dnd( mgr,actionLinks)
|
|
{
|
|
|
|
if (this.options.readonly) return;
|
|
|
|
var self = this;
|
|
|
|
var drop_action = mgr.getActionById('egw_link_drop');
|
|
var drop_change_participant = mgr.getActionById('change_participant');
|
|
var drop_invite = mgr.getActionById('invite');
|
|
var drag_action = mgr.getActionById('egw_link_drag');
|
|
var paste_action = mgr.getActionById('egw_paste');
|
|
|
|
// Disable paste action
|
|
if(paste_action == null)
|
|
{
|
|
paste_action = mgr.addAction('popup', 'egw_paste', egw.lang('Paste'), egw.image('editpaste'), function(){},true);
|
|
}
|
|
paste_action.set_enabled(false);
|
|
|
|
// Check if this app supports linking
|
|
if(!egw.link_get_registry(this.dataStorePrefix || 'calendar', 'query') ||
|
|
egw.link_get_registry(this.dataStorePrefix || 'calendar', '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;
|
|
}
|
|
if(links.length && dropped && dropped.iface.getWidget() && dropped.iface.getWidget().instanceOf(et2_calendar_event))
|
|
{
|
|
// 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);
|
|
|
|
drop_action.acceptedTypes = ['default','link'];
|
|
drop_action.hideOnDisabled = true;
|
|
|
|
// Create the drop action for moving events between planner rows
|
|
var invite_action = function(action, source, target) {
|
|
|
|
// Extract link IDs
|
|
var links = [];
|
|
var id = '';
|
|
for(var i = 0; i < source.length; i++)
|
|
{
|
|
// Check for no ID (invalid) or same manager (dragging an event)
|
|
if(!source[i].id) continue;
|
|
if(source[i].manager === target.manager)
|
|
{
|
|
|
|
// Find the row, could have dropped on an event
|
|
var row = target.iface.getWidget();
|
|
while(target.parent && row.instanceOf && !row.instanceOf(et2_calendar_planner_row))
|
|
{
|
|
target = target.parent;
|
|
row = target.iface.getWidget();
|
|
}
|
|
|
|
// Leave the helper there until the update is done
|
|
var loading = action.ui.helper.clone(true).appendTo(jQuery('body'));
|
|
|
|
// and add a loading icon so user knows something is happening
|
|
if(jQuery('.calendar_timeDemo',loading).length == 0)
|
|
{
|
|
jQuery('.calendar_calEventHeader',loading).addClass('loading');
|
|
}
|
|
else
|
|
{
|
|
jQuery('.calendar_timeDemo',loading).after('<div class="loading"></div>');
|
|
}
|
|
|
|
var event_data = egw.dataGetUIDdata(source[i].id).data;
|
|
et2_calendar_event.recur_prompt(event_data, function(button_id) {
|
|
if(button_id === 'cancel' || !button_id)
|
|
{
|
|
return;
|
|
}
|
|
var add_owner = [row.node.dataset.participants];
|
|
|
|
egw().json('calendar.calendar_uiforms.ajax_invite', [
|
|
button_id==='series' ? event_data.id : event_data.app_id,
|
|
add_owner,
|
|
action.id === 'change_participant' ?
|
|
[source[i].iface.getWidget().getParent().node.dataset.participants] :
|
|
[]
|
|
],
|
|
function() { loading.remove();}
|
|
).sendRequest(true);
|
|
});
|
|
// Ok, stop.
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
drop_change_participant = mgr.addAction('drop', 'change_participant', egw.lang('Move to'), egw.image('participant'), invite_action,true);
|
|
drop_change_participant.acceptedTypes = ['calendar'];
|
|
drop_change_participant.hideOnDisabled = true;
|
|
|
|
drop_invite = mgr.addAction('drop', 'invite', egw.lang('Invite'), egw.image('participant'), invite_action,true);
|
|
drop_invite.acceptedTypes = ['calendar'];
|
|
drop_invite.hideOnDisabled = true;
|
|
}
|
|
if(actionLinks.indexOf(drop_action.id) < 0)
|
|
{
|
|
actionLinks.push(drop_action.id);
|
|
}
|
|
actionLinks.push(drop_invite.id);
|
|
actionLinks.push(drop_change_participant.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) {
|
|
// 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);
|
|
}
|
|
// The planner itself is not draggable, the action is there for the children
|
|
if(false && actionLinks.indexOf(drag_action.id) < 0)
|
|
{
|
|
actionLinks.push(drag_action.id);
|
|
}
|
|
drag_action.set_dragType(['link','calendar']);
|
|
}
|
|
|
|
/**
|
|
* 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(actions)
|
|
{
|
|
var action_links = [];
|
|
|
|
// Only these actions are allowed without a selection (empty actions)
|
|
var empty_actions = ['add'];
|
|
|
|
for(var i in actions)
|
|
{
|
|
var action = actions[i];
|
|
if(empty_actions.indexOf(action.id) !== -1 || action.type === 'drop')
|
|
{
|
|
action_links.push(typeof action.id !== 'undefined' ? action.id : i);
|
|
}
|
|
}
|
|
// Disable automatic paste action, it doesn't have what is needed to work
|
|
action_links.push({
|
|
"actionObj": 'egw_paste',
|
|
"enabled": false,
|
|
"visible": false
|
|
});
|
|
return action_links;
|
|
}
|
|
|
|
/**
|
|
* Show the current time while dragging
|
|
* Used for resizing as well as drag & drop
|
|
*
|
|
* @param {type} element
|
|
* @param {type} position
|
|
* @param {type} height
|
|
*/
|
|
_drag_helper(element, position ,height)
|
|
{
|
|
let time = this._get_time_from_position(position.left, position.top);
|
|
element.dropEnd = time;
|
|
let formatted_time = formatTime(time);
|
|
|
|
element.innerHTML = '<div class="calendar_d-n-d_timeCounter"><span class="calendar_timeDemo" >'+formatted_time+'</span></div>';
|
|
|
|
//jQuery(element).width(jQuery(helper).width());
|
|
}
|
|
|
|
/**
|
|
* Handler for dropping an event on the timegrid
|
|
*
|
|
* @param {type} planner
|
|
* @param {type} event
|
|
* @param {type} ui
|
|
*/
|
|
_event_drop( planner, event,ui)
|
|
{
|
|
var e = new jQuery.Event('change');
|
|
e.originalEvent = event;
|
|
e.data = {start: 0};
|
|
if(typeof this.dropEnd != 'undefined' && this.dropEnd)
|
|
{
|
|
var drop_date = this.dropEnd.toJSON() || false;
|
|
|
|
var event_data = planner._get_event_info(ui.draggable);
|
|
var event_widget = planner.getWidgetById(event_data.widget_id);
|
|
if(event_widget)
|
|
{
|
|
event_widget.options.value.start = event_widget._parent.date_helper(drop_date);
|
|
|
|
// Leave the helper there until the update is done
|
|
var loading = event_data.event_node;
|
|
// and add a loading icon so user knows something is happening
|
|
jQuery('.calendar_calEventHeader', event_widget.div).addClass('loading');
|
|
|
|
event_widget.recur_prompt(function(button_id) {
|
|
if(button_id === 'cancel' || !button_id) return;
|
|
//Get infologID if in case if it's an integrated infolog event
|
|
if (event_data.app === 'infolog')
|
|
{
|
|
// If it is an integrated infolog event we need to edit infolog entry
|
|
egw().json('stylite_infolog_calendar_integration::ajax_moveInfologEvent',
|
|
[event_data.id, event_widget.options.value.start||false],
|
|
function() {loading.remove();}
|
|
).sendRequest(true);
|
|
}
|
|
else
|
|
{
|
|
//Edit calendar event
|
|
egw().json('calendar.calendar_uiforms.ajax_moveEvent', [
|
|
button_id==='series' ? event_data.id : event_data.app_id,event_data.owner,
|
|
event_widget.options.value.start,
|
|
planner.options.owner||egw.user('account_id')
|
|
],
|
|
function() { loading.remove();}
|
|
).sendRequest(true);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use the egw.data system to get data from the calendar list for the
|
|
* selected time span.
|
|
*
|
|
*/
|
|
_fetch_data()
|
|
{
|
|
var value = [];
|
|
var fetch = false;
|
|
this.doInvalidate = false;
|
|
|
|
for(var i = 0; i < this.registeredCallbacks.length; i++)
|
|
{
|
|
egw.dataUnregisterUID(this.registeredCallbacks[i],false,this);
|
|
}
|
|
this.registeredCallbacks.splice(0,this.registeredCallbacks.length);
|
|
|
|
// Remember previous day to avoid multi-days duplicating
|
|
var last_data = [];
|
|
|
|
var t = new Date(this.options.start_date);
|
|
var end = new Date(this.options.end_date);
|
|
do
|
|
{
|
|
value = value.concat(this._cache_register(t, this.options.owner, last_data));
|
|
|
|
t.setUTCDate(t.getUTCDate() + 1);
|
|
}
|
|
while(t < end);
|
|
|
|
this.doInvalidate = true;
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Deal with registering for data cache
|
|
*
|
|
* @param Date t
|
|
* @param String owner Calendar owner
|
|
*/
|
|
_cache_register(t, owner, last_data)
|
|
{
|
|
// Cache is by date (and owner, if seperate)
|
|
var date = t.getUTCFullYear() + sprintf('%02d',t.getUTCMonth()+1) + sprintf('%02d',t.getUTCDate());
|
|
var cache_id = CalendarApp._daywise_cache_id(date, owner);
|
|
var value = [];
|
|
|
|
if(egw.dataHasUID(cache_id))
|
|
{
|
|
var c = egw.dataGetUIDdata(cache_id);
|
|
if(c.data && c.data !== null)
|
|
{
|
|
// There is data, pass it along now
|
|
for(var j = 0; j < c.data.length; j++)
|
|
{
|
|
if(last_data.indexOf(c.data[j]) === -1 && egw.dataHasUID('calendar::'+c.data[j]))
|
|
{
|
|
value.push(egw.dataGetUIDdata('calendar::'+c.data[j]).data);
|
|
}
|
|
}
|
|
last_data = c.data;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Assume it's empty, if there is data it will be filled later
|
|
egw.dataStoreUID(cache_id, []);
|
|
}
|
|
this.registeredCallbacks.push(cache_id);
|
|
|
|
egw.dataRegisterUID(cache_id, function(data)
|
|
{
|
|
|
|
const waitForGroups = [];
|
|
if(data && data.length)
|
|
{
|
|
for(var i = 0; i < data.length; i++)
|
|
{
|
|
let event = egw.dataGetUIDdata('calendar::' + data[i]);
|
|
if(!event || !event.data)
|
|
{
|
|
continue;
|
|
}
|
|
let wait = (<CalendarApp>app.calendar)._fetch_group_members(event.data);
|
|
if(wait !== null)
|
|
{
|
|
waitForGroups.push(wait);
|
|
}
|
|
}
|
|
}
|
|
if(!data || data.length == 0)
|
|
{
|
|
return;
|
|
}
|
|
Promise.all(waitForGroups).then(() =>
|
|
{
|
|
var invalidate = true;
|
|
|
|
// Try to determine rows interested
|
|
var labels = [];
|
|
var events = {};
|
|
if(this.grouper)
|
|
{
|
|
labels = this.grouper.row_labels.call(this);
|
|
invalidate = false;
|
|
}
|
|
|
|
var im = this.getInstanceManager();
|
|
for(var i = 0; i < data.length; i++)
|
|
{
|
|
var event = egw.dataGetUIDdata('calendar::'+data[i]);
|
|
|
|
if(!event) continue;
|
|
events = {};
|
|
|
|
// Try to determine rows interested
|
|
if(event.data && this.grouper)
|
|
{
|
|
this.grouper.group.call(this, labels, events, event.data);
|
|
}
|
|
if(Object.keys(events).length > 0 )
|
|
{
|
|
for(var label_id in events)
|
|
{
|
|
var id = ""+labels[label_id].id;
|
|
if(typeof this.cache[id] === 'undefined')
|
|
{
|
|
this.cache[id] = [];
|
|
}
|
|
if(this.cache[id].indexOf(event.data.row_id) === -1)
|
|
{
|
|
this.cache[id].push(event.data.row_id);
|
|
}
|
|
if (this._deferred_row_updates[id])
|
|
{
|
|
window.clearTimeout(this._deferred_row_updates[id]);
|
|
}
|
|
this._deferred_row_updates[id] = window.setTimeout(jQuery.proxy(this._deferred_row_update, this, id), et2_calendar_planner.DEFERRED_ROW_TIME);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Could be an event no row is interested in, could be a problem.
|
|
// Just redraw everything
|
|
invalidate = true;
|
|
continue;
|
|
}
|
|
|
|
// If displaying by category, we need the infolog (or other app) categories too
|
|
if(event && event.data && event.data.app && this.options.group_by == 'category')
|
|
{
|
|
// Fake it to use the cache / call
|
|
this.nodeName = "ET2-SELECT-CAT_RO"
|
|
let categories = StaticOptions.cached_server_side(this, "cat", ",,," + (event.data.app || 'calendar'), false);
|
|
}
|
|
}
|
|
|
|
if(invalidate)
|
|
{
|
|
this.invalidate(false);
|
|
}
|
|
})
|
|
.then(() =>
|
|
{
|
|
// Update the "now" line _after_ rows are done
|
|
this._updateNow();
|
|
});
|
|
}, this, this.getInstanceManager().execId,this.id);
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Because users may be participants in various events and the time it takes
|
|
* to create many events, we don't want to update a row too soon - we may have
|
|
* to re-draw it if we find the user / category in another event. Pagination
|
|
* makes this worse. We wait a bit before updating the row to avoid
|
|
* having to re-draw it multiple times.
|
|
*
|
|
* @param {type} id
|
|
* @returns {undefined}
|
|
*/
|
|
_deferred_row_update( id)
|
|
{
|
|
// Something's in progress, skip
|
|
if(!this.doInvalidate) return;
|
|
|
|
this.grid.height(0);
|
|
|
|
var id_list = typeof id === 'undefined' ? Object.keys(this.cache) : [id];
|
|
for(var i = 0; i < id_list.length; i++)
|
|
{
|
|
var cache_id = id_list[i];
|
|
var row = <et2_calendar_planner_row>this.getWidgetById('planner_row_'+cache_id);
|
|
|
|
window.clearTimeout(this._deferred_row_updates[cache_id]);
|
|
delete this._deferred_row_updates[cache_id];
|
|
|
|
if(row)
|
|
{
|
|
row._data_callback(this.cache[cache_id]);
|
|
row.set_disabled(this.options.hide_empty && this.cache[cache_id].length === 0);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Updating the row may push things longer, update length
|
|
// Add 1 to keep the scrollbar, otherwise we need to recalculate the
|
|
// header widths too.
|
|
this.grid.height(this.rows[0].scrollHeight+1);
|
|
|
|
// Adjust header if there's a scrollbar - Firefox needs this re-calculated,
|
|
// otherwise the header will be missing the margin space for the scrollbar
|
|
// in some cases
|
|
if(this.rows.children().last().length)
|
|
{
|
|
this.gridHeader.css('margin-right', (this.rows.width() - this.rows.children().first().width()) + 'px');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(events)
|
|
{
|
|
if(typeof events !== 'object') return false;
|
|
|
|
super.set_value(events);
|
|
|
|
// Planner uses an array, not map
|
|
var val = this.value;
|
|
var array = [];
|
|
Object.keys(this.value).forEach(function (key) {
|
|
array.push(val[key]);
|
|
});
|
|
this.value = array;
|
|
}
|
|
|
|
/**
|
|
* Change the start date
|
|
* Planner view uses a date object internally
|
|
*
|
|
* @param {string|number|Date} new_date New starting date
|
|
* @returns {undefined}
|
|
*/
|
|
set_start_date(new_date)
|
|
{
|
|
super.set_start_date(new_date);
|
|
this.options.start_date = new Date(this.options.start_date);
|
|
}
|
|
|
|
/**
|
|
* Change the end date
|
|
* Planner view uses a date object internally
|
|
*
|
|
* @param {string|number|Date} new_date New end date
|
|
* @returns {undefined}
|
|
*/
|
|
set_end_date(new_date)
|
|
{
|
|
super.set_end_date(new_date);
|
|
this.options.end_date = new Date(this.options.end_date);
|
|
}
|
|
|
|
/**
|
|
* Change how the planner is grouped
|
|
*
|
|
* @param {string|number} group_by 'user', 'month', or an integer category ID
|
|
* @returns {undefined}
|
|
*/
|
|
set_group_by(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;
|
|
|
|
this.grouper = this.groupers[isNaN(this.options.group_by) ? this.options.group_by : 'category'];
|
|
|
|
if(old !== this.options.group_by && this.isAttached())
|
|
{
|
|
this.invalidate(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set which users to display
|
|
*
|
|
* Changing the owner will invalidate the display, and it will be redrawn
|
|
* after a timeout. Overwriting here to check for groups without members.
|
|
*
|
|
* @param {number|number[]|string|string[]} _owner - Owner ID, which can
|
|
* be an account ID, a resource ID (as defined in calendar_bo, not
|
|
* necessarily an entry from the resource app), or a list containing a
|
|
* combination of both.
|
|
*
|
|
* @memberOf et2_calendar_view
|
|
*/
|
|
set_owner(_owner)
|
|
{
|
|
super.set_owner(_owner);
|
|
|
|
// If we're grouping by user, we need group members
|
|
if(this.update_timer !== null && this.options.group_by == 'user')
|
|
{
|
|
let options = [];
|
|
let resource = {};
|
|
let missing_resources = [];
|
|
|
|
if(app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner'))
|
|
{
|
|
options = app.calendar.sidebox_et2.getWidgetById('owner').select_options;
|
|
}
|
|
else
|
|
{
|
|
options = this.getArrayMgr("sel_options").getRoot().getEntry('owner');
|
|
}
|
|
for(var i = 0; i < this.options.owner.length; i++)
|
|
{
|
|
var user = this.options.owner[i];
|
|
if(isNaN(user) || user >= 0 || !options) continue;
|
|
|
|
// Owner is a group, see if we have its members
|
|
if(options.find &&
|
|
((resource = options.find(function (element)
|
|
{
|
|
return element.value == user;
|
|
}))))
|
|
{
|
|
// Members found
|
|
continue;
|
|
}
|
|
// Group, but no users found. Need those.
|
|
missing_resources.push(user);
|
|
|
|
// Maybe api already has them?
|
|
egw.accountData(parseInt(user),'account_fullname',true,function(result) {
|
|
missing_resources.splice(missing_resources.indexOf(this),1);
|
|
}.bind(user),user);
|
|
}
|
|
if(missing_resources.length > 0)
|
|
{
|
|
// Ask server, and WAIT or we have to redraw
|
|
egw.json('calendar_owner_etemplate_widget::ajax_owner',[missing_resources],function(data) {
|
|
for(let owner in data)
|
|
{
|
|
if(!owner || typeof owner == "undefined") continue;
|
|
options.push(data[owner]);
|
|
}
|
|
}, this,false,this).sendRequest(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Turn on or off the visibility of weekends
|
|
*
|
|
* @param {boolean} weekends
|
|
*/
|
|
set_show_weekend(weekends)
|
|
{
|
|
weekends = weekends ? true : false;
|
|
if(this.options.show_weekend !== weekends)
|
|
{
|
|
this.options.show_weekend = weekends;
|
|
if(this.isAttached())
|
|
{
|
|
this.invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Turn on or off the visibility of hidden (empty) rows
|
|
*
|
|
* @param {boolean} hidden
|
|
*/
|
|
set_hide_empty(hidden)
|
|
{
|
|
this.options.hide_empty = hidden;
|
|
}
|
|
|
|
/**
|
|
* Call change handler, if set
|
|
*
|
|
* @param {type} event
|
|
*/
|
|
change( 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
|
|
*
|
|
* @param {type} event
|
|
* @param {type} dom_node
|
|
*/
|
|
event_change( event, dom_node)
|
|
{
|
|
if (this.onevent_change)
|
|
{
|
|
var event_data = this._get_event_info(dom_node);
|
|
var event_widget = this.getWidgetById(event_data.widget_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} Continue processing event (true) or stop (false)
|
|
*/
|
|
click(_ev)
|
|
{
|
|
var result = true;
|
|
|
|
// Drag to create in progress
|
|
if(this.drag_create.start !== null) return;
|
|
|
|
// Is this click in the event stuff, or in the header?
|
|
if(!this.options.readonly && this.gridHeader.has(_ev.target).length === 0 && !jQuery(_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;
|
|
}
|
|
else if (!event.id)
|
|
{
|
|
// Clicked in row, but not on an event
|
|
// Default handler to open a new event at the selected time
|
|
if(jQuery(event.target).closest('.calendar_eventRows').length == 0)
|
|
{
|
|
// "Invalid" times, from space after the last planner row, or header
|
|
var date = this._get_time_from_position(_ev.pageX - this.grid.offset().left, _ev.pageY - this.grid.offset().top);
|
|
}
|
|
else if(this.options.group_by == 'month')
|
|
{
|
|
var date = this._get_time_from_position(_ev.clientX, _ev.clientY);
|
|
}
|
|
else
|
|
{
|
|
var date = this._get_time_from_position(_ev.offsetX, _ev.offsetY);
|
|
}
|
|
var row = jQuery(_ev.target).closest('.calendar_plannerRowWidget');
|
|
var data = row.length ? row[0].dataset : {};
|
|
if(date)
|
|
{
|
|
app.calendar.add(jQuery.extend({
|
|
start: date.toJSON(),
|
|
hour: date.getUTCHours(),
|
|
minute: date.getUTCMinutes()
|
|
}, data));
|
|
return false;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
else if (this.gridHeader.has(_ev.target).length > 0 && !jQuery.isEmptyObject(_ev.target.dataset) ||
|
|
jQuery(_ev.target).hasClass('calendar_plannerRowHeader') && !jQuery.isEmptyObject(_ev.target.dataset))
|
|
{
|
|
// Click on a header, we can go there
|
|
_ev.data = jQuery.extend({},_ev.target.parentNode.dataset, _ev.target.dataset);
|
|
for(var key in _ev.data)
|
|
{
|
|
if(!_ev.data[key])
|
|
{
|
|
delete _ev.data[key];
|
|
}
|
|
}
|
|
app.calendar.update_state(_ev.data);
|
|
}
|
|
else if (!this.options.readonly)
|
|
{
|
|
// Default handler to open a new event at the selected time
|
|
// TODO: Determine date / time more accurately from position
|
|
app.calendar.add({
|
|
date: _ev.target.dataset.date || this.options.start_date.toJSON(),
|
|
hour: _ev.target.dataset.hour || this.options.day_start,
|
|
minute: _ev.target.dataset.minute || 0
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get time from position
|
|
*
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @returns {Date|Boolean} A time for the given position, or false if one
|
|
* could not be determined.
|
|
*/
|
|
_get_time_from_position( x,y)
|
|
{
|
|
if(!this.options.start_date || !this.options.end_date)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
x = Math.round(x);
|
|
y = Math.round(y);
|
|
|
|
// Round to user's preferred event interval
|
|
var interval = egw.preference('interval', 'calendar') || 30;
|
|
|
|
// Relative horizontal position, as a percentage
|
|
var width = 0;
|
|
jQuery('.calendar_eventRows', this.div).each(function() {width = Math.max(width, jQuery(this).width());});
|
|
var rel_x = Math.min(x / width, 1);
|
|
|
|
// Relative time, in minutes from start
|
|
var rel_time = 0;
|
|
|
|
var day_header = jQuery('.calendar_plannerScaleDay', this.headers);
|
|
let date;
|
|
|
|
// Simple math, the x is offset from start date
|
|
if(this.options.group_by !== 'month' && (
|
|
// Either all days are visible, or only 1 day (no day header)
|
|
this.options.show_weekend || day_header.length === 0
|
|
))
|
|
{
|
|
rel_time = (new Date(this.options.end_date) - new Date(this.options.start_date)) * rel_x / 1000;
|
|
date = this.date_helper(this.options.start_date.toJSON());
|
|
}
|
|
// Not so simple math, need to account for missing days
|
|
else if(this.options.group_by !== 'month' && !this.options.show_weekend)
|
|
{
|
|
// Find which day
|
|
if(day_header.length === 0) return false;
|
|
var day = document.elementFromPoint(
|
|
day_header.offset().left + rel_x * this.headers.innerWidth(),
|
|
day_header.offset().top
|
|
);
|
|
|
|
// Use day, and find time in that day
|
|
if(day && day.dataset && day.dataset.date)
|
|
{
|
|
date = this.date_helper(day.dataset.date);
|
|
rel_time = ((x - jQuery(day).position().left) / jQuery(day).outerWidth(true)) * 24 * 60;
|
|
date.setUTCMinutes(Math.round(rel_time / interval) * interval);
|
|
return date;
|
|
}
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Find the correct row so we know which month, then get the offset
|
|
var hidden_nodes = [];
|
|
var row = null;
|
|
// Hide any drag or tooltips that may interfere
|
|
do
|
|
{
|
|
row = document.elementFromPoint(x, y);
|
|
if(this.div.has(row).length == 0)
|
|
{
|
|
hidden_nodes.push({element: row, display: row.style.display});
|
|
row.style.display = "none";
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
} while(row && row.nodeName !== 'BODY');
|
|
if(!row) return false;
|
|
|
|
// Restore hidden nodes
|
|
for(var i = 0; i < hidden_nodes.length; i++)
|
|
{
|
|
hidden_nodes[i].element.style.display = hidden_nodes[i].display;
|
|
}
|
|
row = jQuery(row).closest('.calendar_plannerRowWidget');
|
|
|
|
|
|
var row_widget = null;
|
|
for(var i = 0; i < this._children.length && row.length > 0; i++)
|
|
{
|
|
if(this._children[i].div[0] == row[0])
|
|
{
|
|
row_widget = this._children[i];
|
|
break;
|
|
}
|
|
}
|
|
if(row_widget)
|
|
{
|
|
// Not sure where the extra -1 and +2 are coming from, but it makes it work out
|
|
// in FF & Chrome
|
|
rel_x = Math.min((x - row_widget.rows.offset().left - 1) / (row_widget.rows.width() + 2), 1);
|
|
|
|
// 2678400 is the number of seconds in 31 days
|
|
rel_time = (2678400) * rel_x;
|
|
date = this.date_helper(row_widget.options.start_date.toJSON());
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if(rel_time < 0) return false;
|
|
|
|
date.setUTCMinutes(Math.round(rel_time / (60 * interval)) * interval);
|
|
|
|
return date;
|
|
}
|
|
|
|
/**
|
|
* Mousedown handler to support drag to create
|
|
*
|
|
* @param {jQuery.Event} event
|
|
*/
|
|
_mouse_down(event)
|
|
{
|
|
// Only left mouse button
|
|
if(event.which !== 1) return;
|
|
|
|
// Ignore headers
|
|
if(this.headers.has(event.target).length !== 0) return false;
|
|
|
|
// Get time at mouse
|
|
if(this.options.group_by === 'month')
|
|
{
|
|
var time = this._get_time_from_position(event.clientX, event.clientY);
|
|
}
|
|
else
|
|
{
|
|
var time = this._get_time_from_position(event.offsetX, event.offsetY);
|
|
}
|
|
if(!time) return false;
|
|
|
|
// Find the correct row so we know the parent
|
|
var row = event.target.closest('.calendar_plannerRowWidget');
|
|
for(var i = 0; i < this._children.length && row; i++)
|
|
{
|
|
if(this._children[i].div[0] === row)
|
|
{
|
|
this.drag_create.parent = this._children[i];
|
|
// Clear cached events for re-layout
|
|
this._children[i]._cached_rows = [];
|
|
break;
|
|
}
|
|
}
|
|
if(!this.drag_create.parent) return false;
|
|
|
|
this.div.css('cursor', 'ew-resize');
|
|
|
|
return this._drag_create_start(jQuery.extend({},this.drag_create.parent.node.dataset,{date: time.toJSON()}));
|
|
}
|
|
|
|
/**
|
|
* Mouseup handler to support drag to create
|
|
*
|
|
* @param {jQuery.Event} event
|
|
*/
|
|
_mouse_up(event)
|
|
{
|
|
// Get time at mouse
|
|
if(this.options.group_by === 'month')
|
|
{
|
|
var time = this._get_time_from_position(event.clientX, event.clientY);
|
|
}
|
|
else
|
|
{
|
|
var time = this._get_time_from_position(event.offsetX, event.offsetY);
|
|
}
|
|
|
|
return this._drag_create_end(time ? {date: time.toJSON()} : false);
|
|
}
|
|
|
|
/**
|
|
* Code for implementing et2_IDetachedDOM
|
|
*
|
|
* @param {array} _attrs array to add further attributes to
|
|
*/
|
|
getDetachedAttributes( _attrs)
|
|
{
|
|
_attrs.push('start_date','end_date');
|
|
}
|
|
|
|
getDetachedNodes()
|
|
{
|
|
return [this.getDOMNode()];
|
|
}
|
|
|
|
setDetachedAttributes( _nodes, _values)
|
|
{
|
|
this.div = jQuery(_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()
|
|
{
|
|
// Take the whole tab height
|
|
var height = Math.min(jQuery(this.getInstanceManager().DOMContainer).height(),jQuery(this.getInstanceManager().DOMContainer).parent().innerHeight());
|
|
|
|
// Allow for toolbar
|
|
height -= jQuery('#calendar-toolbar',this.div.parents('.egw_fw_ui_tab_content')).outerHeight(true);
|
|
|
|
this.options.height = height;
|
|
this.div.css('height', this.options.height);
|
|
// Set height for rows
|
|
this.rows.height(this.div.height() - this.headers.outerHeight());
|
|
|
|
this.grid.height(this.rows[0].scrollHeight);
|
|
}
|
|
|
|
/**
|
|
* Set up for printing
|
|
*
|
|
* @return {undefined|Deferred} Return a jQuery Deferred object if not done setting up
|
|
* (waiting for data)
|
|
*/
|
|
beforePrint( )
|
|
{
|
|
|
|
if(this.disabled || !this.div.is(':visible'))
|
|
{
|
|
return;
|
|
}
|
|
this.rows.css('overflow-y', 'visible');
|
|
|
|
var rows = jQuery('.calendar_eventRows');
|
|
var width = rows.width();
|
|
var events = jQuery('.calendar_calEvent', rows)
|
|
.each(function() {
|
|
var event = jQuery(this);
|
|
event.width((event.width() / width) * 100 + '%')
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* Reset after printing
|
|
*/
|
|
afterPrint( )
|
|
{
|
|
this.rows.css('overflow-y', 'auto');
|
|
}
|
|
}
|
|
et2_register_widget(et2_calendar_planner, ["calendar-planner"]); |