egroupware_official/calendar/js/et2_widget_planner_row.ts
Milan f430b66d3b converted egw_action from javascript to typescript
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

(cherry picked from commit 5e3c67a5cf)
2023-09-13 10:40:32 +02:00

842 lines
23 KiB
TypeScript

/*
* Egroupware
*
* @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_daycol.js;
/calendar/js/et2_widget_event.js;
*/
import {et2_createWidget, et2_register_widget, WidgetConfig} from "../../api/js/etemplate/et2_core_widget";
import {et2_valueWidget} from "../../api/js/etemplate/et2_core_valueWidget";
import {ClassWithAttributes} from "../../api/js/etemplate/et2_core_inheritance";
import {et2_action_object_impl} from "../../api/js/etemplate/et2_core_DOMWidget";
import {et2_calendar_planner} from "./et2_widget_planner";
import {egw_getObjectManager, egwActionObject} from "../../api/js/egw_action/egw_action";
import {EGW_AI_DRAG_ENTER, EGW_AI_DRAG_OUT} from "../../api/js/egw_action/egw_action_constants";
import {et2_IResizeable} from "../../api/js/etemplate/et2_core_interfaces";
import {egw} from "../../api/js/jsapi/egw_global";
import {et2_calendar_view} from "./et2_widget_view";
/**
* Class for one row of a planner
*
* This widget is responsible for the label on the side
*
*/
export class et2_calendar_planner_row extends et2_valueWidget implements et2_IResizeable
{
static readonly _attributes: any = {
start_date: {
name: "Start date",
type: "any"
},
end_date: {
name: "End date",
type: "any"
},
value: {
type: "any"
}
};
private div: JQuery;
private title: JQuery;
private rows: JQuery;
private _cached_rows: any[];
private _row_height = 20;
private _actionObject: egwActionObject;
/**
* Constructor
*/
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
{
// Call the inherited constructor
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_planner_row._attributes, _child || {}));
// Main container
this.div = jQuery(document.createElement("div"))
.addClass("calendar_plannerRowWidget")
.css('width',this.options.width);
this.title = jQuery(document.createElement('div'))
.addClass("calendar_plannerRowHeader")
.appendTo(this.div);
this.rows = jQuery(document.createElement('div'))
.addClass("calendar_eventRows")
.appendTo(this.div);
this.setDOMNode(this.div[0]);
this.set_start_date(this.options.start_date);
this.set_end_date(this.options.end_date);
this._cached_rows = [];
}
doLoadingFinished( )
{
super.doLoadingFinished();
this.set_label(this.options.label);
this._draw();
// Actions are set on the parent, so we need to explicitly get in here
// and get ours
this._link_actions(this.getParent().options.actions || []);
return true;
}
destroy( )
{
super.destroy();
}
getDOMNode(_sender)
{
if(_sender === this || !_sender)
{
return this.div[0];
}
if(_sender._parent === this)
{
return this.rows[0];
}
}
/**
* Link the actions to the DOM nodes / widget bits.
*
* @param {object} actions {ID: {attributes..}+} map of egw action information
*/
_link_actions(actions)
{
// Get the parent? Might be a grid row, might not. Either way, it is
// just a container with no valid actions
let objectManager = egw_getObjectManager(this.getInstanceManager().app, true, 1);
objectManager = objectManager.getObjectById(this.getInstanceManager().uniqueId,2) || objectManager;
let parent = objectManager.getObjectById(this.id, 1) || objectManager.getObjectById(this.getParent().id, 1) || objectManager;
if(!parent)
{
egw.debug('error','No parent objectManager found');
return;
}
// This binds into the egw action system. Most user interactions (drag to move, resize)
// are handled internally using jQuery directly.
let widget_object = this._actionObject || parent.getObjectById(this.id);
const aoi = new et2_action_object_impl(this, this.getDOMNode(this)).getAOI();
const planner = this.getParent();
for(let i = 0; i < parent.children.length; i++)
{
const parent_finder = jQuery(parent.children[i].iface.doGetDOMNode()).find(this.div);
if(parent_finder.length > 0)
{
parent = parent.children[i];
break;
}
}
// Determine if we allow a dropped event to use the invite/change actions
const _invite_enabled = function (action, event, target)
{
var event = event.iface.getWidget();
const row = target.iface.getWidget() || false;
if(event === row || !event || !row ||
!event.options || !event.options.value.participants
)
{
return false;
}
let owner_match = false;
const own_row = event.getParent() === row;
for (let id in event.options.value.participants)
{
owner_match = owner_match || row.node.dataset.participants === '' + id;
}
const enabled = !owner_match &&
// Not inside its own timegrid
!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' && widget_object.getActionLink('egw_link_drop').enabled)
{
this.getWidget().getParent()._event_drop.call(
jQuery('.calendar_d-n-d_timeCounter', _data.ui.draggable)[0],
this.getWidget().getParent(), event, _data.ui,
this.getWidget()
);
}
const drag_listener = function(_event)
{
let position = {};
if(planner.options.group_by === 'month')
{
position = {left: _event.clientX, top: _event.clientY};
}
else
{
let style = getComputedStyle(_data.ui.helper);
position = {
top: parseInt(style.top),
left: _event.clientX - jQuery(this).parent().offset().left
}
}
aoi.getWidget().getParent()._drag_helper(jQuery('.calendar_d-n-d_timeCounter', _data.ui.draggable)[0], position, 0);
var event = _data.ui.selected[0];
if(!event || event.id && event.id.indexOf('calendar') !== 0)
{
event = false;
}
if(event)
{
_invite_enabled(
widget_object.getActionLink('invite').actionObj,
event,
widget_object
);
}
};
const 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_row' + widget_object.id, drag_listener);
jQuery(_data.ui.draggable).on('dragend.et2_timegrid_row' + widget_object.id, function()
{
jQuery(_data.ui.draggable).off('drag.et2_timegrid_row' + widget_object.id);
});
widget_object.iface.getWidget().div.addClass('drop-hover');
// Disable invite / change actions for same calendar or already participant
let event = _data.ui.selected[0];
if(!event || event.id && event.id.indexOf('calendar') !== 0)
{
event = false;
}
if(event)
{
_invite_enabled(
widget_object.getActionLink('invite').actionObj,
event,
widget_object
);
}
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_row' + widget_object.id);
// Remove highlight
widget_object.iface.getWidget().div.removeClass('drop-hover');
// 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
));
}
else
{
widget_object.setAOI(aoi);
}
this._actionObject = widget_object;
// Delete all old objects
widget_object.clear();
widget_object.unregisterActions();
// Go over the widget & add links - this is where we decide which actions are
// 'allowed' for this widget at this time
const action_links = this._get_action_links(actions);
this.getParent()._init_links_dnd(widget_object.manager, action_links);
widget_object.updateActionLinks(action_links);
}
/**
* 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)
{
const action_links = [];
// Only these actions are allowed without a selection (empty actions)
const empty_actions = ['add'];
for(let i in actions)
{
const action = actions[i];
if(empty_actions.indexOf(action.id) !== -1 || action.type == 'drop')
{
action_links.push(typeof action.id != 'undefined' ? action.id : i);
}
}
return action_links;
}
/**
* Draw the individual divs for weekends and events
*/
_draw( )
{
// Remove any existing
this.rows.remove('.calendar_eventRowsMarkedDay,.calendar_eventRowsFiller').nextAll().remove();
let days = 31;
let width = '100';
if (this.getParent().options.group_by === 'month')
{
days = this.options.end_date.getUTCDate();
if(days < 31)
{
const diff = 31 - days;
width = 'calc('+(diff * 3.23) + '% - ' + (diff * 7) + 'px)';
}
}
// mark weekends and other special days in yearly planner
if (this.getParent().options.group_by == 'month')
{
this.rows.append(this._yearlyPlannerMarkDays(this.options.start_date, days));
}
if (this.getParent().options.group_by === 'month' && days < 31)
{
// add a filler for non existing days in that month
this.rows.after('<div class="calendar_eventRowsFiller"'+
' style="width:'+width+';" ></div>');
}
}
set_label(label)
{
this.options.label = label;
this.title.text(label);
if(this.getParent().options.group_by === 'month')
{
this.title.attr('data-date', this.options.start_date.toJSON());
this.title.attr('data-sortby', 'user');
this.title.addClass('et2_clickable et2_link');
}
else
{
this.title.attr('data-date','');
this.title.removeClass('et2_clickable');
}
}
/**
* Change the start date
*
* @param {Date} new_date New end date
* @returns {undefined}
*/
set_start_date(new_date)
{
if(!new_date || new_date === null)
{
throw new TypeError('Invalid end date. ' + new_date.toString());
}
this.options.start_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON());
this.options.start_date.setUTCHours(0);
this.options.start_date.setUTCMinutes(0);
this.options.start_date.setUTCSeconds(0);
}
/**
* Change the end date
*
* @param {string|number|Date} new_date New end date
* @returns {undefined}
*/
set_end_date(new_date)
{
if(!new_date || new_date === null)
{
throw new TypeError('Invalid end date. ' + new_date.toString());
}
this.options.end_date = new Date(typeof new_date == 'string' ? new_date : new_date.toJSON());
this.options.end_date.setUTCHours(23);
this.options.end_date.setUTCMinutes(59);
this.options.end_date.setUTCSeconds(59);
}
/**
* Mark special days (birthdays, holidays) on the planner
*
* @param {Date} start Start of the month
* @param {number} days How many days in the month
*/
_yearlyPlannerMarkDays(start,days)
{
const day_width = 3.23;
const t = new Date(start);
let content = '';
for(let i = 0; i < days; i++)
{
const holidays = [];
// TODO: implement this, pull / copy data from et2_widget_timegrid
const day_class = (<et2_calendar_planner>this.getParent()).day_class_holiday(t, holidays);
if (day_class) // no regular weekday
{
content += '<div class="calendar_eventRowsMarkedDay '+day_class+
'" style="left: '+(i*day_width)+'%; width:'+day_width+'%;"'+
(holidays ? ' title="'+holidays.join(',')+'"' : '')+
' ></div>';
}
t.setUTCDate(t.getUTCDate()+1);
}
return content;
}
/**
* Callback used when the daywise data changes
*
* Events should update themselves when their data changes, here we are
* dealing with a change in which events are displayed on this row.
*
* @param {String[]} event_ids
* @returns {undefined}
*/
_data_callback( event_ids)
{
const events = [];
if(event_ids == null || typeof event_ids.length == 'undefined') event_ids = [];
for(let i = 0; i < event_ids.length; i++)
{
let event = <any>egw.dataGetUIDdata('calendar::' + event_ids[i]);
event = event && event.data || false;
if(event && event.date)
{
events.push(event);
}
else if (event)
{
// Got an ID that doesn't belong
event_ids.splice(i--,1);
}
}
if(!this.getParent().disabled && event_ids.length > 0)
{
this.resize();
this._update_events(events);
}
}
date_helper(value)
{
return (<et2_calendar_view>this.getParent()).date_helper(value);
}
/**
* Load the event data for this day and create event widgets for each.
*
* If event information is not provided, it will be pulled from the content array.
*
* @param {Object[]} [events] Array of event information, one per event.
*/
_update_events(events)
{
// Remove all events
while(this._children.length > 0)
{
const node = this._children[this._children.length - 1];
this.removeChild(node);
node.destroy();
}
this._cached_rows = [];
for(var c = 0; c < events.length; c++)
{
// Create event
var event = et2_createWidget('calendar-event',{
id:'event_'+events[c].row_id,
value: events[c]
},this);
}
// Seperate loop so column sorting finds all children in the right place
for(var c = 0; c < events.length; c++)
{
let event = this.getWidgetById('event_'+events[c].row_id);
if(!event) continue;
if(this.isInTree())
{
event.doLoadingFinished();
}
}
}
/**
* Position the event according to it's time and how this widget is laid
* out.
*
* @param {undefined|Object|et2_calendar_event} event
*/
position_event(event?)
{
const rows = this._spread_events();
const height = rows.length * this._row_height;
let row_width = this.rows.width();
if(row_width == 0)
{
// Not rendered yet or something
row_width = this.getParent().gridHeader.width() - this.title.width()
}
row_width -= 15;
for(let c = 0; c < rows.length; c++)
{
// Calculate vertical positioning
const top = c * (100.0 / rows.length);
for(let i = 0; (rows[c].indexOf(event) >=0 || !event) && i < rows[c].length; i++)
{
// Calculate horizontal positioning
const left = this._time_to_position(rows[c][i].options.value.start);
const width = this._time_to_position(rows[c][i].options.value.end) - left;
// Position the event
rows[c][i].div.css('top', top+'%');
rows[c][i].div.css('height', (100/rows.length)+'%');
rows[c][i].div.css('left', left.toFixed(1)+'%');
rows[c][i].div.outerWidth((width/100 * row_width) +'px');
}
}
if(height)
{
this.div.height(height+'px');
}
}
/**
* Sort a day's events into non-overlapping rows
*
* @returns {Array[]} Events sorted into rows
*/
_spread_events()
{
// Keep it so we don't have to re-do it when the next event asks
let cached_length = 0;
this._cached_rows.map(function(row) {cached_length+=row.length;});
if(cached_length === this._children.length)
{
return this._cached_rows;
}
// sorting the events in non-overlapping rows
const rows = [];
const row_end = [0];
// Sort in chronological order, so earliest ones are at the top
this._children.sort(function(a,b) {
const start = new Date(a.options.value.start) - new Date(b.options.value.start);
const end = new Date(a.options.value.end) - new Date(b.options.value.end);
// Whole day events sorted by ID, normal events by start / end time
if(a.options.value.whole_day && b.options.value.whole_day)
{
// Longer duration comes first so we have nicer bars across the top
const duration =
(new Date(b.options.value.end) - new Date(b.options.value.start)) -
(new Date(a.options.value.end) - new Date(a.options.value.start));
return duration ? duration : (a.options.value.app_id - b.options.value.app_id);
}
else if (a.options.value.whole_day || b.options.value.whole_day)
{
return a.options.value.whole_day ? -1 : 1;
}
return start ? start : end;
});
for(let n = 0; n < this._children.length; n++)
{
const event = this._children[n].options.value || false;
if(typeof event.start !== 'object')
{
event.start = this.date_helper(event.start);
}
if(typeof event.end !== 'object')
{
event.end = this.date_helper(event.end);
}
if(typeof event['start_m'] === 'undefined')
{
let day_start = event.start.valueOf() / 1000;
const dst_check = new Date(event.start);
dst_check.setUTCHours(12);
// if daylight saving is switched on or off, correct $day_start
// gives correct times after 2am, times between 0am and 2am are wrong
const daylight_diff = day_start + 12 * 60 * 60 - (dst_check.valueOf() / 1000);
if(daylight_diff)
{
day_start -= daylight_diff;
}
event['start_m'] = event.start.getUTCHours() * 60 + event.start.getUTCMinutes();
if (event['start_m'] < 0)
{
event['start_m'] = 0;
event['multiday'] = true;
}
event['end_m'] = event.end.getUTCHours() * 60 + event.end.getUTCMinutes();
if (event['end_m'] >= 24*60)
{
event['end_m'] = 24*60-1;
event['multiday'] = true;
}
if(!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59)
{
event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0');
}
}
// Skip events entirely on hidden weekends
if(this._hidden_weekend_event(event))
{
const node = this._children[n];
this.removeChild(n--);
node.destroy();
continue;
}
const event_start = new Date(event.start).valueOf();
for(var row = 0; row_end[row] > event_start; ++row); // find a "free" row (no other event)
if(typeof rows[row] === 'undefined') rows[row] = [];
rows[row].push(this._children[n]);
row_end[row] = new Date(event['end']).valueOf();
}
this._cached_rows = rows;
return rows;
}
/**
* Check to see if the event is entirely on a hidden weekend
*
* @param values Array of event values, not an et2_widget_event
*/
_hidden_weekend_event(values)
{
if(!this.getParent() || this.getParent().options.group_by == 'month' || this.getParent().options.show_weekend)
{
return false;
}
// Starts on Saturday or Sunday, ends Sat or Sun, less than 2 days long
else if([0,6].indexOf(values.start.getUTCDay()) !== -1 && [0,6].indexOf(values.end.getUTCDay()) !== -1
&& values.end - values.start < 2 * 24 * 3600 * 1000)
{
return true;
}
return false;
}
/**
* Calculates the horizontal position based on the time given, as a percentage
* between the start and end times
*
* @param {int|Date|string} time in minutes from midnight, or a Date in string or object form
* @param {int|Date|string} start Earliest possible time (0%)
* @param {int|Date|string} end Latest possible time (100%)
* @return {float} position in percent
*/
_time_to_position(time, start?, end?)
{
let pos = 0.0;
// Handle the different value types
start = this.options.start_date;
end = this.options.end_date;
if(typeof start === 'string')
{
start = new Date(start);
end = new Date(end);
}
const wd_start = 60 * (parseInt(''+egw.preference('workdaystarts', 'calendar')) || 9);
const wd_end = 60 * (parseInt(''+egw.preference('workdayends', 'calendar')) || 17);
let t = time;
if(typeof time === 'number' && time < 3600)
{
t = new Date(start.valueOf() + wd_start * 3600*1000);
}
else
{
t = new Date(time);
}
// Limits
if(t <= start) return 0; // We are left of our scale
if(t >= end) return 100; // We are right of our scale
// Remove space for weekends, if hidden
let weekend_count = 0;
let weekend_before = 0;
let partial_weekend = 0;
if(this.getParent().options.group_by !== 'month' && this.getParent() && !this.getParent().options.show_weekend)
{
const counter_date = new Date(start);
do
{
if([0,6].indexOf(counter_date.getUTCDay()) !== -1)
{
if(counter_date.getUTCDate() === t.getUTCDate() && counter_date.getUTCMonth() === t.getUTCMonth())
{
// Event is partially on a weekend
partial_weekend += (t.getUTCHours() *60 + t.getUTCMinutes())*60*1000;
}
else if(counter_date < t)
{
weekend_before++;
}
weekend_count++;
}
counter_date.setUTCDate(counter_date.getUTCDate() + 1);
} while(counter_date < end);
// Put it in ms
weekend_before *= 24 * 3600 * 1000;
weekend_count *= 24 * 3600 * 1000;
}
// Basic scaling, doesn't consider working times
pos = (t - start - weekend_before-partial_weekend) / (end - start - weekend_count);
// Month view
if(this.getParent().options.group_by !== 'month')
{
// Daywise scaling
/* Needs hourly scales that consider working hours
var start_date = new Date(start.getUTCFullYear(), start.getUTCMonth(),start.getUTCDate());
var end_date = new Date(end.getUTCFullYear(), end.getUTCMonth(),end.getUTCDate());
var t_date = new Date(t.getUTCFullYear(), t.getUTCMonth(),t.getUTCDate());
var days = Math.round((end_date - start_date) / (24 * 3600 * 1000))+1;
pos = 1 / days * Math.round((t_date - start_date) / (24*3600 * 1000));
var time_of_day = typeof t === 'object' ? 60 * t.getUTCHours() + t.getUTCMinutes() : t;
if (time_of_day >= wd_start)
{
var day_percentage = 0.1;
if (time_of_day > wd_end)
{
day_percentage = 1;
}
else
{
var wd_length = wd_end - wd_start;
if (wd_length <= 0) wd_length = 24*60;
day_percentage = (time_of_day-wd_start) / wd_length; // between 0 and 1
}
pos += day_percentage / days;
}
*/
}
else
{
// 2678400 is the number of seconds in 31 days
pos = (t - start) / 2678400000;
}
pos = 100 * pos;
return pos;
}
// Resizable interface
/**
* Resize
*
* Parent takes care of setting proper width & height for the containing div
* here we just need to adjust the events to fit the new size.
*/
resize ()
{
if(this.disabled || !this.div.is(':visible') || this.getParent().disabled)
{
return;
}
const row = jQuery('<div class="calendar_plannerEventRowWidget"></div>').appendTo(this.rows);
this._row_height = (parseInt(window.getComputedStyle(row[0]).getPropertyValue("height")) || 20);
row.remove();
// Resize & position all events
this.position_event();
}
}
et2_register_widget(et2_calendar_planner_row, ["calendar-planner_row"]);