UNPKG

@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.

325 lines (324 loc) 12.1 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { parseDate, formatDate } from '@progress/kendo-angular-intl'; import { toLocalDate } from '@progress/kendo-date-math'; import { BehaviorSubject } from 'rxjs'; import { setter } from '../common/setter'; import { getter } from '../common/getter'; import { defaultModelFields } from '../common/default-model-fields'; import { isRecurring, isException, isPresent, cloneTo, assignField } from '../common/util'; const DATE_FORMATS = [ "yyyyMMddTHHmmssSSSXXX", "yyyyMMddTHHmmssXXX", "yyyyMMddTHHmmss", "yyyyMMddTHHmm", "yyyyMMddTHH", "yyyyMMdd" ]; /** * A base implementation of the [edit service]({% slug api_scheduler_editservice %}) which persists data to traditional CRUD services such as OData. * * To support custom models, the `BaseEditService` class requires a [field map]({% slug api_scheduler_schedulermodelfields %}) as a constructor parameter. Subclasses require you to * implement the `read` operation, which is not called directly by the base class, and the `save` method which persists the created, * updated, and deleted entities. * * The [`events`](#toc-events) observable will publish the current data which is set upon subscription by using, for example, an [async pipe](https://angular.io/api/common/AsyncPipe) * ([more information]({% slug editing_directives_scheduler %}#toc-custom-service)). * * Implementations which utilize dedicated services, such as Google Calendar and Microsoft Exchange, will typically implement the * [`EditService`]({% slug api_scheduler_editservice %}) of the Scheduler directly. * * See example in [this article]({% slug custom_reactive_editing_scheduler %}). */ export class BaseEditService { /** * The model field map that will be used during the reading and updating of data items. */ fields; /** * An observable stream with the current events. */ events; /** * An array of the currently loaded events which is populated by the derived class. */ data = []; /** * The source subject for the `events` observable. */ source = new BehaviorSubject([]); createdItems = []; updatedItems = []; deletedItems = []; getId; getRecurrenceId; getRecurrenceRule; getRecurrenceExceptions; getStart; setId; setRecurrenceRule; setRecurrenceExceptions; setRecurrenceId; /** * Initializes the base edit service. * * @param fields - A field map that will be used for reading and modifying model objects. Defaults to the [`SchedulerEvent`]({% slug api_scheduler_schedulerevent %}) fields. */ constructor(fields) { this.events = this.source.asObservable(); this.fields = { ...defaultModelFields, ...fields }; this.getId = getter(this.fields.id); this.getRecurrenceId = getter(this.fields.recurrenceId); this.getRecurrenceRule = getter(this.fields.recurrenceRule); this.getRecurrenceExceptions = getter(this.fields.recurrenceExceptions); this.getStart = getter(this.fields.start); this.setId = setter(this.fields.id); this.setRecurrenceRule = setter(this.fields.recurrenceRule); this.setRecurrenceExceptions = setter(this.fields.recurrenceExceptions); this.setRecurrenceId = setter(this.fields.recurrenceId); } create(event) { this.logCreate(event); this.saveChanges(); } /* * Creates an exception to a recurring series. * * The `createException` method performs the following operations: * * Adds the start date of the event to the `recurrenceExceptions` of the master event (recurrence head). * * Creates a new event that stores the recurrence exception itself. */ createException(event, value) { const exception = this.buildException(value); this.logRemoveOccurrence(event); this.logCreate(exception); this.saveChanges(); } update(event, value) { this.assignValues(event, value); this.logUpdate(event); this.saveChanges(); } remove(event) { this.logRemove(event); this.saveChanges(); } removeSeries(event) { const id = this.getId(event); const recurrenceId = this.getRecurrenceId(event); const isHead = this.isRecurrenceHead(event); this.removeItemAndExceptions(isHead ? id : recurrenceId); this.saveChanges(); } removeOccurrence(event) { this.logRemoveOccurrence(event); this.saveChanges(); } /** * Returns the master recurring event for a specified recurring event. * * @param event - An event from the recurrence series. * @returns the master recurring event for the series. */ findRecurrenceMaster(event) { const id = this.getId(event); const recurrenceId = this.getRecurrenceId(event); const headId = this.isRecurrenceHead(event) ? id : recurrenceId; const index = this.itemIndex(headId, this.data); return this.data[index]; } /** * Checks if the event is part of the recurrence series. * * @param event - The event that will be checked. * @returns `true` if the event is an occurrence, an exception, or a master event. Otherwise, returns `false`. */ isRecurring(event) { return isRecurring(event, this.fields); } /** * Checks if the event is a recurrence exception. * * @param event - The event that will be checked. * @returns `true` if the event is a unique event which belongs to a recurrence series. Otherwise, returns `false`. */ isException(event) { return isException(event, this.fields); } /** * Returns a Boolean value which indicates if the event is new. * If the `ID` field is defined, the default implementation returns `true`. * Can be overridden to implement different conditions. * * @param event - The event that will be checked. */ isNew(event) { const id = this.getId(event); return !isPresent(id); } /** * Returns the next `ID` that will be used for new events. * The default implementation returns `undefined`. */ nextId() { return undefined; } /** * Copies values to the target model instance. * To copy the top-level fields, the base implementation uses * [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign). * To copy nested fields, override `assignValues` and handle the model-specific cases. * * @param target - The target object that will receive the field values. * @param source - The source object from which the fields will be read. */ assignValues(target, source) { cloneTo(source, target); } /** * Clones an existing model object. * To copy the top-level model fields, the base creates an empty object and calls [`assignValues`](#toc-assignvalues). * To create models of the correct type, override `cloneEvent`. * * @param event - The model instance to copy. * @returns TEvent - The new model instance. */ cloneEvent(event) { const result = {}; this.assignValues(result, event); return result; } /** * A utility method which parses recurrence exception dates in an ISO format. * * @example * ```ts-no-run * const exdates = '20180614T060000Z;20180615T060000Z'; * const result = super.parseExceptions(exdates); * * // console.log(result); * // Array [ Date 2018-06-14T03:00:00.000Z, Date 2018-06-15T03:00:00.000Z ] * ``` * * @param value - A comma-separated list of ISO-formatted dates. * @returns Date[] - The recurrence exceptions as local dates. */ parseExceptions(value) { if (!isPresent(value) || value === '') { return []; } return value .split(';') .map(ex => parseDate(ex, DATE_FORMATS) || undefined); } /** * A utility method which serializes recurrence exception dates in an ISO format. * * @example * ```ts-no-run * const exdates = [ new Date(2018, 5, 14, 3, 0, 0), new Date(2018, 5, 15, 3, 0, 0) ]; * const result = super.serializeExceptions(exdates); * * // console.log(result); * // '20180614T060000Z;20180615T060000Z' * ``` * * @param value - An array of `Date` instances. * @returns string - A comma-separated list of ISO-formatted dates. */ serializeExceptions(exceptions) { if (!exceptions || exceptions.length === 0) { return ''; } return exceptions.map(date => formatDate(toLocalDate(date), 'yyyyMMddTHHmmss') + 'Z').join(';'); } reset() { this.data = []; this.deletedItems = []; this.updatedItems = []; this.createdItems = []; } itemIndex(id, items) { for (let idx = 0; idx < items.length; idx++) { if (this.getId(items[idx]) === id) { return idx; } } return -1; } buildException(item) { const fields = this.fields; const head = this.findRecurrenceMaster(item); const copy = this.cloneEvent(item); assignField(copy, head, fields.id); this.setId(copy, this.nextId()); this.setRecurrenceRule(copy, undefined); this.setRecurrenceId(copy, this.getId(head)); return copy; } isRecurrenceHead(item) { const id = this.getId(item); const recurrenceRule = this.getRecurrenceRule(item); return !!(id && recurrenceRule); } logCreate(item) { this.data = [...this.data, item]; this.source.next(this.data); this.createdItems.push(item); } logUpdate(item) { const id = this.getId(item); if (!this.isNew(item)) { const index = this.itemIndex(id, this.updatedItems); if (index !== -1) { this.updatedItems.splice(index, 1, item); } else { this.updatedItems.push(item); } } else { const index = this.createdItems.indexOf(item); this.createdItems.splice(index, 1, item); } } logRemove(item) { const id = this.getId(item); let index = this.itemIndex(id, this.data); this.data = this.data.filter((_, i) => i !== index); this.source.next(this.data); index = this.itemIndex(id, this.createdItems); if (index >= 0) { this.createdItems.splice(index, 1); } else { this.deletedItems.push(item); } index = this.itemIndex(id, this.updatedItems); if (index >= 0) { this.updatedItems.splice(index, 1); } } logRemoveOccurrence(event) { const head = this.findRecurrenceMaster(event); const exceptionDate = this.getStart(event); const currentExceptions = this.getRecurrenceExceptions(head) || []; this.setRecurrenceExceptions(head, [...currentExceptions, exceptionDate]); this.logUpdate(head); } removeItemAndExceptions(itemId) { this.deletedItems = this.deletedItems.concat(this.data.filter(ev => this.getRecurrenceId(ev) === itemId || this.getId(ev) === itemId)); } hasChanges() { return this.deletedItems.length + this.updatedItems.length + this.createdItems.length > 0; } saveChanges() { if (!this.hasChanges()) { return; } this.save(this.createdItems, this.updatedItems, this.deletedItems); this.reset(); } }