1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/* import-globals-from calItemBase.js */
6
7var EXPORTED_SYMBOLS = ["CalTodo"];
8
9var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
10var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11
12Services.scriptloader.loadSubScript("resource:///components/calItemBase.js");
13
14/**
15 * Constructor for `calITodo` objects.
16 *
17 * @class
18 * @implements {calITodo}
19 * @param {string} [icalString] - Optional iCal string for initializing existing todos.
20 */
21function CalTodo(icalString) {
22  this.initItemBase();
23
24  this.todoPromotedProps = {
25    DTSTART: true,
26    DTEND: true,
27    DUE: true,
28    COMPLETED: true,
29    __proto__: this.itemBasePromotedProps,
30  };
31
32  if (icalString) {
33    this.icalString = icalString;
34  }
35
36  // Set a default percentComplete if the icalString didn't already set it.
37  if (!this.percentComplete) {
38    this.percentComplete = 0;
39  }
40}
41
42var calTodoClassID = Components.ID("{7af51168-6abe-4a31-984d-6f8a3989212d}");
43var calTodoInterfaces = [Ci.calIItemBase, Ci.calITodo, Ci.calIInternalShallowCopy];
44CalTodo.prototype = {
45  __proto__: calItemBase.prototype,
46
47  classID: calTodoClassID,
48  QueryInterface: cal.generateQI(["calIItemBase", "calITodo", "calIInternalShallowCopy"]),
49  classInfo: cal.generateCI({
50    classID: calTodoClassID,
51    contractID: "@mozilla.org/calendar/todo;1",
52    classDescription: "Calendar Todo",
53    interfaces: calTodoInterfaces,
54  }),
55
56  cloneShallow(aNewParent) {
57    let cloned = new CalTodo();
58    this.cloneItemBaseInto(cloned, aNewParent);
59    return cloned;
60  },
61
62  createProxy(aRecurrenceId) {
63    cal.ASSERT(!this.mIsProxy, "Tried to create a proxy for an existing proxy!", true);
64
65    let proxy = new CalTodo();
66
67    // override proxy's DTSTART/DUE/RECURRENCE-ID
68    // before master is set (and item might get immutable):
69    let duration = this.duration;
70    if (duration) {
71      let dueDate = aRecurrenceId.clone();
72      dueDate.addDuration(duration);
73      proxy.dueDate = dueDate;
74    }
75    proxy.entryDate = aRecurrenceId;
76
77    proxy.initializeProxy(this, aRecurrenceId);
78    proxy.mDirty = false;
79
80    return proxy;
81  },
82
83  makeImmutable() {
84    this.makeItemBaseImmutable();
85  },
86
87  isTodo() {
88    return true;
89  },
90
91  get isCompleted() {
92    return this.completedDate != null || this.percentComplete == 100 || this.status == "COMPLETED";
93  },
94
95  set isCompleted(completed) {
96    if (completed) {
97      if (!this.completedDate) {
98        this.completedDate = cal.dtz.jsDateToDateTime(new Date());
99      }
100      this.status = "COMPLETED";
101      this.percentComplete = 100;
102    } else {
103      this.deleteProperty("COMPLETED");
104      this.deleteProperty("STATUS");
105      this.deleteProperty("PERCENT-COMPLETE");
106    }
107  },
108
109  get duration() {
110    let dur = this.getProperty("DURATION");
111    // pick up duration if available, otherwise calculate difference
112    // between start and enddate
113    if (dur) {
114      return cal.createDuration(dur);
115    }
116    if (!this.entryDate || !this.dueDate) {
117      return null;
118    }
119    return this.dueDate.subtractDate(this.entryDate);
120  },
121
122  set duration(value) {
123    this.setProperty("DURATION", value);
124  },
125
126  get recurrenceStartDate() {
127    // DTSTART is optional for VTODOs, so it's unclear if RRULE is allowed then,
128    // so fallback to DUE if no DTSTART is present:
129    return this.entryDate || this.dueDate;
130  },
131
132  icsEventPropMap: [
133    { cal: "DTSTART", ics: "startTime" },
134    { cal: "DUE", ics: "dueTime" },
135    { cal: "COMPLETED", ics: "completedTime" },
136  ],
137
138  set icalString(value) {
139    this.icalComponent = cal.getIcsService().parseICS(value, null);
140  },
141
142  get icalString() {
143    let calcomp = cal.getIcsService().createIcalComponent("VCALENDAR");
144    cal.item.setStaticProps(calcomp);
145    calcomp.addSubcomponent(this.icalComponent);
146    return calcomp.serializeToICS();
147  },
148
149  get icalComponent() {
150    let icssvc = cal.getIcsService();
151    let icalcomp = icssvc.createIcalComponent("VTODO");
152    this.fillIcalComponentFromBase(icalcomp);
153    this.mapPropsToICS(icalcomp, this.icsEventPropMap);
154
155    for (let [name, value] of this.properties) {
156      try {
157        // When deleting a property of an occurrence, the property is not actually deleted
158        // but instead set to null, so we need to prevent adding those properties.
159        let wasReset = this.mIsProxy && value === null;
160        if (!this.todoPromotedProps[name] && !wasReset) {
161          let icalprop = icssvc.createIcalProperty(name);
162          icalprop.value = value;
163          let propBucket = this.mPropertyParams[name];
164          if (propBucket) {
165            for (let paramName in propBucket) {
166              try {
167                icalprop.setParameter(paramName, propBucket[paramName]);
168              } catch (e) {
169                if (e.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
170                  // Illegal values should be ignored, but we could log them if
171                  // the user has enabled logging.
172                  cal.LOG(
173                    "Warning: Invalid todo parameter value " +
174                      paramName +
175                      "=" +
176                      propBucket[paramName]
177                  );
178                } else {
179                  throw e;
180                }
181              }
182            }
183          }
184          icalcomp.addProperty(icalprop);
185        }
186      } catch (e) {
187        cal.ERROR("failed to set " + name + " to " + value + ": " + e + "\n");
188      }
189    }
190    return icalcomp;
191  },
192
193  todoPromotedProps: null,
194
195  set icalComponent(todo) {
196    this.modify();
197    if (todo.componentType != "VTODO") {
198      todo = todo.getFirstSubcomponent("VTODO");
199      if (!todo) {
200        throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
201      }
202    }
203
204    this.mDueDate = undefined;
205    this.setItemBaseFromICS(todo);
206    this.mapPropsFromICS(todo, this.icsEventPropMap);
207
208    this.importUnpromotedProperties(todo, this.todoPromotedProps);
209    // Importing didn't really change anything
210    this.mDirty = false;
211  },
212
213  isPropertyPromoted(name) {
214    // avoid strict undefined property warning
215    return this.todoPromotedProps[name] || false;
216  },
217
218  set entryDate(value) {
219    this.modify();
220
221    // We're about to change the start date of an item which probably
222    // could break the associated calIRecurrenceInfo. We're calling
223    // the appropriate method here to adjust the internal structure in
224    // order to free clients from worrying about such details.
225    if (this.parentItem == this) {
226      let rec = this.recurrenceInfo;
227      if (rec) {
228        rec.onStartDateChange(value, this.entryDate);
229      }
230    }
231
232    this.setProperty("DTSTART", value);
233  },
234
235  get entryDate() {
236    return this.getProperty("DTSTART");
237  },
238
239  mDueDate: undefined,
240  get dueDate() {
241    let dueDate = this.mDueDate;
242    if (dueDate === undefined) {
243      dueDate = this.getProperty("DUE");
244      if (!dueDate) {
245        let entryDate = this.entryDate;
246        let dur = this.getProperty("DURATION");
247        if (entryDate && dur) {
248          // If there is a duration set on the todo, calculate the right end time.
249          dueDate = entryDate.clone();
250          dueDate.addDuration(cal.createDuration(dur));
251        }
252      }
253      this.mDueDate = dueDate;
254    }
255    return dueDate;
256  },
257
258  set dueDate(value) {
259    this.deleteProperty("DURATION"); // setting dueDate once removes DURATION
260    this.setProperty("DUE", value);
261    this.mDueDate = value;
262  },
263};
264
265makeMemberAttrProperty(CalTodo, "COMPLETED", "completedDate");
266makeMemberAttrProperty(CalTodo, "PERCENT-COMPLETE", "percentComplete");
267