2020-02-27 19:44:28 +01:00
/ *
* Egroupware Calendar event widget
2021-06-11 11:31:06 +02:00
*
* @license https : //opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package calendar
* @subpackage etemplate
* @link https : //www.egroupware.org
2020-02-27 19:44:28 +01:00
* @author Nathan Gray
* /
/ * e g w : u s e s
/ e t e m p l a t e / j s / e t 2 _ c o r e _ v a l u e W i d g e t ;
* /
2022-07-12 17:41:13 +02:00
import { et2_register_widget , et2_widget , WidgetConfig } from "../../api/js/etemplate/et2_core_widget" ;
2020-02-27 19:44:28 +01:00
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" ;
2021-06-10 15:40:49 +02:00
import { et2_IDetachedDOM } from "../../api/js/etemplate/et2_core_interfaces" ;
2021-08-12 16:57:15 +02:00
import { et2_activateLinks , et2_insertLinkText , et2_no_init } from "../../api/js/etemplate/et2_core_common" ;
2021-06-10 15:40:49 +02:00
import { egw_getAppObjectManager , egwActionObject } from '../../api/js/egw_action/egw_action.js' ;
import { egw } from "../../api/js/jsapi/egw_global" ;
import { et2_container } from "../../api/js/etemplate/et2_core_baseWidget" ;
2022-03-18 20:58:13 +01:00
import { Et2Dialog } from "../../api/js/etemplate/Et2Dialog/Et2Dialog" ;
2022-05-26 00:08:40 +02:00
import { formatDate , formatTime } from "../../api/js/etemplate/Et2Date/Et2Date" ;
2022-05-05 15:55:49 +02:00
import { ColorTranslator } from "colortranslator" ;
2022-07-12 17:41:13 +02:00
import { StaticOptions } from "../../api/js/etemplate/Et2Select/StaticOptions" ;
import { Et2Select } from "../../api/js/etemplate/Et2Select/Et2Select" ;
import { SelectOption } from "../../api/js/etemplate/Et2Select/FindSelectOptions" ;
2020-02-27 19:44:28 +01:00
/ * *
* 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
{
2021-08-12 16:57:15 +02:00
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 ;
2020-02-27 19:44:28 +01:00
event . set_statustext ( event . _tooltip ( ) ) ;
2021-08-12 16:57:15 +02:00
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' )
2022-05-24 22:53:11 +02:00
. css ( 'border' , 'none' )
. on ( 'mouseenter' , function ( )
{
if ( event . div )
2021-08-12 16:57:15 +02:00
{
2022-05-24 22:53:11 +02:00
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 ( ) ) ;
} ) ;
} ) ;
} ) ;
2021-08-12 16:57:15 +02:00
} , 105 ) ;
2020-02-27 19:44:28 +01:00
} ) ;
2021-08-12 16:57:15 +02:00
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 ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
this . setDOMNode ( this . div [ 0 ] ) ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
doLoadingFinished ( )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
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 ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
destroy ( )
{
super . destroy ( ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
if ( this . _actionObject )
{
this . _actionObject . remove ( ) ;
this . _actionObject = null ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
this . div . off ( ) ;
this . title . remove ( ) ;
this . title = null ;
this . body . remove ( ) ;
this . body = null ;
this . icons = null ;
this . div . remove ( ) ;
this . div = null ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
jQuery ( 'body.egw_tooltip' ) . remove ( ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// 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 ) ;
}
2020-04-20 20:00:42 +02:00
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
set_value ( _value )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
// 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 ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// 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 ) ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* Callback for changes in cached data
* /
_UID_callback ( event )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
// 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 ( ',' ) ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Make sure id is a string, check values
if ( value )
{
this . _values_check ( value ) ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// 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 ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Copy to avoid changes, which may cause nm problems
this . options . value = jQuery . extend ( { } , value ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
if ( this . getParent ( ) . options . date )
{
this . options . value . date = this . getParent ( ) . options . date ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Let parent position - could also be et2_calendar_planner_row
( < et2_calendar_daycol > this . getParent ( ) ) . position_event ( this ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Parent may remove this if the date isn't the same
if ( this . getParent ( ) )
{
this . _update ( ) ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* Draw the event
* /
_update ( )
2020-02-27 19:44:28 +01:00
{
2022-07-12 17:41:13 +02:00
// Update to reflect new information
const event = this . options . value ;
2020-02-27 19:44:28 +01:00
2022-07-12 17:41:13 +02:00
const id = event . row_id ? event.row_id : event.id + ( event . recur_type ? ':' + event . recur_date : '' ) ;
const formatted_start = event . start . toJSON ( ) ;
2020-02-27 19:44:28 +01:00
2022-07-12 17:41:13 +02:00
this . set_id ( 'event_' + id ) ;
if ( this . _actionObject )
{
this . _actionObject . id = 'calendar::' + id ;
}
2020-02-27 19:44:28 +01:00
2022-07-12 17:41:13 +02:00
this . _need_actions_linked = ! this . options . readonly ;
2021-08-12 16:57:15 +02:00
2022-07-12 17:41:13 +02:00
// Make sure category stuff is there by faking a call to cache
let so = new StaticOptions ( ) ;
so . cached_server_side ( < Et2Select > < unknown > {
nodeName : "ET2-SELECT-CAT_RO" ,
egw : ( ) = > this . egw ( )
} , "select-cat" , ",,,calendar" , true ) ;
2021-08-12 16:57:15 +02:00
2022-05-24 19:11:16 +02:00
2022-07-12 17:41:13 +02:00
// Need cleaning? (DnD helper removes content)
// @ts-ignore
if ( ! this . div . has ( this . title ) . length )
{
2021-08-12 16:57:15 +02:00
this . div
2022-07-12 17:41:13 +02:00
. 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 )
2021-08-12 16:57:15 +02:00
2022-07-12 17:41:13 +02:00
// Put everything we need for basic interaction here, so it's available immediately
. attr ( 'data-id' , event . id )
2021-08-12 16:57:15 +02:00
. 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 ] ) ;
}
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
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 ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
this . body . toggleClass ( 'calendar_calEventBodySmall' , event . whole_day_on_top || false ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Header
const title = ! event . is_private ? egw . htmlspecialchars ( event [ 'title' ] ) : egw . lang ( 'private' ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
this . title
. html ( '<span class="calendar_calTimespan">' + this . _get_timespan ( event ) + '<br /></span>' )
. append ( '<span class="calendar_calEventTitle">' + title + '</span>' ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Colors - don't make them transparent if there is no color
2022-05-05 15:55:49 +02:00
const bg_color = new ColorTranslator ( this . div . css ( 'background-color' ) ) ;
if ( bg_color . RGBA != 'rgb(0,0,0,0)' )
2021-08-12 16:57:15 +02:00
{
// Most statuses use colored borders
2022-05-05 15:55:49 +02:00
this . div . css ( 'border-color' , bg_color . RGBA ) ;
2021-08-12 16:57:15 +02:00
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
this . icons . appendTo ( this . title )
. html ( this . _icons ( ) . join ( '' ) ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Body
if ( event . whole_day_on_top )
{
this . body . html ( title ) ;
}
else
{
2022-05-02 23:23:03 +02:00
// @ts-ignore
const start_time = formatTime ( event . start ) . trim ( ) ;
2021-08-12 16:57:15 +02:00
2022-05-02 23:23:03 +02:00
this . body
. html ( '<span class="calendar_calEventTitle">' + title + '</span>' )
. append ( '<span class="calendar_calTimespan">' + start_time + '</span>' ) ;
if ( this . options . value . description . trim ( ) )
{
2021-08-12 16:57:15 +02:00
this . body
2022-05-02 23:23:03 +02:00
. append ( '<p>' + egw . htmlspecialchars ( this . options . value . description ) + '</p>' ) ;
}
2021-08-12 16:57:15 +02:00
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Clear tooltip for regeneration
this . set_statustext ( '' ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// 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 ( ) ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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 ( )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
if ( this . options . value . whole_day_on_top ) return ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Skip for planner view, it's always small
if ( this . getParent ( ) && this . getParent ( ) . instanceOf ( et2_calendar_planner_row ) ) return ;
2020-04-24 18:54:08 +02:00
2021-08-12 16:57:15 +02:00
// Pre-calculation reset
this . div . removeClass ( 'calendar_calEventSmall' ) ;
this . body . css ( 'height' , 'auto' ) ;
2020-04-24 18:54:08 +02:00
2021-08-12 16:57:15 +02:00
const line_height = parseFloat ( this . div . css ( 'line-height' ) ) ;
let visible_lines = Math . floor ( this . div . innerHeight ( ) / line_height ) ;
2020-02-27 19:44:28 +01:00
2022-09-21 00:54:20 +02:00
if ( ! this . title [ 0 ] . clientHeight )
2021-08-12 16:57:15 +02:00
{
2022-09-21 00:54:20 +02:00
// Handle sizing while hidden, such as when calendar is not the active tab
visible_lines = 1 ;
2021-08-12 16:57:15 +02:00
}
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 ) ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
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' , '' ) ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* Examines the participants & returns CSS classname for status
*
* @returns { String }
* /
_status_class ( )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
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 ( )
{
2022-04-29 22:37:15 +02:00
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 )
{
2022-07-12 17:41:13 +02:00
let so = new StaticOptions ( ) ;
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 || "" ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Activate links in description
let description_node = document . createElement ( "p" ) ;
description_node . className = "calendar_calEvent_description" ;
et2_insertLinkText (
2022-01-17 17:33:50 +01:00
et2_activateLinks ( this . options . value . description ) , description_node , '_blank'
2021-08-12 16:57:15 +02:00
) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Location + Videoconference
let location = '' ;
if ( this . options . value . location || this . options . value [ '##videoconference' ] )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
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-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>' ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
// 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 />" ) ;
}
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
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 )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
return '' ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
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 ( ', ' ) ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* Get actual icons from list
* /
_icons ( ) : string [ ]
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
const icons = [ ] ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
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 ( 'alarm' , 'calendar' ) + '" 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' ) + '"/>' ) ;
}
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// 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 . id ) ;
app . calendar . joinVideoConference ( this . dataset . videoconference , data . data || this . dataset ) ;
} ) ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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
2022-05-02 23:23:03 +02:00
timespan = formatTime ( event . start )
// @ts-ignore
. trim ( ) + ' - ' + formatTime ( event . end ) . trim ( ) ;
2021-08-12 16:57:15 +02:00
}
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
2022-05-02 23:23:03 +02:00
timespan = formatTime ( event . start ) . trim ( ) ;
2021-08-12 16:57:15 +02:00
// @ts-ignore
2022-05-02 23:23:03 +02:00
timespan += ' - ' + formatTime ( event . end ) ;
2021-08-12 16:57:15 +02:00
timespan += ': ' + duration ;
}
return timespan ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* Make sure event data has all proper values , and format them as expected
* @param { Object } event
* /
_values_check ( event )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
// Make sure ID is a string
if ( event . id )
{
event . id = '' + event . id ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Parent might be a daycol or a planner_row
let parent = < et2_calendar_daycol > this . getParent ( ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Use dates as objects
if ( typeof event . start !== 'object' )
{
2022-04-29 22:37:15 +02:00
event . start = parent . date_helper ( event . start ) ;
2021-08-12 16:57:15 +02:00
}
if ( typeof event . end !== 'object' )
{
2022-04-29 22:37:15 +02:00
event . end = parent . date_helper ( event . end )
2021-08-12 16:57:15 +02:00
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// 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' ) ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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 ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Also check participants against owner
const owner_match = et2_calendar_event . owner_check ( event , this . getParent ( ) ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// Simple, same day
if ( owner_match && this . options . value . date && event . date == this . options . value . date )
{
return true ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// 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 ;
}
2020-03-12 16:18:30 +01:00
2021-08-12 16:57:15 +02:00
// Delete all old actions
if ( this . _actionObject )
{
this . _actionObject . clear ( ) ;
this . _actionObject . unregisterActions ( ) ;
this . _actionObject = null ;
}
2020-03-12 16:18:30 +01:00
2021-08-12 16:57:15 +02:00
// 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 ) ;
}
2020-03-12 16:18:30 +01:00
2021-08-12 16:57:15 +02:00
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 ) ;
}
}
2020-03-12 16:18:30 +01:00
2021-08-12 16:57:15 +02:00
return false ;
2020-03-12 16:18:30 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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
2020-03-12 16:18:30 +01:00
{
2021-08-12 16:57:15 +02:00
if ( ! owner || ! event )
{
return false ;
}
2020-03-12 16:18:30 +01:00
2021-08-12 16:57:15 +02:00
// 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 ;
}
2020-03-12 16:18:30 +01:00
2021-08-12 16:57:15 +02:00
// 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 ;
}
2020-03-12 16:18:30 +01:00
2021-08-12 16:57:15 +02:00
// Get the relevant participant
let participant = event . participants [ owner ] ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// If filter says don't look in groups, skip it all
if ( ! participant && filter === 'no-enum-groups' )
{
return false ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
// 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' ) )
{
2022-07-05 18:18:12 +02:00
options = app . calendar . sidebox_et2 . getWidgetById ( 'owner' ) . select_options
2021-08-12 16:57:15 +02:00
}
if ( ( isNaN ( parseInt ( owner ) ) || parseInt ( owner ) < 0 ) && options && typeof options . find == "function" )
{
let resource = options . find ( function ( element )
{
2022-07-05 18:18:12 +02:00
return element . value == owner ;
2021-08-12 16:57:15 +02:00
} ) || { } ;
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 ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
attachToDOM ( )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
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 ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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 )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
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 ) ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
result = this . onclick . apply ( this , args ) ;
}
return result ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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 )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
et2_calendar_event . recur_prompt ( this . options . value , callback , extra_data ) ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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 )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
et2_calendar_event . series_split_prompt ( this . options . value , this . options . value . recur_date , callback ) ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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 ( )
2020-02-27 19:44:28 +01:00
{
2022-05-26 00:08:40 +02:00
// 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 )
)
2021-08-12 16:57:15 +02:00
{
2022-05-26 00:08:40 +02:00
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
2021-08-12 16:57:15 +02:00
}
2022-05-26 00:08:40 +02:00
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* Link the actions to the DOM nodes / widget bits .
*
* @param { object } actions { ID : { attributes . . } + } map of egw action information
* /
_link_actions ( actions )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
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 ) ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* Code for implementing et2_IDetachedDOM
*
* @param { array } _attrs array to add further attributes to
* /
getDetachedAttributes ( _attrs )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
getDetachedNodes ( )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
return [ this . getDOMNode ( ) ] ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
setDetachedAttributes ( _nodes , _values )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
// 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 ? )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
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' ) )
{
2022-07-05 18:18:12 +02:00
options = app . calendar . sidebox_et2 . getWidgetById ( 'owner' ) . select_options ;
2021-08-12 16:57:15 +02:00
}
else
{
options = parent . getArrayMgr ( "sel_options" ) . getRoot ( ) . getEntry ( 'owner' ) ;
}
if ( event . participants && typeof parent . options . owner != 'undefined' && parent . options . owner . length > 0 )
{
2022-01-18 21:00:25 +01:00
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" )
2021-08-12 16:57:15 +02:00
{
2022-01-18 21:00:25 +01:00
var resource = options . find ( function ( element )
{
2022-07-05 18:18:12 +02:00
return element . value == parent_owner [ i ] ;
2022-01-18 21:00:25 +01:00
} ) || { } ;
if ( resource && resource . resources )
{
parent_owner . splice ( i , 1 ) ;
i -- ;
parent_owner = parent_owner . concat ( resource . resources ) ;
}
2021-08-12 16:57:15 +02:00
}
2022-01-18 21:00:25 +01:00
}
let participants = jQuery . extend ( [ ] , Object . keys ( event . participants ) ) ;
2021-08-12 16:57:15 +02:00
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 )
{
2022-07-05 18:18:12 +02:00
return element . value === id ;
2021-08-12 16:57:15 +02:00
} ) ) && 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' ) ;
}
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
const that = this ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
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 ) )
{
2022-03-18 20:58:13 +01:00
const buttons = [
{
label : egw.lang ( "Edit exception" ) ,
id : "exception" ,
class : "ui-priority-primary" ,
2022-08-23 16:54:31 +02:00
default : true ,
image : 'edit'
2022-03-18 20:58:13 +01:00
} ,
2022-08-23 16:54:31 +02:00
{ label : egw.lang ( "Edit series" ) , id : "series" , image : 'recur' } ,
{ label : egw.lang ( "Cancel" ) , id : "cancel" , image : 'cancel' }
2022-03-18 20:58:13 +01:00
] ;
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
) ;
2021-08-12 16:57:15 +02:00
}
else
{
callback . call ( this , 'single' , event_data ) ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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
{
2022-05-26 00:08:40 +02:00
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' ) ;
2021-08-12 16:57:15 +02:00
}
2022-05-26 00:08:40 +02:00
catch ( e )
2021-08-12 16:57:15 +02:00
{
2022-05-26 00:08:40 +02:00
egw = window . egw ( 'calendar' ) ;
2021-08-12 16:57:15 +02:00
}
2020-02-27 19:44:28 +01:00
2022-05-26 00:08:40 +02:00
const that = this ;
2020-02-27 19:44:28 +01:00
2022-05-26 00:08:40 +02:00
if ( typeof instance_date == 'string' )
{
instance_date = new Date ( instance_date ) ;
}
2020-02-27 19:44:28 +01:00
2022-05-26 00:08:40 +02:00
// 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 ) ;
2021-08-12 16:57:15 +02:00
2022-05-26 00:08:40 +02:00
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
) ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
public static drag_helper ( event , ui )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
ui . helper . width ( ui . width ( ) ) ;
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
/ * *
* 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 ? )
2020-02-27 19:44:28 +01:00
{
2021-08-12 16:57:15 +02:00
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 )
{
} ;
2020-02-27 19:44:28 +01:00
2021-08-12 16:57:15 +02:00
return aoi ;
}
2020-02-27 19:44:28 +01:00
}
2021-08-12 16:57:15 +02:00
2020-02-27 19:44:28 +01:00
et2_register_widget ( et2_calendar_event , [ "calendar-event" ] ) ;