summaryrefslogtreecommitdiffstats
path: root/comm/calendar/base/content/calendar-clipboard.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/calendar/base/content/calendar-clipboard.js')
-rw-r--r--comm/calendar/base/content/calendar-clipboard.js306
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;
+ }
+}