Bug 437711 - Improve dnd operations in Today Pane. r=pmorris
authorKhushil Mistry <khushil324@gmail.com>
Thu, 12 Mar 2020 20:06:46 +1300
changeset 38472 40262b53174b5cd70a6e5812f79ca5ca4ac1bc80
parent 38471 506be393d5ef8d60b287903bb8e0cb1fca37460e
child 38473 9aa142882f1850579cd4262bdbb7263306b2f00a
push id400
push userclokep@gmail.com
push dateMon, 04 May 2020 18:56:09 +0000
reviewerspmorris
bugs437711
Bug 437711 - Improve dnd operations in Today Pane. r=pmorris
calendar/base/content/calendar-dnd-listener.js
--- a/calendar/base/content/calendar-dnd-listener.js
+++ b/calendar/base/content/calendar-dnd-listener.js
@@ -5,28 +5,29 @@
 /* import-globals-from ../../../mail/base/content/nsDragAndDrop.js */
 /* import-globals-from calendar-item-editing.js */
 /* import-globals-from calendar-management.js */
 /* import-globals-from import-export.js */
 
 var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var { AppConstants } = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
 
 /* exported invokeEventDragSession, calendarViewDNDObserver,
  *          calendarMailButtonDNDObserver, calendarCalendarButtonDNDObserver,
  *          calendarTaskButtonDNDObserver
  */
 
 var itemConversion = {
   /**
    * Converts an email message to a calendar item.
    *
-   * @param aItem     The target calIItemBase.
-   * @param aMessage  The nsIMsgHdr to convert from.
+   * @param {Object} aItem - The target calIItemBase.
+   * @param {Object} aMsgHdr - The nsIMsgHdr to convert from.
    */
   calendarItemFromMessage(aItem, aMsgHdr) {
     let msgFolder = aMsgHdr.folder;
     let msgUri = msgFolder.getUriForMsg(aMsgHdr);
 
     aItem.calendar = getSelectedCalendar();
     aItem.title = aMsgHdr.mime2DecodedSubject;
 
@@ -54,18 +55,18 @@ var itemConversion = {
     aItem.setProperty("DESCRIPTION", plainTextMessage);
   },
 
   /**
    * 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 aItem     The item to copy from.
-   * @param aTarget   the item to copy to.
+   * @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));
     }
 
@@ -91,18 +92,18 @@ var itemConversion = {
       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 aEvent    The event to copy from.
-   * @return          The resulting task.
+   * @param {Object} aEvent - The event to copy from.
+   * @return {Object} The resulting task.
    */
   taskFromEvent(aEvent) {
     let item = cal.createTodo();
 
     this.copyItemBase(aEvent, item);
 
     // Dates and alarms
     if (!aEvent.startDate.isDate && !aEvent.endDate.isDate) {
@@ -129,18 +130,18 @@ var itemConversion = {
     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 aTask     The task to copy from.
-   * @return          The resulting event.
+   * @param {Object} aTask - The task to copy from.
+   * @return {Object} The resulting event.
    */
   eventFromTask(aTask) {
     let item = cal.createEvent();
 
     this.copyItemBase(aTask, item);
 
     // Dates and alarms
     item.startDate = aTask.entryDate;
@@ -185,73 +186,72 @@ var itemConversion = {
  * A base class for drag and drop observers
  * @class calDNDBaseObserver
  */
 function calDNDBaseObserver() {
   cal.ASSERT(false, "Inheriting objects call calDNDBaseObserver!");
 }
 
 calDNDBaseObserver.prototype = {
-  // initialize this class's members
-  initBase() {},
-
   getSupportedFlavours() {
     let flavourSet = new FlavourSet();
     flavourSet.appendFlavour("text/calendar");
     flavourSet.appendFlavour("text/x-moz-url");
     flavourSet.appendFlavour("text/x-moz-message");
     flavourSet.appendFlavour("text/unicode");
     flavourSet.appendFlavour("application/x-moz-file");
     return flavourSet;
   },
 
   /**
    * Action to take when dropping the event.
    */
 
-  onDrop(aEvent, aTransferData, aDragSession) {
+  onDrop(event, transferData, dragSession) {
+    // Handles text/x-moz-message, text/x-moz-address and text/x-moz-url flavours.
+    if (this.onDropEventData(event.dataTransfer)) {
+      return;
+    }
+
     let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
     transferable.init(null);
     transferable.addDataFlavor("text/calendar");
-    transferable.addDataFlavor("text/x-moz-url");
-    transferable.addDataFlavor("text/x-moz-message");
     transferable.addDataFlavor("text/unicode");
-    transferable.addDataFlavor("application/x-moz-file");
+
+    dragSession.getData(transferable, 0);
 
-    aDragSession.getData(transferable, 0);
-
-    let data = {};
+    let transData = {};
     let bestFlavor = {};
-    transferable.getAnyTransferData(bestFlavor, data);
+    transferable.getAnyTransferData(bestFlavor, transData);
 
     try {
-      data = data.value.QueryInterface(Ci.nsISupportsString);
+      transData = transData.value.QueryInterface(Ci.nsISupportsString);
     } catch (exc) {
       // we currently only supports strings:
       return;
     }
 
     // Treat unicode data with VEVENT in it as text/calendar
-    if (bestFlavor.value == "text/unicode" && data.toString().includes("VEVENT")) {
+    if (bestFlavor.value == "text/unicode" && transData.toString().includes("VEVENT")) {
       bestFlavor.value = "text/calendar";
     }
 
     switch (bestFlavor.value) {
       case "text/calendar": {
         if (AppConstants.platform == "macosx") {
           // Mac likes to convert all \r to \n, we need to reverse this.
-          data = data.data.replace(/\n\n/g, "\r\n");
+          transData = transData.data.replace(/\n\n/g, "\r\n");
         }
         let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser);
-        parser.parseString(data);
+        parser.parseString(transData);
         this.onDropItems(parser.getItems().concat(parser.getParentlessItems()));
         break;
       }
       case "text/unicode": {
-        let droppedUrl = this.retrieveURLFromData(data, bestFlavor.value);
+        let droppedUrl = transData.toString().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);
@@ -264,100 +264,91 @@ calDNDBaseObserver.prototype = {
         try {
           // XXX support csv
           let importer = Cc["@mozilla.org/calendar/import;1?type=ics"].getService(Ci.calIImporter);
           let items = importer.importFromStream(inputStream);
           this.onDropItems(items);
         } finally {
           inputStream.close();
         }
-
         break;
       }
-      case "application/x-moz-file-promise":
-      case "text/x-moz-url": {
-        let uri = Services.io.newURI(data.toString());
-        let loader = cal.provider.createStreamLoader();
-        let channel = Services.io.newChannelFromURI(
-          uri,
-          null,
-          Services.scriptSecurityManager.getSystemPrincipal(),
-          null,
-          Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
-          Ci.nsIContentPolicy.TYPE_OTHER
-        );
-        channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
-
-        let self = this;
-
-        let listener = {
-          onStreamComplete(aLoader, aContext, aStatus, aResultLength, aResult) {
-            let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser);
-            let encoding = channel.contentCharset || "utf-8";
-            let result = aResultLength
-              ? new TextDecoder(encoding).decode(Uint8Array.from(aResult))
-              : "";
-            parser.parseString(result);
-            self.onDropItems(parser.getItems().concat(parser.getParentlessItems()));
-          },
-        };
-
-        try {
-          loader.init(listener);
-          channel.asyncOpen(loader);
-        } catch (e) {
-          cal.ERROR(e);
-        }
-        break;
-      }
-      case "text/x-moz-message": {
-        let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
-        this.onDropMessage(messenger.msgHdrFromURI(data));
-        break;
-      }
-      default:
-        cal.ASSERT(false, "unknown data flavour:" + bestFlavor.value + "\n");
-        break;
     }
   },
 
   onDragStart(aEvent, aTransferData, aDragAction) {},
   onDragOver(aEvent, aFlavor, aDragSession) {},
   onDragExit(aEvent, aDragSession) {},
 
   onDropItems(aItems) {},
   onDropMessage(aMessage) {},
 
-  retrieveURLFromData(aData, aFlavor) {
-    switch (aFlavor) {
-      case "text/unicode": {
-        let data = aData.toString();
-        let separator = data.indexOf("\n");
-        if (separator != -1) {
-          data = data.substr(0, separator);
+  /**
+   * Extract the data of dataTransfer object in the drop event if presents. It will
+   * extract data of following flavours: text/x-moz-message, text/x-moz-address and
+   * text/x-moz-url.
+   *
+   * @param {Object} dataTransfer - The dataTransfer object from the drop event.
+   * @return {boolean} Returns true if we have data from the given flavours.
+   */
+  onDropEventData(dataTransfer) {
+    let flavours = ["text/x-moz-message", "text/x-moz-address", "text/x-moz-url"];
+
+    let dataFlavor, data;
+    const MAX = 8; // Let's say we want to handle max 8 items.
+    for (let i = 0; i < dataTransfer.mozItemCount && i < MAX; i++) {
+      let types = Array.from(dataTransfer.mozTypesAt(i));
+      for (let flavour of flavours) {
+        if (types.includes(flavour)) {
+          let flavorData = dataTransfer.mozGetDataAt(flavour, i);
+          if (flavorData) {
+            data = flavorData;
+            dataFlavor = flavour;
+          }
+          break;
         }
-        return data;
       }
-      case "application/x-moz-file":
-        return aData.URL;
-      default:
-        return null;
+
+      switch (dataFlavor) {
+        case "text/x-moz-address":
+          if ("onAddressDrop" in this) {
+            this.onAddressDrop(data);
+          }
+          break;
+        case "text/x-moz-url": {
+          data = data.toString().split("\n")[0];
+          if (!data) {
+            return false;
+          }
+          let url = Services.io.newURI(data);
+          this.onDropURL(url);
+          break;
+        }
+        case "text/x-moz-message": {
+          let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+          this.onDropMessage(messenger.msgHdrFromURI(data));
+          break;
+        }
+        default:
+          cal.ASSERT(false, "unknown data flavour:" + dataFlavor + "\n");
+          break;
+      }
     }
+    return !!dataFlavor;
   },
 };
 
 /**
  * calViewDNDObserver::calViewDNDObserver
  *
  * Drag'n'drop handler for the calendar views. This handler is
  * derived from the base handler and just implements specific actions.
  */
 function calViewDNDObserver() {
   this.wrappedJSObject = this;
-  this.initBase();
 }
 
 calViewDNDObserver.prototype = {
   __proto__: calDNDBaseObserver.prototype,
 
   /**
    * calViewDNDObserver::onDropItems
    *
@@ -384,29 +375,28 @@ calViewDNDObserver.prototype = {
 /**
  * calMailButtonDNDObserver::calMailButtonDNDObserver
  *
  * Drag'n'drop handler for the 'mail mode'-button. This handler is
  * derived from the base handler and just implements specific actions.
  */
 function calMailButtonDNDObserver() {
   this.wrappedJSObject = this;
-  this.initBase();
 }
 
 calMailButtonDNDObserver.prototype = {
   __proto__: calDNDBaseObserver.prototype,
 
   /**
    * calMailButtonDNDObserver::onDropItems
    *
    * Gets called in case we're dropping an array of items
    * on the 'mail mode'-button.
    *
-   * @param aItems        An array of items to handle.
+   * @param {Object} aItems - An array of items to handle.
    */
   onDropItems(aItems) {
     if (aItems && aItems.length > 0) {
       let item = aItems[0];
       let identity = item.calendar.getProperty("imip.identity");
       let parties = item.getAttendees();
       if (item.organizer) {
         parties.push(item.organizer);
@@ -426,123 +416,200 @@ calMailButtonDNDObserver.prototype = {
   },
 
   /**
    * calMailButtonDNDObserver::onDropMessage
    *
    * Gets called in case we're dropping a message
    * on the 'mail mode'-button.
    *
-   * @param aMessage     The message to handle.
+   * @param {Object} aMessage - The message to handle.
    */
   onDropMessage(aMessage) {},
 };
 
 /**
  * calCalendarButtonDNDObserver::calCalendarButtonDNDObserver
  *
- * Drag'n'drop handler for the 'calendar mode'-button. This handler is
+ * Drag'n'drop handler for the 'open calendar tab'-button. This handler is
  * derived from the base handler and just implements specific actions.
  */
 function calCalendarButtonDNDObserver() {
   this.wrappedJSObject = this;
-  this.initBase();
 }
 
 calCalendarButtonDNDObserver.prototype = {
   __proto__: calDNDBaseObserver.prototype,
 
   /**
    * calCalendarButtonDNDObserver::onDropItems
    *
    * Gets called in case we're dropping an array of items
-   * on the 'calendar mode'-button.
+   * on the 'open calendar tab'-button.
    *
-   * @param aItems        An array of items to handle.
+   * @param {Object} aItems - An array of items to handle.
    */
   onDropItems(aItems) {
     for (let item of aItems) {
       let newItem = item;
       if (cal.item.isToDo(item)) {
         newItem = itemConversion.eventFromTask(item);
       }
       createEventWithDialog(null, null, null, null, newItem);
     }
   },
 
   /**
    * calCalendarButtonDNDObserver::onDropMessage
    *
    * Gets called in case we're dropping a message on the
-   * 'calendar mode'-button. In this case we create a new
+   * '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 aMessage     The message to handle.
+   * @param {Object} aMessage - The message to handle.
    */
   onDropMessage(aMessage) {
     let newItem = cal.createEvent();
     itemConversion.calendarItemFromMessage(newItem, aMessage);
     createEventWithDialog(null, null, null, null, newItem);
   },
+
+  /**
+   * calCalendarButtonDNDObserver::onDropURL
+   *
+   * Gets called in case we're dropping a uri on the 'open calendar tab'-button.
+   *
+   * @param {string} uri - The uri to handle.
+   */
+  onDropURL(uri) {
+    let newItem = cal.createEvent();
+    newItem.calendar = getSelectedCalendar();
+    cal.dtz.setDefaultStartEndHour(newItem);
+    cal.alarms.setDefaultValues(newItem);
+    let attachment = cal.createAttachment();
+    attachment.uri = uri;
+    newItem.addAttachment(attachment);
+    createEventWithDialog(null, null, null, null, newItem);
+  },
+
+  /**
+   * calCalendarButtonDNDObserver::onAddressDrop
+   *
+   * Gets called in case we're dropping addresses on the 'open calendar tab'-button.
+   *
+   * @param {string} addresses - The addresses to handle.
+   */
+  onAddressDrop(addresses) {
+    let parsedInput = MailServices.headerParser.makeFromDisplayAddress(addresses);
+    let attendee = cal.createAttendee();
+    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 = cal.createEvent();
+    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);
+  },
 };
 
 /**
  * calTaskButtonDNDObserver::calTaskButtonDNDObserver
  *
- * Drag'n'drop handler for the 'task mode'-button. This handler is
+ * Drag'n'drop handler for the 'open tasks tab'-button. This handler is
  * derived from the base handler and just implements specific actions.
  */
 function calTaskButtonDNDObserver() {
   this.wrappedJSObject = this;
-  this.initBase();
 }
 
 calTaskButtonDNDObserver.prototype = {
   __proto__: calDNDBaseObserver.prototype,
 
   /**
    * calTaskButtonDNDObserver::onDropItems
    *
    * Gets called in case we're dropping an array of items
-   * on the 'task mode'-button.
+   * on the 'open tasks tab'-button.
    *
-   * @param aItems        An array of items to handle.
+   * @param {Object} aItems - An array of items to handle.
    */
   onDropItems(aItems) {
     for (let item of aItems) {
       let newItem = item;
       if (cal.item.isEvent(item)) {
         newItem = itemConversion.taskFromEvent(item);
       }
       createTodoWithDialog(null, null, null, newItem);
     }
   },
 
   /**
    * calTaskButtonDNDObserver::onDropMessage
    *
    * Gets called in case we're dropping a message
-   * on the 'task mode'-button.
+   * on the 'open tasks tab'-button.
    *
-   * @param aMessage     The message to handle.
+   * @param {Object} aMessage - The message to handle.
    */
   onDropMessage(aMessage) {
     let todo = cal.createTodo();
     itemConversion.calendarItemFromMessage(todo, aMessage);
     createTodoWithDialog(null, null, null, todo);
   },
+
+  /**
+   * calTaskButtonDNDObserver::onDropURL
+   *
+   * Gets called in case we're dropping a uri on the 'open tasks tab'-button.
+   *
+   * @param {string} uri - The uri to handle.
+   */
+  onDropURL(uri) {
+    let todo = cal.createTodo();
+    todo.calendar = getSelectedCalendar();
+    cal.dtz.setDefaultStartEndHour(todo);
+    cal.alarms.setDefaultValues(todo);
+    let attachment = cal.createAttachment();
+    attachment.uri = uri;
+    todo.addAttachment(attachment);
+    createTodoWithDialog(null, null, null, todo);
+  },
 };
 
 /**
  * Invoke a drag session for the passed item. The passed box will be used as a
  * source.
  *
- * @param aItem     The item to drag.
- * @param aXULBox   The XUL box to invoke the drag session from.
+ * @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([Ci.nsIFlavorDataProvider]),