From 1d0772a064cf2ada8c80f2825141dcd6c605d210 Mon Sep 17 00:00:00 2001
From: nathan
Date: Thu, 12 Aug 2021 08:57:15 -0600
Subject: [PATCH] * Calendar: Activate links in location & description in event
tooltip
---
calendar/js/et2_widget_event.ts | 2898 ++++++++++++++++---------------
1 file changed, 1473 insertions(+), 1425 deletions(-)
diff --git a/calendar/js/et2_widget_event.ts b/calendar/js/et2_widget_event.ts
index 8942259736..ad4e87763f 100644
--- a/calendar/js/et2_widget_event.ts
+++ b/calendar/js/et2_widget_event.ts
@@ -20,7 +20,7 @@ import {et2_action_object_impl, et2_DOMWidget} from "../../api/js/etemplate/et2_
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_no_init} from "../../api/js/etemplate/et2_core_common";
+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.js';
import {egw} from "../../api/js/jsapi/egw_global";
import {et2_selectbox} from "../../api/js/etemplate/et2_widget_selectbox";
@@ -55,1433 +55,1481 @@ import {et2_dialog} from "../../api/js/etemplate/et2_widget_dialog";
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() {
- 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 = 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))
- {
- (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
- (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
- // Fake it to use the cache / call - if already there, these will return
- // immediately.
- const im = this.getInstanceManager();
- et2_selectbox.cat_options({
- _type:'select-cat',
- getInstanceManager: function() {return im;}
- },
- {application:event.app||'calendar'}
- );
-
- // Need cleaning? (DnD helper removes content)
- // @ts-ignore
- if(!this.div.has(this.title).length)
- {
- this.div
- .empty()
- .append(this.title)
- .append(this.body);
- }
- if(!this.getParent().options.readonly && !this.options.readonly && this.div.droppable('instance'))
- {
- this.div
- // Let timegrid always get the drag
- .droppable('option','greedy',false);
- }
- 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(''+this._get_timespan(event) + '
')
- .append(''+title+'');
-
- // Colors - don't make them transparent if there is no color
- // @ts-ignore
- if(jQuery.Color("rgba(0,0,0,0)").toRgbaString() != jQuery.Color(this.div,'background-color').toRgbaString())
- {
- // Most statuses use colored borders
- this.div.css('border-color',this.div.css('background-color') );
- }
-
- 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 = jQuery.datepicker.formatTime(
- egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
- {
- hour: event.start_m / 60,
- minute: event.start_m % 60,
- seconds: 0,
- timezone: 0
+ static readonly _attributes: any = {
+ "value": {
+ type: "any",
+ default: et2_no_init
},
- {"ampm": (egw.preference("timeformat") === "12")}
- ).trim();
-
- this.body
- .html(''+title+'')
- .append(''+start_time + '');
- if(this.options.value.description.trim())
- {
- this.body
- .append(''+egw.htmlspecialchars(this.options.value.description)+'
');
- }
- }
-
- // Clear tooltip for regeneration
- this.set_statustext('');
-
- // Height specific section
- // This can take an unreasonable amount of time if parent is hidden
- if(jQuery((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.height())
- {
- // Handle sizing while hidden, such as when calendar is not the active tab
- visible_lines = Math.floor(egw.getHiddenDimensions(this.div).h / egw.getHiddenDimensions(this.title).h);
- }
- 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 ? (this.getParent()) : (this.getParent());
-
- parent.date_helper.set_value(this.options.value.start.valueOf ? new Date(this.options.value.start) : this.options.value.start);
- const start = parent.date_helper.input_date.val();
- parent.date_helper.set_value(this.options.value.end.valueOf ? new Date(this.options.value.end) : this.options.value.end);
- const end = parent.date_helper.input_date.val();
-
- const times = !this.options.value.multiday ?
- '' + this.egw().lang('Time') + ':' + timespan :
- '' + this.egw().lang('Start') + ':' + start + ' ' +
- '' + this.egw().lang('End') + ':' + end;
- let cat_label: (string | string[]) = '';
- if(this.options.value.category)
- {
- const cat = et2_createWidget('select-cat', {'readonly': true}, this);
- cat.set_value(this.options.value.category);
- cat_label = this.options.value.category.indexOf(',') <= 0 ? cat.span.text() : [];
- if(typeof cat_label != 'string')
- {
- cat.span.children().each(function() {
- (cat_label).push(jQuery(this).text());
- });
- cat_label = cat_label.join(', ');
- }
- cat.destroy();
- }
-
- // Location + Videoconference
- let location = '';
- if(this.options.value.location || this.options.value['##videoconference'])
- {
- location += '' + this.egw().lang('Location') + ':' +
- egw.htmlspecialchars(this.options.value.location);
- if(this.options.value['##videoconference'])
- {
- // Click handler is set in _bind_videoconference()
- location += (this.options.value.location.trim() ? '
' : '') +
- ''+
- this.egw().lang('Video conference') +
- '';
- this._bind_videoconference();
- }
- location += '
';
- }
-
- // Participants
- let participants = '';
- if(this.options.value.participant_types[''])
- {
- participants += this.options.value.participant_types[''].join("
");
- }
- for(let type_name in this.options.value.participant_types)
- {
- if(type_name)
- {
- participants += '
'+type_name+':
';
- participants += this.options.value.participant_types[type_name].join("
");
- }
- }
-
- return '
';
- }
-
- /**
- * 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('');
- }
- 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('');
- }
- if(this.options.value.priority == 3)
- {
- icons.push('');
- }
- if(this.options.value.public == '0')
- {
- // Show private flag
- icons.push('');
- }
- if(this.options.value['recur_type'])
- {
- icons.push('');
- }
- // icons for single user, multiple users or group(s) and resources
- const single = '';
- const multiple = '';
- 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;
+ "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."
}
- // @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('');
- }
- if(this.options.value.participants[egw.user('account_id')] && this.options.value.participants[egw.user('account_id')][0] == 'U')
- {
- icons.push('');
- }
- if(this.options.value["##videoconference"])
- {
- icons.push('');
- }
- }
-
- // Always include non-blocking, regardless of privacy
- if(this.options.value.non_blocking)
- {
- icons.push('');
- }
- 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.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 = jQuery.datepicker.formatTime(
- egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
- {
- hour: event.start_m / 60,
- minute: event.start_m % 60,
- seconds: 0,
- timezone: 0
- },
- {"ampm": (egw.preference("timeformat") === "12")}
- // @ts-ignore
- ).trim()+' - '+jQuery.datepicker.formatTime(
- egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
- {
- hour: event.end_m / 60,
- minute: event.end_m % 60,
- seconds: 0,
- timezone: 0
- },
- {"ampm": (egw.preference("timeformat") === "12")}
- ).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 = jQuery.datepicker.formatTime(
- egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
- {
- hour: event.start_m / 60,
- minute: event.start_m % 60,
- seconds: 0,
- timezone: 0
- },
- {"ampm": (egw.preference("timeformat") === "12")}
- ).trim();
-
- // @ts-ignore
- timespan += ' - ' + jQuery.datepicker.formatTime(
- egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
- {
- hour: event.end_m / 60,
- minute: event.end_m % 60,
- seconds: 0,
- timezone: 0
- },
- {"ampm": (egw.preference("timeformat") === "12")}
- ).trim();
-
- 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 = this.getParent();
-
- // Use dates as objects
- if(typeof event.start !== 'object')
- {
- parent.date_helper.set_value(event.start);
- event.start = new Date(parent.date_helper.getValue());
- }
- if(typeof event.end !== 'object')
- {
- parent.date_helper.set_value(event.end);
- event.end = new Date(parent.date_helper.getValue());
- }
-
- // 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').taglist.getSelection();
- }
- if((isNaN(parseInt(owner)) || parseInt(owner) < 0) && options && typeof options.find == "function")
- {
- let resource = options.find(function (element)
- {
- return element.id == 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().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').taglist.getSelection();
- }
- 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;
- 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.id == 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.id === 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 = [
- {text: egw.lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true},
- {text: egw.lang("Edit series"), id: "series"},
- {text: egw.lang("Cancel"), id: "cancel"}
- ];
- et2_dialog.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?"),
- egw.lang("This event is part of a series"), {}, buttons, et2_dialog.QUESTION_MESSAGE
- );
- }
- 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') : date(egw.preference('dateformat'), instance_date);
-
- if(parseInt(event_data.recur_type))
- {
- et2_dialog.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),
- egw.lang("This event is part of a series"), {}, et2_dialog.BUTTONS_OK_CANCEL , et2_dialog.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) {
};
+ private div: JQuery;
+ private title: JQuery;
+ private body: JQuery;
+ private icons: JQuery;
+ private _need_actions_linked: boolean = false;
+ private _actionObject: egwActionObject;
- return aoi;
- }
+ /**
+ * 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 ()
+ {
+ 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 = 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))
+ {
+ (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
+ (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
+ // Fake it to use the cache / call - if already there, these will return
+ // immediately.
+ const im = this.getInstanceManager();
+ et2_selectbox.cat_options({
+ _type: 'select-cat',
+ getInstanceManager: function ()
+ {
+ return im;
+ }
+ },
+ {application: event.app || 'calendar'}
+ );
+
+ // Need cleaning? (DnD helper removes content)
+ // @ts-ignore
+ if (!this.div.has(this.title).length)
+ {
+ this.div
+ .empty()
+ .append(this.title)
+ .append(this.body);
+ }
+ if (!this.getParent().options.readonly && !this.options.readonly && this.div.droppable('instance'))
+ {
+ this.div
+ // Let timegrid always get the drag
+ .droppable('option', 'greedy', false);
+ }
+ 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('' + this._get_timespan(event) + '
')
+ .append('' + title + '');
+
+ // Colors - don't make them transparent if there is no color
+ // @ts-ignore
+ if (jQuery.Color("rgba(0,0,0,0)").toRgbaString() != jQuery.Color(this.div, 'background-color').toRgbaString())
+ {
+ // Most statuses use colored borders
+ this.div.css('border-color', this.div.css('background-color'));
+ }
+
+ 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 = jQuery.datepicker.formatTime(
+ egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
+ {
+ hour: event.start_m / 60,
+ minute: event.start_m % 60,
+ seconds: 0,
+ timezone: 0
+ },
+ {"ampm": (egw.preference("timeformat") === "12")}
+ ).trim();
+
+ this.body
+ .html('' + title + '')
+ .append('' + start_time + '');
+ if (this.options.value.description.trim())
+ {
+ this.body
+ .append('' + egw.htmlspecialchars(this.options.value.description) + '
');
+ }
+ }
+
+ // Clear tooltip for regeneration
+ this.set_statustext('');
+
+ // Height specific section
+ // This can take an unreasonable amount of time if parent is hidden
+ if (jQuery((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.height())
+ {
+ // Handle sizing while hidden, such as when calendar is not the active tab
+ visible_lines = Math.floor(egw.getHiddenDimensions(this.div).h / egw.getHiddenDimensions(this.title).h);
+ }
+ 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 ? (this.getParent()) : (this.getParent());
+
+ parent.date_helper.set_value(this.options.value.start.valueOf ? new Date(this.options.value.start) : this.options.value.start);
+ const start = parent.date_helper.input_date.val();
+ parent.date_helper.set_value(this.options.value.end.valueOf ? new Date(this.options.value.end) : this.options.value.end);
+ const end = parent.date_helper.input_date.val();
+
+ const times = !this.options.value.multiday ?
+ '' + this.egw().lang('Time') + ':' + timespan :
+ '' + this.egw().lang('Start') + ':' + start + ' ' +
+ '' + this.egw().lang('End') + ':' + end;
+ let cat_label: (string | string[]) = '';
+ if (this.options.value.category)
+ {
+ const cat = et2_createWidget('select-cat', {'readonly': true}, this);
+ cat.set_value(this.options.value.category);
+ cat_label = this.options.value.category.indexOf(',') <= 0 ? cat.span.text() : [];
+ if (typeof cat_label != 'string')
+ {
+ cat.span.children().each(function ()
+ {
+ (cat_label).push(jQuery(this).text());
+ });
+ cat_label = cat_label.join(', ');
+ }
+ cat.destroy();
+ }
+
+ // Activate links in description
+ let description_node = document.createElement("p");
+ description_node.className = "calendar_calEvent_description";
+ et2_insertLinkText(
+ et2_activateLinks(egw.htmlspecialchars(this.options.value.description)), description_node, '_blank'
+ );
+
+ // Location + Videoconference
+ let location = '';
+ if (this.options.value.location || this.options.value['##videoconference'])
+ {
+ location = '';
+ 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() ? '
' : '') +
+ '' +
+ this.egw().lang('Video conference') +
+ '';
+ this._bind_videoconference();
+ }
+ location += '
';
+ }
+
+ // Participants
+ let participants = '';
+ if (this.options.value.participant_types[''])
+ {
+ participants += this.options.value.participant_types[''].join("
");
+ }
+ for (let type_name in this.options.value.participant_types)
+ {
+ if (type_name)
+ {
+ participants += '' + type_name + ':
';
+ participants += this.options.value.participant_types[type_name].join("
");
+ }
+ }
+
+ return '
';
+ }
+
+ /**
+ * 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('');
+ }
+ 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('');
+ }
+ if (this.options.value.priority == 3)
+ {
+ icons.push('');
+ }
+ if (this.options.value.public == '0')
+ {
+ // Show private flag
+ icons.push('');
+ }
+ if (this.options.value['recur_type'])
+ {
+ icons.push('');
+ }
+ // icons for single user, multiple users or group(s) and resources
+ const single = '';
+ const multiple = '';
+ 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('');
+ }
+ if (this.options.value.participants[egw.user('account_id')] && this.options.value.participants[egw.user('account_id')][0] == 'U')
+ {
+ icons.push('');
+ }
+ if (this.options.value["##videoconference"])
+ {
+ icons.push('');
+ }
+ }
+
+ // Always include non-blocking, regardless of privacy
+ if (this.options.value.non_blocking)
+ {
+ icons.push('');
+ }
+ 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.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 = jQuery.datepicker.formatTime(
+ egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
+ {
+ hour: event.start_m / 60,
+ minute: event.start_m % 60,
+ seconds: 0,
+ timezone: 0
+ },
+ {"ampm": (egw.preference("timeformat") === "12")}
+ // @ts-ignore
+ ).trim() + ' - ' + jQuery.datepicker.formatTime(
+ egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
+ {
+ hour: event.end_m / 60,
+ minute: event.end_m % 60,
+ seconds: 0,
+ timezone: 0
+ },
+ {"ampm": (egw.preference("timeformat") === "12")}
+ ).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 = jQuery.datepicker.formatTime(
+ egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
+ {
+ hour: event.start_m / 60,
+ minute: event.start_m % 60,
+ seconds: 0,
+ timezone: 0
+ },
+ {"ampm": (egw.preference("timeformat") === "12")}
+ ).trim();
+
+ // @ts-ignore
+ timespan += ' - ' + jQuery.datepicker.formatTime(
+ egw.preference("timeformat") === "12" ? "h:mmtt" : "HH:mm",
+ {
+ hour: event.end_m / 60,
+ minute: event.end_m % 60,
+ seconds: 0,
+ timezone: 0
+ },
+ {"ampm": (egw.preference("timeformat") === "12")}
+ ).trim();
+
+ 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 = this.getParent();
+
+ // Use dates as objects
+ if (typeof event.start !== 'object')
+ {
+ parent.date_helper.set_value(event.start);
+ event.start = new Date(parent.date_helper.getValue());
+ }
+ if (typeof event.end !== 'object')
+ {
+ parent.date_helper.set_value(event.end);
+ event.end = new Date(parent.date_helper.getValue());
+ }
+
+ // 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').taglist.getSelection();
+ }
+ if ((isNaN(parseInt(owner)) || parseInt(owner) < 0) && options && typeof options.find == "function")
+ {
+ let resource = options.find(function (element)
+ {
+ return element.id == 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().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').taglist.getSelection();
+ }
+ 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;
+ 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.id == 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.id === 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 = [
+ {text: egw.lang("Edit exception"), id: "exception", class: "ui-priority-primary", "default": true},
+ {text: egw.lang("Edit series"), id: "series"},
+ {text: egw.lang("Cancel"), id: "cancel"}
+ ];
+ et2_dialog.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?"),
+ egw.lang("This event is part of a series"), {}, buttons, et2_dialog.QUESTION_MESSAGE
+ );
+ }
+ 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') : date(egw.preference('dateformat'), instance_date);
+
+ if (parseInt(event_data.recur_type))
+ {
+ et2_dialog.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),
+ egw.lang("This event is part of a series"), {}, et2_dialog.BUTTONS_OK_CANCEL, et2_dialog.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"]);
\ No newline at end of file