@progress/kendo-angular-scheduler
Version:
Kendo UI Scheduler Angular - Outlook or Google-style angular scheduler calendar. Full-featured and customizable embedded scheduling from the creator developers trust for professional UI components.
352 lines (351 loc) • 13 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 { isEqualDate } from '@progress/kendo-date-math';
import { formatDate } from '@progress/kendo-angular-intl';
import { isPresent, getField, isArray, isObject, isString } from '../common/util';
import { hasClasses } from '../common/dom-queries';
/** @hidden */
export const intersects = (startTime, endTime, periodStart, periodEnd) => (startTime < periodStart && endTime > periodEnd) ||
(periodStart <= startTime && startTime < periodEnd) ||
(periodStart < endTime && endTime <= periodEnd && startTime < endTime);
/** @hidden */
export const dateInRange = (date, start, end) => start.getTime() <= date.getTime() && date.getTime() <= end.getTime();
/** @hidden */
export const roundAllDayEnd = ({ start, end }) => {
const startDate = start.stripTime();
const endDate = end.stripTime();
return endDate.getTime() !== end.getTime() || startDate.getTime() === endDate.getTime() ? endDate.addDays(1) : endDate;
};
/** @hidden */
export function toInvariantTime(date) {
const staticDate = new Date(1980, 0, 1, 0, 0, 0);
if (date) {
staticDate.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
}
return staticDate;
}
/**
* @hidden
* TODO Move to date-math
*/
export const addUTCDays = (date, offset) => {
const newDate = new Date(date.getTime());
newDate.setUTCDate(newDate.getUTCDate() + offset);
return newDate;
};
/**
* @hidden
*/
export function timeOnDate(date, hours = 0, minutes = 0, seconds = 0, ms = 0) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hours, minutes, seconds, ms);
}
// TODO: name? move to date-math
/** @hidden */
export function toUTCTime(localDate, localTime) {
return new Date(Date.UTC(localDate.getFullYear(), localDate.getMonth(), localDate.getDate(), localTime.getHours(), localTime.getMinutes(), localTime.getSeconds(), localTime.getMilliseconds()));
}
// TODO: move to date-math
/** @hidden */
export function toUTCDate(localDate) {
return new Date(Date.UTC(localDate.getFullYear(), localDate.getMonth(), localDate.getDate()));
}
// TODO: move to date-math
/** @hidden */
export function getUTCDate(utcDate) {
return new Date(Date.UTC(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate()));
}
// TODO: move to date-math
/** @hidden */
export function toUTCDateTime(localDate) {
return new Date(Date.UTC(localDate.getFullYear(), localDate.getMonth(), localDate.getDate(), localDate.getHours(), localDate.getMinutes(), localDate.getSeconds(), localDate.getMilliseconds()));
}
/** @hidden */
export function dateWithTime(target, time) {
return new Date(target.getFullYear(), target.getMonth(), target.getDate(), time.getHours(), time.getMinutes());
}
/**
* @hidden
* Flips the start and end values of a slot selection based on whether
* the last dragged-over slot is before or after the slot where the selection began.
*/
export function normaliseRangeStartAndEnd(selectionOrigin, currentSlot) {
let start;
let end;
if (currentSlot.end <= selectionOrigin.end) {
end = selectionOrigin.end;
start = currentSlot.start;
}
else {
start = selectionOrigin.start;
end = currentSlot.end;
}
return { start, end };
}
function getDataIdx(value, resource) {
const data = resource.data;
for (let dataIdx = 0; dataIdx < data.length; dataIdx++) {
if (getField(data[dataIdx], resource.valueField) === value) {
return dataIdx;
}
}
return -1;
}
function resourceItem(value, resource) {
const index = getDataIdx(value, resource);
return index >= 0 ? resource.data[index] : {};
}
function resourceItems(values, resource) {
return values.map(value => resourceItem(value, resource));
}
function cloneResources(arr) {
const result = [];
for (let idx = 0; idx < arr.length; idx++) {
const clone = Object.assign({}, arr[idx]);
clone.resources = clone.resources.slice(0);
result.push(clone);
}
return result;
}
/** @hidden */
export function resourceItemByValue(event, resource) {
const value = getField(event, resource.field);
if (Array.isArray(value)) {
return resourceItems(value, resource);
}
return resourceItem(value, resource);
}
function addNotGroupedResources(event, resources, allResources) {
for (let resourceIdx = 0; resourceIdx < resources.length; resourceIdx++) {
const current = resources[resourceIdx];
for (let idx = 0; idx < allResources.length; idx++) {
const item = allResources[idx];
if (!current.resources[idx] && item.data) {
current.resources[idx] = resourceItemByValue(event, item);
}
}
}
}
/** @hidden */
export function eventResources(event, { taskResources, hasGroups, spans, allResources = [] }) {
let resources = [];
for (let resourceIdx = 0; resourceIdx < taskResources.length; resourceIdx++) {
const resource = taskResources[resourceIdx];
if (!resource.data) {
resources = [{ leafIdx: 0, resources: [] }];
continue;
}
const resourceIndex = allResources.indexOf(resource);
let values = getField(event, resource.field);
if (!Array.isArray(values)) {
values = [values];
}
const expandedResources = [];
for (let valueIdx = 0; valueIdx < values.length; valueIdx++) {
const dataIdx = getDataIdx(values[valueIdx], resource);
if (dataIdx < 0) {
if (hasGroups) {
// No match for this resource, but the event might still match other resources.
continue;
}
return [{ leafIdx: 0, resources: [] }];
}
const item = resource.data[dataIdx];
// has groups - need all copies of the multiple resource
// no groups - just the first
if (resourceIdx === 0 && (hasGroups || valueIdx === 0)) {
const resourceItems = [];
resourceItems[resourceIndex] = resource.multiple && !hasGroups ? [item] : item;
resources.push({
leafIdx: hasGroups ? dataIdx * spans[resourceIdx] : 0,
color: getField(item, resource.colorField),
resources: resourceItems
});
}
else if (hasGroups) { // don't create multiple resource groups if no groups for multiple resources
let currentResources = resources;
if (values.length > 1) {
currentResources = cloneResources(resources);
expandedResources.push(...currentResources);
}
for (let currentIdx = 0; currentIdx < currentResources.length; currentIdx++) {
currentResources[currentIdx].leafIdx += dataIdx * spans[resourceIdx];
currentResources[currentIdx].resources[resourceIndex] = item;
}
}
else if (valueIdx > 0) {
for (let idx = 0; idx < resources.length; idx++) {
resources[idx].resources[resourceIndex].push(item);
}
}
}
if (expandedResources.length) {
resources = expandedResources;
}
}
addNotGroupedResources(event, resources, allResources);
return resources;
}
/** @hidden */
export function assignTasksResources(tasks, options) {
for (let idx = 0; idx < tasks.length; idx++) {
const task = tasks[idx];
task.resources = eventResources(task.event, options);
}
}
/**
* @hidden
*/
export function isEmptyResource(resources) {
return Array.isArray(resources) && resources.length === 1 && resources[0] === undefined;
}
/**
* @hidden
*/
export function resourcesMatch(res1, res2) {
if (res1.length !== res2.length) {
return false;
}
if (isEmptyResource(res1) && isEmptyResource(res2)) {
return true;
}
return res1.every(r1 => res2.some(r2 => r2.value === r1.value));
}
/**
* @hidden
*/
export function isSameRange(range1, range2) {
return (range1.start.getTime() === range2.start.getTime() &&
range1.end.getTime() === range2.end.getTime() &&
range1.isAllDay === range2.isAllDay &&
resourcesMatch(range1.resources, range2.resources));
}
/** @hidden */
export function findRowIndex(events, data) {
if (data.rowIndex !== undefined) {
return data.rowIndex;
}
for (let idx = 0; idx < events.length; idx++) {
if (!events[idx]) {
return idx;
}
}
return events.length;
}
/** @hidden */
export function isRecurrence(task) {
return Boolean(task.event && task.event.recurrenceRule);
}
/** @hidden */
export function isRecurrenceException(task) {
return task.event && isPresent(task.event.recurrenceId) && !task.event.recurrenceRule;
}
/** @hidden */
export const rectContains = (rect, left, top, scaleX = 1) => rect.left * scaleX <= left && left <= rect.left * scaleX + rect.width * scaleX && rect.top * scaleX <= top && top <= rect.top * scaleX + rect.height * scaleX;
/** @hidden */
export const rectContainsX = (rect, left, scaleX = 1) => rect.left * scaleX <= left && left <= rect.left * scaleX + rect.width * scaleX;
/** @hidden */
export const toPx = value => `${value}px`;
/** @hidden */
export const elementOffset = (element) => {
if (!element) {
return null;
}
const box = element.getBoundingClientRect();
const documentElement = document.documentElement;
return {
top: box.top + (window.pageYOffset || documentElement.scrollTop) - (documentElement.clientTop || 0),
left: box.left + (window.pageXOffset || documentElement.scrollLeft) - (documentElement.clientLeft || 0),
width: box.width,
height: box.height
};
};
/** @hidden */
export const pointDistance = (x1, y1, x2, y2) => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
/** @hidden */
export const ignoreContentChild = child => child.nodeName === 'KENDO-RESIZE-SENSOR' || hasClasses(child, 'k-loading-mask');
/** @hidden */
export const setCoordinates = (element, coordinates) => {
for (const field in coordinates) {
if (coordinates.hasOwnProperty(field)) {
element.style[field] = toPx(coordinates[field]);
}
}
};
/** @hidden */
export const convertNgClassBindings = (bindingValues) => {
const result = [];
if (isString(bindingValues)) {
result.push(bindingValues);
}
else if (isArray(bindingValues)) {
result.push(...bindingValues);
}
else if (isObject(bindingValues)) {
for (const field in bindingValues) {
if (bindingValues.hasOwnProperty(field) && bindingValues[field]) {
result.push(field);
}
}
}
return result;
};
/**
* @hidden
*/
export function formatEventTime(start, end, isAllDay, localeId) {
const dateFormat = { skeleton: 'yMMMMEEEEd' };
const timeFormat = 't';
const startFormat = formatEventStart(start, dateFormat, timeFormat, isAllDay, localeId);
const endFormat = formatEventEnd(start, end, dateFormat, timeFormat, localeId);
return isAllDay ?
`${startFormat}` :
`${startFormat}-${endFormat}`;
}
/**
* @hidden
*/
export function formValueOrDefault(group, field, defaultValue) {
const control = group.get(field);
if (!control) {
return defaultValue;
}
return control.value || defaultValue;
}
/**
* @hidden
*/
export const isWorkWeekDay = (day, start, end) => {
if (end < start) {
return day <= end || start <= day;
}
return start <= day && day <= end;
};
/**
* @hidden
*/
export const alwaysFalse = () => false;
/**
* @hidden
*/
const formatEventStart = (start, dateFormat, timeFormat, isAllDay, localeId) => {
let startFormat = `${formatDate(start, dateFormat, localeId)}`;
// the time is relevant only when the event isn't an all day event
if (!isAllDay) {
startFormat += `, ${formatDate(start, timeFormat, localeId)}`;
}
return startFormat;
};
/**
* @hidden
*/
const formatEventEnd = (start, end, dateFormat, timeFormat, localeId) => {
let endFormat = '';
// the end date is relevant only when the event ends on a different day from when it starts
if (!isEqualDate(start, end)) {
endFormat = `${formatDate(end, dateFormat, localeId)}, `;
}
endFormat += `${formatDate(end, timeFormat, localeId)}`;
return endFormat;
};