UNPKG

ical-generator

Version:

ical-generator is a small piece of code which generates ical calendar files

749 lines (662 loc) 22.6 kB
'use strict'; import type ICalEvent from './event.ts'; import { addOrGetCustomAttributes, formatDate, escape, generateCustomAttributes, checkDate, toDurationString, toJSON, checkNameAndMail } from './tools.ts'; import { type ICalDateTimeValue } from './types.ts'; import ICalAttendee, { type ICalAttendeeData } from './attendee.ts'; export enum ICalAlarmType { display = 'display', audio = 'audio', email = 'email' } export const ICalAlarmRelatesTo = { end: 'END', start: 'START' } as const; export type ICalAlarmRelatesTo = typeof ICalAlarmRelatesTo[keyof typeof ICalAlarmRelatesTo]; export type ICalAlarmTypeValue = keyof ICalAlarmType; export interface ICalAttachment { uri: string; mime: string | null; } export type ICalAlarmData = ICalAlarmBaseData | ICalAlarmTriggerData | ICalAlarmTriggerAfterData | ICalAlarmTriggerBeforeData; export type ICalAlarmTriggerData = ICalAlarmBaseData & { trigger: number | ICalDateTimeValue }; export type ICalAlarmTriggerAfterData = ICalAlarmBaseData & { triggerAfter: number | ICalDateTimeValue }; export type ICalAlarmTriggerBeforeData = ICalAlarmBaseData & { triggerBefore: number | ICalDateTimeValue }; export interface ICalAlarmBaseData { type?: ICalAlarmType; relatesTo?: ICalAlarmRelatesTo | null; repeat?: ICalAlarmRepeatData | null; attach?: string | ICalAttachment | null; description?: string | null; summary?: string | null; attendees?: ICalAttendee[] | ICalAttendeeData[]; x?: {key: string, value: string}[] | [string, string][] | Record<string, string>; } export interface ICalAlarmRepeatData { times: number; interval: number; } interface ICalInternalAlarmData { type: ICalAlarmType; trigger: ICalDateTimeValue | number; relatesTo: ICalAlarmRelatesTo | null; repeat: ICalAlarmRepeatData | null; interval: number | null; attach: ICalAttachment | null; description: string | null; summary: string | null; attendees: ICalAttendee[]; x: [string, string][]; } export interface ICalAlarmJSONData { type: ICalAlarmType; trigger: string | number; relatesTo: ICalAlarmRelatesTo | null; repeat: ICalAlarmRepeatData | null; interval: number | null; attach: ICalAttachment | null; description: string | null; summary: string | null; attendees: ICalAttendee[]; x: {key: string, value: string}[]; } /** * Usually you get an {@link ICalAlarm} object like this: * * ```javascript * import ical from 'ical-generator'; * const calendar = ical(); * const event = calendar.createEvent(); * const alarm = event.createAlarm(); * ``` * * You can also use the {@link ICalAlarm} object directly: * * ```javascript * import ical, {ICalAlarm} from 'ical-generator'; * const alarm = new ICalAlarm(); * event.alarms([alarm]); * ``` */ export default class ICalAlarm { private readonly data: ICalInternalAlarmData; private readonly event: ICalEvent; /** * Constructor of {@link ICalAttendee}. The event reference is required * to query the calendar's timezone and summary when required. * * @param data Alarm Data * @param event Reference to ICalEvent object */ constructor (data: ICalAlarmData, event: ICalEvent) { this.data = { type: ICalAlarmType.display, trigger: -600, relatesTo: null, repeat: null, interval: null, attach: null, description: null, summary: null, attendees: [], x: [] }; this.event = event; if (!event) { throw new Error('`event` option required!'); } if (data.type !== undefined) this.type(data.type); if ('trigger' in data && data.trigger !== undefined) this.trigger(data.trigger); if ('triggerBefore' in data && data.triggerBefore !== undefined) this.triggerBefore(data.triggerBefore); if ('triggerAfter' in data && data.triggerAfter !== undefined) this.triggerAfter(data.triggerAfter); if (data.repeat) this.repeat(data.repeat); if (data.attach !== undefined) this.attach(data.attach); if (data.description !== undefined) this.description(data.description); if (data.summary !== undefined) this.summary(data.summary); if (data.attendees !== undefined) this.attendees(data.attendees); if (data.x !== undefined) this.x(data.x); } /** * Get the alarm type * @since 0.2.1 */ type (type: ICalAlarmType): this; /** * Set the alarm type. See {@link ICalAlarmType} * for available status options. * @since 0.2.1 */ type (): ICalAlarmType; type (type?: ICalAlarmType): this | ICalAlarmType { if (type === undefined) { return this.data.type; } if (!type || !Object.keys(ICalAlarmType).includes(type)) { throw new Error('`type` is not correct, must be either `display` or `audio`!'); } this.data.type = type; return this; } /** * Get the trigger time for the alarm. Can either * be a date and time value ({@link ICalDateTimeValue}) or * a number, which will represent the seconds between * alarm and event start. The number is negative, if the * alarm is triggered after the event started. * * @since 0.2.1 */ trigger (): number | ICalDateTimeValue; /** * Use this method to set the alarm time. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = cal.createAlarm(); * * alarm.trigger(600); // -> 10 minutes before event starts * alarm.trigger(new Date()); // -> now * ``` * * You can use any supported date object, see * [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) * for details about supported values and timezone handling. * * @since 0.2.1 */ trigger (trigger: number | ICalDateTimeValue | Date): this; trigger (trigger?: number | ICalDateTimeValue | Date): this | number | ICalDateTimeValue { // Getter if (trigger === undefined && typeof this.data.trigger === 'number') { return -1 * this.data.trigger; } if (trigger === undefined) { return this.data.trigger; } // Setter if (typeof trigger === 'number' && isFinite(trigger)) { this.data.trigger = -1 * trigger; } else if(!trigger || typeof trigger === 'number') { throw new Error('`trigger` is not correct, must be a finite number or a supported date!'); } else { this.data.trigger = checkDate(trigger, 'trigger'); } return this; } /** * Get to which time alarm trigger relates to. * Can be either `START` or `END`. If the value is * `START` the alarm is triggerd relative to the event start time. * If the value is `END` the alarm is triggerd relative to the event end time * * @since 4.0.1 */ relatesTo(): ICalAlarmRelatesTo | null; /** * Use this method to set to which time alarm trigger relates to. * Works only if trigger is a `number` * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = cal.createAlarm(); * * alarm.trigger(600); // -> 10 minutes before event starts * * alarm.relatesTo('START'); // -> 10 minutes before event starts * alarm.relatesTo('END'); // -> 10 minutes before event ends * * alarm.trigger(-600); // -> 10 minutes after event starts * * alarm.relatesTo('START'); // -> 10 minutes after event starts * alarm.relatesTo('END'); // -> 10 minutes after event ends * ``` * @since 4.0.1 */ relatesTo(relatesTo: ICalAlarmRelatesTo | null): this; relatesTo(relatesTo?: ICalAlarmRelatesTo | null): this | ICalAlarmRelatesTo | null { if (relatesTo === undefined) { return this.data.relatesTo; } if (!relatesTo) { this.data.relatesTo = null; return this; } if (!Object.values(ICalAlarmRelatesTo).includes(relatesTo)) { throw new Error('`relatesTo` is not correct, must be either `START` or `END`!'); } this.data.relatesTo = relatesTo; return this; } /** * Get the trigger time for the alarm. Can either * be a date and time value ({@link ICalDateTimeValue}) or * a number, which will represent the seconds between * alarm and event start. The number is negative, if the * alarm is triggered before the event started. * * @since 0.2.1 */ triggerAfter (): number | ICalDateTimeValue; /** * Use this method to set the alarm time. Unlike `trigger`, this time * the alarm takes place after the event has started. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = cal.createAlarm(); * * alarm.trigger(600); // -> 10 minutes after event starts * ``` * * You can use any supported date object, see * [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) * for details about supported values and timezone handling. * * @since 0.2.1 */ triggerAfter (trigger: number | ICalDateTimeValue): this; triggerAfter (trigger?: number | ICalDateTimeValue): this | number | ICalDateTimeValue { if (trigger === undefined) { return this.data.trigger; } return this.trigger(typeof trigger === 'number' ? -1 * trigger : trigger); } /** * Get the trigger time for the alarm. Can either * be a date and time value ({@link ICalDateTimeValue}) or * a number, which will represent the seconds between * alarm and event start. The number is negative, if the * alarm is triggered after the event started. * * @since 0.2.1 * @see {@link trigger} * @see {@link triggerAfter} */ triggerBefore (trigger: number | ICalDateTimeValue): this; /** * Use this method to set the alarm time. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * const alarm = cal.createAlarm(); * * alarm.trigger(600); // -> 10 minutes before event starts * alarm.trigger(new Date()); // -> now * ``` * * You can use any supported date object, see * [readme](https://github.com/sebbo2002/ical-generator#-date-time--timezones) * for details about supported values and timezone handling. * * @since 0.2.1 * @see {@link trigger} * @see {@link triggerAfter} */ triggerBefore (): number | ICalDateTimeValue; triggerBefore (trigger?: number | ICalDateTimeValue): this | number | ICalDateTimeValue { if(trigger === undefined) { return this.trigger(); } return this.trigger(trigger); } /** * Get Alarm Repetitions * @since 0.2.1 */ repeat(): ICalAlarmRepeatData | null; /** * Set Alarm Repetitions. Use this to repeat the alarm. * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * * // repeat the alarm 4 times every 5 minutes… * cal.createAlarm({ * repeat: { * times: 4, * interval: 300 * } * }); * ``` * * @since 0.2.1 */ repeat(repeat: ICalAlarmRepeatData | null): this; repeat (repeat?: ICalAlarmRepeatData | null): this | ICalAlarmRepeatData | null { if (repeat === undefined) { return this.data.repeat; } if (!repeat) { this.data.repeat = null; return this; } if (typeof repeat !== 'object') { throw new Error('`repeat` is not correct, must be an object!'); } if (typeof repeat.times !== 'number' || !isFinite(repeat.times)) { throw new Error('`repeat.times` is not correct, must be numeric!'); } if (typeof repeat.interval !== 'number' || !isFinite(repeat.interval)) { throw new Error('`repeat.interval` is not correct, must be numeric!'); } this.data.repeat = repeat; return this; } /** * Get Attachment * @since 0.2.1 */ attach (): {uri: string, mime: string | null} | null; /** * Set Alarm attachment. Used to set the alarm sound * if alarm type is audio. Defaults to "Basso". * * ```javascript * const cal = ical(); * const event = cal.createEvent(); * * event.createAlarm({ * attach: 'https://example.com/notification.aud' * }); * * // OR * * event.createAlarm({ * attach: { * uri: 'https://example.com/notification.aud', * mime: 'audio/basic' * } * }); * ``` * * @since 0.2.1 */ attach (attachment: {uri: string, mime?: string | null} | string | null): this; attach (attachment?: {uri: string, mime?: string | null} | string | null): this | {uri: string, mime: string | null} | null { if (attachment === undefined) { return this.data.attach; } if (!attachment) { this.data.attach = null; return this; } let _attach = null; if (typeof attachment === 'string') { _attach = { uri: attachment, mime: null }; } else if (typeof attachment === 'object') { _attach = { uri: attachment.uri, mime: attachment.mime || null }; } else { throw new Error( '`attachment` needs to be a valid formed string or an object. See https://sebbo2002.github.io/' + 'ical-generator/develop/reference/classes/ICalAlarm.html#attach' ); } if (!_attach.uri) { throw new Error('`attach.uri` is empty!'); } this.data.attach = { uri: _attach.uri, mime: _attach.mime }; return this; } /** * Get the alarm description. Used to set the alarm message * if alarm type is `display`. If the alarm type is `email`, it's * used to set the email body. Defaults to the event's summary. * * @since 0.2.1 */ description (): string | null; /** * Set the alarm description. Used to set the alarm message * if alarm type is `display`. If the alarm type is `email`, it's * used to set the email body. Defaults to the event's summary. * * @since 0.2.1 */ description (description: string | null): this; description (description?: string | null): this | string | null { if (description === undefined) { return this.data.description; } if (!description) { this.data.description = null; return this; } this.data.description = description; return this; } /** * Get the alarm summary. Used to set the email subject * if alarm type is `email`. Defaults to the event's summary. * * @since 7.0.0 */ summary (): string | null; /** * Set the alarm summary. Used to set the email subject * if alarm type is display. Defaults to the event's summary. * * @since 0.2.1 */ summary (summary: string | null): this; summary (summary?: string | null): this | string | null { if (summary === undefined) { return this.data.summary; } if (!summary) { this.data.summary = null; return this; } this.data.summary = summary; return this; } /** * Creates a new {@link ICalAttendee} and returns it. Use options to prefill * the attendee's attributes. Calling this method without options will create * an empty attendee. * * @since 7.0.0 */ createAttendee(data: ICalAttendee | ICalAttendeeData | string): ICalAttendee { if (data instanceof ICalAttendee) { this.data.attendees.push(data); return data; } if (typeof data === 'string') { data = { email: data, ...checkNameAndMail('data', data) }; } const attendee = new ICalAttendee(data, this); this.data.attendees.push(attendee); return attendee; } /** * Get all attendees * @since 7.0.0 */ attendees(): ICalAttendee[]; /** * Add multiple attendees to your event * * @since 7.0.0 */ attendees(attendees: (ICalAttendee | ICalAttendeeData | string)[]): this; attendees(attendees?: (ICalAttendee | ICalAttendeeData | string)[]): this | ICalAttendee[] { if (!attendees) { return this.data.attendees; } attendees.forEach(attendee => this.createAttendee(attendee)); return this; } /** * Set X-* attributes. Woun't filter double attributes, * which are also added by another method (e.g. type), * so these attributes may be inserted twice. * * ```javascript * alarm.x([ * { * key: "X-MY-CUSTOM-ATTR", * value: "1337!" * } * ]); * * alarm.x([ * ["X-MY-CUSTOM-ATTR", "1337!"] * ]); * * alarm.x({ * "X-MY-CUSTOM-ATTR": "1337!" * }); * ``` * * @since 1.9.0 */ x (keyOrArray: {key: string, value: string}[] | [string, string][] | Record<string, string>): this; /** * Set a X-* attribute. Woun't filter double attributes, * which are also added by another method (e.g. type), * so these attributes may be inserted twice. * * ```javascript * alarm.x("X-MY-CUSTOM-ATTR", "1337!"); * ``` * * @since 1.9.0 */ x (keyOrArray: string, value: string): this; /** * Get all custom X-* attributes. * @since 1.9.0 */ x (): {key: string, value: string}[]; x (keyOrArray?: ({key: string, value: string})[] | [string, string][] | Record<string, string> | string, value?: string): this | void | ({key: string, value: string})[] { if(keyOrArray === undefined) { return addOrGetCustomAttributes (this.data); } if(typeof keyOrArray === 'string' && typeof value === 'string') { addOrGetCustomAttributes (this.data, keyOrArray, value); } else if(typeof keyOrArray === 'object') { addOrGetCustomAttributes (this.data, keyOrArray); } else { throw new Error('Either key or value is not a string!'); } return this; } /** * Return a shallow copy of the alarm's options for JSON stringification. * Third party objects like moment.js values are stringified as well. Can * be used for persistence. * * @since 0.2.4 */ toJSON (): ICalAlarmJSONData { const trigger = this.trigger(); return Object.assign({}, this.data, { trigger: typeof trigger === 'number' ? trigger : toJSON(trigger), x: this.x() }); } /** * Return generated event as a string. * * ```javascript * const alarm = event.createAlarm(); * console.log(alarm.toString()); // → BEGIN:VALARM… * ``` */ toString (): string { let g = 'BEGIN:VALARM\r\n'; // ACTION g += 'ACTION:' + this.data.type.toUpperCase() + '\r\n'; if (typeof this.data.trigger === 'number' && this.data.relatesTo === null) { if (this.data.trigger > 0) { g += 'TRIGGER;RELATED=END:' + toDurationString(this.data.trigger) + '\r\n'; } else { g += 'TRIGGER:' + toDurationString(this.data.trigger) + '\r\n'; } } else if (typeof this.data.trigger === 'number') { g += 'TRIGGER;RELATED=' + this.data.relatesTo?.toUpperCase() + ':' + toDurationString(this.data.trigger) + '\r\n'; } else { g += 'TRIGGER;VALUE=DATE-TIME:' + formatDate(this.event.timezone(), this.data.trigger) + '\r\n'; } // REPEAT if (this.data.repeat) { if (!this.data.repeat.times) { throw new Error('No value for `repeat.times` in ICalAlarm given, but required for `interval`!'); } if (!this.data.repeat.interval) { throw new Error('No value for `repeat.interval` in ICalAlarm given, but required for `repeat`!'); } g += 'REPEAT:' + this.data.repeat.times + '\r\n'; g += 'DURATION:' + toDurationString(this.data.repeat.interval) + '\r\n'; } // ATTACH if (this.data.type === 'audio' && this.data.attach && this.data.attach.mime) { g += 'ATTACH;FMTTYPE=' + escape(this.data.attach.mime, false) + ':' + escape(this.data.attach.uri, false) + '\r\n'; } else if (this.data.type === 'audio' && this.data.attach) { g += 'ATTACH;VALUE=URI:' + escape(this.data.attach.uri, false) + '\r\n'; } else if (this.data.type === 'audio') { g += 'ATTACH;VALUE=URI:Basso\r\n'; } // DESCRIPTION if (this.data.type !== 'audio' && this.data.description) { g += 'DESCRIPTION:' + escape(this.data.description, false) + '\r\n'; } else if (this.data.type !== 'audio') { g += 'DESCRIPTION:' + escape(this.event.summary(), false) + '\r\n'; } // SUMMARY if (this.data.type === 'email' && this.data.summary) { g += 'SUMMARY:' + escape(this.data.summary, false) + '\r\n'; } else if (this.data.type === 'email') { g += 'SUMMARY:' + escape(this.event.summary(), false) + '\r\n'; } // ATTENDEES if (this.data.type === 'email') { this.data.attendees.forEach(attendee => { g += attendee.toString(); }); } // CUSTOM X ATTRIBUTES g += generateCustomAttributes(this.data); g += 'END:VALARM\r\n'; return g; } }