UNPKG

@schedule-x/calendar

Version:

Schedule-X calendar component

1,160 lines (1,111 loc) 233 kB
import { createContext, render, createElement, Fragment as Fragment$1 } from 'preact'; import { jsx, Fragment, jsxs } from 'preact/jsx-runtime'; import { useContext, useEffect, useState, useMemo } from 'preact/hooks'; import { createPortal } from 'preact/compat'; import { useSignalEffect, signal, effect, computed, batch } from '@preact/signals'; const AppContext$1 = createContext({}); const DateFormats = { DATE_STRING: /^\d{4}-\d{2}-\d{2}$/, DATE_TIME_STRING: /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/, }; class InvalidDateTimeError extends Error { constructor(dateTimeSpecification) { super(`Invalid date time specification: ${dateTimeSpecification}`); } } const toJSDate = (dateTimeSpecification) => { if (!DateFormats.DATE_TIME_STRING.test(dateTimeSpecification) && !DateFormats.DATE_STRING.test(dateTimeSpecification)) throw new InvalidDateTimeError(dateTimeSpecification); return new Date(Number(dateTimeSpecification.slice(0, 4)), Number(dateTimeSpecification.slice(5, 7)) - 1, Number(dateTimeSpecification.slice(8, 10)), Number(dateTimeSpecification.slice(11, 13)), // for date strings this will be 0 Number(dateTimeSpecification.slice(14, 16)) // for date strings this will be 0 ); }; const toIntegers = (dateTimeSpecification) => { const hours = dateTimeSpecification.slice(11, 13), minutes = dateTimeSpecification.slice(14, 16); return { year: Number(dateTimeSpecification.slice(0, 4)), month: Number(dateTimeSpecification.slice(5, 7)) - 1, date: Number(dateTimeSpecification.slice(8, 10)), hours: hours !== '' ? Number(hours) : undefined, minutes: minutes !== '' ? Number(minutes) : undefined, }; }; const toLocalizedMonth = (date, locale) => { return date.toLocaleString(locale, { month: 'long' }); }; const toLocalizedDateString = (date, locale) => { return date.toLocaleString(locale, { month: 'numeric', day: 'numeric', year: 'numeric', }); }; const getOneLetterDayNames = (week, locale) => { return week.map((date) => { return date.toLocaleString(locale, { weekday: 'short' }).charAt(0); }); }; const getDayNameShort = (date, locale) => { if (locale === 'he-IL') { return date.toLocaleString(locale, { weekday: 'narrow' }); } return date.toLocaleString(locale, { weekday: 'short' }); }; const getDayNamesShort = (week, locale) => { return week.map((date) => getDayNameShort(date, locale)); }; const getOneLetterOrShortDayNames = (week, locale) => { if (['zh-cn', 'zh-tw', 'ca-es', 'he-il'].includes(locale.toLowerCase())) { return getDayNamesShort(week, locale); } return getOneLetterDayNames(week, locale); }; var img = "data:image/svg+xml,%3c%3fxml version='1.0' encoding='utf-8'%3f%3e%3c!-- Uploaded to: SVG Repo%2c www.svgrepo.com%2c Generator: SVG Repo Mixer Tools --%3e%3csvg width='800px' height='800px' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M6 9L12 15L18 9' stroke='%23DED8E1' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e"; /** * Can be used for generating a random id for an entity * Should, however, never be used in potentially resource intense loops, * since the performance cost of this compared to new Date().getTime() is ca x4 in v8 * */ const randomStringId = () => 's' + Math.random().toString(36).substring(2, 11); const isKeyEnterOrSpace = (keyboardEvent) => keyboardEvent.key === 'Enter' || keyboardEvent.key === ' '; function AppInput() { const datePickerInputId = randomStringId(); const datePickerLabelId = randomStringId(); const inputWrapperId = randomStringId(); const $app = useContext(AppContext$1); const getLocalizedDate = (dateString) => { if (dateString === '') return $app.translate('MM/DD/YYYY'); return toLocalizedDateString(toJSDate(dateString), $app.config.locale.value); }; useEffect(() => { $app.datePickerState.inputDisplayedValue.value = getLocalizedDate($app.datePickerState.selectedDate.value); }, [$app.datePickerState.selectedDate.value, $app.config.locale.value]); const [wrapperClasses, setWrapperClasses] = useState([]); const setInputElement = () => { const inputWrapperEl = document.getElementById(inputWrapperId); $app.datePickerState.inputWrapperElement.value = inputWrapperEl instanceof HTMLDivElement ? inputWrapperEl : undefined; }; useEffect(() => { if ($app.config.teleportTo) setInputElement(); const newClasses = ['sx__date-input-wrapper']; if ($app.datePickerState.isOpen.value) newClasses.push('sx__date-input--active'); setWrapperClasses(newClasses); }, [$app.datePickerState.isOpen.value]); const handleKeyUp = (event) => { if (event.key === 'Enter') handleInputValue(event); }; const handleInputValue = (event) => { event.stopPropagation(); // prevent date picker from closing try { $app.datePickerState.inputDisplayedValue.value = event.target.value; $app.datePickerState.close(); } catch (e) { console.log('Error setting input value:' + e); } }; useEffect(() => { const inputElement = document.getElementById(datePickerInputId); if (inputElement === null) return; inputElement.addEventListener('change', handleInputValue); // Preact onChange triggers on every input return () => inputElement.removeEventListener('change', handleInputValue); }); const handleClick = (event) => { handleInputValue(event); $app.datePickerState.open(); }; const handleButtonKeyDown = (keyboardEvent) => { if (isKeyEnterOrSpace(keyboardEvent)) { keyboardEvent.preventDefault(); $app.datePickerState.open(); setTimeout(() => { const element = document.querySelector('[data-focus="true"]'); if (element instanceof HTMLElement) element.focus(); }, 50); } }; return (jsx(Fragment, { children: jsxs("div", { className: wrapperClasses.join(' '), id: inputWrapperId, children: [jsx("label", { for: datePickerInputId, id: datePickerLabelId, className: "sx__date-input-label", children: $app.config.label || $app.translate('Date') }), jsx("input", { id: datePickerInputId, tabIndex: $app.datePickerState.isDisabled.value ? -1 : 0, name: $app.config.name || 'date', "aria-describedby": datePickerLabelId, value: $app.datePickerState.inputDisplayedValue.value, "data-testid": "date-picker-input", className: "sx__date-input", onClick: handleClick, onKeyUp: handleKeyUp, type: "text" }), jsx("button", { type: "button", tabIndex: $app.datePickerState.isDisabled.value ? -1 : 0, "aria-label": $app.translate('Choose Date'), onKeyDown: handleButtonKeyDown, onClick: () => $app.datePickerState.open(), className: "sx__date-input-chevron-wrapper", children: jsx("img", { className: "sx__date-input-chevron", src: img, alt: "" }) })] }) })); } var DatePickerView; (function (DatePickerView) { DatePickerView["MONTH_DAYS"] = "month-days"; DatePickerView["YEARS"] = "years"; })(DatePickerView || (DatePickerView = {})); const YEARS_VIEW = 'years-view'; const MONTH_VIEW = 'months-view'; const DATE_PICKER_WEEK = 'date-picker-week'; class NumberRangeError extends Error { constructor(min, max) { super(`Number must be between ${min} and ${max}.`); Object.defineProperty(this, "min", { enumerable: true, configurable: true, writable: true, value: min }); Object.defineProperty(this, "max", { enumerable: true, configurable: true, writable: true, value: max }); } } const doubleDigit = (number) => { if (number < 0 || number > 99) throw new NumberRangeError(0, 99); return String(number).padStart(2, '0'); }; const toDateString$1 = (date) => { return `${date.getFullYear()}-${doubleDigit(date.getMonth() + 1)}-${doubleDigit(date.getDate())}`; }; const toTimeString = (date) => { return `${doubleDigit(date.getHours())}:${doubleDigit(date.getMinutes())}`; }; const toDateTimeString = (date) => { return `${toDateString$1(date)} ${toTimeString(date)}`; }; const addMonths = (to, nMonths) => { const { year, month, date, hours, minutes } = toIntegers(to); const isDateTimeString = hours !== undefined && minutes !== undefined; const jsDate = new Date(year, month, date, hours !== null && hours !== void 0 ? hours : 0, minutes !== null && minutes !== void 0 ? minutes : 0); let expectedMonth = (jsDate.getMonth() + nMonths) % 12; if (expectedMonth < 0) expectedMonth += 12; jsDate.setMonth(jsDate.getMonth() + nMonths); // handle date overflow and underflow if (jsDate.getMonth() > expectedMonth) { jsDate.setDate(0); } else if (jsDate.getMonth() < expectedMonth) { jsDate.setMonth(jsDate.getMonth() + 1); jsDate.setDate(0); } if (isDateTimeString) { return toDateTimeString(jsDate); } return toDateString$1(jsDate); }; const addDays = (to, nDays) => { const { year, month, date, hours, minutes } = toIntegers(to); const isDateTimeString = hours !== undefined && minutes !== undefined; const jsDate = new Date(year, month, date, hours !== null && hours !== void 0 ? hours : 0, minutes !== null && minutes !== void 0 ? minutes : 0); jsDate.setDate(jsDate.getDate() + nDays); if (isDateTimeString) { return toDateTimeString(jsDate); } return toDateString$1(jsDate); }; const dateFromDateTime = (dateTime) => { return dateTime.slice(0, 10); }; const timeFromDateTime = (dateTime) => { return dateTime.slice(11); }; const setDateOfMonth = (dateString, date) => { dateString = dateString.slice(0, 8) + doubleDigit(date) + dateString.slice(10); return dateString; }; const getFirstDayOPreviousMonth = (dateString) => { dateString = addMonths(dateString, -1); return setDateOfMonth(dateString, 1); }; const getFirstDayOfNextMonth = (dateString) => { dateString = addMonths(dateString, 1); return setDateOfMonth(dateString, 1); }; const setTimeInDateTimeString = (dateTimeString, newTime) => { const dateCache = toDateString$1(toJSDate(dateTimeString)); return `${dateCache} ${newTime}`; }; function Chevron({ direction, onClick, buttonText, disabled = false, }) { const handleKeyDown = (keyboardEvent) => { if (isKeyEnterOrSpace(keyboardEvent)) onClick(); }; return (jsx("button", { type: "button", disabled: disabled, className: "sx__chevron-wrapper sx__ripple", onMouseUp: onClick, onKeyDown: handleKeyDown, tabIndex: 0, children: jsx("i", { className: `sx__chevron sx__chevron--${direction}`, children: buttonText }) })); } function MonthViewHeader({ setYearsView }) { const $app = useContext(AppContext$1); const dateStringToLocalizedMonthName = (selectedDate) => { const selectedDateJS = toJSDate(selectedDate); return toLocalizedMonth(selectedDateJS, $app.config.locale.value); }; const getYearFrom = (datePickerDate) => { return toIntegers(datePickerDate).year; }; const [selectedDateMonthName, setSelectedDateMonthName] = useState(dateStringToLocalizedMonthName($app.datePickerState.datePickerDate.value)); const [datePickerYear, setDatePickerYear] = useState(getYearFrom($app.datePickerState.datePickerDate.value)); const setPreviousMonth = () => { $app.datePickerState.datePickerDate.value = getFirstDayOPreviousMonth($app.datePickerState.datePickerDate.value); }; const setNextMonth = () => { $app.datePickerState.datePickerDate.value = getFirstDayOfNextMonth($app.datePickerState.datePickerDate.value); }; useEffect(() => { setSelectedDateMonthName(dateStringToLocalizedMonthName($app.datePickerState.datePickerDate.value)); setDatePickerYear(getYearFrom($app.datePickerState.datePickerDate.value)); }, [$app.datePickerState.datePickerDate.value]); const handleOpenYearsView = (e) => { e.stopPropagation(); setYearsView(); }; return (jsx(Fragment, { children: jsxs("header", { className: "sx__date-picker__month-view-header", children: [jsx(Chevron, { direction: 'previous', onClick: () => setPreviousMonth(), buttonText: $app.translate('Previous month') }), jsx("button", { type: "button", className: "sx__date-picker__month-view-header__month-year", onClick: (event) => handleOpenYearsView(event), children: selectedDateMonthName + ' ' + datePickerYear }), jsx(Chevron, { direction: 'next', onClick: () => setNextMonth(), buttonText: $app.translate('Next month') })] }) })); } function DayNames() { const $app = useContext(AppContext$1); const aWeek = $app.timeUnitsImpl.getWeekFor(toJSDate($app.datePickerState.datePickerDate.value)); const dayNames = getOneLetterOrShortDayNames(aWeek, $app.config.locale.value); return (jsx("div", { className: "sx__date-picker__day-names", children: dayNames.map((dayName) => (jsx("span", { "data-testid": "day-name", className: "sx__date-picker__day-name", children: dayName }))) })); } const isToday = (date) => { const today = new Date(); return (date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear()); }; const isSameMonth = (date1, date2) => { return (date1.getMonth() === date2.getMonth() && date1.getFullYear() === date2.getFullYear()); }; /** * Origin of SVG: https://www.svgrepo.com/svg/506771/time * License: PD License * Author Salah Elimam * Author website: https://www.figma.com/@salahelimam * */ function TimeIcon({ strokeColor }) { return (jsx(Fragment, { children: jsxs("svg", { className: "sx__event-icon", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("g", { id: "SVGRepo_bgCarrier", "stroke-width": "0" }), jsx("g", { id: "SVGRepo_tracerCarrier", "stroke-linecap": "round", "stroke-linejoin": "round" }), jsxs("g", { id: "SVGRepo_iconCarrier", children: [jsx("path", { d: "M12 8V12L15 15", stroke: strokeColor, "stroke-width": "2", "stroke-linecap": "round" }), jsx("circle", { cx: "12", cy: "12", r: "9", stroke: strokeColor, "stroke-width": "2" })] })] }) })); } /** * Origin of SVG: https://www.svgrepo.com/svg/506772/user * License: PD License * Author Salah Elimam * Author website: https://www.figma.com/@salahelimam * */ function UserIcon({ strokeColor }) { return (jsx(Fragment, { children: jsxs("svg", { className: "sx__event-icon", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("g", { id: "SVGRepo_bgCarrier", "stroke-width": "0" }), jsx("g", { id: "SVGRepo_tracerCarrier", "stroke-linecap": "round", "stroke-linejoin": "round" }), jsxs("g", { id: "SVGRepo_iconCarrier", children: [jsx("path", { d: "M15 7C15 8.65685 13.6569 10 12 10C10.3431 10 9 8.65685 9 7C9 5.34315 10.3431 4 12 4C13.6569 4 15 5.34315 15 7Z", stroke: strokeColor, "stroke-width": "2" }), jsx("path", { d: "M5 19.5C5 15.9101 7.91015 13 11.5 13H12.5C16.0899 13 19 15.9101 19 19.5V20C19 20.5523 18.5523 21 18 21H6C5.44772 21 5 20.5523 5 20V19.5Z", stroke: strokeColor, "stroke-width": "2" })] })] }) })); } /** * Origin of SVG: https://www.svgrepo.com/svg/489502/location-pin * License: PD License * Author: Dariush Habibpour * Author website: https://redl.ink/dariush/links?ref=svgrepo.com * */ function LocationPinIcon({ strokeColor }) { return (jsx(Fragment, { children: jsxs("svg", { className: "sx__event-icon", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("g", { id: "SVGRepo_bgCarrier", "stroke-width": "0" }), jsx("g", { id: "SVGRepo_tracerCarrier", "stroke-linecap": "round", "stroke-linejoin": "round" }), jsxs("g", { id: "SVGRepo_iconCarrier", children: [jsxs("g", { "clip-path": "url(#clip0_429_11046)", children: [jsx("rect", { x: "12", y: "11", width: "0.01", height: "0.01", stroke: strokeColor, "stroke-width": "2", "stroke-linejoin": "round" }), jsx("path", { d: "M12 22L17.5 16.5C20.5376 13.4624 20.5376 8.53757 17.5 5.5C14.4624 2.46244 9.53757 2.46244 6.5 5.5C3.46244 8.53757 3.46244 13.4624 6.5 16.5L12 22Z", stroke: strokeColor, "stroke-width": "2", "stroke-linejoin": "round" })] }), jsx("defs", { children: jsx("clipPath", { id: "clip0_429_11046", children: jsx("rect", { width: "24", height: "24", fill: "white" }) }) })] })] }) })); } // regex for strings between 00:00 and 23:59 const timeStringRegex = /^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/; const dateTimeStringRegex = /^(\d{4})-(\d{2})-(\d{2}) (0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/; const dateStringRegex = /^(\d{4})-(\d{2})-(\d{2})$/; class InvalidTimeStringError extends Error { constructor(timeString) { super(`Invalid time string: ${timeString}`); } } const minuteTimePointMultiplier = 1.6666666666666667; // 100 / 60 const timePointsFromString = (timeString) => { if (!timeStringRegex.test(timeString) && timeString !== '24:00') throw new InvalidTimeStringError(timeString); const [hoursInt, minutesInt] = timeString .split(':') .map((time) => parseInt(time, 10)); let minutePoints = (minutesInt * minuteTimePointMultiplier).toString(); if (minutePoints.split('.')[0].length < 2) minutePoints = `0${minutePoints}`; return Number(hoursInt + minutePoints); }; const timeStringFromTimePoints = (timePoints) => { const hours = Math.floor(timePoints / 100); const minutes = Math.round((timePoints % 100) / minuteTimePointMultiplier); return `${doubleDigit(hours)}:${doubleDigit(minutes)}`; }; const addTimePointsToDateTime = (dateTimeString, pointsToAdd) => { const minutesToAdd = pointsToAdd / minuteTimePointMultiplier; const jsDate = toJSDate(dateTimeString); jsDate.setMinutes(jsDate.getMinutes() + minutesToAdd); return toDateTimeString(jsDate); }; var WeekDay; (function (WeekDay) { WeekDay[WeekDay["SUNDAY"] = 0] = "SUNDAY"; WeekDay[WeekDay["MONDAY"] = 1] = "MONDAY"; WeekDay[WeekDay["TUESDAY"] = 2] = "TUESDAY"; WeekDay[WeekDay["WEDNESDAY"] = 3] = "WEDNESDAY"; WeekDay[WeekDay["THURSDAY"] = 4] = "THURSDAY"; WeekDay[WeekDay["FRIDAY"] = 5] = "FRIDAY"; WeekDay[WeekDay["SATURDAY"] = 6] = "SATURDAY"; })(WeekDay || (WeekDay = {})); const DEFAULT_LOCALE = 'en-US'; const DEFAULT_FIRST_DAY_OF_WEEK = WeekDay.MONDAY; const DEFAULT_EVENT_COLOR_NAME = 'primary'; class CalendarEventImpl { constructor(_config, id, start, end, title, people, location, description, calendarId, _options = undefined, _customContent = {}, _foreignProperties = {}) { Object.defineProperty(this, "_config", { enumerable: true, configurable: true, writable: true, value: _config }); Object.defineProperty(this, "id", { enumerable: true, configurable: true, writable: true, value: id }); Object.defineProperty(this, "start", { enumerable: true, configurable: true, writable: true, value: start }); Object.defineProperty(this, "end", { enumerable: true, configurable: true, writable: true, value: end }); Object.defineProperty(this, "title", { enumerable: true, configurable: true, writable: true, value: title }); Object.defineProperty(this, "people", { enumerable: true, configurable: true, writable: true, value: people }); Object.defineProperty(this, "location", { enumerable: true, configurable: true, writable: true, value: location }); Object.defineProperty(this, "description", { enumerable: true, configurable: true, writable: true, value: description }); Object.defineProperty(this, "calendarId", { enumerable: true, configurable: true, writable: true, value: calendarId }); Object.defineProperty(this, "_options", { enumerable: true, configurable: true, writable: true, value: _options }); Object.defineProperty(this, "_customContent", { enumerable: true, configurable: true, writable: true, value: _customContent }); Object.defineProperty(this, "_foreignProperties", { enumerable: true, configurable: true, writable: true, value: _foreignProperties }); Object.defineProperty(this, "_previousConcurrentEvents", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_totalConcurrentEvents", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_maxConcurrentEvents", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_nDaysInGrid", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_createdAt", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_eventFragments", { enumerable: true, configurable: true, writable: true, value: {} }); } get _isSingleDayTimed() { return (dateTimeStringRegex.test(this.start) && dateTimeStringRegex.test(this.end) && dateFromDateTime(this.start) === dateFromDateTime(this.end)); } get _isSingleDayFullDay() { return (dateStringRegex.test(this.start) && dateStringRegex.test(this.end) && this.start === this.end); } get _isMultiDayTimed() { return (dateTimeStringRegex.test(this.start) && dateTimeStringRegex.test(this.end) && dateFromDateTime(this.start) !== dateFromDateTime(this.end)); } get _isMultiDayFullDay() { return (dateStringRegex.test(this.start) && dateStringRegex.test(this.end) && this.start !== this.end); } get _isSingleHybridDayTimed() { if (!this._config.isHybridDay) return false; if (!dateTimeStringRegex.test(this.start) || !dateTimeStringRegex.test(this.end)) return false; const startDate = dateFromDateTime(this.start); const endDate = dateFromDateTime(this.end); const endDateMinusOneDay = toDateString$1(new Date(toJSDate(endDate).getTime() - 86400000)); if (startDate !== endDate && startDate !== endDateMinusOneDay) return false; const dayBoundaries = this._config.dayBoundaries.value; const eventStartTimePoints = timePointsFromString(timeFromDateTime(this.start)); const eventEndTimePoints = timePointsFromString(timeFromDateTime(this.end)); return ((eventStartTimePoints >= dayBoundaries.start && (eventEndTimePoints <= dayBoundaries.end || eventEndTimePoints > eventStartTimePoints)) || (eventStartTimePoints < dayBoundaries.end && eventEndTimePoints <= dayBoundaries.end)); } get _color() { if (this.calendarId && this._config.calendars.value && this.calendarId in this._config.calendars.value) { return this._config.calendars.value[this.calendarId].colorName; } return DEFAULT_EVENT_COLOR_NAME; } _getForeignProperties() { return this._foreignProperties; } _getExternalEvent() { return { id: this.id, start: this.start, end: this.end, title: this.title, people: this.people, location: this.location, description: this.description, calendarId: this.calendarId, _options: this._options, ...this._getForeignProperties(), }; } } class CalendarEventBuilder { constructor(_config, id, start, end) { Object.defineProperty(this, "_config", { enumerable: true, configurable: true, writable: true, value: _config }); Object.defineProperty(this, "id", { enumerable: true, configurable: true, writable: true, value: id }); Object.defineProperty(this, "start", { enumerable: true, configurable: true, writable: true, value: start }); Object.defineProperty(this, "end", { enumerable: true, configurable: true, writable: true, value: end }); Object.defineProperty(this, "people", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "location", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "description", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "title", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "calendarId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_foreignProperties", { enumerable: true, configurable: true, writable: true, value: {} }); Object.defineProperty(this, "_options", { enumerable: true, configurable: true, writable: true, value: undefined }); Object.defineProperty(this, "_customContent", { enumerable: true, configurable: true, writable: true, value: {} }); } build() { return new CalendarEventImpl(this._config, this.id, this.start, this.end, this.title, this.people, this.location, this.description, this.calendarId, this._options, this._customContent, this._foreignProperties); } withTitle(title) { this.title = title; return this; } withPeople(people) { this.people = people; return this; } withLocation(location) { this.location = location; return this; } withDescription(description) { this.description = description; return this; } withForeignProperties(foreignProperties) { this._foreignProperties = foreignProperties; return this; } withCalendarId(calendarId) { this.calendarId = calendarId; return this; } withOptions(options) { this._options = options; return this; } withCustomContent(customContent) { this._customContent = customContent; return this; } } const deepCloneEvent = (calendarEvent, $app) => { const calendarEventInternal = new CalendarEventBuilder($app.config, calendarEvent.id, calendarEvent.start, calendarEvent.end) .withTitle(calendarEvent.title) .withPeople(calendarEvent.people) .withCalendarId(calendarEvent.calendarId) .withForeignProperties(JSON.parse(JSON.stringify(calendarEvent._getForeignProperties()))) .withLocation(calendarEvent.location) .withDescription(calendarEvent.description) .withOptions(calendarEvent._options) .withCustomContent(calendarEvent._customContent) .build(); calendarEventInternal._nDaysInGrid = calendarEvent._nDaysInGrid; return calendarEventInternal; }; const concatenatePeople = (people) => { return people.reduce((acc, person, index) => { if (index === 0) return person; if (index === people.length - 1) return `${acc} & ${person}`; return `${acc}, ${person}`; }, ''); }; const dateFn = (dateTimeString, locale) => { const { year, month, date } = toIntegers(dateTimeString); return new Date(year, month, date).toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric', }); }; const getLocalizedDate = dateFn; const timeFn = (dateTimeString, locale) => { const { year, month, date, hours, minutes } = toIntegers(dateTimeString); return new Date(year, month, date, hours, minutes).toLocaleTimeString(locale, { hour: 'numeric', minute: 'numeric', }); }; const getTimeStamp = (calendarEvent, // to facilitate testing. In reality, we will always have a full CalendarEventInternal locale, delimiter = '\u2013') => { const eventTime = { start: calendarEvent.start, end: calendarEvent.end }; if (calendarEvent._isSingleDayFullDay) { return dateFn(eventTime.start, locale); } if (calendarEvent._isMultiDayFullDay) { return `${dateFn(eventTime.start, locale)} ${delimiter} ${dateFn(eventTime.end, locale)}`; } if (calendarEvent._isSingleDayTimed && eventTime.start !== eventTime.end) { return `${dateFn(eventTime.start, locale)} <span aria-hidden="true">⋅</span> ${timeFn(eventTime.start, locale)} ${delimiter} ${timeFn(eventTime.end, locale)}`; } if (calendarEvent._isSingleDayTimed && calendarEvent.start === calendarEvent.end) { return `${dateFn(eventTime.start, locale)}, ${timeFn(eventTime.start, locale)}`; } return `${dateFn(eventTime.start, locale)}, ${timeFn(eventTime.start, locale)} ${delimiter} ${dateFn(eventTime.end, locale)}, ${timeFn(eventTime.end, locale)}`; }; function MonthViewWeek({ week }) { const $app = useContext(AppContext$1); const weekDays = week.map((day) => { const classes = ['sx__date-picker__day']; if (isToday(day)) classes.push('sx__date-picker__day--today'); if (toDateString$1(day) === $app.datePickerState.selectedDate.value) classes.push('sx__date-picker__day--selected'); if (!isSameMonth(day, toJSDate($app.datePickerState.datePickerDate.value))) classes.push('is-leading-or-trailing'); return { day, classes, }; }); const isDateSelectable = (date) => { const dateString = toDateString$1(date); return dateString >= $app.config.min && dateString <= $app.config.max; }; const selectDate = (date) => { $app.datePickerState.selectedDate.value = toDateString$1(date); $app.datePickerState.close(); }; const hasFocus = (weekDay) => toDateString$1(weekDay.day) === $app.datePickerState.datePickerDate.value; const handleKeyDown = (event) => { if (event.key === 'Enter') { $app.datePickerState.selectedDate.value = $app.datePickerState.datePickerDate.value; $app.datePickerState.close(); return; } const keyMapDaysToAdd = new Map([ ['ArrowDown', 7], ['ArrowUp', -7], ['ArrowLeft', -1], ['ArrowRight', 1], ]); $app.datePickerState.datePickerDate.value = addDays($app.datePickerState.datePickerDate.value, keyMapDaysToAdd.get(event.key) || 0); }; return (jsx(Fragment, { children: jsx("div", { "data-testid": DATE_PICKER_WEEK, className: "sx__date-picker__week", children: weekDays.map((weekDay) => (jsx("button", { type: "button", tabIndex: hasFocus(weekDay) ? 0 : -1, disabled: !isDateSelectable(weekDay.day), "aria-label": getLocalizedDate($app.datePickerState.datePickerDate.value, $app.config.locale.value), className: weekDay.classes.join(' '), "data-focus": hasFocus(weekDay) ? 'true' : undefined, onClick: () => selectDate(weekDay.day), onKeyDown: handleKeyDown, children: weekDay.day.getDate() }))) }) })); } function MonthView({ seatYearsView }) { const elementId = randomStringId(); const $app = useContext(AppContext$1); const [month, setMonth] = useState([]); const renderMonth = () => { const newDatePickerDate = toJSDate($app.datePickerState.datePickerDate.value); setMonth($app.timeUnitsImpl.getMonthWithTrailingAndLeadingDays(newDatePickerDate.getFullYear(), newDatePickerDate.getMonth())); }; useEffect(() => { renderMonth(); }, [$app.datePickerState.datePickerDate.value]); useEffect(() => { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { const mutatedElement = mutation.target; if (mutatedElement.dataset.focus === 'true') mutatedElement.focus(); }); }); const monthViewElement = document.getElementById(elementId); observer.observe(monthViewElement, { childList: true, subtree: true, attributes: true, }); return () => observer.disconnect(); }, []); return (jsx(Fragment, { children: jsxs("div", { id: elementId, "data-testid": MONTH_VIEW, className: "sx__date-picker__month-view", children: [jsx(MonthViewHeader, { setYearsView: seatYearsView }), jsx(DayNames, {}), month.map((week) => (jsx(MonthViewWeek, { week: week })))] }) })); } function YearsViewAccordion({ year, setYearAndMonth, isExpanded, expand, }) { const $app = useContext(AppContext$1); const yearWithDates = $app.timeUnitsImpl.getMonthsFor(year); const handleClickOnMonth = (event, month) => { event.stopPropagation(); setYearAndMonth(year, month.getMonth()); }; return (jsx(Fragment, { children: jsxs("li", { className: isExpanded ? 'sx__is-expanded' : '', children: [jsx("button", { type: "button", className: "sx__date-picker__years-accordion__expand-button sx__ripple--wide", onClick: () => expand(year), children: year }), isExpanded && (jsx("div", { className: "sx__date-picker__years-view-accordion__panel", children: yearWithDates.map((month) => (jsx("button", { type: "button", className: "sx__date-picker__years-view-accordion__month", onClick: (event) => handleClickOnMonth(event, month), children: toLocalizedMonth(month, $app.config.locale.value) }))) }))] }) })); } function YearsView({ setMonthView }) { const $app = useContext(AppContext$1); const minYear = toJSDate($app.config.min).getFullYear(); const maxYear = toJSDate($app.config.max).getFullYear(); const years = Array.from({ length: maxYear - minYear + 1 }, (_, i) => minYear + i); const { year: selectedYear } = toIntegers($app.datePickerState.selectedDate.value); const [expandedYear, setExpandedYear] = useState(selectedYear); const setNewDatePickerDate = (year, month) => { $app.datePickerState.datePickerDate.value = toDateString$1(new Date(year, month, 1)); setMonthView(); }; useEffect(() => { var _a; const initiallyExpandedYear = (_a = document .querySelector('.sx__date-picker__years-view')) === null || _a === void 0 ? void 0 : _a.querySelector('.sx__is-expanded'); if (!initiallyExpandedYear) return; initiallyExpandedYear.scrollIntoView({ block: 'center', }); }, []); return (jsx(Fragment, { children: jsx("ul", { className: "sx__date-picker__years-view", "data-testid": YEARS_VIEW, children: years.map((year) => (jsx(YearsViewAccordion, { year: year, setYearAndMonth: (year, month) => setNewDatePickerDate(year, month), isExpanded: expandedYear === year, expand: (year) => setExpandedYear(year) }))) }) })); } const isScrollable = (el) => { if (el) { const hasScrollableContent = el.scrollHeight > el.clientHeight; const overflowYStyle = window.getComputedStyle(el).overflowY; const isOverflowHidden = overflowYStyle.indexOf('hidden') !== -1; return hasScrollableContent && !isOverflowHidden; } return true; }; const getScrollableParents = (el, acc = []) => { if (!el || el === document.body || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { acc.push(window); return acc; } if (isScrollable(el)) { acc.push(el); } return getScrollableParents((el.assignedSlot ? el.assignedSlot.parentNode : el.parentNode), acc); }; const POPUP_CLASS_NAME = 'sx__date-picker-popup'; function AppPopup() { const $app = useContext(AppContext$1); const [datePickerView, setDatePickerView] = useState(DatePickerView.MONTH_DAYS); const classList = useMemo(() => { const returnValue = [ POPUP_CLASS_NAME, $app.datePickerState.isDark.value ? 'is-dark' : '', $app.config.teleportTo ? 'is-teleported' : '', ]; if ($app.config.placement && !$app.config.teleportTo) { returnValue.push($app.config.placement); } return returnValue; }, [ $app.datePickerState.isDark.value, $app.config.placement, $app.config.teleportTo, ]); const clickOutsideListener = (event) => { const target = event.target; if (!target.closest(`.${POPUP_CLASS_NAME}`)) $app.datePickerState.close(); }; const escapeKeyListener = (e) => { if (e.key === 'Escape') { if ($app.config.listeners.onEscapeKeyDown) $app.config.listeners.onEscapeKeyDown($app); else $app.datePickerState.close(); } }; useEffect(() => { document.addEventListener('click', clickOutsideListener); document.addEventListener('keydown', escapeKeyListener); return () => { document.removeEventListener('click', clickOutsideListener); document.removeEventListener('keydown', escapeKeyListener); }; }, []); const remSize = Number(getComputedStyle(document.documentElement).fontSize.split('px')[0]); const popupHeight = 362; const popupWidth = 332; const getFixedPositionStyles = () => { const inputWrapperEl = $app.datePickerState.inputWrapperElement.value; const inputRect = inputWrapperEl === null || inputWrapperEl === void 0 ? void 0 : inputWrapperEl.getBoundingClientRect(); if (inputWrapperEl === undefined || !(inputRect instanceof DOMRect)) return undefined; return { top: $app.config.placement.includes('bottom') ? inputRect.height + inputRect.y + 1 // 1px border : inputRect.y - remSize - popupHeight, // subtract remsize to leave room for label text left: $app.config.placement.includes('start') ? inputRect.x : inputRect.x + inputRect.width - popupWidth, width: popupWidth, position: 'fixed', }; }; const [fixedPositionStyle, setFixedPositionStyle] = useState(getFixedPositionStyles()); useEffect(() => { const inputWrapper = $app.datePickerState.inputWrapperElement.value; if (inputWrapper === undefined) return; const scrollableParents = getScrollableParents(inputWrapper); const scrollListener = () => setFixedPositionStyle(getFixedPositionStyles()); scrollableParents.forEach((parent) => parent.addEventListener('scroll', scrollListener)); return () => scrollableParents.forEach((parent) => parent.removeEventListener('scroll', scrollListener)); }, []); return (jsx(Fragment, { children: jsx("div", { style: $app.config.teleportTo ? fixedPositionStyle : undefined, "data-testid": "date-picker-popup", className: classList.join(' '), children: datePickerView === DatePickerView.MONTH_DAYS ? (jsx(MonthView, { seatYearsView: () => setDatePickerView(DatePickerView.YEARS) })) : (jsx(YearsView, { setMonthView: () => setDatePickerView(DatePickerView.MONTH_DAYS) })) }) })); } function AppWrapper({ $app }) { const initialClassList = ['sx__date-picker-wrapper']; const [classList, setClassList] = useState(initialClassList); useEffect(() => { var _a; const list = [...initialClassList]; if ($app.datePickerState.isDark.value) list.push('is-dark'); if ((_a = $app.config.style) === null || _a === void 0 ? void 0 : _a.fullWidth) list.push('has-full-width'); if ($app.datePickerState.isDisabled.value) list.push('is-disabled'); setClassList(list); }, [$app.datePickerState.isDark.value, $app.datePickerState.isDisabled.value]); let appPopupJSX = jsx(AppPopup, {}); if ($app.config.teleportTo) appPopupJSX = createPortal(appPopupJSX, $app.config.teleportTo); return (jsx(Fragment, { children: jsx("div", { className: classList.join(' '), children: jsxs(AppContext$1.Provider, { value: $app, children: [jsx(AppInput, {}), $app.datePickerState.isOpen.value && appPopupJSX] }) }) })); } const AppContext = createContext({}); class DatePickerAppSingletonImpl { constructor(datePickerState, config, timeUnitsImpl, translate) { Object.defineProperty(this, "datePickerState", { enumerable: true, configurable: true, writable: true, value: datePickerState }); Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: config }); Object.defineProperty(this, "timeUnitsImpl", { enumerable: true, configurable: true, writable: true, value: timeUnitsImpl }); Object.defineProperty(this, "translate", { enumerable: true, configurable: true, writable: true, value: translate }); } } class DatePickerAppSingletonBuilder { constructor() { Object.defineProperty(this, "datePickerState", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "timeUnitsImpl", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "translate", { enumerable: true, configurable: true, writable: true, value: void 0 }); } build() { return new DatePickerAppSingletonImpl(this.datePickerState, this.config, this.timeUnitsImpl, this.translate); } withDatePickerState(datePickerState) { this.datePickerState = datePickerState; return this; } withConfig(config) { this.config = config; return this; } withTimeUnitsImpl(timeUnitsImpl) { this.timeUnitsImpl = timeUnitsImpl; return this; } withTranslate(translate) { this.translate = translate; return this; } } // This enum is used to represent names of all internally built views of the calendar var InternalViewName; (function (InternalViewName) { InternalViewName["Day"] = "day"; InternalViewName["Week"] = "week"; InternalViewName["MonthGrid"] = "month-grid"; InternalViewName["MonthAgenda"] = "month-agenda"; })(InternalViewName || (InternalViewName = {})); const getLocaleStringMonthArgs = ($app) => { return [$app.config.locale.value, { month: 'long' }]; }; const getLocaleStringYearArgs = ($app) => { return [$app.config.locale.value, { year: 'numeric' }]; }; const getMonthAndYearForDateRange = ($app, rangeStart, rangeEnd) => { const startDateMonth = toJSDate(rangeStart).toLocaleString(...getLocaleStringMonthArgs($app)); const startDateYear = toJSDate(rangeStart).toLocaleString(...getLocaleStringYearArgs($app)); const endDateMonth = toJSDate(rangeEnd).toLocaleString(...getLocaleStringMonthArgs($app)); const endDateYear = toJSDate(rangeEnd).toLocaleString(...getLocaleStringYearArgs($app)); if (startDateMonth === endDateMonth && startDateYear === endDateYear) { return `${startDateMonth} ${startDateYear}`; } else if (startDateMonth !== endDateMonth && startDateYear === endDateYear) { return `${startDateMonth} – ${endDateMonth} ${startDateYear}`; } return `${startDateMonth} ${startDateYear} – ${endDateMonth} ${endDateYear}`; }; const getMonthAndYearForSelectedDate = ($app) => { const dateMonth = toJSDate($app.datePickerState.selectedDate.value).toLocaleString(...getLocaleStringMonthArgs($app)); const dateYear = toJSDate($app.datePickerState.selectedDate.value).toLocaleString(...getLocaleStringYearArgs($app)); return `${dateMonth} ${dateYear}`; }; function RangeHeading() { const $app = useContext(AppContext); const [currentHeading, setCurrentHeading] = useState(''); useEffect(() => { if ($app.calendarState.view.value === InternalViewName.Week) { setCurrentHeading(getMonthAndYearForDateRange($app, $app.calendarState.range.value.start, $app.calendarState.range.value.end)); } if ($app.calendarState.view.value === InternalViewName.MonthGrid || $app.calendarState.view.value === InternalViewName.Day || $app.calendarState.view.value === InternalViewName.MonthAgenda) { setCurrentHeading(getMonthAndYearForSelectedDate($app)); } }, [$app.calendarState.range.value]); return jsx("span", { className: 'sx__range-heading', children: currentHeading }); } function TodayButton() { const $app = useContext(AppContext); const setToday = () => { $app.datePickerState.selectedDate.value = toDateString$1(new Date()); }; return (jsx("button", { type: "button", className: 'sx__today-button sx__ripple', onClick: setToday, children: $app.translate('Today') })); } function ViewSelection() { const $app = useContext(AppContext); const [availableViews, setAvailableViews] = useState([]); useSignalEffect(() => { if ($app.calendarState.isCalendarSmall.value) { setAvailableViews($app.config.views.value.filter((view) => view.hasSmallScreenCompat)); } else { setAvailableViews($app.config.views.value.filter((view) => view.hasWideScreenCompat)); } }); const [selectedViewLabel, setSelectedViewLabel] = useState(''); useSignalEffect(() => { const selectedView = $app.config.views.value.find((view) => view.name === $app.calendarState.view.value); if (!selectedView) return; setSelectedViewLabel($app.translate(selectedView.label)); }); const [isOpen, setIsOpen] = useState(false); const clickOutsideListener = (event) => { const target = event.target; if (target instanceof HTMLElement && !target.closest('.sx__view-selection')) { setIsOpen(false); } }; useEffect(() => { document.addEventListener('click', clickOutsideListener); return () => document.removeEventListener('click', clickOutsideListener); }, []); const handleClickOnSelectionItem = (viewName) => { setIsOpen(false); $app.calendarState.setView(viewName, $app.datePickerState.selectedDate.value); }; const [viewSelectionItems, setViewSelectionItems] = useState(); const [focusedViewIndex, setFocusedViewIndex] = useState(0); const handleSelectedViewKeyDown = (keyboardEvent) => { if (isKeyEnterOrSpace(keyboardEvent)) { setIsOpen(!isOpen); } setTimeout(() => { var _a; const allOptions = (_a = $app.elements.calendarWrapper) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.sx__view-selection-item'); if (!allOptions) return; setViewSelectionItems(allOptions); const firstOption = allOptions[0]; if (firstOption instanceof HTMLElement) { setFocusedViewIndex(0); firstOption.focus(); } }, 50); }; const navigateUpOrDown = (keyboardEvent, viewName) => { if (!viewSelectionItems) return; if (keyboardEvent.key === 'ArrowDown') { const nextOption = viewSelectionItems[focusedViewIndex + 1]; if (nextOption instanceof HTMLElement) { setFocusedViewIndex(focusedViewIndex + 1); nextOption.focus(); } } else if (keyboardEvent.key === 'ArrowUp') { const prevOption = viewSelectionItems[focusedViewIndex - 1]; if (prevOption instanceof HTMLElement) { setFocusedViewIndex(focusedViewIndex - 1); prevOption.focus(); } } else if (isKeyEnterOrS