Fix bug 377744 - Optimize drag & drop and rezising of events/tasks. r=philipp
authorDecathlon <bv1578@gmail.com>
Tue, 06 Dec 2011 09:13:00 +0100
changeset 11039 817576bcae20addaa6634a0709e0cad91e6e219d
parent 11038 714c2c71f32367b79f6190a15179a18291325750
child 11040 a7fa3f3063dcf32d140a1d849afbdbeaf0d136fe
push id463
push userbugzilla@standard8.plus.com
push dateTue, 24 Apr 2012 17:34:51 +0000
treeherdercomm-beta@e53588e8f7b0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersphilipp
bugs377744
Fix bug 377744 - Optimize drag & drop and rezising of events/tasks. r=philipp
calendar/base/content/calendar-multiday-view.xml
calendar/base/themes/common/calendar-views.css
--- a/calendar/base/content/calendar-multiday-view.xml
+++ b/calendar/base/content/calendar-multiday-view.xml
@@ -811,16 +811,30 @@
                               chunkBox.setAttribute("height", size);
                           } else {
                               chunkBox.setAttribute("width", size);
                           }
                           chunkBox.setAttribute("context",
                                                 this.getAttribute("item-context") ||
                                                   this.getAttribute("context"));
                           chunkBox.setAttribute("orient", orient);
+
+                          // Set the gripBars visibility in the chunk
+                          let startGripVisible = (chunk.event.startDate || chunk.event.entryDate)
+                                                     .compare(chunk.startDate) == 0;
+                          let endGripVisible = (chunk.event.endDate || chunk.event.dueDate)
+                                                   .compare(chunk.endDate) <= 0;
+                          if (startGripVisible && endGripVisible) {
+                              chunkBox.setAttribute("gripBars", "both");
+                          } else if (endGripVisible) {
+                              chunkBox.setAttribute("gripBars", "end");
+                          } else if (startGripVisible) {
+                              chunkBox.setAttribute("gripBars", "start");
+                          }
+
                           innerColumn.appendChild(chunkBox);
                           chunkBox.calendarView = this.calendarView;
                           chunkBox.occurrence = chunk.event;
                           chunkBox.parentColumn = this;
                           if (chunk.event.hashId in this.mSelectedItemIds) {
                               chunkBox.selected = true;
                               this.mSelectedChunks.push(chunkBox);
                           }
@@ -1238,16 +1252,17 @@
                   var dur = floatstart.subtractDate(prevEnd);
                   if (dur.inSeconds) {
                       col.push({duration: dur});
                   }
                   var floatend = end.clone();
                   floatend.timezone = floating();
                   col.push({event: data.itemInfo.event,
                             endDate: end,
+                            startDate: start,
                             duration: floatend.subtractDate(floatstart)});
               }
               layerOffset = layers.length;
           }
           return layers;
         ]]></body>
       </method>
 
@@ -1434,37 +1449,58 @@
                       [event.ctrlKey ? item.parentItem : item]);
               }
               invokeEventDragSession(dragState.dragOccurrence, col);
               return;
           }
 
           col.fgboxes.box.setAttribute("dragging", "true");
           col.fgboxes.dragbox.setAttribute("dragging", "true");
+          let minutesInDay = col.mEndMin - col.mStartMin;
 
           // check if we need to jump a column
-          if (dragState.dragType == "move") {
-              let newcol = col.calendarView.findColumnForClientPoint(event.screenX, event.screenY);
-              if (newcol && newcol != col) {
-                  // kill our drag state
-                  for (let column = firstCol, i = firstIndex;
-                       column && i < col.mDragState.shadows;
-                       column = column.nextSibling, i++) {
-                      column.fgboxes.dragbox.removeAttribute("dragging");
-                      column.fgboxes.box.removeAttribute("dragging");
+          let jumpedColumns;
+          let newcol = col.calendarView.findColumnForClientPoint(event.screenX, event.screenY);
+          if (newcol && newcol != col) {
+              // Find how many columns we are jumping by subtracting the dates.
+              let dur = newcol.mDate.subtractDate(col.mDate);
+              jumpedColumns = dur.days;
+              jumpedColumns *= dur.isNegative ? -1 : 1;
+              if (dragState.dragType == "modify-start") {
+                  // prevent dragging the start date after the end date in a new column
+                  if ((dragState.limitEndMin - minutesInDay * jumpedColumns) < 0) {
+                      return;
+                  }
+                  dragState.limitEndMin -= minutesInDay * jumpedColumns;
+              } else if (dragState.dragType == "modify-end") {
+                  // prevent dragging the end date before the start date in a new column
+                  if ((dragState.limitStartMin - minutesInDay * jumpedColumns) > minutesInDay) {
+                      return;
                   }
-
-                  // jump ship
-                  newcol.acceptInProgressSweep(dragState);
-
-                  // restart event handling
-                  col.onEventSweepMouseMove(event);
-
-                  return;
+                  dragState.limitStartMin -= minutesInDay * jumpedColumns;
+              } else if (dragState.dragType == "new") {
+                  dragState.limitEndMin -= minutesInDay * jumpedColumns;
+                  dragState.limitStartMin -= minutesInDay * jumpedColumns;
+                  dragState.jumpedColumns += jumpedColumns;
               }
+              // kill our drag state
+              for (let column = firstCol, i = firstIndex;
+                   column && i < col.mDragState.shadows;
+                   column = column.nextSibling, i++) {
+                  column.fgboxes.dragbox.removeAttribute("dragging");
+                  column.fgboxes.box.removeAttribute("dragging");
+              }
+
+              // jump ship
+              newcol.acceptInProgressSweep(dragState);
+
+              // restart event handling
+              col.onEventSweepMouseMove(event);
+
+              return;
           }
 
           let mousePos;
           let sizeattr;
           if (col.getAttribute("orient") == "vertical") {
               mousePos = event.screenY - col.parentNode.boxObject.screenY;
               sizeattr = "height";
           } else {
@@ -1478,77 +1514,65 @@
           let snapIntMin = (event.shiftKey &&
                             !event.ctrlKey &&
                             !event.altKey  &&
                             !event.metaKey) ? 1 : 15;
           let interval = col.mPixPerMin * snapIntMin;
           let curmin = Math.floor(pos / interval) * snapIntMin;
           let deltamin = curmin - dragState.origMin;
 
-          // variables to keep track of the start/end times for the event under
-          // construction (values sometimes different from those needed to
-          // build the drag shadows)
-          dragState.lastEnd = dragState.origMin;
-          dragState.lastStart = dragState.origMinStart;
-
           let shadowElements;
           if (dragState.dragType == "new") {
+              // Extend deltamin in a linear way over the columns
+              deltamin += minutesInDay * dragState.jumpedColumns;
               if (deltamin < 0) {
                   // create a new event modifying the start. End time is fixed
-                  // and start time is always in this day, hence we don't need
-                  // to keep track of any other value and we can build the drag
-                  // shadow without calling getShadowElements()
-                  dragState.startMin = dragState.origMin + deltamin;
-                  dragState.endMin = dragState.origMin;
-                  shadowElements = {shadows: 1, offset: 0,
-                                    startMin: dragState.startMin,
-                                    endMin: dragState.endMin};
+                  shadowElements = {shadows: 1 - dragState.jumpedColumns,
+                                    offset: 0,
+                                    startMin: curmin,
+                                    endMin: dragState.origMin};
               } else {
                   // create a new event modifying the end. Start time is fixed
-                  // but the end can also go on the next day (at least for 0:00)
-                  // so we need to keep track and build a shadow for one or two days
-                  dragState.startMin = dragState.origMin;
-                  dragState.endMin = dragState.origMin + deltamin;
-                  dragState.lastEnd += deltamin;
-                  shadowElements = col.getShadowElements(dragState.limitStartMin, dragState.lastEnd);
-                  dragState.endMin = shadowElements.endMin;
+                  shadowElements = {shadows: dragState.jumpedColumns + 1,
+                                    offset: dragState.jumpedColumns,
+                                    startMin: dragState.origMin,
+                                    endMin: curmin};
               }
-          } else if (dragState.dragType == "move") {
-              // if we're moving, we modify startMin and endMin. When startMin
-              // is negative, the event start is in the previous day. We keep
-              // track to calculate the event's final position with "lastStart"
-              // variable, while startMin and endMin allow to build the shadows
-              dragState.lastStart += deltamin;
-              shadowElements = col.getShadowElements(dragState.lastStart, dragState.origMinEnd + deltamin);
               dragState.startMin = shadowElements.startMin;
               dragState.endMin = shadowElements.endMin;
+          } else if (dragState.dragType == "move") {
+              // if we're moving, we modify startMin and endMin of the shadow.
+              shadowElements = col.getShadowElements(dragState.origMinStart + deltamin,
+                                                     dragState.origMinEnd + deltamin);
+              dragState.startMin = shadowElements.startMin;
+              dragState.endMin = shadowElements.endMin;
+              // Keep track of the last start position because it will help to
+              // build the event at the end of the drag session.
+              dragState.lastStart = dragState.origMinStart + deltamin;
           } else if (dragState.dragType == "modify-start") {
               // if we're modifying the start, the end time is fixed.
               shadowElements = col.getShadowElements(dragState.origMin + deltamin, dragState.limitEndMin);
               dragState.startMin = shadowElements.startMin;
               dragState.endMin = shadowElements.endMin;
 
               // but we need to not go past the end; if we hit
               // the end, then we'll clamp to the previous snap interval minute
-              if (dragState.limitEndMin <= dragState.startMin) {
-                  dragState.startMin = Math.floor((dragState.limitEndMin - snapIntMin) / snapIntMin) * snapIntMin;
+              if (dragState.startMin >= dragState.limitEndMin) {
+                  dragState.startMin = Math.ceil((dragState.limitEndMin - snapIntMin) / snapIntMin) * snapIntMin;
               }
           } else if (dragState.dragType == "modify-end") {
-              // if we're modifying the end, the start time is fixed, and we'll always
-              // set the spacer to a constant size.
-              dragState.lastEnd += deltamin;
-              shadowElements = col.getShadowElements(dragState.limitStartMin, dragState.lastEnd);
+              // if we're modifying the end, the start time is fixed.
+              shadowElements = col.getShadowElements(dragState.limitStartMin, dragState.origMin + deltamin);
               dragState.startMin = shadowElements.startMin;
               dragState.endMin = shadowElements.endMin;
 
               // but we need to not go past the start; if we hit
               // the start, then we'll clamp to the next snap interval minute
-              if (dragState.lastEnd <= dragState.limitStartMin) {
+              if (dragState.endMin <= dragState.limitStartMin) {
                   dragState.endMin = Math.floor((dragState.limitStartMin + snapIntMin) / snapIntMin) * snapIntMin;
-                  dragState.lastEnd = dragState.endMin;
               }
           }
           let currentOffset = shadowElements.offset;
           let currentShadows = shadowElements.shadows;
 
           // now we can update the shadow boxes position and size
           col.updateShadowsBoxes(dragState.startMin, dragState.endMin,
                                  currentOffset, currentShadows,
@@ -1595,34 +1619,27 @@
                   if (Math.abs(event.screenY - dragState.origLoc) < 3)
                       ignore = true;
               } else {
                   if (Math.abs(event.screenX - dragState.origLoc) < 3)
                       ignore = true;
               }
 
               if (ignore) {
-                  document.calendarEventColumnDragging = null;
                   col.mDragState = null;
                   return;
               }
           }
 
           var newStart;
           var newEnd;
           var startTZ;
           var endTZ;
           let dragDay = col.mDate;
-
-          if (dragState.dragType == "new") {
-              newStart = dragDay.clone();
-              newStart.isDate = false;
-              newEnd = dragDay.clone();
-              newEnd.isDate = false;
-          } else {
+          if (dragState.dragType != "new") {
               var oldStart = dragState.dragOccurrence.startDate || dragState.dragOccurrence.entryDate;
               var oldEnd = dragState.dragOccurrence.endDate || dragState.dragOccurrence.dueDate;
               newStart = oldStart.clone();
               newEnd = oldEnd.clone();
 
               // Our views are pegged to the default timezone.  If the event
               // isn't also in the timezone, we're going to need to do some
               // tweaking. We could just do this for every event but
@@ -1632,35 +1649,48 @@
                   col.mTimezone != newEnd.timezone) {
                   startTZ = newStart.timezone;
                   endTZ = newEnd.timezone;
                   newStart = newStart.getInTimezone(col.calendarView.mTimezone);
                   newEnd = newEnd.getInTimezone(col.calendarView.mTimezone);
               }
           }
 
-          if (dragState.dragType == "modify-start" ||
-              dragState.dragType == "new") {
+          if (dragState.dragType == "modify-start") {
               newStart.resetTo(dragDay.year, dragDay.month, dragDay.day,
                                0, dragState.startMin + col.mStartMin, 0,
                                newStart.timezone);
-          }
-
-          if (dragState.dragType == "modify-end" ||
-              dragState.dragType == "new") {
+          } else if (dragState.dragType == "modify-end") {
               newEnd.resetTo(dragDay.year, dragDay.month, dragDay.day,
-                             0, dragState.lastEnd + col.mStartMin, 0,
+                             0, dragState.endMin + col.mStartMin, 0,
                              newEnd.timezone);
-          }
-
-          if (dragState.dragType == "move") {
-              // Figure out how much the event moved to take the same position
-              // of the shadow (snap to full 15 minutes and multiples or 1 minute
-              // if modifier key is pressed).
-              let duration = dragDay.subtractDate(dragState.origDate);
+          } else if (dragState.dragType == "new") {
+              let startDay = dragState.origColumn.mDate;
+              let draggedForward = (dragDay.compare(startDay) > 0);
+              newStart = draggedForward ? startDay.clone() : dragDay.clone();
+              newEnd = draggedForward ? dragDay.clone() : startDay.clone();
+              newStart.isDate = false;
+              newEnd.isDate = false;
+              newStart.resetTo(newStart.year, newStart.month, newStart.day,
+                               0, dragState.startMin + col.mStartMin, 0,
+                               newStart.timezone);
+              newEnd.resetTo(newEnd.year, newEnd.month, newEnd.day,
+                             0, dragState.endMin + col.mStartMin, 0,
+                             newEnd.timezone);
+
+              // Edit the event title on the first of the new event's occurrences
+              if (draggedForward) {
+                  dragState.origColumn.mCreatedNewEvent = true;
+              } else {
+                  col.mCreatedNewEvent = true;
+              }
+          } else if (dragState.dragType == "move") {
+              // Figure out the new date-times of the event by adding the duration
+              // of the total movement (days and minutes) to the old dates.
+              let duration = dragDay.subtractDate(dragState.origColumn.mDate);
               let minutes = dragState.lastStart - dragState.realStart;
 
               // Since both boxDate and beginMove are dates (note datetimes),
               // subtractDate will only give us a non-zero number of hours on
               // DST changes. While strictly speaking, subtractDate's behavior
               // is correct, we need to move the event a discrete number of
               // days here. There is no need for normalization here, since
               // addDuration does the job for us. Also note, the duration used
@@ -1691,17 +1721,16 @@
           if (startTZ) {
               newStart = newStart.getInTimezone(startTZ);
           }
           if (endTZ) {
               newEnd = newEnd.getInTimezone(endTZ);
           }
 
           if (dragState.dragType == "new") {
-              col.mCreatedNewEvent = true;
               // We won't pass a calendar, since the display calendar is the
               // composite anyway. createNewEvent() will use the selected
               // calendar.
               // TODO We might want to get rid of the extra displayCalendar
               // member.
               col.calendarView.controller.createNewEvent(null,
                                                          newStart,
                                                          newEnd);
@@ -1736,54 +1765,48 @@
         <body><![CDATA[
           if (!isCalendarWritable(aOccurrence.calendar)
               || !userCanModifyItem(aOccurrence)
               || (aOccurrence.calendar instanceof Components.interfaces.calISchedulingSupport && aOccurrence.calendar.isInvitation(aOccurrence))
               || aOccurrence.calendar.getProperty("capabilities.events.supported") === false) {
               return;
           }
 
-          //dump ("startSweepingToModify\n");
           this.mDragState = {
               origColumn: this,
               dragOccurrence: aOccurrence,
               mouseOffset: 0,
               offset: null,
               shadows: null,
-              limitStartMin: null
+              limitStartMin: null,
+              lastStart: 0,
+              jumpedColumns: 0
           };
 
           // snap interval: 15 minutes or 1 minute if modifier key is pressed
           let snapIntMin = aSnapInt || 15;
-          let interval = this.mPixPerMin * snapIntMin;
           let sizeattr;
-
-          let frameloc;
           if (this.getAttribute("orient") == "vertical") {
               this.mDragState.origLoc = aMouseY;
-              frameloc = aMouseY - this.parentNode.boxObject.screenY;
               sizeattr = "height";
           } else {
               this.mDragState.origLoc = aMouseX;
-              frameloc = aMouseX - this.parentNode.boxObject.screenX;
               sizeattr = "width";
           }
 
           let mins = this.getStartEndMinutesForOccurrence(aOccurrence);
 
           // these are only used to compute durations or to compute UI
           // sizes, so offset by this.mStartMin for sanity here (at the
           // expense of possible insanity later)
           mins.start -= this.mStartMin;
           mins.end -= this.mStartMin;
 
-          this.mDragState.limitDurationMin = mins.realEnd - mins.realStart;
           if (aGrabbedElement == "start") {
               this.mDragState.dragType = "modify-start";
-              // drag the start is possible on every day of the occurence, so
               // we have to use "realEnd" as fixed end value
               this.mDragState.limitEndMin = mins.realEnd;
 
               // snap start
               this.mDragState.origMin = Math.floor(mins.start / snapIntMin) * snapIntMin;
 
               // show the shadows and drag labels when clicking on gripbars
               let shadowElements = this.getShadowElements(this.mDragState.origMin,
@@ -1797,17 +1820,16 @@
                                       sizeattr);
 
               // update drag labels
               let lastCol = this.firstLastShadowColumns().lastCol;
               this.updateDragLabels(this, lastCol);
 
           } else if (aGrabbedElement == "end") {
               this.mDragState.dragType =  "modify-end";
-              // drag the end is possible on every day of the occurence, so
               // we have to use "realStart" as fixed end value
               this.mDragState.limitStartMin = mins.realStart;
 
               // snap end
               this.mDragState.origMin = Math.floor(mins.end / snapIntMin) * snapIntMin;
 
               // show the shadows and drag labels when clicking on gripbars
               let shadowElements = this.getShadowElements(this.mDragState.limitStartMin,
@@ -1821,56 +1843,55 @@
                                       sizeattr);
 
               // update drag labels
               let firstCol = this.firstLastShadowColumns().firstCol;
               this.updateDragLabels(firstCol, this);
 
           } else if (aGrabbedElement == "middle") {
               this.mDragState.dragType = "move";
-
-              // in a move, origMin will be the min of the start element;
-              // but we also use the min of start and end separately because
-              // we keep track of both to figure out the real shadow size.
+              // in a move, origMin will be the start minute of the element where
+              // the drag occurs. Along with mouseOffset, it allows to track the
+              // shadow position. origMinStart and origMinEnd allow to figure out
+              // the real shadow size.
               // We snap to the start and add the real duration to find the end
+              let limitDurationMin = mins.realEnd - mins.realStart;
               this.mDragState.origMin = Math.floor(mins.start / snapIntMin) * snapIntMin;
               this.mDragState.origMinStart = Math.floor(mins.realStart / snapIntMin) * snapIntMin;
-              this.mDragState.origMinEnd = this.mDragState.limitDurationMin + this.mDragState.origMinStart;
+              this.mDragState.origMinEnd = this.mDragState.origMinStart + limitDurationMin;
+              // Keep also track of the real Start, it will be used at the end
+              // of the drag session to calculate the new start and end datetimes.
               this.mDragState.realStart = mins.realStart;
+
               let shadowElements = this.getShadowElements(this.mDragState.origMinStart,
                                                           this.mDragState.origMinEnd);
               this.mDragState.shadows = shadowElements.shadows;
               this.mDragState.offset = shadowElements.offset;
-              // Because we can pass this event to other columns, we also need
-              // to track the original column's date too, to get the correct offset
-              this.mDragState.origDate = this.mDate;
               // we need to set a mouse offset, since we're not dragging from
               // one end of the element
               if (aEventBox) {
                   if (this.getAttribute("orient") == "vertical")
                       this.mDragState.mouseOffset = aMouseY - aEventBox.boxObject.screenY;
                   else
                       this.mDragState.mouseOffset = aMouseX - aEventBox.boxObject.screenX;
               }
           } else {
-              dump ("+++ Invalid grabbed element: '" + aGrabbedElement + "'\n");
+              // Invalid grabbed element.
           }
 
           document.calendarEventColumnDragging = this;
 
-          //dump (">>> drag is: " + this.mDragState.dragType + "\n");
-
           window.addEventListener("mousemove", this.onEventSweepMouseMove, false);
           window.addEventListener("mouseup", this.onEventSweepMouseUp, false);
           window.addEventListener("keypress", this.onEventSweepKeypress, false);
         ]]></body>
       </method>
 
       <!-- called by sibling columns to tell us to take over the sweeping
-         - of an event.  Used by "move".
+         - of an event.
         -->
       <method name="acceptInProgressSweep">
         <parameter name="aDragState"/>
         <body><![CDATA[
           this.mDragState = aDragState;
           document.calendarEventColumnDragging = this;
 
           this.fgboxes.box.setAttribute("dragging", "true");
@@ -1992,29 +2013,33 @@
         }
 
         this.mDragState = {
             origColumn: this,
             dragType: "new",
             mouseOffset: 0,
             offset: null,
             shadows: null,
-            limitStartMin: null
+            limitStartMin: null,
+            limitEndMin: null,
+            jumpedColumns: 0
         };
 
         // snap interval: 15 minutes or 1 minute if modifier key is pressed
         let snapIntMin = (event.shiftKey &&
                           !event.ctrlKey &&
                           !event.altKey  &&
                           !event.metaKey) ? 1 : 15;
         let interval = this.mPixPerMin * snapIntMin;
 
         if (this.getAttribute("orient") == "vertical") {
             this.mDragState.origLoc = event.screenY;
             this.mDragState.origMin = Math.floor((event.screenY - this.parentNode.boxObject.screenY)/interval) * snapIntMin;
+            this.mDragState.limitEndMin = this.mDragState.origMin;
+            this.mDragState.limitStartMin = this.mDragState.origMin;
             this.fgboxes.dragspacer.setAttribute("height", this.mDragState.origMin * this.mPixPerMin);
         } else {
             this.mDragState.origLoc = event.screenX;
             this.mDragState.origMin = Math.floor((event.screenX - this.parentNode.boxObject.screenX)/interval) * snapIntMin;
             this.fgboxes.dragspacer.setAttribute("width", this.mDragState.origMin * this.mPixPerMin);
         }
 
         document.calendarEventColumnDragging = this;
@@ -3500,19 +3525,19 @@
          - the calendar-event-column that contains it.  If
          - no column contains it, return null.
         -->
       <method name="findColumnForClientPoint">
         <parameter name="aClientX"/>
         <parameter name="aClientY"/>
         <body><![CDATA[
           for each (var col in this.mDateColumns) {
-              var bo = col.column.topbox.boxObject;
-              if ((aClientX >= bo.screenX) && (aClientX < (bo.screenX + bo.width)) &&
-                  (aClientY >= bo.screenY) && (aClientY < (bo.screenY + bo.height)))
+              let bo = document.getAnonymousElementByAttribute(col.column, "anonid", "boxstack").boxObject;
+              if ((aClientX >= bo.screenX) && (aClientX <= (bo.screenX + bo.width)) &&
+                  (aClientY >= bo.screenY) && (aClientY <= (bo.screenY + bo.height)))
               {
                   return col.column;
               }
           }
           return null;
         ]]></body>
       </method>
 
--- a/calendar/base/themes/common/calendar-views.css
+++ b/calendar/base/themes/common/calendar-views.css
@@ -899,40 +899,31 @@ calendar-event-box[orient="vertical"] .c
     visibility: hidden;
 }
 
 calendar-event-box[orient="vertical"] .calendar-event-box-grippy-bottom {
     list-style-image: url("chrome://calendar/skin/event-grippy-bottom.png");
     visibility: hidden;
 }
 
-calendar-event-box[orient="horizontal"]:hover .calendar-event-box-grippy-top,
-calendar-event-box[orient="horizontal"]:hover .calendar-event-box-grippy-bottom {
-    visibility: visible;
-}
-
-calendar-event-box[orient="vertical"]:hover .calendar-event-box-grippy-top,
-calendar-event-box[orient="vertical"]:hover .calendar-event-box-grippy-bottom {
-    visibility: visible;
-}
-
 calendar-event-box[orient="horizontal"] .calendar-event-box-grippy-top {
     list-style-image: url("chrome://calendar/skin/event-grippy-left.png");
     visibility: hidden;
 }
 
 calendar-event-box[orient="horizontal"] .calendar-event-box-grippy-bottom {
     list-style-image: url("chrome://calendar/skin/event-grippy-right.png");
     visibility: hidden;
 }
 
-calendar-event-box[readonly="true"]:hover .calendar-event-box-grippy-top,
-calendar-event-box[readonly="true"]:hover .calendar-event-box-grippy-bottom {
-    visibility: hidden;
-    cursor: auto;
+calendar-event-box[gripBars="start"]:not([readonly="true"]):hover .calendar-event-box-grippy-top,
+calendar-event-box[gripBars="end"]:not([readonly="true"]):hover .calendar-event-box-grippy-bottom,
+calendar-event-box[gripBars="both"]:not([readonly="true"]):hover .calendar-event-box-grippy-top,
+calendar-event-box[gripBars="both"]:not([readonly="true"]):hover .calendar-event-box-grippy-bottom {
+    visibility: visible;
 }
 
 /* tooltips */
 vbox.tooltipBox {
     max-width: 40em;
 }
 
 column.tooltipValueColumn {