diff options
Diffstat (limited to 'comm/calendar/base/content/calendar-dnd-listener.js')
-rw-r--r-- | comm/calendar/base/content/calendar-dnd-listener.js | 922 |
1 files changed, 922 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-dnd-listener.js b/comm/calendar/base/content/calendar-dnd-listener.js new file mode 100644 index 0000000000..994d305c95 --- /dev/null +++ b/comm/calendar/base/content/calendar-dnd-listener.js @@ -0,0 +1,922 @@ +/* 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/. */ + +/* globals getSelectedCalendar, MODE_RDONLY, startBatchTransaction, doTransaction, + endBatchTransaction, createEventWithDialog, createTodoWithDialog */ + +/* exported invokeEventDragSession, + * calendarMailButtonDNDObserver, calendarCalendarButtonDNDObserver, + * calendarTaskButtonDNDObserver + */ + +var calendarViewDNDObserver; +var calendarMailButtonDNDObserver; +var calendarCalendarButtonDNDObserver; +var calendarTaskButtonDNDObserver; + +// Wrap in a block to prevent leaking to window scope. +{ + var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + var { AppConstants } = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs"); + var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); + var { XPCOMUtils } = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs"); + + XPCOMUtils.defineLazyModuleGetters(this, { + CalAttachment: "resource:///modules/CalAttachment.jsm", + CalAttendee: "resource:///modules/CalAttendee.jsm", + CalEvent: "resource:///modules/CalEvent.jsm", + CalTodo: "resource:///modules/CalTodo.jsm", + }); + + var itemConversion = { + /** + * Converts an email message to a calendar item. + * + * @param {calIItemBase} item - The target calIItemBase. + * @param {nsIMsgDBHdr} message - The nsIMsgDBHdr to convert from. + */ + async calendarItemFromMessage(item, message) { + let folder = message.folder; + let msgUri = folder.getUriForMsg(message); + + item.calendar = getSelectedCalendar(); + item.title = message.mime2DecodedSubject; + item.setProperty("URL", `mid:${message.messageId}`); + + cal.dtz.setDefaultStartEndHour(item); + cal.alarms.setDefaultValues(item); + + let content = ""; + await new Promise((resolve, reject) => { + let streamListener = { + QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), + onDataAvailable(request, inputStream, offset, count) { + let text = folder.getMsgTextFromStream( + inputStream, + message.charset, + count, // bytesToRead + 32768, // maxOutputLen + false, // compressQuotes + true, // stripHTMLTags + {} // out contentType + ); + // If we ever got text, we're good. Ignore further chunks. + content ||= text; + }, + onStartRequest(request) {}, + onStopRequest(request, statusCode) { + if (!Components.isSuccessCode(statusCode)) { + reject(new Error(statusCode)); + } + resolve(); + }, + }; + MailServices.messageServiceFromURI(msgUri).streamMessage( + msgUri, + streamListener, + null, + null, + false, + "", + false + ); + }); + item.setProperty("DESCRIPTION", content); + }, + + /** + * Copy base item properties from aItem to aTarget. This includes properties + * like title, location, description, priority, transparency, attendees, + * categories, calendar, recurrence and possibly more. + * + * @param {object} aItem - The item to copy from. + * @param {object} aTarget - The item to copy to. + */ + copyItemBase(aItem, aTarget) { + const copyProps = ["SUMMARY", "LOCATION", "DESCRIPTION", "URL", "CLASS", "PRIORITY"]; + + for (let prop of copyProps) { + aTarget.setProperty(prop, aItem.getProperty(prop)); + } + + // Attendees + let attendees = aItem.getAttendees(); + for (let attendee of attendees) { + aTarget.addAttendee(attendee.clone()); + } + + // Categories + let categories = aItem.getCategories(); + aTarget.setCategories(categories); + + // Organizer + aTarget.organizer = aItem.organizer ? aItem.organizer.clone() : null; + + // Calendar + aTarget.calendar = getSelectedCalendar(); + + // Recurrence + if (aItem.recurrenceInfo) { + aTarget.recurrenceInfo = aItem.recurrenceInfo.clone(); + aTarget.recurrenceInfo.item = aTarget; + } + }, + + /** + * Creates a task from the passed event. This function copies the base item + * and a few event specific properties (dates, alarms, ...). + * + * @param {object} aEvent - The event to copy from. + * @returns {object} The resulting task. + */ + taskFromEvent(aEvent) { + let item = new CalTodo(); + + this.copyItemBase(aEvent, item); + + // Dates and alarms + if (!aEvent.startDate.isDate && !aEvent.endDate.isDate) { + // Dates + item.entryDate = aEvent.startDate.clone(); + item.dueDate = aEvent.endDate.clone(); + + // Alarms + for (let alarm of aEvent.getAlarms()) { + item.addAlarm(alarm.clone()); + } + item.alarmLastAck = aEvent.alarmLastAck ? aEvent.alarmLastAck.clone() : null; + } + + // Map Status values + let statusMap = { + TENTATIVE: "NEEDS-ACTION", + CONFIRMED: "IN-PROCESS", + CANCELLED: "CANCELLED", + }; + if (aEvent.getProperty("STATUS") in statusMap) { + item.setProperty("STATUS", statusMap[aEvent.getProperty("STATUS")]); + } + return item; + }, + + /** + * Creates an event from the passed task. This function copies the base item + * and a few task specific properties (dates, alarms, ...). If the task has + * no due date, the default event length is used. + * + * @param {object} aTask - The task to copy from. + * @returns {object} The resulting event. + */ + eventFromTask(aTask) { + let item = new CalEvent(); + + this.copyItemBase(aTask, item); + + // Dates and alarms + item.startDate = aTask.entryDate; + if (!item.startDate) { + if (aTask.dueDate) { + item.startDate = aTask.dueDate.clone(); + item.startDate.minute -= Services.prefs.getIntPref("calendar.event.defaultlength", 60); + } else { + item.startDate = cal.dtz.getDefaultStartDate(); + } + } + + item.endDate = aTask.dueDate; + if (!item.endDate) { + // Make the event be the default event length if no due date was + // specified. + item.endDate = item.startDate.clone(); + item.endDate.minute += Services.prefs.getIntPref("calendar.event.defaultlength", 60); + } + + // Alarms + for (let alarm of aTask.getAlarms()) { + item.addAlarm(alarm.clone()); + } + item.alarmLastAck = aTask.alarmLastAck ? aTask.alarmLastAck.clone() : null; + + // Map Status values + let statusMap = { + "NEEDS-ACTION": "TENTATIVE", + COMPLETED: "CONFIRMED", + "IN-PROCESS": "CONFIRMED", + CANCELLED: "CANCELLED", + }; + if (aTask.getProperty("STATUS") in statusMap) { + item.setProperty("STATUS", statusMap[aTask.getProperty("STATUS")]); + } + return item; + }, + }; + + /** + * CalDNDTransferHandler provides a base class for handling drag and drop data + * transfers based on detected mime types. Actual processing of the dropped + * data is left up to CalDNDListener however children of this class mostly + * do some preprocessing first. + * + * The main methods here are the handleDataTransferItem() and handleString() + * methods that initiate transfer from a DataTransferItem or string + * respectively. Whether the data is passed as a DataTransferItem or string + * mostly depends on whether dropped from an external application or + * internally. + * + * @abstract + */ + class CalDNDTransferHandler { + /** + * List of mime types this class handles (Overridden by child class). + * + * @type {string[]} + */ + mimeTypes = []; + + /** + * @param {CalDNDListener} listener - The listener that received the + * original drop event. Most CalDNDTransferHandlers will invoke a method on + * this class once data has been processed. + */ + constructor(listener) { + this.listener = listener; + } + + /** + * Returns true if the handler is able to process any of the given mime types. + * + * @param {string|string[]} mime - The mime type to handle. + * + * @returns {boolean} + */ + willTransfer(mime) { + return Array.isArray(mime) + ? this.mimeTypes.find(type => mime.includes(type)) + : this.mimeTypes.includes(mime); + } + + /** + * Selects the most appropriate type from a list to use with mozGetDataAt(). + * + * @param {string[]} types + * + * @returns {string?} + */ + getMozType(types) { + return types.find(type => this.mimeTypes.includes(type)); + } + + /** + * Overridden by child classes that handle DataTransferItems. By default, no + * processing is done. + * + * @param {DataTransferItem} item + */ + async handleDataTransferItem(item) {} + + /** + * Overridden by child classes that handle string data. By default, no + * processing is done. + * + * @param {string} data + */ + async handleString() {} + } + + /** + * CalDNDMozMessageTransferHandler handles messages dropped from the + * message pane. + */ + class CalDNDMozMessageTransferHandler extends CalDNDTransferHandler { + mimeTypes = ["text/x-moz-message"]; + + /** + * Treats the provided data as a message uri. Invokes the listener's + * onMessageDrop() method with the corresponding message header. + * + * @param {string} data + */ + async handleString(data) { + let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); + this.listener.onDropMessage(messenger.msgHdrFromURI(data)); + } + } + + /** + * CalDNDAddressTransferHandler handles address book data internally dropped. + */ + class CalDNDAddressTransferHandler extends CalDNDTransferHandler { + mimeTypes = ["text/x-moz-address"]; + + /** + * Invokes the listener's onDropAddress() method. + * + * @param {string} data + */ + async handleString(data) { + this.listener.onDropAddress(data); + } + } + + /** + * CalDNDDefaultTransferHandler serves as a "catch all" and should be included + * last in the list of handlers. + */ + class CalDNDDefaultTransferHandler extends CalDNDTransferHandler { + willTransfer() { + return true; + } + + /** + * If the dropped item is a file, it is treated as an event attachment, + * otherwise it is ignored. + * + * @param {DataTransferItem} item + */ + async handleDataTransferItem(item) { + if (item.kind == "file") { + let path = item.getAsFile().mozFullPath; + if (path) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(path); + + let uri = Services.io.newFileURI(file); + this.listener.onDropURL(uri); + } + } + } + } + + /** + * CalDNDDirectTransferHandler provides a base class for CalDNDTransferHandlers + * that directly extract the contents of a DataTransferItem for processing. + * + * @abstract + */ + class CalDNDDirectTransferHandler extends CalDNDTransferHandler { + /** + * Extracts the raw string data from a DataTransferItem before passing to + * handleString(). + * + * @param {DataTransferItem} item + */ + async handleDataTransferItem(item) { + if (item.kind == "string") { + let txt = await new Promise(resolve => item.getAsString(resolve)); + await this.handleString(txt); + } else if (item.kind == "file") { + let txt = await item.getAsFile().text(); + await this.handleString(txt); + } + } + } + + /** + * CalDNDICSTransferHandler handles internal or external data in ICS format. + */ + class CalDNDICSTransferHandler extends CalDNDDirectTransferHandler { + mimeTypes = ["text/calendar", "application/x-extension-ics"]; + + /** + * Parses the provided data as an ICS string before invoking the listener's + * onDropItems() method. + * + * @param {string} data + */ + async handleString(data) { + if (AppConstants.platform == "macosx") { + // Mac likes to convert all \r to \n, we need to reverse this. + data = data.replace(/\n\n/g, "\r\n"); + } + + let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser); + parser.parseString(data); + this.listener.onDropItems(parser.getItems().concat(parser.getParentlessItems())); + } + } + + /** + * CalDNDURLTransferHandler handles urls (dropped internally or externally). + */ + class CalDNDURLTransferHandler extends CalDNDDirectTransferHandler { + mimeTypes = ["text/uri-list", "text/x-moz-url"]; + + _icsFilename = /filename=.*\.ics/; + + /** + * Treats the provided data as a url. If we determine it is a url to an + * ICS file, we delegate to the "text/calendar" handler. The listener's + * onDropURL method is invoked otherwise. + * + * @param {string} data + */ + async handleString(data) { + data = data.split("\n")[0]; + if (!data) { + return; + } + + let uri = Services.io.newURI(data); + + // Below we attempt to detect ics files dropped from the message pane's + // attachment list. These will appear as uris rather than file blobs so we + // check the "filename" query parameter for a .ics extension. + if (this._icsFilename.test(uri.query)) { + let url = uri.mutate().setUsername("").setUserPass("").finalize().spec; + + let resp = await fetch(new Request(url, { method: "GET" })); + let txt = await resp.text(); + await this.listener.getHandler("text/calendar").handleString(txt); + } else { + this.listener.onDropURL(uri); + } + } + } + + /** + * CalDNDPlainTextTransferHandler handles text/plain transfers coming mainly + * from internally dropped text. + */ + class CalDNDPlainTextTransferHandler extends CalDNDDirectTransferHandler { + mimeTypes = ["text/plain"]; + + _keyWords = ["VEVENT", "VTODO", "VCALENDAR"]; + + _isICS(data) { + return this._keyWords.some(kwrd => data.includes(kwrd)); + } + + /** + * Treats the data provided as an uri to an .ics file and attempts to parse + * its contents. If we detect calendar data however, we delegate to the + * "text/calendar" handler. + * + * @param {string} data + */ + async handleString(data) { + if (this._isICS(data)) { + this.listener.getHandler("text/calendar").handleString(data); + return; + } + + let droppedUrl = data.split("\n")[0]; + if (!droppedUrl) { + return; + } + + let url = Services.io.newURI(droppedUrl); + + let localFileInstance = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + localFileInstance.initWithPath(url.pathQueryRef); + + let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + inputStream.init(localFileInstance, MODE_RDONLY, parseInt("0444", 8), {}); + + try { + let importer = Cc["@mozilla.org/calendar/import;1?type=ics"].getService(Ci.calIImporter); + let items = importer.importFromStream(inputStream); + this.onDropItems(items); + } finally { + inputStream.close(); + } + } + } + + /** + * This is the base class for calendar drag and drop listeners. + */ + class CalDNDListener { + /** + * Limits the number of items to process from a drop operation. In the + * future, this could be removed in favour of better UI for bulk operations. + * + * @type {number} + */ + maxItemsTransferred = 8; + + /** + * A list of CalDNDTransferHandlers for all of the supported mime types. + * The order of this list is important as it dictates which types will be + * selected first. + * + * @type {CalDNDTransferHandler[]} + */ + mimeHandlers = [ + new CalDNDICSTransferHandler(this), + new CalDNDMozMessageTransferHandler(this), + new CalDNDAddressTransferHandler(this), + new CalDNDURLTransferHandler(this), + new CalDNDPlainTextTransferHandler(this), + new CalDNDDefaultTransferHandler(this), + ]; + + /** + * Provides the most suitable handler for the type or one of the types of a + * list. + * + * @param {string|string[]} mime + * + * @returns {CalDNDTransferHandler} + */ + getHandler(mime) { + return this.mimeHandlers.find(handler => handler.willTransfer(mime)); + } + + /** + * Prevents the browser's default behaviour when an item is dragged over the + * drop target. + * + * @param {Event} event + */ + onDragOver(event) { + event.preventDefault(); + } + + /** + * Handles calendar event items. + * + * @param {calIItemBase[]} items + */ + onDropItems() {} + + /** + * Handles mail messages. + * + * @param {nsIMsgHdr} msgHdr + */ + onDropMessage() {} + + /** + * Handles address book data. + */ + onDropAddress() {} + + /** + * Handles the drop event. The items property of DataTransfer can be + * interpreted differently depending on whether the drop is coming from an + * internal or external source (really its up to whatever is sending the + * data to decide what the transfer entails). + * + * Mozilla seems to treat it as alternative formats for the data being + * sent while external/other applications may only have one data transfer + * item per single thing dropped. The item's interface seems to have + * more accurate mime types than the ones of mozTypesAt() so working with + * those are preferable however not always possible. + * + * This method tries to determine which of the APIs is more appropriate for + * processing the drop. It does that by checking for a source node or a + * difference between length of DataTransfer.items and DataTransfer + * .mozItemCount. + * + * Note: While testing, it was noticed that dragging text from an external + * application shows up erroneously as a file in DataTransfer.items. This is + * dealt with too. + * + * @param {Event} event + */ + async onDrop(event) { + let { dataTransfer } = event; + + // No mozSourceNode means it's an external drop, however if the drop is + // coming from Firefox then we can expect the same behaviour as done + // internally. Generally there may be more DataTransferItems than + // mozItemCount indicates. + let isInternal = + dataTransfer.mozSourceNode || dataTransfer.items.length != dataTransfer.mozItemCount; + + // For the strange case of copied text having the "file" kind, the files + // property will have a length of zero. + let actualFiles = Array.from(dataTransfer.items).filter(i => i.kind == "file").length; + let isExternalText = actualFiles != dataTransfer.files.length; + + if (isInternal || isExternalText) { + await this.onInternalDrop(dataTransfer); + } else { + await this.onExternalDrop(dataTransfer); + } + } + + /** + * This method is intended for use when the drop event originates internally. + * + * @param {DataTransfer} dataTransfer + */ + async onInternalDrop(dataTransfer) { + for (let i = 0; i < dataTransfer.mozItemCount; i++) { + if (i == this.maxItemsTransferred) { + break; + } + + let types = Array.from(dataTransfer.mozTypesAt(i)); + let handler = this.getHandler(types); + let data = dataTransfer.mozGetDataAt(handler.getMozType(types), i); + + if (typeof data == "string") { + await handler.handleString(data); + } + } + } + + /** + * This method is intended for use when the drop event originates externally. + * + * @param {DataTransfer} dataTransfer + */ + async onExternalDrop(dataTransfer) { + let i = 0; + for (let item of dataTransfer.items) { + if (i == this.maxItemsTransferred) { + break; + } + + let handler = this.getHandler(item.type); + await handler.handleDataTransferItem(item, i, dataTransfer); + i++; + } + } + } + + /** + * Drag'n'drop handler for the calendar views. + */ + class CalViewDNDObserver extends CalDNDListener { + wrappedJSObject = this; + + /** + * Gets called in case we're dropping an array of items on one of the + * calendar views. In this case we just try to add these items to the + * currently selected calendar. + * + * @param {calIItemBase[]} items + */ + onDropItems(items) { + let destCal = getSelectedCalendar(); + startBatchTransaction(); + // we fall back explicitly to the popup to ask whether to send a + // notification to participants if required + let extResp = { responseMode: Ci.calIItipItem.USER }; + try { + for (let item of items) { + doTransaction("add", item, destCal, null, null, extResp); + } + } finally { + endBatchTransaction(); + } + } + } + + /** + * Drag'n'drop handler for the 'mail mode'-button. This handler is derived + * from the base handler and just implements specific actions. + */ + class CalMailButtonDNDObserver extends CalDNDListener { + wrappedJSObject = this; + + /** + * Gets called in case we're dropping an array of items on the + * 'mail mode'-button. + * + * @param {calIItemBase[]} items + */ + onDropItems(items) { + if (items && items.length > 0) { + let item = items[0]; + let identity = item.calendar.getProperty("imip.identity"); + let parties = item.getAttendees(); + if (item.organizer) { + parties.push(item.organizer); + } + if (identity) { + // if no identity is defined, the composer will fall back to + // whatever seems suitable - in this case we don't try to remove + // the sender from the recipient list + identity = identity.QueryInterface(Ci.nsIMsgIdentity); + parties = parties.filter(aParty => { + return identity.email != cal.email.getAttendeeEmail(aParty, false); + }); + } + let recipients = cal.email.createRecipientList(parties); + cal.email.sendTo(recipients, item.title, item.getProperty("DESCRIPTION"), identity); + } + } + } + + /** + * Drag'n'drop handler for the 'open calendar tab'-button. This handler is + * derived from the base handler and just implements specific actions. + */ + class CalCalendarButtonObserver extends CalDNDListener { + wrappedJSObject = this; + + /** + * Gets called in case we're dropping an array of items + * on the 'open calendar tab'-button. + * + * @param {calIItemBase[]} items + */ + onDropItems(items) { + for (let item of items) { + let newItem = item; + if (item.isTodo()) { + newItem = itemConversion.eventFromTask(item); + } + createEventWithDialog(null, null, null, null, newItem); + } + } + + /** + * Gets called in case we're dropping a message on the 'open calendar tab'- + * button. In this case we create a new event from the mail. We open the + * default event dialog and just use the subject of the message as the event + * title. + * + * @param {nsIMsgHdr} msgHdr + */ + async onDropMessage(msgHdr) { + let newItem = new CalEvent(); + await itemConversion.calendarItemFromMessage(newItem, msgHdr); + createEventWithDialog(null, null, null, null, newItem); + } + + /** + * Gets called in case we're dropping a uri on the 'open calendar tab'- + * button. + * + * @param {nsIURI} uri + */ + onDropURL(uri) { + let newItem = new CalEvent(); + newItem.calendar = getSelectedCalendar(); + cal.dtz.setDefaultStartEndHour(newItem); + cal.alarms.setDefaultValues(newItem); + let attachment = new CalAttachment(); + attachment.uri = uri; + newItem.addAttachment(attachment); + createEventWithDialog(null, null, null, null, newItem); + } + + /** + * Gets called in case we're dropping addresses on the 'open calendar tab' + * -button. + * + * @param {string} addresses + */ + onDropAddress(addresses) { + let parsedInput = MailServices.headerParser.makeFromDisplayAddress(addresses); + let attendee = new CalAttendee(); + attendee.id = ""; + attendee.rsvp = "TRUE"; + attendee.role = "REQ-PARTICIPANT"; + attendee.participationStatus = "NEEDS-ACTION"; + let attendees = parsedInput + .filter(address => address.name.length > 0) + .map((address, index) => { + // Convert address to attendee. + if (index > 0) { + attendee = attendee.clone(); + } + attendee.id = cal.email.prependMailTo(address.email); + let commonName = null; + if (address.name.length > 0) { + // We remove any double quotes within CN due to bug 1209399. + let name = address.name.replace(/(?:(?:[\\]")|(?:"))/g, ""); + if (address.email != name) { + commonName = name; + } + } + attendee.commonName = commonName; + return attendee; + }); + let newItem = new CalEvent(); + newItem.calendar = getSelectedCalendar(); + cal.dtz.setDefaultStartEndHour(newItem); + cal.alarms.setDefaultValues(newItem); + for (let attendee of attendees) { + newItem.addAttendee(attendee); + } + createEventWithDialog(null, null, null, null, newItem); + } + } + + /** + * Drag'n'drop handler for the 'open tasks tab'-button. This handler is + * derived from the base handler and just implements specific actions. + */ + class CalTaskButtonObserver extends CalDNDListener { + wrappedJSObject = this; + + /** + * Gets called in case we're dropping an array of items on the + * 'open tasks tab'-button. + * + * @param {object} items - An array of items to handle. + */ + onDropItems(items) { + for (let item of items) { + let newItem = item; + if (item.isEvent()) { + newItem = itemConversion.taskFromEvent(item); + } + createTodoWithDialog(null, null, null, newItem); + } + } + + /** + * Gets called in case we're dropping a message on the 'open tasks tab' + * -button. + * + * @param {nsIMsgHdr} msgHdr + */ + async onDropMessage(msgHdr) { + let todo = new CalTodo(); + await itemConversion.calendarItemFromMessage(todo, msgHdr); + createTodoWithDialog(null, null, null, todo); + } + + /** + * Gets called in case we're dropping a uri on the 'open tasks tab'-button. + * + * @param {nsIURI} uri + */ + onDropURL(uri) { + let todo = new CalTodo(); + todo.calendar = getSelectedCalendar(); + cal.dtz.setDefaultStartEndHour(todo); + cal.alarms.setDefaultValues(todo); + let attachment = new CalAttachment(); + attachment.uri = uri; + todo.addAttachment(attachment); + createTodoWithDialog(null, null, null, todo); + } + } + + calendarViewDNDObserver = new CalViewDNDObserver(); + calendarMailButtonDNDObserver = new CalMailButtonDNDObserver(); + calendarCalendarButtonDNDObserver = new CalCalendarButtonObserver(); + calendarTaskButtonDNDObserver = new CalTaskButtonObserver(); +} + +/** + * Invoke a drag session for the passed item. The passed box will be used as a + * source. + * + * @param {object} aItem - The item to drag. + * @param {object} aXULBox - The XUL box to invoke the drag session from. + */ +function invokeEventDragSession(aItem, aXULBox) { + let transfer = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + transfer.init(null); + transfer.addDataFlavor("text/calendar"); + + let flavourProvider = { + QueryInterface: ChromeUtils.generateQI(["nsIFlavorDataProvider"]), + + item: aItem, + getFlavorData(aInTransferable, aInFlavor, aOutData) { + if ( + aInFlavor == "application/vnd.x-moz-cal-event" || + aInFlavor == "application/vnd.x-moz-cal-task" + ) { + aOutData.value = aItem; + } else { + cal.ASSERT(false, "error:" + aInFlavor); + } + }, + }; + + if (aItem.isEvent()) { + transfer.addDataFlavor("application/vnd.x-moz-cal-event"); + transfer.setTransferData("application/vnd.x-moz-cal-event", flavourProvider); + } else if (aItem.isTodo()) { + transfer.addDataFlavor("application/vnd.x-moz-cal-task"); + transfer.setTransferData("application/vnd.x-moz-cal-task", flavourProvider); + } + + // Also set some normal data-types, in case we drag into another app + let serializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance( + Ci.calIIcsSerializer + ); + serializer.addItems([aItem]); + + let supportsString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + supportsString.data = serializer.serializeToString(); + transfer.setTransferData("text/calendar", supportsString); + transfer.setTransferData("text/plain", supportsString); + + let action = Ci.nsIDragService.DRAGDROP_ACTION_MOVE; + let mutArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + mutArray.appendElement(transfer); + aXULBox.sourceObject = aItem; + try { + cal.dragService.invokeDragSession(aXULBox, null, null, null, mutArray, action); + } catch (e) { + if (e.result != Cr.NS_ERROR_FAILURE) { + // Pressing Escape on some platforms results in NS_ERROR_FAILURE + // being thrown. Catch this exception, but throw anything else. + throw e; + } + } +} |