Selaa lähdekoodia

added droppable/drop, refactored selectable code, fixed issue 406

Adam Shaw 15 vuotta sitten
vanhempi
sitoutus
8ebda5094f
9 muutettua tiedostoa jossa 596 lisäystä ja 453 poistoa
  1. 247 226
      src/agenda.js
  2. 104 77
      src/grid.js
  3. 15 0
      src/main.js
  4. 1 1
      src/misc/head.txt
  5. 19 90
      src/selection_util.js
  6. 76 58
      src/util.js
  7. 132 0
      tests/droppable.html
  8. 1 0
      tests/plain.html
  9. 1 1
      version.txt

+ 247 - 226
src/agenda.js

@@ -88,10 +88,6 @@ function Agenda(element, options, methods) {
 			return bg.find('td:eq(' + col + ') div div');
 		}),
 		slotTopCache = {},
-		daySelectionManager,
-		slotSelectionManager,
-		selectionHelper,
-		selectionMatrix,
 		// ...
 		
 	view = $.extend(this, viewMethods, methods, {
@@ -669,38 +665,27 @@ function Agenda(element, options, methods) {
 	
 	function draggableDayEvent(event, eventElement, isStart) {
 		if (!options.disableDragging && eventElement.draggable) {
-			var origPosition, origWidth,
-				resetElement,
-				allDay=true,
-				matrix;
+			var origWidth;
+			var allDay=true;
+			var dayDelta;
 			eventElement.draggable({
 				zIndex: 9,
 				opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using
 				revertDuration: options.dragRevertDuration,
 				start: function(ev, ui) {
-					view.hideEvents(event, eventElement);
 					view.trigger('eventDragStart', eventElement, event, ev, ui);
-					origPosition = eventElement.position();
+					view.hideEvents(event, eventElement);
 					origWidth = eventElement.width();
-					resetElement = function() {
-						if (!allDay) {
-							eventElement
-								.width(origWidth)
-								.height('')
-								.draggable('option', 'grid', null);
-							allDay = true;
-						}
-					};
-					matrix = buildDayMatrix(function(cell) {
-						eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta);
-						view.clearOverlays();
+					hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
+						eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
+						clearOverlay();
 						if (cell) {
+							dayDelta = colDelta * dis;
 							if (!cell.row) {
 								// on full-days
 								renderDayOverlay(
-									matrix,
-									addDays(cloneDate(event.start), cell.colDelta),
-									addDays(exclEndDay(event), cell.colDelta)
+									addDays(cloneDate(event.start), dayDelta),
+									addDays(exclEndDay(event), dayDelta)
 								);
 								resetElement();
 							}else{
@@ -710,50 +695,51 @@ function Agenda(element, options, methods) {
 									setOuterHeight(
 										eventElement.width(colWidth - 10), // don't use entire width
 										slotHeight * Math.round(
-											(event.end ? ((event.end - event.start)/MINUTE_MS) : options.defaultEventMinutes)
-											/options.slotMinutes)
+											(event.end ? ((event.end - event.start) / MINUTE_MS) : options.defaultEventMinutes)
+											/ options.slotMinutes
+										)
 									);
 									eventElement.draggable('option', 'grid', [colWidth, 1]);
 									allDay = false;
 								}
 							}
 						}
-					},true);
-					matrix.mouse(ev);
-				},
-				drag: function(ev, ui) {
-					matrix.mouse(ev);
+					}, ev, 'drag');
 				},
 				stop: function(ev, ui) {
+					var cell = hoverListener.stop();
+					clearOverlay();
 					view.trigger('eventDragStop', eventElement, event, ev, ui);
-					view.clearOverlays();
-					var cell = matrix.cell;
-					var dayDelta = dis * (
-						allDay ? // can't trust cell.colDelta when using slot grid
-							(cell ? cell.colDelta : 0) :
-							Math.floor((ui.position.left - origPosition.left) / colWidth)
-					);
-					if (!cell || !dayDelta && !cell.rowDelta) {
-						// over nothing (has reverted)
+					if (cell && (!allDay || dayDelta)) {
+						// changed!
+						eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
+						var minuteDelta = 0;
+						if (!allDay) {
+							minuteDelta = Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight)
+								* options.slotMinutes
+								+ minMinute
+								- (event.start.getHours() * 60 + event.start.getMinutes());
+						}
+						view.eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
+					}else{
+						// hasn't moved or is out of bounds (draggable has already reverted)
 						resetElement();
 						if ($.browser.msie) {
 							eventElement.css('filter', ''); // clear IE opacity side-effects
 						}
 						view.showEvents(event, eventElement);
-					}else{
-						eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
-						view.eventDrop(
-							this, event, dayDelta,
-							allDay ? 0 : // minute delta
-								Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight)
-								* options.slotMinutes
-								+ minMinute
-								- (event.start.getHours() * 60 + event.start.getMinutes()),
-							allDay, ev, ui
-						);
 					}
 				}
 			});
+			function resetElement() {
+				if (!allDay) {
+					eventElement
+						.width(origWidth)
+						.height('')
+						.draggable('option', 'grid', null);
+					allDay = true;
+				}
+			}
 		}
 	}
 	
@@ -763,11 +749,11 @@ function Agenda(element, options, methods) {
 	
 	function draggableSlotEvent(event, eventElement, timeElement) {
 		if (!options.disableDragging && eventElement.draggable) {
-			var origPosition,
-				resetElement,
-				prevSlotDelta, slotDelta,
-				allDay=false,
-				matrix;
+			var origPosition;
+			var allDay=false;
+			var dayDelta;
+			var minuteDelta;
+			var prevMinuteDelta;
 			eventElement.draggable({
 				zIndex: 9,
 				scroll: false,
@@ -776,26 +762,20 @@ function Agenda(element, options, methods) {
 				opacity: view.option('dragOpacity'),
 				revertDuration: options.dragRevertDuration,
 				start: function(ev, ui) {
-					view.hideEvents(event, eventElement);
 					view.trigger('eventDragStart', eventElement, event, ev, ui);
+					view.hideEvents(event, eventElement);
 					if ($.browser.msie) {
 						eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide
 					}
 					origPosition = eventElement.position();
-					resetElement = function() {
-						// convert back to original slot-event
-						if (allDay) {
-							timeElement.css('display', ''); // show() was causing display=inline
-							eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
-							allDay = false;
-						}
-					};
-					prevSlotDelta = 0;
-					matrix = buildDayMatrix(function(cell) {
+					minuteDelta = prevMinuteDelta = 0;
+					hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
 						eventElement.draggable('option', 'revert', !cell);
-						view.clearOverlays();
+						clearOverlay();
 						if (cell) {
-							if (!cell.row && options.allDaySlot) { // over full days
+							dayDelta = colDelta * dis;
+							if (options.allDaySlot && !cell.row) {
+								// over full days
 								if (!allDay) {
 									// convert to temporary all-day event
 									allDay = true;
@@ -803,61 +783,63 @@ function Agenda(element, options, methods) {
 									eventElement.draggable('option', 'grid', null);
 								}
 								renderDayOverlay(
-									matrix,
-									addDays(cloneDate(event.start), cell.colDelta),
-									addDays(exclEndDay(event), cell.colDelta)
+									addDays(cloneDate(event.start), dayDelta),
+									addDays(exclEndDay(event), dayDelta)
 								);
-							}else{ // on slots
+							}else{
+								// on slots
 								resetElement();
 							}
 						}
-					},true);
-					matrix.mouse(ev);
+					}, ev, 'drag');
 				},
 				drag: function(ev, ui) {
-					slotDelta = Math.round((ui.position.top - origPosition.top) / slotHeight);
-					if (slotDelta != prevSlotDelta) {
+					minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * options.slotMinutes;
+					if (minuteDelta != prevMinuteDelta) {
 						if (!allDay) {
-							// update time header
-							var minuteDelta = slotDelta*options.slotMinutes,
-								newStart = addMinutes(cloneDate(event.start), minuteDelta),
-								newEnd;
-							if (event.end) {
-								newEnd = addMinutes(cloneDate(event.end), minuteDelta);
-							}
-							timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat')));
+							updateTimeText(minuteDelta);
 						}
-						prevSlotDelta = slotDelta;
+						prevMinuteDelta = minuteDelta;
 					}
-					matrix.mouse(ev);
 				},
 				stop: function(ev, ui) {
-					view.clearOverlays();
+					var cell = hoverListener.stop();
+					clearOverlay();
 					view.trigger('eventDragStop', eventElement, event, ev, ui);
-					var cell = matrix.cell,
-						dayDelta = dis * (
-							allDay ? // can't trust cell.colDelta when using slot grid
-							(cell ? cell.colDelta : 0) : 
-							Math.floor((ui.position.left - origPosition.left) / colWidth)
-						);
-					if (!cell || !slotDelta && !dayDelta) {
+					if (cell && (dayDelta || minuteDelta || allDay)) {
+						// changed!
+						view.eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui);
+					}else{
+						// either no change or out-of-bounds (draggable has already reverted)
 						resetElement();
+						eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
+						updateTimeText(0);
 						if ($.browser.msie) {
 							eventElement
 								.css('filter', '') // clear IE opacity side-effects
-								.find('span.fc-event-bg').css('display', ''); // .show() made display=inline
+								.find('span.fc-event-bg')
+									.css('display', ''); // .show() made display=inline
 						}
-						eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
 						view.showEvents(event, eventElement);
-					}else{
-						view.eventDrop(
-							this, event, dayDelta,
-							allDay ? 0 : slotDelta * options.slotMinutes, // minute delta
-							allDay, ev, ui
-						);
 					}
 				}
 			});
+			function updateTimeText(minuteDelta) {
+				var newStart = addMinutes(cloneDate(event.start), minuteDelta);
+				var newEnd;
+				if (event.end) {
+					newEnd = addMinutes(cloneDate(event.end), minuteDelta);
+				}
+				timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat')));
+			}
+			function resetElement() {
+				// convert back to original slot-event
+				if (allDay) {
+					timeElement.css('display', ''); // show() was causing display=inline
+					eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
+					allDay = false;
+				}
+			}
 		}
 	}
 	
@@ -918,114 +900,152 @@ function Agenda(element, options, methods) {
 	
 	
 	
-	/* Selecting
+	/* Coordinate Utilities
 	-----------------------------------------------------------------------------*/
-
-	daySelectionManager = new SelectionManager(
-		view,
-		unselect,
-		function(startDate, endDate, allDay) {
-			renderDayOverlay(
-				selectionMatrix,
-				startDate,
-				addDays(cloneDate(endDate), 1)
-			);
-		},
-		clearSelection
-	);
 	
-	function daySelectionMousedown(ev) {
-		if (view.option('selectable')) {
-			selectionMatrix = buildDayMatrix(function(cell) {
-				if (cell) {
-					var d = dayColDate(cell.col);
-					daySelectionManager.drag(d, d, true);
-				}else{
-					daySelectionManager.drag();
-				}
-			});
-			documentDragHelp(
-				function(ev) {
-					selectionMatrix.mouse(ev);
-				},
-				function(ev) {
-					daySelectionManager.dragStop(ev);
-				}
-			);
-			daySelectionManager.dragStart(ev);
-			selectionMatrix.mouse(ev);
-			return false; // prevent auto-unselect and text selection
+	var coordinateGrid = new CoordinateGrid(function(rows, cols) {
+		var e, n, p;
+		bg.find('td').each(function(i, _e) {
+			e = $(_e);
+			n = e.offset().left;
+			if (i) {
+				p[1] = n;
+			}
+			p = [n];
+			cols[i] = p;
+		});
+		p[1] = n + e.outerWidth();
+		if (options.allDaySlot) {
+			e = head.find('td');
+			n = e.offset().top;
+			rows[0] = [n, n+e.outerHeight()];
+		}
+		var bodyContentTop = bodyContent.offset().top;
+		var bodyTop = body.offset().top;
+		var bodyBottom = bodyTop + body.outerHeight();
+		function constrain(n) {
+			return Math.max(bodyTop, Math.min(bodyBottom, n));
+		}
+		for (var i=0; i<slotCnt; i++) {
+			rows.push([
+				constrain(bodyContentTop + slotHeight*i),
+				constrain(bodyContentTop + slotHeight*(i+1))
+			]);
+		}
+	});
+	
+	var hoverListener = new HoverListener(coordinateGrid);
+	
+	// get the Y coordinate of the given time on the given day (both Date objects)
+	function timePosition(day, time) { // both date objects. day holds 00:00 of current day
+		day = cloneDate(day, true);
+		if (time < addMinutes(cloneDate(day), minMinute)) {
+			return 0;
+		}
+		if (time >= addMinutes(cloneDate(day), maxMinute)) {
+			return bodyContent.height();
+		}
+		var slotMinutes = options.slotMinutes,
+			minutes = time.getHours()*60 + time.getMinutes() - minMinute,
+			slotI = Math.floor(minutes / slotMinutes),
+			slotTop = slotTopCache[slotI];
+		if (slotTop === undefined) {
+			slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop;
 		}
+		return Math.max(0, Math.round(
+			slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
+		));
 	}
 	
-	slotSelectionManager = new SelectionManager(
-		view,
-		unselect,
-		renderSlotSelection,
-		clearSelection
+	
+	
+	
+	/* Selecting
+	-----------------------------------------------------------------------------*/
+	
+	var selected = false;
+	var daySelectionMousedown = selection_dayMousedown(
+		view, hoverListener, cellDate, renderDayOverlay, clearOverlay, reportSelection, unselect
 	);
 	
 	function slotSelectionMousedown(ev) {
 		if (view.option('selectable')) {
-			selectionMatrix = buildSlotMatrix(function(cell) {
-				if (cell) {
-					var d = slotCellDate(cell.row, cell.origCol);
-					slotSelectionManager.drag(d, addMinutes(cloneDate(d), options.slotMinutes), false);
+			unselect();
+			var dates;
+			hoverListener.start(function(cell, origCell) {
+				clearSelection();
+				if (cell && cell.col == origCell.col) {
+					var d1 = cellDate(origCell);
+					var d2 = cellDate(cell);
+					dates = [
+						d1,
+						addMinutes(cloneDate(d1), options.slotMinutes),
+						d2,
+						addMinutes(cloneDate(d2), options.slotMinutes)
+					].sort(cmp);
+					renderSlotSelection(dates[0], dates[3]);
 				}else{
-					slotSelectionManager.drag();
+					dates = null;
 				}
-			});
-			documentDragHelp(
-				function(ev) {
-					selectionMatrix.mouse(ev);
-				},
-				function(ev) {
-					slotSelectionManager.dragStop(ev);
+			}, ev);
+			$(document).one('mouseup', function() {
+				hoverListener.stop();
+				if (dates) {
+					reportSelection(dates[0], dates[3], false);
 				}
-			);
-			slotSelectionManager.dragStart(ev);
-			selectionMatrix.mouse(ev);
-			return false; // prevent auto-unselect and text selection
+			});
 		}
 	}
 	
-	documentUnselectAuto(view, unselect);
-	
-	this.select = function(start, end, allDay) {
+	view.select = function(startDate, endDate, allDay) {
+		coordinateGrid.build();
+		unselect();
 		if (allDay) {
 			if (options.allDaySlot) {
-				if (!end) {
-					end = cloneDate(start);
+				if (!endDate) {
+					endDate = cloneDate(startDate);
 				}
-				selectionMatrix = buildDayMatrix();
-				daySelectionManager.select(start, end, allDay);
+				renderDayOverlay(startDate, addDays(cloneDate(endDate), 1));
 			}
 		}else{
-			if (!end) {
-				end = addMinutes(cloneDate(start), options.slotMinutes);
+			if (!endDate) {
+				endDate = addMinutes(cloneDate(startDate), options.slotMinutes);
 			}
-			selectionMatrix = buildSlotMatrix();
-			slotSelectionManager.select(start, end, allDay);
+			renderSlotSelection(startDate, endDate);
 		}
+		reportSelection(startDate, endDate, allDay);
 	};
 	
+	function reportSelection(startDate, endDate, allDay) {
+		selected = true;
+		view.trigger('select', view, startDate, endDate, allDay);
+	}
+	
 	function unselect() {
-		slotSelectionManager.unselect();
-		daySelectionManager.unselect();
+		if (selected) {
+			clearSelection();
+			selected = false;
+			view.trigger('unselect', view);
+		}
 	}
-	this.unselect = unselect;
+	view.unselect = unselect;
+	
+	selection_unselectAuto(view, unselect);
+	
 	
 	
 	
 	/* Selecting drawing utils
 	-----------------------------------------------------------------------------*/
 	
+	var selectionHelper;
+	
 	function renderSlotSelection(startDate, endDate) {
 		var helperOption = view.option('selectHelper');
 		if (helperOption) {
-			var col = dayDiff(startDate, view.visStart);
+			var col = dayDiff(startDate, view.visStart) * dis + dit;
 			if (col >= 0 && col < colCnt) { // only works when times are on same day
-				var rect = selectionMatrix.rect(0, col*dis+dit, 1, col*dis+dit+1, bodyContent); // only for horizontal coords
+				var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only for horizontal coords
 				var top = timePosition(startDate, startDate);
 				var bottom = timePosition(startDate, endDate);
 				if (bottom > top) { // protect against selections that are entirely before or after visible range
@@ -1068,18 +1088,17 @@ function Agenda(element, options, methods) {
 				}
 			}
 		}else{
-			renderSlotOverlay(selectionMatrix, startDate, endDate);
+			renderSlotOverlay(startDate, endDate);
 		}
 	}
 	
 	function clearSelection() {
-		clearOverlays();
+		clearOverlay();
 		if (selectionHelper) {
 			selectionHelper.remove();
 			selectionHelper = null;
 		}
 	}
-
 	
 	
 	
@@ -1087,7 +1106,7 @@ function Agenda(element, options, methods) {
 	/* Semi-transparent Overlay Helpers
 	-----------------------------------------------------*/
 
-	function renderDayOverlay(matrix, startDate, endDate) {
+	function renderDayOverlay(startDate, endDate) {
 		var startCol, endCol;
 		if (rtl) {
 			startCol = dayDiff(endDate, view.visStart)*dis+dit+1;
@@ -1099,21 +1118,26 @@ function Agenda(element, options, methods) {
 		startCol = Math.max(0, startCol);
 		endCol = Math.min(colCnt, endCol);
 		if (startCol < endCol) {
-			var rect = matrix.rect(0, startCol, 1, endCol, head);
 			dayBind(
-				view.renderOverlay(rect, head)
+				_renderDayOverlay(0, startCol, 0, endCol-1)
 			);
 		}
 	}
+	
+	function _renderDayOverlay(col0, row0, col1, row1) {
+		var rect = coordinateGrid.rect(col0, row0, col1, row1, head);
+		return view.renderOverlay(rect, head);
+	}
 
-	function renderSlotOverlay(matrix, overlayStart, overlayEnd) {
+	function renderSlotOverlay(overlayStart, overlayEnd) {
 		var dayStart = cloneDate(view.visStart);
 		var dayEnd = addDays(cloneDate(dayStart), 1);
 		for (var i=0; i<colCnt; i++) {
 			var stretchStart = new Date(Math.max(dayStart, overlayStart));
 			var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
 			if (stretchStart < stretchEnd) {
-				var rect = matrix.rect(0, i*dis+dit, 1, i*dis+dit+1, bodyContent); // only use it for horizontal coords
+				var col = i*dis+dit;
+				var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only use it for horizontal coords
 				var top = timePosition(dayStart, stretchStart);
 				var bottom = timePosition(dayStart, stretchEnd);
 				rect.top = top;
@@ -1127,48 +1151,42 @@ function Agenda(element, options, methods) {
 		}
 	}
 	
-	function clearOverlays() {
+	function clearOverlay() {
 		view.clearOverlays();
 	}
 	
 	
 	
 	
-	/* Coordinate Utilities
-	-----------------------------------------------------------------------------*/
+	/* External dragging
+	-----------------------------------------------------*/
 	
-	// get the Y coordinate of the given time on the given day (both Date objects)
-	function timePosition(day, time) { // both date objects. day holds 00:00 of current day
-		day = cloneDate(day, true);
-		if (time < addMinutes(cloneDate(day), minMinute)) {
-			return 0;
-		}
-		if (time >= addMinutes(cloneDate(day), maxMinute)) {
-			return bodyContent.height();
-		}
-		var slotMinutes = options.slotMinutes,
-			minutes = time.getHours()*60 + time.getMinutes() - minMinute,
-			slotI = Math.floor(minutes / slotMinutes),
-			slotTop = slotTopCache[slotI];
-		if (slotTop === undefined) {
-			slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop;
-		}
-		return Math.max(0, Math.round(
-			slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
-		));
-	}
+	view.isExternalDraggable = function(_element) {
+		return _element.parentNode != daySegmentContainer[0] && _element.parentNode != slotSegmentContainer[0];
+	};
 	
-	function buildDayMatrix(changeCallback, includeSlotArea) {
-		var rowElements = options.allDaySlot ? head.find('td') : $([]);
-		if (includeSlotArea) {
-			rowElements = rowElements.add(body);
-		}
-		return new HoverMatrix(rowElements, bg.find('td'), changeCallback);
-	}
+	view.dragStart = function(ev) {
+		hoverListener.start(function(cell) {
+			clearOverlay();
+			if (cell) {
+				if (cellIsAllDay(cell)) {
+					_renderDayOverlay(cell.row, cell.col, cell.row, cell.col);
+				}else{
+					var d1 = cellDate(cell);
+					var d2 = addMinutes(cloneDate(d1), options.slotMinutes); //options.defaultEventMinutes);
+					renderSlotOverlay(d1, d2);
+				}
+			}
+		}, ev);
+	};
 	
-	function buildSlotMatrix(changeCallback) {
-		return new HoverMatrix(bodyTable.find('td'), bg.find('td'), changeCallback);
-	}
+	view.dragStop = function(ev, ui) {
+		var cell = hoverListener.stop();
+		clearOverlay();
+		if (cell) {
+			view.trigger('drop', view, cellDate(cell), cellIsAllDay(cell), ev, ui);
+		}
+	};
 	
 	
 	
@@ -1188,17 +1206,20 @@ function Agenda(element, options, methods) {
 		return ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt)*dis+dit;
 	}
 	
-	
-	// generating dates from cell row & columns
-
-	function dayColDate(col) {
-		return addDays(cloneDate(view.visStart), col*dis+dit);
+	function cellDate(cell) {
+		var d = addDays(cloneDate(view.visStart), cell.col*dis+dit);
+		var slotIndex = cell.row;
+		if (options.allDaySlot) {
+			slotIndex--;
+		}
+		if (slotIndex >= 0) {
+			addMinutes(d, minMinute + slotIndex*options.slotMinutes);
+		}
+		return d;
 	}
 	
-	function slotCellDate(row, col) {
-		var d = dayColDate(col);
-		addMinutes(d, minMinute + row*options.slotMinutes);
-		return d;
+	function cellIsAllDay(cell) {
+		return options.allDaySlot && !cell.row;
 	}
 	
 	

+ 104 - 77
src/grid.js

@@ -121,8 +121,6 @@ function Grid(element, options, methods) {
 		dayContentPositions = new HorizontalPositionCache(function(dayOfWeek) {
 			return tbody.find('td:eq(' + ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt) + ') div div');
 		}),
-		selectionManager,
-		selectionMatrix,
 		// ...
 		
 	// initialize superclass
@@ -425,37 +423,32 @@ function Grid(element, options, methods) {
 	
 	function draggableEvent(event, eventElement) {
 		if (!options.disableDragging && eventElement.draggable) {
-			var matrix,
-				dayDelta = 0;
+			var dayDelta;
 			eventElement.draggable({
 				zIndex: 9,
 				delay: 50,
 				opacity: view.option('dragOpacity'),
 				revertDuration: options.dragRevertDuration,
 				start: function(ev, ui) {
-					view.hideEvents(event, eventElement);
 					view.trigger('eventDragStart', eventElement, event, ev, ui);
-					matrix = buildDayMatrix(function(cell) {
-						eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta);
-						clearOverlays();
+					view.hideEvents(event, eventElement);
+					hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
+						eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
+						clearOverlay();
 						if (cell) {
-							dayDelta = cell.rowDelta*7 + cell.colDelta*dis;
-							renderDayOverlays(
-								matrix,
+							dayDelta = rowDelta*7 + colDelta*dis;
+							renderDayOverlay(
 								addDays(cloneDate(event.start), dayDelta),
 								addDays(exclEndDay(event), dayDelta)
 							);
 						}else{
 							dayDelta = 0;
 						}
-					});
-					matrix.mouse(ev);
-				},
-				drag: function(ev) {
-					matrix.mouse(ev);
+					}, ev, 'drag');
 				},
 				stop: function(ev, ui) {
-					clearOverlays();
+					hoverListener.stop();
+					clearOverlay();
 					view.trigger('eventDragStop', eventElement, event, ev, ui);
 					if (dayDelta) {
 						eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link
@@ -495,68 +488,108 @@ function Grid(element, options, methods) {
 	
 	
 	
-	/* Selecting
+	/* Coordinate Utilities
 	--------------------------------------------------------*/
-
-	selectionManager = new SelectionManager(
-		view,
-		unselect,
-		function(startDate, endDate, allDay) {
-			renderDayOverlays(
-				selectionMatrix,
-				startDate,
-				addDays(cloneDate(endDate), 1)
-			);
-		},
-		clearOverlays
-	);
 	
-	function selectionMousedown(ev) {
-		if (view.option('selectable')) {
-			selectionMatrix = buildDayMatrix(function(cell) {
-				if (cell) {
-					var d = cellDate(cell.row, cell.col);
-					selectionManager.drag(d, d, true);
-				}else{
-					selectionManager.drag();
-				}
-			});
-			documentDragHelp(
-				function(ev) {
-					selectionMatrix.mouse(ev);
-				},
-				function(ev) {
-					selectionManager.dragStop(ev);
-				}
-			);
-			selectionManager.dragStart(ev);
-			selectionMatrix.mouse(ev);
-			return false; // prevent auto-unselect and text selection
+	var coordinateGrid = new CoordinateGrid(function(rows, cols) {
+		var e, n, p;
+		var tds = tbody.find('tr:first td');
+		if (rtl) {
+			tds = $(tds.get().reverse());
 		}
-	}
+		tds.each(function(i, _e) {
+			e = $(_e);
+			n = e.offset().left;
+			if (i) {
+				p[1] = n;
+			}
+			p = [n];
+			cols[i] = p;
+		});
+		p[1] = n + e.outerWidth();
+		tbody.find('tr').each(function(i, _e) {
+			e = $(_e);
+			n = e.offset().top;
+			if (i) {
+				p[1] = n;
+			}
+			p = [n];
+			rows[i] = p;
+		});
+		p[1] = n + e.outerHeight();
+	});
+	
+	var hoverListener = new HoverListener(coordinateGrid);
+	
+	
+	
+	/* Selecting
+	--------------------------------------------------------*/
 	
-	documentUnselectAuto(view, unselect);
+	var selected = false;
+	var selectionMousedown = selection_dayMousedown(
+		view, hoverListener, cellDate, renderDayOverlay, clearOverlay, reportSelection, unselect
+	);
 	
-	view.select = function(start, end, allDay) {
-		if (!end) {
-			end = cloneDate(start);
+	view.select = function(startDate, endDate, allDay) {
+		coordinateGrid.build();
+		unselect();
+		if (!endDate) {
+			endDate = cloneDate(startDate);
 		}
-		selectionMatrix = buildDayMatrix();
-		selectionManager.select(start, end, allDay);
+		renderDayOverlay(startDate, addDays(cloneDate(endDate), 1));
+		reportSelection(startDate, endDate, allDay);
 	};
 	
+	function reportSelection(startDate, endDate, allDay) {
+		selected = true;
+		view.trigger('select', view, startDate, endDate, allDay);
+	}
+	
 	function unselect() {
-		selectionManager.unselect();
+		if (selected) {
+			clearOverlay();
+			selected = false;
+			view.trigger('unselect', view);
+		}
 	}
 	view.unselect = unselect;
 	
+	selection_unselectAuto(view, unselect);
+	
+	
+	
+	/* External dragging
+	------------------------------------------------------*/
+	
+	view.isExternalDraggable = function(_element) {
+		return _element.parentNode != segmentContainer[0];
+	};
+	
+	view.dragStart = function(ev, ui) {
+		hoverListener.start(function(cell) {
+			clearOverlay();
+			if (cell) {
+				_renderDayOverlay(cell.row, cell.col, cell.row, cell.col);
+			}
+		}, ev);
+	};
+	
+	view.dragStop = function(ev, ui) {
+		var cell = hoverListener.stop();
+		clearOverlay();
+		if (cell) {
+			var d = cellDate(cell);
+			view.trigger('drop', view, d, true, ev, ui);
+		}
+	};
 	
 	
 	
 	/* Semi-transparent Overlay Helpers
 	------------------------------------------------------*/
 	
-	function renderDayOverlays(matrix, overlayStart, overlayEnd) { // overlayEnd is exclusive
+	function renderDayOverlay(overlayStart, overlayEnd) { // overlayEnd is exclusive
 		var rowStart = cloneDate(view.visStart);
 		var rowEnd = addDays(cloneDate(rowStart), colCnt);
 		for (var i=0; i<rowCnt; i++) {
@@ -571,9 +604,8 @@ function Grid(element, options, methods) {
 					colStart = dayDiff(stretchStart, rowStart);
 					colEnd = dayDiff(stretchEnd, rowStart);
 				}
-				var rect = matrix.rect(i, colStart, i+1, colEnd, element);
 				dayBind(
-					view.renderOverlay(rect, element)
+					_renderDayOverlay(i, colStart, i, colEnd-1)
 				);
 			}
 			addDays(rowStart, 7);
@@ -581,28 +613,23 @@ function Grid(element, options, methods) {
 		}
 	}
 	
-	function clearOverlays() {
-		view.clearOverlays();
+	function _renderDayOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
+		var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
+		return view.renderOverlay(rect, element);
 	}
 	
+	function clearOverlay() {
+		view.clearOverlays();
+	}
 	
 	
 	
-	/* Utils
+	/* Date Utils
 	---------------------------------------------------*/
 	
-
-	function buildDayMatrix(changeCallback) {
-		var tds = tbody.find('tr:first td');
-		if (rtl) {
-			tds = $(tds.get().reverse());
-		}
-		return new HoverMatrix(tbody.find('tr'), tds, changeCallback);
-	}
-	
 	
-	function cellDate(r, c) { // convert r,c to date
-		return addDays(cloneDate(view.visStart), r*7 + c*dis+dit);
+	function cellDate(cell) {
+		return addDays(cloneDate(view.visStart), cell.row*7 + cell.col*dis+dit);
 		// TODO: what about weekends in middle of week?
 	}
 	

+ 15 - 0
src/main.js

@@ -840,6 +840,21 @@ $.fn.fullCalendar = function(options) {
 		$(window).resize(windowResize);
 		
 		
+		if (options.droppable) {
+			$(document)
+				.bind('dragstart', function(ev, ui) {
+					if (view.isExternalDraggable(ev.target)) {
+						view.dragStart(ev, ui);
+					}
+				})
+				.bind('dragstop', function(ev, ui) {
+					if (view.isExternalDraggable(ev.target)) {
+						view.dragStop(ev, ui);
+					}
+				});
+		}
+		
+		
 		// let's begin...
 		changeView(options.defaultView);
 		

+ 1 - 1
src/misc/head.txt

@@ -16,5 +16,5 @@
  *
  */
  
-(function($) {
+(function($, undefined) {
 

+ 19 - 90
src/selection_util.js

@@ -1,96 +1,28 @@
 
-function SelectionManager(view, initFunc, displayFunc, clearFunc) {
 
-	var t = this;
-	var selected = false;
-	var initialElement;
-	var initialRange;
-	var start;
-	var end;
-	var allDay;
-	
-	
-	t.dragStart = function(ev) {
-		initFunc();
-		start = end = undefined;
-		initialRange = undefined;
-		initialElement = ev.currentTarget;
-	};
-	
-	
-	t.drag = function(currentStart, currentEnd, currentAllDay) {
-		if (currentStart) {
-			var range = [currentStart, currentEnd];
-			if (!initialRange) {
-				initialRange = range;
-			}
-			var dates = initialRange.concat(range).sort(cmp);
-			start = dates[0];
-			end = dates[3];
-			allDay = currentAllDay;
-			clearFunc();
-			displayFunc(cloneDate(start), cloneDate(end), allDay);
-		}else{
-			// called with no arguments
-			start = end = undefined;
-			clearFunc();
-		}
-	};
-	
-	
-	t.dragStop = function(ev) {
-		if (start) {
-			if (+initialRange[0] == +start && +initialRange[1] == +end) {
-				view.trigger('dayClick', initialElement, start, allDay, ev);
-			}
-			_select();
-		}
-	};
-	
-	
-	t.select = function(newStart, newEnd, newAllDay) {
-		initFunc();
-		start = newStart;
-		end = newEnd;
-		allDay = newAllDay;
-		displayFunc(cloneDate(start), cloneDate(end), allDay);
-		_select();
-	};
-	
-	
-	function _select() { // just set the selected flag, and trigger
-		selected = true;
-		view.trigger('select', view, start, end, allDay);
-	}
-	
-	
-	function unselect() {
-		if (selected) {
-			selected = false;
-			start = end = undefined;
-			clearFunc();
-			view.trigger('unselect', view);
+function selection_dayMousedown(view, hoverListener, cellDate, renderSelection, clearSelection, reportSelection, unselect) {
+	return function(ev) {
+		if (view.option('selectable')) {
+			unselect();
+			var dates;
+			hoverListener.start(function(cell, origCell) {
+				clearSelection();
+				if (cell) {
+					dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp);
+					renderSelection(dates[0], addDays(cloneDate(dates[1]), 1), true);
+				}
+			}, ev);
+			$(document).one('mouseup', function(ev) {
+				if (hoverListener.stop()) { // over a cell?
+					reportSelection(dates[0], dates[1], true);
+				}
+			});
 		}
 	}
-	t.unselect = unselect;
-
 }
 
 
-function documentDragHelp(mousemove, mouseup) {
-	function _mouseup(ev) {
-		mouseup(ev);
-		$(document)
-			.unbind('mousemove', mousemove)
-			.unbind('mouseup', _mouseup);
-	}
-	$(document)
-		.mousemove(mousemove)
-		.mouseup(_mouseup);
-}
-
-
-function documentUnselectAuto(view, unselectFunc) {
+function selection_unselectAuto(view, unselect) {
 	if (view.option('selectable') && view.option('unselectAuto')) {
 		$(document).mousedown(function(ev) {
 			var ignore = view.option('unselectCancel');
@@ -99,10 +31,7 @@ function documentUnselectAuto(view, unselectFunc) {
 					return;
 				}
 			}
-			unselectFunc();
+			unselect();
 		});
 	}
 }
-
-
-

+ 76 - 58
src/util.js

@@ -378,72 +378,47 @@ function topCorrect(tr) { // tr/th/td or anything else
 
 
 
-/* Hover Matrix
+/* Coordinate Grid
 -----------------------------------------------------------------------------*/
 
-function HoverMatrix(rowElements, colElements, changeCallback) {
+function CoordinateGrid(buildFunc) {
 
-	var t=this,
-		tops=[], lefts=[],
-		origRow, origCol,
-		currRow, currCol,
-		e;
-		
-	$.each(rowElements, function(i, _e) {
-		e = $(_e);
-		tops.push(e.offset().top + topCorrect(e));
-	});
-	tops.push(tops[tops.length-1] + e.outerHeight());
-	$.each(colElements, function(i, _e) {
-		e = $(_e);
-		lefts.push(e.offset().left);
-	});
-	lefts.push(lefts[lefts.length-1] + e.outerWidth());
+	var t = this;
+	var rows;
+	var cols;
 	
-
-	t.mouse = function(ev) {
-		var x = ev.pageX;
-		var y = ev.pageY;
-		var r, c;
-		for (r=0; r<tops.length && y>=tops[r]; r++) {}
-		for (c=0; c<lefts.length && x>=lefts[c]; c++) {}
-		r = r >= tops.length ? -1 : r - 1;
-		c = c >= lefts.length ? -1 : c - 1;
-		if (r != currRow || c != currCol) {
-			currRow = r;
-			currCol = c;
-			if (r == -1 || c == -1) {
-				t.cell = null;
-			}else{
-				if (origRow === undefined) {
-					origRow = r;
-					origCol = c;
-				}
-				t.cell = {
-					row: r,
-					col: c,
-					top: tops[r],
-					left: lefts[c],
-					width: lefts[c+1] - lefts[c],
-					height: tops[r+1] - tops[r],
-					origRow: origRow,
-					origCol: origCol,
-					isOrig: r==origRow && c==origCol,
-					rowDelta: r-origRow,
-					colDelta: c-origCol
-				};
+	t.build = function() {
+		rows = [];
+		cols = [];
+		buildFunc(rows, cols);
+	};
+	
+	t.cell = function(x, y) {
+		var rowCnt = rows.length;
+		var colCnt = cols.length;
+		var i, r=-1, c=-1;
+		for (i=0; i<rowCnt; i++) {
+			if (y >= rows[i][0] && y < rows[i][1]) {
+				r = i;
+				break;
+			}
+		}
+		for (i=0; i<colCnt; i++) {
+			if (x >= cols[i][0] && x < cols[i][1]) {
+				c = i;
+				break;
 			}
-			changeCallback(t.cell);
 		}
+		return (r>=0 && c>=0) ? { row:r, col:c } : null;
 	};
 	
-	t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 are exclusive
+	t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
 		var origin = originElement.offset();
 		return {
-			top: tops[row0] - origin.top,
-			left: lefts[col0] - origin.left,
-			width: lefts[col1] - lefts[col0],
-			height: tops[row1] - tops[row0]
+			top: rows[row0][0] - origin.top,
+			left: cols[col0][0] - origin.left,
+			width: cols[col1][1] - cols[col0][0],
+			height: rows[row1][1] - rows[row0][0]
 		};
 	};
 
@@ -451,11 +426,54 @@ function HoverMatrix(rowElements, colElements, changeCallback) {
 
 
 
+/* Hover Listener
+-----------------------------------------------------------------------------*/
+
+function HoverListener(coordinateGrid) {
+
+	var t = this;
+	var bindType;
+	var change;
+	var firstCell;
+	var cell;
+	
+	t.start = function(_change, ev, _bindType) {
+		change = _change;
+		firstCell = cell = null;
+		coordinateGrid.build();
+		mouse(ev);
+		bindType = _bindType || 'mousemove';
+		$(document).bind(bindType, mouse);
+	};
+	
+	function mouse(ev) {
+		var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
+		if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
+			if (newCell) {
+				if (!firstCell) {
+					firstCell = newCell;
+				}
+				change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
+			}else{
+				change(newCell, firstCell);
+			}
+			cell = newCell;
+		}
+	}
+	
+	t.stop = function() {
+		$(document).unbind(bindType, mouse);
+		return cell;
+	};
+	
+}
+
+
+
 /* Misc Utils
 -----------------------------------------------------------------------------*/
 
-var undefined,
-	dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
+var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
 
 function zeroPad(n) {
 	return (n < 10 ? '0' : '') + n;

+ 132 - 0
tests/droppable.html

@@ -0,0 +1,132 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+<link rel='stylesheet' type='text/css' href='../examples/redmond/theme.css' />
+<script type='text/javascript' src='loader.js'></script>
+<script type='text/javascript'>
+
+	$(document).ready(function() {
+	
+		var date = new Date();
+		var d = date.getDate();
+		var m = date.getMonth();
+		var y = date.getFullYear();
+		
+		$('#calendar').fullCalendar({
+		
+			droppable: true,
+			drop: function(date, allDay) {
+				console.log('drop', date, allDay);
+			},
+			
+			//defaultView: 'agendaWeek',
+			//isRTL: true,
+			
+			header: {
+				left: 'prev,next today',
+				center: 'title',
+				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
+			},
+			editable: true,
+			events: [
+				{
+					title: 'All Day Event',
+					start: new Date(y, m, 1)
+				},
+				{
+					title: 'Long Event',
+					start: new Date(y, m, d-5),
+					end: new Date(y, m, d-2)
+				},
+				{
+					id: 999,
+					title: 'Repeating Event',
+					start: new Date(y, m, d-3, 16, 0),
+					allDay: false
+				},
+				{
+					id: 999,
+					title: 'Repeating Event',
+					start: new Date(y, m, d+4, 16, 0),
+					allDay: false
+				},
+				{
+					title: 'Meeting',
+					start: new Date(y, m, d, 10, 30),
+					allDay: false
+				},
+				{
+					title: 'Lunch',
+					start: new Date(y, m, d, 12, 5),
+					end: new Date(y, m, d, 14, 43),
+					allDay: false
+				},
+				{
+					title: 'Birthday Party',
+					start: new Date(y, m, d+1, 19, 0),
+					end: new Date(y, m, d+1, 22, 30),
+					allDay: false
+				},
+				{
+					title: 'Click for Google',
+					start: new Date(y, m, 28),
+					end: new Date(y, m, 29),
+					url: 'http://google.com/'
+				}
+			]
+		});
+		
+		$('.external-event').draggable({
+			revert: true,
+			revertDuration: 0
+		});
+		
+	});
+
+</script>
+<style type='text/css'>
+
+	body {
+		margin-top: 40px;
+		text-align: center;
+		font-size: 13px;
+		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
+		}
+
+	#calendar {
+		width: 900px;
+		float: left;
+		}
+		
+	#external-events {
+		position: relative;
+		left: 50px;
+		text-align: left;
+		float: left;
+		width: 140px;
+		padding: 10px;
+		border: 1px solid #aaa;
+		background: #ccc;
+		}
+		
+	.external-event {
+		height: 20px;
+		line-height: 20px;
+		color: #fff;
+		background: blue;
+		margin-bottom: 10px;
+		padding-left: 5px;
+		cursor: pointer;
+		}
+
+</style>
+</head>
+<body>
+<div id='calendar'></div>
+<div id='external-events'>
+	<div class='external-event'>Draggable 1</div>
+	<div class='external-event'>Draggable 2</div>
+	<div class='external-event'>Draggable 3</div>
+</div>
+</body>
+</html>

+ 1 - 0
tests/plain.html

@@ -19,6 +19,7 @@
 				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
 			},
 			editable: true,
+			//isRTL: true,
 			events: [
 				{
 					title: 'All Day Event',

+ 1 - 1
version.txt

@@ -1 +1 @@
-1.4.6
+1.4.7