UNPKG

@eclipse-scout/core

Version:
1,385 lines (1,205 loc) 84.4 kB
/* * 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' : ''; }