@progress/kendo-angular-dateinputs
Version:
Kendo UI for Angular Date Inputs Package - Everything you need to add date selection functionality to apps (DatePicker, TimePicker, DateInput, DateRangePicker, DateTimePicker, Calendar, and MultiViewCalendar).
407 lines (406 loc) • 12.4 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { MS_PER_HOUR, MS_PER_MINUTE, addDays, cloneDate, getDate, isEqual } from '@progress/kendo-date-math';
import { MIDNIGHT_DATE } from './defaults';
import { EMPTY_SELECTIONRANGE } from './calendar/models/selection-range.interface';
import { isPresent } from './common/utils';
const isSet = (value) => value !== null && value !== undefined;
const setter = (method) => (date, value) => {
const clone = cloneDate(date);
clone[method](value);
return clone;
};
/**
* @hidden
*/
export const setTime = (origin, candidate) => {
const date = cloneDate(origin);
date.setHours(candidate.getHours(), candidate.getMinutes(), candidate.getSeconds(), candidate.getMilliseconds());
return date;
};
/**
* @hidden
*/
const getTimeInMilliseconds = (candidate) => {
const hoursInMilliseconds = candidate.getHours() * MS_PER_HOUR;
const minutesInMilliseconds = candidate.getMinutes() * MS_PER_MINUTE;
const secondsInMilliseconds = candidate.getSeconds() * 1000;
return hoursInMilliseconds + minutesInMilliseconds + secondsInMilliseconds + candidate.getMilliseconds();
};
const normalizeTimes = (candidate, min, max) => {
return {
candidateValue: setTime(MIDNIGHT_DATE, candidate),
maxValue: addDays(setTime(MIDNIGHT_DATE, max), getTimeInMilliseconds(min) < getTimeInMilliseconds(max) ? 0 : 1),
minValue: setTime(MIDNIGHT_DATE, min)
};
};
/**
* @hidden
*/
export const setYears = setter('setFullYear');
/**
* @hidden
*/
export const setHours = setter('setHours');
/**
* @hidden
*/
export const setMinutes = setter('setMinutes');
/**
* @hidden
*/
export const setSeconds = setter('setSeconds');
/**
* @hidden
*/
export const setMilliseconds = setter('setMilliseconds');
/**
* @hidden
*/
export const range = (start, end, step = 1) => {
const result = [];
for (let i = start; i < end; i = i + step) {
result.push(i);
}
return result;
};
/**
* @hidden
*/
export const isInRange = (candidate, min, max) => (!candidate || !((min && min > candidate) || (max && max < candidate)));
/**
* @hidden
*/
export const isInTimeRange = (candidate, min, max) => {
if (!candidate || !min || !max) {
return true;
}
const { candidateValue, minValue, maxValue } = normalizeTimes(candidate, min, max);
return minValue <= candidateValue && candidateValue <= maxValue;
};
/**
* @hidden
*/
export const isValidRange = (min, max) => (!isSet(min) || !isSet(max) || min <= max);
/**
* @hidden
*/
export const dateInRange = (candidate, min, max) => {
if (!candidate) {
return candidate;
}
if (min && candidate < min) {
return cloneDate(min);
}
if (max && candidate > max) {
return cloneDate(max);
}
return candidate;
};
/**
* @hidden
*/
export const timeInRange = (candidate, min, max) => {
if (!candidate || !min || !max) {
return candidate;
}
const { candidateValue, minValue, maxValue } = normalizeTimes(candidate, min, max);
if (candidateValue < minValue) {
return setTime(candidate, min);
}
if (candidateValue > maxValue) {
return setTime(candidate, max);
}
return candidate;
};
/**
* @hidden
*/
export const getNow = () => new Date();
/**
* @hidden
*/
export const getToday = () => getDate(new Date());
/**
* @hidden
*/
export const noop = (_) => { }; // eslint-disable-line no-empty
/**
* @hidden
*/
export const isWindowAvailable = () => {
return typeof window !== 'undefined';
};
/**
* @hidden
*/
export const stringifyClassObject = (classes) => {
const pushToAcc = (acc, cls) => classes[cls] ? acc.concat(cls) : acc;
return Object.keys(classes).reduce(pushToAcc, []).join(' ');
};
/**
* @hidden
*/
export const shiftWeekNames = (names, offset) => (names.slice(offset).concat(names.slice(0, offset)));
/**
* @hidden
*/
export const approximateStringMatching = (oldTextOrigin, oldFormat, newTextOrigin, caret) => {
// Remove the right part of the cursor.
//oldFormat = oldFormat.substring(0, caret + oldText.length - newText.length);
const oldIndex = caret + oldTextOrigin.length - newTextOrigin.length;
const oldTextSeparator = oldTextOrigin[oldIndex];
const oldText = oldTextOrigin.substring(0, caret + oldTextOrigin.length - newTextOrigin.length);
const newText = newTextOrigin.substring(0, caret);
const diff = [];
// Handle typing a single character over the same selection.
if (oldText === newText && caret > 0) {
diff.push([oldFormat[caret - 1], newText[caret - 1]]);
return diff;
}
if (oldText.indexOf(newText) === 0 && (newText.length === 0 || oldFormat[newText.length - 1] !== oldFormat[newText.length])) {
// Handle Delete/Backspace.
let deletedSymbol = "";
//XXX:
// Whole text is replaced with a same char
// Nasty patch required to keep the selection in the first segment
if (newText.length === 1) {
diff.push([oldFormat[0], newText[0]]);
}
for (let i = newText.length; i < oldText.length; i++) {
if (oldFormat[i] !== deletedSymbol && oldFormat[i] !== "_") {
deletedSymbol = oldFormat[i];
diff.push([deletedSymbol, ""]);
}
}
return diff;
}
// Handle inserting text (the new text is longer than the previous one).
// Handle typing over a literal as well.
if (newText.indexOf(oldText) === 0 || oldFormat[caret - 1] === "_") {
let symbol = oldFormat[0];
for (let i = Math.max(0, oldText.length - 1); i < oldFormat.length; i++) {
if (oldFormat[i] !== "_") {
symbol = oldFormat[i];
break;
}
}
return [[symbol, newText[caret - 1]]];
}
// Handle entering a space or a separator, for navigation to the next item.
if (newText[newText.length - 1] === " " || (newText[newText.length - 1] === oldTextSeparator && oldFormat[oldIndex] === '_')) {
return [[oldFormat[caret - 1], "_"]];
}
// Handle typing over a correctly selected part.
return [[oldFormat[caret - 1], newText[caret - 1]]];
};
/**
* @hidden
*/
export const domContainerFactory = (type) => (children, classes = "", styles = {}) => {
const container = document.createElement(type);
container.className = classes;
Object.keys(styles).map(key => container.style[key] = styles[key]);
if (typeof children === 'string') {
container.innerHTML = children || '';
}
else {
(children || []).forEach(child => child && container.appendChild(child));
}
return container;
};
/**
* @hidden
*/
export const hasChange = (changes, field) => changes[field] !== undefined;
/**
* @hidden
*/
export const hasExistingValue = (changes, field) => changes[field] && changes[field].currentValue !== undefined && changes[field].currentValue !== null;
/**
* @hidden
*/
export const last = (list = []) => list && list[list.length - 1];
/**
* @hidden
*/
export const isInSelectionRange = (value, selectionRange) => {
const { start, end } = selectionRange || EMPTY_SELECTIONRANGE;
if (!start || !end) {
return false;
}
return start < value && value < end;
};
/**
* @hidden
*/
export const either = (value1, value2) => value1 || value2;
/**
* @hidden
*/
export const clampRange = (value) => ({ start: value, end: value });
/**
* @hidden
*/
export const isEqualRange = (initial, updated) => {
const { start: initialStart, end: initialEnd } = initial || EMPTY_SELECTIONRANGE;
const { start: updatedStart, end: updatedEnd } = updated || EMPTY_SELECTIONRANGE;
return isEqual(initialStart, updatedStart) && isEqual(initialEnd, updatedEnd);
};
/**
* @hidden
*/
export const areDatesEqual = (first, second) => {
first = first || [];
second = second || [];
return first.length === second.length && first.every((date, index) => isEqual(date, second[index]));
};
/**
* @hidden
*/
export const sortDates = (dates) => {
return dates.filter(date => isPresent(date)).sort((a, b) => a.getTime() - b.getTime());
};
/**
* @hidden
*
* Creates a new date based on the date information from the specified date portion
* and the time information from the time portion.
* If a parameter is not provided, returns `null`.
*/
export const mergeDateAndTime = (date, time) => {
if (!(date && time)) {
return null;
}
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), time.getHours(), time.getMinutes(), time.getSeconds(), time.getMilliseconds());
};
/**
* @hidden
*/
export const lastMillisecondOfDate = (date) => {
if (!date) {
return null;
}
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
};
/**
* @hidden
*
* Returns an array with dates ranging between and including the specified start and
* end dates that are evaluated as disabled.
*/
export const disabledDatesInRange = (start, end, isDateDisabled) => {
if (!(start && end && isDateDisabled) || (start > end)) {
return [];
}
const dates = [];
let current = start;
while (current <= end) {
if (isDateDisabled(current)) {
dates.push(current);
}
current = addDays(current, 1);
}
return dates;
};
/**
* @hidden
*
* Crops the last two digits from the year of the provided date value.
*/
export const cropTwoDigitYear = (date) => {
if (!isPresent(date) || isNaN(date.getTime())) {
return 0;
}
return Number(date
.getFullYear()
.toString()
.slice(-2));
};
/**
* @hidden
*
* Used when resetting millisecond segment value in the DateInput
*/
export const msPaddingFromFormat = (format) => {
return Array(format.match(/S+(\1)/)[0].length).join("0"); // eslint-disable-line no-useless-backreference
};
/**
* @hidden
*/
export const millisecondDigitsInFormat = (format) => {
const result = format && format.match(/S+(\1)/); // eslint-disable-line no-useless-backreference
return result ? result[0].length : 0;
};
/**
* @hidden
*/
export const millisecondStepFor = (digits) => {
return Math.pow(10, 3 - digits);
};
/**
* @hidden
*
* Checks if a tab keydown would would move the focus outside of the calendar.
*/
export const isTabExitingCalendar = (calendarType, focusedElement, shiftKey) => {
if (!isPresent(focusedElement)) {
return false;
}
return calendarType === 'infinite' || ( // infinte calendar is always exited on first tab keydown
calendarType === 'classic' &&
(shiftKey && focusedElement.classList.contains('k-calendar-table')) || // exited on main calendar element focused and back-tab
(!shiftKey && focusedElement.classList.contains('k-calendar-table')) // exited on next button focused and regular tab
);
};
/**
* @hidden
* Returns the size class based on the component and size input.
*/
export const getSizeClass = (component, size) => {
const SIZE_CLASSES = {
'small': `k-${component}-sm`,
'medium': `k-${component}-md`,
'large': `k-${component}-lg`
};
return SIZE_CLASSES[size];
};
/**
* @hidden
* Returns the rounded class based on the rounded input.
*/
export const getRoundedClass = (rounded) => {
const ROUNDED_CLASSES = {
'small': 'k-rounded-sm',
'medium': 'k-rounded-md',
'large': 'k-rounded-lg',
'full': 'k-rounded-full'
};
return ROUNDED_CLASSES[rounded];
};
/**
* @hidden
* Return the fillMode class based on the component and fillMode input.
*/
export const getFillModeClass = (component, fillMode) => {
const FILLMODE_CLASSES = {
'solid': `k-${component}-solid`,
'flat': `k-${component}-flat`,
'outline': `k-${component}-outline`
};
return FILLMODE_CLASSES[fillMode];
};
/**
* @hidden
*/
export const DEFAULT_ROUNDED = 'medium';
/**
* @hidden
*/
export const DEFAULT_SIZE = 'medium';
/**
* @hidden
*/
export const DEFAULT_FILL_MODE = 'solid';