blob: bfb34483489710388021ae80cd20221b46ceff9d [file] [log] [blame]
/*
* Copyright (c) 2010-2019 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
import {dates, Popup, Range, scout, strings, Widget} from '../index';
import * as $ from 'jquery';
export default class CalendarComponent extends Widget {
constructor() {
super();
/**
* Selected is a GUI only property (the model doesn't have it)
*/
this.selected = false;
this.fullDay = false;
this.item = null;
this._$parts = [];
}
/**
* If day of a month is smaller than 100px, the components get the class compact
*/
static MONTH_COMPACT_THRESHOLD = 100;
_init(model) {
super._init(model);
this._syncCoveredDaysRange(this.coveredDaysRange);
}
_syncCoveredDaysRange(coveredDaysRange) {
if (coveredDaysRange) {
this.coveredDaysRange = new Range(
dates.parseJsonDate(coveredDaysRange.from),
dates.parseJsonDate(coveredDaysRange.to));
}
}
_remove() {
// remove $parts because they're not children of this.$container
this._$parts.forEach(function($part) {
$part.remove();
});
this._$parts = [];
super._remove();
}
_startLoopDay() {
// start date is either beginning of the component or beginning of viewRange
if (dates.compare(this.coveredDaysRange.from, this.parent.viewRange.from) > 0) {
return this.coveredDaysRange.from;
}
return this.parent.viewRange.from;
}
_render() {
var partDay, $day, $part;
if (!this.coveredDaysRange) {
// coveredDaysRange is not set on current CalendarComponent. Cannot show calendar component without from and to values.
return;
}
var loopDay = this._startLoopDay();
var appointmentToDate = dates.parseJsonDate(this.toDate);
var appointmentFromDate = dates.parseJsonDate(this.fromDate);
var coveredDaysRangeTo = this.coveredDaysRange.to;
if (!this.fullDay) {
var truncToDate = dates.trunc(appointmentToDate);
if (!dates.isSameDay(appointmentFromDate, appointmentToDate) && dates.compare(appointmentToDate, truncToDate) === 0) {
appointmentToDate = dates.shiftTime(appointmentToDate, 0, 0, 0, -1);
coveredDaysRangeTo = dates.shift(coveredDaysRangeTo, 0, 0, -1);
}
}
appointmentToDate = dates.toJsonDate(appointmentToDate);
var lastComponentDay = dates.shift(coveredDaysRangeTo, 0, 0, 1);
if (dates.compare(loopDay, lastComponentDay) > 0) {
// start day for the while loop is greater then the exit condition
return;
}
while (!dates.isSameDay(loopDay, lastComponentDay)) {
partDay = loopDay;
loopDay = dates.shift(loopDay, 0, 0, 1); // increase day for loop
// check if day is in visible view range
if (dates.compare(partDay, this.parent.viewRange.to) > 0) {
// break condition, partDay is now out of range.
break;
}
if (this.fullDay && !this.parent._isMonth()) {
$day = this._findDayInGrid(partDay, this.parent.$topGrid);
} else {
$day = this._findDayInGrid(partDay, this.parent.$grid);
}
if (!$day) {
// next day, partDay not found in grid
continue;
}
$part = $day.appendDiv('calendar-component');
$part
.addClass(this.item.cssClass)
.data('component', this)
.data('partDay', partDay)
.mousedown(this._onMouseDown.bind(this))
.on('contextmenu', this._onContextMenu.bind(this));
$part.appendDiv('calendar-component-leftcolorborder');
$part.appendDiv('content', this.item.subject);
this._$parts.push($part);
if (this.parent._isMonth()) {
var width = $day.data('new-width') || $day.width(); // prefer width from layoutSize
$part.addClass('component-month')
.toggleClass('compact', width < CalendarComponent.MONTH_COMPACT_THRESHOLD);
} else {
if (this.fullDay) {
// Full day tasks are rendered in the topGrid
var alreadyExistingTasks = $('.component-task', $day).length;
// Offset of initial task: 30px for the day-of-month number
// Offset of following tasks: 26px * preceding number of tasks. 26px: Task 23px high, 1px border. Spaced by 2px
this._arrangeTask(30 + 26 * alreadyExistingTasks);
$part.addClass('component-task');
} else {
var
fromDate = dates.parseJsonDate(this.fromDate),
toDate = dates.parseJsonDate(appointmentToDate),
partFrom = this._getHours(this.fromDate),
partTo = this._getHours(appointmentToDate);
// position and height depending on start and end date
$part.addClass('component-day');
if (dates.isSameDay(dates.trunc(this.coveredDaysRange.from), dates.trunc(coveredDaysRangeTo))) {
this._partPosition($part, partFrom, partTo);
} else if (dates.isSameDay(partDay, fromDate)) {
this._partPosition($part, partFrom, 25) // 25: indicate that it takes longer than that day
.addClass('component-open-bottom');
} else if (dates.isSameDay(partDay, toDate)) {
// Start at zero: No need to indicate that it starts earlier since indicator needs no extra space
this._partPosition($part, 0, partTo)
.addClass('component-open-top');
} else {
this._partPosition($part, 0, 25) // 25: indicate that it takes longer than that day
.addClass('component-open-top')
.addClass('component-open-bottom');
}
}
}
}
}
_getHours(date) {
var d = dates.parseJsonDate(date);
return d.getHours() + d.getMinutes() / 60;
}
_findDayInGrid(date, $grid) {
return $grid.find('.calendar-day').filter(
function(i, elem) {
return dates.isSameDay($(this).data('date'), date);
}).eq(0);
}
_isTask() {
return !this.parent._isMonth() && this.fullDay;
}
_arrangeTask(taskOffset) {
this._$parts.forEach(function($part) {
$part.css('top', taskOffset + 'px');
});
}
_isDayPart() {
return !this.parent._isMonth() && !this.fullDay;
}
_getHourRange(day) {
var hourRange = new Range(this._getHours(this.fromDate), this._getHours(this.toDate));
var dateRange = new Range(dates.parseJsonDate(this.fromDate), dates.parseJsonDate(this.toDate));
if (dates.isSameDay(day, dateRange.from) && dates.isSameDay(day, dateRange.to)) {
return new Range(hourRange.from, hourRange.to);
} else if (dates.isSameDay(day, dateRange.from)) {
return new Range(hourRange.from, 24);
} else if (dates.isSameDay(day, dateRange.to)) {
return new Range(0, hourRange.to);
}
return new Range(0, 24);
}
getPartDayPosition(day) {
return this._getDisplayDayPosition(this._getHourRange(day));
}
_getDisplayDayPosition(range) {
// Doesn't support minutes yet...
var preferredRange = new Range(this.parent._dayPosition(range.from, 0), this.parent._dayPosition(range.to, 0));
// Fixed number of divisions...
var minRangeSize = Math.round(100 * 100 / 24 / this.parent.numberOfHourDivisions) / 100; // Round to two digits
if (preferredRange.size() < minRangeSize) {
return new Range(preferredRange.from, preferredRange.from + minRangeSize);
}
return preferredRange;
}
_partPosition($part, y1, y2) {
// Compensate open bottom (height: square of 16px, rotated 45°, approx. 23px = sqrt(16^2 + 16^2)
var compensateBottom = y2 === 25 ? 23 : 0;
y2 = Math.min(24, y2);
var range = new Range(y1, y2);
var r = this._getDisplayDayPosition(range);
// Convert to %, rounded to two decimal places
compensateBottom = Math.round(100 * (100 / 1920 * compensateBottom)) / 100;
return $part
.css('top', r.from + '%')
.css('height', r.to - r.from - compensateBottom + '%');
}
_renderProperties() {
super._renderProperties();
this._renderSelected();
}
_renderSelected() {
this._$parts.forEach(function($part) {
$part.toggleClass('comp-selected', this.selected);
}, this);
}
setSelected(selected) {
this.setProperty('selected', selected);
}
updateSelectedComponent($part, updateScrollPosition) {
this.parent._selectedComponentChanged(this, $part.data('partDay'), updateScrollPosition);
}
_onMouseDown(event) {
var $part = $(event.delegateTarget);
this.updateSelectedComponent($part, false);
if (event.button === 0) {
var popup = scout.create('WidgetPopup', {
parent: this.parent,
$anchor: $part,
closeOnAnchorMouseDown: true,
closeOnMouseDownOutside: true,
closeOnOtherPopupOpen: true,
horizontalAlignment: Popup.Alignment.LEFT,
verticalAlignment: Popup.Alignment.CENTER,
trimWidth: false,
trimHeight: false,
horizontalSwitch: true,
verticalSwitch: true,
withArrow: true,
cssClass: 'tooltip',
scrollType: 'remove',
location: {
y: event.originalEvent.y
},
widget: {
objectType: 'Label',
htmlEnabled: true,
cssClass: 'tooltip-content',
value: this._description()
}
});
popup.open();
}
// stop propagation to avoid fire mouse-down event on calendar-day (Calendar#_onDayMouseDown)
event.stopPropagation();
}
_onContextMenu(event) {
this.parent._showContextMenu(event, 'Calendar.CalendarComponent');
}
_format(date, pattern) {
return dates.format(date, this.session.locale, pattern);
}
_description() {
var descParts = [],
range = null,
text = '',
fromDate = dates.parseJsonDate(this.fromDate),
toDate = dates.parseJsonDate(this.toDate);
// subject
if (strings.hasText(this.item.subject)) {
descParts.push({
text: strings.encode(this.item.subject),
cssClass: 'calendar-component-title'
});
}
// time-range
if (this.fullDay) {
// NOP
} else if (dates.isSameDay(fromDate, toDate)) {
range = this.session.text('ui.FromXToY', this._format(fromDate, 'HH:mm'), this._format(toDate, 'HH:mm'));
} else {
range = this.session.text('ui.FromXToY', this._format(fromDate, 'EEEE HH:mm '), this._format(toDate, 'EEEE HH:mm'));
}
if (strings.hasText(range)) {
descParts.push({
text: range,
cssClass: 'calendar-component-intro'
});
}
// description
if (strings.hasText(this.item.description)) {
descParts.push({
text: strings.nl2br(this.item.description)
});
}
// build text
descParts.forEach(function(part) {
text += (part.cssClass ? '<span class="' + part.cssClass + '">' + part.text + '</span>' : part.text) + '<br/>';
});
return text;
}
}