import { EventDef } from '../structs/event-def.js' import { EVENT_NON_DATE_REFINERS, EVENT_DATE_REFINERS } from '../structs/event-parse.js' import { EventInstance } from '../structs/event-instance.js' import { EVENT_UI_REFINERS, EventUiHash } from '../component/event-ui.js' import { EventMutation, applyMutationToEventStore } from '../structs/event-mutation.js' import { diffDates, computeAlignedDayRange } from '../util/date.js' import { createDuration, durationsEqual } from '../datelib/duration.js' import { createFormatter } from '../datelib/formatting.js' import { CalendarContext } from '../CalendarContext.js' import { getRelevantEvents, EventStore } from '../structs/event-store.js' import { Dictionary } from '../options.js' import { EventApi } from './EventApi.js' import { EventSourceImpl } from './EventSourceImpl.js' import { DateInput, DurationInput, FormatterInput, } from './structs.js' export class EventImpl implements EventApi { _context: CalendarContext _def: EventDef _instance: EventInstance | null // instance will be null if expressing a recurring event that has no current instances, // OR if trying to validate an incoming external event that has no dates assigned constructor(context: CalendarContext, def: EventDef, instance?: EventInstance) { this._context = context this._def = def this._instance = instance || null } /* TODO: make event struct more responsible for this */ setProp(name: string, val: any): void { if (name in EVENT_DATE_REFINERS) { console.warn('Could not set date-related prop \'name\'. Use one of the date-related methods instead.') // TODO: make proper aliasing system? } else if (name === 'id') { val = EVENT_NON_DATE_REFINERS[name](val) this.mutate({ standardProps: { publicId: val }, // hardcoded internal name }) } else if (name in EVENT_NON_DATE_REFINERS) { val = EVENT_NON_DATE_REFINERS[name](val) this.mutate({ standardProps: { [name]: val }, }) } else if (name in EVENT_UI_REFINERS) { let ui = EVENT_UI_REFINERS[name](val) if (name === 'color') { ui = { backgroundColor: val, borderColor: val } } else if (name === 'editable') { ui = { startEditable: val, durationEditable: val } } else { ui = { [name]: val } } this.mutate({ standardProps: { ui }, }) } else { console.warn(`Could not set prop '${name}'. Use setExtendedProp instead.`) } } setExtendedProp(name: string, val: any): void { this.mutate({ extendedProps: { [name]: val }, }) } setStart(startInput: DateInput, options: { granularity?: string, maintainDuration?: boolean } = {}): void { let { dateEnv } = this._context let start = dateEnv.createMarker(startInput) if (start && this._instance) { // TODO: warning if parsed bad let instanceRange = this._instance.range let startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity) // what if parsed bad!? if (options.maintainDuration) { this.mutate({ datesDelta: startDelta }) } else { this.mutate({ startDelta }) } } } setEnd(endInput: DateInput | null, options: { granularity?: string } = {}): void { let { dateEnv } = this._context let end if (endInput != null) { end = dateEnv.createMarker(endInput) if (!end) { return // TODO: warning if parsed bad } } if (this._instance) { if (end) { let endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity) this.mutate({ endDelta }) } else { this.mutate({ standardProps: { hasEnd: false } }) } } } setDates(startInput: DateInput, endInput: DateInput | null, options: { allDay?: boolean, granularity?: string } = {}): void { let { dateEnv } = this._context let standardProps = { allDay: options.allDay } as any let start = dateEnv.createMarker(startInput) let end if (!start) { return // TODO: warning if parsed bad } if (endInput != null) { end = dateEnv.createMarker(endInput) if (!end) { // TODO: warning if parsed bad return } } if (this._instance) { let instanceRange = this._instance.range // when computing the diff for an event being converted to all-day, // compute diff off of the all-day values the way event-mutation does. if (options.allDay === true) { instanceRange = computeAlignedDayRange(instanceRange) } let startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity) if (end) { let endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity) if (durationsEqual(startDelta, endDelta)) { this.mutate({ datesDelta: startDelta, standardProps }) } else { this.mutate({ startDelta, endDelta, standardProps }) } } else { // means "clear the end" standardProps.hasEnd = false this.mutate({ datesDelta: startDelta, standardProps }) } } } moveStart(deltaInput: DurationInput): void { let delta = createDuration(deltaInput) if (delta) { // TODO: warning if parsed bad this.mutate({ startDelta: delta }) } } moveEnd(deltaInput: DurationInput): void { let delta = createDuration(deltaInput) if (delta) { // TODO: warning if parsed bad this.mutate({ endDelta: delta }) } } moveDates(deltaInput: DurationInput): void { let delta = createDuration(deltaInput) if (delta) { // TODO: warning if parsed bad this.mutate({ datesDelta: delta }) } } setAllDay(allDay: boolean, options: { maintainDuration?: boolean } = {}): void { let standardProps = { allDay } as any let { maintainDuration } = options if (maintainDuration == null) { maintainDuration = this._context.options.allDayMaintainDuration } if (this._def.allDay !== allDay) { standardProps.hasEnd = maintainDuration } this.mutate({ standardProps }) } formatRange(formatInput: FormatterInput): string { let { dateEnv } = this._context let instance = this._instance let formatter = createFormatter(formatInput) if (this._def.hasEnd) { return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, { forcedStartTzo: instance.forcedStartTzo, forcedEndTzo: instance.forcedEndTzo, }) } return dateEnv.format(instance.range.start, formatter, { forcedTzo: instance.forcedStartTzo, }) } mutate(mutation: EventMutation): void { // meant to be private. but plugins need access let instance = this._instance if (instance) { let def = this._def let context = this._context let { eventStore } = context.getCurrentData() let relevantEvents = getRelevantEvents(eventStore, instance.instanceId) let eventConfigBase = { '': { // HACK. always allow API to mutate events display: '', startEditable: true, durationEditable: true, constraints: [], overlap: null, allows: [], backgroundColor: '', borderColor: '', textColor: '', classNames: [], }, } as EventUiHash relevantEvents = applyMutationToEventStore(relevantEvents, eventConfigBase, mutation, context) let oldEvent = new EventImpl(context, def, instance) // snapshot this._def = relevantEvents.defs[def.defId] this._instance = relevantEvents.instances[instance.instanceId] context.dispatch({ type: 'MERGE_EVENTS', eventStore: relevantEvents, }) context.emitter.trigger('eventChange', { oldEvent, event: this, relatedEvents: buildEventApis(relevantEvents, context, instance), revert() { context.dispatch({ type: 'RESET_EVENTS', eventStore, // the ORIGINAL store }) }, }) } } remove(): void { let context = this._context let asStore = eventApiToStore(this) context.dispatch({ type: 'REMOVE_EVENTS', eventStore: asStore, }) context.emitter.trigger('eventRemove', { event: this, relatedEvents: [], revert() { context.dispatch({ type: 'MERGE_EVENTS', eventStore: asStore, }) }, }) } get source(): EventSourceImpl | null { let { sourceId } = this._def if (sourceId) { return new EventSourceImpl( this._context, this._context.getCurrentData().eventSources[sourceId], ) } return null } get start(): Date | null { return this._instance ? this._context.dateEnv.toDate(this._instance.range.start) : null } get end(): Date | null { return (this._instance && this._def.hasEnd) ? this._context.dateEnv.toDate(this._instance.range.end) : null } get startStr(): string { let instance = this._instance if (instance) { return this._context.dateEnv.formatIso(instance.range.start, { omitTime: this._def.allDay, forcedTzo: instance.forcedStartTzo, }) } return '' } get endStr(): string { let instance = this._instance if (instance && this._def.hasEnd) { return this._context.dateEnv.formatIso(instance.range.end, { omitTime: this._def.allDay, forcedTzo: instance.forcedEndTzo, }) } return '' } // computable props that all access the def // TODO: find a TypeScript-compatible way to do this at scale get id() { return this._def.publicId } get groupId() { return this._def.groupId } get allDay() { return this._def.allDay } get title() { return this._def.title } get url() { return this._def.url } get display() { return this._def.ui.display || 'auto' } // bad. just normalize the type earlier get startEditable() { return this._def.ui.startEditable } get durationEditable() { return this._def.ui.durationEditable } get constraint() { return this._def.ui.constraints[0] || null } get overlap() { return this._def.ui.overlap } get allow() { return this._def.ui.allows[0] || null } get backgroundColor() { return this._def.ui.backgroundColor } get borderColor() { return this._def.ui.borderColor } get textColor() { return this._def.ui.textColor } // NOTE: user can't modify these because Object.freeze was called in event-def parsing get classNames() { return this._def.ui.classNames } get extendedProps() { return this._def.extendedProps } toPlainObject(settings: { collapseExtendedProps?: boolean, collapseColor?: boolean } = {}): Dictionary { let def = this._def let { ui } = def let { startStr, endStr } = this let res: Dictionary = { allDay: def.allDay, } if (def.title) { res.title = def.title } if (startStr) { res.start = startStr } if (endStr) { res.end = endStr } if (def.publicId) { res.id = def.publicId } if (def.groupId) { res.groupId = def.groupId } if (def.url) { res.url = def.url } if (ui.display && ui.display !== 'auto') { res.display = ui.display } // TODO: what about recurring-event properties??? // TODO: include startEditable/durationEditable/constraint/overlap/allow if (settings.collapseColor && ui.backgroundColor && ui.backgroundColor === ui.borderColor) { res.color = ui.backgroundColor } else { if (ui.backgroundColor) { res.backgroundColor = ui.backgroundColor } if (ui.borderColor) { res.borderColor = ui.borderColor } } if (ui.textColor) { res.textColor = ui.textColor } if (ui.classNames.length) { res.classNames = ui.classNames } if (Object.keys(def.extendedProps).length) { if (settings.collapseExtendedProps) { Object.assign(res, def.extendedProps) } else { res.extendedProps = def.extendedProps } } return res } toJSON(): Dictionary { return this.toPlainObject() } } export function eventApiToStore(eventApi: EventImpl): EventStore { let def = eventApi._def let instance = eventApi._instance return { defs: { [def.defId]: def }, instances: instance ? { [instance.instanceId]: instance } : {}, } } export function buildEventApis(eventStore: EventStore, context: CalendarContext, excludeInstance?: EventInstance): EventImpl[] { let { defs, instances } = eventStore let eventApis: EventImpl[] = [] let excludeInstanceId = excludeInstance ? excludeInstance.instanceId : '' for (let id in instances) { let instance = instances[id] let def = defs[instance.defId] if (instance.instanceId !== excludeInstanceId) { eventApis.push(new EventImpl(context, def, instance)) } } return eventApis }