diff options
Diffstat (limited to 'comm/calendar/base/content/calendar-clipboard.js')
-rw-r--r-- | comm/calendar/base/content/calendar-clipboard.js | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/comm/calendar/base/content/calendar-clipboard.js b/comm/calendar/base/content/calendar-clipboard.js new file mode 100644 index 0000000000..d3a755d167 --- /dev/null +++ b/comm/calendar/base/content/calendar-clipboard.js @@ -0,0 +1,306 @@ +/* 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, getSelectedItems, promptOccurrenceModification, + calendarViewController, currentView, startBatchTransaction, doTransaction, + endBatchTransaction */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +/* exported cutToClipboard, pasteFromClipboard */ + +/** + * Test if a writable calendar is selected, and if the clipboard has items that + * can be pasted into Calendar. The data must be of type "text/calendar" or + * "text/plain". + * + * @returns If true, pasting is currently possible. + */ +function canPaste() { + if (Services.prefs.getBoolPref("calendar.paste.intoSelectedCalendar", false)) { + let selectedCal = getSelectedCalendar(); + if ( + !selectedCal || + !cal.acl.isCalendarWritable(selectedCal) || + !cal.acl.userCanAddItemsToCalendar(selectedCal) + ) { + return false; + } + } else { + let calendars = cal.manager + .getCalendars() + .filter(cal.acl.isCalendarWritable) + .filter(cal.acl.userCanAddItemsToCalendar); + if (!calendars.length) { + return false; + } + } + + const flavors = ["text/calendar", "text/plain"]; + return Services.clipboard.hasDataMatchingFlavors(flavors, Ci.nsIClipboard.kGlobalClipboard); +} + +/** + * Copy the ics data of the current view's selected events to the clipboard and + * deletes the events on success + * + * @param aCalendarItemArray (optional) an array of items to cut. If not + * passed, the current view's selected items will + * be used. + */ +function cutToClipboard(aCalendarItemArray = null) { + copyToClipboard(aCalendarItemArray, true); +} + +/** + * Copy the ics data of the items in calendarItemArray to the clipboard. Fills + * both text/unicode and text/calendar mime types. + * + * @param aCalendarItemArray (optional) an array of items to copy. If not + * passed, the current view's selected items will + * be used. + * @param aCutMode (optional) set to true, if this is a cut operation + */ +function copyToClipboard(aCalendarItemArray = null, aCutMode = false) { + let calendarItemArray = aCalendarItemArray || getSelectedItems(); + if (!calendarItemArray.length) { + cal.LOG("[calendar-clipboard] No items selected."); + return; + } + if (aCutMode) { + let items = calendarItemArray.filter( + aItem => + cal.acl.userCanModifyItem(aItem) || + (aItem.calendar && cal.acl.userCanDeleteItemsFromCalendar(aItem.calendar)) + ); + if (items.length < calendarItemArray.length) { + cal.LOG("[calendar-clipboard] No privilege to delete some or all selected items."); + return; + } + calendarItemArray = items; + } + let [targetItems, , response] = promptOccurrenceModification( + calendarItemArray, + true, + aCutMode ? "cut" : "copy" + ); + if (!response) { + // The user canceled the dialog, bail out + return; + } + + let icsSerializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance( + Ci.calIIcsSerializer + ); + icsSerializer.addItems(targetItems); + let icsString = icsSerializer.serializeToString(); + + let clipboard = Services.clipboard; + let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + + if (trans && clipboard) { + // Register supported data flavors + trans.init(null); + trans.addDataFlavor("text/calendar"); + trans.addDataFlavor("text/plain"); + + // Create the data objects + let icsWrapper = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + icsWrapper.data = icsString; + + // Add data objects to transferable + // Both Outlook 2000 client and Lotus Organizer use text/unicode + // when pasting iCalendar data. + trans.setTransferData("text/calendar", icsWrapper); + trans.setTransferData("text/plain", icsWrapper); + + clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard); + if (aCutMode) { + // check for MODIFICATION_PARENT + let useParent = response == 3; + calendarViewController.deleteOccurrences(targetItems, useParent, true); + } + } +} + +/** + * Reads ics data from the clipboard, parses it into items and inserts the items + * into the currently selected calendar. + */ +function pasteFromClipboard() { + if (!canPaste()) { + return; + } + + let clipboard = Services.clipboard; + let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + + if (!trans || !clipboard) { + return; + } + + // Register the wanted data flavors (highest fidelity first!) + trans.init(null); + trans.addDataFlavor("text/calendar"); + trans.addDataFlavor("text/plain"); + + // Get transferable from clipboard + clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard); + + // Ask transferable for the best flavor. + let flavor = {}; + let data = {}; + trans.getAnyTransferData(flavor, data); + data = data.value.QueryInterface(Ci.nsISupportsString).data; + switch (flavor.value) { + case "text/calendar": + case "text/plain": { + let icsParser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser); + try { + icsParser.parseString(data); + } catch (e) { + // Ignore parser errors from the clipboard data, if it fails + // there will just be 0 items. + } + + let items = icsParser.getItems(); + if (items.length == 0) { + return; + } + + // If there are multiple items on the clipboard, the earliest + // should be set to the selected day and the rest adjusted. + let earliestDate = null; + for (let item of items) { + let date = null; + if (item.startDate) { + date = item.startDate.clone(); + } else if (item.entryDate) { + date = item.entryDate.clone(); + } else if (item.dueDate) { + date = item.dueDate.clone(); + } + + if (!date) { + continue; + } + + if (!earliestDate || date.compare(earliestDate) < 0) { + earliestDate = date; + } + } + let firstDate = currentView().selectedDay; + + let offset = null; + if (earliestDate) { + // Timezones and DT/DST time may differ between the earliest item + // and the selected day. Determine the offset between the + // earliestDate in local time and the selected day in whole days. + earliestDate = earliestDate.getInTimezone(cal.dtz.defaultTimezone); + earliestDate.isDate = true; + offset = firstDate.subtractDate(earliestDate); + let deltaDST = firstDate.timezoneOffset - earliestDate.timezoneOffset; + offset.inSeconds += deltaDST; + } + + // we only will need to ask whether to send notifications, if there + // are attendees at all + let withAttendees = items.filter(aItem => aItem.getAttendees().length > 0); + + let notify = Ci.calIItipItem.USER; + let destCal = null; + if (Services.prefs.getBoolPref("calendar.paste.intoSelectedCalendar", false)) { + destCal = getSelectedCalendar(); + } else { + let pasteText = "paste"; + if (withAttendees.length) { + if (withAttendees.every(item => item.isEvent())) { + pasteText += "Event"; + } else if (withAttendees.every(item => item.isTodo())) { + pasteText += "Task"; + } else { + pasteText += "Item"; + } + if (withAttendees.length > 1) { + pasteText += "s"; + } + } + let validPasteText = pasteText != "paste" && !pasteText.endsWith("Item"); + pasteText += items.length == withAttendees.length ? "Only" : "Also"; + + let calendars = cal.manager + .getCalendars() + .filter(cal.acl.isCalendarWritable) + .filter(cal.acl.userCanAddItemsToCalendar) + .filter(aCal => { + let status = aCal.getProperty("currentStatus"); + return Components.isSuccessCode(status); + }); + if (calendars.length > 1) { + let args = {}; + args.calendars = calendars; + args.promptText = cal.l10n.getCalString("pastePrompt"); + + if (validPasteText) { + pasteText = cal.l10n.getCalString(pasteText); + let note = cal.l10n.getCalString("pasteNotifyAbout", [pasteText]); + args.promptNotify = note; + + args.labelExtra1 = cal.l10n.getCalString("pasteDontNotifyLabel"); + args.onExtra1 = aCal => { + destCal = aCal; + notify = Ci.calIItipItem.NONE; + }; + args.labelOk = cal.l10n.getCalString("pasteAndNotifyLabel"); + args.onOk = aCal => { + destCal = aCal; + notify = Ci.calIItipItem.AUTO; + }; + } else { + args.onOk = aCal => { + destCal = aCal; + }; + } + + window.openDialog( + "chrome://calendar/content/chooseCalendarDialog.xhtml", + "_blank", + "chrome,titlebar,modal,resizable", + args + ); + } else if (calendars.length == 1) { + destCal = calendars[0]; + } + } + if (!destCal) { + return; + } + + startBatchTransaction(); + for (let item of items) { + // TODO: replace the UUID only it it already exists in the + // calendar to avoid to break invitation scenarios where remote + // parties rely on the UUID. + let newItem = item.clone(); + // Set new UID to allow multiple paste actions of the same + // clipboard content. + newItem.id = cal.getUUID(); + if (offset) { + cal.item.shiftOffset(newItem, offset); + } + + let extResp = { responseMode: Ci.calIItipItem.NONE }; + if (item.getAttendees().length > 0) { + extResp.responseMode = notify; + } + + doTransaction("add", newItem, destCal, null, null, extResp); + } + endBatchTransaction(); + break; + } + default: + break; + } +} |