import { LitElement, html, css, nothing } from 'lit';
import { property, customElement } from 'lit/decorators.js';
import { toDate } from 'date-fns-tz'
import { Resolution } from './model/Resolution';
import { Weekday } from './model/Weekday';
import { BlockRowData } from './model/blockRowData';
import { ILocaleDataProvider } from './model/ILocaleDataProvider';
import { IResolutionBlockFiller } from './model/IResolutionBlockFiller';
import { IResolutionBlockRegisterer } from './model/IResolutionBlockRegisterer';
import * as DateUtil from './util/dateTimeUtil';
import { DateTimeConstants } from './util/dateTimeUtil';
import * as ElementUtil from './util/elementUtil';
import { DefaultLocaleDataProvider } from './model/DefaultLocaleDataProvider';
import { getISOWeek } from 'date-fns';
import { query } from 'lit-element/decorators.js';
/**
* Scalable timeline web component that supports more than one
* resolutions ({@link Resolution}). When timeline element doesn't overflow
* horizontally in it's parent element, it scales the content width up to fit in
* the space available.
*
* When this component scales up, all widths are calculated as percentages.
* Pixel widths are used otherwise. Some browsers may not support percentages
* accurately enough, and for those it's best to call
* {@link #setAlwaysCalculatePixelWidths(boolean)} with 'true' to disable
* percentage values.
*
* There's always a minimum width calculated and updated to the timeline
* element. Percentage values set some limitation for the component's width.
* Wider the component (> 4000px), bigger the chance to get year, month and
* date blocks not being vertically in-line with each others.
*
* Supports setting a scroll left position.
*
* After construction, attach the component to it's parent and call update
* method with a required parameters and the timeline is ready. After that, all
* widths are calculated and all other API methods available can be used safely.
*
*/
@customElement('timeline-element')
export class TimelineElement extends LitElement {
private static readonly STYLE_ROW: string = "row";
private static readonly STYLE_COL: string = "col";
private static readonly STYLE_MONTH: string = "month";
private static readonly STYLE_YEAR: string = "year";
private static readonly STYLE_DAY: string = "day";
private static readonly STYLE_WEEK: string = "w";
private static readonly STYLE_RESOLUTION: string = "resolution";
private static readonly STYLE_EVEN: string = "even";
private static readonly STYLE_WEEKEND: string = "weekend";
private static readonly STYLE_SPACER: string = "spacer";
private static readonly STYLE_FIRST: string = "f-col";
private static readonly STYLE_CENTER: string = "c-col";
private static readonly STYLE_LAST: string = "l-col";
private static readonly STYLE_MEASURE: string = "measure";
private readonly resolutionWeekDayblockWidth: number = 4;
@property({
reflect: true,
converter: {
fromAttribute: (value: string, type) => {
return Resolution[value];
},
toAttribute: (value: Resolution, type) => {
return Resolution[value];
}
}
})
public resolution: Resolution = Resolution.Day;
/* Inclusive start Date (hour accuracy). E.g. "2020-04-01" or "2020-04-01T00" for Hour resolution.*/
@property({
reflect: true,
converter: {
fromAttribute: (value: string, type) => {
return (value) ? (value.length > 13) ? value.substring(0, 13) : value : value;
},
}
})
public startDateTime: string;
/* Inclusive end Date (hour accuracy). E.g. "2020-04-01" or "2020-04-01T00" for Hour resolution. */
@property({
reflect: true,
converter: {
fromAttribute: (value: string, type) => {
return (value) ? (value.length > 13) ? value.substring(0, 13) : value : value;
},
}
})
public endDateTime: string;
/** Start Date of the timeline. Native Date object can be in "wrong" time zone, as it matches browser's time zone. */
public internalInclusiveStartDateTime: Date;
/** End Date of the timeline. */
public internalInclusiveEndDateTime: Date; // internalEndDateTime may be adjusted for resolution
/** Timezone for date and time formatting. Doesn't match with actual start and end Date object's time zone. */
@property({ reflect: true})
public timeZone: string = "Europe/London";
@property({ reflect: true})
public locale: string = "en-US";
@property({ reflect: true}) firstDayOfWeek: number = 1; // sunday;
@property({ reflect: true, type: Boolean }) twelveHourClock: boolean = false;
@property() minWidth: number;
@property() normalStartDate: Date;
@property() normalEndDate: Date;
@property() lastDayOfWeek: number;
/* First day of the whole range. Allowed values are 1-7. 1 is Sunday. Required with {@link Resolution#Week} and weekend tracking. */
@property() firstDayOfRange: number;
/* First hour of the range. Allowed values are 0-23. Required with {@link Resolution#Hour}. */
@property() firstHourOfRange: number;
@property({ reflect: true }) scrollContainerId: string;
@property() monthRowVisible: boolean = true;
@property() yearRowVisible: boolean = true;
@property() monthNames: string[];
@property() weekdayNames: string[];
private localeDataProvider: ILocaleDataProvider;
/*
* number of blocks in resolution range. Days for Day/Week resolution, Hours
* for hour resolution..
*/
private blocksInRange: number = 0;
/*
* number of elements in resolution range. Same as blocksInRange for
* Day/Hour resolution. blocksInRange / 7 for Week resolution.
*/
private resolutionBlockCount: number = 0;
private firstResBlockCount: number = 0;
private lastResBlockCount: number = 0;
private firstDay: boolean;
private timelineOverflowingHorizontally: boolean;
private monthFormat: string;
private yearFormat: string;
private weekFormat: string;
private dayFormat: string;
/*
* resolutionDiv contains the resolution specific elements that represents a
* timeline's sub-parts like hour, day or week.
*/
@query('#resolutionDiv')
public resolutionDiv: HTMLDivElement;
private resSpacerDiv: HTMLDivElement;
private spacerBlocks: HTMLDivElement[] = [];
private yearRowData: BlockRowData = new BlockRowData();
private monthRowData: BlockRowData = new BlockRowData();
// days/daysLength are needed only with resolutions smaller than Day.
private dayRowData: BlockRowData = new BlockRowData();
/*
* Currently active widths. Updated each time when timeline column widths
* are updated.
*/
private dayWidthPercentage: number = 0;
private dayOrHourWidthPx: number = 0;
private resBlockMinWidthPx: number = 0;
private resBlockWidthPx: number = 0;
private resBlockWidthPercentage: number = 0;
private minResolutionWidth: number = -1;
private calcPixels: boolean = false;
private positionLeft: number = 0;
private setPositionForEachBlock: boolean = false;
private firstWeekBlockHidden: boolean = false;
private ie: boolean = false; // deprecated property
private lazyResolutionPaint: any;
/* directlyInsideScrollContainer:
true: timeline element is a child element inside a container with scroll bar.
false: timeline.style.left is adjusted by scrollHandler. */
private directlyInsideScrollContainer: boolean = true;
private scrollHandler: any;
private scrollContainer: HTMLElement | Window;
private previousContainerScrollLeft: number = 0;
private previousContainerScrollTop: number = 0;
connectedCallback() {
super.connectedCallback();
if (this.scrollContainer && this.scrollHandler) {
this.scrollContainer.addEventListener('scroll', this.scrollHandler);
}
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.scrollContainer && this.scrollHandler) {
this.scrollContainer.removeEventListener('scroll', this.scrollHandler);
}
}
static get styles() {
return css`
:host {
display: block;
overflow: hidden;
position: relative;
--no-user-select: {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
:host([hidden]) {
display: none;
}
.year,
.month,
.day {
padding-left: 2px;
text-overflow: ellipsis;
white-space: nowrap;
border-right: 1px solid #A9A9A9;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
.year.spacer,
.month.spacer,
.day.spacer {
padding-left: 0px;
}
.month:nth-of-type(even),
.day:nth-of-type(even) {
background-color: #ddd;
}
.col.even {
background-color: #ccc;
}
.col {
position: var(--timeline-col-position);
left: var(--timeline-col-left);
height: 100%;
float: left;
overflow: hidden;
border-right: 1px solid #A9A9A9;
background-color: var(--timeline-col-background-color, #ddd);
font-size: var(--timeline-col-font-size, 10px);
text-align: center;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
-webkit-touch-callout: none;
@apply --no-user-select;
}
.c-col {
width: var(--timeline-col-center-width);
}
.f-col {
width: var(--timeline-col-first-width);
}
.l-col {
width: var(--timeline-col-last-width);
}
.col.w {
text-align: left;
}
.col.weekend {
background-color: var(--timeline-col-weekend, #ccc);
}
.col.measure {
// Change min-width to adjust grid's cell width with day and hour-resolution.
//min-width: 40px;
}
.col.w.measure {
// Change min-width to adjust grid's cell width with week-resolution.
//min-width: 70px;
}
.row {
width: 100%;
float: left;
overflow: hidden;
height: var(--timeline-row-height, 15px);
font-size: var(--timeline-row-font-size, 10px);
background-color: var(--timeline-row-background, #d0d0d0);
-ms-flex-pack: justify;
-webkit-touch-callout: none;
@apply --no-user-select;
}
`;
}
render() {
return html`
${this.yearBlocks()}
${this.monthBlocks()}
${this.dayBlocks()}
`;
}
yearBlocks() {
if (this.yearRowVisible) {
return this.timelineBlocks(this.yearRowData, TimelineElement.STYLE_YEAR);
}
return nothing;
}
monthBlocks() {
if (this.monthRowVisible) {
return this.timelineBlocks(this.monthRowData, TimelineElement.STYLE_MONTH);
}
return nothing;
}
dayBlocks() {
if (this.isDayRowVisible) {
return this.timelineBlocks(this.dayRowData, TimelineElement.STYLE_DAY);
}
return nothing;
}
shouldUpdate(changedProperties: any) {
return changedProperties.has('resolution')
|| changedProperties.has('startDateTime')
|| changedProperties.has('endDateTime')
|| changedProperties.has('locale')
|| changedProperties.has('timeZone')
|| changedProperties.has('firstDayOfWeek')
|| changedProperties.has('twelveHourClock')
|| changedProperties.has('yearRowVisible')
|| changedProperties.has('monthRowVisible')
|| changedProperties.has('monthNames')
|| changedProperties.has('weekdayNames')
;
}
willUpdate(changedProps: any) {
if (changedProps.has('resolution')) {
this.minResolutionWidth = -1;
}
if(changedProps.has('resolution') || changedProps.has('startDateTime') || changedProps.has('timeZone')) {
this.firstDayOfRange = null;
if(this.resolution === Resolution.Hour) {
this.internalInclusiveStartDateTime = toDate(this.startDateTime, { timeZone: this.timeZone});
} else {
this.internalInclusiveStartDateTime = toDate(this.startDateTime.substring(0, 10) + 'T00:00:00.000', { timeZone: this.timeZone});
}
}
if (changedProps.has('resolution') || changedProps.has('endDateTime') || changedProps.has('timeZone')) {
// given time must be always exact hour in millisecod accuracy 1AM means exactly "01:00:00.000".
if(this.resolution === Resolution.Hour) {
// convert given time to last millisecond in the given hour. 1AM becomes "01:59:59.999" set to internalEndDateTime.
this.internalInclusiveEndDateTime = toDate(this.endDateTime.substring(0, 13) + ':59:59.999', { timeZone: this.timeZone});
} else {
this.internalInclusiveEndDateTime = toDate(this.endDateTime.substring(0, 10) + 'T23:59:59.999', { timeZone: this.timeZone});
}
}
this.updateTimeLine(this.resolution, this.internalInclusiveStartDateTime, this.internalInclusiveEndDateTime, new DefaultLocaleDataProvider(this.locale, this.timeZone, this.firstDayOfWeek, this.twelveHourClock));
}
protected updated(changedProps: any): void {
if(!(this.resolution) || !this.internalInclusiveStartDateTime || !this.internalInclusiveEndDateTime) {
return;
}
if (this.resolution !== Resolution.Day && this.resolution !== Resolution.Week && this.resolution !== Resolution.Hour) {
console.log("TimelineElement resolution " + (this.resolution ? Resolution[this.resolution] : "null")
+ " is not supported");
return;
}
console.log("TimelineElement Constructed content.");
this.updateWidths();
console.log("TimelineElement is updated for resolution " + Resolution[this.resolution] + ".");
this.registerScrollHandler();
}
/**
*
* Updates the content of this component. Builds the time-line and calculates
* width and heights for the content (calls in the end
* {@link #updateWidths()}). This should be called explicitly. Otherwise the
* component will be empty.
*
* Date values should always follow specification in {@link Date#getTime()}.
* Start and end date is always required.
*
* @param resolution
* Resolution enum (not null)
* @param startDate
* Time-line's start date. (inclusive; not null)
* @param endDate
* Time-line's end date. (inclusive; not null)
* @param localeDataProvider
* Data provider for locale specific data. month names, first day
* of week etc.
*
*/
updateTimeLine(resolution: Resolution, startDate: Date, endDate: Date,
localeDataProvider: ILocaleDataProvider) {
if (!localeDataProvider) {
console.log("TimelineElement requires ILocaleDataProvider. Can't complete update(...) operation.");
return;
}
this.clear();
console.log("TimelineElement content cleared.");
if(!(resolution) || !startDate || !endDate) {
return;
}
console.log("TimelineElement Updating content.");
this.localeDataProvider = localeDataProvider;
this.resolution = resolution;
this.resetDateRange(startDate, endDate);
this.lastDayOfWeek = (localeDataProvider.getFirstDayOfWeek() == 1) ? 7 : Math.max((localeDataProvider.getFirstDayOfWeek() - 1) % 8, 1);
this.monthNames = this.monthNames || localeDataProvider.getMonthNames();
this.weekdayNames = this.weekdayNames || localeDataProvider.getWeekdayNames();
if (this.minResolutionWidth < 0) {
this.minResolutionWidth = this.calculateResolutionMinWidth();
}
if (this.resolution === Resolution.Day || this.resolution === Resolution.Week) {
this.prepareTimelineForDayOrWeekResolution(this.internalInclusiveStartDateTime, this.internalInclusiveEndDateTime);
} else if (this.resolution === Resolution.Hour) {
this.prepareTimelineForHourResolution(this.internalInclusiveStartDateTime, this.internalInclusiveEndDateTime);
}
}
resetDateRange(startDate: Date, endDate: Date) {
this.internalInclusiveStartDateTime = startDate;
this.internalInclusiveEndDateTime = endDate;
this.normalStartDate = this.toNormalDate(this.internalInclusiveStartDateTime);
this.normalEndDate = this.toNormalDate(this.internalInclusiveEndDateTime);
// Date#getDay() is zero-based: Sunday = 0, Monday = 1, ...
// this.firstDayOfRange is 1-based (Sunday = 1).
this.firstDayOfRange = this.firstDayOfRange || this.internalInclusiveStartDateTime.getDay() + 1; // TODO is weekday still correct here?
this.firstHourOfRange = this.firstHourOfRange || parseInt(this.localeDataProvider.formatTime(this.internalInclusiveStartDateTime, "HH"));
}
registerScrollHandler() {
if (this.scrollHandler) {
return;
}
let timeline = this;
this.scrollContainer = this.setupScrollContainer();
this.scrollHandler = function (e: any) {
window.requestAnimationFrame(function () {
let container: any = timeline.scrollContainer;
let sl: number = container.scrollLeft || container.scrollX;
let st: number = container.scrollTop || container.scrollY;
if (sl != timeline.previousContainerScrollLeft) {
timeline.setScrollLeft(sl);
timeline.previousContainerScrollLeft = sl;
}
if (st != timeline.previousContainerScrollTop) {
timeline.previousContainerScrollTop = st;
}
});
};
this.scrollContainer.addEventListener('scroll', this.scrollHandler);
}
setupScrollContainer(): HTMLElement | Window {
let scrollContainer;
if(this.scrollContainerId) {
scrollContainer = this.getParentElement(this).querySelector('#' + this.scrollContainerId);
if(!scrollContainer) {
scrollContainer = document.querySelector('#' + this.scrollContainerId);
}
if(scrollContainer) {
(scrollContainer).style.overflowX = "auto";
}
}
if(!scrollContainer) {
scrollContainer = this.getParentElement(this);
this.directlyInsideScrollContainer = true;
if(scrollContainer === document.body) {
return window; // window scrolls by default, not body
}
}
return scrollContainer;
}
clear() {
this.spacerBlocks = [];
this.yearRowData.clear();
this.monthRowData.clear();
this.dayRowData.clear();
}
calculateResolutionMinWidth(): number {
let resDivMeasure: HTMLDivElement = document.createElement('div');
resDivMeasure.classList.add(TimelineElement.STYLE_ROW, TimelineElement.STYLE_RESOLUTION);
let resBlockMeasure: HTMLDivElement = document.createElement('div');
if (this.resolution === Resolution.Week) {
// configurable with '.col.w.measure' selector
resBlockMeasure.classList.add(TimelineElement.STYLE_COL, TimelineElement.STYLE_WEEK, TimelineElement.STYLE_MEASURE);
} else {
// measure for text 'MM'
resBlockMeasure.innerText = "MM";
// configurable with '.col.measure' selector
resBlockMeasure.classList.add(TimelineElement.STYLE_COL, TimelineElement.STYLE_MEASURE);
}
resDivMeasure.appendChild(resBlockMeasure);
this.shadowRoot.appendChild(resDivMeasure);
let width: number = resBlockMeasure.clientWidth;
if (this.resolution === Resolution.Week) {
// divide given width by number of days in week
width = width / DateTimeConstants.DAYS_IN_WEEK;
}
width = (width < this.resolutionWeekDayblockWidth) ? this.resolutionWeekDayblockWidth : width;
this.shadowRoot.removeChild(resDivMeasure);
return width;
}
registerHourResolutionBlock() {
this.blocksInRange++;
this.resolutionBlockCount++;
}
registerDayResolutionBlock() {
this.blocksInRange++;
this.resolutionBlockCount++;
}
registerWeekResolutionBlock(index: number, weekDay: Weekday, lastBlock: boolean, firstWeek: boolean) {
if (index == 0 || weekDay === Weekday.First) {
this.resolutionBlockCount++;
}
if (firstWeek && (weekDay === Weekday.Last || lastBlock)) {
this.firstResBlockCount = index + 1;
} else if (lastBlock) {
this.lastResBlockCount = (index + 1 - this.firstResBlockCount) % 7;
}
this.blocksInRange++;
}
timelineBlocks(rowData: BlockRowData, style: string) {
const itemTemplates = [];
for (let entry of rowData.getBlockEntries()) {
itemTemplates.push(html`${entry[1]}`);
}
if (this.isAlwaysCalculatePixelWidths()) {
itemTemplates.push(html`${this.createSpacerBlock(style)}`);
}
return itemTemplates;
}
/**
* Returns true if Widget is set to calculate widths by itself. Default is
* false.
*
* @return
*/
isAlwaysCalculatePixelWidths(): boolean {
return this.calcPixels;
}
createSpacerBlock(className: string): HTMLDivElement {
let block: HTMLDivElement = document.createElement('div');
block.classList.add(TimelineElement.STYLE_ROW, TimelineElement.STYLE_YEAR, TimelineElement.STYLE_SPACER);
block.innerText = " ";
block.style.display = "none"; // not visible by default
this.spacerBlocks.push(block);
return block;
}
/** Clears Daylight saving time adjustment from the given time. */
toNormalDate(zonedDate: Date): Date {
return DateUtil.toNormalDate(zonedDate, this.localeDataProvider.getDaylightAdjustment(zonedDate));
}
getDSTAdjustedDate(previousIsDST: boolean, zonedDate: Date): Date {
return DateUtil.getDSTAdjustedDate(previousIsDST, zonedDate, this.localeDataProvider.getDaylightAdjustment(zonedDate));
}
getParentElement(node: any): any {
var parent = node.parentNode;
if (!parent || parent.nodeType != 1) {
parent = null;
}
return parent;
}
getDay(date: Date): string {
// by adjusting the date to the middle of the day before formatting is a
// workaround to avoid DST issues with DateTimeFormatter.
let adjusted: Date = DateUtil.adjustToMiddleOfDay(date, this.localeDataProvider.getTimeZone());
return this.localeDataProvider.formatDate(adjusted, "d");
}
getYear(date: Date): string {
return this.localeDataProvider.formatDate(date, "yyyy");
}
getMonth(date: Date): number {
let m: string = this.localeDataProvider.formatDate(date, "M");
return parseInt(m) - 1;
}
isWeekEnd(dayCounter: number): boolean {
return dayCounter == 1 || dayCounter == 7;
}
key(prefix: string, rowData: BlockRowData): string {
return prefix + "_" + (rowData.size());
}
newKey(prefix: string, rowData: BlockRowData): string {
return prefix + "_" + (rowData.size() + 1);
}
addBlock(current: string, target: string, date: Date, rowData: BlockRowData, operation: (target: string, value: string, date: Date) => void): string {
let key: string;
if (target !== current) {
current = target;
key = this.newKey("" + current, rowData);
operation(target, key, date);
} else {
key = this.key("" + current, rowData);
rowData.setBlockLength(key, rowData.getBlockLength(key) + 1);
}
return current;
}
addDayBlock(currentDay: string, date: Date): string {
let day: string = this.getDay(date);
return this.addBlock(currentDay, day, date, this.dayRowData, (day: string, key: string, date: Date) => {
this.addDayBlockElement(key, this.formatDayCaption(day, date));
});
}
addMonthBlock(currentMonth: string, date: Date): string {
let month: number = this.getMonth(date);
return this.addBlock(currentMonth, ""+month, date, this.monthRowData, (target: string, key: string, date: Date) => {
this.addMonthBlockElement(key, this.formatMonthCaption(month, date));
});
}
addYearBlock(currentYear: string, date: Date): string {
let year: string = this.getYear(date);
return this.addBlock(currentYear, year, date, this.yearRowData, (year: string, key: string, date: Date) => {
this.addYearBlockElement(key, this.formatYearCaption(year, date));
});
}
addMonthBlockElement(key: string, text: string) {
this.createTimelineBlock(key, text, TimelineElement.STYLE_MONTH, this.monthRowData);
}
addYearBlockElement(key: string, text: string) {
this.createTimelineBlock(key, text, TimelineElement.STYLE_YEAR, this.yearRowData);
}
addDayBlockElement(key: string, text: string) {
this.createTimelineBlock(key, text, TimelineElement.STYLE_DAY, this.dayRowData);
}
createTimelineBlock(key: string, text: string, styleSuffix: string, rowData: BlockRowData): HTMLDivElement {
let div: HTMLDivElement = document.createElement('div');
div.classList.add(TimelineElement.STYLE_ROW, styleSuffix);
div.innerText = text;
rowData.setBlockLength(key, 1);
rowData.setBlock(key, div);
return div;
}
formatDayCaption(day: string, date: Date): string {
if (!this.dayFormat || this.dayFormat === "") {
return day;
}
return this.localeDataProvider.formatDate(date, this.dayFormat);
}
formatYearCaption(year: string, date: Date): string {
if (!this.yearFormat || this.yearFormat === "") {
return year;
}
return this.localeDataProvider.formatDate(date, this.yearFormat);
}
formatWeekCaption(date: Date): string {
if (!this.weekFormat || this.weekFormat === "") {
return "" + getISOWeek(date);
}
return this.localeDataProvider.formatDate(date, this.weekFormat);
}
formatMonthCaption(month: number, date: Date): string {
if (!this.monthFormat || this.monthFormat === "") {
return this.monthNames[month];
}
return this.localeDataProvider.formatDate(date, this.monthFormat);
}
getWeekday(dayCounter: number): Weekday {
if (dayCounter === this.localeDataProvider.getFirstDayOfWeek()) {
return Weekday.First;
}
if (dayCounter === this.lastDayOfWeek) {
return Weekday.Last;
}
return Weekday.Between;
}
prepareTimelineForHourResolution(startDate: Date, endDate: Date) {
let timeline = this;
this.firstDay = true;
let hourCounter: number = this.firstHourOfRange;
this.prepareTimelineForHour(DateTimeConstants.HOUR_INTERVAL, startDate, endDate, {
registerResolutionBlock(index: number, date: Date, currentYear: string, lastTimelineBlock: boolean) {
timeline.registerHourResolutionBlock();
hourCounter = Math.max((hourCounter + 1) % 25, 1);
}
});
}
prepareTimelineForHour(interval: number, startDate: Date, endDate: Date, resBlockRegisterer: IResolutionBlockRegisterer) {
this.blocksInRange = 0;
this.resolutionBlockCount = 0;
this.firstResBlockCount = 0;
this.lastResBlockCount = 0;
let currentYear = null;
let currentMonth = null;
let currentDay = null;
let pos: Date = startDate;
let end: Date = endDate;
let index: number = 0;
let lastTimelineBlock: boolean = false;
let date: Date;
while (pos.getTime() <= end.getTime()) {
date = pos;
let nextHour: Date = new Date(pos.getTime() + interval);
lastTimelineBlock = nextHour.getTime() > end.getTime();
resBlockRegisterer.registerResolutionBlock(index, date, currentYear, lastTimelineBlock);
if (this.yearRowVisible) {
currentYear = this.addYearBlock(currentYear, date);
}
if (this.monthRowVisible) {
currentMonth = this.addMonthBlock(currentMonth, date);
}
if (this.isDayRowVisible()) {
currentDay = this.addDayBlock(currentDay, date);
}
pos = nextHour;
index++;
}
}
prepareTimelineForDayOrWeekResolution(startDate: Date, endDate: Date) {
let timeline = this;
let dayCounter: number = this.firstDayOfRange;
let weekday: Weekday;
let firstWeek: boolean = true;
this.prepareTimelineForDayOrWeek(DateTimeConstants.DAY_INTERVAL, startDate, endDate, {
registerResolutionBlock: function (index: number, date: Date, currentYear: string, lastTimelineBlock: boolean) {
weekday = timeline.getWeekday(dayCounter);
if (timeline.resolution === Resolution.Week) {
timeline.registerWeekResolutionBlock(index, weekday, lastTimelineBlock, firstWeek);
if (firstWeek && (weekday === Weekday.Last || lastTimelineBlock)) {
firstWeek = false;
}
} else {
timeline.registerDayResolutionBlock();
}
dayCounter = Math.max((dayCounter + 1) % 8, 1);
}
});
}
prepareTimelineForDayOrWeek(interval: number, startDate: Date, endDate: Date,
resBlockRegisterer: IResolutionBlockRegisterer) {
this.blocksInRange = 0;
this.resolutionBlockCount = 0;
this.firstResBlockCount = 0;
this.lastResBlockCount = 0;
let currentYear: string = null;
let currentMonth: string = null;
let currentDay: string = null;
let pos: Date = DateUtil.adjustToMiddleOfDay(startDate, this.localeDataProvider.getTimeZone());
let end: Date = endDate;
let index: number = 0;
let lastTimelineBlock: boolean = false;
let date: Date;
let isDST: boolean = false;
let isPreviousDst: boolean = this.localeDataProvider.isDaylightTime(startDate);
while (!lastTimelineBlock) {
let date: Date = DateUtil.getDSTAdjustedDate(isPreviousDst, pos, this.localeDataProvider.getDaylightAdjustment(pos));
pos = date;
isDST = this.localeDataProvider.isDaylightTime(date);
let d: Date = new Date(date.getTime() + interval);
lastTimelineBlock = DateUtil.getDSTAdjustedDate(isDST, d, this.localeDataProvider.getDaylightAdjustment(d)).getTime() > end.getTime();
resBlockRegisterer.registerResolutionBlock(index, date, currentYear, lastTimelineBlock);
if (this.yearRowVisible) {
currentYear = this.addYearBlock(currentYear, date);
}
if (this.monthRowVisible) {
currentMonth = this.addMonthBlock(currentMonth, date);
}
if (this.isDayRowVisible()) {
currentDay = this.addDayBlock(currentDay, date);
}
isPreviousDst = isDST;
pos = new Date(pos.getTime() + interval);
index++;
}
}
isDayRowVisible(): boolean {
return this.resolution === Resolution.Hour;
}
/**
* Get actual width of the timeline.
*
* @return
*/
public getResolutionWidth(): number {
if (!this.isTimelineOverflowingHorizontally()) {
return this.calculateTimelineWidth();
}
let width: number = this.getResolutionDivWidth();
if (this.isAlwaysCalculatePixelWidths() && this.containsResBlockSpacer()) {
width = width - ElementUtil.getWidth(this.resSpacerDiv);
}
return width;
}
/**
* Calculate the exact width of the timeline. Excludes any spacers in the
* end.
*
* @return
*/
public calculateTimelineWidth(): number {
let last: HTMLElement = this.getLastResolutionElement();
if (last === null) {
return 0.0;
}
let r: number = ElementUtil.getRight(last);
let l: number = ElementUtil.getLeft(this.getFirstResolutionElement());
let timelineRealWidth: number = r - l;
return timelineRealWidth;
}
/*
* Get width of the resolution div element.
*/
private getResolutionDivWidth(): number {
if (!this.isTimelineOverflowingHorizontally()) {
return ElementUtil.getWidth(this.resolutionDiv);
}
return this.blocksInRange * this.minResolutionWidth;
}
/**
* Calculate matching left offset in percentage for a date (
* {@link Date#getTime()}).
*
* @param date
* Target date in milliseconds.
* @param contentWidth
* Width of the content that the given 'date' is relative to.
* @return Left offset in percentage.
*/
public getLeftPositionPercentageForDate(date: Date, contentWidth: number): number {
let timelineLeft: number = this.getLeftPositionForDate(date);
let relativeLeft: number = this.convertRelativeLeftPosition(timelineLeft, contentWidth);
let width: number = this.getResolutionWidth();
return (100.0 / width) * relativeLeft;
}
/**
* Calculate CSS value for 'left' property matching left offset in
* percentage for a date ( {@link Date#getTime()}).
*
* May return '2.123456%' or 'calc(2.123456%)' if IE;
*
* @param date
* Target date in milliseconds.
* @param contentWidth
* Width of the content that the given 'date' is relative to.
* @return Left offset as a String value.
*/
public getLeftPositionPercentageStringForDate(date: Date, contentWidth: number): string {
let timelineLeft: number = this.getLeftPositionForDate(date);
let relativeLeft: number = this.convertRelativeLeftPosition(timelineLeft, contentWidth);
let width: number = this.getResolutionWidth();
let calc: string = this.createCalcCssValue(width, relativeLeft);
if (calc != null) {
return calc;
}
return (100.0 / width) * relativeLeft + "%";
}
public getLeftPositionPercentageStringForDateRange(date: Date, rangeWidth: number, rangeStartDate: Date,
rangeEndDate: Date): string {
let rangeLeft: number = this.getLeftPositionForDateRange(date, rangeWidth, rangeStartDate, rangeEndDate);
let width: number = rangeWidth;
let calc: string = this.createCalcCssValue(width, rangeLeft);
if (calc != null) {
return calc;
}
return (100.0 / width) * rangeLeft + "%";
}
/**
* Calculate CSS value for 'width' property matching date interval inside
* the time-line. Returns percentage value. Interval is in milliseconds.
*
* May return '2.123456%' or 'calc(2.123456%)' if IE;
*
* @param interval
* Date interval in milliseconds.
* @return
*/
public getWidthPercentageStringForDateInterval(interval: number): string {
let range: number = this.internalInclusiveEndDateTime.getTime() - this.internalInclusiveStartDateTime.getTime();
return this.getWidthPercentageStringForDateIntervalForRange(interval, range);
}
/** @see #getWidthPercentageStringForDateInterval(long) */
public getWidthPercentageStringForDateIntervalForRange(interval: number, range: number): string {
let calc: string = this.createCalcCssValue(range, interval);
if (calc != null) {
return calc;
}
return (100.0 / range) * interval + "%";
}
/**
* Calculate matching left offset in pixels for a date (
* {@link Date#getTime()}).
*
* @param date
* Target date in milliseconds.
* @return Left offset in pixels.
*/
public getLeftPositionForDate(date: Date): number {
return this.getLeftPositionForDateRange(date, this.getResolutionWidth(), this.internalInclusiveStartDateTime, this.internalInclusiveEndDateTime);
}
public getLeftPositionForDateRange(date: Date, rangeWidth: number, rangeStartDate: Date, rangeEndDate: Date): number {
let width: number = rangeWidth;
let range: number = rangeEndDate.getTime() - rangeStartDate.getTime();
if (range <= 0) {
return 0;
}
let p: number = width / range;
let offset: number = date.getTime() - rangeStartDate.getTime();
let left: number = p * offset;
return left;
}
/**
* Calculate matching date ({@link Date#getTime()}) for the target left
* pixel offset.
*
* @param left
* Left offset in pixels.
* @return Date in a milliseconds or null if timeline width is invalid (<=0).
*/
public getDateForLeftPosition(left: number): Date {
return this.getDateForLeftPositionNoticeDST(left, this.resolution === Resolution.Hour);
}
public getDateForLeftPositionNoticeDST(left: number, noticeDST: boolean): Date {
let width: number = this.getResolutionWidth();
if (width <= 0) {
return null;
}
let range: number = this.normalEndDate.getTime() - this.normalStartDate.getTime();
if (noticeDST) {
range = this.adjustDateRangeByDST(range);
}
let p: number = range / width;
let offset: number = p * left;
let date: Date = new Date(this.internalInclusiveStartDateTime.getTime() + offset);
console.log("Zoned: " + this.localeDataProvider.formatDate(date, "dd. HH:mm") + " DST: "
+ this.localeDataProvider.getDaylightAdjustment(date) / 60000);
return date;
}
/**
* Convert left position for other relative target width.
*
* @param left
* @param contentWidthToConvertFor
* @return
*/
public convertRelativeLeftPosition(left: number, contentWidthToConvertFor: number): number {
let width: number = this.getResolutionWidth();
if (width <= 0 || contentWidthToConvertFor <= 0) {
return 0;
}
let relativePosition: number = (1.0 / contentWidthToConvertFor) * left;
let timelineLeft: number = relativePosition * width;
return timelineLeft;
}
adjustDateRangeByDST(range: number): number {
/*
* Notice extra block(s) or missing block(s) in range when start time is
* in DST and end time is not, or vice versa.
*/
let dstStart = this.localeDataProvider.getDaylightAdjustment(this.internalInclusiveStartDateTime);
let dstEnd = this.localeDataProvider.getDaylightAdjustment(this.internalInclusiveEndDateTime);
if (dstStart > dstEnd) {
range -= Math.abs(dstStart - dstEnd);
} else if (dstEnd > dstStart) {
range += Math.abs(dstEnd - dstStart);
}
return range;
}
/**
* Set horizontal scroll position for the time-line.
*
* @param left
* Scroll position in pixels.
*/
public setScrollLeft(left: number) {
if (this.positionLeft === left) {
return;
}
this.positionLeft = left || 0;
if(!this.directlyInsideScrollContainer) {
this.style.left = -this.positionLeft + "px";
}
this.lazyResolutionPaint = setTimeout(() => this.fillVisibleTimeline(), 20);
}
/**
* Re-calculates required widths for this widget.
*
* Re-creates and fills the visible part of the resolution element.
*/
updateWidths() {
if (this.resolutionDiv == null) {
console.log("TimelineElement is not ready for updateWidths() call. Call update(...) instead.");
return;
}
console.log("TimelineElement Started updating widths.");
// start by clearing old content in resolution element
while (this.resolutionDiv.firstChild) {
this.resolutionDiv.removeChild(this.resolutionDiv.lastChild);
}
this.setMinWidth(this.blocksInRange * this.minResolutionWidth);
// update horizontal overflow state here, after min-width is updated.
this.updateTimelineOverflowingHorizontally();
this.createTimelineElementsOnVisibleArea();
// fill timeline
this.fillVisibleTimeline();
// remove spacer block if it exist
this.removeResolutionSpacerBlock();
// calculate new block width for day-resolution.
// Year and month blocks are vertically in-line with days.
this.dayWidthPercentage = 100.0 / this.blocksInRange;
this.dayOrHourWidthPx = this.calculateDayOrHourResolutionBlockWidthPx(this.blocksInRange);
// calculate block width for currently selected resolution
// (day,week,...)
// resolution div's content may not be vertically in-line with
// year/month blocks. This is the case for example with Week resolution.
this.resBlockMinWidthPx = this.minResolutionWidth;
this.resBlockWidthPx = this.calculateActualResolutionBlockWidthPx(this.dayOrHourWidthPx);
this.resBlockWidthPercentage = 100.0 / this.resolutionBlockCount;
let pct: string = this.createCalcCssValue(this.resolutionBlockCount, null);
if (this.resolution === Resolution.Week) {
this.resBlockMinWidthPx = DateTimeConstants.DAYS_IN_WEEK * this.minResolutionWidth;
this.resBlockWidthPercentage = this.dayWidthPercentage * DateTimeConstants.DAYS_IN_WEEK;
pct = this.createCalcCssValue(this.blocksInRange, DateTimeConstants.DAYS_IN_WEEK);
}
// update resolution block widths
this.updateResolutionBlockWidths(pct);
if (this.yearRowVisible) {
// update year block widths
this.updateBlockWidths(this.yearRowData);
}
if (this.monthRowVisible) {
// update month block widths
this.updateBlockWidths(this.monthRowData);
}
if (this.isDayRowVisible()) {
this.updateBlockWidths(this.dayRowData);
}
if (this.isAlwaysCalculatePixelWidths()) {
this.updateSpacerBlocks(this.dayOrHourWidthPx);
}
console.log("TimelineElement Widths are updated.");
}
updateBlockWidths(rowData: BlockRowData) {
for (let entry of rowData.getBlockEntries()) {
this.setWidth(entry[1], rowData.getBlockLength(entry[0]));
}
}
updateSpacerBlocks(dayWidthPx: number) {
let spaceLeft: number = this.getResolutionDivWidth() - (this.blocksInRange * dayWidthPx);
if (spaceLeft > 0) {
for (let e of this.spacerBlocks) {
e.style.removeProperty("display");
e.style.width = spaceLeft + "px";
}
this.resSpacerDiv = this.createResolutionBlock();
this.resSpacerDiv.classList.add(TimelineElement.STYLE_SPACER);
this.resSpacerDiv.style.width = spaceLeft + "px";
this.resSpacerDiv.innerText = " ";
this.resolutionDiv.appendChild(this.resSpacerDiv);
} else {
this.hideSpacerBlocks();
}
}
hideSpacerBlocks() {
for (let e of this.spacerBlocks) {
e.style.display = "none";
}
}
/**
* Set minimum width (pixels) of this widget's root DIV element. Default is
* -1. Notice that
* {@link #update(Resolution, long, long, int, int, LocaleDataProvider)}
* will calculate min-width and call this internally.
*
* @param minWidth
* Minimum width in pixels.
*/
setMinWidth(minWidth: number) {
this.minWidth = minWidth;
this.style.minWidth = this.minWidth + "px";
this.resolutionDiv.style.minWidth = this.minWidth + "px";
}
/**
* Returns true if the timeline is overflowing the parent's width. This
* works only when this widget is attached to some parent.
*
* @return True when timeline width is more than the parent's width (@see
* {@link Element#getClientWidth()}).
*/
isTimelineOverflowingHorizontally(): boolean {
return this.timelineOverflowingHorizontally;
}
/**
* Update horizontal overflow state.
*/
updateTimelineOverflowingHorizontally() {
this.timelineOverflowingHorizontally = (ElementUtil.getWidth(this.resolutionDiv) > ElementUtil.getWidth(this.getParentElement(this)));
}
createTimelineElementsOnVisibleArea() {
// create place holder elements that represents weeks/days/hours
// depending on the resolution in the timeline.
// Only visible blocks are created, and only once, content will change
// on scroll.
// first: detect how many blocks we can fit in the screen
let blocks: number = this.resolutionBlockCount;
if (this.isTimelineOverflowingHorizontally()) {
blocks = Math.floor((ElementUtil.getWidth(this.getParentElement(this))
/ this.calculateMinimumResolutionBlockWidth()));
if (this.resolutionBlockCount < blocks) {
// blocks need to be scaled up to fit the screen
blocks = this.resolutionBlockCount;
} else {
blocks += 2;
}
}
let element: HTMLDivElement = null;
for (let i = 0; i < blocks; i++) {
switch (this.resolution) {
case Resolution.Hour:
element = this.createHourResolutionBlock();
break;
case Resolution.Day:
element = this.createDayResolutionBlock();
break;
case Resolution.Week:
element = this.createWeekResolutionBlock();
break;
}
this.resolutionDiv.appendChild(element);
}
console.log(`TimelineElement Added ${blocks} visible timeline elements for resolution ${Resolution[this.resolution]}`);
}
calculateMinimumResolutionBlockWidth(): number {
if (this.resolution === Resolution.Week) {
return DateTimeConstants.DAYS_IN_WEEK * this.minResolutionWidth;
}
return this.minResolutionWidth;
}
createResolutionBlock(): HTMLDivElement {
let resBlock: HTMLDivElement = document.createElement('div');
resBlock.classList.add("col");
return resBlock;
}
createHourResolutionBlock(): HTMLDivElement {
let resBlock: HTMLDivElement = this.createResolutionBlock();
resBlock.classList.add("h", TimelineElement.STYLE_CENTER);
return resBlock;
}
createDayResolutionBlock(): HTMLDivElement {
let resBlock: HTMLDivElement = this.createResolutionBlock();
resBlock.classList.add(TimelineElement.STYLE_CENTER);
return resBlock;
}
createWeekResolutionBlock(): HTMLDivElement {
let resBlock: HTMLDivElement = this.createResolutionBlock();
resBlock.classList.add("w", TimelineElement.STYLE_CENTER);
return resBlock;
}
fillVisibleTimeline() {
if (this.isTimelineOverflowingHorizontally()) {
this.showResolutionBlocksOnView();
} else {
this.showAllResolutionBlocks();
}
}
showResolutionBlocksOnView() {
let positionLeftSnapshot: number = this.positionLeft;
let datePos: number = positionLeftSnapshot;
this.firstWeekBlockHidden = false;
let left: number = Math.floor(positionLeftSnapshot);
if (positionLeftSnapshot > 0 && this.resBlockWidthPx > 0) {
let overflow: number = 0.0;
let firstResBlockShort: boolean = this.isFirstResBlockShort();
overflow = this.getScrollOverflowForResolutionBlock(positionLeftSnapshot, left, firstResBlockShort);
left = Math.floor(positionLeftSnapshot - overflow);
datePos = this.adjustLeftPositionForDateDetection(left);
}
if (datePos < 0.0) {
datePos = positionLeftSnapshot;
}
let leftDate: Date;
let noticeDst: boolean = this.resolution === Resolution.Hour;
leftDate = this.getDateForLeftPositionNoticeDST(datePos, noticeDst);
let containerWidth: number = ElementUtil.getWidth(this.getParentElement(this));
this.fillTimelineForResolution(leftDate,
new Date(Math.min(this.internalInclusiveEndDateTime.getTime(), this.getDateForLeftPositionNoticeDST(datePos + containerWidth, noticeDst).getTime())), left);
this.style.setProperty("--timeline-col-position", "relative");
this.style.setProperty("--timeline-col-left", left + "px");
console.log(`TimelineElement Updated visible timeline elements for horizontal scroll position ${left} (plus ${datePos-left} to center-of-first-block)`);
}
showAllResolutionBlocks() {
this.style.setProperty("--timeline-col-position", "relative");
this.style.setProperty("--timeline-col-left", "0px");
this.fillTimelineForResolution(this.internalInclusiveStartDateTime, this.internalInclusiveEndDateTime, 0);
}
fillTimelineForResolution(startDate: Date, endDate: Date, left: number) {
if (this.resolution === Resolution.Day || this.resolution === Resolution.Week) {
this.fillTimelineForDayResolution(startDate, endDate, left);
} else if (this.resolution == Resolution.Hour) {
this.fillTimelineForHourResolution(startDate, endDate, left);
} else {
console.log("TimelineElement resolution " + (this.resolution != null ? Resolution[this.resolution] : "null")
+ " is not supported");
return;
}
console.log("TimelineElement Filled new data and styles to visible timeline elements");
}
isFirstResBlockShort(): boolean {
return this.firstResBlockCount > 0 && ((this.resolution === Resolution.Week && this.firstResBlockCount < DateTimeConstants.DAYS_IN_WEEK));
}
isLastResBlockShort(): boolean {
return this.lastResBlockCount > 0 && ((this.resolution === Resolution.Week && this.lastResBlockCount < DateTimeConstants.DAYS_IN_WEEK));
}
getScrollOverflowForResolutionBlock(positionLeftSnapshot: number, left: number, firstResBlockShort: boolean): number {
let overflow: number;
if (firstResBlockShort && left <= this.getFirstResolutionElementWidth()) {
overflow = this.getScrollOverflowForShortFirstResolutionBlock(positionLeftSnapshot);
} else {
overflow = this.getScrollOverflowForRegularResoultionBlock(positionLeftSnapshot, firstResBlockShort);
}
return overflow;
}
getScrollOverflowForRegularResoultionBlock(positionLeftSnapshot: number, firstResBlockShort: boolean): number {
let overflow: number;
let firstBlockWidth: number = this.getFirstResolutionElementWidth();
let positionLeft: number = (positionLeftSnapshot - (firstResBlockShort ? firstBlockWidth : 0));
overflow = positionLeft % this.resBlockWidthPx;
if (firstResBlockShort) {
overflow += firstBlockWidth;
this.firstWeekBlockHidden = true;
}
return overflow;
}
getScrollOverflowForShortFirstResolutionBlock(positionLeftSnapshot: number): number {
let overflow;
// need to notice a short resolution block due to timeline's
// start date which is in middle of a week.
overflow = positionLeftSnapshot % this.getFirstResolutionElementWidth();
if (overflow == 0.0) {
overflow = this.getFirstResolutionElementWidth();
}
return overflow;
}
/**
* Returns a width of the first resolution block.
*
* @return
*/
getFirstResolutionElementWidth(): number {
if (this.isFirstResBlockShort()) {
if (this.isTimelineOverflowingHorizontally()) {
return this.firstResBlockCount * this.minResolutionWidth;
} else {
return ElementUtil.getWidth(this.getFirstResolutionElement());
}
} else {
if (this.isTimelineOverflowingHorizontally()) {
return this.resBlockMinWidthPx;
} else {
return ElementUtil.getWidth(this.getFirstResolutionElement());
}
}
}
getFirstResolutionElement(): HTMLElement {
if (this.resolutionDiv.hasChildNodes()) {
return this.resolutionDiv.firstElementChild;
}
return null;
}
getLastResolutionElement(): HTMLElement {
let div: HTMLDivElement = this.resolutionDiv;
if (!div) {
return null;
}
let nodeList: NodeListOf = div.childNodes;
if (!nodeList) {
return null;
}
let blockCount: number = nodeList.length;
if (blockCount < 1) {
return null;
}
if (this.containsResBlockSpacer()) {
let index: number = blockCount - 2;
if (blockCount > 1 && index >= 0) {
return this.resolutionDiv.childNodes.item(index);
}
return null;
}
return this.resolutionDiv.lastChild;
}
containsResBlockSpacer(): boolean {
return this.resSpacerDiv != null && this.resSpacerDiv.parentElement
&& this.resSpacerDiv.parentElement === this.resolutionDiv;
}
removeResolutionSpacerBlock() {
if (this.containsResBlockSpacer()) {
this.resSpacerDiv.parentNode.removeChild(this.resSpacerDiv);
}
}
/*
* Calculates either day or hour resolution block width depending on the
* current resolution.
*/
calculateDayOrHourResolutionBlockWidthPx(blockCount: number): number {
let dayOrHourWidthPx: number = Math.round(this.resolutionDiv.clientWidth / blockCount);
while ((blockCount * dayOrHourWidthPx) < this.resolutionDiv.clientWidth) {
dayOrHourWidthPx++;
}
return dayOrHourWidthPx;
}
/*
* Calculates the actual width of one resolution block element. For example:
* week resolution will return 7 * dayOrHourBlockWidthPx.
*/
calculateActualResolutionBlockWidthPx(dayOrHourBlockWidthPx: number): number {
if (this.resolution === Resolution.Week) {
return DateTimeConstants.DAYS_IN_WEEK * dayOrHourBlockWidthPx;
}
return dayOrHourBlockWidthPx;
}
/**
* Adjust left position for optimal position to detect accurate date with
* the current resolution.
*/
adjustLeftPositionForDateDetection(left: number): number {
let datePos: number;
if (this.resolution === Resolution.Week) {
// detect date from the center of the first day block inside the
// week block.
datePos = left + this.dayOrHourWidthPx / 2;
} else {
// detect date from the center of the block (day/hour)
datePos = left + this.resBlockWidthPx / 2;
}
return datePos;
}
createCalcCssValue(v: number, multiplier: number): string {
if (this.ie) {
// see comments in createCalcCssValue(int, Integer)
let percents: number = 100.0 / v * multiplier;
return "calc(" + percents + "%)";
}
return null;
}
updateResolutionBlockWidths(pct: string) {
if (this.setPositionForEachBlock) {
if (!this.isTimelineOverflowingHorizontally()) {
this.resolutionDiv.style.display = "flex";
} else {
this.resolutionDiv.style.removeProperty("display");
}
let firstResBlockIsShort: boolean = this.isFirstResBlockShort();
let lastResBlockIsShort: boolean = this.isLastResBlockShort();
// when setPositionForEachBlock is true, set width for each block explicitly.
let count: number = this.resolutionDiv.childElementCount;
if (this.containsResBlockSpacer()) {
count--;
}
let lastIndex: number = count - 1;
let i: number;
let resBlock: HTMLElement;
for (i = 0; i < count; i++) {
resBlock = this.resolutionDiv.childNodes.item(i);
// first and last week blocks may be thinner than other
// resolution blocks.
if (firstResBlockIsShort && i == 0) {
this.setWidth(resBlock, this.firstResBlockCount);
} else if (lastResBlockIsShort && i == lastIndex) {
this.setWidth(resBlock, this.lastResBlockCount);
} else {
this.setWidthPct(this.resBlockWidthPx, pct, resBlock);
}
}
} else {
// set widths by updating injected styles in one place. Faster than
// setting widths explicitly for each element.
let center: string = this.getWidthStyleValue(pct);
let first: string = center;
let last: string = center;
if (this.isFirstResBlockShort()) {
first = this.getWidth(this.firstResBlockCount);
}
if (this.isLastResBlockShort()) {
last = this.getWidth(this.lastResBlockCount);
}
this.style.setProperty("--timeline-col-center-width", center);
this.style.setProperty("--timeline-col-first-width", first);
this.style.setProperty("--timeline-col-last-width", last);
}
}
getWidth(multiplier: number): string {
if (this.isTimelineOverflowingHorizontally()) {
return (multiplier * this.minResolutionWidth) + "px";
} else {
if (this.isAlwaysCalculatePixelWidths()) {
return multiplier * this.dayOrHourWidthPx + "px";
} else {
return this.getCssPercentageWidth(this.blocksInRange, this.dayWidthPercentage, multiplier);
}
}
}
setWidth(element: HTMLElement, multiplier: number) {
if (this.isTimelineOverflowingHorizontally()) {
element.style.width = (multiplier * this.minResolutionWidth) + "px";
} else {
if (this.isAlwaysCalculatePixelWidths()) {
element.style.width = (multiplier * this.dayOrHourWidthPx) + "px";
} else {
this.setCssPercentageWidth(element, this.blocksInRange, this.dayWidthPercentage, multiplier);
}
}
}
setWidthPct(resBlockWidthPx: number, pct: string, element: HTMLElement) {
if (this.isTimelineOverflowingHorizontally()) {
element.style.width = this.resBlockMinWidthPx + "px";
} else {
if (this.isAlwaysCalculatePixelWidths()) {
element.style.width = resBlockWidthPx + "px";
} else {
if (this.ie) {
element.style.flex = "1";
}
this.setCssPercentageWidthFor(element, this.resBlockWidthPercentage, pct);
}
}
}
setCssPercentageWidth(element: HTMLElement, daysInRange: number, width: number, position: number) {
let pct: string = this.createCalcCssValue(daysInRange, position);
this.setCssPercentageWidthFor(element, position * width, pct);
}
getCssPercentageWidth(daysInRange: number, width: number, position: number): string {
let pct: string = this.createCalcCssValue(daysInRange, position);
return this.getPercentageWidthString(position * width, pct);
}
setCssPercentageWidthFor(element: HTMLElement, nValue: number, pct: string) {
if (pct) {
element.style.width = pct;
} else {
element.style.width = nValue + "%";
}
}
getPercentageWidthString(nValue: number, pct: string): string {
if (pct) {
return pct;
} else {
return nValue + "%";
}
}
getWidthStyleValue(pct: string): string {
if (this.isTimelineOverflowingHorizontally()) {
return this.resBlockMinWidthPx + "px";
} else {
if (this.isAlwaysCalculatePixelWidths()) {
return this.resBlockWidthPx + "px";
} else {
return this.getPercentageWidthString(this.resBlockWidthPercentage, pct);
}
}
}
fillTimelineForHourResolution(startDate: Date, endDate: Date, left: number) {
let timeline = this;
this.firstDay = true;
let hourCounter: number;
let even: boolean;
this.fillTimelineForHour(DateTimeConstants.HOUR_INTERVAL, startDate, endDate, {
setup() {
hourCounter = this.getFirstHourOfVisibleRange(startDate);
even = this.isEven(startDate);
},
fillResolutionBlock(index: number, date: Date, currentYear: string, lastTimelineBlock: boolean) {
let childCount: number = timeline.resolutionDiv.childElementCount;
if (timeline.isValidChildIndex(index, childCount)) {
let resBlock: HTMLDivElement = timeline.resolutionDiv.childNodes.item(index);
timeline.fillHourResolutionBlock(resBlock, date, index, hourCounter, lastTimelineBlock, left, even);
hourCounter = (hourCounter + 1) % 24;
even = !even;
} else {
timeline.logIndexOutOfBounds("hour", index, childCount);
return;
}
},
isEven(startDate: Date): boolean {
let normalDate: Date = timeline.toNormalDate(startDate);
if (timeline.normalStartDate.getTime() < normalDate.getTime()) {
let hours: number = Math.floor(((normalDate.getTime() - timeline.normalStartDate.getTime()) / DateTimeConstants.HOUR_INTERVAL));
return (hours % 2) == 1;
}
return false;
},
getFirstHourOfVisibleRange(startDate: Date): number {
let normalDate: Date = timeline.toNormalDate(startDate);
if (timeline.normalStartDate.getTime() < normalDate.getTime()) {
let hours: number = Math.floor(((normalDate.getTime() - timeline.normalStartDate.getTime()) / DateTimeConstants.HOUR_INTERVAL));
return ((timeline.firstHourOfRange + hours) % 24);
}
return timeline.firstHourOfRange;
}
});
}
fillTimelineForDayResolution(startDate: Date, endDate: Date, left: number) {
let timeline = this;
let dayCounter: number;
let even: boolean;
let firstWeek: boolean = true;
let weekIndex: number = 0;
let weekday: Weekday;
this.fillTimelineForDayOrWeek(DateTimeConstants.DAY_INTERVAL, startDate, endDate, {
setup: function () {
dayCounter = this.getFirstDayOfVisibleRange(startDate);
even = this.isEven(startDate, timeline.firstDayOfRange);
},
fillResolutionBlock: function (index: number, date: Date, currentYear: string, lastTimelineBlock: boolean) {
try {
weekday = timeline.getWeekday(dayCounter);
if (timeline.resolution === Resolution.Week) {
this.fillWeekBlock(left, index, date, lastTimelineBlock);
} else {
this.fillDayBlock(left, index, date);
}
} finally {
dayCounter = Math.max((dayCounter + 1) % 8, 1);
}
},
fillDayBlock: function (left: number, index: number, date: Date) {
let childCount: number = timeline.resolutionDiv.childElementCount;
if (timeline.isValidChildIndex(index, childCount)) {
let resBlock: HTMLDivElement = timeline.resolutionDiv.childNodes.item(index);
timeline.fillDayResolutionBlock(resBlock, date, index, timeline.isWeekEnd(dayCounter), left);
} else {
timeline.logIndexOutOfBounds("day", index, childCount);
return;
}
},
fillWeekBlock: function (left: number, index: number, date: Date, lastTimelineBlock: boolean) {
let resBlock: HTMLDivElement = null;
if (index > 0 && weekday == Weekday.First) {
weekIndex++;
firstWeek = false;
even = !even;
}
if (index == 0 || weekday == Weekday.First) {
let childCount: number = timeline.resolutionDiv.childElementCount;
if (timeline.isValidChildIndex(weekIndex, childCount)) {
resBlock = timeline.resolutionDiv.childNodes.item(weekIndex);
} else {
timeline.logIndexOutOfBounds("week", weekIndex, childCount);
return;
}
}
timeline.fillWeekResolutionBlock(resBlock, date, weekIndex, weekday, firstWeek, lastTimelineBlock, left, even);
},
calcDaysLeftInFirstWeek: function (startDay: number): number {
let daysLeftInWeek: number = 0;
if (startDay != timeline.firstDayOfWeek) {
for (let i = startDay; ; i++) {
daysLeftInWeek++;
if (Math.max(i % 8, 1) === timeline.lastDayOfWeek) {
break;
}
}
}
return daysLeftInWeek;
},
isEven: function (startDate: Date, startDay: number): boolean {
let visibleRangeNormalStartDate: Date = timeline.toNormalDate(startDate);
if (timeline.normalStartDate.getTime() < visibleRangeNormalStartDate.getTime()) {
let daysHidden: number = Math.floor(((visibleRangeNormalStartDate.getTime() - timeline.normalStartDate.getTime()) / DateTimeConstants.DAY_INTERVAL));
console.log("Days hidden: " + daysHidden);
console.log("firstWeekBlockHidden = " + timeline.firstWeekBlockHidden);
if (daysHidden === 0) {
return false;
}
let daysLeftInFirstWeek: number = this.calcDaysLeftInFirstWeek(startDay);
if (daysHidden > daysLeftInFirstWeek) {
daysHidden -= daysLeftInFirstWeek;
}
let weeks: number = daysHidden / DateTimeConstants.DAYS_IN_WEEK;
let even: boolean = (weeks % 2) === 1;
return (timeline.firstWeekBlockHidden) ? !even : even;
}
return false;
},
getFirstDayOfVisibleRange: function (startDate: Date): number {
let visibleRangeNormalStartDate: Date = timeline.toNormalDate(startDate);
if (timeline.normalStartDate.getTime() < visibleRangeNormalStartDate.getTime()) {
let days: number = Math.floor(((visibleRangeNormalStartDate.getTime() - timeline.normalStartDate.getTime()) / DateTimeConstants.DAY_INTERVAL));
return ((timeline.firstDayOfRange - 1 + days) % 7) + 1;
}
return timeline.firstDayOfRange;
}
});
}
logIndexOutOfBounds(indexName: string, index: number, childCount: number) {
console.log("${indexName} index ${index} out of bounds with childCount ${childCount}. Can't fill content.");
}
fillTimelineForHour(interval: number, startDate: Date, endDate: Date,
resBlockFiller: IResolutionBlockFiller) {
let currentYear: string = null;
let pos: Date = startDate;
let end: Date = endDate;
let index: number = 0;
let lastTimelineBlock: boolean = false;
let date: Date;
resBlockFiller.setup();
while (pos <= end) {
date = pos;
let nextHour: Date = new Date(pos.getTime() + interval);
lastTimelineBlock = nextHour.getTime() > end.getTime();
resBlockFiller.fillResolutionBlock(index, date, currentYear, lastTimelineBlock);
pos = nextHour;
index++;
}
}
fillTimelineForDayOrWeek(interval: number, startDate: Date, endDate: Date, resBlockFiller: IResolutionBlockFiller) {
let currentYear: string = null;
let pos: Date = startDate;
pos = DateUtil.adjustToMiddleOfDay(pos, this.localeDataProvider.getTimeZone());
let end: Date = endDate;
let index: number = 0;
let lastTimelineBlock: boolean = false;
let date: Date;
let isDST: boolean = false;
let previousIsDST: boolean = this.localeDataProvider.isDaylightTime(startDate);
resBlockFiller.setup();
while (!lastTimelineBlock) {
let dstAdjusted: Date = this.getDSTAdjustedDate(previousIsDST, pos);
date = dstAdjusted;
pos = dstAdjusted;
isDST = this.localeDataProvider.isDaylightTime(date);
lastTimelineBlock = this.getDSTAdjustedDate(isDST, new Date(date.getTime() + interval)).getTime() > end.getTime();
resBlockFiller.fillResolutionBlock(index, date, currentYear, lastTimelineBlock);
previousIsDST = isDST;
pos = new Date(pos.getTime() + interval);
index++;
}
}
isValidChildIndex(index: number, childCount: number): boolean {
return (index >= 0) && (index < childCount);
}
fillDayResolutionBlock(resBlock: HTMLDivElement, date: Date, index: number, weekend: boolean, left: number) {
resBlock.innerText = this.localeDataProvider.formatDate(date, "d");
if (weekend) {
resBlock.classList.add(TimelineElement.STYLE_WEEKEND);
} else {
resBlock.classList.remove(TimelineElement.STYLE_WEEKEND);
}
if (this.setPositionForEachBlock && this.isTimelineOverflowingHorizontally()) {
resBlock.style.position = "relative";
resBlock.style.left = left + "px";
}
}
fillWeekResolutionBlock(resBlock: HTMLDivElement, date: Date, index: number, weekDay: Weekday, firstWeek: boolean,
lastBlock: boolean, left: number, even: boolean) {
if (resBlock != null) {
resBlock.innerText = this.formatWeekCaption(date);
if (even) {
resBlock.classList.add(TimelineElement.STYLE_EVEN);
} else {
resBlock.classList.remove(TimelineElement.STYLE_EVEN);
}
if (this.setPositionForEachBlock && this.isTimelineOverflowingHorizontally()) {
resBlock.style.position = "relative";
resBlock.style.left = left + "px";
}
resBlock.classList.remove(TimelineElement.STYLE_FIRST, TimelineElement.STYLE_LAST);
}
if (firstWeek && (weekDay === Weekday.Last || lastBlock)) {
let firstEl: HTMLElement = this.resolutionDiv.firstElementChild;
if (!firstEl.classList.contains(TimelineElement.STYLE_FIRST)) {
firstEl.classList.add(TimelineElement.STYLE_FIRST);
}
} else if (lastBlock) {
let lastEl: HTMLElement = this.resolutionDiv.lastChild;
if (!lastEl.classList.contains(TimelineElement.STYLE_LAST)) {
lastEl.classList.add(TimelineElement.STYLE_LAST);
}
}
}
fillHourResolutionBlock(resBlock: HTMLDivElement, date: Date, index: number, hourCounter: number, lastBlock: boolean,
left: number, even: boolean) {
if (this.localeDataProvider.isTwelveHourClock()) {
resBlock.innerText = this.localeDataProvider.formatTime(date, "h");
} else {
resBlock.innerText = this.localeDataProvider.formatTime(date, "HH");
}
if (even) {
resBlock.classList.add(TimelineElement.STYLE_EVEN);
} else {
resBlock.classList.remove(TimelineElement.STYLE_EVEN);
}
if (this.firstDay && (hourCounter == 24 || lastBlock)) {
this.firstDay = false;
this.firstResBlockCount = index + 1;
} else if (lastBlock) {
this.lastResBlockCount = (index + 1 - this.firstResBlockCount) % 24;
}
if (this.setPositionForEachBlock && this.isTimelineOverflowingHorizontally()) {
resBlock.style.position = "relative";
resBlock.style.left = left + "px";
}
}
}