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