Răsfoiți Sursa

separate Grid into more files

Adam Shaw 8 ani în urmă
părinte
comite
472e92ad5d

+ 54 - 0
src/common/Grid.business-hours.js

@@ -0,0 +1,54 @@
+
+Grid.mixin({
+
+	// Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
+	// Called by fillSegHtml.
+	businessHoursSegClasses: function(seg) {
+		return [ 'fc-nonbusiness', 'fc-bgevent' ];
+	},
+
+
+	// Compute business hour segs for the grid's current date range.
+	// Caller must ask if whole-day business hours are needed.
+	buildBusinessHourSegs: function(wholeDay) {
+		return this.eventFootprintsToSegs(
+			this.buildBusinessHourEventFootprints(wholeDay)
+		);
+	},
+
+
+	// Compute business hour *events* for the grid's current date range.
+	// Caller must ask if whole-day business hours are needed.
+	// FOR RENDERING
+	buildBusinessHourEventFootprints: function(wholeDay) {
+		var calendar = this.view.calendar;
+
+		return this._buildBusinessHourEventFootprints(wholeDay, calendar.opt('businessHours'));
+	},
+
+
+	_buildBusinessHourEventFootprints: function(wholeDay, businessHourDef) {
+		var calendar = this.view.calendar;
+		var eventInstanceGroup;
+		var eventRanges;
+
+		eventInstanceGroup = calendar.buildBusinessInstanceGroup(
+			wholeDay,
+			businessHourDef,
+			this.unzonedRange
+		);
+
+		if (eventInstanceGroup) {
+			eventRanges = eventInstanceGroup.sliceRenderRanges(
+				this.unzonedRange,
+				calendar
+			);
+		}
+		else {
+			eventRanges = [];
+		}
+
+		return this.eventRangesToEventFootprints(eventRanges);
+	}
+
+});

+ 239 - 0
src/common/Grid.event-dragging.js

@@ -0,0 +1,239 @@
+
+/*
+Wired up via Grid.event-interation.js by calling
+buildSegDragListener
+*/
+Grid.mixin({
+
+	isDraggingSeg: false, // is a segment being dragged? boolean
+
+
+	// Builds a listener that will track user-dragging on an event segment.
+	// Generic enough to work with any type of Grid.
+	// Has side effect of setting/unsetting `segDragListener`
+	buildSegDragListener: function(seg) {
+		var _this = this;
+		var view = this.view;
+		var calendar = view.calendar;
+		var eventManager = calendar.eventManager;
+		var el = seg.el;
+		var eventDef = seg.footprint.eventDef;
+		var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
+		var isDragging;
+		var mouseFollower; // A clone of the original element that will move with the mouse
+		var eventDefMutation;
+
+		if (this.segDragListener) {
+			return this.segDragListener;
+		}
+
+		// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
+		// of the view.
+		var dragListener = this.segDragListener = new HitDragListener(view, {
+			scroll: this.opt('dragScroll'),
+			subjectEl: el,
+			subjectCenter: true,
+			interactionStart: function(ev) {
+				seg.component = _this; // for renderDrag
+				isDragging = false;
+				mouseFollower = new MouseFollower(seg.el, {
+					additionalClass: 'fc-dragging',
+					parentEl: view.el,
+					opacity: dragListener.isTouch ? null : _this.opt('dragOpacity'),
+					revertDuration: _this.opt('dragRevertDuration'),
+					zIndex: 2 // one above the .fc-view
+				});
+				mouseFollower.hide(); // don't show until we know this is a real drag
+				mouseFollower.start(ev);
+			},
+			dragStart: function(ev) {
+				if (
+					dragListener.isTouch &&
+					!view.isEventDefSelected(eventDef) &&
+					eventInstance
+				) {
+					// if not previously selected, will fire after a delay. then, select the event
+					view.selectEventInstance(eventInstance);
+				}
+				isDragging = true;
+				_this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
+				_this.segDragStart(seg, ev);
+				view.hideEventsWithId(eventDef.id); // hide all event segments. our mouseFollower will take over
+			},
+			hitOver: function(hit, isOrig, origHit) {
+				var isAllowed = true;
+				var origFootprint;
+				var footprint;
+				var mutatedEventInstanceGroup;
+				var dragHelperEls;
+
+				// starting hit could be forced (DayGrid.limit)
+				if (seg.hit) {
+					origHit = seg.hit;
+				}
+
+				// hit might not belong to this grid, so query origin grid
+				origFootprint = origHit.component.getSafeHitFootprint(origHit);
+				footprint = hit.component.getSafeHitFootprint(hit);
+
+				if (origFootprint && footprint) {
+					eventDefMutation = _this.computeEventDropMutation(origFootprint, footprint, eventDef);
+
+					if (eventDefMutation) {
+						mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
+							eventDef.id,
+							eventDefMutation
+						);
+						isAllowed = _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
+					}
+					else {
+						isAllowed = false;
+					}
+				}
+				else {
+					isAllowed = false;
+				}
+
+				if (!isAllowed) {
+					eventDefMutation = null;
+					disableCursor();
+				}
+
+				// if a valid drop location, have the subclass render a visual indication
+				if (
+					eventDefMutation &&
+					(dragHelperEls = view.renderDrag(
+						_this.eventRangesToEventFootprints(
+							mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, calendar)
+						),
+						seg
+					))
+				) {
+					dragHelperEls.addClass('fc-dragging');
+					if (!dragListener.isTouch) {
+						_this.applyDragOpacity(dragHelperEls);
+					}
+
+					mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
+				}
+				else {
+					mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
+				}
+
+				if (isOrig) {
+					// needs to have moved hits to be a valid drop
+					eventDefMutation = null;
+				}
+			},
+			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
+				view.unrenderDrag(); // unrender whatever was done in renderDrag
+				mouseFollower.show(); // show in case we are moving out of all hits
+				eventDefMutation = null;
+			},
+			hitDone: function() { // Called after a hitOut OR before a dragEnd
+				enableCursor();
+			},
+			interactionEnd: function(ev) {
+				delete seg.component; // prevent side effects
+
+				// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
+				mouseFollower.stop(!eventDefMutation, function() {
+					if (isDragging) {
+						view.unrenderDrag();
+						_this.segDragStop(seg, ev);
+					}
+
+					if (eventDefMutation) {
+						// no need to re-show original, will rerender all anyways. esp important if eventRenderWait
+						view.reportEventDrop(eventInstance, eventDefMutation, el, ev);
+					}
+					else {
+						view.showEventsWithId(eventDef.id);
+					}
+				});
+				_this.segDragListener = null;
+			}
+		});
+
+		return dragListener;
+	},
+
+
+	// Called before event segment dragging starts
+	segDragStart: function(seg, ev) {
+		this.isDraggingSeg = true;
+		this.publiclyTrigger('eventDragStart', {
+			context: seg.el[0],
+			args: [
+				seg.footprint.getEventLegacy(),
+				ev,
+				{}, // jqui dummy
+				this.view
+			]
+		});
+	},
+
+
+	// Called after event segment dragging stops
+	segDragStop: function(seg, ev) {
+		this.isDraggingSeg = false;
+		this.publiclyTrigger('eventDragStop', {
+			context: seg.el[0],
+			args: [
+				seg.footprint.getEventLegacy(),
+				ev,
+				{}, // jqui dummy
+				this.view
+			]
+		});
+	},
+
+
+	// DOES NOT consider overlap/constraint
+	computeEventDropMutation: function(startFootprint, endFootprint, eventDef) {
+		var date0 = startFootprint.unzonedRange.getStart();
+		var date1 = endFootprint.unzonedRange.getStart();
+		var clearEnd = false;
+		var forceTimed = false;
+		var forceAllDay = false;
+		var dateDelta;
+		var dateMutation;
+		var eventDefMutation;
+
+		if (startFootprint.isAllDay !== endFootprint.isAllDay) {
+			clearEnd = true;
+
+			if (endFootprint.isAllDay) {
+				forceAllDay = true;
+				date0.stripTime();
+			}
+			else {
+				forceTimed = true;
+			}
+		}
+
+		dateDelta = this.diffDates(date1, date0);
+
+		dateMutation = new EventDefDateMutation();
+		dateMutation.clearEnd = clearEnd;
+		dateMutation.forceTimed = forceTimed;
+		dateMutation.forceAllDay = forceAllDay;
+		dateMutation.setDateDelta(dateDelta);
+
+		eventDefMutation = new EventDefMutation();
+		eventDefMutation.setDateMutation(dateMutation);
+
+		return eventDefMutation;
+	},
+
+
+	// Utility for apply dragOpacity to a jQuery set
+	applyDragOpacity: function(els) {
+		var opacity = this.opt('dragOpacity');
+
+		if (opacity != null) {
+			els.css('opacity', opacity);
+		}
+	}
+
+});

+ 247 - 0
src/common/Grid.event-interaction.js

@@ -0,0 +1,247 @@
+
+/*
+Contains:
+- event clicking/mouseover/mouseout
+- things that are common to event dragging AND resizing
+- event helper rendering
+*/
+Grid.mixin({
+
+	// self-config, overridable by subclasses
+	segSelector: '.fc-event-container > *', // what constitutes an event element?
+
+	mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
+
+	// if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
+	// of the date areas. if not defined, assumes to be day and time granularity.
+	// TODO: port isTimeScale into same system?
+	largeUnit: null,
+
+
+	// Diffs the two dates, returning a duration, based on granularity of the grid
+	// TODO: port isTimeScale into this system?
+	diffDates: function(a, b) {
+		if (this.largeUnit) {
+			return diffByUnit(a, b, this.largeUnit);
+		}
+		else {
+			return diffDayTime(a, b);
+		}
+	},
+
+
+	// Attaches event-element-related handlers for *all* rendered event segments of the view.
+	bindSegHandlers: function() {
+		this.bindSegHandlersToEl(this.el);
+	},
+
+
+	// Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
+	bindSegHandlersToEl: function(el) {
+		this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
+		this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
+		this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
+		this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
+		this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
+	},
+
+
+	// Executes a handler for any a user-interaction on a segment.
+	// Handler gets called with (seg, ev), and with the `this` context of the Grid
+	bindSegHandlerToEl: function(el, name, handler) {
+		var _this = this;
+
+		el.on(name, this.segSelector, function(ev) {
+			var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEventsPayload
+
+			// only call the handlers if there is not a drag/resize in progress
+			if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
+				return handler.call(_this, seg, ev); // context will be the Grid
+			}
+		});
+	},
+
+
+	handleSegClick: function(seg, ev) {
+		var res = this.publiclyTrigger('eventClick', { // can return `false` to cancel
+			context: seg.el[0],
+			args: [ seg.footprint.getEventLegacy(), ev, this.view ]
+		});
+
+		if (res === false) {
+			ev.preventDefault();
+		}
+	},
+
+
+	// Updates internal state and triggers handlers for when an event element is moused over
+	handleSegMouseover: function(seg, ev) {
+		if (
+			!GlobalEmitter.get().shouldIgnoreMouse() &&
+			!this.mousedOverSeg
+		) {
+			this.mousedOverSeg = seg;
+
+			if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
+				seg.el.addClass('fc-allow-mouse-resize');
+			}
+
+			this.publiclyTrigger('eventMouseover', {
+				context: seg.el[0],
+				args: [ seg.footprint.getEventLegacy(), ev, this.view ]
+			});
+		}
+	},
+
+
+	// Updates internal state and triggers handlers for when an event element is moused out.
+	// Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
+	handleSegMouseout: function(seg, ev) {
+		ev = ev || {}; // if given no args, make a mock mouse event
+
+		if (this.mousedOverSeg) {
+			seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
+			this.mousedOverSeg = null;
+
+			if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
+				seg.el.removeClass('fc-allow-mouse-resize');
+			}
+
+			this.publiclyTrigger('eventMouseout', {
+				context: seg.el[0],
+				args: [ seg.footprint.getEventLegacy(), ev, this.view ]
+			});
+		}
+	},
+
+
+	handleSegMousedown: function(seg, ev) {
+		var isResizing = this.startSegResize(seg, ev, { distance: 5 });
+
+		if (!isResizing && this.view.isEventDefDraggable(seg.footprint.eventDef)) {
+			this.buildSegDragListener(seg)
+				.startInteraction(ev, {
+					distance: 5
+				});
+		}
+	},
+
+
+	handleSegTouchStart: function(seg, ev) {
+		var view = this.view;
+		var eventDef = seg.footprint.eventDef;
+		var isSelected = view.isEventDefSelected(eventDef);
+		var isDraggable = view.isEventDefDraggable(eventDef);
+		var isResizable = view.isEventDefResizable(eventDef);
+		var isResizing = false;
+		var dragListener;
+		var eventLongPressDelay;
+
+		if (isSelected && isResizable) {
+			// only allow resizing of the event is selected
+			isResizing = this.startSegResize(seg, ev);
+		}
+
+		if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
+
+			eventLongPressDelay = this.opt('eventLongPressDelay');
+			if (eventLongPressDelay == null) {
+				eventLongPressDelay = this.opt('longPressDelay'); // fallback
+			}
+
+			dragListener = isDraggable ?
+				this.buildSegDragListener(seg) :
+				this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
+
+			dragListener.startInteraction(ev, { // won't start if already started
+				delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
+			});
+		}
+	},
+
+
+	// seg isn't draggable, but let's use a generic DragListener
+	// simply for the delay, so it can be selected.
+	// Has side effect of setting/unsetting `segDragListener`
+	buildSegSelectListener: function(seg) {
+		var _this = this;
+		var view = this.view;
+		var eventDef = seg.footprint.eventDef;
+		var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
+
+		if (this.segDragListener) {
+			return this.segDragListener;
+		}
+
+		var dragListener = this.segDragListener = new DragListener({
+			dragStart: function(ev) {
+				if (
+					dragListener.isTouch &&
+					!view.isEventDefSelected(eventDef) &&
+					eventInstance
+				) {
+					// if not previously selected, will fire after a delay. then, select the event
+					view.selectEventInstance(eventInstance);
+				}
+			},
+			interactionEnd: function(ev) {
+				_this.segDragListener = null;
+			}
+		});
+
+		return dragListener;
+	},
+
+
+	// NOTE: very similar to isExternalInstanceGroupAllowed
+	isEventInstanceGroupAllowed: function(eventInstanceGroup) {
+		var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
+		var i;
+
+		for (i = 0; i < eventFootprints.length; i++) {
+			if (!this.view.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
+				return false;
+			}
+		}
+
+		return this.view.calendar.isEventInstanceGroupAllowed(eventInstanceGroup);
+	},
+
+
+	/* Event Helper
+	------------------------------------------------------------------------------------------------------------------*/
+	// TODO: should probably move this to Grid.events, like we did event dragging / resizing
+
+
+	renderHelperEventFootprints: function(eventFootprints, sourceSeg) {
+		return this.renderHelperEventFootprintEls(eventFootprints, sourceSeg)
+			.addClass('fc-helper');
+	},
+
+
+	renderHelperEventFootprintEls: function(eventFootprints, sourceSeg) {
+		// Subclasses must implement.
+		// Must return all mock event elements.
+	},
+
+
+	// Unrenders a mock event
+	// TODO: have this in ChronoComponent
+	unrenderHelper: function() {
+		// subclasses must implement
+	},
+
+
+	fabricateEventFootprint: function(componentFootprint) {
+		var calendar = this.view.calendar;
+		var eventDateProfile = calendar.footprintToDateProfile(componentFootprint);
+		var dummyEvent = new SingleEventDef(new EventSource(calendar));
+		var dummyInstance;
+
+		dummyEvent.dateProfile = eventDateProfile;
+		dummyInstance = dummyEvent.buildInstance();
+
+		return new EventFootprint(componentFootprint, dummyEvent, dummyInstance);
+	}
+
+});

+ 301 - 0
src/common/Grid.event-rendering.js

@@ -0,0 +1,301 @@
+
+Grid.mixin({
+
+	segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
+
+	// derived from options
+	// TODO: move initialization from Grid.js
+	eventTimeFormat: null,
+	displayEventTime: null,
+	displayEventEnd: null,
+
+
+	// Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
+	computeEventTimeFormat: function() {
+		return this.opt('smallTimeFormat');
+	},
+
+
+	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
+	// Only applies to non-all-day events.
+	computeDisplayEventTime: function() {
+		return true;
+	},
+
+
+	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
+	computeDisplayEventEnd: function() {
+		return true;
+	},
+
+
+	renderEventsPayload: function(eventsPayload) {
+		var id, eventInstanceGroup;
+		var eventRenderRanges;
+		var eventFootprints;
+		var eventSegs;
+		var bgSegs = [];
+		var fgSegs = [];
+
+		for (id in eventsPayload) {
+			eventInstanceGroup = eventsPayload[id];
+
+			eventRenderRanges = eventInstanceGroup.sliceRenderRanges(this.view.activeUnzonedRange);
+			eventFootprints = this.eventRangesToEventFootprints(eventRenderRanges);
+			eventSegs = this.eventFootprintsToSegs(eventFootprints);
+
+			if (eventInstanceGroup.getEventDef().hasBgRendering()) {
+				bgSegs.push.apply(bgSegs, // append
+					eventSegs
+				);
+			}
+			else {
+				fgSegs.push.apply(fgSegs, // append
+					eventSegs
+				);
+			}
+		}
+
+		this.segs = [].concat( // record all segs
+			this.renderBgSegs(bgSegs) || bgSegs,
+			this.renderFgSegs(fgSegs) || fgSegs
+		);
+	},
+
+
+	// Unrenders all events currently rendered on the grid
+	unrenderEvents: function() {
+		this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
+		this.clearDragListeners();
+
+		this.unrenderFgSegs();
+		this.unrenderBgSegs();
+
+		this.segs = null;
+	},
+
+
+	// Retrieves all rendered segment objects currently rendered on the grid
+	getEventSegs: function() {
+		return this.segs || [];
+	},
+
+
+	// Background Segment Rendering
+	// ---------------------------------------------------------------------------------------------------------------
+	// TODO: move this to ChronoComponent, but without fill
+
+
+	// Renders the given background event segments onto the grid.
+	// Returns a subset of the segs that were actually rendered.
+	renderBgSegs: function(segs) {
+		return this.renderFill('bgEvent', segs);
+	},
+
+
+	// Unrenders all the currently rendered background event segments
+	unrenderBgSegs: function() {
+		this.unrenderFill('bgEvent');
+	},
+
+
+	// Renders a background event element, given the default rendering. Called by the fill system.
+	bgEventSegEl: function(seg, el) {
+		return this.filterEventRenderEl(seg.footprint, el);
+	},
+
+
+	// Generates an array of classNames to be used for the default rendering of a background event.
+	// Called by fillSegHtml.
+	bgEventSegClasses: function(seg) {
+		var eventDef = seg.footprint.eventDef;
+
+		return [ 'fc-bgevent' ].concat(
+			eventDef.className,
+			eventDef.source.className
+		);
+	},
+
+
+	// Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
+	// Called by fillSegHtml.
+	bgEventSegCss: function(seg) {
+		return {
+			'background-color': this.getSegSkinCss(seg)['background-color']
+		};
+	},
+
+
+	/* Rendering Utils
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	// Compute the text that should be displayed on an event's element.
+	// `range` can be the Event object itself, or something range-like, with at least a `start`.
+	// If event times are disabled, or the event has no time, will return a blank string.
+	// If not specified, formatStr will default to the eventTimeFormat setting,
+	// and displayEnd will default to the displayEventEnd setting.
+	getEventTimeText: function(eventFootprint, formatStr, displayEnd) {
+		return this._getEventTimeText(
+			eventFootprint.eventInstance.dateProfile.start,
+			eventFootprint.eventInstance.dateProfile.end,
+			eventFootprint.componentFootprint.isAllDay,
+			formatStr,
+			displayEnd
+		);
+	},
+
+
+	_getEventTimeText: function(start, end, isAllDay, formatStr, displayEnd) {
+
+		if (formatStr == null) {
+			formatStr = this.eventTimeFormat;
+		}
+
+		if (displayEnd == null) {
+			displayEnd = this.displayEventEnd;
+		}
+
+		if (this.displayEventTime && !isAllDay) {
+			if (displayEnd && end) {
+				return this.view.formatRange(
+					{ start: start, end: end },
+					false, // allDay
+					formatStr
+				);
+			}
+			else {
+				return start.format(formatStr);
+			}
+		}
+
+		return '';
+	},
+
+
+	// Generic utility for generating the HTML classNames for an event segment's element
+	getSegClasses: function(seg, isDraggable, isResizable) {
+		var view = this.view;
+		var classes = [
+			'fc-event',
+			seg.isStart ? 'fc-start' : 'fc-not-start',
+			seg.isEnd ? 'fc-end' : 'fc-not-end'
+		].concat(this.getSegCustomClasses(seg));
+
+		if (isDraggable) {
+			classes.push('fc-draggable');
+		}
+		if (isResizable) {
+			classes.push('fc-resizable');
+		}
+
+		// event is currently selected? attach a className.
+		if (view.isEventDefSelected(seg.footprint.eventDef)) {
+			classes.push('fc-selected');
+		}
+
+		return classes;
+	},
+
+
+	// List of classes that were defined by the caller of the API in some way
+	getSegCustomClasses: function(seg) {
+		var eventDef = seg.footprint.eventDef;
+
+		return [].concat(
+			eventDef.className, // guaranteed to be an array
+			eventDef.source.className
+		);
+	},
+
+
+	// Utility for generating event skin-related CSS properties
+	getSegSkinCss: function(seg) {
+		return {
+			'background-color': this.getSegBackgroundColor(seg),
+			'border-color': this.getSegBorderColor(seg),
+			color: this.getSegTextColor(seg)
+		};
+	},
+
+
+	// Queries for caller-specified color, then falls back to default
+	getSegBackgroundColor: function(seg) {
+		var eventDef = seg.footprint.eventDef;
+
+		return eventDef.backgroundColor ||
+			eventDef.color ||
+			this.getSegDefaultBackgroundColor(seg);
+	},
+
+
+	getSegDefaultBackgroundColor: function(seg) {
+		var source = seg.footprint.eventDef.source;
+
+		return source.backgroundColor ||
+			source.color ||
+			this.opt('eventBackgroundColor') ||
+			this.opt('eventColor');
+	},
+
+
+	// Queries for caller-specified color, then falls back to default
+	getSegBorderColor: function(seg) {
+		var eventDef = seg.footprint.eventDef;
+
+		return eventDef.borderColor ||
+			eventDef.color ||
+			this.getSegDefaultBorderColor(seg);
+	},
+
+
+	getSegDefaultBorderColor: function(seg) {
+		var source = seg.footprint.eventDef.source;
+
+		return source.borderColor ||
+			source.color ||
+			this.opt('eventBorderColor') ||
+			this.opt('eventColor');
+	},
+
+
+	// Queries for caller-specified color, then falls back to default
+	getSegTextColor: function(seg) {
+		var eventDef = seg.footprint.eventDef;
+
+		return eventDef.textColor ||
+			this.getSegDefaultTextColor(seg);
+	},
+
+
+	getSegDefaultTextColor: function(seg) {
+		var source = seg.footprint.eventDef.source;
+
+		return source.textColor ||
+			this.opt('eventTextColor');
+	},
+
+
+	sortEventSegs: function(segs) {
+		segs.sort(proxy(this, 'compareEventSegs'));
+	},
+
+
+	// A cmp function for determining which segments should take visual priority
+	compareEventSegs: function(seg1, seg2) {
+		var f1 = seg1.footprint.componentFootprint;
+		var r1 = f1.unzonedRange;
+		var f2 = seg2.footprint.componentFootprint;
+		var r2 = f2.unzonedRange;
+
+		return r1.startMs - r2.startMs || // earlier events go first
+			(r2.endMs - r2.startMs) - (r1.endMs - r1.startMs) || // tie? longer events go first
+			f2.isAllDay - f1.isAllDay || // tie? put all-day events first (booleans cast to 0/1)
+			compareByFieldSpecs(
+				seg1.footprint.eventDef,
+				seg2.footprint.eventDef,
+				this.view.eventOrderSpecs
+			);
+	}
+
+});

+ 215 - 0
src/common/Grid.event-resizing.js

@@ -0,0 +1,215 @@
+
+/*
+Wired up via Grid.event-interation.js by calling
+startSegResize
+*/
+Grid.mixin({
+
+	isResizingSeg: false, // is a segment being resized? boolean
+
+
+	// returns boolean whether resizing actually started or not.
+	// assumes the seg allows resizing.
+	// `dragOptions` are optional.
+	startSegResize: function(seg, ev, dragOptions) {
+		if ($(ev.target).is('.fc-resizer')) {
+			this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
+				.startInteraction(ev, dragOptions);
+			return true;
+		}
+		return false;
+	},
+
+
+	// Creates a listener that tracks the user as they resize an event segment.
+	// Generic enough to work with any type of Grid.
+	buildSegResizeListener: function(seg, isStart) {
+		var _this = this;
+		var view = this.view;
+		var calendar = view.calendar;
+		var eventManager = calendar.eventManager;
+		var el = seg.el;
+		var eventDef = seg.footprint.eventDef;
+		var eventInstance = seg.footprint.eventInstance;
+		var isDragging;
+		var resizeMutation; // zoned event date properties. falsy if invalid resize
+
+		// Tracks mouse movement over the *grid's* coordinate map
+		var dragListener = this.segResizeListener = new HitDragListener(this, {
+			scroll: this.opt('dragScroll'),
+			subjectEl: el,
+			interactionStart: function() {
+				isDragging = false;
+			},
+			dragStart: function(ev) {
+				isDragging = true;
+				_this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
+				_this.segResizeStart(seg, ev);
+			},
+			hitOver: function(hit, isOrig, origHit) {
+				var isAllowed = true;
+				var origHitFootprint = _this.getSafeHitFootprint(origHit);
+				var hitFootprint = _this.getSafeHitFootprint(hit);
+				var mutatedEventInstanceGroup;
+
+				if (origHitFootprint && hitFootprint) {
+					resizeMutation = isStart ?
+						_this.computeEventStartResizeMutation(origHitFootprint, hitFootprint, seg.footprint) :
+						_this.computeEventEndResizeMutation(origHitFootprint, hitFootprint, seg.footprint);
+
+					if (resizeMutation) {
+						mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
+							eventDef.id,
+							resizeMutation
+						);
+						isAllowed = _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
+					}
+					else {
+						isAllowed = false;
+					}
+				}
+				else {
+					isAllowed = false;
+				}
+
+				if (!isAllowed) {
+					resizeMutation = null;
+					disableCursor();
+				}
+				else if (resizeMutation.isEmpty()) {
+					// no change. (FYI, event dates might have zones)
+					resizeMutation = null;
+				}
+
+				if (resizeMutation) {
+					view.hideEventsWithId(eventDef.id);
+
+					_this.renderEventResize(
+						_this.eventRangesToEventFootprints(
+							mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, calendar)
+						),
+						seg
+					);
+				}
+			},
+			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
+				resizeMutation = null;
+				view.showEventsWithId(eventDef.id); // for when out-of-bounds. show original
+			},
+			hitDone: function() { // resets the rendering to show the original event
+				_this.unrenderEventResize();
+				enableCursor();
+			},
+			interactionEnd: function(ev) {
+				if (isDragging) {
+					_this.segResizeStop(seg, ev);
+				}
+
+				if (resizeMutation) { // valid date to resize to?
+					// no need to re-show original, will rerender all anyways. esp important if eventRenderWait
+					view.reportEventResize(eventInstance, resizeMutation, el, ev);
+				}
+				else {
+					view.showEventsWithId(eventDef.id);
+				}
+				_this.segResizeListener = null;
+			}
+		});
+
+		return dragListener;
+	},
+
+
+	// Called before event segment resizing starts
+	segResizeStart: function(seg, ev) {
+		this.isResizingSeg = true;
+		this.publiclyTrigger('eventResizeStart', {
+			context: seg.el[0],
+			args: [
+				seg.footprint.getEventLegacy(),
+				ev,
+				{}, // jqui dummy
+				this.view
+			]
+		});
+	},
+
+
+	// Called after event segment resizing stops
+	segResizeStop: function(seg, ev) {
+		this.isResizingSeg = false;
+		this.publiclyTrigger('eventResizeStop', {
+			context: seg.el[0],
+			args: [
+				seg.footprint.getEventLegacy(),
+				ev,
+				{}, // jqui dummy
+				this.view
+			]
+		});
+	},
+
+
+	// Returns new date-information for an event segment being resized from its start
+	computeEventStartResizeMutation: function(startFootprint, endFootprint, origEventFootprint) {
+		var origRange = origEventFootprint.componentFootprint.unzonedRange;
+		var startDelta = this.diffDates(
+			endFootprint.unzonedRange.getStart(),
+			startFootprint.unzonedRange.getStart()
+		);
+		var dateMutation;
+		var eventDefMutation;
+
+		if (origRange.getStart().add(startDelta) < origRange.getEnd()) {
+
+			dateMutation = new EventDefDateMutation();
+			dateMutation.setStartDelta(startDelta);
+
+			eventDefMutation = new EventDefMutation();
+			eventDefMutation.setDateMutation(dateMutation);
+
+			return eventDefMutation;
+		}
+
+		return false;
+	},
+
+
+	// Returns new date-information for an event segment being resized from its end
+	computeEventEndResizeMutation: function(startFootprint, endFootprint, origEventFootprint) {
+		var origRange = origEventFootprint.componentFootprint.unzonedRange;
+		var endDelta = this.diffDates(
+			endFootprint.unzonedRange.getEnd(),
+			startFootprint.unzonedRange.getEnd()
+		);
+		var dateMutation;
+		var eventDefMutation;
+
+		if (origRange.getEnd().add(endDelta) > origRange.getStart()) {
+
+			dateMutation = new EventDefDateMutation();
+			dateMutation.setEndDelta(endDelta);
+
+			eventDefMutation = new EventDefMutation();
+			eventDefMutation.setDateMutation(dateMutation);
+
+			return eventDefMutation;
+		}
+
+		return false;
+	},
+
+
+	// Renders a visual indication of an event being resized.
+	// Must return elements used for any mock events.
+	renderEventResize: function(eventFootprints, seg) {
+		// subclasses must implement
+	},
+
+
+	// Unrenders a visual indication of an event being resized.
+	unrenderEventResize: function() {
+		// subclasses must implement
+	}
+
+});

+ 0 - 1275
src/common/Grid.events.js

@@ -1,1275 +0,0 @@
-
-/* Event-rendering and event-interaction methods for the abstract Grid class
-----------------------------------------------------------------------------------------------------------------------
-*/
-
-Grid.mixin({
-
-	// self-config, overridable by subclasses
-	segSelector: '.fc-event-container > *', // what constitutes an event element?
-
-	mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
-	isDraggingSeg: false, // is a segment being dragged? boolean
-	isResizingSeg: false, // is a segment being resized? boolean
-	isDraggingExternal: false, // jqui-dragging an external element? boolean
-	segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
-
-
-	renderEventsPayload: function(eventsPayload) {
-		var id, eventInstanceGroup;
-		var eventRenderRanges;
-		var eventFootprints;
-		var eventSegs;
-		var bgSegs = [];
-		var fgSegs = [];
-
-		for (id in eventsPayload) {
-			eventInstanceGroup = eventsPayload[id];
-
-			eventRenderRanges = eventInstanceGroup.sliceRenderRanges(this.view.activeUnzonedRange);
-			eventFootprints = this.eventRangesToEventFootprints(eventRenderRanges);
-			eventSegs = this.eventFootprintsToSegs(eventFootprints);
-
-			if (eventInstanceGroup.getEventDef().hasBgRendering()) {
-				bgSegs.push.apply(bgSegs, // append
-					eventSegs
-				);
-			}
-			else {
-				fgSegs.push.apply(fgSegs, // append
-					eventSegs
-				);
-			}
-		}
-
-		this.segs = [].concat( // record all segs
-			this.renderBgSegs(bgSegs) || bgSegs,
-			this.renderFgSegs(fgSegs) || fgSegs
-		);
-	},
-
-
-	// Unrenders all events currently rendered on the grid
-	unrenderEvents: function() {
-		this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
-		this.clearDragListeners();
-
-		this.unrenderFgSegs();
-		this.unrenderBgSegs();
-
-		this.segs = null;
-	},
-
-
-	// Retrieves all rendered segment objects currently rendered on the grid
-	getEventSegs: function() {
-		return this.segs || [];
-	},
-
-
-	// Background Segment Rendering
-	// ---------------------------------------------------------------------------------------------------------------
-	// TODO: move this to ChronoComponent, but without fill
-
-
-	// Renders the given background event segments onto the grid.
-	// Returns a subset of the segs that were actually rendered.
-	renderBgSegs: function(segs) {
-		return this.renderFill('bgEvent', segs);
-	},
-
-
-	// Unrenders all the currently rendered background event segments
-	unrenderBgSegs: function() {
-		this.unrenderFill('bgEvent');
-	},
-
-
-	// Renders a background event element, given the default rendering. Called by the fill system.
-	bgEventSegEl: function(seg, el) {
-		return this.filterEventRenderEl(seg.footprint, el);
-	},
-
-
-	// Generates an array of classNames to be used for the default rendering of a background event.
-	// Called by fillSegHtml.
-	bgEventSegClasses: function(seg) {
-		var eventDef = seg.footprint.eventDef;
-
-		return [ 'fc-bgevent' ].concat(
-			eventDef.className,
-			eventDef.source.className
-		);
-	},
-
-
-	// Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
-	// Called by fillSegHtml.
-	bgEventSegCss: function(seg) {
-		return {
-			'background-color': this.getSegSkinCss(seg)['background-color']
-		};
-	},
-
-
-	// Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
-	// Called by fillSegHtml.
-	businessHoursSegClasses: function(seg) {
-		return [ 'fc-nonbusiness', 'fc-bgevent' ];
-	},
-
-
-	/* Business Hours
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	// Compute business hour segs for the grid's current date range.
-	// Caller must ask if whole-day business hours are needed.
-	buildBusinessHourSegs: function(wholeDay) {
-		return this.eventFootprintsToSegs(
-			this.buildBusinessHourEventFootprints(wholeDay)
-		);
-	},
-
-
-	// Compute business hour *events* for the grid's current date range.
-	// Caller must ask if whole-day business hours are needed.
-	// FOR RENDERING
-	buildBusinessHourEventFootprints: function(wholeDay) {
-		var calendar = this.view.calendar;
-
-		return this._buildBusinessHourEventFootprints(wholeDay, calendar.opt('businessHours'));
-	},
-
-
-	_buildBusinessHourEventFootprints: function(wholeDay, businessHourDef) {
-		var calendar = this.view.calendar;
-		var eventInstanceGroup;
-		var eventRanges;
-
-		eventInstanceGroup = calendar.buildBusinessInstanceGroup(
-			wholeDay,
-			businessHourDef,
-			this.unzonedRange
-		);
-
-		if (eventInstanceGroup) {
-			eventRanges = eventInstanceGroup.sliceRenderRanges(
-				this.unzonedRange,
-				calendar
-			);
-		}
-		else {
-			eventRanges = [];
-		}
-
-		return this.eventRangesToEventFootprints(eventRanges);
-	},
-
-
-	/* Handlers
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	// Attaches event-element-related handlers for *all* rendered event segments of the view.
-	bindSegHandlers: function() {
-		this.bindSegHandlersToEl(this.el);
-	},
-
-
-	// Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
-	bindSegHandlersToEl: function(el) {
-		this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
-		this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
-		this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
-		this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
-		this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
-	},
-
-
-	// Executes a handler for any a user-interaction on a segment.
-	// Handler gets called with (seg, ev), and with the `this` context of the Grid
-	bindSegHandlerToEl: function(el, name, handler) {
-		var _this = this;
-
-		el.on(name, this.segSelector, function(ev) {
-			var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEventsPayload
-
-			// only call the handlers if there is not a drag/resize in progress
-			if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
-				return handler.call(_this, seg, ev); // context will be the Grid
-			}
-		});
-	},
-
-
-	handleSegClick: function(seg, ev) {
-		var res = this.publiclyTrigger('eventClick', { // can return `false` to cancel
-			context: seg.el[0],
-			args: [ seg.footprint.getEventLegacy(), ev, this.view ]
-		});
-
-		if (res === false) {
-			ev.preventDefault();
-		}
-	},
-
-
-	// Updates internal state and triggers handlers for when an event element is moused over
-	handleSegMouseover: function(seg, ev) {
-		if (
-			!GlobalEmitter.get().shouldIgnoreMouse() &&
-			!this.mousedOverSeg
-		) {
-			this.mousedOverSeg = seg;
-
-			if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
-				seg.el.addClass('fc-allow-mouse-resize');
-			}
-
-			this.publiclyTrigger('eventMouseover', {
-				context: seg.el[0],
-				args: [ seg.footprint.getEventLegacy(), ev, this.view ]
-			});
-		}
-	},
-
-
-	// Updates internal state and triggers handlers for when an event element is moused out.
-	// Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
-	handleSegMouseout: function(seg, ev) {
-		ev = ev || {}; // if given no args, make a mock mouse event
-
-		if (this.mousedOverSeg) {
-			seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
-			this.mousedOverSeg = null;
-
-			if (this.view.isEventDefResizable(seg.footprint.eventDef)) {
-				seg.el.removeClass('fc-allow-mouse-resize');
-			}
-
-			this.publiclyTrigger('eventMouseout', {
-				context: seg.el[0],
-				args: [ seg.footprint.getEventLegacy(), ev, this.view ]
-			});
-		}
-	},
-
-
-	handleSegMousedown: function(seg, ev) {
-		var isResizing = this.startSegResize(seg, ev, { distance: 5 });
-
-		if (!isResizing && this.view.isEventDefDraggable(seg.footprint.eventDef)) {
-			this.buildSegDragListener(seg)
-				.startInteraction(ev, {
-					distance: 5
-				});
-		}
-	},
-
-
-	handleSegTouchStart: function(seg, ev) {
-		var view = this.view;
-		var eventDef = seg.footprint.eventDef;
-		var isSelected = view.isEventDefSelected(eventDef);
-		var isDraggable = view.isEventDefDraggable(eventDef);
-		var isResizable = view.isEventDefResizable(eventDef);
-		var isResizing = false;
-		var dragListener;
-		var eventLongPressDelay;
-
-		if (isSelected && isResizable) {
-			// only allow resizing of the event is selected
-			isResizing = this.startSegResize(seg, ev);
-		}
-
-		if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
-
-			eventLongPressDelay = this.opt('eventLongPressDelay');
-			if (eventLongPressDelay == null) {
-				eventLongPressDelay = this.opt('longPressDelay'); // fallback
-			}
-
-			dragListener = isDraggable ?
-				this.buildSegDragListener(seg) :
-				this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
-
-			dragListener.startInteraction(ev, { // won't start if already started
-				delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
-			});
-		}
-	},
-
-
-	// returns boolean whether resizing actually started or not.
-	// assumes the seg allows resizing.
-	// `dragOptions` are optional.
-	startSegResize: function(seg, ev, dragOptions) {
-		if ($(ev.target).is('.fc-resizer')) {
-			this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
-				.startInteraction(ev, dragOptions);
-			return true;
-		}
-		return false;
-	},
-
-
-
-	/* Event Dragging
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	// Builds a listener that will track user-dragging on an event segment.
-	// Generic enough to work with any type of Grid.
-	// Has side effect of setting/unsetting `segDragListener`
-	buildSegDragListener: function(seg) {
-		var _this = this;
-		var view = this.view;
-		var calendar = view.calendar;
-		var eventManager = calendar.eventManager;
-		var el = seg.el;
-		var eventDef = seg.footprint.eventDef;
-		var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
-		var isDragging;
-		var mouseFollower; // A clone of the original element that will move with the mouse
-		var eventDefMutation;
-
-		if (this.segDragListener) {
-			return this.segDragListener;
-		}
-
-		// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
-		// of the view.
-		var dragListener = this.segDragListener = new HitDragListener(view, {
-			scroll: this.opt('dragScroll'),
-			subjectEl: el,
-			subjectCenter: true,
-			interactionStart: function(ev) {
-				seg.component = _this; // for renderDrag
-				isDragging = false;
-				mouseFollower = new MouseFollower(seg.el, {
-					additionalClass: 'fc-dragging',
-					parentEl: view.el,
-					opacity: dragListener.isTouch ? null : _this.opt('dragOpacity'),
-					revertDuration: _this.opt('dragRevertDuration'),
-					zIndex: 2 // one above the .fc-view
-				});
-				mouseFollower.hide(); // don't show until we know this is a real drag
-				mouseFollower.start(ev);
-			},
-			dragStart: function(ev) {
-				if (
-					dragListener.isTouch &&
-					!view.isEventDefSelected(eventDef) &&
-					eventInstance
-				) {
-					// if not previously selected, will fire after a delay. then, select the event
-					view.selectEventInstance(eventInstance);
-				}
-				isDragging = true;
-				_this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
-				_this.segDragStart(seg, ev);
-				view.hideEventsWithId(eventDef.id); // hide all event segments. our mouseFollower will take over
-			},
-			hitOver: function(hit, isOrig, origHit) {
-				var isAllowed = true;
-				var origFootprint;
-				var footprint;
-				var mutatedEventInstanceGroup;
-				var dragHelperEls;
-
-				// starting hit could be forced (DayGrid.limit)
-				if (seg.hit) {
-					origHit = seg.hit;
-				}
-
-				// hit might not belong to this grid, so query origin grid
-				origFootprint = origHit.component.getSafeHitFootprint(origHit);
-				footprint = hit.component.getSafeHitFootprint(hit);
-
-				if (origFootprint && footprint) {
-					eventDefMutation = _this.computeEventDropMutation(origFootprint, footprint, eventDef);
-
-					if (eventDefMutation) {
-						mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
-							eventDef.id,
-							eventDefMutation
-						);
-						isAllowed = _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
-					}
-					else {
-						isAllowed = false;
-					}
-				}
-				else {
-					isAllowed = false;
-				}
-
-				if (!isAllowed) {
-					eventDefMutation = null;
-					disableCursor();
-				}
-
-				// if a valid drop location, have the subclass render a visual indication
-				if (
-					eventDefMutation &&
-					(dragHelperEls = view.renderDrag(
-						_this.eventRangesToEventFootprints(
-							mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, calendar)
-						),
-						seg
-					))
-				) {
-					dragHelperEls.addClass('fc-dragging');
-					if (!dragListener.isTouch) {
-						_this.applyDragOpacity(dragHelperEls);
-					}
-
-					mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
-				}
-				else {
-					mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
-				}
-
-				if (isOrig) {
-					// needs to have moved hits to be a valid drop
-					eventDefMutation = null;
-				}
-			},
-			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
-				view.unrenderDrag(); // unrender whatever was done in renderDrag
-				mouseFollower.show(); // show in case we are moving out of all hits
-				eventDefMutation = null;
-			},
-			hitDone: function() { // Called after a hitOut OR before a dragEnd
-				enableCursor();
-			},
-			interactionEnd: function(ev) {
-				delete seg.component; // prevent side effects
-
-				// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
-				mouseFollower.stop(!eventDefMutation, function() {
-					if (isDragging) {
-						view.unrenderDrag();
-						_this.segDragStop(seg, ev);
-					}
-
-					if (eventDefMutation) {
-						// no need to re-show original, will rerender all anyways. esp important if eventRenderWait
-						view.reportEventDrop(eventInstance, eventDefMutation, el, ev);
-					}
-					else {
-						view.showEventsWithId(eventDef.id);
-					}
-				});
-				_this.segDragListener = null;
-			}
-		});
-
-		return dragListener;
-	},
-
-
-	// seg isn't draggable, but let's use a generic DragListener
-	// simply for the delay, so it can be selected.
-	// Has side effect of setting/unsetting `segDragListener`
-	buildSegSelectListener: function(seg) {
-		var _this = this;
-		var view = this.view;
-		var eventDef = seg.footprint.eventDef;
-		var eventInstance = seg.footprint.eventInstance; // null for inverse-background events
-
-		if (this.segDragListener) {
-			return this.segDragListener;
-		}
-
-		var dragListener = this.segDragListener = new DragListener({
-			dragStart: function(ev) {
-				if (
-					dragListener.isTouch &&
-					!view.isEventDefSelected(eventDef) &&
-					eventInstance
-				) {
-					// if not previously selected, will fire after a delay. then, select the event
-					view.selectEventInstance(eventInstance);
-				}
-			},
-			interactionEnd: function(ev) {
-				_this.segDragListener = null;
-			}
-		});
-
-		return dragListener;
-	},
-
-
-	// Called before event segment dragging starts
-	segDragStart: function(seg, ev) {
-		this.isDraggingSeg = true;
-		this.publiclyTrigger('eventDragStart', {
-			context: seg.el[0],
-			args: [
-				seg.footprint.getEventLegacy(),
-				ev,
-				{}, // jqui dummy
-				this.view
-			]
-		});
-	},
-
-
-	// Called after event segment dragging stops
-	segDragStop: function(seg, ev) {
-		this.isDraggingSeg = false;
-		this.publiclyTrigger('eventDragStop', {
-			context: seg.el[0],
-			args: [
-				seg.footprint.getEventLegacy(),
-				ev,
-				{}, // jqui dummy
-				this.view
-			]
-		});
-	},
-
-
-	// DOES NOT consider overlap/constraint
-	computeEventDropMutation: function(startFootprint, endFootprint, eventDef) {
-		var date0 = startFootprint.unzonedRange.getStart();
-		var date1 = endFootprint.unzonedRange.getStart();
-		var clearEnd = false;
-		var forceTimed = false;
-		var forceAllDay = false;
-		var dateDelta;
-		var dateMutation;
-		var eventDefMutation;
-
-		if (startFootprint.isAllDay !== endFootprint.isAllDay) {
-			clearEnd = true;
-
-			if (endFootprint.isAllDay) {
-				forceAllDay = true;
-				date0.stripTime();
-			}
-			else {
-				forceTimed = true;
-			}
-		}
-
-		dateDelta = this.diffDates(date1, date0);
-
-		dateMutation = new EventDefDateMutation();
-		dateMutation.clearEnd = clearEnd;
-		dateMutation.forceTimed = forceTimed;
-		dateMutation.forceAllDay = forceAllDay;
-		dateMutation.setDateDelta(dateDelta);
-
-		eventDefMutation = new EventDefMutation();
-		eventDefMutation.setDateMutation(dateMutation);
-
-		return eventDefMutation;
-	},
-
-
-	// Utility for apply dragOpacity to a jQuery set
-	applyDragOpacity: function(els) {
-		var opacity = this.opt('dragOpacity');
-
-		if (opacity != null) {
-			els.css('opacity', opacity);
-		}
-	},
-
-
-	/* External Element Dragging
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	// Called when a jQuery UI drag is initiated anywhere in the DOM
-	externalDragStart: function(ev, ui) {
-		var el;
-		var accept;
-
-		if (this.opt('droppable')) { // only listen if this setting is on
-			el = $((ui ? ui.item : null) || ev.target);
-
-			// Test that the dragged element passes the dropAccept selector or filter function.
-			// FYI, the default is "*" (matches all)
-			accept = this.opt('dropAccept');
-			if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
-				if (!this.isDraggingExternal) { // prevent double-listening if fired twice
-					this.listenToExternalDrag(el, ev, ui);
-				}
-			}
-		}
-	},
-
-
-	// Called when a jQuery UI drag starts and it needs to be monitored for dropping
-	listenToExternalDrag: function(el, ev, ui) {
-		var _this = this;
-		var view = this.view;
-		var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
-		var singleEventDef; // a null value signals an unsuccessful drag
-
-		// listener that tracks mouse movement over date-associated pixel regions
-		var dragListener = _this.externalDragListener = new HitDragListener(this, {
-			interactionStart: function() {
-				_this.isDraggingExternal = true;
-			},
-			hitOver: function(hit) {
-				var isAllowed = true;
-				var hitFootprint = hit.component.getSafeHitFootprint(hit); // hit might not belong to this grid
-				var mutatedEventInstanceGroup;
-
-				if (hitFootprint) {
-					singleEventDef = _this.computeExternalDrop(hitFootprint, meta);
-
-					if (singleEventDef) {
-						mutatedEventInstanceGroup = new EventInstanceGroup(
-							singleEventDef.buildInstances()
-						);
-						isAllowed = meta.eventProps ? // isEvent?
-							_this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup) :
-							_this.isExternalInstanceGroupAllowed(mutatedEventInstanceGroup);
-					}
-					else {
-						isAllowed = false;
-					}
-				}
-				else {
-					isAllowed = false;
-				}
-
-				if (!isAllowed) {
-					singleEventDef = null;
-					disableCursor();
-				}
-
-				if (singleEventDef) {
-					_this.renderDrag( // called without a seg parameter
-						_this.eventRangesToEventFootprints(
-							mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, view.calendar)
-						)
-					);
-				}
-			},
-			hitOut: function() {
-				singleEventDef = null; // signal unsuccessful
-			},
-			hitDone: function() { // Called after a hitOut OR before a dragEnd
-				enableCursor();
-				_this.unrenderDrag();
-			},
-			interactionEnd: function(ev) {
-
-				if (singleEventDef) { // element was dropped on a valid hit
-					view.reportExternalDrop(
-						singleEventDef,
-						Boolean(meta.eventProps), // isEvent
-						Boolean(meta.stick), // isSticky
-						el, ev, ui
-					);
-				}
-
-				_this.isDraggingExternal = false;
-				_this.externalDragListener = null;
-			}
-		});
-
-		dragListener.startDrag(ev); // start listening immediately
-	},
-
-
-	// Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
-	// returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
-	// Returning a null value signals an invalid drop hit.
-	// DOES NOT consider overlap/constraint.
-	// Assumes both footprints are non-open-ended.
-	computeExternalDrop: function(componentFootprint, meta) {
-		var calendar = this.view.calendar;
-		var start = FC.moment.utc(componentFootprint.unzonedRange.startMs).stripZone();
-		var end;
-		var eventDef;
-
-		if (componentFootprint.isAllDay) {
-			// if dropped on an all-day span, and element's metadata specified a time, set it
-			if (meta.startTime) {
-				start.time(meta.startTime);
-			}
-			else {
-				start.stripTime();
-			}
-		}
-
-		if (meta.duration) {
-			end = start.clone().add(meta.duration);
-		}
-
-		start = calendar.applyTimezone(start);
-
-		if (end) {
-			end = calendar.applyTimezone(end);
-		}
-
-		eventDef = SingleEventDef.parse(
-			$.extend({}, meta.eventProps, {
-				start: start,
-				end: end
-			}),
-			new EventSource(calendar)
-		);
-
-		return eventDef;
-	},
-
-
-	/* Resizing
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	// Creates a listener that tracks the user as they resize an event segment.
-	// Generic enough to work with any type of Grid.
-	buildSegResizeListener: function(seg, isStart) {
-		var _this = this;
-		var view = this.view;
-		var calendar = view.calendar;
-		var eventManager = calendar.eventManager;
-		var el = seg.el;
-		var eventDef = seg.footprint.eventDef;
-		var eventInstance = seg.footprint.eventInstance;
-		var isDragging;
-		var resizeMutation; // zoned event date properties. falsy if invalid resize
-
-		// Tracks mouse movement over the *grid's* coordinate map
-		var dragListener = this.segResizeListener = new HitDragListener(this, {
-			scroll: this.opt('dragScroll'),
-			subjectEl: el,
-			interactionStart: function() {
-				isDragging = false;
-			},
-			dragStart: function(ev) {
-				isDragging = true;
-				_this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
-				_this.segResizeStart(seg, ev);
-			},
-			hitOver: function(hit, isOrig, origHit) {
-				var isAllowed = true;
-				var origHitFootprint = _this.getSafeHitFootprint(origHit);
-				var hitFootprint = _this.getSafeHitFootprint(hit);
-				var mutatedEventInstanceGroup;
-
-				if (origHitFootprint && hitFootprint) {
-					resizeMutation = isStart ?
-						_this.computeEventStartResizeMutation(origHitFootprint, hitFootprint, seg.footprint) :
-						_this.computeEventEndResizeMutation(origHitFootprint, hitFootprint, seg.footprint);
-
-					if (resizeMutation) {
-						mutatedEventInstanceGroup = eventManager.buildMutatedEventInstanceGroup(
-							eventDef.id,
-							resizeMutation
-						);
-						isAllowed = _this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup);
-					}
-					else {
-						isAllowed = false;
-					}
-				}
-				else {
-					isAllowed = false;
-				}
-
-				if (!isAllowed) {
-					resizeMutation = null;
-					disableCursor();
-				}
-				else if (resizeMutation.isEmpty()) {
-					// no change. (FYI, event dates might have zones)
-					resizeMutation = null;
-				}
-
-				if (resizeMutation) {
-					view.hideEventsWithId(eventDef.id);
-
-					_this.renderEventResize(
-						_this.eventRangesToEventFootprints(
-							mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, calendar)
-						),
-						seg
-					);
-				}
-			},
-			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
-				resizeMutation = null;
-				view.showEventsWithId(eventDef.id); // for when out-of-bounds. show original
-			},
-			hitDone: function() { // resets the rendering to show the original event
-				_this.unrenderEventResize();
-				enableCursor();
-			},
-			interactionEnd: function(ev) {
-				if (isDragging) {
-					_this.segResizeStop(seg, ev);
-				}
-
-				if (resizeMutation) { // valid date to resize to?
-					// no need to re-show original, will rerender all anyways. esp important if eventRenderWait
-					view.reportEventResize(eventInstance, resizeMutation, el, ev);
-				}
-				else {
-					view.showEventsWithId(eventDef.id);
-				}
-				_this.segResizeListener = null;
-			}
-		});
-
-		return dragListener;
-	},
-
-
-	// Called before event segment resizing starts
-	segResizeStart: function(seg, ev) {
-		this.isResizingSeg = true;
-		this.publiclyTrigger('eventResizeStart', {
-			context: seg.el[0],
-			args: [
-				seg.footprint.getEventLegacy(),
-				ev,
-				{}, // jqui dummy
-				this.view
-			]
-		});
-	},
-
-
-	// Called after event segment resizing stops
-	segResizeStop: function(seg, ev) {
-		this.isResizingSeg = false;
-		this.publiclyTrigger('eventResizeStop', {
-			context: seg.el[0],
-			args: [
-				seg.footprint.getEventLegacy(),
-				ev,
-				{}, // jqui dummy
-				this.view
-			]
-		});
-	},
-
-
-	// Returns new date-information for an event segment being resized from its start
-	computeEventStartResizeMutation: function(startFootprint, endFootprint, origEventFootprint) {
-		var origRange = origEventFootprint.componentFootprint.unzonedRange;
-		var startDelta = this.diffDates(
-			endFootprint.unzonedRange.getStart(),
-			startFootprint.unzonedRange.getStart()
-		);
-		var dateMutation;
-		var eventDefMutation;
-
-		if (origRange.getStart().add(startDelta) < origRange.getEnd()) {
-
-			dateMutation = new EventDefDateMutation();
-			dateMutation.setStartDelta(startDelta);
-
-			eventDefMutation = new EventDefMutation();
-			eventDefMutation.setDateMutation(dateMutation);
-
-			return eventDefMutation;
-		}
-
-		return false;
-	},
-
-
-	// Returns new date-information for an event segment being resized from its end
-	computeEventEndResizeMutation: function(startFootprint, endFootprint, origEventFootprint) {
-		var origRange = origEventFootprint.componentFootprint.unzonedRange;
-		var endDelta = this.diffDates(
-			endFootprint.unzonedRange.getEnd(),
-			startFootprint.unzonedRange.getEnd()
-		);
-		var dateMutation;
-		var eventDefMutation;
-
-		if (origRange.getEnd().add(endDelta) > origRange.getStart()) {
-
-			dateMutation = new EventDefDateMutation();
-			dateMutation.setEndDelta(endDelta);
-
-			eventDefMutation = new EventDefMutation();
-			eventDefMutation.setDateMutation(dateMutation);
-
-			return eventDefMutation;
-		}
-
-		return false;
-	},
-
-
-	// Renders a visual indication of an event being resized.
-	// Must return elements used for any mock events.
-	renderEventResize: function(eventFootprints, seg) {
-		// subclasses must implement
-	},
-
-
-	// Unrenders a visual indication of an event being resized.
-	unrenderEventResize: function() {
-		// subclasses must implement
-	},
-
-
-	/* Rendering Utils
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	// Compute the text that should be displayed on an event's element.
-	// `range` can be the Event object itself, or something range-like, with at least a `start`.
-	// If event times are disabled, or the event has no time, will return a blank string.
-	// If not specified, formatStr will default to the eventTimeFormat setting,
-	// and displayEnd will default to the displayEventEnd setting.
-	getEventTimeText: function(eventFootprint, formatStr, displayEnd) {
-		return this._getEventTimeText(
-			eventFootprint.eventInstance.dateProfile.start,
-			eventFootprint.eventInstance.dateProfile.end,
-			eventFootprint.componentFootprint.isAllDay,
-			formatStr,
-			displayEnd
-		);
-	},
-
-
-	_getEventTimeText: function(start, end, isAllDay, formatStr, displayEnd) {
-
-		if (formatStr == null) {
-			formatStr = this.eventTimeFormat;
-		}
-
-		if (displayEnd == null) {
-			displayEnd = this.displayEventEnd;
-		}
-
-		if (this.displayEventTime && !isAllDay) {
-			if (displayEnd && end) {
-				return this.view.formatRange(
-					{ start: start, end: end },
-					false, // allDay
-					formatStr
-				);
-			}
-			else {
-				return start.format(formatStr);
-			}
-		}
-
-		return '';
-	},
-
-
-	// Generic utility for generating the HTML classNames for an event segment's element
-	getSegClasses: function(seg, isDraggable, isResizable) {
-		var view = this.view;
-		var classes = [
-			'fc-event',
-			seg.isStart ? 'fc-start' : 'fc-not-start',
-			seg.isEnd ? 'fc-end' : 'fc-not-end'
-		].concat(this.getSegCustomClasses(seg));
-
-		if (isDraggable) {
-			classes.push('fc-draggable');
-		}
-		if (isResizable) {
-			classes.push('fc-resizable');
-		}
-
-		// event is currently selected? attach a className.
-		if (view.isEventDefSelected(seg.footprint.eventDef)) {
-			classes.push('fc-selected');
-		}
-
-		return classes;
-	},
-
-
-	// List of classes that were defined by the caller of the API in some way
-	getSegCustomClasses: function(seg) {
-		var eventDef = seg.footprint.eventDef;
-
-		return [].concat(
-			eventDef.className, // guaranteed to be an array
-			eventDef.source.className
-		);
-	},
-
-
-	// Utility for generating event skin-related CSS properties
-	getSegSkinCss: function(seg) {
-		return {
-			'background-color': this.getSegBackgroundColor(seg),
-			'border-color': this.getSegBorderColor(seg),
-			color: this.getSegTextColor(seg)
-		};
-	},
-
-
-	// Queries for caller-specified color, then falls back to default
-	getSegBackgroundColor: function(seg) {
-		var eventDef = seg.footprint.eventDef;
-
-		return eventDef.backgroundColor ||
-			eventDef.color ||
-			this.getSegDefaultBackgroundColor(seg);
-	},
-
-
-	getSegDefaultBackgroundColor: function(seg) {
-		var source = seg.footprint.eventDef.source;
-
-		return source.backgroundColor ||
-			source.color ||
-			this.opt('eventBackgroundColor') ||
-			this.opt('eventColor');
-	},
-
-
-	// Queries for caller-specified color, then falls back to default
-	getSegBorderColor: function(seg) {
-		var eventDef = seg.footprint.eventDef;
-
-		return eventDef.borderColor ||
-			eventDef.color ||
-			this.getSegDefaultBorderColor(seg);
-	},
-
-
-	getSegDefaultBorderColor: function(seg) {
-		var source = seg.footprint.eventDef.source;
-
-		return source.borderColor ||
-			source.color ||
-			this.opt('eventBorderColor') ||
-			this.opt('eventColor');
-	},
-
-
-	// Queries for caller-specified color, then falls back to default
-	getSegTextColor: function(seg) {
-		var eventDef = seg.footprint.eventDef;
-
-		return eventDef.textColor ||
-			this.getSegDefaultTextColor(seg);
-	},
-
-
-	getSegDefaultTextColor: function(seg) {
-		var source = seg.footprint.eventDef.source;
-
-		return source.textColor ||
-			this.opt('eventTextColor');
-	},
-
-
-	/* Event Location Validation
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	isEventInstanceGroupAllowed: function(eventInstanceGroup) {
-		var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
-		var i;
-
-		for (i = 0; i < eventFootprints.length; i++) {
-			if (!this.view.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
-				return false;
-			}
-		}
-
-		return this.view.calendar.isEventInstanceGroupAllowed(eventInstanceGroup);
-	},
-
-
-	// when it's a completely anonymous external drag, no event.
-	isExternalInstanceGroupAllowed: function(eventInstanceGroup) {
-		var calendar = this.view.calendar;
-		var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
-		var i;
-
-		for (i = 0; i < eventFootprints.length; i++) {
-			if (!this.view.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
-				return false;
-			}
-		}
-
-		for (i = 0; i < eventFootprints.length; i++) {
-			// treat it as a selection
-			if (!calendar.isSelectionFootprintAllowed(eventFootprints[i].componentFootprint)) {
-				return false;
-			}
-		}
-
-		return true;
-	},
-
-
-	/* Converting eventRange -> eventFootprint -> eventSegs
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	eventRangesToEventFootprints: function(eventRanges) {
-		var eventFootprints = [];
-		var i;
-
-		for (i = 0; i < eventRanges.length; i++) {
-			eventFootprints.push.apply(eventFootprints,
-				this.eventRangeToEventFootprints(eventRanges[i])
-			);
-		}
-
-		return eventFootprints;
-	},
-
-
-	// Given an event's unzoned date range, return an array of eventSpan objects.
-	// eventSpan - { start, end, isStart, isEnd, otherthings... }
-	// Subclasses can override.
-	// Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans.
-	// TODO: somehow more DRY with Calendar::eventRangeToEventFootprints
-	eventRangeToEventFootprints: function(eventRange) {
-		return [
-			new EventFootprint(
-				new ComponentFootprint(
-					eventRange.unzonedRange,
-					eventRange.eventDef.isAllDay()
-				),
-				eventRange.eventDef,
-				eventRange.eventInstance // might not exist
-			)
-		];
-	},
-
-
-	eventFootprintsToSegs: function(eventFootprints) {
-		var segs = [];
-		var i;
-
-		for (i = 0; i < eventFootprints.length; i++) {
-			segs.push.apply(segs,
-				this.eventFootprintToSegs(eventFootprints[i])
-			);
-		}
-
-		return segs;
-	},
-
-
-	// Given an event's span (unzoned start/end and other misc data), and the event itself,
-	// slices into segments and attaches event-derived properties to them.
-	// eventSpan - { start, end, isStart, isEnd, otherthings... }
-	// constraintRange allow additional clipping. optional. eventually remove this.
-	eventFootprintToSegs: function(eventFootprint, constraintRange) {
-		var unzonedRange = eventFootprint.componentFootprint.unzonedRange;
-		var segs;
-		var i, seg;
-
-		if (constraintRange) {
-			unzonedRange = unzonedRange.intersect(constraintRange);
-		}
-
-		segs = this.componentFootprintToSegs(eventFootprint.componentFootprint);
-
-		for (i = 0; i < segs.length; i++) {
-			seg = segs[i];
-
-			if (!unzonedRange.isStart) {
-				seg.isStart = false;
-			}
-			if (!unzonedRange.isEnd) {
-				seg.isEnd = false;
-			}
-
-			seg.footprint = eventFootprint;
-			// TODO: rename to seg.eventFootprint
-		}
-
-		return segs;
-	},
-
-
-	sortEventSegs: function(segs) {
-		segs.sort(proxy(this, 'compareEventSegs'));
-	},
-
-
-	// A cmp function for determining which segments should take visual priority
-	compareEventSegs: function(seg1, seg2) {
-		var f1 = seg1.footprint.componentFootprint;
-		var r1 = f1.unzonedRange;
-		var f2 = seg2.footprint.componentFootprint;
-		var r2 = f2.unzonedRange;
-
-		return r1.startMs - r2.startMs || // earlier events go first
-			(r2.endMs - r2.startMs) - (r1.endMs - r1.startMs) || // tie? longer events go first
-			f2.isAllDay - f1.isAllDay || // tie? put all-day events first (booleans cast to 0/1)
-			compareByFieldSpecs(
-				seg1.footprint.eventDef,
-				seg2.footprint.eventDef,
-				this.view.eventOrderSpecs
-			);
-	}
-
-});
-
-
-/* External-Dragging-Element Data
-----------------------------------------------------------------------------------------------------------------------*/
-
-// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
-// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
-FC.dataAttrPrefix = '';
-
-// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
-// to be used for Event Object creation.
-// A defined `.eventProps`, even when empty, indicates that an event should be created.
-function getDraggedElMeta(el) {
-	var prefix = FC.dataAttrPrefix;
-	var eventProps; // properties for creating the event, not related to date/time
-	var startTime; // a Duration
-	var duration;
-	var stick;
-
-	if (prefix) { prefix += '-'; }
-	eventProps = el.data(prefix + 'event') || null;
-
-	if (eventProps) {
-		if (typeof eventProps === 'object') {
-			eventProps = $.extend({}, eventProps); // make a copy
-		}
-		else { // something like 1 or true. still signal event creation
-			eventProps = {};
-		}
-
-		// pluck special-cased date/time properties
-		startTime = eventProps.start;
-		if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
-		duration = eventProps.duration;
-		stick = eventProps.stick;
-		delete eventProps.start;
-		delete eventProps.time;
-		delete eventProps.duration;
-		delete eventProps.stick;
-	}
-
-	// fallback to standalone attribute values for each of the date/time properties
-	if (startTime == null) { startTime = el.data(prefix + 'start'); }
-	if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
-	if (duration == null) { duration = el.data(prefix + 'duration'); }
-	if (stick == null) { stick = el.data(prefix + 'stick'); }
-
-	// massage into correct data types
-	startTime = startTime != null ? moment.duration(startTime) : null;
-	duration = duration != null ? moment.duration(duration) : null;
-	stick = Boolean(stick);
-
-	return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
-}
-

+ 227 - 0
src/common/Grid.external-dropping.js

@@ -0,0 +1,227 @@
+
+/*
+Wired up via Grid.js by calling
+externalDragStart
+*/
+Grid.mixin({
+
+	isDraggingExternal: false, // jqui-dragging an external element? boolean
+
+
+	// Called when a jQuery UI drag is initiated anywhere in the DOM
+	externalDragStart: function(ev, ui) {
+		var el;
+		var accept;
+
+		if (this.opt('droppable')) { // only listen if this setting is on
+			el = $((ui ? ui.item : null) || ev.target);
+
+			// Test that the dragged element passes the dropAccept selector or filter function.
+			// FYI, the default is "*" (matches all)
+			accept = this.opt('dropAccept');
+			if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
+				if (!this.isDraggingExternal) { // prevent double-listening if fired twice
+					this.listenToExternalDrag(el, ev, ui);
+				}
+			}
+		}
+	},
+
+
+	// Called when a jQuery UI drag starts and it needs to be monitored for dropping
+	listenToExternalDrag: function(el, ev, ui) {
+		var _this = this;
+		var view = this.view;
+		var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
+		var singleEventDef; // a null value signals an unsuccessful drag
+
+		// listener that tracks mouse movement over date-associated pixel regions
+		var dragListener = _this.externalDragListener = new HitDragListener(this, {
+			interactionStart: function() {
+				_this.isDraggingExternal = true;
+			},
+			hitOver: function(hit) {
+				var isAllowed = true;
+				var hitFootprint = hit.component.getSafeHitFootprint(hit); // hit might not belong to this grid
+				var mutatedEventInstanceGroup;
+
+				if (hitFootprint) {
+					singleEventDef = _this.computeExternalDrop(hitFootprint, meta);
+
+					if (singleEventDef) {
+						mutatedEventInstanceGroup = new EventInstanceGroup(
+							singleEventDef.buildInstances()
+						);
+						isAllowed = meta.eventProps ? // isEvent?
+							_this.isEventInstanceGroupAllowed(mutatedEventInstanceGroup) :
+							_this.isExternalInstanceGroupAllowed(mutatedEventInstanceGroup);
+					}
+					else {
+						isAllowed = false;
+					}
+				}
+				else {
+					isAllowed = false;
+				}
+
+				if (!isAllowed) {
+					singleEventDef = null;
+					disableCursor();
+				}
+
+				if (singleEventDef) {
+					_this.renderDrag( // called without a seg parameter
+						_this.eventRangesToEventFootprints(
+							mutatedEventInstanceGroup.sliceRenderRanges(_this.unzonedRange, view.calendar)
+						)
+					);
+				}
+			},
+			hitOut: function() {
+				singleEventDef = null; // signal unsuccessful
+			},
+			hitDone: function() { // Called after a hitOut OR before a dragEnd
+				enableCursor();
+				_this.unrenderDrag();
+			},
+			interactionEnd: function(ev) {
+
+				if (singleEventDef) { // element was dropped on a valid hit
+					view.reportExternalDrop(
+						singleEventDef,
+						Boolean(meta.eventProps), // isEvent
+						Boolean(meta.stick), // isSticky
+						el, ev, ui
+					);
+				}
+
+				_this.isDraggingExternal = false;
+				_this.externalDragListener = null;
+			}
+		});
+
+		dragListener.startDrag(ev); // start listening immediately
+	},
+
+
+	// Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
+	// returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
+	// Returning a null value signals an invalid drop hit.
+	// DOES NOT consider overlap/constraint.
+	// Assumes both footprints are non-open-ended.
+	computeExternalDrop: function(componentFootprint, meta) {
+		var calendar = this.view.calendar;
+		var start = FC.moment.utc(componentFootprint.unzonedRange.startMs).stripZone();
+		var end;
+		var eventDef;
+
+		if (componentFootprint.isAllDay) {
+			// if dropped on an all-day span, and element's metadata specified a time, set it
+			if (meta.startTime) {
+				start.time(meta.startTime);
+			}
+			else {
+				start.stripTime();
+			}
+		}
+
+		if (meta.duration) {
+			end = start.clone().add(meta.duration);
+		}
+
+		start = calendar.applyTimezone(start);
+
+		if (end) {
+			end = calendar.applyTimezone(end);
+		}
+
+		eventDef = SingleEventDef.parse(
+			$.extend({}, meta.eventProps, {
+				start: start,
+				end: end
+			}),
+			new EventSource(calendar)
+		);
+
+		return eventDef;
+	},
+
+
+	// NOTE: very similar to isEventInstanceGroupAllowed
+	// when it's a completely anonymous external drag, no event.
+	isExternalInstanceGroupAllowed: function(eventInstanceGroup) {
+		var calendar = this.view.calendar;
+		var eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges());
+		var i;
+
+		for (i = 0; i < eventFootprints.length; i++) {
+			if (!this.view.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
+				return false;
+			}
+		}
+
+		for (i = 0; i < eventFootprints.length; i++) {
+			// treat it as a selection
+			if (!calendar.isSelectionFootprintAllowed(eventFootprints[i].componentFootprint)) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+});
+
+
+/* External-Dragging-Element Data
+----------------------------------------------------------------------------------------------------------------------*/
+
+// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
+// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
+FC.dataAttrPrefix = '';
+
+// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
+// to be used for Event Object creation.
+// A defined `.eventProps`, even when empty, indicates that an event should be created.
+function getDraggedElMeta(el) {
+	var prefix = FC.dataAttrPrefix;
+	var eventProps; // properties for creating the event, not related to date/time
+	var startTime; // a Duration
+	var duration;
+	var stick;
+
+	if (prefix) { prefix += '-'; }
+	eventProps = el.data(prefix + 'event') || null;
+
+	if (eventProps) {
+		if (typeof eventProps === 'object') {
+			eventProps = $.extend({}, eventProps); // make a copy
+		}
+		else { // something like 1 or true. still signal event creation
+			eventProps = {};
+		}
+
+		// pluck special-cased date/time properties
+		startTime = eventProps.start;
+		if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
+		duration = eventProps.duration;
+		stick = eventProps.stick;
+		delete eventProps.start;
+		delete eventProps.time;
+		delete eventProps.duration;
+		delete eventProps.stick;
+	}
+
+	// fallback to standalone attribute values for each of the date/time properties
+	if (startTime == null) { startTime = el.data(prefix + 'start'); }
+	if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
+	if (duration == null) { duration = el.data(prefix + 'duration'); }
+	if (stick == null) { stick = el.data(prefix + 'stick'); }
+
+	// massage into correct data types
+	startTime = startTime != null ? moment.duration(startTime) : null;
+	duration = duration != null ? moment.duration(duration) : null;
+	stick = Boolean(stick);
+
+	return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
+}

+ 100 - 89
src/common/Grid.js

@@ -1,6 +1,14 @@
 
 
 /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
 /* An abstract class comprised of a "grid" of areas that each represent a specific datetime
-----------------------------------------------------------------------------------------------------------------------*/
+----------------------------------------------------------------------------------------------------------------------
+Contains:
+- day click
+- selection
+- hit system
+- range->footprint->seg pipeline
+- initializing mouse/touch handlers for everything
+- initializing event rendering-related options
+*/
 
 
 var Grid = FC.Grid = ChronoComponent.extend({
 var Grid = FC.Grid = ChronoComponent.extend({
 
 
@@ -12,15 +20,7 @@ var Grid = FC.Grid = ChronoComponent.extend({
 
 
 	unzonedRange: null,
 	unzonedRange: null,
 
 
-	// derived from options
-	eventTimeFormat: null,
-	displayEventTime: null,
-	displayEventEnd: null,
-
-	// if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
-	// of the date areas. if not defined, assumes to be day and time granularity.
-	// TODO: port isTimeScale into same system?
-	largeUnit: null,
+	hitsNeededDepth: 0, // necessary because multiple callers might need the same hits
 
 
 	dayClickListener: null,
 	dayClickListener: null,
 	daySelectListener: null,
 	daySelectListener: null,
@@ -46,29 +46,6 @@ var Grid = FC.Grid = ChronoComponent.extend({
 	},
 	},
 
 
 
 
-	/* Options
-	------------------------------------------------------------------------------------------------------------------*/
-
-
-	// Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
-	computeEventTimeFormat: function() {
-		return this.opt('smallTimeFormat');
-	},
-
-
-	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
-	// Only applies to non-all-day events.
-	computeDisplayEventTime: function() {
-		return true;
-	},
-
-
-	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
-	computeDisplayEventEnd: function() {
-		return true;
-	},
-
-
 	/* Dates
 	/* Dates
 	------------------------------------------------------------------------------------------------------------------*/
 	------------------------------------------------------------------------------------------------------------------*/
 
 
@@ -93,7 +70,7 @@ var Grid = FC.Grid = ChronoComponent.extend({
 		var displayEventTime;
 		var displayEventTime;
 		var displayEventEnd;
 		var displayEventEnd;
 
 
-		this.eventTimeFormat =
+		this.eventTimeFormat = // for Grid.event-rendering.js
 			this.opt('eventTimeFormat') ||
 			this.opt('eventTimeFormat') ||
 			this.opt('timeFormat') || // deprecated
 			this.opt('timeFormat') || // deprecated
 			this.computeEventTimeFormat();
 			this.computeEventTimeFormat();
@@ -113,28 +90,10 @@ var Grid = FC.Grid = ChronoComponent.extend({
 	},
 	},
 
 
 
 
-	componentFootprintToSegs: function(componentFootprint) {
-		// subclasses must implement
-	},
-
-
-	// Diffs the two dates, returning a duration, based on granularity of the grid
-	// TODO: port isTimeScale into this system?
-	diffDates: function(a, b) {
-		if (this.largeUnit) {
-			return diffByUnit(a, b, this.largeUnit);
-		}
-		else {
-			return diffDayTime(a, b);
-		}
-	},
-
 
 
 	/* Hit Area
 	/* Hit Area
 	------------------------------------------------------------------------------------------------------------------*/
 	------------------------------------------------------------------------------------------------------------------*/
 
 
-	hitsNeededDepth: 0, // necessary because multiple callers might need the same hits
-
 
 
 	hitsNeeded: function() {
 	hitsNeeded: function() {
 		if (!(this.hitsNeededDepth++)) {
 		if (!(this.hitsNeededDepth++)) {
@@ -408,43 +367,6 @@ var Grid = FC.Grid = ChronoComponent.extend({
 	},
 	},
 
 
 
 
-	/* Event Helper
-	------------------------------------------------------------------------------------------------------------------*/
-	// TODO: should probably move this to Grid.events, like we did event dragging / resizing
-
-
-	renderHelperEventFootprints: function(eventFootprints, sourceSeg) {
-		return this.renderHelperEventFootprintEls(eventFootprints, sourceSeg)
-			.addClass('fc-helper');
-	},
-
-
-	renderHelperEventFootprintEls: function(eventFootprints, sourceSeg) {
-		// Subclasses must implement.
-		// Must return all mock event elements.
-	},
-
-
-	// Unrenders a mock event
-	// TODO: have this in ChronoComponent
-	unrenderHelper: function() {
-		// subclasses must implement
-	},
-
-
-	fabricateEventFootprint: function(componentFootprint) {
-		var calendar = this.view.calendar;
-		var eventDateProfile = calendar.footprintToDateProfile(componentFootprint);
-		var dummyEvent = new SingleEventDef(new EventSource(calendar));
-		var dummyInstance;
-
-		dummyEvent.dateProfile = eventDateProfile;
-		dummyInstance = dummyEvent.buildInstance();
-
-		return new EventFootprint(componentFootprint, dummyEvent, dummyInstance);
-	},
-
-
 	/* Selection
 	/* Selection
 	------------------------------------------------------------------------------------------------------------------*/
 	------------------------------------------------------------------------------------------------------------------*/
 
 
@@ -516,6 +438,95 @@ var Grid = FC.Grid = ChronoComponent.extend({
 	// Unrenders the emphasis on a date range
 	// Unrenders the emphasis on a date range
 	unrenderHighlight: function() {
 	unrenderHighlight: function() {
 		this.unrenderFill('highlight');
 		this.unrenderFill('highlight');
+	},
+
+
+	/* Converting eventRange -> eventFootprint -> eventSegs
+	------------------------------------------------------------------------------------------------------------------*/
+
+
+	eventRangesToEventFootprints: function(eventRanges) {
+		var eventFootprints = [];
+		var i;
+
+		for (i = 0; i < eventRanges.length; i++) {
+			eventFootprints.push.apply(eventFootprints,
+				this.eventRangeToEventFootprints(eventRanges[i])
+			);
+		}
+
+		return eventFootprints;
+	},
+
+
+	// Given an event's unzoned date range, return an array of eventSpan objects.
+	// eventSpan - { start, end, isStart, isEnd, otherthings... }
+	// Subclasses can override.
+	// Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans.
+	// TODO: somehow more DRY with Calendar::eventRangeToEventFootprints
+	eventRangeToEventFootprints: function(eventRange) {
+		return [
+			new EventFootprint(
+				new ComponentFootprint(
+					eventRange.unzonedRange,
+					eventRange.eventDef.isAllDay()
+				),
+				eventRange.eventDef,
+				eventRange.eventInstance // might not exist
+			)
+		];
+	},
+
+
+	eventFootprintsToSegs: function(eventFootprints) {
+		var segs = [];
+		var i;
+
+		for (i = 0; i < eventFootprints.length; i++) {
+			segs.push.apply(segs,
+				this.eventFootprintToSegs(eventFootprints[i])
+			);
+		}
+
+		return segs;
+	},
+
+
+	// Given an event's span (unzoned start/end and other misc data), and the event itself,
+	// slices into segments and attaches event-derived properties to them.
+	// eventSpan - { start, end, isStart, isEnd, otherthings... }
+	// constraintRange allow additional clipping. optional. eventually remove this.
+	eventFootprintToSegs: function(eventFootprint, constraintRange) {
+		var unzonedRange = eventFootprint.componentFootprint.unzonedRange;
+		var segs;
+		var i, seg;
+
+		if (constraintRange) {
+			unzonedRange = unzonedRange.intersect(constraintRange);
+		}
+
+		segs = this.componentFootprintToSegs(eventFootprint.componentFootprint);
+
+		for (i = 0; i < segs.length; i++) {
+			seg = segs[i];
+
+			if (!unzonedRange.isStart) {
+				seg.isStart = false;
+			}
+			if (!unzonedRange.isEnd) {
+				seg.isEnd = false;
+			}
+
+			seg.footprint = eventFootprint;
+			// TODO: rename to seg.eventFootprint
+		}
+
+		return segs;
+	},
+
+
+	componentFootprintToSegs: function(componentFootprint) {
+		// subclasses must implement
 	}
 	}
 
 
 });
 });