Fix bug 424808 - addItem in calendar-month-day-box scales badly. r=mmecca
authorPhilipp Kewisch <mozilla@kewis.ch>
Fri, 05 Aug 2011 20:49:28 +0200
changeset 8356 0f3c47940325cb003984493ad2a97d491cde36ee
parent 8355 39b63aa540e7232effecfddb47aafc5728ee64b2
child 8357 29ea4eb2c6fbb292cc6b1d1a826c382053d3caa8
push idunknown
push userunknown
push dateunknown
reviewersmmecca
bugs424808
Fix bug 424808 - addItem in calendar-month-day-box scales badly. r=mmecca
calendar/base/content/calendar-month-view.xml
calendar/base/content/dialogs/calendar-alarm-dialog.js
calendar/base/src/calUtils.js
--- a/calendar/base/content/calendar-month-view.xml
+++ b/calendar/base/content/calendar-month-view.xml
@@ -156,36 +156,34 @@
           return val;
         ]]></setter>
       </property>
     </implementation>
   </binding>
 
   <binding id="calendar-month-day-box" extends="chrome://calendar/content/widgets/calendar-widgets.xml#dragndropContainer">
     <content orient="vertical">
-      <xul:label anonid="day-label" 
+      <xul:label anonid="day-label"
                  crop="end"
                  mousethrough="always"
-                 class="calendar-month-day-box-date-label" 
+                 class="calendar-month-day-box-date-label"
                  xbl:inherits="relation,selected,value"/>
       <xul:vbox anonid="day-items" class="calendar-month-day-box-items-box" flex="1">
         <children/>
       </xul:vbox>
     </content>
 
     <implementation>
       <field name="mDate">null</field>
-      <!-- mItemData will always be kept sorted in display order -->
-      <field name="mItemData">[]</field>
+      <field name="mItemHash">{}</field>
       <field name="mShowMonthLabel">false</field>
 
-      <property name="date">
-        <getter>return this.mDate;</getter>
-        <setter>this.setDate(val); return val;</setter>
-      </property>
+      <property name="date"
+                onget="return this.mDate"
+                onset="this.setDate(val); return val;"/>
 
       <property name="selected">
         <getter><![CDATA[
           var sel = this.getAttribute("selected");
           if (sel && sel == "true") {
             return true;
           }
 
@@ -221,29 +219,27 @@
             let monthName = calGetString("dateFormat", "month." + (this.mDate.month + 1) + ".Mmm");
             this.setAttribute("value", this.mDate.day + " " + monthName);
           } else {
             this.setAttribute("value", this.mDate.day);
           }
           return val;
         ]]></setter>
       </property>
-      
+
       <method name="setDate">
         <parameter name="aDate"/>
         <body><![CDATA[
           if (!aDate) {
             throw Components.results.NS_ERROR_NULL_POINTER;
           }
 
           // Remove all the old events
-          this.mItemData = new Array();
-          while (this.hasChildNodes()) {
-              this.removeChild(this.lastChild);
-          }
+          this.mItemHash = {};
+          removeChildren(this);
 
           if (this.mDate && this.mDate.compare(aDate) == 0) {
             return;
           }
 
           this.mDate = aDate;
 
           // Set up DOM attributes for custom CSS coloring.
@@ -260,158 +256,101 @@
              this.setAttribute("value", aDate.day);
           }
         ]]></body>
       </method>
 
       <method name="addItem">
         <parameter name="aItem"/>
         <body><![CDATA[
-          // XXX This scales badly for adding lots of items: O(N^2)
-          for each (ed in this.mItemData) {
-            if (aItem.hashId == ed.item.hashId)
-            {
-              this.deleteItem(aItem);
-              break;
-            }
+          if (aItem.hashId in this.mItemHash) {
+            this.deleteItem(aItem);
           }
 
           function dayboxItemComparator(a, b) {
-            var aIsEvent = isEvent(a.item);
-            var aIsTodo = isToDo(a.item);
-
-            var bIsEvent = isEvent(b.item);
-            var bIsTodo = isToDo(b.item);
+            let aIsEvent = cal.isEvent(a);
+            let aIsTodo = cal.isToDo(a);
 
-            if ((!aIsEvent && !aIsTodo) || (!bIsEvent && !bIsTodo)) {
-              // XXX ????
-              dump ("Don't know how to sort these two events: " + a.item + " " + b.item + "\n");
-              return 0;
-            }
+            let bIsEvent = cal.isEvent(b);
+            let bIsTodo = cal.isToDo(b);
 
             // sort todos before events
             if (aIsTodo && bIsEvent) return -1;
             if (aIsEvent && bIsTodo) return 1;
 
-            // XXX how do I sort todos?
-            if (aIsTodo && bIsTodo) {
-              return 0;
-            }
-
             if (aIsEvent && bIsEvent) {
               // sort all day events before events with a duration
-              if (a.item.startDate.isDate && !b.item.startDate.isDate) return -1;
-              if (!a.item.startDate.isDate && b.item.startDate.isDate) return 1;
+              if (a.startDate.isDate && !b.startDate.isDate) return -1;
+              if (!a.startDate.isDate && b.startDate.isDate) return 1;
 
-              var cmp;
-
-              cmp = a.item.startDate.compare(b.item.startDate);
+              let cmp = a.startDate.compare(b.startDate);
               if (cmp != 0)
                 return cmp;
 
-              cmp = a.item.endDate.compare(b.item.endDate);
+              cmp = a.endDate.compare(b.endDate);
               if (cmp != 0)
                 return cmp;
 
-              if (a.item.title < b.item.title)
-                return -1;
-              if (a.item.title > b.item.title)
-                return 1;
+              cmp = (a.title > b.title) - (a.title < b.title);
+              return cmp;
             }
 
             return 0;
           }
 
-          // Insert the new item block (while keeping the array sorted),
-          // and then relayout. Use a binary search insertation because the
-          // array is already sorted. Array.Sort would use quicksort which is
-          // not good for this case.
-          binaryInsert(this.mItemData, { item: aItem }, dayboxItemComparator, false);
-          this.relayout();
+          let box = createXULElement("calendar-month-day-box-item");
+          let context = this.getAttribute("item-context") ||
+                        this.getAttribute("context");
+          box.setAttribute("context", context);
+          box.setAttribute("calendar-uri", aItem.calendar.uri.spec);
+          box.setAttribute("calendar-id", aItem.calendar.id);
+
+          cal.binaryInsertNode(this, box, aItem, dayboxItemComparator, false);
+
+          box.calendarView = this.calendarView;
+          box.item = aItem;
+          box.parentBox = this;
+          box.occurrence = aItem;
+
+          this.mItemHash[aItem.hashId] = box;
+          return box;
         ]]></body>
       </method>
 
       <method name="selectItem">
         <parameter name="aItem"/>
         <body><![CDATA[
-          for each (var itd in this.mItemData) {
-              if (aItem && (itd.item.hashId == aItem.hashId)) {
-                  itd.box.selected = true;
-              }
+          if (aItem.hashId in this.mItemHash) {
+            this.mItemHash[aItem.hashId].selected = true;
           }
         ]]></body>
       </method>
 
       <method name="unselectItem">
         <parameter name="aItem"/>
         <body><![CDATA[
-          for each (var itd in this.mItemData) {
-              if (aItem && (itd.item.hashId == aItem.hashId)) {
-                  itd.box.selected = false;
-              }
+          if (aItem.hashId in this.mItemHash) {
+            this.mItemHash[aItem.hashId].selected = false;
           }
         ]]></body>
       </method>
 
       <method name="deleteItem">
         <parameter name="aItem"/>
         <body><![CDATA[
-          var deleted = [];
-
-          var origLen = this.mItemData.length;
-          this.mItemData = this.mItemData.filter(
-            function(itd) {
-              if (aItem.hashId == itd.item.hashId)
-              {
-                deleted.push(itd);
-                return false;
-              }
-              return true;
-            });
-
-          if (deleted.length > 0) {
-            for each (itd in deleted) {
-              if (itd.box)
-                this.removeChild(itd.box);
-            }
-            // no need to relayout; all we did was delete
-            //this.relayout();
+          if (aItem.hashId in this.mItemHash) {
+            let node =  this.mItemHash[aItem.hashId];
+            node.parentNode.removeChild(node);
+            delete this.mItemHash[aItem.hashId];
           }
         ]]></body>
       </method>
 
-      <method name="relayout">
-        <body><![CDATA[
-          for (var i = 0; i < this.mItemData.length; i++) {
-            var itd = this.mItemData[i];
-
-            if (!itd.box) {
-              // find what element to insert before
-              var before = null;
-              for (var j = i+1; !before && this.mItemData[j]; j++)
-                before = this.mItemData[j].box;
-
-              var box = createXULElement("calendar-month-day-box-item");
-              box.setAttribute("context", this.getAttribute("item-context") || this.getAttribute("context"));
-              box.setAttribute("calendar-uri", itd.item.calendar.uri.spec);
-              box.setAttribute("calendar-id", itd.item.calendar.id);
-
-              this.insertBefore(box, before);
-
-              box.calendarView = this.calendarView;
-              box.item = itd.item;
-              box.parentBox = this;
-              box.occurrence = itd.item;
-              itd.box = box;
-            }
-          }
-        ]]></body>
-      </method>
       <method name="onDropItem">
-        <parameter name="aItem"/>      
+        <parameter name="aItem"/>
         <body><![CDATA[
           return cal.moveItem(aItem, this.mDate);
         ]]></body>
       </method>
 
     </implementation>
 
     <handlers>
@@ -1044,19 +983,18 @@
         ]]></body>
       </method>
 
       <method name="deleteItemsFromCalendar">
         <parameter name="aCalendar"/>
         <body><![CDATA[
           let deleteHash = {};
           for each (let box in this.mDateBoxes) {
-            let boxItemData = Array.slice(box.mItemData);
-            for each (let itemData in boxItemData) {
-              let item = itemData.item;
+            for each (let node in box.mItemHash) {
+              let item = node.item;
               if (!deleteHash[item.hashId] && item.calendar.id == aCalendar.id) {
                 deleteHash[item.hashId] = true;
                 this.doDeleteItem(item);
               }
             }
           }
         ]]></body>
       </method>
@@ -1072,22 +1010,22 @@
             // No need to animate if the indicator should not be shown.
             return;
           }
 
           // Make sure the flashing attribute is set or reset on all visible
           // boxes.
           let boxes = this.findDayBoxesForItem(aAlarmItem);
           for each (var box in boxes) {
-            for each (var itemData in box.mItemData) {
+            for each (var itemData in box.mItemHash) {
               if (itemData.item.hasSameIds(aAlarmItem)) {
                 if (aStop) {
-                  itemData.box.removeAttribute("flashing");
+                  itemData.removeAttribute("flashing");
                 } else {
-                  itemData.box.setAttribute("flashing", "true");
+                  itemData.setAttribute("flashing", "true");
                 }
               }
             }
           }
 
           if (!aStop) {
             // Set up a timer to stop the flashing after the total time.
             var this_ = this;
--- a/calendar/base/content/dialogs/calendar-alarm-dialog.js
+++ b/calendar/base/content/dialogs/calendar-alarm-dialog.js
@@ -209,44 +209,44 @@ function setupTitle() {
     document.title = title.replace("#1", reminders);
 }
 
 /**
  * Comparison function for the start date of a calendar item and
  * the start date of a calendar-alarm-widget.
  *
  * @param aItem                 A calendar item for the comparison of the start date property
- * @param calAlarmWidget        A calendar-alarm-widget for the start date comparison with the given calendar item
+ * @param calAlarmWidget        The alarm widget item for the start date comparison with the given calendar item
  * @return                      1 - if the calendar item starts before the calendar-alarm-widget
  *                             -1 - if the calendar-alarm-widget starts before the calendar item
  *                              0 - otherwise
  */
-function widgetAlarmComptor(aItem, calAlarmWidget) {
+function widgetAlarmComptor(aItem, aWidgetItem) {
 
-    if (aItem == null || calAlarmWidget == null || calAlarmWidget.item == null) return -1;
+    if (aItem == null || aWidgetItem == null) return -1;
 
     // Get the dates to compare
     let aDate = aItem[calGetStartDateProp(aItem)];
-    let bDate = calAlarmWidget.item[calGetStartDateProp(calAlarmWidget.item)];
+    let bDate = aWidgetItem[calGetStartDateProp(aWidgetItem)];
 
     return aDate.compare(bDate);
 }
 
 /**
  * Add an alarm widget for the passed alarm and item.
  *
  * @param aItem       The calendar item to add a widget for.
  * @param aAlarm      The alarm to add a widget for.
  */
 function addWidgetFor(aItem, aAlarm) {
     let widget = document.createElement("calendar-alarm-widget");
     let alarmRichlist = document.getElementById("alarm-richlist");
 
     // Add widgets sorted by start date ascending
-    binaryInsertNode(alarmRichlist, widget, aItem, widgetAlarmComptor, false);
+    cal.binaryInsertNode(alarmRichlist, widget, aItem, widgetAlarmComptor, false);
 
     widget.item = aItem;
     widget.alarm = aAlarm;
     widget.addEventListener("snooze", onSnoozeAlarm, false);
     widget.addEventListener("dismiss", onDismissAlarm, false);
     widget.addEventListener("itemdetails", onItemDetails, false);
 
     setupTitle();
--- a/calendar/base/src/calUtils.js
+++ b/calendar/base/src/calUtils.js
@@ -1815,39 +1815,43 @@ function binarySearch(itemArray, newItem
 
 /**
  * Insert a new node underneath the given parentNode, using binary search. See binarySearch
  * for a note on how the comptor works.
  *
  * @param parentNode           The parent node underneath the new node should be inserted.
  * @param inserNode            The node to insert
  * @param aItem                The calendar item to add a widget for.
- * @param comptor              A comparison function that can compare two nodes.
+ * @param comptor              A comparison function that can compare two items (not DOM Nodes!)
  * @param discardDuplicates    Use the comptor function to check if the item in
  *                               question is already in the array. If so, the
  *                               new item is not inserted.
+ * @param itemAccessor         [optional] A function that receives a DOM node and returns the associated item
+ *                               If null, this function will be used: function(n) n.item
  */
-function binaryInsertNode(parentNode, insertNode, aItem, comptor, discardDuplicates) {
+function binaryInsertNode(parentNode, insertNode, aItem, comptor, discardDuplicates, itemAccessor) {
+    let accessor = itemAccessor || binaryInsertNode.defaultAccessor;
 
     // Get the index of the node before which the inserNode will be inserted
-    var newIndex = binarySearch(parentNode.childNodes, aItem, comptor);
+    let newIndex = binarySearch(Array.map(parentNode.childNodes, accessor), aItem, comptor);
 
     if (newIndex < 0) {
         parentNode.appendChild(insertNode);
         newIndex = 0;
     } else if (!discardDuplicates ||
-        comptor(parentNode.childNodes[Math.min(newIndex, parentNode.childNodes.length - 1)], insertNode) >= 0) {
+        comptor(accessor(parentNode.childNodes[Math.min(newIndex, parentNode.childNodes.length - 1)]), aItem) >= 0) {
 
         // Only add the node if duplicates should not be discarded, or if
         // they should and the childNode[newIndex] == node.
         let node = parentNode.childNodes[newIndex];
         parentNode.insertBefore(insertNode, node);
     }
     return newIndex;
 }
+binaryInsertNode.defaultAccessor = function(n) n.item;
 
 /**
  * Insert an item into the given array, using binary search. See binarySearch
  * for a note on how the comptor works.
  *
  * @param itemArray             The array to insert into.
  * @param item                  The item to insert into the array.
  * @param comptor               A comparation function that can compare two items.