Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/docs/partials/functions/dates.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,29 @@ <h2 id='add'>add(DateTime)
</div>

<div class='row'>
<h2 id='setValue'>setValue(value: DateTime, index?: number)
<h2 id='setValue'>setValue(value: DateTime, index?: number, proposedRangeValid?: boolean)
<a class='anchor-link' aria-label='Anchor' href='#setValue'><i class='fa-solid fa-anchor'
aria-hidden='true'></i></a>
</h2>
<p>
Sets the select date index (or the first, if not provided) to the provided DateTime object.
For <code>dateRange</code> pickers, please use <code>setRangeValues</code> instead. The
<code>proposedRangeValid</code> parameter is intended for internal use.
</p>

</div>

<div class='row'>
<h2 id='setRangeValues'>setRangeValues(startTarget?: DateTime, endTarget?: DateTime): void
<a class='anchor-link' aria-label='Anchor' href='#setRangeValues'>
<i class='fa-solid fa-anchor' aria-hidden='true'></i></a>
</h2>
<p>
Sets the range dates to the provided DateTime objects. This should only be used for
<code>dateRange</code> pickers.
</p>
</div>

<div class='row'>
<h2 id='formatInput'>formatInput(value: DateTime): string
<a class='anchor-link' aria-label='Anchor' href='#formatInput'><i class='fa-solid fa-anchor'
Expand Down
74 changes: 72 additions & 2 deletions src/js/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,40 @@ export default class Dates {
}
}

//eslint-disable-next-line @typescript-eslint/no-explicit-any
setRangeFromInput(values: any[]) {
const converted: DateTime[] = [];
// For keeping track of which indexes are undefined because the value
// evaluated to false vs undefined because the value couldn't parse
const missingValueIndexes: { [key: number]: true } = {};

for (let i = 0; i < values.length; i++) {
const value = values[i];
if (!value) {
converted.push(undefined);
missingValueIndexes[i] = true;
} else {
converted.push(this.parseInput(value));
}
}

// The logic below mimics what would happen if setFromInput were to be
// called on each value individually, including whether setValue would be
// called or not
const proposedRangeValid =
this.validation.proposedDateRangeIsValid(converted);
for (let i = 0; i < converted.length; i++) {
const convertedValue = converted[i];

if (missingValueIndexes[i]) {
this.setValue(undefined, i, proposedRangeValid);
} else if (convertedValue) {
convertedValue.setLocalization(this.optionsStore.options.localization);
this.setValue(convertedValue, i, proposedRangeValid);
}
}
}

/**
* Adds a new DateTime to selected dates array
* @param date
Expand Down Expand Up @@ -201,10 +235,19 @@ export default class Dates {
* If multi-date is being used then it will be removed from the array.
* If `target` is valid and multi-date is used then if `index` is
* provided the date at that index will be replaced, otherwise it is appended.
* When proposedRangeValid is provided, it will be passed along to the
* dateRangeIsValid check, which will supersede the result unless it is
* undefined. This should only be provided if the dates were validated before
* this is called via `validation.proposedDateRangeIsValid()`.
* @param target
* @param index
* @param proposedRangeValid
*/
setValue(target?: DateTime, index?: number): void {
setValue(
target?: DateTime,
index?: number,
proposedRangeValid?: boolean
): void {
const noIndex = typeof index === 'undefined',
isClear = !target && noIndex;
let oldDate = this.optionsStore.unset ? null : this._dates[index]?.clone;
Expand Down Expand Up @@ -253,7 +296,12 @@ export default class Dates {

if (
this.validation.isValid(target) &&
this.validation.dateRangeIsValid(this.picked, index, target)
this.validation.dateRangeIsValid(
this.picked,
index,
target,
proposedRangeValid
)
) {
onUpdate(true);
return;
Expand Down Expand Up @@ -295,4 +343,26 @@ export default class Dates {

this._eventEmitters.updateDisplay.emit('all');
}

/**
* Exposes a way to set the range programmatically. This considers both dates
* for determining validity, and should be preferred over setValue for date
* ranges.
* @param startTarget
* @param endTarget
*/
setRangeValues(startTarget?: DateTime, endTarget?: DateTime): void {
if (!this.optionsStore.options.dateRange)
throw new Error('Cannot call setRangeValues except for a dateRange');
const proposedRangeValid = this.validation.proposedDateRangeIsValid([
startTarget,
endTarget,
]);

startTarget?.setLocalization(this.optionsStore.options.localization);
this.setValue(startTarget, 0, proposedRangeValid);

endTarget?.setLocalization(this.optionsStore.options.localization);
this.setValue(endTarget, 1, proposedRangeValid);
}
}
8 changes: 6 additions & 2 deletions src/js/tempus-dominus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,12 @@ class TempusDominus {
const valueSplit = value.split(
this.optionsStore.options.multipleDatesSeparator
);
for (let i = 0; i < valueSplit.length; i++) {
this.dates.setFromInput(valueSplit[i], i);
if (this.optionsStore.options.dateRange) {
this.dates.setRangeFromInput(valueSplit);
} else {
for (let i = 0; i < valueSplit.length; i++) {
this.dates.setFromInput(valueSplit[i], i);
}
}
setViewDate();
} catch {
Expand Down
65 changes: 64 additions & 1 deletion src/js/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,69 @@ export default class Validation {
);
}

dateRangeIsValid(dates: DateTime[], index: number, target: DateTime) {
/**
* The dateRangeIsValid check only looks at a single date at a time, but if
* the user changes the text input as a whole (or the developer updates it
* programmatically), then we must consider all the dates together, otherwise we
* will be comparing the new start date with the old end date or vice versa (which
* is an invalid comparison). This method will return undefined if the validation
* does not make sense (such as if there aren't 2 dates to compare, along with
* other similarly invalid checks). It returns true or false if the range is valid
* @param dates
*/
proposedDateRangeIsValid(dates: DateTime[]): boolean {
// If we're not using the option, then the validation is irrelevant
if (!this.optionsStore.options.dateRange) return undefined;

// Technically, this should be undefined, but 1 date is problematic for
// dateRangeIsValid if it is before the existing range
if (dates.length === 1 && dates[0]) return true;

// If we are not looking at exactly 2 dates, we will not pre-approve this
// date range
if (dates.length !== 2) return undefined;

// If either date is undefined, something probably went wrong in parsing
if (!dates[0] || !dates[1]) return undefined;

const startDate = dates[0].clone;
const endDate = dates[1].clone;

// The startDate must be before or the same as the end date
if (startDate.isAfter(endDate)) return false;

// We are immediately invalid if either date is invalid
if (
!this.isValid(startDate, Unit.date) ||
!this.isValid(endDate, Unit.date)
)
return false;

// Finally, check each date within the range
startDate.manipulate(1, Unit.date);

while (!startDate.isSame(endDate, Unit.date)) {
if (!this.isValid(startDate, Unit.date)) return false;

startDate.manipulate(1, Unit.date);
}

return true;
}

dateRangeIsValid(
dates: DateTime[],
index: number,
target: DateTime,
proposedRangeValid?: boolean
) {
// if we're not using the option, then return valid
if (!this.optionsStore.options.dateRange) return true;

// when setting the value explicitly, we validate everything ahead of time,
// so just use those results
if (proposedRangeValid !== undefined) return proposedRangeValid;

// if we've only selected 0..1 dates, and we're not setting the end date
// then return valid. We only want to validate the range if both are selected,
// because the other validation on the target has already occurred.
Expand All @@ -211,6 +270,10 @@ export default class Validation {
// add one day to start; start has already been validated
start.manipulate(1, Unit.date);

// sanity check to avoid an infinite loop
if (start.isAfter(target))
throw new Error(`Unexpected '${start}' is after '${target}'`);

// check each date in the range to make sure it's valid
while (!start.isSame(target, Unit.date)) {
const valid = this.isValid(start, Unit.date);
Expand Down
Loading