egroupware/calendar/js/et2_widget_daycol.ts
milan 5e3c67a5cf converted egw_action from javascript to typescript
classes are now uppercase and in their own files. lowercase classes are deprecated.
Interfaces are now actual interfaces that should be implemented instead of creating and returning an ai Object every time
2023-07-10 16:54:22 +02:00

1250 lines
35 KiB
TypeScript

/*
* Egroupware
*
* @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package calendar
* @subpackage etemplate
* @link https://www.egroupware.org
* @author Nathan Gray
*/
/*egw:uses
et2_core_valueWidget;
/calendar/js/et2_widget_event.js;
*/
import {et2_createWidget, et2_register_widget, WidgetConfig} from "../../api/js/etemplate/et2_core_widget";
import {et2_valueWidget} from "../../api/js/etemplate/et2_core_valueWidget";
import {et2_calendar_timegrid} from "./et2_widget_timegrid";
import {et2_calendar_event} from "./et2_widget_event";
import {ClassWithAttributes} from "../../api/js/etemplate/et2_core_inheritance";
import {et2_IDetachedDOM, et2_IResizeable} from "../../api/js/etemplate/et2_core_interfaces";
import {et2_no_init} from "../../api/js/etemplate/et2_core_common";
import {egw} from "../../api/js/jsapi/egw_global";
import {egwIsMobile, sprintf} from "../../api/js/egw_action/egw_action_common";
import {CalendarApp} from "./app";
import {et2_calendar_view} from "./et2_widget_view";
import flatpickr from "flatpickr";
import {formatDate} from "../../api/js/etemplate/Et2Date/Et2Date";
import {ColorTranslator} from "colortranslator";
/**
* Class which implements the "calendar-timegrid" XET-Tag for displaying a single days
*
* This widget is responsible mostly for positioning its events
*
*/
export class et2_calendar_daycol extends et2_valueWidget implements et2_IDetachedDOM, et2_IResizeable
{
static readonly _attributes: any = {
date: {
name: "Date",
type: "any",
description: "What date is this daycol for. YYYYMMDD or Date",
default: et2_no_init
},
owner: {
name: "Owner",
type: "any", // Integer, string, or array of either
default: et2_no_init,
description: "Account ID number of the calendar owner, if not the current user"
},
display_birthday_as_event: {
name: "Birthdays",
type: "boolean",
default: false,
description: "Display birthdays as events"
},
display_holiday_as_event: {
name: "Holidays",
type: "boolean",
default: false,
description: "Display holidays as events"
}
};
private div: JQuery;
private header: JQuery;
private title: JQuery;
private event_wrapper: JQuery;
private user_spacer: JQuery;
private all_day: JQuery;
private registeredUID: string = null;
// Init to defaults, just in case - they will be updated from parent
private display_settings: any = {
wd_start: 60*9,
wd_end: 60*17,
granularity: 30,
rowsToDisplay: 10,
rowHeight: 20,
// Percentage; not yet available
titleHeight: 2.0
};
private date: Date;
private class: string;
/**
* Constructor
*/
constructor(_parent, _attrs? : WidgetConfig, _child? : object)
{
// Call the inherited constructor
super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_calendar_daycol._attributes, _child || {}));
// Main container
this.div = jQuery(document.createElement("div"))
.addClass("calendar_calDayCol")
.css('width',this.options.width)
.css('left', this.options.left);
this.header = jQuery(document.createElement('div'))
.addClass("calendar_calDayColHeader")
.css('width',this.options.width)
.css('left', this.options.left);
this.title = jQuery(document.createElement('div'))
.addClass('et2_clickable et2_link')
.appendTo(this.header);
this.user_spacer = jQuery(document.createElement('div'))
.addClass("calendar_calDayColHeader_spacer")
.appendTo(this.header);
this.all_day = jQuery(document.createElement('div'))
.addClass("calendar_calDayColAllDay")
.css('max-height', (egw.preference('limit_all_day_lines', 'calendar') || 3) * 1.4 + 'em')
.appendTo(this.header);
this.event_wrapper = jQuery(document.createElement('div'))
.addClass("event_wrapper")
.appendTo(this.div);
this.click = this.click.bind(this);
this.setDOMNode(this.div[0]);
}
doLoadingFinished( )
{
let result = super.doLoadingFinished();
// Parent will have everything we need, just load it from there
if(this.getParent() && this.getParent().options.owner)
{
this.set_owner(this.getParent().options.owner);
}
if(this.title.text() === '' && this.options.date &&
this.getParent() && this.getParent().instanceOf(et2_calendar_timegrid))
{
// Forces an update
const date = this.options.date;
this.options.date = '';
this.set_date(date);
}
return result;
}
destroy( )
{
super.destroy();
this.div.off();
this.header.off().remove();
this.title.off();
this.div = null;
this.header = null;
this.title = null;
this.user_spacer = null;
egw.dataUnregisterUID(this.registeredUID,null,this);
}
getDOMNode(sender)
{
if(!sender || sender === this) return this.div[0];
if(sender.instanceOf && sender.instanceOf(et2_calendar_event))
{
if(this.display_settings.granularity === 0)
{
return this.event_wrapper[0];
}
if(sender.options.value.whole_day_on_top ||
sender.options.value.whole_day && sender.options.value.non_blocking === true)
{
return this.all_day[0];
}
return this.div[0];
}
}
/**
* Draw the individual divs for clicking to add an event
*/
_draw( )
{
// Remove any existing
jQuery('.calendar_calAddEvent',this.div).remove();
// Grab real values from parent
if(this.getParent() && this.getParent().instanceOf(et2_calendar_timegrid))
{
this.display_settings.wd_start = 60*this.getParent().options.day_start;
this.display_settings.wd_end = 60*this.getParent().options.day_end;
this.display_settings.granularity = this.getParent().options.granularity;
const header = this.getParent().dayHeader.children();
// Figure out insert index
let idx = 0;
const siblings = this.getParent().getDOMNode(this).childNodes;
while(idx < siblings.length && siblings[idx] != this.getDOMNode())
{
idx++;
}
// Stick header in the right place
if(idx == 0)
{
this.getParent().dayHeader.prepend(this.header);
}
else if(header.length)
{
header.eq(Math.min(header.length,idx)-1).after(this.header);
}
}
this.div.attr('data-date', this.options.date);
}
getDate() : Date
{
return this.date;
}
date_helper(value)
{
return (<et2_calendar_view>this.getParent()).date_helper(value);
}
/**
* Set the date
*
* @param {string|Date} _date New date
* @param {Object[]} events =false List of event data to be displayed, or false to
* automatically fetch data from content array
* @param {boolean} force_redraw =false Redraw even if the date is the same.
* Used for when new data is available.
*/
set_date(_date, events?, force_redraw?)
{
if(typeof events === 'undefined' || !events)
{
events = false;
}
if(typeof force_redraw === 'undefined' || !force_redraw)
{
force_redraw = false;
}
if(!this.getParent())
{
egw.debug('warn', 'Day col widget "' + this.id + '" is missing its parent.');
return false;
}
this.date = (<et2_calendar_view>this.getParent()).date_helper(_date);
// Keep internal option in Ymd format, it gets passed around in this format
const new_date = formatDate(this.date, {dateFormat: "Ymd"});
// Set label
if(!this.options.label)
{
// Add timezone offset back in, or formatDate will lose those hours
const formatDate = new Date(this.date.valueOf() + this.date.getTimezoneOffset() * 60 * 1000);
this.title.html('<span class="long_date">' + egw.lang(flatpickr.formatDate(formatDate, 'l')) +
'</span><span class="short_date">' + egw.lang(flatpickr.formatDate(formatDate, 'D')) + '</span>' +
flatpickr.formatDate(formatDate, 'd'));
}
this.title
.attr("data-date", new_date)
.toggleClass('et2_label', !!this.options.label);
this.header
.attr('data-date',new_date)
.attr('data-whole_day',true);
// Avoid redrawing if date is the same
if(new_date === this.options.date &&
this.display_settings.granularity === this.getParent().options.granularity &&
!force_redraw
)
{
return;
}
const cache_id = CalendarApp._daywise_cache_id(new_date, this.options.owner);
if(this.options.date && this.registeredUID &&
cache_id !== this.registeredUID)
{
egw.dataUnregisterUID(this.registeredUID,null,this);
// Remove existing events
while(this._children.length > 0)
{
const node = this._children[this._children.length - 1];
this.removeChild(node);
node.destroy();
}
}
this.options.date = new_date;
// Set holiday and today classes
this.day_class_holiday();
// Update all the little boxes
this._draw();
// Register for updates on events for this day
if(this.registeredUID !== cache_id)
{
this.registeredUID = cache_id;
egw.dataRegisterUID(this.registeredUID, this._data_callback,this,this.getInstanceManager().execId,this.id);
}
}
/**
* Set the owner of this day
*
* @param {number|number[]|string|string[]} _owner - Owner ID, which can
* be an account ID, a resource ID (as defined in calendar_bo, not
* necessarily an entry from the resource app), or a list containing a
* combination of both.
*/
set_owner( _owner)
{
this.title
.attr("data-owner", _owner);
this.header.attr('data-owner',_owner);
this.div.attr('data-owner',_owner);
// Simple comparison, both numbers
if(_owner === this.options.owner) return;
// More complicated comparison, one or the other is an array
if((typeof _owner == 'object' || typeof this.options.owner == 'object') &&
_owner.toString() == this.options.owner.toString())
{
return;
}
this.options.owner = typeof _owner !== 'object' ? [_owner] : _owner;
const cache_id = CalendarApp._daywise_cache_id(this.options.date, _owner);
if(this.options.date && this.registeredUID &&
cache_id !== this.registeredUID)
{
egw.dataUnregisterUID(this.registeredUID,null,this);
}
if(this.registeredUID !== cache_id)
{
this.registeredUID = cache_id;
egw.dataRegisterUID(this.registeredUID, this._data_callback,this,this.getInstanceManager().execId,this.id);
}
}
set_class( classnames)
{
this.header.removeClass(this.class);
super.set_class(classnames);
this.header.addClass(classnames);
}
/**
* Callback used when the daywise data changes
*
* Events should update themselves when their data changes, here we are
* dealing with a change in which events are displayed on this day.
*
* @param {String[]} event_ids
* @returns {undefined}
*/
_data_callback( event_ids)
{
const events = [];
const waitForGroups = [];
if(event_ids == null || typeof event_ids.length == 'undefined')
{
event_ids = [];
}
for(let i = 0; i < event_ids.length; i++)
{
let event : any = egw.dataGetUIDdata('calendar::' + event_ids[i]);
event = event && event.data || false;
if(!event)
{
continue;
}
let wait = (<CalendarApp>this.getInstanceManager().app_obj.calendar)._fetch_group_members(event);
if(wait === null)
{
continue;
}
waitForGroups.push(wait.then(() =>
{
if(event && event.date && et2_calendar_event.owner_check(event, this) && (
event.date === this.options.date ||
// Accept multi-day events
new Date(event.start) <= this.date //&& new Date(event.end) >= this.date
))
{
events.push(event);
}
else if(event)
{
// Got an ID that doesn't belong
event_ids.splice(i--, 1);
}
}));
}
Promise.all(waitForGroups).then(() =>
{
if(!this.div.is(":visible"))
{
// Not visible, defer the layout or it all winds up at the top
// Cancel any existing listener & bind
jQuery(this.getInstanceManager().DOMContainer.parentNode)
.off('show.' + CalendarApp._daywise_cache_id(this.options.date, this.options.owner))
.one('show.' + CalendarApp._daywise_cache_id(this.options.date, this.options.owner), function()
{
this._update_events(events)
}.bind(this));
return;
}
if(!this.getParent().disabled)
{
this._update_events(events);
}
});
}
set_label( label)
{
this.options.label = label;
this.title.text(label);
this.title.toggleClass('et2_clickable et2_link',label === '');
}
set_left( left)
{
if(this.div)
{
this.div.css('left',left);
}
}
set_width(width)
{
this.options.width = width;
if(this.div)
{
this.div.outerWidth(this.options.width);
this.header.outerWidth(this.options.width);
}
}
/**
* Applies class for today, and any holidays for current day
*/
async day_class_holiday( )
{
this.title
// Remove all special day classes
.removeClass('calendar_calToday calendar_calBirthday calendar_calHoliday')
// Except this one...
.addClass("et2_clickable et2_link");
this.title.attr('data-holiday','');
// Set today class - note +1 when dealing with today, as months in JS are 0-11
const today = new Date();
today.setUTCMinutes(today.getUTCMinutes() - today.getTimezoneOffset());
this.title.toggleClass("calendar_calToday", this.options.date === ''+today.getUTCFullYear()+
sprintf("%02d",today.getUTCMonth()+1)+
sprintf("%02d",today.getUTCDate())
);
// Holidays and birthdays
let fetched_holidays = await this.egw().holidays(this.options.date.substring(0, 4));
const holiday_list = [];
let holiday_pref = (egw.preference('birthdays_as_events', 'calendar') || []);
if(typeof holiday_pref === 'string')
{
holiday_pref = holiday_pref.split(',');
}
else
{
holiday_pref = jQuery.extend([], holiday_pref);
}
// Show holidays as events on mobile or by preference
const holidays_as_events = egwIsMobile() || egw.preference('birthdays_as_events', 'calendar') === true ||
holiday_pref.indexOf('holiday') >= 0;
const birthdays_as_events = egwIsMobile() || holiday_pref.indexOf('birthday') >= 0;
if(fetched_holidays && fetched_holidays[this.options.date])
{
fetched_holidays = fetched_holidays[this.options.date];
for(let i = 0; i < fetched_holidays.length; i++)
{
if(typeof fetched_holidays[i]['birthyear'] !== 'undefined')
{
// Show birthdays as events on mobile or by preference
if(birthdays_as_events && this.getWidgetById("event_" + escape(fetched_holidays[i].name)) == null)
{
// Create event
var event = et2_createWidget('calendar-event', {
id: 'event_' + fetched_holidays[i].name,
value: {
row_id: escape(fetched_holidays[i].name),
title: fetched_holidays[i].name,
whole_day: true,
whole_day_on_top: true,
start: (<et2_calendar_view>this.getParent()).date_helper(this.options.date),
end: this.options.date,
owner: this.options.owner,
participants: this.options.owner,
app: 'calendar',
class: 'calendar_calBirthday'
},
readonly: true,
class: 'calendar_calBirthday'
},this);
event.doLoadingFinished();
event._update();
}
if (!egwIsMobile())
{
//If the birthdays are already displayed as event, don't
//show them in the caption
this.title.addClass('calendar_calBirthday');
holiday_list.push(fetched_holidays[i]['name']);
}
}
else
{
// Show holidays as events on mobile
if(holidays_as_events && this.getWidgetById("event_" + escape(fetched_holidays[i].name)) == null)
{
// Create event
var event = et2_createWidget('calendar-event', {
id: 'event_' + fetched_holidays[i].name,
value: {
row_id: escape(fetched_holidays[i].name),
title: fetched_holidays[i].name,
whole_day: true,
whole_day_on_top: true,
start: (<et2_calendar_view>this.getParent()).date_helper(this.options.date),
end: this.options.date,
owner: this.options.owner,
participants: this.options.owner,
app: 'calendar',
class: 'calendar_calHoliday'
},
readonly: true,
class: 'calendar_calHoliday'
},this);
event.doLoadingFinished();
event._update();
}
else
{
this.title.addClass('calendar_calHoliday');
this.title.attr('data-holiday', fetched_holidays[i]['name']);
//If the birthdays are already displayed as event, don't
//show them in the caption
if (!this.options.display_holiday_as_event)
{
holiday_list.push(fetched_holidays[i]['name']);
}
}
}
}
}
this.title.attr('title', holiday_list.join(', '));
}
/**
* Load the event data for this day and create event widgets for each.
*
* If event information is not provided, it will be pulled from the content array.
*
* @param {Object[]} [_events] Array of event information, one per event.
*/
_update_events(_events)
{
let c;
const events = _events || this.getArrayMgr('content').getEntry(this.options.date) || [];
// Remove extra events
while(this._children.length > 0)
{
const node = this._children[this._children.length - 1];
this.removeChild(node);
node.destroy();
}
// Make sure children are in cronological order, or columns are backwards
events.sort(function(a,b) {
const start = new Date(a.start) - new Date(b.start);
const end = new Date(a.end) - new Date(b.end);
// Whole day events sorted by ID, normal events by start / end time
if(a.whole_day && b.whole_day)
{
return (a.app_id - b.app_id);
}
else if (a.whole_day || b.whole_day)
{
return a.whole_day ? -1 : 1;
}
return start ? start : end;
});
for(c = 0; c < events.length; c++)
{
// Create event
var event = et2_createWidget('calendar-event',{
id:'event_'+events[c].id,
value: events[c]
},this);
}
// Seperate loop so column sorting finds all children in the right place
let child_length = this._children.length;
for(c = 0; c < events.length && c < child_length; c++)
{
let event = this.getWidgetById('event_'+events[c].id);
if(!event) continue;
if(this.isInTree())
{
event.doLoadingFinished();
}
}
// Show holidays as events on mobile or by preference
if(egwIsMobile() || egw.preference('birthdays_as_events','calendar'))
{
this.day_class_holiday();
}
// Apply styles to hidden events
this._out_of_view();
}
/**
* Apply styles for out-of-view and partially hidden events
*
* There are 3 different states or modes of display:
*
* - 'Normal' - When showing events positioned by time, the indicator is just
* a bar colored by the last category color. On hover it shows either the
* title of a single event or "x event(s)" if more than one are hidden.
* Clicking adjusts the current view to show the earliest / latest hidden
* event
*
* - Fixed - When showing events positioned by time but in a fixed-height
* week (not auto-sized to fit screen) the indicator is the same as sized.
* On hover it shows the titles of the hidden events, clicking changes
* the view to the selected day.
*
* - GridList - When showing just a list, the indicator shows "x event(s)",
* and on hover shows the category color, title & time. Clicking changes
* the view to the selected day, and opens the event for editing.
*/
_out_of_view()
{
// Reset
this.header.children('.hiddenEventBefore').remove();
this.div.children('.hiddenEventAfter').remove();
this.event_wrapper.css('overflow','visible');
this.all_day.removeClass('overflown');
jQuery('.calendar_calEventBody', this.div).css({'padding-top': '','margin-top':''});
const timegrid = <et2_calendar_timegrid>this.getParent();
// elem is jquery div of event
function isHidden(elem) {
// Add an extra 5px top and bottom to include events just on the
// edge of visibility
const docViewTop = timegrid.scrolling.scrollTop() + 5,
docViewBottom = docViewTop + (
this.display_settings.granularity === 0 ?
this.event_wrapper.height() :
timegrid.scrolling.height() - 10
),
elemTop = elem.position().top,
elemBottom = elemTop + elem.outerHeight(true);
if((elemBottom <= docViewBottom) && (elemTop >= docViewTop))
{
// Entirely visible
return false;
}
const visible = {
hidden: elemTop > docViewTop ? 'bottom' : 'top',
completely: false
};
visible.completely = visible.hidden == 'top' ? elemBottom < docViewTop : elemTop > docViewBottom;
return visible;
}
// In gridlist view, we can quickly check if we need it at all
if(this.display_settings.granularity === 0 && this._children.length)
{
jQuery('div.calendar_calEvent',this.div).show(0);
if(Math.ceil(this.div.height() / this._children[0].div.height()) > this._children.length)
{
return;
}
}
// Check all day overflow
this.all_day.toggleClass('overflown',
this.all_day[0].scrollHeight - this.all_day.innerHeight() > 5
);
// Check each event
this.iterateOver(function(event) {
// Skip whole day events and events missing value
if(this.display_settings.granularity && (
(!event.options || !event.options.value || event.options.value.whole_day_on_top))
)
{
return;
}
// Reset
event.title.css({'top':'','background-color':''});
event.body.css({'padding-top':'','margin-top':''});
const hidden = isHidden.call(this, event.div);
const day = this;
if(!hidden)
{
return;
}
// Only top is hidden, move label
// Bottom hidden is fine
if(hidden.hidden === 'top' && !hidden.completely && !event.div.hasClass('calendar_calEventSmall'))
{
const title_height = event.title.outerHeight();
event.title.css({
'top': timegrid.scrolling.scrollTop() - event.div.position().top,
'background-color': 'transparent'
});
event.body.css({
'padding-top': timegrid.scrolling.scrollTop() - event.div.position().top + title_height,
'margin-top': -title_height
});
}
// Too many in gridlist view, show indicator
else if (this.display_settings.granularity === 0 && hidden)
{
if(jQuery('.hiddenEventAfter', this.div).length == 0)
{
this.event_wrapper.css('overflow', 'hidden');
}
this._hidden_indicator(event, false, function()
{
app.calendar.update_state({view: 'day', date: day.date});
});
}
// Completely out of view, show indicator
else if (hidden.completely)
{
this._hidden_indicator(event, hidden.hidden == 'top',false);
}
}, this, et2_calendar_event);
}
/**
* Show an indicator that there are hidden events
*
* The indicator works 3 different ways, depending on if the day can be
* scrolled, is fixed, or if in gridview.
*
* @see _out_of_view()
*
* @param {et2_calendar_event} event Event we're creating the indicator for
* @param {boolean} top Events hidden at the top (true) or bottom (false)
* @param {function} [onclick] Callback for when user clicks on the indicator
*/
_hidden_indicator(event, top, onclick)
{
let indicator = null;
const day = this;
const timegrid = this.getParent();
const fixed_height = timegrid.div.hasClass('calendar_calTimeGridFixed');
// Event is before the displayed times
if(top)
{
// Create if not already there
if(jQuery('.hiddenEventBefore',this.header).length === 0)
{
indicator = jQuery('<div class="hiddenEventBefore"></div>')
.appendTo(this.header)
.attr('data-hidden_count', 1);
if(!fixed_height)
{
indicator
.text(event.options.value.title)
.on('click', typeof onclick === 'function' ? onclick : function() {
jQuery('.calendar_calEvent',day.div).first()[0].scrollIntoView();
return false;
});
}
}
else
{
indicator = jQuery('.hiddenEventBefore',this.header);
indicator.attr('data-hidden_count', parseInt(indicator.attr('data-hidden_count')) + 1);
if (!fixed_height)
{
indicator.text(day.egw().lang('%1 event(s) %2',indicator.attr('data-hidden_count'),''));
}
}
}
// Event is after displayed times
else
{
indicator = jQuery('.hiddenEventAfter',this.div);
// Create if not already there
if(indicator.length === 0)
{
indicator = jQuery('<div class="hiddenEventAfter"></div>')
.attr('data-hidden_count', 0)
.appendTo(this.div);
if(!fixed_height)
{
indicator
.on('click', typeof onclick === 'function' ? onclick : function() {
jQuery('.calendar_calEvent',day.div).last()[0].scrollIntoView(false);
// Better re-run this to clean up
day._out_of_view();
return false;
});
}
else
{
indicator
.on('mouseover', function() {
indicator.css({
'height': (indicator.attr('data-hidden_count')*1.2) + 'em',
'margin-top': -(indicator.attr('data-hidden_count')*1.2) + 'em'
});
})
.on('mouseout', function() {
indicator.css({
'height': '',
'margin-top': ''
});
});
}
}
const count = parseInt(indicator.attr('data-hidden_count')) + 1;
indicator.attr('data-hidden_count', count);
if(this.display_settings.granularity === 0)
{
indicator.append(event.div);
indicator.attr('data-hidden_label', day.egw().lang('%1 event(s) %2', indicator.attr('data-hidden_count'), ''));
}
else if (!fixed_height)
{
indicator.text(day.egw().lang('%1 event(s) %2',indicator.attr('data-hidden_count'),''));
}
indicator.css('top',timegrid.scrolling.height() + timegrid.scrolling.scrollTop()-indicator.innerHeight());
}
// Show different stuff for fixed height
if(fixed_height)
{
indicator
.append("<div id='"+event.dom_id +
"' data-id='"+event.options.value.id+"'>"+
event.options.value.title+
"</div>"
);
}
// Match color to the event
if(indicator !== null)
{
// Avoid white, which is hard to see
// Use border-bottom-color, Firefox doesn't give a value with border-color
const color = (new ColorTranslator(event.div.css('background-color') || '#FFFFFF')).RGB !== 'rgb(255,255,255)' ?
event.div.css('background-color') : event.div.css('border-bottom-color');
if(color !== 'rgba(0, 0, 0, 0)')
{
indicator.css('border-color', color);
}
}
}
/**
* Sort a day's events into minimally overlapping columns
*
* @returns {Array[]} Events sorted into columns
*/
_spread_events()
{
if(!this.date) return [];
let day_start = this.date.valueOf() / 1000;
const dst_check = new Date(this.date);
dst_check.setUTCHours(12);
// if daylight saving is switched on or off, correct $day_start
// gives correct times after 2am, times between 0am and 2am are wrong
const daylight_diff = day_start + 12 * 60 * 60 - (dst_check.valueOf() / 1000);
if(daylight_diff)
{
day_start -= daylight_diff;
}
const eventCols = [], col_ends = [];
// Make sure children are in cronological order, or columns are backwards
this._children.sort(function(a,b) {
const start = new Date(a.options.value.start) - new Date(b.options.value.start);
const end = new Date(a.options.value.end) - new Date(b.options.value.end);
// Whole day events sorted by ID, normal events by start / end time
if(a.options.value.whole_day && b.options.value.whole_day)
{
// Longer duration comes first so we have nicer bars across the top
const duration =
(new Date(b.options.value.end) - new Date(b.options.value.start)) -
(new Date(a.options.value.end) - new Date(a.options.value.start));
return (Math.abs(duration) > 360000) ? duration : (a.options.value.title.localeCompare(b.options.value.title));
}
else if (a.options.value.whole_day || b.options.value.whole_day)
{
return a.options.value.whole_day ? -1 : 1;
}
return start ? start : end;
});
for(let i = 0; i < this._children.length; i++)
{
const event = this._children[i].options.value || false;
if(!event) continue;
if(event.date && event.date != this.options.date &&
// Multi-day events date may be different
(new Date(event.start) >= this.date || new Date(event.end) < this.date )
)
{
// Still have a child event that has changed date (DnD)
this._children[i].destroy();
this.removeChild(this._children[i]);
continue;
}
let c = 0;
event['multiday'] = false;
if(typeof event.start !== 'object')
{
event.start = new Date(event.start);
}
if(typeof event.end !== 'object')
{
event.end = new Date(event.end);
}
event['start_m'] = parseInt(String((event.start.valueOf() / 1000 - day_start) / 60), 10);
if (event['start_m'] < 0)
{
event['start_m'] = 0;
event['multiday'] = true;
}
event['end_m'] = parseInt(String((event.end.valueOf() / 1000 - day_start) / 60), 10);
if (event['end_m'] >= 24*60)
{
event['end_m'] = 24*60-1;
event['multiday'] = true;
}
if(!event.start.getUTCHours() && !event.start.getUTCMinutes() && event.end.getUTCHours() == 23 && event.end.getUTCMinutes() == 59)
{
event.whole_day_on_top = (event.non_blocking && event.non_blocking != '0');
}
if (!event['whole_day_on_top'])
{
for(c = 0; event['start_m'] < col_ends[c]; ++c);
col_ends[c] = event['end_m'];
}
if(typeof eventCols[c] === 'undefined')
{
eventCols[c] = [];
}
eventCols[c].push(this._children[i]);
}
return eventCols;
}
/**
* Position the event according to its time and how this widget is laid
* out.
*
* @param {et2_calendar_event} [event] - Event to be updated
* If a single event is not provided, all events are repositioned.
*/
position_event(event?)
{
// If hidden, skip it - it takes too long
if(!this.div.is(':visible')) return;
// Sort events into minimally-overlapping columns
const columns = this._spread_events();
for(let c = 0; c < columns.length; c++)
{
// Calculate horizontal positioning
let left = Math.ceil(5 + (1.5 * 100 / (parseFloat(this.options.width) || 100)));
let right = 2;
if (columns.length !== 1)
{
right = !c ? 30 : 2;
left += c * (100.0-left) / columns.length;
}
for(let i = 0; (columns[c].indexOf(event) >= 0 || !event) && i < columns[c].length; i++)
{
// Calculate vertical positioning
let top = 0;
let height = 0;
// Position the event
if(this.display_settings.granularity === 0)
{
if(this.all_day.has(columns[c][i].div).length)
{
columns[c][i].div.prependTo(this.event_wrapper);
}
else if(this.event_wrapper.has(columns[c][i].div).length == 0)
{
columns[c][i].div.appendTo(this.event_wrapper);
}
columns[c][i].div.css('top', '');
columns[c][i].div.css('height', '');
columns[c][i].div.css('left', '');
columns[c][i].div.css('right', '');
// Strip out of view padding
columns[c][i].body.css('padding-top','');
continue;
}
if(columns[c][i].options.value.whole_day_on_top)
{
if(!this.all_day.has(columns[c][i].div).length)
{
columns[c][i].div.css('top', '');
columns[c][i].div.css('height','');
columns[c][i].div.css('left', '');
columns[c][i].div.css('right', '');
columns[c][i].body.css('padding-top','');
columns[c][i].div
.appendTo(this.all_day);
this.getParent().resizeTimes();
}
continue;
}
else
{
if(this.all_day.has(columns[c][i].div).length)
{
columns[c][i].div.appendTo(this.event_wrapper);
this.getParent().resizeTimes();
}
top = this._time_to_position(columns[c][i].options.value.start_m);
height = this._time_to_position(columns[c][i].options.value.end_m)-top;
}
// Position the event
if(event && columns[c].indexOf(event) >= 0 || !event)
{
columns[c][i].div.css('top', top+'%');
columns[c][i].div.css('height', height+'%');
// Remove spacing from border, but only if visible or the height will be wrong
if(columns[c][i].div.is(':visible'))
{
const border_diff = columns[c][i].div.outerHeight() - columns[c][i].div.height();
columns[c][i].div.css('height','calc('+height+'% - ' +border_diff+')');
}
// This gives the wrong height
//columns[c][i].div.outerHeight(height+'%');
columns[c][i].div.css('left', left.toFixed(1)+'%');
columns[c][i].div.css('right', right.toFixed(1)+'%');
columns[c][i].div.css('z-index',parseInt(20)+c);
columns[c][i]._small_size();
}
}
// Only wanted to position this event, leave the other columns alone
if(event && columns[c].indexOf(event) >= 0)
{
return;
}
}
}
/**
* Calculates the vertical position based on the time
*
* This calculation is a percentage from 00:00 to 23:59
*
* @param {int} time in minutes from midnight
* @return {float} position in percent
*/
_time_to_position(time)
{
let pos = 0.0;
// 24h
pos = ((time / 60) / 24) * 100;
return pos.toFixed(1);
}
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");
}
// But we do want to listen to certain clicks, and handle them internally
this.node.addEventListener("click", this.click);
return result;
}
removeFromDOM()
{
this.node.removeEventListener("click", this.click)
}
/**
* Click handler calling custom handler set via onclick attribute to this.onclick,
* or the default which is to open a new event at that time.
*
* Normally, you don't bind to this one, but the attribute is supported if you
* can get a reference to the widget.
*
* @param {Event} _ev
* @returns {boolean}
*/
click(_ev)
{
if(this.getParent().options.readonly ) return;
// Drag to create in progress
if(this.getParent().drag_create.start !== null) return;
// Click on the title
if (jQuery(_ev.target).hasClass('calendar_calAddEvent'))
{
if(this.header.has(_ev.target).length == 0 && !_ev.target.dataset.whole_day)
{
// Default handler to open a new event at the selected time
var options = {
date: _ev.target.dataset.date || this.options.date,
hour: _ev.target.dataset.hour || this.getParent().options.day_start,
minute: _ev.target.dataset.minute || 0,
owner: this.options.owner
};
this.getInstanceManager().app_obj.calendar.add(options);
_ev.stopImmediatePropagation();
_ev.preventDefault();
return false;
}
// Header, all day non-blocking
else if (this.header.has(_ev.target).length && !jQuery('.hiddenEventBefore',this.header).has(_ev.target).length ||
this.header.is(_ev.target)
)
{
// Click on the header, but not the title. That's an all-day non-blocking
const end = this.date.getFullYear() + '-' + (this.date.getUTCMonth() + 1) + '-' + this.date.getUTCDate() + 'T23:59';
let options = {
start: this.date.toJSON(),
end: end,
non_blocking: true,
owner: this.options.owner
};
this.getInstanceManager().app_obj.calendar.add(options);
_ev.preventDefault();
_ev.stopImmediatePropagation();
return false;
}
}
// Day label
else if(this.title.is(_ev.target) || this.title.has(_ev.target).length)
{
this.getInstanceManager().app_obj.calendar.update_state({view: 'day', date: this.date.toJSON()});
_ev.preventDefault();
_ev.stopImmediatePropagation();
return false;
}
}
/**
* Code for implementing et2_IDetachedDOM
*
* @param {array} _attrs array to add further attributes to
*/
getDetachedAttributes( _attrs)
{
}
getDetachedNodes() {
return [this.getDOMNode(this)];
}
setDetachedAttributes( _nodes, _values)
{
}
// Resizable interface
/**
* Resize
*
* Parent takes care of setting proper width & height for the containing div
* here we just need to adjust the events to fit the new size.
*/
resize ()
{
if(this.disabled || !this.div.is(':visible') || this.getParent().disabled)
{
return;
}
if(this.display_settings.granularity !== this.getParent().options.granularity)
{
// Layout has changed
this._draw();
// Resize & position all events
this.position_event();
}
else
{
// Don't need to resize & reposition, just clear some stuff
// to reset for _out_of_view()
this.iterateOver(function(widget) {
widget._small_size();
}, this, et2_calendar_event);
}
this._out_of_view();
}
}
et2_register_widget(et2_calendar_daycol, ["calendar-daycol"]);