| /* |
| * 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 {CalendarComponent, CalendarLayout, CalendarListComponent, DateRange, dates, GroupBox, HtmlComponent, KeyStrokeContext, menus, objects, scout, scrollbars, strings, Widget} from '../index'; |
| import $ from 'jquery'; |
| |
| export default class Calendar extends Widget { |
| |
| constructor() { |
| super(); |
| |
| this.monthViewNumberOfWeeks = 6; |
| this.numberOfHourDivisions = this.getConfiguredNumberOfHourDivisions(); |
| this.heightPerDivision = this.getConfiguredHeightPerDivision(); |
| this.startHour = this.getConfiguredStartHour(); |
| 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.displayMode; |
| this.displayCondensed = false; |
| this.loadInProgress = false; |
| this.selectedDate = null; |
| this.showDisplayModeSelection = true; |
| this.title = null; |
| this.useOverflowCells = true; |
| this.viewRange = null; |
| this.calendarToggleListWidth = 270; |
| this.calendarToggleYearWidth = 215; |
| |
| // 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; |
| |
| // additional modes; should be stored in model |
| this._showYearPanel = false; |
| this._showListPanel = false; |
| |
| /** |
| * 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. |
| */ |
| this._exactRange = null; |
| |
| /** |
| * When the list panel is shown, this list contains the scout.CalenderListComponent |
| * items visible on the list. |
| */ |
| this._listComponents = []; |
| this.menuInjectionTarget = null; |
| this._menuInjectionTargetMenusChangedHandler = null; |
| |
| this._addWidgetProperties(['components', 'menus', 'selectedComponent']); |
| } |
| |
| init(model, session, register) { |
| super.init(model, session, register); |
| } |
| |
| /** |
| * 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 |
| }; |
| |
| /** |
| * Used as a multiplier in date calculations back- and forward (in time). |
| */ |
| static Direction = { |
| BACKWARD: -1, |
| FORWARD: 1 |
| }; |
| |
| getConfiguredNumberOfHourDivisions() { |
| return 2; |
| } |
| |
| getConfiguredHeightPerDivision() { |
| return 30; |
| } |
| |
| getConfiguredStartHour() { |
| return 6; |
| } |
| |
| _isDay() { |
| return this.displayMode === Calendar.DisplayMode.DAY; |
| } |
| |
| _isWeek() { |
| return this.displayMode === Calendar.DisplayMode.WEEK; |
| } |
| |
| _isMonth() { |
| return this.displayMode === Calendar.DisplayMode.MONTH; |
| } |
| |
| _isWorkWeek() { |
| return this.displayMode === Calendar.DisplayMode.WORK_WEEK; |
| } |
| |
| /** |
| * @override |
| */ |
| _createKeyStrokeContext() { |
| return new KeyStrokeContext(); |
| } |
| |
| _init(model) { |
| super._init(model); |
| this._yearPanel = scout.create('YearPanel', { |
| parent: this |
| }); |
| this._yearPanel.on('dateSelect', this._onYearPanelDateSelect.bind(this)); |
| this.modesMenu = scout.create('CalendarModesMenu', { |
| parent: this, |
| visible: false, |
| displayMode: this.displayMode |
| }); |
| this._setSelectedDate(model.selectedDate); |
| this._setDisplayMode(model.displayMode); |
| this._setMenuInjectionTarget(model.menuInjectionTarget); |
| this._exactRange = this._calcExactRange(); |
| this._yearPanel.setViewRange(this._exactRange); |
| this.viewRange = this._calcViewRange(); |
| } |
| |
| setSelectedDate(date) { |
| this.setProperty('selectedDate', date); |
| } |
| |
| _setSelectedDate(date) { |
| date = dates.ensure(date); |
| this._setProperty('selectedDate', date); |
| this._yearPanel.selectDate(this.selectedDate); |
| } |
| |
| setDisplayMode(displayMode) { |
| if (objects.equals(this.displayMode, displayMode)) { |
| return; |
| } |
| var oldDisplayMode = this.displayMode; |
| this._setDisplayMode(displayMode); |
| if (this.rendered) { |
| this._renderDisplayMode(oldDisplayMode); |
| } |
| } |
| |
| _setDisplayMode(displayMode) { |
| this._setProperty('displayMode', displayMode); |
| this._yearPanel.setDisplayMode(this.displayMode); |
| this.modesMenu.setDisplayMode(displayMode); |
| if (this._isWorkWeek()) { |
| // change date if selectedDate is on a weekend |
| var p = this._dateParts(this.selectedDate, true); |
| if (p.day > 4) { |
| this.setSelectedDate(new Date(p.year, p.month, p.date - p.day + 4)); |
| } |
| } |
| } |
| |
| _renderDisplayMode(oldDisplayMode) { |
| if (this.rendering) { |
| // only do it on property changes |
| return; |
| } |
| this._updateModel(false, true); |
| |
| // only render if components have another layout |
| if (oldDisplayMode === Calendar.DisplayMode.MONTH || this.displayMode === Calendar.DisplayMode.MONTH) { |
| this._renderComponents(); |
| this.needsScrollToStartHour = true; |
| } |
| } |
| |
| _setViewRange(viewRange) { |
| viewRange = DateRange.ensure(viewRange); |
| this._setProperty('viewRange', viewRange); |
| } |
| |
| _setMenus(menus) { |
| if (this._checkMenuInjectionTarget(this.menuInjectionTarget)) { |
| var originalMenus = this._removeInjectedMenus(this.menuInjectionTarget, this.menus); |
| this.menuInjectionTarget.setMenus(menus.concat(originalMenus)); |
| } |
| this._setProperty('menus', menus); |
| } |
| |
| _setMenuInjectionTarget(menuInjectionTarget) { |
| if (objects.isString(menuInjectionTarget)) { |
| menuInjectionTarget = scout.widget(menuInjectionTarget); |
| } |
| // Remove injected menus and installed listener from old injection target |
| if (this._checkMenuInjectionTarget(this.menuInjectionTarget)) { |
| menuInjectionTarget.off('propertyChange:menus', this._menuInjectionTargetMenusChangedHandler); |
| var 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 = menuInjectionTarget.on('propertyChange:menus', |
| function(evt) { |
| if (this.menuInjectionTarget.menus.some(function(element) { |
| return this.menus.includes(element); |
| }.bind(this))) { |
| // Menus have already been injected => Do nothing |
| return; |
| } |
| this.menuInjectionTarget.setMenus(this.menus.concat(this.menuInjectionTarget.menus)); |
| }.bind(this) |
| ); |
| } |
| this._setProperty('menuInjectionTarget', menuInjectionTarget); |
| } |
| |
| _checkMenuInjectionTarget(menuInjectionTarget) { |
| return menuInjectionTarget instanceof GroupBox; |
| } |
| |
| _removeInjectedMenus(menuInjectionTarget, injectedMenus) { |
| return menuInjectionTarget.menus.filter(function(element) { |
| return !injectedMenus.includes(element); |
| }); |
| } |
| |
| _render() { |
| this.$container = this.$parent.appendDiv('calendar'); |
| |
| var layout = new CalendarLayout(this); |
| this.htmlComp = HtmlComponent.install(this.$container, this.session); |
| this.htmlComp.setLayout(layout); |
| |
| // main elements |
| this.$header = this.$container.appendDiv('calendar-header'); |
| this.$headerRow1 = this.$header.appendDiv('calendar-header-row first'); |
| this.$headerRow2 = this.$header.appendDiv('calendar-header-row last'); |
| this._yearPanel.render(); |
| |
| this.$grids = this.$container.appendDiv('calendar-grids'); |
| this.$topGrid = this.$grids.appendDiv('calendar-top-grid'); |
| this.$grid = this.$grids.appendDiv('calendar-grid'); |
| |
| this.$list = this.$container.appendDiv('calendar-list-container').appendDiv('calendar-list'); |
| 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').click(this._onPreviousClick.bind(this)); |
| this.$range.appendDiv('calendar-today', this.session.text('ui.CalendarToday')).click(this._onTodayClick.bind(this)); |
| this.$range.appendDiv('calendar-next').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).click(this._onDisplayModeClick.bind(this)); |
| this.$commands.appendDiv('calendar-mode', this.session.text('ui.CalendarWorkWeek')).attr('data-mode', Calendar.DisplayMode.WORK_WEEK).click(this._onDisplayModeClick.bind(this)); |
| this.$commands.appendDiv('calendar-mode', this.session.text('ui.CalendarWeek')).attr('data-mode', Calendar.DisplayMode.WEEK).click(this._onDisplayModeClick.bind(this)); |
| this.$commands.appendDiv('calendar-mode last', this.session.text('ui.CalendarMonth')).attr('data-mode', Calendar.DisplayMode.MONTH).click(this._onDisplayModeClick.bind(this)); |
| this.modesMenu.render(this.$commands); |
| this.$commands.appendDiv('calendar-toggle-year').click(this._onYearClick.bind(this)); |
| this.$commands.appendDiv('calendar-toggle-list').click(this._onListClick.bind(this)); |
| |
| // Append the top grid (day/week views) |
| var $weekHeader = this.$topGrid.appendDiv('calendar-week-header'); |
| $weekHeader.appendDiv('calendar-week-name'); |
| for (var dayTop = 0; dayTop < 7; dayTop++) { |
| $weekHeader.appendDiv('calendar-day-name') |
| .data('day', dayTop); |
| } |
| |
| this.$topGrid.appendDiv('calendar-week-task').attr('data-axis-name', this.session.text('ui.CalendarDay')); |
| var $weekTopGridDays = this.$topGrid.appendDiv('calendar-week-allday-container'); |
| $weekTopGridDays.appendDiv('calendar-week-name'); |
| |
| var dayContextMenuCallback = this._onDayContextMenu.bind(this); |
| for (var dayBottom = 0; dayBottom < 7; dayBottom++) { |
| $weekTopGridDays.appendDiv('calendar-day') |
| .addClass('calendar-scrollable-components') |
| .data('day', dayBottom) |
| .on('contextmenu', dayContextMenuCallback); |
| } |
| |
| for (var w = 1; w < 7; w++) { |
| var $w = this.$grid.appendDiv('calendar-week'); |
| for (var d = 0; d < 8; d++) { |
| var $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); |
| } |
| } |
| } |
| |
| // click event on all day and children elements |
| var mousedownCallbackWithTime = this._onDayMouseDown.bind(this, true); |
| this.$grid.find('.calendar-day').on('mousedown', mousedownCallbackWithTime); |
| var mousedownCallback = this._onDayMouseDown.bind(this, false); |
| this.$topGrid.find('.calendar-day').on('mousedown', mousedownCallback); |
| |
| this._updateScreen(false, false); |
| } |
| |
| _renderProperties() { |
| super._renderProperties(); |
| this._renderComponents(); |
| this._renderSelectedComponent(); |
| this._renderLoadInProgress(); |
| this._renderDisplayMode(); |
| } |
| |
| _renderComponents() { |
| this.components.sort(this._sortFromTo); |
| this.components.forEach(component => component.remove()); |
| this.components.forEach(component => component.render()); |
| this._arrangeComponents(); |
| this._updateListPanel(); |
| } |
| |
| _renderSelectedComponent() { |
| if (this.selectedComponent) { |
| this.selectedComponent.setSelected(true); |
| } |
| } |
| |
| _renderLoadInProgress() { |
| this.$progress.setVisible(this.loadInProgress); |
| } |
| |
| updateScrollPosition(animate) { |
| 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); |
| } |
| } |
| |
| _updateScrollPosition(scrollToInitialTime, animate) { |
| 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 { |
| var date = dates.parseJsonDate(this.selectedComponent.fromDate, this.selectedComponent); |
| var topPercent = this._dayPosition(date.getHours(), date.getMinutes()) / 100; |
| var topPos = this.heightPerDay * topPercent; |
| scrollbars.scrollTop(this.$grid, topPos - this.spaceBeforeScrollTop, { |
| animate: animate |
| }); |
| } |
| } else if (scrollToInitialTime) { |
| this._scrollToInitialTime(animate); |
| } |
| } |
| } |
| |
| _scrollToSelectedComponent(animate) { |
| 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 |
| }); |
| } |
| } |
| |
| _scrollToInitialTime(animate) { |
| this.needsScrollToStartHour = false; |
| if (!this._isMonth()) { |
| if (this.selectedComponent && !this.selectedComponent.fullDay) { |
| var date = dates.parseJsonDate(this.selectedComponent.fromDate); |
| var topPercent = this._dayPosition(date.getHours(), date.getMinutes()) / 100; |
| var topPos = this.heightPerDay * topPercent; |
| scrollbars.scrollTop(this.$grid, topPos - this.spaceBeforeScrollTop, { |
| animate: animate |
| }); |
| } else { |
| var scrollTargetTop = this.heightPerHour * this.startHour; |
| scrollbars.scrollTop(this.$grid, scrollTargetTop - this.spaceBeforeScrollTop, { |
| animate: animate |
| }); |
| } |
| } |
| } |
| |
| /* -- basics, events -------------------------------------------- */ |
| |
| _onPreviousClick(event) { |
| this._navigateDate(Calendar.Direction.BACKWARD); |
| } |
| |
| _onNextClick(event) { |
| this._navigateDate(Calendar.Direction.FORWARD); |
| } |
| |
| _dateParts(date, modulo) { |
| var parts = { |
| year: date.getFullYear(), |
| month: date.getMonth(), |
| date: date.getDate(), |
| day: date.getDay() |
| }; |
| if (modulo) { |
| parts.day = (date.getDay() + 6) % 7; |
| } |
| return parts; |
| } |
| |
| _navigateDate(direction) { |
| this.selectedDate = this._calcSelectedDate(direction); |
| this._updateModel(true, false); |
| } |
| |
| _calcSelectedDate(direction) { |
| var 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); |
| } else if (this._isWeek() || this._isWorkWeek()) { |
| return new Date(p.year, p.month, p.date + weekOperand); |
| } else if (this._isMonth()) { |
| return dates.shift(this.selectedDate, 0, monthOperand, 0); |
| } |
| } |
| |
| _updateModel(updateTopGrid, animate) { |
| this._exactRange = this._calcExactRange(); |
| this._yearPanel.setViewRange(this._exactRange); |
| this.viewRange = this._calcViewRange(); |
| this.trigger('modelChange'); |
| this._updateScreen(updateTopGrid, animate); |
| } |
| |
| /** |
| * Calculates exact date range of displayed components based on selected-date. |
| */ |
| _calcExactRange() { |
| var 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 + 1); |
| } 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. |
| */ |
| _calcViewRange() { |
| var viewFrom = calcViewFromDate(this._exactRange.from), |
| viewTo = calcViewToDate(viewFrom); |
| return new DateRange(viewFrom, viewTo); |
| |
| function calcViewFromDate(fromDate) { |
| var 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) { |
| var i, tmpDate = new Date(fromDate.valueOf()); |
| for (i = 0; i < 42; i++) { |
| tmpDate.setDate(tmpDate.getDate() + 1); |
| } |
| return tmpDate; |
| } |
| } |
| |
| _onTodayClick(event) { |
| this.selectedDate = new Date(); |
| this._updateModel(true, false); |
| } |
| |
| _onDisplayModeClick(event) { |
| var displayMode = $(event.target).data('mode'); |
| this.setDisplayMode(displayMode); |
| } |
| |
| _onYearClick(event) { |
| this._showYearPanel = !this._showYearPanel; |
| this._updateScreen(true, true); |
| } |
| |
| _onListClick(event) { |
| this._showListPanel = !this._showListPanel; |
| this._updateScreen(false, true); |
| } |
| |
| _onDayMouseDown(withTime, event) { |
| var selectedDate = new Date($(event.delegateTarget).data('date')); |
| if (withTime && (this._isDay() || this._isWeek() || this._isWorkWeek())) { |
| var seconds = Math.floor(event.originalEvent.layerY / this.heightPerDivision) / this.numberOfHourDivisions * 60 * 60; |
| if (seconds < 60 * 60 * 24) { |
| selectedDate.setSeconds(seconds); |
| } |
| } |
| this._setSelection(selectedDate, null, false); |
| } |
| |
| /** |
| * @param selectedDate |
| * @param selectedComponent may be null when a day is selected |
| */ |
| _setSelection(selectedDate, selectedComponent, updateScrollPosition) { |
| var changed = false; |
| |
| // selected date |
| if (dates.compareDays(this.selectedDate, selectedDate) !== 0) { |
| changed = true; |
| $('.calendar-day', this.$container).each(function(index, element) { |
| var $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 |
| } |
| }.bind(this)); |
| this.selectedDate = selectedDate; |
| } |
| |
| // selected component / part (may be 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'); |
| this._updateListPanel(); |
| if (updateScrollPosition) { |
| this._updateScrollPosition(false, true); |
| } |
| } |
| |
| if (this._showYearPanel) { |
| this._yearPanel.selectDate(this.selectedDate); |
| } |
| } |
| |
| /* -- set display mode and range ------------------------------------- */ |
| |
| _updateScreen(updateTopGrid, animate) { |
| $.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._showYearPanel) { |
| this._yearPanel.selectDate(this.selectedDate); |
| } |
| |
| this._updateListPanel(); |
| this._updateScrollbars(this.$grid, animate); |
| if (updateTopGrid && !this._isMonth()) { |
| this._updateTopGrid(); |
| } |
| } |
| |
| layoutSize(animate) { |
| // reset animation sizes |
| $('div', this.$container).removeData(['new-width', 'new-height']); |
| |
| if (this._isMonth()) { |
| this.$topGrid.addClass('calendar-top-grid-short'); |
| this.$grid.removeClass('calendar-grid-short'); |
| } else { |
| this.$topGrid.removeClass('calendar-top-grid-short'); |
| this.$grid.addClass('calendar-grid-short'); |
| } |
| |
| // init vars (Selected: Day) |
| var $selected = $('.selected', this.$grid), |
| $topSelected = $('.selected', this.$topGrid), |
| containerW = this.$container.width(), |
| gridH = this.$grid.height(), |
| gridW = containerW - 20; // containerW - @root-group-box-padding-right |
| |
| // show or hide year |
| $('.calendar-toggle-year', this.$commands).select(this._showYearPanel); |
| if (this._showYearPanel) { |
| this._yearPanel.$container.data('new-width', this.calendarToggleYearWidth); |
| gridW -= this.calendarToggleYearWidth; |
| containerW -= this.calendarToggleYearWidth; |
| } else { |
| this._yearPanel.$container.data('new-width', 0); |
| } |
| |
| // 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); |
| |
| var $weeksToHide = $(); // Empty |
| var $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 |
| var 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 |
| var 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); |
| var $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 |
| var 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); |
| } else if (this._isWorkWeek()) { |
| this.$topGrid.find('.calendar-day-name').data('new-width', 0); |
| this.$grids.find('.calendar-day').data('new-width', 0); |
| var newWidthWorkWeek = Math.round(contentW / this.workDayIndices.length); |
| $('.calendar-day-name:nth-child(-n+6), ' + |
| '.calendar-day:nth-child(-n+6)', this.$topGrid) |
| .data('new-width', newWidthWorkWeek); |
| $('.calendar-day:nth-child(-n+6)', this.$grid) |
| .data('new-width', newWidthWorkWeek); |
| } else if (this._isMonth() || this._isWeek()) { |
| var newWidthMonthOrWeek = Math.round(contentW / 7); |
| this.$grids.find('.calendar-day').data('new-width', newWidthMonthOrWeek); |
| this.$topGrid.find('.calendar-day-name').data('new-width', newWidthMonthOrWeek); |
| } |
| |
| // layout components |
| if (this._isMonth()) { |
| $('.component-month', this.$grid).each(function() { |
| var $comp = $(this), |
| $day = $comp.closest('.calendar-day'); |
| $comp.toggleClass('compact', $day.data('new-width') < CalendarComponent.MONTH_COMPACT_THRESHOLD); |
| }); |
| } |
| |
| var afterLayoutCallback = this._afterLayout.bind(this); |
| |
| // animate old to new sizes |
| $('div', this.$container).each(function() { |
| var $e = $(this), |
| w = $e.data('new-width'), |
| h = $e.data('new-height'); |
| $e.stop(false, true); |
| |
| if (w !== undefined && w !== $e.outerWidth()) { |
| if (animate) { |
| $e.animateAVCSD('width', w, afterLayoutCallback.bind(this, $e, animate)); |
| } else { |
| $e.css('width', w); |
| afterLayoutCallback($e, animate); |
| } |
| } |
| if (h !== undefined && h !== $e.outerHeight()) { |
| if (h > 0) { |
| $e.removeClass('hidden'); |
| } |
| if (animate) { |
| $e.animateAVCSD('height', h, function() { |
| if (h === 0) { |
| $e.addClass('hidden'); |
| } |
| afterLayoutCallback($e, animate); |
| }); |
| } else { |
| $e.css('height', h); |
| if (h === 0) { |
| $e.addClass('hidden'); |
| } |
| afterLayoutCallback($e, animate); |
| } |
| } |
| }); |
| } |
| |
| _afterLayout($parent, animate) { |
| this._updateScrollbars($parent, animate); |
| this._updateWeekdayNames(); |
| } |
| |
| _updateWeekdayNames() { |
| // set day-name (based on width of shown column) |
| var 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', this.$topGrid).each(function(index) { |
| $(this).attr('data-day-name', weekdays[index]); |
| }); |
| } |
| |
| _updateScrollbars($parent, animate) { |
| var $scrollables = $('.calendar-scrollable-components', $parent); |
| $scrollables.each(function() { |
| var $scrollable = $(this); |
| scrollbars.update($scrollable, true); |
| }); |
| this.updateScrollPosition(animate); |
| } |
| |
| _updateTopGrid() { |
| $('.calendar-component', this.$topGrid).each(function($index, part) { |
| $(part).data('component').remove(); |
| }); |
| var allDayComponents = this.components.filter(component => component.fullDay); |
| // first remove all components and add them from scratch |
| allDayComponents.forEach(component => component.remove()); |
| allDayComponents.forEach(component => component.render()); |
| this._updateScrollbars(this.$topGrid, false); |
| } |
| |
| layoutYearPanel() { |
| if (this._showYearPanel) { |
| scrollbars.update(this._yearPanel.$yearList); |
| this._yearPanel._scrollYear(); |
| } |
| } |
| |
| layoutLabel() { |
| var text, $dates, $topGridDates, |
| exFrom = this._exactRange.from, |
| exTo = this._exactRange.to; |
| |
| // set range text |
| if (this._isDay()) { |
| text = this._format(exFrom, 'd. MMMM yyyy'); |
| } else if (this._isWorkWeek() || this._isWeek()) { |
| var 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); |
| |
| var 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' : ''; |
| } |
| if (dates.isSameDay(date, new Date())) { |
| cssClass += ' now'; |
| } |
| if (dates.isSameDay(date, this.selectedDate)) { |
| cssClass += ' selected'; |
| } |
| if (!this._isMonth()) { |
| cssClass += ' calendar-no-label'; // If we're not in the month view, number is shown on top |
| } |
| |
| // adjust position for days between 10 and 19 (because "1" is narrower than "0" or "2") |
| if (date.getDate() > 9 && date.getDate() < 20) { |
| cssClass += ' center-nice'; |
| } |
| |
| text = this._format(date, 'dd'); |
| $dates.eq(w * 7 + d) |
| .removeClass('weekend-out weekend out selected now calendar-no-label') |
| .addClass(cssClass) |
| .attr('data-day-name', text) |
| .data('date', new Date(date.valueOf())); |
| date.setDate(date.getDate() + 1); |
| } |
| } |
| |
| // Top grid: loop days of one calendar week and set value and class |
| if (!this._isMonth()) { |
| $topGridDates = $('.calendar-day', this.$topGrid); |
| // From the view range, find the week we are in |
| var exactDate = new Date(this._exactRange.from.valueOf()); |
| |
| // Find first day of week. |
| date = dates.firstDayOfWeek(exactDate, 1); |
| |
| 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' : ''; |
| } |
| if (dates.isSameDay(date, new Date())) { |
| cssClass += ' now'; |
| } |
| if (dates.isSameDay(date, this.selectedDate)) { |
| cssClass += ' selected'; |
| } |
| |
| text = this._format(date, 'dd'); |
| $topGridDates.eq(d) |
| .removeClass('weekend-out weekend out selected now') |
| .addClass(cssClass) |
| .attr('data-day-name', text) |
| .data('date', new Date(date.valueOf())); |
| |
| date.setDate(date.getDate() + 1); |
| } |
| } |
| |
| } |
| |
| layoutAxis() { |
| var $e; |
| |
| // remove old axis |
| $('.calendar-week-axis, .calendar-week-task', this.$grid).remove(); |
| |
| // set weekname |
| var session = this.session; |
| |
| $('.calendar-week-name', this.$container).each(function(index) { |
| if (index > 0) { |
| $e = $(this); |
| $e.text(session.text('ui.CW', dates.weekInYear($e.next().data('date')))); |
| } |
| }); |
| |
| // day schedule |
| if (!this._isMonth()) { |
| // Parent of selected day: Week |
| // var $parent = $selected.parent(); |
| var $parent = $('.calendar-week', this.$grid); |
| |
| for (var h = 0; h < 24; h++) { // Render lines for each hour |
| var paddedHour = ('00' + h).slice(-2); |
| var topPos = h * this.heightPerHour; |
| $parent.appendDiv('calendar-week-axis hour' + (h === 0 ? ' first' : '')).attr('data-axis-name', paddedHour + ':00').css('top', topPos + 'px'); |
| |
| for (var m = 1; m < this.numberOfHourDivisions; m++) { // First one rendered above. Start at the next |
| topPos += this.heightPerDivision; |
| $parent.appendDiv('calendar-week-axis').attr('data-axis-name', '').css('top', topPos + 'px'); |
| } |
| } |
| } |
| } |
| |
| /* -- year events ---------------------------------------- */ |
| |
| _onYearPanelDateSelect(event) { |
| this.selectedDate = event.date; |
| this._updateModel(true, false); |
| } |
| |
| _updateListPanel() { |
| if (this._showListPanel) { |
| |
| // remove old list-components |
| this._listComponents.forEach(function(listComponent) { |
| listComponent.remove(); |
| }); |
| |
| this._listComponents = []; |
| this._renderListPanel(); |
| } |
| } |
| |
| _remove() { |
| var $days = $('.calendar-day', this.$grid); |
| |
| // Ensure that scrollbars are unregistered |
| for (var k = 0; k < $days.length; k++) { |
| var $day = $days.eq(k); |
| var $scrollableContainer = $day.children('.calendar-scrollable-components'); |
| |
| if ($scrollableContainer.length > 0) { |
| scrollbars.uninstall($scrollableContainer, this.session); |
| $scrollableContainer.remove(); |
| } |
| } |
| |
| super._remove(); |
| } |
| |
| /** |
| * Renders the panel on the left, showing all components of the selected date. |
| */ |
| _renderListPanel() { |
| var listComponent, components = []; |
| |
| // set title |
| this.$listTitle.text(this._format(this.selectedDate, 'd. MMMM yyyy')); |
| |
| // find components to display on the list panel |
| this.components.forEach(function(component) { |
| if (belongsToSelectedDate.call(this, component)) { |
| components.push(component); |
| } |
| }.bind(this)); |
| |
| function belongsToSelectedDate(component) { |
| var selectedDate = dates.trunc(this.selectedDate); |
| return dates.compare(selectedDate, component.coveredDaysRange.from) >= 0 && |
| dates.compare(selectedDate, component.coveredDaysRange.to) <= 0; |
| } |
| |
| components.forEach(function(component) { |
| listComponent = new CalendarListComponent(this.selectedDate, component); |
| listComponent.render(this.$list); |
| this._listComponents.push(listComponent); |
| }.bind(this)); |
| } |
| |
| /* -- components, events-------------------------------------------- */ |
| |
| _selectedComponentChanged(component, partDay, updateScrollPosition) { |
| this._setSelection(partDay, component, updateScrollPosition); |
| } |
| |
| _onDayContextMenu(event) { |
| this._showContextMenu(event, 'Calendar.EmptySpace'); |
| } |
| |
| _showContextMenu(event, allowedType) { |
| event.preventDefault(); |
| event.stopPropagation(); |
| |
| var func = function func(event, allowedType) { |
| if (!this.rendered || !this.attached) { // check needed because function is called asynchronously |
| return; |
| } |
| var filteredMenus = menus.filter(this.menus, [allowedType], true), |
| $part = $(event.currentTarget); |
| if (filteredMenus.length === 0) { |
| return; |
| } |
| var popup = scout.create('ContextMenuPopup', { |
| parent: this, |
| menuItems: filteredMenus, |
| location: { |
| x: event.pageX, |
| y: event.pageY |
| }, |
| $anchor: $part |
| }); |
| popup.open(); |
| }.bind(this); |
| |
| this.session.onRequestsDone(func, event, allowedType); |
| } |
| |
| /* -- components, arrangement------------------------------------ */ |
| |
| _arrangeComponents() { |
| var k, j, $day, $allChildren, $children, $scrollableContainer, dayComponents, day; |
| |
| var $days = $('.calendar-day', this.$grid); |
| // Main (Bottom) grid: Iterate over days |
| for (k = 0; k < $days.length; k++) { |
| $day = $days.eq(k); |
| $children = $day.children('.calendar-component:not(.component-task)'); |
| $allChildren = $day.children('.calendar-component'); |
| day = $day.data('date'); |
| |
| // Remove old element containers |
| $scrollableContainer = $day.children('.calendar-scrollable-components'); |
| if ($scrollableContainer.length > 0) { |
| scrollbars.uninstall($scrollableContainer, this.session); |
| $scrollableContainer.remove(); |
| } |
| |
| if (this._isMonth() && $allChildren.length > 0) { |
| $scrollableContainer = $day.appendDiv('calendar-scrollable-components'); |
| |
| for (j = 0; j < $allChildren.length; j++) { |
| var $child = $allChildren.eq(j); |
| // non-tasks (communications) are distributed manually |
| // within the parent container in all views except the monthly view. |
| if (!this._isMonth() && !$child.hasClass('component-task')) { |
| continue; |
| } |
| $scrollableContainer.append($child); |
| } |
| |
| scrollbars.install($scrollableContainer, { |
| parent: this, |
| session: this.session, |
| axis: 'y' |
| }); |
| } |
| |
| if (this._isMonth() && $children.length > 2) { |
| $day.addClass('many-items'); |
| } else if (!this._isMonth() && $children.length > 1) { |
| // logical placement |
| dayComponents = this._getComponents($children); |
| this._arrange(dayComponents, day); |
| |
| // screen placement |
| this._arrangeComponentSetPlacement($children, day); |
| } |
| } |
| |
| if (this._isMonth()) { |
| this._uninstallScrollbars(); |
| this.$grid.removeClass('calendar-scrollable-components'); |
| } else { |
| this.$grid.addClass('calendar-scrollable-components'); |
| // If we're in the non-month views, the time can scroll. Add scrollbars |
| this._installScrollbars({ |
| parent: this, |
| session: this.session, |
| axis: 'y' |
| }); |
| |
| var $topDays = $('.calendar-scrollable-components', this.$topGrid); |
| for (k = 0; k < $topDays.length; k++) { |
| var $topDay = $topDays.eq(k); |
| scrollbars.install($topDay, { |
| parent: this, |
| session: this.session, |
| axis: 'y' |
| }); |
| } |
| } |
| } |
| |
| _getComponents($children) { |
| var i, $child; |
| var components = []; |
| for (i = 0; i < $children.length; i++) { |
| $child = $children.eq(i); |
| components.push($child.data('component')); |
| } |
| return components; |
| } |
| |
| _sort(components) { |
| components.sort(this._sortFromTo); |
| } |
| |
| /** |
| * Arrange components (stack width, stack index) per day |
| * */ |
| _arrange(components, day) { |
| var i, j, c, r, k, |
| columns = []; |
| |
| // ordered by from, to |
| this._sort(components); |
| |
| // clear existing placement |
| for (i = 0; i < components.length; i++) { |
| c = components[i]; |
| if (!c.stack) { |
| c.stack = {}; |
| } |
| c.stack[day] = {}; |
| } |
| |
| for (i = 0; i < components.length; i++) { |
| c = components[i]; |
| r = c.getPartDayPosition(day); // Range [from,to] |
| |
| // reduce number of columns, if all components end before this one |
| if (columns.length > 0 && this._allEndBefore(columns, r.from, day)) { |
| columns = []; |
| } |
| |
| // replace an component that ends before and can be replaced |
| k = this._findReplacableColumn(columns, r.from, day); |
| |
| // insert |
| if (k >= 0) { |
| columns[k] = c; |
| c.stack[day].x = k; |
| } else { |
| columns.push(c); |
| c.stack[day].x = columns.length - 1; |
| } |
| |
| // update stackW |
| for (j = 0; j < columns.length; j++) { |
| columns[j].stack[day].w = columns.length; |
| } |
| } |
| } |
| |
| _allEndBefore(columns, pos, day) { |
| var i; |
| for (i = 0; i < columns.length; i++) { |
| if (!this._endsBefore(columns[i], pos, day)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| _findReplacableColumn(columns, pos, day) { |
| var j; |
| for (j = 0; j < columns.length; j++) { |
| if (this._endsBefore(columns[j], pos, day)) { |
| return j; |
| } |
| } |
| return -1; |
| } |
| |
| _endsBefore(component, pos, day) { |
| return component.getPartDayPosition(day).to <= pos; |
| } |
| |
| _arrangeComponentSetPlacement($children, day) { |
| var i, $child, stack; |
| |
| // loop and place based on data |
| for (i = 0; i < $children.length; i++) { |
| $child = $children.eq(i); |
| stack = $child.data('component').stack[day]; |
| |
| // make last element smaller |
| $child |
| .css('width', 100 / stack.w + '%') |
| .css('left', stack.x * 100 / stack.w + '%'); |
| } |
| } |
| |
| get$Scrollable() { |
| return this.$grid; |
| } |
| |
| /* -- helper ---------------------------------------------------- */ |
| |
| _dayPosition(hour, minutes) { |
| // Height position in percent of total calendar |
| |
| var pos; |
| if (hour < 0) { |
| pos = 0; // All day event |
| } else { |
| pos = 100 / (24 * 60) * (hour * 60 + minutes); |
| } |
| return Math.round(pos * 100) / 100; |
| } |
| |
| _hourToNumber(hour) { |
| var splits = hour.split(':'); |
| return parseFloat(splits[0]) + parseFloat(splits[1]) / 60; |
| } |
| |
| _format(date, pattern) { |
| return dates.format(date, this.session.locale, pattern); |
| } |
| |
| _sortFromTo(c1, c2) { |
| var from1 = dates.parseJsonDate(c1.fromDate); |
| var from2 = dates.parseJsonDate(c2.fromDate); |
| var diffFrom = dates.compare(from1, from2); |
| if (diffFrom !== 0) { |
| return diffFrom; |
| } |
| var to1 = dates.parseJsonDate(c1.toDate); |
| var to2 = dates.parseJsonDate(c2.toDate); |
| var diffTo = dates.compare(to1, to2); |
| if (diffTo !== 0) { |
| return diffTo; |
| } |
| var s1 = c1.item && c1.item.subject ? c1.item.subject : ''; |
| var s2 = c2.item && c2.item.subject ? c2.item.subject : ''; |
| return s1.localeCompare(s2); |
| } |
| } |