@eclipse-scout/core
Version:
Eclipse Scout runtime
1,385 lines (1,205 loc) • 84.4 kB
text/typescript
/*
* Copyright (c) 2010, 2024 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {
arrays, CalendarComponent, CalendarDirection, CalendarDisplayMode, CalendarEventMap, CalendarLayout, CalendarListComponent, CalendarModel, CalendarModesMenu, CalendarMoveData, CalendarResourceDo, CalendarSidebar, ContextMenuPopup,
DateRange, dates, Device, EventHandler, events, GroupBox, HtmlComponent, InitModelOf, JsonDateRange, KeyStrokeContext, Menu, menus, numbers, objects, Point, PropertyChangeEvent, ResourcePanel, RoundingMode, scout, scrollbars, strings,
UuidPool, ViewportScroller, Widget, YearPanel, YearPanelDateSelectEvent
} from '../index';
import $ from 'jquery';
export class Calendar extends Widget implements CalendarModel {
declare model: CalendarModel;
declare eventMap: CalendarEventMap;
declare self: Calendar;
monthViewNumberOfWeeks: number;
numberOfHourDivisions: number;
widthPerDivision: number;
heightPerDivision: number;
startHour: number;
heightPerHour: number;
heightPerDay: number;
spaceBeforeScrollTop: number;
workDayIndices: number[];
displayCondensed: boolean;
displayMode: CalendarDisplayMode;
components: CalendarComponent[];
selectedComponent: CalendarComponent;
loadInProgress: boolean;
selectedDate: Date;
selectorStart: Date;
selectorEnd: Date;
showDisplayModeSelection: boolean;
rangeSelectionAllowed: boolean;
resources: CalendarResourceDo[];
selectedResource: CalendarResourceDo;
defaultResource: CalendarResourceDo;
title: string;
useOverflowCells: boolean;
viewRange: DateRange;
calendarToggleListWidth: number;
calendarToggleYearWidth: number;
menuInjectionTarget: GroupBox;
modesMenu: CalendarModesMenu;
menus: Menu[];
calendarSidebar: CalendarSidebar;
yearPanel: YearPanel;
resourcePanel: ResourcePanel;
selectedRange: DateRange;
needsScrollToStartHour: boolean;
defaultMenuTypes: string[];
showCalendarSidebar: boolean;
showResourcePanel: boolean;
showListPanel: boolean;
$header: JQuery;
$range: JQuery;
$commands: JQuery;
$grids: JQuery;
$grid: JQuery;
$topGrid: JQuery;
$list: JQuery;
$listContainer: JQuery;
$listTitle: JQuery;
$progress: JQuery;
$headerRow1: JQuery;
$headerRow2: JQuery;
$title: JQuery;
$select: JQuery;
$window: JQuery<Window>;
/**
* The narrow view range is different from the regular view range.
* It contains only dates that exactly match the requested dates,
* the regular view range contains also dates from the first and
* next month. The exact range is not sent to the server.
*/
protected _exactRange: DateRange;
/**
* When the list panel is shown, this list contains the scout.CalenderListComponent
* items visible on the list.
*/
protected _listComponents: CalendarListComponent[];
protected _menuInjectionTargetMenusChangedHandler: EventHandler<PropertyChangeEvent<Menu[], GroupBox>>;
/**
* Resources, which do not have a child resources
*/
protected _leafResources: CalendarResourceDo[];
/**
* Temporary data structure to store data while mouse actions are handled
* @internal
*/
_moveData: CalendarMoveData;
/** @internal */
_rangeSelectionStarted: boolean;
protected _mouseMoveHandler: (event: JQuery.MouseMoveEvent) => void;
protected _mouseUpHandler: (event: JQuery.MouseUpEvent) => void;
protected _mouseMoveRangeSelectionHandler: (event: JQuery.MouseMoveEvent) => void;
protected _mouseUpRangeSelectionHandler: (event: JQuery.MouseUpEvent) => void;
constructor() {
super();
this.monthViewNumberOfWeeks = 6;
this.numberOfHourDivisions = 2;
this.heightPerDivision = 30;
this.startHour = 6;
this.heightPerHour = this.numberOfHourDivisions * this.heightPerDivision;
this.heightPerDay = 24 * this.heightPerHour;
this.spaceBeforeScrollTop = 15;
this.workDayIndices = [1, 2, 3, 4, 5]; // Workdays: Mon-Fri (Week starts at Sun in JS)
this.components = [];
this.displayCondensed = false;
this.displayMode = Calendar.DisplayMode.MONTH;
this.loadInProgress = false;
this.selectedDate = new Date();
this.showDisplayModeSelection = true;
this.rangeSelectionAllowed = false;
this.defaultResource = this._createDefaultResource();
this.resources = [];
this.selectedResource = this.defaultResource;
this.title = null;
this.useOverflowCells = true;
this.viewRange = null;
this.calendarToggleListWidth = 270;
this.calendarToggleYearWidth = 215;
this.defaultMenuTypes = [Calendar.MenuType.EmptySpace];
// main elements
this.$container = null;
this.$header = null;
this.$range = null;
this.$commands = null;
this.$grids = null;
this.$grid = null;
this.$topGrid = null;
this.$list = null;
this.$progress = null;
this.showCalendarSidebar = false;
this.showListPanel = false;
this.showResourcePanel = false;
this._exactRange = null;
this._listComponents = [];
this.menuInjectionTarget = null;
this._menuInjectionTargetMenusChangedHandler = null;
this._moveData = null;
this._mouseMoveHandler = this._onMouseMove.bind(this);
this._mouseUpHandler = this._onMouseUp.bind(this);
this._mouseMoveRangeSelectionHandler = this._onMouseMoveRangeSelection.bind(this);
this._mouseUpRangeSelectionHandler = this._onMouseUpRangeSelection.bind(this);
this.selectedRange = null;
this._addWidgetProperties(['components', 'menus', 'selectedComponent', 'calendarSidebar']);
this._addPreserveOnPropertyChangeProperties(['selectedComponent']);
}
/**
* Enum providing display-modes for calender-like components like calendar and planner.
* @see ICalendarDisplayMode.java
*/
static DisplayMode = {
DAY: 1,
WEEK: 2,
MONTH: 3,
WORK_WEEK: 4
} as const;
/**
* Used as a multiplier in date calculations back- and forward (in time).
*/
static Direction = {
BACKWARD: -1,
FORWARD: 1
} as const;
static MenuType = {
EmptySpace: 'Calendar.EmptySpace',
CalendarComponent: 'Calendar.CalendarComponent'
} as const;
isDay(): boolean {
return this.displayMode === Calendar.DisplayMode.DAY;
}
isWeek(): boolean {
return this.displayMode === Calendar.DisplayMode.WEEK;
}
isMonth(): boolean {
return this.displayMode === Calendar.DisplayMode.MONTH;
}
isWorkWeek(): boolean {
return this.displayMode === Calendar.DisplayMode.WORK_WEEK;
}
protected override _createKeyStrokeContext(): KeyStrokeContext {
return new KeyStrokeContext();
}
protected _createDefaultResource(): CalendarResourceDo {
return {
resourceId: UuidPool.ZERO_UUID,
selectable: true,
visible: true
};
}
protected override _init(model: InitModelOf<this>) {
super._init(model);
this.calendarSidebar = scout.create(CalendarSidebar, {
parent: this
});
this.yearPanel = this.calendarSidebar.yearPanel;
this.resourcePanel = this.calendarSidebar.resoucePanel;
this.resourcePanel.treeBox.on('propertyChange:value', this._onResourceVisibilityChanged.bind(this));
this.yearPanel.on('dateSelect', this._onYearPanelDateSelect.bind(this));
this.modesMenu = scout.create(CalendarModesMenu, {
parent: this,
visible: false,
displayMode: this.displayMode
});
this._setSelectedDate(this.selectedDate);
this._setDisplayMode(this.displayMode);
this._setMenuInjectionTarget(this.menuInjectionTarget);
this._setResources(this.resources);
this._exactRange = this._calcExactRange();
this.yearPanel.setViewRange(this._exactRange);
this.viewRange = this._calcViewRange();
}
protected _updateResourcePanelDisplayable() {
this.calendarSidebar.setResourcePanelDisplayable(this.resources.length > 1);
}
setSelectedDate(date: Date | string) {
this.setProperty('selectedDate', date);
}
protected _setSelectedDate(date: Date | string) {
date = dates.ensure(date);
scout.assertParameter('selectedDate', date, Date);
this._setProperty('selectedDate', date);
this.yearPanel.selectDate(this.selectedDate);
}
protected _renderSelectedDate() {
this._updateModel(false);
}
setDisplayMode(displayMode: CalendarDisplayMode) {
if (objects.equals(this.displayMode, displayMode)) {
return;
}
let oldDisplayMode = this.displayMode;
this._setDisplayMode(displayMode);
if (this.rendered) {
this._renderDisplayMode(oldDisplayMode);
}
}
protected _setDisplayMode(displayMode: CalendarDisplayMode) {
this._setProperty('displayMode', displayMode);
this.yearPanel.setDisplayMode(this.displayMode);
this.modesMenu.setDisplayMode(displayMode);
if (this.isWorkWeek()) {
// change date if selectedDate is on a weekend
let p = this._dateParts(this.selectedDate, true);
if (p.day > 4) {
this.setSelectedDate(new Date(p.year, p.month, p.date - p.day + 4));
}
}
this.selectedRange = null;
this.selectedResource = this.defaultResource;
this.trigger('selectedRangeChange');
this.trigger('selectedResourceChange', {resourceId: null});
}
protected _renderDisplayMode(oldDisplayMode?: CalendarDisplayMode) {
if (this.rendering) {
// only do it on property changes
return;
}
this._updateModel(true);
// only render if components have another layout
let renderRequired = arrays.containsAny([this.displayMode, oldDisplayMode], [Calendar.DisplayMode.DAY, Calendar.DisplayMode.MONTH]);
if (renderRequired) {
this._renderComponents();
this.needsScrollToStartHour = true;
}
}
protected _setViewRange(viewRange: DateRange | JsonDateRange) {
viewRange = DateRange.ensure(viewRange);
this._setProperty('viewRange', viewRange);
}
protected _setRangeSelectionAllowed(rangeSelectionAllowed: boolean) {
this.rangeSelectionAllowed = rangeSelectionAllowed;
if (!this.rangeSelectionAllowed) {
this.setSelectedRange(null);
}
}
setSelectedRange(range: DateRange | JsonDateRange) {
let selectedRange = DateRange.ensure(range);
if (selectedRange && selectedRange.from && selectedRange.to) {
this.selectorStart = new Date(selectedRange.from);
this.selectorStart.setHours(0, this._getHours(this.selectorStart) * 60);
this.selectorEnd = new Date(selectedRange.to);
this.selectorEnd.setHours(0, this._getHours(this.selectorEnd) * 60 - 30);
this._setRangeSelection();
} else {
this.selectorStart = null;
this.selectorEnd = null;
this._removeRangeSelection();
}
this._updateSelectedRange();
}
protected _setMenus(menus: Menu[]) {
if (this._checkMenuInjectionTarget(this.menuInjectionTarget)) {
let originalMenus = this._removeInjectedMenus(this.menuInjectionTarget, this.menus);
this.menuInjectionTarget.setMenus(menus.concat(originalMenus));
}
this._setProperty('menus', menus);
}
protected _setMenuInjectionTarget(menuInjectionTarget: GroupBox | string) {
if (objects.isString(menuInjectionTarget)) {
menuInjectionTarget = scout.widget(menuInjectionTarget) as GroupBox;
}
// Remove injected menus and installed listener from old injection target
if (this._checkMenuInjectionTarget(this.menuInjectionTarget)) {
menuInjectionTarget.off('propertyChange:menus', this._menuInjectionTargetMenusChangedHandler);
let originalMenus = this._removeInjectedMenus(this.menuInjectionTarget, this.menus);
this.menuInjectionTarget.setMenus(originalMenus);
}
if (this._checkMenuInjectionTarget(menuInjectionTarget)) {
menuInjectionTarget.setMenus(this.menus.concat(menuInjectionTarget.menus));
// Listen for menu changes on the injection target. Re inject menus into target if the menus have been altered.
this._menuInjectionTargetMenusChangedHandler = evt => {
if (this.menuInjectionTarget.menus.some(element => this.menus.includes(element))) {
// Menus have already been injected => Do nothing
return;
}
this.menuInjectionTarget.setMenus(this.menus.concat(this.menuInjectionTarget.menus));
};
menuInjectionTarget.on('propertyChange:menus', this._menuInjectionTargetMenusChangedHandler);
}
this._setProperty('menuInjectionTarget', menuInjectionTarget);
}
protected _checkMenuInjectionTarget(menuInjectionTarget: GroupBox): boolean {
return menuInjectionTarget instanceof GroupBox;
}
protected _removeInjectedMenus(menuInjectionTarget: GroupBox, injectedMenus: Menu[]): Menu[] {
return menuInjectionTarget.menus.filter(element => !injectedMenus.includes(element));
}
setShowCalendarSidebar(showCalendarSidebar: boolean) {
this.setProperty('showCalendarSidebar', showCalendarSidebar);
}
setShowResourcePanel(showResourcePanel: boolean) {
this.setProperty('showResourcePanel', showResourcePanel);
}
setShowListPanel(showListPanel: boolean) {
this.setProperty('showListPanel', showListPanel);
}
protected override _render() {
this.$container = this.$parent.appendDiv('calendar');
let layout = new CalendarLayout(this);
this.htmlComp = HtmlComponent.install(this.$container, this.session);
this.htmlComp.setLayout(layout);
let isMobile = Device.get().type === Device.Type.MOBILE;
// main elements
this.$header = this.$container.appendDiv('calendar-header');
this.$header.toggleClass('mobile', isMobile);
this.$headerRow1 = this.$header.appendDiv('calendar-header-row first');
this.$headerRow2 = this.$header.appendDiv('calendar-header-row last');
this.calendarSidebar.render();
this.$grids = this.$container.appendDiv('calendar-grids');
this.$topGrid = this.$grids.appendDiv('calendar-top-grid');
this.$topGrid.toggleClass('mobile', isMobile);
this.$grid = this.$grids.appendDiv('calendar-grid');
this.$grid.toggleClass('mobile', isMobile);
this.$listContainer = this.$container.appendDiv('calendar-list-container');
this.$list = this.$listContainer.appendDiv('calendar-list calendar-scrollable-components');
this.$listTitle = this.$list.appendDiv('calendar-list-title');
// header contains range, title and commands. On small screens title will be moved to headerRow2
this.$range = this.$headerRow1.appendDiv('calendar-range');
this.$range.appendDiv('calendar-previous')
.on('click', this._onPreviousClick.bind(this));
this.$range.appendDiv('calendar-today', this.session.text('ui.CalendarToday'))
.on('click', this._onTodayClick.bind(this));
this.$range.appendDiv('calendar-next')
.on('click', this._onNextClick.bind(this));
// title
this.$title = this.$headerRow1.appendDiv('calendar-title');
this.$select = this.$title.appendDiv('calendar-select');
this.$progress = this.$title.appendDiv('busyindicator-label');
// commands
this.$commands = this.$headerRow1.appendDiv('calendar-commands');
this.$commands.appendDiv('calendar-mode first', this.session.text('ui.CalendarDay'))
.attr('data-mode', Calendar.DisplayMode.DAY)
.on('click', this._onDisplayModeClick.bind(this));
this.$commands.appendDiv('calendar-mode', this.session.text('ui.CalendarWorkWeek'))
.attr('data-mode', Calendar.DisplayMode.WORK_WEEK)
.on('click', this._onDisplayModeClick.bind(this));
this.$commands.appendDiv('calendar-mode', this.session.text('ui.CalendarWeek'))
.attr('data-mode', Calendar.DisplayMode.WEEK)
.on('click', this._onDisplayModeClick.bind(this));
this.$commands.appendDiv('calendar-mode last', this.session.text('ui.CalendarMonth'))
.attr('data-mode', Calendar.DisplayMode.MONTH)
.on('click', this._onDisplayModeClick.bind(this));
this.modesMenu.render(this.$commands);
// Top-right menus
this.$commands.appendDiv('calendar-toggle-sidebar')
.on('click', this._onCalendarSidebarClick.bind(this));
this._updateResourcePanelDisplayable();
this.$commands.appendDiv('calendar-toggle-list')
.on('click', this._onListClick.bind(this));
// Append the top grid (day/week views)
let $weekHeader = this.$topGrid.appendDiv('calendar-week-header');
$weekHeader.appendDiv('calendar-week-name');
for (let dayTop = 0; dayTop < 7; dayTop++) {
$weekHeader.appendDiv('calendar-day-name')
.data('day', dayTop);
}
let $weekTopGridDays = this.$topGrid.appendDiv('calendar-week-allday-container');
$weekTopGridDays.appendDiv('calendar-week-name');
let dayContextMenuCallback = this._onDayContextMenu.bind(this);
for (let dayBottom = 0; dayBottom < 7; dayBottom++) {
$weekTopGridDays.appendDiv('calendar-day')
.data('day', dayBottom)
.on('contextmenu', dayContextMenuCallback);
}
for (let w = 1; w < 7; w++) {
let $w = this.$grid.appendDiv('calendar-week');
for (let d = 0; d < 8; d++) {
let $d = $w.appendDiv();
if (w > 0 && d === 0) {
$d.addClass('calendar-week-name');
} else if (w > 0 && d > 0) {
$d.addClass('calendar-day')
.data('day', d)
.data('week', w)
.on('contextmenu', dayContextMenuCallback);
}
}
}
this.$window = this.$container.window();
this.$container.on('mousedown touchstart', this._onMouseDown.bind(this));
this._updateScreen(false, false);
}
protected override _renderProperties() {
super._renderProperties();
this._renderResources();
this._renderComponents();
this._renderSelectedComponent();
this._renderLoadInProgress();
this._renderDisplayMode();
}
setResources(resources: CalendarResourceDo[]) {
this.setProperty('resources', resources);
}
protected _setResources(resources: CalendarResourceDo[]) {
this._setProperty('resources', resources);
this._updateLeafResources();
this._updateResourcePanelDisplayable();
this._updateResourcePanel();
this._validateSelectedResource();
}
protected _renderResources() {
let $dayName = this.$topGrid.find('.calendar-week-header > .calendar-day-name');
let $fullDay = this.$topGrid.find('.calendar-week-allday-container > .calendar-day');
let $day = this.$grid.find('.calendar-week > .calendar-day');
// Remove old resource columns
$dayName.find('.resource-column').remove();
$fullDay.find('.resource-column').remove();
$day.find('.resource-column').remove();
// Add default resource columns
$dayName.appendDiv('resource-column')
.data('resourceId', this.defaultResource.resourceId);
$fullDay.appendDiv('resource-column')
.data('resourceId', this.defaultResource.resourceId)
.addClass('calendar-scrollable-components');
$day.appendDiv('resource-column')
.data('resourceId', this.defaultResource.resourceId);
// Add new resources columns
this._leafResources.forEach(resources => {
$dayName.appendDiv('resource-column')
.data('resourceId', resources.resourceId)
.attr('data-resource-name', resources.name);
$fullDay.appendDiv('resource-column')
.data('resourceId', resources.resourceId)
.addClass('calendar-scrollable-components');
$day.appendDiv('resource-column')
.data('resourceId', resources.resourceId);
});
// click event on all day and children elements
let mousedownCallbackWithTime = this._onDayColumnMouseDown.bind(this, true);
this.$grid.find('.resource-column').on('mousedown', mousedownCallbackWithTime);
let mousedownCallback = this._onDayColumnMouseDown.bind(this, false);
this.$topGrid.find('.resource-column').on('mousedown', mousedownCallback);
// Only required when rendering is triggered by setter
if (!this.rendering) {
this._renderComponents();
}
}
protected _updateResourcePanel() {
this.resourcePanel.treeBox.lookupCall.setResources(this.resources);
this.resourcePanel.treeBox.refreshLookup();
let value = this.resources
.filter(resource => resource.visible)
.map(resource => resource.resourceId);
this.resourcePanel.treeBox.setValue(value);
}
protected _validateSelectedResource() {
if (!arrays.contains(this.resources, this.selectedResource)) {
this.selectedResource = this.defaultResource;
this.trigger('selectedResourceChange', {resourceId: this.selectedResource.resourceId});
}
}
setComponents(components: CalendarComponent[]) {
this.setProperty('components', components);
}
protected _setComponents(components: CalendarComponent[]) {
let layoutRequired = false;
if (this.isDay()) {
// Re-layout required when visibility of default resource will change when setting new components
layoutRequired = this._defaultResourceVisible() !== this._defaultResourceVisible(components);
}
this._setProperty('components', components);
if (this.rendered && layoutRequired) {
this.layoutSize(true);
}
}
addComponents(components: CalendarComponent[]) {
this.setComponents([...this.components, ...components]);
}
protected _renderComponents() {
this.components.sort(this._sortFromTo);
this._updateFullDayIndices();
this.components.forEach(component => component.remove());
this.components.forEach(component => component.render());
scrollbars.update(this.$grid);
this._arrangeComponents();
this._updateListPanel();
}
protected _renderSelectedComponent() {
if (this.selectedComponent) {
this.selectedComponent.setSelected(true);
}
}
protected _renderLoadInProgress() {
this.$progress.setVisible(this.loadInProgress);
}
updateScrollPosition(animate: boolean) {
if (!this.rendered) {
// Execute delayed because table may be not layouted yet
this.session.layoutValidator.schedulePostValidateFunction(this._updateScrollPosition.bind(this, true, animate));
} else {
this._updateScrollPosition(true, animate);
}
}
protected _updateScrollPosition(scrollToInitialTime: boolean, animate: boolean) {
if (this.isMonth()) {
this._scrollToSelectedComponent(animate);
} else {
if (this.selectedComponent) {
if (this.selectedComponent.fullDay) {
this._scrollToSelectedComponent(animate); // scroll top-grid to selected component
if (scrollToInitialTime) {
this._scrollToInitialTime(animate); // scroll grid to initial time
}
} else {
let date = dates.parseJsonDate(this.selectedComponent.fromDate);
let topPercent = this._dayPosition(date.getHours(), date.getMinutes()) / 100;
let topPos = this.heightPerDay * topPercent;
scrollbars.scrollTop(this.$grid, topPos - this.spaceBeforeScrollTop, {
animate: animate
});
}
} else if (scrollToInitialTime) {
this._scrollToInitialTime(animate);
}
}
}
protected _updateScrollShadow() {
scrollbars.updateScrollShadow(this.$grid);
}
protected _scrollToSelectedComponent(animate: boolean) {
if (this.selectedComponent && this.selectedComponent._$parts[0] && this.selectedComponent._$parts[0].parent() && this.selectedComponent._$parts[0].isVisible()) {
scrollbars.scrollTo(this.selectedComponent._$parts[0].parent(), this.selectedComponent._$parts[0], {
animate: animate
});
}
}
protected _scrollToInitialTime(animate: boolean) {
this.needsScrollToStartHour = false;
if (!this.isMonth()) {
if (this.selectedComponent && !this.selectedComponent.fullDay) {
let date = dates.parseJsonDate(this.selectedComponent.fromDate);
let topPercent = this._dayPosition(date.getHours(), date.getMinutes()) / 100;
let topPos = this.heightPerDay * topPercent;
scrollbars.scrollTop(this.$grid, topPos - this.spaceBeforeScrollTop, {
animate: animate
});
} else {
let scrollTargetTop = this.heightPerHour * this.startHour;
scrollbars.scrollTop(this.$grid, scrollTargetTop - this.spaceBeforeScrollTop, {
animate: animate
});
}
}
}
/* -- basics, events -------------------------------------------- */
protected _onPreviousClick() {
this._navigateDate(Calendar.Direction.BACKWARD);
}
protected _onNextClick() {
this._navigateDate(Calendar.Direction.FORWARD);
}
protected _dateParts(date: Date, modulo?: boolean): { year: number; month: number; date: number; day: number } {
let parts = {
year: date.getFullYear(),
month: date.getMonth(),
date: date.getDate(),
day: date.getDay()
};
if (modulo) {
parts.day = (date.getDay() + 6) % 7;
}
return parts;
}
protected _navigateDate(direction: CalendarDirection) {
this.selectedDate = this._calcSelectedDate(direction);
this._updateModel(false);
}
protected _calcSelectedDate(direction: CalendarDirection): Date {
// noinspection UnnecessaryLocalVariableJS
let p = this._dateParts(this.selectedDate),
dayOperand = direction,
weekOperand = direction * 7,
monthOperand = direction;
if (this.isDay()) {
return new Date(p.year, p.month, p.date + dayOperand);
}
if (this.isWeek() || this.isWorkWeek()) {
return new Date(p.year, p.month, p.date + weekOperand);
}
if (this.isMonth()) {
return dates.shift(this.selectedDate, 0, monthOperand, 0);
}
}
protected _updateModel(animate: boolean) {
this._exactRange = this._calcExactRange();
this.yearPanel.setViewRange(this._exactRange);
this.viewRange = this._calcViewRange();
this.trigger('modelChange');
this._updateScreen(true, animate);
}
/**
* Calculates exact date range of displayed components based on selected-date.
*/
protected _calcExactRange(): DateRange {
let from, to,
p = this._dateParts(this.selectedDate, true);
if (this.isDay()) {
from = new Date(p.year, p.month, p.date);
to = new Date(p.year, p.month, p.date);
} else if (this.isWeek()) {
from = new Date(p.year, p.month, p.date - p.day);
to = new Date(p.year, p.month, p.date - p.day + 6);
} else if (this.isMonth()) {
from = new Date(p.year, p.month, 1);
to = new Date(p.year, p.month + 1, 0);
} else if (this.isWorkWeek()) {
from = new Date(p.year, p.month, p.date - p.day);
to = new Date(p.year, p.month, p.date - p.day + 4);
} else {
throw new Error('invalid value for displayMode');
}
return new DateRange(from, to);
}
/**
* Calculates the view-range, which is what the user sees in the UI.
* The view-range is wider than the exact-range in the monthly mode,
* as it contains also dates from the previous and next month.
*/
protected _calcViewRange(): DateRange {
let viewFrom = calcViewFromDate(this._exactRange.from),
viewTo = calcViewToDate(viewFrom);
return new DateRange(viewFrom, viewTo);
function calcViewFromDate(fromDate: Date): Date {
let i, tmpDate = new Date(fromDate.valueOf());
for (i = 0; i < 42; i++) {
tmpDate.setDate(tmpDate.getDate() - 1);
if ((tmpDate.getDay() === 1) && tmpDate.getMonth() !== fromDate.getMonth()) {
return tmpDate;
}
}
throw new Error('failed to calc viewFrom date');
}
function calcViewToDate(fromDate: Date): Date {
let i, tmpDate = new Date(fromDate.valueOf());
for (i = 0; i < 42; i++) {
tmpDate.setDate(tmpDate.getDate() + 1);
}
return tmpDate;
}
}
protected _onTodayClick(event: JQuery.ClickEvent) {
this.selectedDate = new Date();
this._updateModel(false);
}
protected _onDisplayModeClick(event: JQuery.ClickEvent) {
let displayMode = $(event.target).data('mode');
this.setDisplayMode(displayMode);
}
protected _onCalendarSidebarClick(event: JQuery.ClickEvent) {
this.setShowCalendarSidebar(!this.showCalendarSidebar);
this._updateScreen(true, true);
}
protected _onListClick(event: JQuery.ClickEvent) {
this.setShowListPanel(!this.showListPanel);
this._updateScreen(false, true);
}
protected _onDayColumnMouseDown(withTime: boolean, event: JQuery.MouseDownEvent) {
let selectedDayColumn = $(event.delegateTarget),
selectedDate = new Date(selectedDayColumn.parent().data('date')),
timeChanged = false,
selectedResourceId = selectedDayColumn.data('resourceId'),
selectedResourceChanged = selectedResourceId !== this.selectedResource.resourceId;
// Do not apply selection when clicked on calendar component
if (this._get$CalendarComponent($(event.target)).length > 0) {
return;
}
// When left click on component was made, which was covered by a range selection, apply the selection on the component
let componentPartElement = document.elementsFromPoint(event.pageX, event.pageY).find(e => e.classList.contains('calendar-component'));
if (event.button === 0 && componentPartElement) {
let $part = $(componentPartElement as HTMLElement);
let component = $part.data('component') as CalendarComponent;
if (component) {
component.applySelection($part, true, event.originalEvent.clientY);
return;
}
}
// Check if date is valid
if (!selectedDate.valueOf()) {
return;
}
let rangeSelectionPossible = withTime && (this.isDay() || this.isWeek() || this.isWorkWeek());
// Set seconds of date
if (rangeSelectionPossible) {
let seconds = this._getSelectedSeconds(event);
if (seconds < 60 * 60 * 24) {
selectedDate.setSeconds(seconds);
timeChanged = true;
}
}
this._setSelection(selectedDate, selectedResourceId, null, false, timeChanged);
if (rangeSelectionPossible) {
this._startRangeSelection(event, selectedResourceChanged);
}
}
protected _getSelectedDate(event: JQuery.MouseEventBase): Date {
let date = null;
let $target = $(event.target);
if ($target.hasClass('calendar-day')) {
date = $target.data('date');
} else if ($target.hasClass('resource-column')) {
date = $target.parent().data('date');
} else if ($target.hasClass('calendar-component')
|| $target.parents('.calendar-component').length > 0
|| $target.hasClass('calendar-range-selector')) {
date = $target.closest('.calendar-day').data('date');
}
if (date) {
return new Date(date);
}
return null;
}
protected _getSelectedSeconds(event: JQuery.MouseEventBase): number {
let y = event.originalEvent.layerY;
let $target = $(event.target);
let $component = this._get$CalendarComponent($target);
if ($component.length > 0) {
y += $component.position().top;
} else if ($target.hasClass('calendar-range-selector')) {
y += $target.position().top;
}
return Math.floor(y / this.heightPerDivision) / this.numberOfHourDivisions * 60 * 60;
}
protected _getSelectedDateTime(event: JQuery.MouseEventBase): Date {
let selectedDate = this._getSelectedDate(event);
if (selectedDate && (this.isDay() || this.isWeek() || this.isWorkWeek())) {
let seconds = this._getSelectedSeconds(event);
if (seconds < 60 * 60 * 24) {
selectedDate.setSeconds(seconds);
}
}
return selectedDate;
}
/**
* @param selectedComponent may be null when a day is selected
*/
protected _setSelection(selectedDate: Date, selectedResource: CalendarResourceDo | string, selectedComponent: CalendarComponent, updateScrollPosition: boolean, timeChanged: boolean) {
let changed = false;
let dateChanged = dates.compareDays(this.selectedDate, selectedDate) !== 0;
// selected date
if (dateChanged || timeChanged) {
changed = true;
if (dateChanged) {
$('.calendar-day', this.$container).each((index, element) => {
let $day = $(element),
date = $day.data('date');
if (!date || dates.compareDays(date, this.selectedDate) === 0) {
$day.select(false); // de-select old date
} else if (dates.compareDays(date, selectedDate) === 0) {
$day.select(true); // select new date
}
});
}
this.selectedDate = selectedDate;
}
// Set selected resource
let newSelectedResource = this.selectedResource;
if (objects.isString(selectedResource)) {
newSelectedResource = this.findResourceForId(selectedResource as string);
} else if (selectedResource) {
newSelectedResource = selectedResource as CalendarResourceDo;
}
if (newSelectedResource !== this.selectedResource && newSelectedResource.selectable) {
changed = true;
this.selectedResource = newSelectedResource;
this.trigger('selectedResourceChange', {resourceId: newSelectedResource.resourceId});
}
// selected component / part (maybe null)
if (this.selectedComponent !== selectedComponent) {
changed = true;
if (this.selectedComponent) {
this.selectedComponent.setSelected(false);
}
if (selectedComponent) {
selectedComponent.setSelected(true);
}
this.selectedComponent = selectedComponent;
}
if (changed) {
this.trigger('selectionChange');
if (dateChanged) {
this._updateListPanel();
}
if (updateScrollPosition) {
this._updateScrollPosition(false, true);
}
}
if (this.showCalendarSidebar) {
this.yearPanel.selectDate(this.selectedDate);
}
}
protected _get$CalendarComponent($element: JQuery) {
return $element.closest('.calendar-component');
}
/* -- set display mode and range ------------------------------------- */
protected _updateScreen(updateTopGrid: boolean, animate: boolean) {
$.log.isInfoEnabled() && $.log.info('(Calendar#_updateScreen)');
// select mode
$('.calendar-mode', this.$commands).select(false);
$('[data-mode="' + this.displayMode + '"]', this.$commands).select(true);
// remove selected day
$('.selected', this.$grid).select(false);
// layout grid
this.layoutLabel();
this.layoutSize(animate);
this.layoutAxis();
if (this.showCalendarSidebar) {
this.yearPanel.selectDate(this.selectedDate);
}
this._updateListPanel();
this._updateScrollbars(this.$grid, animate);
if (updateTopGrid && !this.isMonth()) {
this._updateTopGrid();
}
this.setSelectedRange(this.selectedRange);
}
layoutSize(animate?: boolean) {
// reset animation sizes
$('div', this.$container).removeData(['new-width', 'new-height']);
this.$grids.toggleClass('calendar-grids-month', this.isMonth());
// init vars (Selected: Day)
let $selected = $('.selected', this.$grid),
$topSelected = $('.selected', this.$topGrid),
containerW = this.$container.width(),
gridH = this.$grid.height(),
gridPaddingX = this.$grid.innerWidth() - this.$grid.width(),
gridW = containerW - gridPaddingX;
// show or hide calendar sidebar
if (this.showCalendarSidebar) {
this.calendarSidebar.$container.data('new-width', this.calendarToggleYearWidth);
gridW -= this.calendarToggleYearWidth;
containerW -= this.calendarToggleYearWidth;
} else {
this.calendarSidebar.$container.data('new-width', 0);
}
this.calendarSidebar.setResourcePanelExpanded(this.showResourcePanel);
// show or hide work list
$('.calendar-toggle-list', this.$commands).select(this.showListPanel);
if (this.showListPanel) {
this.$list.parent().data('new-width', this.calendarToggleListWidth);
gridW -= this.calendarToggleListWidth;
containerW -= this.calendarToggleListWidth;
} else {
this.$list.parent().data('new-width', 0);
}
// basic grid width
this.$grids.data('new-width', containerW);
let $weeksToHide = $(); // Empty
let $allWeeks = $('.calendar-week', this.$grid);
// layout week
if (this.isDay() || this.isWeek() || this.isWorkWeek()) {
$allWeeks.removeClass('calendar-week-noborder');
// Parent of selected (Day) is a week
let selectedWeek = $selected.parent();
$weeksToHide = $allWeeks.not(selectedWeek); // Hide all (other) weeks delayed, height will animate to zero
$weeksToHide.data('new-height', 0);
$weeksToHide.removeClass('invisible');
selectedWeek.data('new-height', this.heightPerDay);
selectedWeek.addClass('calendar-week-noborder');
selectedWeek.removeClass('hidden invisible'); // Current week must be shown
$('.calendar-day', selectedWeek).data('new-height', this.heightPerDay);
// Hide the week-number in the lower grid
$('.calendar-week-name', this.$grid).addClass('invisible'); // Keep the reserved space
$('.calendar-week-allday-container', this.$topGrid).removeClass('hidden');
$('.calendar-week-task', this.$topGrid).removeClass('hidden');
} else {
// Month
let newHeightMonth = gridH / this.monthViewNumberOfWeeks;
$allWeeks.removeClass('calendar-week-noborder invisible hidden');
$allWeeks.eq(0).addClass('calendar-week-noborder');
$allWeeks.data('new-height', newHeightMonth);
$('.calendar-day', this.$grid).data('new-height', newHeightMonth);
let $allDays = $('.calendar-week-name', this.$grid);
$allDays.removeClass('hidden invisible');
$allDays.data('new-height', newHeightMonth);
$('.calendar-week-allday-container', this.$topGrid).addClass('hidden');
$('.calendar-week-task', this.$topGrid).addClass('hidden');
}
// layout days
let contentW = gridW - 45; // gridW - @calendar-week-name-width
if (this.isDay()) {
$('.calendar-day-name, .calendar-day', this.$topGrid).data('new-width', 0);
$('.calendar-day', this.$grid).data('new-width', 0);
$('.calendar-day-name:nth-child(' + ($topSelected.index() + 1) + ')', this.$topGrid)
.data('new-width', contentW);
$('.calendar-day:nth-child(' + ($topSelected.index() + 1) + ')', this.$topGrid).data('new-width', contentW);
$('.calendar-day:nth-child(' + ($selected.index() + 1) + ')', this.$grid).data('new-width', contentW);
this.widthPerDivision = contentW;
} else if (this.isWorkWeek()) {
this.$topGrid.find('.calendar-day-name').data('new-width', 0);
this.$grids.find('.calendar-day').data('new-width', 0);
let newWidthWorkWeek = Math.round(contentW / this.workDayIndices.length);
this.$topGrid.find('.calendar-day-name').slice(0, 5).data('new-width', newWidthWorkWeek);
this.$topGrid.find('.calendar-day').slice(0, 5).data('new-width', newWidthWorkWeek);
$('.calendar-day:nth-child(-n+6)', this.$grid).data('new-width', newWidthWorkWeek);
this.widthPerDivision = newWidthWorkWeek;
} else if (this.isMonth() || this.isWeek()) {
let newWidthMonthOrWeek = Math.round(contentW / 7);
this.$grids.find('.calendar-day').data('new-width', newWidthMonthOrWeek);
this.$topGrid.find('.calendar-day-name').data('new-width', newWidthMonthOrWeek);
this.widthPerDivision = newWidthMonthOrWeek;
}
// layout resource columns
let columnWidth = 0;
if (this.isDay()) {
columnWidth = Math.round(contentW / (this._leafResources.filter(c => c.visible).length + (this._defaultResourceVisible() ? 1 : 0)));
} else if (this.isWorkWeek()) {
columnWidth = Math.round(contentW / this.workDayIndices.length);
} else {
columnWidth = Math.round(contentW / 7);
}
if (this.isDay()) {
// Set size to 0 for all
$('.resource-column', this.$grids).data('new-width', 0);
// Resize visible columns of selected day
$('.calendar-day-name:nth-child(' + ($topSelected.index() + 1) + ')', this.$topGrid)
.add($('.calendar-day:nth-child(' + ($topSelected.index() + 1) + ')', this.$grids))
.find('.resource-column')
.filter((i, e) => {
let id = $(e).data('resourceId');
if (id === this.defaultResource.resourceId) {
return this._defaultResourceVisible();
}
let foundResource = this.findResourceForId(id);
return foundResource ? foundResource.visible : false;
}).data('new-width', columnWidth);
} else {
// Set size 0 for all resource columns
$('.resource-column', this.$grids).data('new-width', 0);
// Full size for default column
$('.resource-column', this.$grids)
.filter((i, e) => $(e).data('resourceId') === this.defaultResource.resourceId)
.data('new-width', columnWidth);
}
// layout components
if (this.isMonth()) {
$('.component-month', this.$grid).each(function() {
let $comp = $(this),
$day = $comp.closest('.calendar-day');
$comp.toggleClass('compact', $day.data('new-width') < CalendarComponent.MONTH_COMPACT_THRESHOLD);
});
}
// animate old to new sizes
$('div', this.$container).each((i, elem) => {
let $e = $(elem);
let w = $e.data('new-width');
let h = $e.data('new-height');
$e.stop(false, true);
if (w !== undefined && w !== $e.outerWidth()) {
if (animate) {
let opts: JQuery.EffectsOptions<HTMLElement> = {
complete: () => this._afterLayout($e, animate)
};
if ($e[0] === this.$grids[0]) {
// Grid contains scroll shadows that should be updated during animation (don't due it always for performance reasons)
opts.progress = () => this._afterLayout($e, animate);
}
$e.animate({width: w}, opts);
} else {
$e.css('width', w);
this._afterLayout($e, animate);
}
}
if (h !== undefined && h !== $e.outerHeight()) {
if (h > 0) {
$e.removeClass('hidden');
}
if (animate) {
$e.animateAVCSD('height', h,
() => {
if (h === 0) {
$e.addClass('hidden');
}
this._afterLayout($e, animate);
},
() => {
this._afterLayout($e, animate);
});
} else {
$e.css('height', h);
if (h === 0) {
$e.addClass('hidden');
}
this._afterLayout($e, animate);
}
}
});
}
protected _afterLayout($parent: JQuery, animate: boolean) {
this.calendarSidebar.invalidateLayoutTree(false);
this._updateScrollbars($parent, animate);
this._updateWeekdayNames();
}
protected _defaultResourceVisible(components = this.components): boolean {
return components
.filter(comp => comp.coveredDaysRange.covers(this.selectedDate, true))
.map(comp => comp.item)
.map(item => item ? item.resourceId : this.defaultResource.resourceId)
.map(resourceId => this.isLeafResource(resourceId) ? resourceId : this.defaultResource.resourceId)
.filter(resourceId => resourceId === this.defaultResource.resourceId)
.length > 0;
}
isLeafResource(resourceId: string) {
return !!this._leafResources.find(res => res.resourceId === resourceId);
}
findResourceForComponent(component: CalendarComponent): CalendarResourceDo {
return this.findResourceForId(component.getResourceId());
}
findResourceForId(resourceId: string): CalendarResourceDo {
if (!resourceId || resourceId === this.defaultResource.resourceId) {
return this.defaultResource;
}
let resource = this.resources.find(resource => resource.resourceId === resourceId);
// No resource found for this id
if (!resource) {
return this.defaultResource;
}
return resource;
}
protected _updateWeekdayNames() {
// set day-name (based on width of shown column)
let weekdayWidth = this.$topGrid.width(),
weekdays;
if (this.isDay()) {
weekdayWidth /= 1;
} else if (this.isWorkWeek()) {
weekdayWidth /= this.workDayIndices.length;
} else if (this.isWeek()) {
weekdayWidth /= 7;
} else if (this.isMonth()) {
weekdayWidth /= 7;
}
if (weekdayWidth > 90) {
weekdays = this.session.locale.dateFormat.symbols.weekdaysOrdered;
} else {
weekdays = this.session.locale.dateFormat.symbols.weekdaysShortOrdered;
}
$('.calendar-day-name > .resource-column', this.$topGrid)
.filter((i, element) => $(element).data('resourceId') === this.defaultResource.resourceId)
.each((i, elm) => {
$(elm).attr('data-resource-name', weekdays[i]);
});
}
protected _updateScrollbars($parent: JQuery, animate: boolean) {
let $scrollables = $('.calendar-scrollable-components', $parent);
$scrollables.each((i, elem) => {
scrollbars.update($(elem), true);
});
this.updateScrollPosition(animate);
this._updateScrollShadow();
}
protected _uninstallComponentScrollbars($parent: JQuery) {
$parent.find('.calendar-scrollable-components').each((i, elem) => {
scrollbars.uninstall($(elem), this.session);
});
}
protected _updateTopGrid() {
$('.calendar-component', this.$topGrid).each((index, part) => {
let component = $(part).data('component');
if (component) {
component.remove();
}
});
const fullDayComponents = this.components.filter(component => component.fullDay);
this._updateFullDayIndices(fullDayComponents);
// first remove all components and add them from scratch
fullDayComponents.forEach(component => component.remove());
fullDayComponents.forEach(component => component.render());
this._updateScrollbars(this.$topGrid, false);
scrollbars.update(this.$grid);
}
protected _updateFullDayIndices(fullDayComponents?: CalendarComponent[]) {
if (!fullDayComponents) {
fullDayComponents = this.components.filter(component => component.fullDay);
}
fullDayComponents.sort(this._sortFromTo);
const {from, to} = this._exactRange;
const usedIndicesMap = new Map<string, number[]>();
let maxComponentsPerDay = 0;
for (const component of fullDayComponents) {
component.fullDayIndex = -1;
if (component.coveredDaysRange.to < from || component.coveredDaysRange.from > to) {
// component is not in range
continue;
}
let date = component.coveredDaysRange.from;
if (date < from) {
date = from;
}
let fullDayIndexKey = this._calculateFullDayIndexKey(component, date);
let usedIndices = arrays.ensure(usedIndicesMap.get(fullDayIndexKey));
// get the first unused index
// create [0, 1, 2, ..., maxIndex, maxIndex + 1] remove the used indices
// => the minimum of the remaining array is the first unused index
const maxIndex = usedIndices.length ? arrays.max(usedIndices) : 0;
const indexCandidates = arrays.init(maxIndex + 2, null).map((elem, idx) => idx);
arrays.removeAll(indexCandidates, usedIndices);
const index = arrays.min(indexCandidates);
component.fullDayIndex = index;
// mark the index as used for all dates of the components range
// none of these indices can be used already due to the order of the components
while (date <= component.coveredDaysRange.to && date <= to) {
usedIndices.push(index);
usedIndicesMap.set(fullDayIndexKey, usedIndices);
date = dates.shift(date, 0, 0, 1);
fullDayIndexKey = this._calculateFullDayIndexKey(component, date);
usedIndices = arrays.ensure(usedIndicesMap.get(fullDayIndexKey));
}
maxComponentsPerDay = Math.max(index + 1, maxComponentsPerDay);
}
this.$grids.css('--full-day-components', maxComponentsPerDay);
}
protected _calculateFullDayIndexKey(component: CalendarComponent, date: Date): string {
let prefix = '';
if (this.isDay()) {
prefix = component.getResourceId();
}
return prefix + date.valueOf();
}
layoutYearPanel() {
if (this.showCalendarSidebar) {
scrollbars.update(this.yearPanel.$yearList);
this.yearPanel._scrollYear();
}
}
layoutLabel() {
let text, $dates, $topGridDates,
exFrom = this._exactRange.from,
exTo = this._exactRange.to;
// set range text
if (this.isDay()) {
text = this._format(exFrom, 'EEEE, d. MMMM yyyy');
} else if (this.isWorkWeek() || this.isWeek()) {
let toText = this.session.text('ui.to');
if (exFrom.getMonth() === exTo.getMonth()) {
text = strings.join(' ', this._format(exFrom, 'd.'), toText, this._format(exTo, 'd. MMMM yyyy'));
} else if (exFrom.getFullYear() === exTo.getFullYear()) {
text = strings.join(' ', this._format(exFrom, 'd. MMMM'), toText, this._format(exTo, 'd. MMMM yyyy'));
} else {
text = strings.join(' ', this._format(exFrom, 'd. MMMM yyyy'), toText, this._format(exTo, 'd. MMMM yyyy'));
}
} else if (this.isMonth()) {
text = this._format(exFrom, 'MMMM yyyy');
}
this.$select.text(text);
// prepare to set all day date and mark selected one
$dates = $('.calendar-day', this.$grid);
let w, d, cssClass,
currentMonth = this._exactRange.from.getMonth(),
date = new Date(this.viewRange.from.valueOf());
// Main grid: loop all days and set value and class
for (w = 0; w < this.monthViewNumberOfWeeks; w++) {
for (d = 0; d < 7; d++) {
cssClass = '';
if (this.workDayIndices.indexOf(date.getDay()) === -1) {
cssClass = date.getMonth() !== currentMonth ? ' weekend-out' : ' weekend';
} else {
cssClass = date.getMonth() !== currentMonth ? ' out' : '';
}