mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-26 01:43:47 +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
1493 lines
49 KiB
TypeScript
1493 lines
49 KiB
TypeScript
/*
|
|
* Egroupware Calendar event widget
|
|
*
|
|
* @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
|
|
/etemplate/js/et2_core_valueWidget;
|
|
*/
|
|
|
|
import {et2_register_widget, et2_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, et2_DOMWidget} from "../../api/js/etemplate/et2_core_DOMWidget";
|
|
import {et2_calendar_daycol} from "./et2_widget_daycol";
|
|
import {et2_calendar_planner_row} from "./et2_widget_planner_row";
|
|
import {et2_IDetachedDOM} from "../../api/js/etemplate/et2_core_interfaces";
|
|
import {et2_activateLinks, et2_insertLinkText, et2_no_init} from "../../api/js/etemplate/et2_core_common";
|
|
import {egw_getAppObjectManager, egwActionObject} from '../../api/js/egw_action/egw_action';
|
|
import {egw} from "../../api/js/jsapi/egw_global";
|
|
import {et2_container} from "../../api/js/etemplate/et2_core_baseWidget";
|
|
import {Et2Dialog} from "../../api/js/etemplate/Et2Dialog/Et2Dialog";
|
|
import {formatDate, formatTime} from "../../api/js/etemplate/Et2Date/Et2Date";
|
|
import {ColorTranslator} from "colortranslator";
|
|
import {StaticOptions as so} from "../../api/js/etemplate/Et2Select/StaticOptions";
|
|
import {Et2Select} from "../../api/js/etemplate/Et2Select/Et2Select";
|
|
import {SelectOption} from "../../api/js/etemplate/Et2Select/FindSelectOptions";
|
|
import {CalendarApp} from "./app";
|
|
|
|
/**
|
|
* Class for a single event, displayed in either the timegrid or planner view
|
|
*
|
|
* It is possible to directly provide all information directly, but calendar
|
|
* uses egw.data for caching, so ID is all that is needed.
|
|
*
|
|
* Note that there are several pieces of information that have 'ID' in them:
|
|
* - row_id - used by both et2_calendar_event and the nextmatch to uniquely
|
|
* identify a particular entry or entry ocurrence
|
|
* - id - Recurring events may have their recurrence as a timestamp after their ID,
|
|
* such as '194:1453318200', or not. It's usually (always?) the same as row ID.
|
|
* - app_id - the ID according to the source application. For calendar, this
|
|
* is the same as ID (but always with the recurrence), for other apps this is
|
|
* usually just an integer. With app_id and app, you should be able to call
|
|
* egw.open() and get the specific entry.
|
|
* - Events from other apps will have their app name prepended to their ID, such
|
|
* as 'infolog123', so app_id and id will be different for these events
|
|
* - Cache ID is the same as other apps, and looks like 'calendar::<row_id>'
|
|
* - The DOM ID for the containing div is event_<row_id>
|
|
*
|
|
* Events are expected to be added to either et2_calendar_daycol or
|
|
* et2_calendar_planner_row rather than either et2_calendar_timegrid or
|
|
* et2_calendar_planner directly.
|
|
*
|
|
*/
|
|
export class et2_calendar_event extends et2_valueWidget implements et2_IDetachedDOM
|
|
{
|
|
|
|
static readonly _attributes: any = {
|
|
"value": {
|
|
type: "any",
|
|
default: et2_no_init
|
|
},
|
|
"onclick": {
|
|
"description": "JS code which is executed when the element is clicked. " +
|
|
"If no handler is provided, or the handler returns true and the event is not read-only, the " +
|
|
"event will be opened according to calendar settings."
|
|
}
|
|
};
|
|
private div: JQuery;
|
|
private title: JQuery;
|
|
private body: JQuery;
|
|
private icons: JQuery;
|
|
private _need_actions_linked: boolean = false;
|
|
private _actionObject: egwActionObject;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
constructor(_parent, _attrs?: WidgetConfig, _child?: object)
|
|
{
|
|
// Call the inherited constructor
|
|
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_event._attributes, _child || {}));
|
|
|
|
const event = this;
|
|
|
|
// Main container
|
|
this.div = jQuery(document.createElement("div"))
|
|
.addClass("calendar_calEvent")
|
|
.addClass(this.options.class)
|
|
.css('width', this.options.width)
|
|
.on('mouseenter', function ()
|
|
{
|
|
// Bind actions on first mouseover for faster creation
|
|
if (event._need_actions_linked)
|
|
{
|
|
event._copy_parent_actions();
|
|
}
|
|
// Tooltip
|
|
if (!event._tooltipElem)
|
|
{
|
|
event.options.statustext_html = true;
|
|
event.set_statustext(event._tooltip());
|
|
if (event.statustext)
|
|
{
|
|
return event.div.trigger('mouseenter');
|
|
}
|
|
}
|
|
// Hacky to remove egw's tooltip border and let the mouse in
|
|
window.setTimeout(function ()
|
|
{
|
|
jQuery('body .egw_tooltip')
|
|
.css('border', 'none')
|
|
.on('mouseenter', function()
|
|
{
|
|
if(event.div)
|
|
{
|
|
event.div.off('mouseleave.tooltip');
|
|
}
|
|
|
|
jQuery('body.egw_tooltip').remove();
|
|
jQuery('body').append(this);
|
|
jQuery(this).stop(true).fadeTo(400, 1)
|
|
.on('mouseleave', function()
|
|
{
|
|
jQuery(this).fadeOut('400', function()
|
|
{
|
|
jQuery(this).remove();
|
|
// Set up to work again
|
|
event.set_statustext(event._tooltip());
|
|
});
|
|
});
|
|
});
|
|
|
|
}, 105);
|
|
});
|
|
this.title = jQuery(document.createElement('div'))
|
|
.addClass("calendar_calEventHeader")
|
|
.appendTo(this.div);
|
|
this.body = jQuery(document.createElement('div'))
|
|
.addClass("calendar_calEventBody")
|
|
.appendTo(this.div);
|
|
this.icons = jQuery(document.createElement('div'))
|
|
.addClass("calendar_calEventIcons")
|
|
.appendTo(this.title);
|
|
|
|
this.setDOMNode(this.div[0]);
|
|
}
|
|
|
|
doLoadingFinished()
|
|
{
|
|
super.doLoadingFinished();
|
|
|
|
// Already know what is needed to hook to cache
|
|
if (this.options.value && this.options.value.row_id)
|
|
{
|
|
egw.dataRegisterUID(
|
|
'calendar::' + this.options.value.row_id,
|
|
this._UID_callback,
|
|
this,
|
|
this.getInstanceManager().execId,
|
|
this.id
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
destroy()
|
|
{
|
|
super.destroy();
|
|
|
|
if (this._actionObject)
|
|
{
|
|
this._actionObject.remove();
|
|
this._actionObject = null;
|
|
}
|
|
|
|
this.div.off();
|
|
this.title.remove();
|
|
this.title = null;
|
|
this.body.remove();
|
|
this.body = null;
|
|
this.icons = null;
|
|
this.div.remove();
|
|
this.div = null;
|
|
|
|
jQuery('body.egw_tooltip').remove();
|
|
|
|
// Unregister, or we'll continue to be notified...
|
|
if (this.options.value)
|
|
{
|
|
const old_app_id = this.options.value.row_id;
|
|
egw.dataUnregisterUID('calendar::' + old_app_id, null, this);
|
|
}
|
|
}
|
|
|
|
set_value(_value)
|
|
{
|
|
// Un-register for updates
|
|
if (this.options.value)
|
|
{
|
|
var old_id = this.options.value.row_id;
|
|
if (!_value || !_value.row_id || old_id !== _value.row_id)
|
|
{
|
|
egw.dataUnregisterUID('calendar::' + old_id, null, this);
|
|
}
|
|
}
|
|
this.options.value = _value;
|
|
|
|
// Register for updates
|
|
const id = this.options.value.row_id;
|
|
if (!old_id || old_id !== id)
|
|
{
|
|
egw.dataRegisterUID('calendar::' + id, this._UID_callback, this, this.getInstanceManager().execId, this.id);
|
|
}
|
|
if (_value && !egw.dataHasUID('calendar::' + id))
|
|
{
|
|
egw.dataStoreUID('calendar::' + id, _value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Callback for changes in cached data
|
|
*/
|
|
_UID_callback(event)
|
|
{
|
|
// Copy to avoid changes, which may cause nm problems
|
|
const value = event === null ? null : jQuery.extend({}, event);
|
|
let parent = <et2_DOMWidget>this.getParent();
|
|
let parent_owner = parent.getDOMNode(parent).dataset['owner'] || parent.getParent().options.owner;
|
|
if (parent_owner.indexOf(',') >= 0)
|
|
{
|
|
parent_owner = parent_owner.split(',');
|
|
}
|
|
|
|
// Make sure id is a string, check values
|
|
if (value)
|
|
{
|
|
this._values_check(value);
|
|
}
|
|
|
|
// Check for changing days in the grid view
|
|
let state = this.getInstanceManager().app_obj.calendar.getState() || app.calendar.getState();
|
|
if (!this._sameday_check(value) || !this._status_check(value, state.status_filter, parent_owner))
|
|
{
|
|
// May need to update parent to remove out-of-view events
|
|
parent.removeChild(this);
|
|
if (event === null && parent && parent.instanceOf(et2_calendar_daycol))
|
|
{
|
|
(<et2_calendar_daycol>parent)._out_of_view();
|
|
}
|
|
|
|
// This should now cease to exist, as new events have been created
|
|
this.destroy();
|
|
return;
|
|
}
|
|
|
|
// Copy to avoid changes, which may cause nm problems
|
|
this.options.value = jQuery.extend({}, value);
|
|
|
|
if (this.getParent().options.date)
|
|
{
|
|
this.options.value.date = this.getParent().options.date;
|
|
}
|
|
|
|
// Let parent position - could also be et2_calendar_planner_row
|
|
(<et2_calendar_daycol>this.getParent()).position_event(this);
|
|
|
|
// Parent may remove this if the date isn't the same
|
|
if (this.getParent())
|
|
{
|
|
this._update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draw the event
|
|
*/
|
|
_update()
|
|
{
|
|
|
|
// Update to reflect new information
|
|
const event = this.options.value;
|
|
|
|
const id = event.row_id ? event.row_id : event.id + (event.recur_type ? ':' + event.recur_date : '');
|
|
const formatted_start = event.start.toJSON();
|
|
|
|
this.set_id('event_' + id);
|
|
if(this._actionObject)
|
|
{
|
|
this._actionObject.id = 'calendar::' + id;
|
|
}
|
|
|
|
this._need_actions_linked = !this.options.readonly;
|
|
|
|
// Make sure category stuff is there by faking a call to cache
|
|
so.cached_server_side(<Et2Select><unknown>{
|
|
nodeName: "ET2-SELECT-CAT_RO",
|
|
egw: () => this.egw()
|
|
}, "select-cat", ",,,calendar", true);
|
|
|
|
|
|
// Need cleaning? (DnD helper removes content)
|
|
// @ts-ignore
|
|
if(!this.div.has(this.title).length)
|
|
{
|
|
this.div
|
|
.empty()
|
|
.append(this.title)
|
|
.append(this.body);
|
|
}
|
|
|
|
let tooltip = jQuery(this._tooltip()).text();
|
|
// DOM nodes
|
|
this.div
|
|
// Set full day flag
|
|
.attr('data-full_day', event.whole_day)
|
|
|
|
// Put everything we need for basic interaction here, so it's available immediately
|
|
.attr('data-id', event.id)
|
|
.attr('data-app', event.app || 'calendar')
|
|
.attr('data-app_id', event.app_id)
|
|
.attr('data-start', formatted_start)
|
|
.attr('data-owner', event.owner)
|
|
.attr('data-recur_type', event.recur_type)
|
|
.attr('data-resize', event.whole_day ? 'WD' : '' + (event.recur_type ? 'S' : ''))
|
|
.attr('data-priority', event.priority)
|
|
// Accessibility
|
|
.attr("tabindex", 0)
|
|
.attr("aria-label", tooltip)
|
|
// Remove any category classes
|
|
.removeClass(function (index, css)
|
|
{
|
|
return (css.match(/(^|\s)cat_\S+/g) || []).join(' ');
|
|
})
|
|
// Remove any status classes
|
|
.removeClass(function (index, css)
|
|
{
|
|
return (css.match(/calendar_calEvent\S+/g) || []).join(' ');
|
|
})
|
|
.removeClass('calendar_calEventSmall')
|
|
.addClass(event.class)
|
|
.toggleClass('calendar_calEventPrivate', typeof event.private !== 'undefined' && event.private);
|
|
this.options.class = event.class;
|
|
const status_class = this._status_class();
|
|
|
|
// Add category classes, if real categories are set
|
|
if (event.category && event.category != '0')
|
|
{
|
|
const cats = event.category.split(',');
|
|
for (let i = 0; i < cats.length; i++)
|
|
{
|
|
this.div.addClass('cat_' + cats[i]);
|
|
}
|
|
}
|
|
|
|
this.div.toggleClass('calendar_calEventUnknown', event.participants[egw.user('account_id')] ? event.participants[egw.user('account_id')][0] === 'U' : false);
|
|
this.div.addClass(status_class);
|
|
|
|
this.body.toggleClass('calendar_calEventBodySmall', event.whole_day_on_top || false);
|
|
|
|
// Header
|
|
const title = !event.is_private ? egw.htmlspecialchars(event['title']) : egw.lang('private');
|
|
|
|
this.title
|
|
.html('<span class="calendar_calTimespan">' + this._get_timespan(event) + '<br /></span>')
|
|
.append('<span class="calendar_calEventTitle">' + title + '</span>');
|
|
|
|
// Colors - don't make them transparent if there is no color
|
|
const bg_color = new ColorTranslator(this.div.css('background-color'));
|
|
if (bg_color.RGBA != 'rgb(0,0,0,0)')
|
|
{
|
|
// Most statuses use colored borders
|
|
this.div.css('border-color', bg_color.RGBA);
|
|
}
|
|
|
|
this.icons.appendTo(this.title)
|
|
.html(this._icons().join(''));
|
|
|
|
// Body
|
|
if (event.whole_day_on_top)
|
|
{
|
|
this.body.html(title);
|
|
}
|
|
else
|
|
{
|
|
// @ts-ignore
|
|
const start_time = formatTime(event.start).trim();
|
|
|
|
this.body
|
|
.html('<span class="calendar_calEventTitle">' + title + '</span>')
|
|
.append('<span class="calendar_calTimespan">' + start_time + '</span>');
|
|
if(this.options.value.description.trim())
|
|
{
|
|
this.body
|
|
.append('<p>' + egw.htmlspecialchars(this.options.value.description) + '</p>');
|
|
}
|
|
}
|
|
|
|
// Clear tooltip for regeneration
|
|
this.set_statustext('');
|
|
|
|
// Height specific section
|
|
// This can take an unreasonable amount of time if parent is hidden
|
|
if (jQuery((<et2_DOMWidget>this.getParent()).getDOMNode(this)).is(':visible'))
|
|
{
|
|
this._small_size();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate display variants for when event is too short for full display
|
|
*
|
|
* Display is based on the number of visible lines, calculated off the header
|
|
* height:
|
|
* 1 - show just the event title, with ellipsis
|
|
* 2 - Show timespan and title, with ellipsis
|
|
* > 4 - Show description as well, truncated to fit
|
|
*/
|
|
_small_size()
|
|
{
|
|
|
|
if (this.options.value.whole_day_on_top) return;
|
|
|
|
// Skip for planner view, it's always small
|
|
if (this.getParent() && this.getParent().instanceOf(et2_calendar_planner_row)) return;
|
|
|
|
// Pre-calculation reset
|
|
this.div.removeClass('calendar_calEventSmall');
|
|
this.body.css('height', 'auto');
|
|
|
|
const line_height = parseFloat(this.div.css('line-height'));
|
|
let visible_lines = Math.floor(this.div.innerHeight() / line_height);
|
|
|
|
if(!this.title[0].clientHeight)
|
|
{
|
|
// Handle sizing while hidden, such as when calendar is not the active tab
|
|
visible_lines = 1;
|
|
}
|
|
visible_lines = Math.max(1, visible_lines);
|
|
|
|
if (this.getParent() && this.getParent().instanceOf(et2_calendar_daycol))
|
|
{
|
|
this.div.toggleClass('calendar_calEventSmall', visible_lines < 4);
|
|
this.div
|
|
.attr('data-visible_lines', visible_lines);
|
|
}
|
|
else if (this.getParent() && this.getParent().instanceOf(et2_calendar_planner_row))
|
|
{
|
|
// Less than 8 hours is small
|
|
this.div.toggleClass('calendar_calEventSmall', this.options.value.end.valueOf() - this.options.value.start.valueOf() < 28800000);
|
|
}
|
|
|
|
|
|
if (this.body.height() > this.div.height() - this.title.height() && visible_lines >= 4)
|
|
{
|
|
this.body.css('height', Math.floor((visible_lines - 1) * line_height - this.title.height()) + 'px');
|
|
}
|
|
else
|
|
{
|
|
this.body.css('height', '');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Examines the participants & returns CSS classname for status
|
|
*
|
|
* @returns {String}
|
|
*/
|
|
_status_class()
|
|
{
|
|
let status_class = 'calendar_calEventAllAccepted';
|
|
for (let id in this.options.value.participants)
|
|
{
|
|
let status = this.options.value.participants[id];
|
|
|
|
status = et2_calendar_event.split_status(status);
|
|
|
|
switch (status)
|
|
{
|
|
case 'A':
|
|
case '': // app without status
|
|
break;
|
|
case 'U':
|
|
status_class = 'calendar_calEventSomeUnknown';
|
|
return status_class; // break for
|
|
default:
|
|
status_class = 'calendar_calEventAllAnswered';
|
|
break;
|
|
}
|
|
}
|
|
return status_class;
|
|
}
|
|
|
|
/**
|
|
* Create tooltip shown on hover
|
|
*
|
|
* @return {String}
|
|
*/
|
|
_tooltip()
|
|
{
|
|
if(!this.div || !this.options.value || !this.options.value.app_id)
|
|
{
|
|
return '';
|
|
}
|
|
|
|
const border = this.div.css('borderTopColor');
|
|
const bg_color = this.div.css('background-color');
|
|
const header_color = this.title.css('color');
|
|
const timespan = this._get_timespan(this.options.value);
|
|
const parent = this.getParent() instanceof et2_calendar_daycol ? (<et2_calendar_daycol>this.getParent()) : (<et2_calendar_planner_row>this.getParent());
|
|
|
|
const start = parent.date_helper(this.options.value.start);
|
|
const end = parent.date_helper(this.options.value.end);
|
|
|
|
const times = !this.options.value.multiday ?
|
|
'<span class="calendar_calEventLabel">' + this.egw().lang('Time') + '</span>: ' + timespan :
|
|
'<span class="calendar_calEventLabel">' + this.egw().lang('Start') + '</span>: ' + start + ' ' +
|
|
'<span class="calendar_calEventLabel">' + this.egw().lang('End') + '</span>: ' + end;
|
|
let cat_label : (string | string[]) = '';
|
|
if(this.options.value.category)
|
|
{
|
|
let options = <SelectOption[]>so.cached_server_side(<Et2Select><unknown>{
|
|
nodeName: "ET2-SELECT-CAT_RO",
|
|
egw: () => this.egw()
|
|
}, "select-cat", ",,calendar", false) || [];
|
|
cat_label = options.find((o) => o.value == this.options.value.category)?.label || "";
|
|
}
|
|
|
|
// Activate links in description
|
|
let description_node = document.createElement("p");
|
|
description_node.className = "calendar_calEvent_description";
|
|
et2_insertLinkText(
|
|
et2_activateLinks(this.options.value.description), description_node, '_blank'
|
|
);
|
|
|
|
// Location + Videoconference
|
|
let location = '';
|
|
if (this.options.value.location || this.options.value['##videoconference'])
|
|
{
|
|
location = '<p>';
|
|
let location_node = document.createElement("span");
|
|
location_node.className = "calendar_calEventLabel";
|
|
et2_insertLinkText(et2_activateLinks(
|
|
this.egw().lang('Location') + ': ' +
|
|
egw.htmlspecialchars(this.options.value.location)), location_node, '_blank');
|
|
location += location_node.outerHTML;
|
|
|
|
if (this.options.value['##videoconference'])
|
|
{
|
|
// Click handler is set in _bind_videoconference()
|
|
location += (this.options.value.location.trim() ? '<br />' : '') +
|
|
'<span data-videoconference="' + this.options.value['##videoconference'] +
|
|
'" data-id="' + this.options.value['id'] + '" data-app_id="' + this.options.value['app_id'] +
|
|
'" data-title="' + this.options.value['title'] +
|
|
'" data-start="' + this.options.value['start'].toJSON() + '" data-end="' + this.options.value['end'].toJSON() + '">' +
|
|
this.egw().lang('Video conference') +
|
|
'<img src="' + this.egw().image('videoconference', 'calendar') + '"/></span>';
|
|
this._bind_videoconference();
|
|
}
|
|
location += '</p>';
|
|
}
|
|
|
|
// Participants
|
|
let participants = '';
|
|
if (this.options.value.participant_types[''])
|
|
{
|
|
participants += this.options.value.participant_types[''].join("<br />");
|
|
}
|
|
for (let type_name in this.options.value.participant_types)
|
|
{
|
|
if (type_name)
|
|
{
|
|
participants += '</p><p><span class="calendar_calEventLabel">' + type_name + ':</span><br />';
|
|
participants += this.options.value.participant_types[type_name].join("<br />");
|
|
}
|
|
}
|
|
|
|
return '<div class="calendar_calEventTooltip ' + this._status_class() + ' ' + this.options.class +
|
|
'" style="border-color: ' + border + '; background-color: ' + bg_color + ';">' +
|
|
'<div class="calendar_calEventHeaderSmall">' +
|
|
'<span style="color:' + header_color + '">' + timespan + '</span>' +
|
|
this.icons[0].outerHTML +
|
|
'</div>' +
|
|
'<div class="calendar_calEventBody">' +
|
|
'<h1 class="calendar_calEventTitle">' + egw.htmlspecialchars(this.options.value.title) + '</h1><br>' +
|
|
description_node.outerHTML +
|
|
'<p style="margin: 2px 0px;">' + times + '</p>' +
|
|
location +
|
|
(cat_label ? '<p><h2 class="calendar_calEventLabel">' + this.egw().lang('Category') + ': </h2>' + cat_label + '</p>' : '') +
|
|
'<p><h2 class="calendar_calEventLabel">' + this.egw().lang('Participants') + ': </h2><br />' +
|
|
participants + '</p>' + this._participant_summary(this.options.value.participants) +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
/**
|
|
* Generate participant summary line
|
|
*
|
|
* @returns {String}
|
|
*/
|
|
_participant_summary(participants)
|
|
{
|
|
if (Object.keys(this.options.value.participants).length < 2)
|
|
{
|
|
return '';
|
|
}
|
|
|
|
const participant_status = {A: 0, R: 0, T: 0, U: 0, D: 0};
|
|
const status_label = {A: 'accepted', R: 'rejected', T: 'tentative', U: 'unknown', D: 'delegated'};
|
|
const participant_summary = Object.keys(this.options.value.participants).length + ' ' + this.egw().lang('Participants') + ': ';
|
|
const status_totals = [];
|
|
|
|
for (let id in this.options.value.participants)
|
|
{
|
|
var status = this.options.value.participants[id].substr(0, 1);
|
|
participant_status[status]++;
|
|
}
|
|
for (let status in participant_status)
|
|
{
|
|
if (participant_status[status] > 0)
|
|
{
|
|
status_totals.push(participant_status[status] + ' ' + this.egw().lang(status_label[status]));
|
|
}
|
|
}
|
|
return participant_summary + status_totals.join(', ');
|
|
}
|
|
|
|
/**
|
|
* Get actual icons from list
|
|
*/
|
|
_icons(): string[]
|
|
{
|
|
const icons = [];
|
|
|
|
if (this.options.value.is_private)
|
|
{
|
|
// Hide everything
|
|
icons.push('<img src="' + this.egw().image('private', 'calendar') + '" title="' + this.egw().lang('private event') + '"/>');
|
|
}
|
|
else
|
|
{
|
|
if (this.options.value.icons)
|
|
{
|
|
jQuery.extend(icons, this.options.value.icons);
|
|
}
|
|
else if (this.options.value.app !== 'calendar')
|
|
{
|
|
let app_icon = "" + (egw.link_get_registry(this.options.value.app, 'icon') || (this.options.value.app + '/navbar'));
|
|
icons.push('<img src="' + this.egw().image(app_icon) + '" title="' + this.egw().lang(this.options.value.app) + '"/>');
|
|
}
|
|
if (this.options.value.priority == 3)
|
|
{
|
|
icons.push('<img src="' + this.egw().image('high', 'calendar') + '" title="' + this.egw().lang('high priority') + '"/>');
|
|
}
|
|
if (this.options.value.public == '0')
|
|
{
|
|
// Show private flag
|
|
icons.push('<img src="' + this.egw().image('private', 'calendar') + '" title="' + this.egw().lang('private event') + '"/>');
|
|
}
|
|
if (this.options.value['recur_type'])
|
|
{
|
|
icons.push('<img src="' + this.egw().image('recur', 'calendar') + '" title="' + this.egw().lang('recurring event') + '"/>');
|
|
}
|
|
// icons for single user, multiple users or group(s) and resources
|
|
const single = '<img src="' + this.egw().image('single', 'calendar') + '" title="' + this.egw().lang("single participant") + '"/>';
|
|
const multiple = '<img src="' + this.egw().image('users', 'calendar') + '" title="' + this.egw().lang("multiple participants") + '"/>';
|
|
for (const uid in this.options.value['participants'])
|
|
{
|
|
// @ts-ignore
|
|
if (Object.keys(this.options.value.participants).length == 1 && !isNaN(uid))
|
|
{
|
|
icons.push(single);
|
|
break;
|
|
}
|
|
// @ts-ignore
|
|
if (!isNaN(uid) && icons.indexOf(multiple) === -1)
|
|
{
|
|
icons.push(multiple);
|
|
}
|
|
/*
|
|
* TODO: resource icons
|
|
elseif(!isset($icons[$uid[0]]) && isset($this->bo->resources[$uid[0]]) && isset($this->bo->resources[$uid[0]]['icon']))
|
|
{
|
|
$icons[$uid[0]] = html::image($this->bo->resources[$uid[0]]['app'],
|
|
($this->bo->resources[$uid[0]]['icon'] ? $this->bo->resources[$uid[0]]['icon'] : 'navbar'),
|
|
lang($this->bo->resources[$uid[0]]['app']),
|
|
'width="16px" height="16px"');
|
|
}
|
|
*/
|
|
}
|
|
|
|
if (this.options.value.alarm && !jQuery.isEmptyObject(this.options.value.alarm) && !this.options.value.is_private)
|
|
{
|
|
icons.push('<img src="' + this.egw().image('notification_message') + '" title="' + this.egw().lang('alarm') + '"/>');
|
|
}
|
|
if (this.options.value.participants[egw.user('account_id')] && this.options.value.participants[egw.user('account_id')][0] == 'U')
|
|
{
|
|
icons.push('<img src="' + this.egw().image('needs-action', 'calendar') + '" title="' + this.egw().lang('Needs action') + '"/>');
|
|
}
|
|
if (this.options.value["##videoconference"])
|
|
{
|
|
icons.push('<img src="' + this.egw().image('videoconference', 'calendar') + '" title="' + this.egw().lang('video conference') + '"/>');
|
|
}
|
|
}
|
|
|
|
// Always include non-blocking, regardless of privacy
|
|
if (this.options.value.non_blocking)
|
|
{
|
|
icons.push('<img src="' + this.egw().image('nonblocking', 'calendar') + '" title="' + this.egw().lang('non blocking') + '"/>');
|
|
}
|
|
return icons;
|
|
}
|
|
|
|
/**
|
|
* Bind the click handler for opening the video conference
|
|
*
|
|
* Tooltips are placed in the DOM directly in the body, managed by egw.
|
|
*/
|
|
_bind_videoconference()
|
|
{
|
|
let vc_event = 'click.calendar_videoconference';
|
|
jQuery('body').off(vc_event)
|
|
.on(vc_event, '[data-videoconference]', function (event)
|
|
{
|
|
let data = egw.dataGetUIDdata("calendar::" + this.dataset.app_id);
|
|
app.calendar.joinVideoConference(this.dataset.videoconference, data?.data || this.dataset);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a text representation of the timespan of the event. Either start
|
|
* - end, or 'all day'
|
|
*
|
|
* @param {Object} event Event to get the timespan for
|
|
* @param {number} event.start_m Event start, in minutes from midnight
|
|
* @param {number} event.end_m Event end, in minutes from midnight
|
|
*
|
|
* @return {string} Timespan
|
|
*/
|
|
_get_timespan(event)
|
|
{
|
|
let timespan = '';
|
|
if (event['start_m'] === 0 && event['end_m'] >= 24 * 60 - 1)
|
|
{
|
|
if (event['end_m'] > 24 * 60)
|
|
{
|
|
// @ts-ignore
|
|
timespan = formatTime(event.start)
|
|
// @ts-ignore
|
|
.trim() + ' - ' + formatTime(event.end).trim();
|
|
}
|
|
else
|
|
{
|
|
timespan = this.egw().lang('Whole day');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
let duration: string | number = event.multiday ?
|
|
(event.end - event.start) / 60000 :
|
|
(event.end_m - event.start_m);
|
|
duration = Math.floor(duration / 60) + this.egw().lang('h') + (duration % 60 ? duration % 60 : '');
|
|
|
|
// @ts-ignore
|
|
timespan = formatTime(event.start).trim();
|
|
|
|
// @ts-ignore
|
|
timespan += ' - ' + formatTime(event.end);
|
|
|
|
timespan += ': ' + duration;
|
|
}
|
|
return timespan;
|
|
}
|
|
|
|
/**
|
|
* Make sure event data has all proper values, and format them as expected
|
|
* @param {Object} event
|
|
*/
|
|
_values_check(event)
|
|
{
|
|
// Make sure ID is a string
|
|
if (event.id)
|
|
{
|
|
event.id = '' + event.id;
|
|
}
|
|
|
|
// Parent might be a daycol or a planner_row
|
|
let parent = <et2_calendar_daycol>this.getParent();
|
|
|
|
// Use dates as objects
|
|
if (typeof event.start !== 'object')
|
|
{
|
|
event.start = parent.date_helper(event.start);
|
|
}
|
|
if (typeof event.end !== 'object')
|
|
{
|
|
event.end = parent.date_helper(event.end)
|
|
}
|
|
|
|
// We need minutes for durations
|
|
if (typeof event.start_m === 'undefined')
|
|
{
|
|
event.start_m = event.start.getUTCHours() * 60 + event.start.getUTCMinutes();
|
|
event.end_m = event.end.getUTCHours() * 60 + event.end.getUTCMinutes();
|
|
}
|
|
if (typeof event.multiday === 'undefined')
|
|
{
|
|
event.multiday = (event.start.getUTCFullYear() !== event.end.getUTCFullYear() ||
|
|
event.start.getUTCMonth() !== event.end.getUTCMonth() ||
|
|
event.start.getUTCDate() != event.end.getUTCDate());
|
|
}
|
|
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');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check to see if the provided event information is for the same date as
|
|
* what we're currently expecting, and that it has not been changed.
|
|
*
|
|
* If the date has changed, we adjust the associated daywise caches to move
|
|
* the event's ID to where it should be. This check allows us to be more
|
|
* directly reliant on the data cache, and less on any other control logic
|
|
* elsewhere first.
|
|
*
|
|
* @param {Object} event Map of event data from cache
|
|
* @param {string} event.date For non-recurring, single day events, this is
|
|
* the date the event is on.
|
|
* @param {string} event.start Start of the event (used for multi-day events)
|
|
* @param {string} event.end End of the event (used for multi-day events)
|
|
*
|
|
* @return {Boolean} Provided event data is for the same date
|
|
*/
|
|
_sameday_check(event)
|
|
{
|
|
// Event somehow got orphaned, or deleted
|
|
if (!this.getParent() || event === null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Also check participants against owner
|
|
const owner_match = et2_calendar_event.owner_check(event, this.getParent());
|
|
|
|
// Simple, same day
|
|
if (owner_match && this.options.value.date && event.date == this.options.value.date)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Multi-day non-recurring event spans days - date does not match
|
|
const event_start = new Date(event.start);
|
|
const event_end = new Date(event.end);
|
|
const parent = this.getParent();
|
|
if (owner_match && (parent instanceof et2_calendar_daycol) && parent.getDate() >= event_start && parent.getDate() <= event_end)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Delete all old actions
|
|
if (this._actionObject)
|
|
{
|
|
this._actionObject.clear();
|
|
this._actionObject.unregisterActions();
|
|
this._actionObject = null;
|
|
}
|
|
|
|
// Update daywise caches
|
|
const new_cache_id = CalendarApp._daywise_cache_id(event.date, this.getParent().options.owner);
|
|
let new_daywise: any = egw.dataGetUIDdata(new_cache_id);
|
|
new_daywise = new_daywise && new_daywise.data ? new_daywise.data : [];
|
|
let old_cache_id = '';
|
|
if (this.options.value && this.options.value.date)
|
|
{
|
|
old_cache_id = CalendarApp._daywise_cache_id(this.options.value.date, parent.options.owner);
|
|
}
|
|
|
|
if (new_cache_id != old_cache_id)
|
|
{
|
|
let old_daywise: any = egw.dataGetUIDdata(old_cache_id);
|
|
old_daywise = old_daywise && old_daywise.data ? old_daywise.data : [];
|
|
old_daywise.splice(old_daywise.indexOf(this.options.value.row_id), 1);
|
|
egw.dataStoreUID(old_cache_id, old_daywise);
|
|
|
|
if (new_daywise.indexOf(event.row_id) < 0)
|
|
{
|
|
new_daywise.push(event.row_id);
|
|
}
|
|
if (egw.dataHasUID(new_cache_id))
|
|
{
|
|
egw.dataStoreUID(new_cache_id, new_daywise);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check that the event passes the given status filter.
|
|
* Status filter is set in the sidebox and used when fetching several events, but if user changes their status
|
|
* for an event, it may no longer match and have to be removed.
|
|
*
|
|
* @param event
|
|
* @param filter
|
|
* @param owner The owner of the target / parent, not the event owner
|
|
* @private
|
|
*/
|
|
_status_check(event, filter: string, owner: string | string[]): boolean
|
|
{
|
|
if (!owner || !event)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If we're doing a bunch, just one passing is enough
|
|
if (typeof owner !== "string")
|
|
{
|
|
let pass = false;
|
|
for (let j = 0; j < owner.length && pass == false; j++)
|
|
{
|
|
pass = pass || this._status_check(event, filter, owner[j]);
|
|
}
|
|
return pass;
|
|
}
|
|
|
|
// Show also events just owned by selected user
|
|
// Group members can be owner too, those get handled when we check group memberships below
|
|
if (filter == 'owner' && owner == event.owner)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Get the relevant participant
|
|
let participant = event.participants[owner];
|
|
|
|
// If filter says don't look in groups, skip it all
|
|
if (!participant && filter === 'no-enum-groups')
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Couldn't find the current owner in the participant list, check groups & resources
|
|
if (!participant)
|
|
{
|
|
let options: any = null;
|
|
if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner'))
|
|
{
|
|
options = app.calendar.sidebox_et2.getWidgetById('owner').select_options
|
|
}
|
|
if ((isNaN(parseInt(owner)) || parseInt(owner) < 0) && options && typeof options.find == "function")
|
|
{
|
|
let resource = options.find(function (element)
|
|
{
|
|
return element.value == owner;
|
|
}) || {};
|
|
let matching_participant = typeof resource.resources == "undefined" ?
|
|
resource :
|
|
resource?.resources.filter(id => typeof event.participants[id] != "undefined");
|
|
if (matching_participant.length > 0)
|
|
{
|
|
return this._status_check(event, filter, matching_participant);
|
|
}
|
|
else if (filter == 'owner' && resource && resource.resources && resource.resources.indexOf(event.owner))
|
|
{
|
|
// owner param was a group but event is owned by someone in that group
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
let status = et2_calendar_event.split_status(participant);
|
|
|
|
switch (filter)
|
|
{
|
|
default:
|
|
case 'all':
|
|
return true;
|
|
case 'default': // Show all status, but rejected
|
|
return status !== 'R';
|
|
case 'accepted': //Show only accepted events
|
|
return status === 'A'
|
|
case 'unknown': // Show only invitations, not yet accepted or rejected
|
|
return status === 'U';
|
|
case 'tentative': // Show only tentative accepted events
|
|
return status === 'T';
|
|
case 'delegated': // Show only delegated events
|
|
return status === 'D';
|
|
case 'rejected': // Show only rejected events
|
|
return status === 'R';
|
|
// Handled above
|
|
//case 'owner': // Show also events just owned by selected user
|
|
case 'hideprivate': // Show all events, as if they were private
|
|
// handled server-side
|
|
return true;
|
|
case 'showonlypublic': // Show only events flagged as public, -not checked as private
|
|
return event.public == '1';
|
|
// Handled above
|
|
// case 'no-enum-groups': // Do not include events of group members
|
|
case 'not-unknown': // Show all status, but unknown
|
|
return status !== 'U';
|
|
case 'deleted': // Show events that have been deleted
|
|
return event.deleted;
|
|
}
|
|
}
|
|
|
|
attachToDOM()
|
|
{
|
|
let result = super.attachToDOM();
|
|
|
|
// Remove the binding for the click handler, unless there's something
|
|
// custom here.
|
|
if (!this.onclick)
|
|
{
|
|
jQuery(this.node).off("click");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Click handler calling custom handler set via onclick attribute to this.onclick.
|
|
* All other handling is done by the timegrid widget.
|
|
*
|
|
* @param {Event} _ev
|
|
* @returns {boolean}
|
|
*/
|
|
click(_ev)
|
|
{
|
|
let result = true;
|
|
if (typeof this.onclick == 'function')
|
|
{
|
|
// Make sure function gets a reference to the widget, splice it in as 2. argument if not
|
|
const args = Array.prototype.slice.call(arguments);
|
|
if (args.indexOf(this) == -1) args.splice(1, 0, this);
|
|
|
|
result = this.onclick.apply(this, args);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Show the recur prompt for this event
|
|
*
|
|
* Calls et2_calendar_event.recur_prompt with this event's value.
|
|
*
|
|
* @param {et2_calendar_event~prompt_callback} callback
|
|
* @param {Object} [extra_data]
|
|
*/
|
|
recur_prompt(callback, extra_data)
|
|
{
|
|
et2_calendar_event.recur_prompt(this.options.value, callback, extra_data);
|
|
}
|
|
|
|
/**
|
|
* Show the series split prompt for this event
|
|
*
|
|
* Calls et2_calendar_event.series_split_prompt with this event's value.
|
|
*
|
|
* @param {et2_calendar_event~prompt_callback} callback
|
|
*/
|
|
series_split_prompt(callback)
|
|
{
|
|
et2_calendar_event.series_split_prompt(this.options.value, this.options.value.recur_date, callback);
|
|
}
|
|
|
|
/**
|
|
* Copy the actions set on the parent, apply them to self
|
|
*
|
|
* This can take a while to do, so we try to do it only when needed - on mouseover
|
|
*/
|
|
_copy_parent_actions()
|
|
{
|
|
// Copy actions set in parent
|
|
if(!this.options.readonly && this.getParent() && !this.getParent().options.readonly)
|
|
{
|
|
let action_parent : et2_widget = this;
|
|
while(action_parent != null && !action_parent.options.actions &&
|
|
!(action_parent instanceof et2_container)
|
|
)
|
|
{
|
|
action_parent = action_parent.getParent();
|
|
}
|
|
try
|
|
{
|
|
this._link_actions(action_parent.options.actions || {});
|
|
this._need_actions_linked = false;
|
|
}
|
|
catch(e)
|
|
{
|
|
// something went wrong, but keep quiet about it
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Link the actions to the DOM nodes / widget bits.
|
|
*
|
|
* @param {object} actions {ID: {attributes..}+} map of egw action information
|
|
*/
|
|
_link_actions(actions)
|
|
{
|
|
if (!this._actionObject)
|
|
{
|
|
// Get the top level element - timegrid or so
|
|
var objectManager = this.getParent()._actionObject || this.getParent().getParent()._actionObject ||
|
|
egw_getAppObjectManager(true).getObjectById(this.getParent().getParent().getParent().id) || egw_getAppObjectManager(true);
|
|
this._actionObject = objectManager.getObjectById('calendar::' + this.options.value.row_id);
|
|
}
|
|
|
|
if (this._actionObject == null)
|
|
{
|
|
// Add a new container to the object manager which will hold the widget
|
|
// objects
|
|
this._actionObject = objectManager.insertObject(false, new egwActionObject(
|
|
'calendar::' + this.options.value.row_id, objectManager, et2_calendar_event.et2_event_action_object_impl(this, this.getDOMNode()),
|
|
this._actionManager || objectManager.manager.getActionById('calendar::' + this.options.value.row_id) || objectManager.manager
|
|
));
|
|
}
|
|
else
|
|
{
|
|
this._actionObject.setAOI(et2_calendar_event.et2_event_action_object_impl(this, this.getDOMNode(this)));
|
|
}
|
|
|
|
// Delete all old objects
|
|
this._actionObject.clear();
|
|
this._actionObject.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);
|
|
action_links.push('egw_link_drag');
|
|
action_links.push('egw_link_drop');
|
|
if (this._actionObject.parent.getActionLink('invite'))
|
|
{
|
|
action_links.push('invite');
|
|
}
|
|
this._actionObject.updateActionLinks(action_links);
|
|
}
|
|
|
|
/**
|
|
* Code for implementing et2_IDetachedDOM
|
|
*
|
|
* @param {array} _attrs array to add further attributes to
|
|
*/
|
|
getDetachedAttributes(_attrs)
|
|
{
|
|
|
|
}
|
|
|
|
getDetachedNodes()
|
|
{
|
|
return [this.getDOMNode()];
|
|
}
|
|
|
|
setDetachedAttributes(_nodes, _values)
|
|
{
|
|
|
|
}
|
|
|
|
// Static class stuff
|
|
/**
|
|
* Check event owner against a parent object
|
|
*
|
|
* As an event is edited, its participants may change. Also, as the state
|
|
* changes we may change which events are displayed and show the same event
|
|
* in several places for different users. Here we check the event participants
|
|
* against an owner value (which may be an array) to see if the event should be
|
|
* displayed or included.
|
|
*
|
|
* @param {Object} event - Event information
|
|
* @param {et2_widget_daycol|et2_widget_planner_row} parent - potential parent object
|
|
* that has an owner option
|
|
* @param {boolean} [owner_too] - Include the event owner in consideration, or only
|
|
* event participants
|
|
*
|
|
* @return {boolean} Should the event be displayed
|
|
*/
|
|
static owner_check(event, parent, owner_too?)
|
|
{
|
|
let owner_match = true;
|
|
let state = (parent.getInstanceManager ? parent.getInstanceManager().app_obj.calendar.state : false) || app.calendar?.state || {}
|
|
if (typeof owner_too === 'undefined' && state.status_filter)
|
|
{
|
|
owner_too = state.status_filter === 'owner';
|
|
}
|
|
let options: any = null;
|
|
if (app.calendar && app.calendar.sidebox_et2 && app.calendar.sidebox_et2.getWidgetById('owner'))
|
|
{
|
|
options = app.calendar.sidebox_et2.getWidgetById('owner').select_options;
|
|
}
|
|
else
|
|
{
|
|
options = parent.getArrayMgr("sel_options").getRoot().getEntry('owner');
|
|
}
|
|
if (event.participants && typeof parent.options.owner != 'undefined' && parent.options.owner.length > 0)
|
|
{
|
|
var parent_owner = jQuery.extend([], typeof parent.options.owner !== 'object' ?
|
|
[parent.options.owner] :
|
|
parent.options.owner);
|
|
owner_match = false;
|
|
if(!options)
|
|
{
|
|
// Could not find the owner options. Probably on home, just let it go.
|
|
owner_match = true;
|
|
}
|
|
const length = parent_owner.length;
|
|
for(var i = 0; i < length; i++)
|
|
{
|
|
// Handle groups & grouped resources like mailing lists, they won't match so
|
|
// we need the list - pull it from sidebox owner
|
|
if((isNaN(parent_owner[i]) || parent_owner[i] < 0) && options && typeof options.find == "function")
|
|
{
|
|
var resource = options.find(function(element)
|
|
{
|
|
return element.value == parent_owner[i];
|
|
}) || {};
|
|
if(resource && resource.resources)
|
|
{
|
|
parent_owner.splice(i, 1);
|
|
i--;
|
|
parent_owner = parent_owner.concat(resource.resources);
|
|
|
|
}
|
|
}
|
|
}
|
|
let participants = jQuery.extend([], Object.keys(event.participants));
|
|
for (var i = 0; i < participants.length; i++)
|
|
{
|
|
const id = participants[i];
|
|
// Expand group invitations
|
|
if (parseInt(id) < 0)
|
|
{
|
|
// Add in groups, if we can get them from options, great
|
|
var resource;
|
|
if (options && options.find && (resource = options.find(function (element)
|
|
{
|
|
return element.value === id;
|
|
})) && resource.resources)
|
|
{
|
|
participants = participants.concat(resource.resources);
|
|
}
|
|
else
|
|
{
|
|
// Add in groups, if we can get them (this is asynchronous)
|
|
egw.accountData(id, 'account_id', true, function (members)
|
|
{
|
|
participants = participants.concat(Object.keys(members));
|
|
}, this);
|
|
}
|
|
}
|
|
if (parent.options.owner == id ||
|
|
parent_owner.indexOf &&
|
|
parent_owner.indexOf(id) >= 0)
|
|
{
|
|
owner_match = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (owner_too && !owner_match)
|
|
{
|
|
owner_match = (parent.options.owner == event.owner ||
|
|
parent_owner.indexOf &&
|
|
parent_owner.indexOf(event.owner) >= 0);
|
|
}
|
|
return owner_match;
|
|
}
|
|
|
|
/**
|
|
* @callback et2_calendar_event~prompt_callback
|
|
* @param {string} button_id - One of ok, exception, series, single or cancel
|
|
* depending on which buttons are on the prompt
|
|
* @param {Object} event_data - Event information - whatever you passed in to
|
|
* the prompt.
|
|
*/
|
|
/**
|
|
* Recur prompt
|
|
* If the event is recurring, asks the user if they want to edit the event as
|
|
* an exception, or change the whole series. Then the callback is called.
|
|
*
|
|
* If callback is not provided, egw.open() will be used to open an edit dialog.
|
|
*
|
|
* If you call this on a single (non-recurring) event, the callback will be
|
|
* executed immediately, with the passed button_id as 'single'.
|
|
*
|
|
* @param {Object} event_data - Event information
|
|
* @param {string} event_data.id - Unique ID for the event, possibly with a
|
|
* timestamp
|
|
* @param {string|Date} event_data.start - Start date/time for the event
|
|
* @param {number} event_data.recur_type - Recur type, or 0 for a non-recurring event
|
|
* @param {et2_calendar_event~prompt_callback} [callback] - Callback is
|
|
* called with the button (exception, series, single or cancel) and the event
|
|
* data.
|
|
* @param {Object} [extra_data] - Additional data passed to the callback, used
|
|
* as extra parameters for default callback
|
|
*
|
|
* @augments {et2_calendar_event}
|
|
*/
|
|
public static recur_prompt(event_data, callback?, extra_data?)
|
|
{
|
|
let egw;
|
|
const edit_id = event_data.app_id;
|
|
const edit_date = event_data.start;
|
|
|
|
// seems window.opener somehow in certain conditions could be from different origin
|
|
// we try to catch the exception and in this case retrieve the egw object from current window.
|
|
try
|
|
{
|
|
egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar');
|
|
}
|
|
catch (e)
|
|
{
|
|
egw = window.egw('calendar');
|
|
}
|
|
|
|
const that = this;
|
|
|
|
const extra_params = extra_data && typeof extra_data == 'object' ? extra_data : {};
|
|
extra_params.date = edit_date.toJSON ? edit_date.toJSON() : edit_date;
|
|
if (typeof callback != 'function')
|
|
{
|
|
callback = function (_button_id)
|
|
{
|
|
switch (_button_id)
|
|
{
|
|
case 'exception':
|
|
extra_params.exception = '1';
|
|
egw.open(edit_id, event_data.app || 'calendar', 'edit', extra_params);
|
|
break;
|
|
case 'series':
|
|
case 'single':
|
|
egw.open(edit_id, event_data.app || 'calendar', 'edit', extra_params);
|
|
break;
|
|
case 'cancel':
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
}
|
|
if (parseInt(event_data.recur_type))
|
|
{
|
|
const buttons = [
|
|
{
|
|
label: egw.lang("Edit exception"),
|
|
id: "exception",
|
|
class: "ui-priority-primary",
|
|
default: true,
|
|
image: 'edit'
|
|
},
|
|
{label: egw.lang("Edit series"), id: "series", image: 'recur'},
|
|
{label: egw.lang("Cancel"), id: "cancel", image: 'cancel'}
|
|
];
|
|
Et2Dialog.show_dialog(
|
|
function(button_id)
|
|
{
|
|
callback.call(that, button_id, event_data);
|
|
},
|
|
(!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" +
|
|
egw.lang("Do you want to edit this event as an exception or the whole series?"),
|
|
"This event is part of a series", {}, buttons, Et2Dialog.QUESTION_MESSAGE,
|
|
"", egw
|
|
);
|
|
}
|
|
else
|
|
{
|
|
callback.call(this, 'single', event_data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Split series prompt
|
|
*
|
|
* If the event is recurring and the user adjusts the time or duration, we may need
|
|
* to split the series, ending the current one and creating a new one with the changes.
|
|
* This prompts the user if they really want to do that.
|
|
*
|
|
* There is no default callback, and nothing happens if you call this on a
|
|
* single (non-recurring) event
|
|
*
|
|
* @param {Object} event_data - Event information
|
|
* @param {string} event_data.id - Unique ID for the event, possibly with a timestamp
|
|
* @param {string|Date} instance_date - The date of the edited instance of the event
|
|
* @param {et2_calendar_event~prompt_callback} callback - Callback is
|
|
* called with the button (ok or cancel) and the event data.
|
|
* @augments {et2_calendar_event}
|
|
*/
|
|
public static series_split_prompt(event_data, instance_date, callback)
|
|
{
|
|
let egw;
|
|
// seems window.opener somehow in certian conditions could be from different origin
|
|
// we try to catch the exception and in this case retrieve the egw object from current window.
|
|
try
|
|
{
|
|
egw = this.egw ? (typeof this.egw == 'function' ? this.egw() : this.egw) : window.opener && typeof window.opener.egw != 'undefined' ? window.opener.egw('calendar') : window.egw('calendar');
|
|
}
|
|
catch(e)
|
|
{
|
|
egw = window.egw('calendar');
|
|
}
|
|
|
|
const that = this;
|
|
|
|
if(typeof instance_date == 'string')
|
|
{
|
|
instance_date = new Date(instance_date);
|
|
}
|
|
|
|
// Check for modifying a series that started before today
|
|
const tempDate = new Date();
|
|
const today = new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate(), tempDate.getHours(), -tempDate.getTimezoneOffset(), tempDate.getSeconds());
|
|
const termination_date = instance_date < today ? egw.lang('today') : formatDate(instance_date);
|
|
|
|
if(parseInt(event_data.recur_type))
|
|
{
|
|
Et2Dialog.show_dialog(
|
|
function(button_id)
|
|
{
|
|
callback.call(that, button_id, event_data);
|
|
},
|
|
(!event_data.is_private ? event_data['title'] : egw.lang('private')) + "\n" +
|
|
egw.lang("Do you really want to change the start of this series? If you do, the original series will be terminated as of %1 and a new series for the future reflecting your changes will be created.", termination_date),
|
|
"This event is part of a series", {}, Et2Dialog.BUTTONS_OK_CANCEL, Et2Dialog.WARNING_MESSAGE
|
|
);
|
|
}
|
|
}
|
|
|
|
public static drag_helper(event, ui)
|
|
{
|
|
ui.helper.width(ui.width());
|
|
}
|
|
|
|
/**
|
|
* splits the combined status, quantity and role
|
|
*
|
|
* @param {string} status - combined value, O: status letter: U, T, A, R
|
|
* @param {int} [quantity] - quantity
|
|
* @param {string} [role]
|
|
* @return string status U, T, A or R, same as $status parameter on return
|
|
*/
|
|
public static split_status(status, quantity?, role?)
|
|
{
|
|
quantity = 1;
|
|
role = 'REQ-PARTICIPANT';
|
|
//error_log(__METHOD__.__LINE__.array2string($status));
|
|
let matches = null;
|
|
if (typeof status === 'string' && status.length > 1)
|
|
{
|
|
matches = status.match(/^.([0-9]*)(.*)$/gi);
|
|
}
|
|
if (matches)
|
|
{
|
|
if (parseInt(matches[1]) > 0) quantity = parseInt(matches[1]);
|
|
if (matches[2]) role = matches[2];
|
|
status = status[0];
|
|
}
|
|
else if (status === true)
|
|
{
|
|
status = 'U';
|
|
}
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* The egw_action system requires an egwActionObjectInterface Interface implementation
|
|
* to tie actions to DOM nodes. I'm not sure if we need this.
|
|
*
|
|
* The class extension is different than the widgets
|
|
*
|
|
* @param {et2_DOMWidget} widget
|
|
* @param {Object} node
|
|
*
|
|
*/
|
|
public static et2_event_action_object_impl(widget, node)
|
|
{
|
|
const aoi = new et2_action_object_impl(widget, node).getAOI();
|
|
|
|
// _outerCall may be used to determine, whether the state change has been
|
|
// evoked from the outside and the stateChangeCallback has to be called
|
|
// or not.
|
|
aoi.doSetState = function (_state, _outerCall)
|
|
{
|
|
};
|
|
|
|
return aoi;
|
|
}
|
|
}
|
|
|
|
et2_register_widget(et2_calendar_event, ["calendar-event"]); |