@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
410 lines (406 loc) • 28.1 kB
JavaScript
import { html as uHtml } from 'uhtml';
import LayerTimeFormatter from '../../../tools/time/layertimeformatter';
import TimeWidget from '../tools/timewidget';
/**
* A widget that represents a time slider with configurable time resolution. The slider supports
* a single value or a time range depending on the specified mode.
*
* It consists of two `<input>` elements of type `range` that are configured by receiving `timeOptions`
* in its initialize method. It validates input changes to ensure compliance
* with the defined `timeOptions` and dynamically updates output elements to display nicely formatted time values.
*/
class TimeSliderComponent extends TimeWidget {
constructor() {
super('time-slider');
Object.defineProperty(this, "template", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
return uHtml `<style>
*{font-family:Arial,sans-serif}.hidden{display:none}.gg-rotate90{transform:rotate(90deg)}.gg-rotate180{transform:rotate(180deg)}.gg-rotate270{transform:rotate(270deg)}img{filter:var(--svg-filter)}img.legend-image{filter:var(--svg-map-filter);background:var(--svg-legend-bkg)}div{scrollbar-width:thin}a{color:var(--link-color)}a:visited{color:var(--link-color)}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}100%{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-iteration-count:infinite;animation-timing-function:linear}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-iteration-count:infinite;animation-timing-function:linear}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}.gg-button,.gg-input,.gg-select,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);outline:0;border-radius:3px;border:var(--app-standard-border);display:inline-block;box-sizing:border-box;padding:0 0 0 .5rem;margin:0;cursor:pointer}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;display:inline-block;padding:0;margin:0}.gg-button,.gg-input,.gg-label,.gg-select{min-height:calc(var(--app-standard-height)/ 1.5);max-height:calc(var(--app-standard-height)/ 1.5);line-height:calc(var(--app-standard-height) / 1.5)}.gg-textarea{padding:.5rem;height:6rem}.gg-input{cursor:text}.gg-checkbox{accent-color:var(--text-color);width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad2);background-color:var(--text-color-grad2);color:var(--bkg-color)}.gg-button:disabled{background-color:#d3d3d3;color:grey;border:none;cursor:not-allowed}.gg-button>img{vertical-align:middle}.gg-icon-button{border:none;background-color:transparent;display:flex;flex-direction:column;color:var(--text-color);padding:0;align-items:center;justify-content:center;cursor:pointer}.gg-big,.gg-big-withtext{min-width:var(--app-standard-height);min-height:var(--app-standard-height);max-height:var(--app-standard-height)}.gg-big img,.gg-big-withtext img{margin:0;width:calc(var(--app-standard-height) - 1.5rem)}.gg-big-withtext span{font-size:.9rem;font-variant:small-caps;padding:0 1rem}.gg-medium,.gg-medium-withtext{flex-direction:row;min-width:calc(var(--app-standard-height)/ 1.2);min-height:calc(var(--app-standard-height)/ 1.2);max-height:calc(var(--app-standard-height)/ 1.2)}.gg-medium img{margin:0;width:calc(var(--app-standard-height)/ 2.4)}.gg-medium-withtext img{margin-left:.5rem;width:calc(var(--app-standard-height)/ 2.4)}.gg-medium-withtext span{font-size:.9rem;padding:0 1rem;padding-left:.5rem}.gg-small,.gg-small-withtext{flex-direction:row;min-width:calc(var(--app-standard-height)/ 2);min-height:calc(var(--app-standard-height)/ 2);max-height:calc(var(--app-standard-height)/ 2)}.gg-small img{margin:0;width:calc(var(--app-standard-height)/ 3)}.gg-small-withtext img{margin-left:.5rem;width:calc(var(--app-standard-height)/ 3)}.gg-small-withtext span{font-size:.9rem;padding:0 .5rem;padding-left:.3rem}.gg-button:hover,.gg-icon-button:hover,.gg-input:hover,.gg-select:hover,.gg-textarea:hover{background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{background-color:transparent;opacity:1}.gg-tabs{display:flex;justify-content:left;margin:.5rem auto 0;border-bottom:1px solid grey;cursor:pointer;height:2rem;align-items:end;gap:1rem;margin-bottom:1rem}.gg-tab{border:none;background:0 0;cursor:pointer;padding:.5rem;color:var(--text-color)}.gg-tab.active{border-bottom:solid 2px var(--text-color-grad2);font-weight:600}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{border:none;background-color:transparent;display:flex;flex-direction:column;color:var(--text-color)}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color-grad1);cursor:pointer}.girafe-button-big.dark,.girafe-button-large.dark,.girafe-button-small.dark,.girafe-button-tiny.dark{background-color:var(--bkg-color);filter:invert(1)}.girafe-button-big{width:var(--app-standard-height);height:var(--app-standard-height);align-items:center;padding:1rem}.girafe-button-big img{overflow:hidden}.girafe-button-large{flex-direction:row}.girafe-button-large img{margin:.3rem;height:2rem}.girafe-button-large span{line-height:2rem;height:2rem;margin:.3rem}.girafe-button-small{min-width:calc(var(--app-standard-height)/ 2);height:calc(var(--app-standard-height)/ 2);align-items:center;padding:.5rem}.girafe-button-small img{overflow:hidden}.girafe-button-small span{width:100%;text-align:left;overflow:hidden;text-overflow:ellipsis}.girafe-button-tiny{width:1rem;height:1rem;align-items:center;padding:0}.girafe-button-tiny img{overflow:hidden}
</style><style>
#container{--slider-color:var(--text-color);--slider-disabled-color:var(--text-color-grad2);--slider-highlight-color:var(--bkg-color-grad2);--slider-width:170px;--track-width:calc(var(--slider-width) - var(--thumb-width));--thumb-width:19px;--margin-left:calc(var(--thumb-width) / 2);--slider-track-height:9px;--thumb-margin-top:calc(-0.5 * var(--thumb-width) + 0.5 * var(--slider-track-height));--slider-track-border-color:var(--text-color-grad2);--slider-track-outside-color:var(--bkg-color-grad2);--slider-track-inside-color:var(--text-color);color:var(--text-color);display:flex;flex-direction:column;margin:.5rem 0 0 .5rem}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;pointer-events:all;width:var(--thumb-width);height:var(--thumb-width);margin-top:var(--thumb-margin-top);background:var(--slider-color);border:1px solid var(--slider-highlight-color);border-radius:50%;box-shadow:1px 1px 2px #9c9c9c;cursor:pointer}input[type=range]::-webkit-slider-thumb:hover{box-shadow:inset 0 0 2px 0 var(--slider-highlight-color),0 0 4px var(--slider-highlight-color);border-color:var(--bkg-color)}input[type=range]::-webkit-slider-thumb:active{box-shadow:inset 0 0 2px 0 var(--slider-highlight-color),0 0 4px var(--slider-highlight-color);-webkit-box-shadow:inset 0 0 2px 0 var(--slider-highlight-color),0 0 4px var(--slider-highlight-color);border-color:var(--bkg-color)}input[type=range]::-webkit-slider-runnable-track{height:var(--slider-track-height)}input[type=range]::-moz-range-thumb{-webkit-appearance:none;pointer-events:all;width:var(--thumb-width);height:var(--thumb-width);margin-top:var(--thumb-margin-top);background:var(--slider-color);border:1px solid var(--slider-highlight-color);border-radius:50%;cursor:pointer}input[type=range]::-moz-range-thumb:hover{box-shadow:inset 0 0 2px 0 var(--slider-highlight-color),0 0 4px var(--slider-highlight-color);border-color:var(--bkg-color)}input[type=range]::-moz-range-thumb:active{box-shadow:inset 0 0 2px 0 var(--slider-highlight-color),0 0 4px var(--slider-highlight-color);-webkit-box-shadow:inset 0 0 2px 0 var(--slider-highlight-color),0 0 4px var(--slider-highlight-color);border-color:var(--bkg-color)}input[type=range]::-moz-range-track{height:var(--slider-track-height)}input[type=range]{-webkit-appearance:none;appearance:none;height:var(--slider-track-height);width:var(--slider-width);background-color:transparent;pointer-events:none;z-index:2;margin:0}#input-upper{position:absolute}#slider-track{width:var(--track-width);margin-left:var(--margin-left);height:var(--slider-track-height);position:absolute;cursor:pointer;pointer-events:all;z-index:0;border:1px solid var(--slider-track-border-color);border-radius:3px}.slider-label{width:var(--track-width);margin:calc(.25 * var(--thumb-width)) 0 0 var(--margin-left);display:flex;justify-content:space-around;align-items:center;line-height:1em;border:1px solid transparent}.slider-label.editable{margin-top:calc(.5 * var(--thumb-width));margin-bottom:.3rem}.slider-label>input{line-height:1em;text-align:center;padding:0;max-width:80px}.slider-label>input::-webkit-inner-spin-button,.slider-label>input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.slider-label>input[type=number]{-moz-appearance:textfield}.slider-label>input:disabled{border-color:transparent}.slider-label>input:disabled:hover{background-color:initial;cursor:default}.slider-label>div{margin:0 2px}
</style>
<div id="container">
<input id="input-lower" type="range" min="0" max="1" step="1" />
<input id="input-upper" type="range" min="0" max="1" step="1" />
<div id="slider-track"></div>
<div class="${this.resolution === 'year' ? 'slider-label editable' : 'slider-label'}">
<input id="output-label-lower" disabled class="gg-input gg-small" />
<div class="${this.mode === 'range' ? '' : 'hidden'}">/</div>
<input id="output-label-upper" disabled class="${this.mode === 'range' ? 'gg-input gg-small' : 'hidden'}" />
</div>
</div>
`;
}
});
Object.defineProperty(this, "discreteTimeSteps", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
}
/**
* Initializes the widget and sets properties of the input elements. Must be called before using the widget.
*
* @param {ITimeOptions} timeOptions - Configuration options for the time widget.
*/
initialize(timeOptions) {
super.initialize(timeOptions);
// timeOptions can optionally define a list of discrete time values for the slider to step through
this.discreteTimeSteps =
timeOptions.values?.map((d) => this.timeFormatter.formatDateString(d)).filter((dstr) => dstr && dstr !== '') ??
undefined;
if (this.discreteTimeSteps) {
// Validate min/max values by checking if they correspond to the first and last value in the discreteTimeSteps list
this.minValue = this.timeFormatter.parseDateString(this.discreteTimeSteps[0]);
this.maxValue = this.timeFormatter.parseDateString(this.discreteTimeSteps[this.discreteTimeSteps.length - 1]);
}
}
renderComponent() {
super.renderComponent();
this.configureInputElements();
// Set default values for slider handles
this.reset();
this.addEventListeners();
}
/**
* Retrieves the value associated with the input element of the specified limit.
*
* @param {TimeRangeLimit} [limit] - Optional parameter specifying the limit ('upper' or 'lower') for which value is to be fetched.
* @return {string} The date string value retrieved from the input element.
*/
getValue(limit) {
return this.sliderPositionToDateString(super.getValue(limit));
}
/**
* Transforms the date string to a slider position and set it to the specified input element.
* Optionally, the `timeChange` event can be omitted.
* Updates the UI elements (output, slider track) to the new value
* and validates the positions of the sliders so the handles do not cross each other `range` mode.
*
* @param {string} dateStr - The date string to be set as the value.
* @param {TimeRangeLimit} [limit='lower'] - The time range limit for which the value is to be set ('lower' or 'upper').
* @param {boolean} [triggerEvent=true] - Weather the `timeChange` event should be dispatched.
*/
setValue(dateStr, limit = 'lower', triggerEvent = true) {
if (!this.rendered)
throw new Error('Cannot set value on unrendered component');
if (limit === 'upper' && this.mode === 'value')
return;
this.getInputElement(limit).value = this.dateStringToSliderPosition(dateStr);
// Update UI elements, trigger validation and dispatch event
this.updateOutputLabel(limit);
this.updateSliderColorRange();
this.toggleSliderState(dateStr !== '');
limit === 'lower' ? this.validateLowerLimit() : this.validateUpperLimit();
if (triggerEvent)
this.dispatchTimeChangeEvent();
}
/**
* Resets the input elements to their default states. This will be the min and max slider position
* or the optionally defined `minDefaultValue` and `maxDefaultValue`.
* Without `minDefaultValue` and `maxDefaultValue`, the slider state is set to disable.
* The method will trigger a `TimeChangeEvent` that bubbles up to the parent component.
*/
reset() {
if (this.minDefaultValue || this.maxDefaultValue) {
// Apply default values
this.setValue(this.minDefaultValue, 'lower');
if (this.mode === 'range')
this.setValue(this.maxDefaultValue, 'upper');
}
else {
// Without any default values, set the lower handle to the min and the upper handle to the max position
this.setValue(this.minDefaultValue, 'lower', false);
if (this.mode === 'range')
this.setValue(this.timeFormatter.formatDateString(this.maxValue), 'upper', false);
this.toggleSliderState(false);
this.dispatchTimeChangeEvent(true);
}
}
configureInputElements() {
this.lowerInputElem.max = this.countSliderStepsInRange().toString();
this.lowerInputElem.value = this.dateStringToSliderPosition(this.minDefaultValue);
if (this.mode === 'range') {
this.upperInputElem.max = this.countSliderStepsInRange().toString();
this.upperInputElem.value = this.maxDefaultValue
? this.dateStringToSliderPosition(this.maxDefaultValue)
: this.countSliderStepsInRange().toString();
}
// Hide the second slider in 'value' mode
if (this.mode === 'value') {
this.upperInputElem.classList.add('hidden');
}
}
addEventListeners() {
// Update UI elements when the slider handle is moved
this.lowerInputElem.addEventListener('input', (_event) => {
this.updateOutputLabel('lower');
this.updateSliderColorRange();
this.toggleSliderState(true);
});
// Validate and dispatch event when the slider handle stops at the new position
this.lowerInputElem.addEventListener('change', (_event) => {
this.validateLowerLimit();
this.dispatchTimeChangeEvent();
});
if (this.mode === 'range') {
// Update UI elements when the slider handle is moved
this.upperInputElem.addEventListener('input', (_event) => {
this.updateOutputLabel('upper');
this.updateSliderColorRange();
this.toggleSliderState(true);
});
// Validate and dispatch event when the slider handle stops at the new position
this.upperInputElem.addEventListener('change', (_event) => {
this.validateUpperLimit();
this.dispatchTimeChangeEvent();
});
}
// Register clicks on the slider track and translate them to handle positions
this.getById('slider-track').addEventListener('click', (event) => {
this.trackClickToSliderPosition(event);
});
// Make time labels editable if resolution is year
if (this.resolution === 'year') {
const lowerLabel = this.getById(`output-label-lower`);
const upperLabel = this.getById(`output-label-upper`);
[lowerLabel, upperLabel].forEach((label) => {
label.removeAttribute('disabled');
label.setAttribute('type', 'number');
});
const yearEntryToSliderVal = (event) => {
let year = event.target?.value;
year = year.padStart(4, '0');
return this.dateStringToSliderPosition(`${year}-01-01`);
};
// Update the slider position and track when the label is changed
lowerLabel.addEventListener('change', (event) => {
this.lowerInputElem.value = yearEntryToSliderVal(event);
this.lowerInputElem.dispatchEvent(new Event('input'));
});
// Trigger a time restriction change when the enter key is pressed
lowerLabel.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
this.lowerInputElem.dispatchEvent(new Event('change'));
}
});
// Trigger a time restriction change when the input looses focus
lowerLabel.addEventListener('focusout', (_event) => this.lowerInputElem.dispatchEvent(new Event('change')));
if (this.mode === 'range') {
// Same event listeners for the upper input element
upperLabel.addEventListener('change', (event) => {
this.upperInputElem.value = yearEntryToSliderVal(event);
this.upperInputElem.dispatchEvent(new Event('input'));
});
upperLabel.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
this.upperInputElem.dispatchEvent(new Event('change'));
}
});
upperLabel.addEventListener('focusout', (_event) => this.upperInputElem.dispatchEvent(new Event('change')));
}
}
}
updateOutputLabel(limit) {
const inputElem = this.getInputElement(limit);
const valueOutput = this.getById(`output-label-${limit}`);
const newValue = inputElem.value ?? '';
valueOutput.value = this.timeFormatter.formatDateString(this.sliderPositionToDateString(newValue));
}
/**
* In `range` mode, the slider track gets a linear gradient to indicate the inside and outside of the time range.
*/
updateSliderColorRange() {
const maxPosition = this.countSliderStepsInRange();
if (this.mode !== 'range' || maxPosition === 0)
return;
const lowVal = (100 * parseInt(this.lowerInputElem.value)) / maxPosition;
const highVal = (100 * (this.mode === 'range' ? parseInt(this.upperInputElem.value) : maxPosition)) / maxPosition;
const sliderTrackElem = this.getById('slider-track');
const outCol = 'var(--slider-track-outside-color)';
const inCol = 'var(--slider-track-inside-color)';
sliderTrackElem.style.background = `linear-gradient(to right, ${outCol} 0%, ${outCol} ${lowVal}%, ${inCol} ${lowVal}%, ${inCol} ${highVal}%, ${outCol} ${highVal}%, ${outCol} 100%)`;
}
/**
* Indicate an inactive slider by coloring the output labels and slider track in a lighter color.
*/
toggleSliderState(active) {
this.getById('output-label-lower').style.color = active ? 'var(--slider-color)' : 'var(--slider-disabled-color)';
this.getById('output-label-upper').style.color = active ? 'var(--slider-color)' : 'var(--slider-disabled-color)';
this.getById('slider-track').style.borderColor = active
? 'var(--slider-track-border-color)'
: 'var(--slider-disabled-color)';
}
/**
* Registers the position where the user clicks on the slider track
* and moves the lower or upper slider handle to that position.
*
* @param {MouseEvent} event - The mouse event triggered by the click on the slider track.
*/
trackClickToSliderPosition(event) {
const offsetX = event.offsetX;
const trackWidth = event.target.offsetWidth;
const sliderPosition = Math.round((offsetX / trackWidth) * this.countSliderStepsInRange());
const dateStr = this.sliderPositionToDateString(sliderPosition.toString());
if (this.mode === 'value') {
this.setValue(dateStr, 'lower');
}
else {
// Decide which slider handle to move
const currentLowerPosition = Number(this.lowerInputElem.value);
const currentUpperPosition = Number(this.upperInputElem.value);
const middlePosition = (currentLowerPosition + currentUpperPosition) / 2;
let elementToMove;
if (sliderPosition <= currentLowerPosition) {
elementToMove = 'lower';
}
else if (sliderPosition >= currentUpperPosition) {
elementToMove = 'upper';
}
else {
elementToMove = sliderPosition < middlePosition ? 'lower' : 'upper';
}
this.setValue(dateStr, elementToMove);
}
}
/**
* Attaches event listeners to input elements to validate their values when user interactions occur.
* Makes sure the slider positions never cross each other in 'range' mode. Also takes care that
* slider mouse handlers can be grabbed at any position on the slider by changing the zIndex.
*/
validateLowerLimit() {
const newValue = this.sliderPositionToDateString(this.lowerInputElem.value);
const positionOfUpper = this.sliderPositionToDateString(this.upperInputElem.value);
if (this.mode === 'range' && newValue > positionOfUpper) {
this.setValue(positionOfUpper, 'lower');
}
}
validateUpperLimit() {
const newValue = this.sliderPositionToDateString(this.upperInputElem.value);
const positionOfLower = this.sliderPositionToDateString(this.lowerInputElem.value);
if (this.mode === 'range' && newValue < positionOfLower) {
this.setValue(positionOfLower, 'upper');
}
// Increase z-index of the upper handle if it is at the 0 position,
// otherwise it is behind the other slider and can't be grabbed any more
if (Number(this.upperInputElem.value) <= 0) {
this.upperInputElem.style.zIndex = '3';
}
else {
this.upperInputElem.style.zIndex = '1';
}
}
/**
* Converts a date string to a corresponding time increment value based on the defined range (minValue/maxValue)
* and resolution.
*
* @param {string} dateStr - The date string to be parsed and evaluated.
* @return {number} The calculated position on the slider.
* Returns 0 if the date string is invalid or falls outside the minimum range.
* Returns the maximum time increment if the date exceeds the defined range.
*/
dateStringToSliderPosition(dateStr) {
let position = 0;
// Only proceed if the string contains a valid date
if (this.timeFormatter.parseDateString(dateStr)) {
const date = new Date(dateStr);
if (date > this.maxValue) {
// Return slider max position
position = this.countSliderStepsInRange();
}
else if (date > this.minValue) {
position = this.countSliderStepsInRange(date);
}
}
return position.toString();
}
/**
* Converts a given slider position to a formatted date string based on the minValue / maxValue range
* and resolution (day, week, month, or year). To stay independent of local time, new dates are created in UTC.
*
* @param {number} position - The number of increments to add to the current date based on the specified resolution.
* @return {string} The formatted date string corresponding to the increment value and resolution.
* @throws {Error} If the resolution is invalid.
*/
sliderPositionToDateString(position) {
if (position === '' || isNaN(Number(position)))
return '';
const increment = Number(position);
const oneDayInMillis = 1000 * 60 * 60 * 24;
let newDate;
if (this.discreteTimeSteps) {
if (this.discreteTimeSteps.length === 0 || increment >= this.discreteTimeSteps.length) {
return '';
}
return this.discreteTimeSteps[increment];
}
switch (this.resolution) {
case 'day':
newDate = new Date(Date.UTC(this.minValue.getUTCFullYear(), this.minValue.getUTCMonth(), this.minValue.getUTCDate() + increment));
break;
case 'week':
newDate = new Date(this.minValue.getTime() + oneDayInMillis * 7 * increment);
break;
case 'month':
newDate = new Date(Date.UTC(this.minValue.getUTCFullYear(), this.minValue.getUTCMonth() + increment, this.minValue.getUTCDate()));
break;
case 'year':
newDate = new Date(Date.UTC(this.minValue.getUTCFullYear() + increment, this.minValue.getUTCMonth(), this.minValue.getUTCDate()));
break;
default:
throw new Error('Invalid resolution');
}
return LayerTimeFormatter.queryStringFromSingleValueAndResolution(newDate, this.resolution);
}
/**
* Calculates the number of steps within the given time range based on the specified time resolution.
* The returned number is zero base, indicating the index of the slider position.
* The range starts at the minValue of the provided timeOptions.
*
* @param {Date} [maxVal] - The upper limit for the range. Defaults to the timeOptions > maxValue if not provided.
* @return {number} The number of slider steps based on the resolution and time range.
*/
countSliderStepsInRange(maxVal) {
maxVal ??= this.maxValue;
if (this.discreteTimeSteps) {
const listIdx = this.getPositionInTimeValuesList(maxVal);
return listIdx ?? 0;
}
const timeRangeInMillis = maxVal.getTime() - this.minValue.getTime();
const oneDayInMillis = 1000 * 60 * 60 * 24;
const numberOfYears = maxVal.getUTCFullYear() - this.minValue.getUTCFullYear();
// Return number of steps based on the resolution
switch (this.resolution) {
case 'day':
return Math.round(timeRangeInMillis / oneDayInMillis);
case 'week':
return Math.round(timeRangeInMillis / (oneDayInMillis * 7));
case 'month':
return numberOfYears * 12 - this.minValue.getUTCMonth() + maxVal.getUTCMonth();
case 'year':
return numberOfYears;
default:
return 0;
}
}
/**
* Retrieves the position of a given date within a list of discrete time steps.
* If the discrete time steps are not defined, the method returns undefined.
*
* @param {Date} date - The date for which the position needs to be determined.
* @return {number | undefined} The index of the date in the list of discrete time steps, or undefined if not found.
*/
getPositionInTimeValuesList(date) {
if (!this.discreteTimeSteps) {
// No discrete time values defined
return undefined;
}
const formattedDate = this.timeFormatter.formatDateString(date);
return this.discreteTimeSteps.findIndex((value) => value === formattedDate);
}
}
export default TimeSliderComponent;