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});
});
// Avoid partially visible events
// We need to hide all, or the next row will be visible
event.div.hide(0);
}
// 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('')
.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('')
.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.clone());
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(""+
event.options.value.title+
"
"
);
}
// 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 = jQuery.Color(event.div.css('background-color')).toString() !== jQuery.Color('white').toString() ?
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 duration ? duration : (a.options.value.app_id - b.options.value.app_id);
}
else if (a.options.value.whole_day || b.options.value.whole_day)
{
return a.options.value.whole_day ? -1 : 1;
}
return start ? start : end;
});
for(let 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'] = (event.start.valueOf()/1000 - day_start) / 60;
if (event['start_m'] < 0)
{
event['start_m'] = 0;
event['multiday'] = true;
}
event['end_m'] = (event.end.valueOf()/1000 - day_start) / 60;
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);
}
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
jQuery(this.node).on('click.et2_daycol',
'.calendar_calDayColHeader,.calendar_calAddEvent',
jQuery.proxy(this.click, this)
);
return result;
}
/**
* 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
};
app.calendar.add(options);
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
};
app.calendar.add(options);
return false;
}
}
// Day label
else if(this.title.is(_ev.target) || this.title.has(_ev.target).length)
{
app.calendar.update_state({view: 'day',date: this.date.toJSON()});
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"]);