UNPKG

@syncfusion/ej2-gantt

Version:
408 lines (407 loc) 19.2 kB
import { createElement, formatUnit, remove, isNullOrUndefined, SanitizeHtmlHelper } from '@syncfusion/ej2-base'; import * as cls from '../base/css-constants'; var EventMarker = /** @class */ (function () { /** * Initializes the EventMarker renderer. * @param {Gantt} gantt - The parent Gantt component instance */ function EventMarker(gantt) { /** * Regex pattern for ISO date format detection (YYYY-MM-DD). * Cached at class level for performance optimization (avoids recompilation on each call). * @type {RegExp} * @readonly * @private */ this.ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}/; this.parent = gantt; this.eventMarkersContainer = null; } /** * @returns {void} . * @private */ EventMarker.prototype.renderEventMarkers = function () { if (this.parent.eventMarkers && this.parent.eventMarkers.length > 0) { if (!this.parent.ganttChartModule.chartBodyContent.contains(this.eventMarkersContainer)) { this.eventMarkersContainer = createElement('div', { className: cls.eventMarkersContainer }); this.eventMarkersContainer.setAttribute('role', 'term'); this.parent.ganttChartModule.chartBodyContent.appendChild(this.eventMarkersContainer); } this.eventMarkersContainer.innerHTML = ''; this.getEventMarkersElements(this.eventMarkersContainer); } else { this.removeContainer(); } }; /** * @returns {void} . * @private */ EventMarker.prototype.removeContainer = function () { if (this.eventMarkersContainer) { remove(this.eventMarkersContainer); this.eventMarkersContainer = null; } }; /** * Method to get event markers as html string * * @param {HTMLElement} container . * @returns {void} . */ EventMarker.prototype.getEventMarkersElements = function (container) { var left; var eventMarkerElement; var spanElement; var rightArrow; var top; var eventMarkerCollection = []; for (var i = 0; i < this.parent.eventMarkers.length; i++) { if (!isNullOrUndefined(this.parent.eventMarkers[i].day)) { var updatedDate = void 0; var eventMarkerDate = this.parent.eventMarkers[i].day; this.parent['isFromEventMarker'] = true; if (eventMarkerDate instanceof Date) { updatedDate = new Date(eventMarkerDate.getTime()); } else { var dateObject = this.parent.globalize.parseDate(eventMarkerDate, { format: this.parent.getDateFormat(), type: 'dateTime' }); updatedDate = isNullOrUndefined(dateObject) && !isNaN(new Date(eventMarkerDate).getTime()) ? new Date(eventMarkerDate) : dateObject; } /** * Timezone normalization for date-only event markers. * Applies only when the user provided a date-only input (no explicit time). * This ensures the marker renders at the correct timeline position regardless of timezone. * For example: "2025-04-01" should display on April 1 in all timezones (IST, EST, UTC, etc). * ISO dates parsed as UTC midnight are converted to local midnight to preserve visual date. */ if (this.isDateOnlyInput(eventMarkerDate, updatedDate) && !this.hasExplicitTime(eventMarkerDate)) { updatedDate = this.normalizeToTimezone(updatedDate); } var formattedEventMarkerDate = updatedDate; left = this.parent.dataOperation.getTaskLeft(formattedEventMarkerDate, false, this.parent.defaultCalendarContext, true); top = this.parent.eventMarkers[i]['properties'].top; var heightException = true; if (this.parent.ganttHeight) { heightException = parseFloat(top) <= (this.parent.ganttHeight - 155); } var markerTop = (top && parseFloat(top) >= 0 && heightException && top.includes('px')) ? top : '50px'; if (!top && document.body.className.includes('e-bigger')) { markerTop = parseFloat(markerTop) + 15 + "px"; } this.parent['isFromEventMarker'] = false; eventMarkerCollection.push({ id: i, left: left, label: this.parent.eventMarkers[i].label, date: formattedEventMarkerDate, top: markerTop }); var align = void 0; if (this.parent.enableRtl) { align = "right:" + left + "px;"; } else { align = "left:" + left + "px;"; } eventMarkerElement = createElement('div', { className: cls.eventMarkersChild, styles: align + " height:100%;", id: 'stripline' + i }); if (this.parent.eventMarkers[i].label) { spanElement = createElement('div', { className: cls.eventMarkersSpan }); var property = this.parent.disableHtmlEncode ? 'textContent' : 'innerHTML'; spanElement[property] = this.parent.eventMarkers[i].label; if (this.parent.enableHtmlSanitizer && typeof (spanElement[property]) === 'string') { spanElement[property] = SanitizeHtmlHelper.sanitize(spanElement[property]); } if (this.parent.enableRtl) { spanElement.style.right = '5px'; } else { spanElement.style.left = '5px'; } spanElement.style.setProperty('top', markerTop, 'important'); eventMarkerElement.appendChild(spanElement); rightArrow = createElement('div', { className: 'e-gantt-right-arrow' }); var rightArrowTop = parseFloat(spanElement.style.top) < 10 ? parseFloat(spanElement.style.top) - 1 : parseFloat(spanElement.style.top); if (document.body.className.includes('e-bigger')) { rightArrow.style.setProperty('top', rightArrowTop + 8 + "px", 'important'); } else { rightArrow.style.setProperty('top', rightArrowTop + 10 + "px", 'important'); } eventMarkerElement.appendChild(rightArrow); } if (this.parent.eventMarkers[i].cssClass) { eventMarkerElement.classList.add(this.parent.eventMarkers[i].cssClass); } eventMarkerElement.setAttribute('tabindex', '-1'); eventMarkerElement.setAttribute('aria-label', this.parent.localeObj.getConstant('eventMarkers') + ' ' + (typeof this.parent.eventMarkers[i].day === 'string' ? this.parent.eventMarkers[i].day : this.parent.getFormatedDate(this.parent.eventMarkers[i].day)) + ' ' + this.parent.eventMarkers[i].label); container.appendChild(eventMarkerElement); } } this.parent.eventMarkerColloction = eventMarkerCollection; }; /** * Detects whether the user input represents a date-only value (no explicit time component). * * This method distinguishes between: * - **Date-only inputs**: "2025-04-01", new Date("2025-04-01"), new Date(2025, 3, 1) * - **Datetime inputs**: "2025-04-01T14:30:00", new Date(2025, 3, 1, 14, 30) * * For Date objects, it checks both local and UTC midnight to handle timezone offsets: * - Local midnight: Detects dates created with constructor (e.g., new Date(2025, 3, 1)) * - UTC midnight: Detects ISO-parsed dates (e.g., new Date("2025-04-01")) * * @param {string | Date} input - The original user-provided event marker date * @param {Date} dateObj - The parsed Date object for comparison * @returns {boolean} true if input is date-only (no explicit time), false if explicit time provided * * @example * // String inputs * isDateOnlyInput("2025-04-01", dateObj) → true (date-only) * isDateOnlyInput("2025-04-01T14:30:00", dateObj) → false (explicit time) * * // Date objects (both timezone-aware cases) * isDateOnlyInput(new Date(2025, 3, 1), dateObj) → true (local midnight) * isDateOnlyInput(new Date("2025-04-01"), dateObj) → true (UTC midnight, ISO detected) * * @private */ EventMarker.prototype.isDateOnlyInput = function (input, dateObj) { // Case 1: STRING date input if (typeof input === 'string') { // ISO or datetime string: "2025-04-01T00:00:00.000Z" if (input.includes('T')) { // For ISO format strings, check if UTC time is midnight return (dateObj.getUTCHours() === 0 && dateObj.getUTCMinutes() === 0 && dateObj.getUTCSeconds() === 0); } // If string contains time separator (:) → explicit datetime if (input.includes(':')) { return false; } // Otherwise treat as date-only (handles non-ISO date strings) return true; } // Case 2: Date object input - must handle both timezone origins // Check 1: Local midnight (catches dates like new Date(2025, 3, 1)) var isLocalMidnight = (dateObj.getHours() === 0 && dateObj.getMinutes() === 0 && dateObj.getSeconds() === 0 && dateObj.getMilliseconds() === 0); // Check 2: UTC midnight (catches ISO-parsed dates like new Date("2025-04-01")) // These are parsed as UTC, so UTC being midnight indicates ISO origin var isUTCMidnight = (dateObj.getUTCHours() === 0 && dateObj.getUTCMinutes() === 0 && dateObj.getUTCSeconds() === 0); // Date is date-only if EITHER local OR UTC is midnight (not both, due to timezone offset) return isLocalMidnight || isUTCMidnight; }; /** * Checks if the input string contains an explicit non-midnight time component. * * This method validates whether the user explicitly provided a specific time (not just midnight). * It only processes string inputs; Date objects are assumed to have implicit times. * * Time is considered "explicit" if: * - String contains a time separator ':' AND * - Extracted time is NOT 00:00:00 (midnight) * * Supports multiple time format separators: * - ISO format with 'T': "2025-04-01T14:30:00" * - Space separator: "2025-04-01 14:30:00" * - Locale formats with colons: "01-04-2025 14:30" * * @param {string | Date} input - The original user-provided event marker date * @returns {boolean} true if string has explicit non-midnight time, false otherwise * * @example * // Date-only strings (no explicit time) * hasExplicitTime("2025-04-01") → false (no time) * hasExplicitTime("2025-04-01T00:00:00") → false (midnight is implicit) * * // Datetime strings with explicit times * hasExplicitTime("2025-04-01T14:30:00") → true (explicit time) * hasExplicitTime("2025-04-01 07:15:30") → true (explicit time) * * // Date objects (not analyzed for time) * hasExplicitTime(new Date(2025, 3, 1, 14, 30)) → false (Date objects ignored) * * @private */ EventMarker.prototype.hasExplicitTime = function (input) { // Only analyze string inputs; Date objects are not considered to have explicit times if (typeof input !== 'string') { return false; } // Quick check: no ':' separator means no time component present if (!input.includes(':')) { return false; } // Extract time portion after separator (T or space) // Split by regex to handle both 'T' and space separators var timePart = input.split(/T|\s/)[1]; // Parse time components (hours, minutes, seconds) with safe defaults var _a = timePart.split(':'), _b = _a[0], hour = _b === void 0 ? '0' : _b, _c = _a[1], minute = _c === void 0 ? '0' : _c, _d = _a[2], second = _d === void 0 ? '0' : _d; var hours = parseInt(hour, 10) || 0; var minutes = parseInt(minute, 10) || 0; var seconds = parseInt(second, 10) || 0; // Explicit time is only true if the time is NOT midnight (00:00:00) return !(hours === 0 && minutes === 0 && seconds === 0); }; /** * Normalizes a date to local midnight while preserving the intended visual date. * * This method solves the timezone offset problem where ISO dates like "2025-04-01" * are parsed as UTC midnight but render at an offset time in local timezones. * * **Problem Example (IST timezone, UTC+5:30):** * - Input: "2025-04-01" (intended: April 1st) * - Parsed as: 2025-04-01 00:00:00 UTC * - Browser converts to: 2025-04-01 05:30:00 IST * - Without fix: getTaskLeft() uses 05:30 → renders April 1 afternoon ❌ * - With fix: Normalizes to 00:00 IST → renders April 1 morning ✓ * * **Algorithm:** * 1. Detects input origin (ISO string, ISO-parsed Date, or local Date) * 2. For ISO dates: Extract Y/M/D from UTC values * 3. For local dates: Extract Y/M/D from local values * 4. Reconstructs as local midnight: new Date(year, month, day, 0, 0, 0, 0) * * **Timezone Examples:** * - IST (UTC+5:30): April 1 UTC → April 1 00:00 IST * - EST (UTC-5): April 1 UTC → April 1 00:00 EST (not March 31) * - UTC (±0): April 1 UTC → April 1 00:00 UTC * * @param {Date} date - The parsed Date object to normalize * @param {string | Date} [input] - Optional original user input (helps detect ISO format strings) * @returns {Date} Date object representing local midnight of the visual date * * @example * // ISO string case * normalizeToTimezone(new Date("2025-04-01"), "2025-04-01") * → new Date(2025, 3, 1) (IST: 2025-04-01 00:00:00 IST) * * // ISO-parsed Date object case * normalizeToTimezone(new Date("2025-04-01")) * → Detects ISO origin via isISODateObject() → uses UTC values * → new Date(2025, 3, 1) (IST: 2025-04-01 00:00:00 IST) * * // Local Date object case * normalizeToTimezone(new Date(2025, 3, 1)) * → Uses local values → new Date(2025, 3, 1) (unchanged, already correct) * * @private * @see isISODateObject For ISO-origin detection logic */ EventMarker.prototype.normalizeToTimezone = function (date, input) { var year; var month; var day; // Case 1: ISO string input (YYYY-MM-DD format) // Use cached regex pattern for better performance if (typeof input === 'string' && this.ISO_DATE_PATTERN.test(input)) { // ISO format strings are parsed as UTC, so extract from UTC values year = date.getUTCFullYear(); month = date.getUTCMonth(); day = date.getUTCDate(); } // Case 2: ISO-based Date object (e.g., new Date("2025-04-01")) // These are also parsed as UTC, detected by isISODateObject() else if (this.isISODateObject(date)) { // Extract Y/M/D from UTC to get the intended date year = date.getUTCFullYear(); month = date.getUTCMonth(); day = date.getUTCDate(); } // Case 3: Local date formats or Date objects created with constructor // These use local time values directly else { year = date.getFullYear(); month = date.getMonth(); day = date.getDate(); } // Return a new Date representing local midnight of the extracted date // This ensures the visual date is preserved across all timezones return new Date(year, month, day, 0, 0, 0, 0); }; /** * Detects whether a Date object originated from ISO string parsing. * * **Detection Pattern:** * ISO dates like "2025-04-01" are parsed by JavaScript as UTC midnight (00:00:00 UTC). * When a non-UTC timezone is active, the browser applies an offset, creating a distinctive pattern: * - UTC time IS midnight (the parsed UTC value) * - Local time is NOT midnight (timezone offset applied) * * This pattern is unique to ISO-parsed dates and doesn't occur with: * - Local constructor dates: new Date(2025, 3, 1) → UTC offset is applied backward * - Explicit times: getUTCHours() ≠ 0 → not UTC midnight * * **Examples (IST timezone, UTC+5:30):** * * ISO-parsed (returns true): * ``` * new Date("2025-04-01") * → UTC: 2025-04-01 00:00:00.000 ✓ (midnight) * → Local: 2025-04-01 05:30:00 ✓ (NOT midnight, offset applied) * ``` * * Local constructor (returns false): * ``` * new Date(2025, 3, 1) * → UTC: 2025-03-31 18:30:00.000 ✗ (NOT midnight) * → Local: 2025-04-01 00:00:00 (midnight) * ``` * * Explicit time (returns false): * ``` * new Date("2025-04-01T14:30:00Z") * → UTC: 2025-04-01 14:30:00.000 ✗ (NOT midnight) * → Local: 2025-04-01 20:00:00 * ``` * * @param {Date} date - The Date object to analyze * @returns {boolean} true if Date originated from ISO string parsing, false otherwise * * @private */ EventMarker.prototype.isISODateObject = function (date) { // Check 1: UTC time must be midnight (00:00:00.000) var isUTCMidnight = (date.getUTCHours() === 0 && date.getUTCMinutes() === 0 && date.getUTCSeconds() === 0 && date.getUTCMilliseconds() === 0); // Check 2: Local time must NOT be midnight (indicates timezone offset was applied) var isLocalNotMidnight = !(date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0); // Both conditions indicate ISO origin: UTC is midnight but local is not return isUTCMidnight && isLocalNotMidnight; }; /** * @returns {void} . * @private */ EventMarker.prototype.updateContainerHeight = function () { if (this.eventMarkersContainer) { this.eventMarkersContainer.style.height = formatUnit(this.parent.getContentHeight()); } }; return EventMarker; }()); export { EventMarker };