/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch */ const NAME_INDEX = 0; const PROP_INDEX = 1; const TYPE_INDEX = 2; const VALUE_INDEX = 3; import design from "./design.js"; import ICALStringify from "./stringify.js"; import ICALParse from "./parse.js"; // needed for typescript type resolution // eslint-disable-next-line no-unused-vars import Component from "./component.js"; // needed for typescript type resolution // eslint-disable-next-line no-unused-vars import Duration from "./duration.js"; // needed for typescript type resolution // eslint-disable-next-line no-unused-vars import UtcOffset from "./utc_offset.js"; // needed for typescript type resolution // eslint-disable-next-line no-unused-vars import Binary from "./binary.js"; // needed for typescript type resolution // eslint-disable-next-line no-unused-vars import Period from "./period.js"; // needed for typescript type resolution // eslint-disable-next-line no-unused-vars import Recur from "./recur.js"; // needed for typescript type resolution // eslint-disable-next-line no-unused-vars import Time from "./time.js"; /** * This lets typescript resolve our custom types in the * generated d.ts files (jsdoc typedefs are converted to typescript types). * Ignore prevents the typedefs from being documented more than once. * @ignore * @typedef {import("./types.js").designSet} designSet * Imports the 'designSet' type from the "types.js" module * @typedef {import("./types.js").Geo} Geo * Imports the 'Geo' type from the "types.js" module */ /** * Provides a layer on top of the raw jCal object for manipulating a single property, with its * parameters and value. * * @memberof ICAL */ class Property { /** * Create an {@link ICAL.Property} by parsing the passed iCalendar string. * * @param {String} str The iCalendar string to parse * @param {designSet=} designSet The design data to use for this property * @return {Property} The created iCalendar property */ static fromString(str, designSet) { return new Property(ICALParse.property(str, designSet)); } /** * Creates a new ICAL.Property instance. * * It is important to note that mutations done in the wrapper directly mutate the jCal object used * to initialize. * * Can also be used to create new properties by passing the name of the property (as a String). * * @param {Array|String} jCal Raw jCal representation OR the new name of the property * @param {Component=} parent Parent component */ constructor(jCal, parent) { this._parent = parent || null; if (typeof(jCal) === 'string') { // We are creating the property by name and need to detect the type this.jCal = [jCal, {}, design.defaultType]; this.jCal[TYPE_INDEX] = this.getDefaultType(); } else { this.jCal = jCal; } this._updateType(); } /** * The value type for this property * @type {String} */ get type() { return this.jCal[TYPE_INDEX]; } /** * The name of this property, in lowercase. * @type {String} */ get name() { return this.jCal[NAME_INDEX]; } /** * The parent component for this property. * @type {Component} */ get parent() { return this._parent; } set parent(p) { // Before setting the parent, check if the design set has changed. If it // has, we later need to update the type if it was unknown before. let designSetChanged = !this._parent || (p && p._designSet != this._parent._designSet); this._parent = p; if (this.type == design.defaultType && designSetChanged) { this.jCal[TYPE_INDEX] = this.getDefaultType(); this._updateType(); } } /** * The design set for this property, e.g. icalendar vs vcard * * @type {designSet} * @private */ get _designSet() { return this.parent ? this.parent._designSet : design.defaultSet; } /** * Updates the type metadata from the current jCal type and design set. * * @private */ _updateType() { let designSet = this._designSet; if (this.type in designSet.value) { if ('decorate' in designSet.value[this.type]) { this.isDecorated = true; } else { this.isDecorated = false; } if (this.name in designSet.property) { this.isMultiValue = ('multiValue' in designSet.property[this.name]); this.isStructuredValue = ('structuredValue' in designSet.property[this.name]); } } } /** * Hydrate a single value. The act of hydrating means turning the raw jCal * value into a potentially wrapped object, for example {@link ICAL.Time}. * * @private * @param {Number} index The index of the value to hydrate * @return {?Object} The decorated value. */ _hydrateValue(index) { if (this._values && this._values[index]) { return this._values[index]; } // for the case where there is no value. if (this.jCal.length <= (VALUE_INDEX + index)) { return null; } if (this.isDecorated) { if (!this._values) { this._values = []; } return (this._values[index] = this._decorate( this.jCal[VALUE_INDEX + index] )); } else { return this.jCal[VALUE_INDEX + index]; } } /** * Decorate a single value, returning its wrapped object. This is used by * the hydrate function to actually wrap the value. * * @private * @param {?} value The value to decorate * @return {Object} The decorated value */ _decorate(value) { return this._designSet.value[this.type].decorate(value, this); } /** * Undecorate a single value, returning its raw jCal data. * * @private * @param {Object} value The value to undecorate * @return {?} The undecorated value */ _undecorate(value) { return this._designSet.value[this.type].undecorate(value, this); } /** * Sets the value at the given index while also hydrating it. The passed * value can either be a decorated or undecorated value. * * @private * @param {?} value The value to set * @param {Number} index The index to set it at */ _setDecoratedValue(value, index) { if (!this._values) { this._values = []; } if (typeof(value) === 'object' && 'icaltype' in value) { // decorated value this.jCal[VALUE_INDEX + index] = this._undecorate(value); this._values[index] = value; } else { // undecorated value this.jCal[VALUE_INDEX + index] = value; this._values[index] = this._decorate(value); } } /** * Gets a parameter on the property. * * @param {String} name Parameter name (lowercase) * @return {Array|String} Parameter value */ getParameter(name) { if (name in this.jCal[PROP_INDEX]) { return this.jCal[PROP_INDEX][name]; } else { return undefined; } } /** * Gets first parameter on the property. * * @param {String} name Parameter name (lowercase) * @return {String} Parameter value */ getFirstParameter(name) { let parameters = this.getParameter(name); if (Array.isArray(parameters)) { return parameters[0]; } return parameters; } /** * Sets a parameter on the property. * * @param {String} name The parameter name * @param {Array|String} value The parameter value */ setParameter(name, value) { let lcname = name.toLowerCase(); if (typeof value === "string" && lcname in this._designSet.param && 'multiValue' in this._designSet.param[lcname]) { value = [value]; } this.jCal[PROP_INDEX][name] = value; } /** * Removes a parameter * * @param {String} name The parameter name */ removeParameter(name) { delete this.jCal[PROP_INDEX][name]; } /** * Get the default type based on this property's name. * * @return {String} The default type for this property */ getDefaultType() { let name = this.jCal[NAME_INDEX]; let designSet = this._designSet; if (name in designSet.property) { let details = designSet.property[name]; if ('defaultType' in details) { return details.defaultType; } } return design.defaultType; } /** * Sets type of property and clears out any existing values of the current * type. * * @param {String} type New iCAL type (see design.*.values) */ resetType(type) { this.removeAllValues(); this.jCal[TYPE_INDEX] = type; this._updateType(); } /** * Finds the first property value. * * @return {Binary | Duration | Period | * Recur | Time | UtcOffset | Geo | string | null} First property value */ getFirstValue() { return this._hydrateValue(0); } /** * Gets all values on the property. * * NOTE: this creates an array during each call. * * @return {Array} List of values */ getValues() { let len = this.jCal.length - VALUE_INDEX; if (len < 1) { // it is possible for a property to have no value. return []; } let i = 0; let result = []; for (; i < len; i++) { result[i] = this._hydrateValue(i); } return result; } /** * Removes all values from this property */ removeAllValues() { if (this._values) { this._values.length = 0; } this.jCal.length = 3; } /** * Sets the values of the property. Will overwrite the existing values. * This can only be used for multi-value properties. * * @param {Array} values An array of values */ setValues(values) { if (!this.isMultiValue) { throw new Error( this.name + ': does not not support mulitValue.\n' + 'override isMultiValue' ); } let len = values.length; let i = 0; this.removeAllValues(); if (len > 0 && typeof(values[0]) === 'object' && 'icaltype' in values[0]) { this.resetType(values[0].icaltype); } if (this.isDecorated) { for (; i < len; i++) { this._setDecoratedValue(values[i], i); } } else { for (; i < len; i++) { this.jCal[VALUE_INDEX + i] = values[i]; } } } /** * Sets the current value of the property. If this is a multi-value * property, all other values will be removed. * * @param {String|Object} value New property value. */ setValue(value) { this.removeAllValues(); if (typeof(value) === 'object' && 'icaltype' in value) { this.resetType(value.icaltype); } if (this.isDecorated) { this._setDecoratedValue(value, 0); } else { this.jCal[VALUE_INDEX] = value; } } /** * Returns the Object representation of this component. The returned object * is a live jCal object and should be cloned if modified. * @return {Object} */ toJSON() { return this.jCal; } /** * The string representation of this component. * @return {String} */ toICALString() { return ICALStringify.property( this.jCal, this._designSet, true ); } } export default Property;