mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-07-28 11:20:16 +08:00
435 lines
13 KiB
JavaScript
435 lines
13 KiB
JavaScript
|
|
/* Event-rendering and event-interaction methods for the abstract Grid class
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
|
|
$.extend(Grid.prototype, {
|
|
|
|
isMouseOverSeg: false, // is the user's mouse over a segment?
|
|
isDraggingSeg: false, // is a segment being dragged?
|
|
isResizingSeg: false, // is a segment being resized?
|
|
|
|
|
|
// Renders the given events onto the grid
|
|
renderEvents: function(events) {
|
|
// subclasses must implement
|
|
},
|
|
|
|
|
|
// Retrieves all rendered segment objects in this grid
|
|
getSegs: function() {
|
|
// subclasses must implement
|
|
},
|
|
|
|
|
|
// Unrenders all events
|
|
destroyEvents: function() {
|
|
// subclasses must implement
|
|
},
|
|
|
|
|
|
// Renders a `el` property for each seg, and only returns segments that successfully rendered
|
|
renderSegs: function(segs, disableResizing) {
|
|
var view = this.view;
|
|
var html = '';
|
|
var renderedSegs = [];
|
|
var i;
|
|
|
|
// build a large concatenation of event segment HTML
|
|
for (i = 0; i < segs.length; i++) {
|
|
html += this.renderSegHtml(segs[i], disableResizing);
|
|
}
|
|
|
|
// Grab individual elements from the combined HTML string. Use each as the default rendering.
|
|
// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
|
|
$(html).each(function(i, node) {
|
|
var seg = segs[i];
|
|
var el = view.resolveEventEl(seg.event, $(node));
|
|
if (el) {
|
|
el.data('fc-seg', seg); // used by handlers
|
|
seg.el = el;
|
|
renderedSegs.push(seg);
|
|
}
|
|
});
|
|
|
|
return renderedSegs;
|
|
},
|
|
|
|
|
|
// Generates the HTML for the default rendering of a segment
|
|
renderSegHtml: function(seg, disableResizing) {
|
|
// subclasses must implement
|
|
},
|
|
|
|
|
|
// Converts an array of event objects into an array of segment objects
|
|
eventsToSegs: function(events, intervalStart, intervalEnd) {
|
|
var _this = this;
|
|
|
|
return $.map(events, function(event) {
|
|
return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together
|
|
});
|
|
},
|
|
|
|
|
|
// Slices a single event into an array of event segments.
|
|
// When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval.
|
|
// Otherwise, let the subclass decide how it wants to slice the segments over the grid.
|
|
eventToSegs: function(event, intervalStart, intervalEnd) {
|
|
var eventStart = event.start.clone().stripZone(); // normalize
|
|
var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize
|
|
var segs;
|
|
var i, seg;
|
|
|
|
if (intervalStart && intervalEnd) {
|
|
seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd);
|
|
segs = seg ? [ seg ] : [];
|
|
}
|
|
else {
|
|
segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass
|
|
}
|
|
|
|
// assign extra event-related properties to the segment objects
|
|
for (i = 0; i < segs.length; i++) {
|
|
seg = segs[i];
|
|
seg.event = event;
|
|
seg.eventStartMS = +eventStart;
|
|
seg.eventDurationMS = eventEnd - eventStart;
|
|
}
|
|
|
|
return segs;
|
|
},
|
|
|
|
|
|
/* Handlers
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
// Attaches event-element-related handlers to the container element and leverage bubbling
|
|
bindSegHandlers: function() {
|
|
var _this = this;
|
|
var view = this.view;
|
|
|
|
$.each(
|
|
{
|
|
mouseenter: function(seg, ev) {
|
|
_this.triggerSegMouseover(seg, ev);
|
|
},
|
|
mouseleave: function(seg, ev) {
|
|
_this.triggerSegMouseout(seg, ev);
|
|
},
|
|
click: function(seg, ev) {
|
|
return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
|
|
},
|
|
mousedown: function(seg, ev) {
|
|
if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
|
|
_this.segResizeMousedown(seg, ev);
|
|
}
|
|
else if (view.isEventDraggable(seg.event)) {
|
|
_this.segDragMousedown(seg, ev);
|
|
}
|
|
}
|
|
},
|
|
function(name, func) {
|
|
// attach the handler to the container element and only listen for real event elements via bubbling
|
|
_this.el.on(name, '.fc-event-container > *', function(ev) {
|
|
var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
|
|
|
|
// only call the handlers if there is not a drag/resize in progress
|
|
if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
|
|
func.call(this, seg, ev); // `this` will be the event element
|
|
}
|
|
});
|
|
}
|
|
);
|
|
},
|
|
|
|
|
|
// Updates internal state and triggers handlers for when an event element is moused over
|
|
triggerSegMouseover: function(seg, ev) {
|
|
if (!this.isMouseOverSeg) {
|
|
this.isMouseOverSeg = true;
|
|
this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
|
|
}
|
|
},
|
|
|
|
|
|
// Updates internal state and triggers handlers for when an event element is moused out
|
|
triggerSegMouseout: function(seg, ev) {
|
|
if (this.isMouseOverSeg) {
|
|
this.isMouseOverSeg = false;
|
|
this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
|
|
}
|
|
},
|
|
|
|
|
|
/* Dragging
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
// Called when the user does a mousedown on an event, which might lead to dragging.
|
|
// Generic enough to work with any type of Grid.
|
|
segDragMousedown: function(seg, ev) {
|
|
var _this = this;
|
|
var view = this.view;
|
|
var el = seg.el;
|
|
var event = seg.event;
|
|
var newStart, newEnd;
|
|
|
|
// A clone of the original element that will move with the mouse
|
|
var mouseFollower = new MouseFollower(seg.el, {
|
|
parentEl: view.el,
|
|
opacity: view.opt('dragOpacity'),
|
|
revertDuration: view.opt('dragRevertDuration'),
|
|
zIndex: 2 // one above the .fc-view
|
|
});
|
|
|
|
// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
|
|
// of the view.
|
|
var dragListener = new DragListener(view.coordMap, {
|
|
distance: 5,
|
|
listenStart: function(ev) {
|
|
mouseFollower.hide(); // don't show until we know this is a real drag
|
|
mouseFollower.start(ev);
|
|
},
|
|
dragStart: function(ev) {
|
|
_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
|
|
_this.isDraggingSeg = true;
|
|
view.hideEvent(event); // hide all event segments. our mouseFollower will take over
|
|
view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy
|
|
},
|
|
cellOver: function(cell, date) {
|
|
var res = _this.computeDraggedEventDates(seg, dragListener.origDate, date);
|
|
newStart = res.start;
|
|
newEnd = res.end;
|
|
|
|
if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication
|
|
mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own
|
|
}
|
|
else {
|
|
mouseFollower.show();
|
|
}
|
|
},
|
|
cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
|
|
newStart = null;
|
|
view.destroyDrag(); // unrender whatever was done in view.renderDrag
|
|
mouseFollower.show(); // show in case we are moving out of all cells
|
|
},
|
|
dragStop: function(ev) {
|
|
var hasChanged = newStart && !newStart.isSame(event.start);
|
|
|
|
// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
|
|
mouseFollower.stop(!hasChanged, function() {
|
|
_this.isDraggingSeg = false;
|
|
view.destroyDrag();
|
|
view.showEvent(event);
|
|
view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy
|
|
|
|
if (hasChanged) {
|
|
view.eventDrop(el[0], event, newStart, ev); // will rerender all events...
|
|
}
|
|
});
|
|
},
|
|
listenStop: function() {
|
|
mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
|
|
}
|
|
});
|
|
|
|
dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
|
|
},
|
|
|
|
|
|
// Given a segment, where it originally resided on the grid, and the new date it has been dragged to,
|
|
// calculates the Event Object's new start and end dates.
|
|
computeDraggedEventDates: function(seg, origDate, newDate) {
|
|
var view = this.view;
|
|
var event = seg.event;
|
|
var start = event.start;
|
|
var end = view.calendar.getEventEnd(event);
|
|
var delta;
|
|
var newStart;
|
|
var newEnd;
|
|
|
|
// the segment might be explicitly marked as not-in-the-grid
|
|
if (seg.isDetached) {
|
|
origDate = null;
|
|
}
|
|
|
|
// calculate the delta (a Duration) that the event's dates must be moved.
|
|
// if `delta` remains undefined, that means the event's start will literally become newDate.
|
|
if (!origDate) {
|
|
if (newDate.hasTime()) { // over a time slot
|
|
delta = dayishDiff(newDate, start); // will move the start to the exact new datetime
|
|
}
|
|
else { // over a whole-day cell
|
|
delta = dayDiff(newDate, start); // will be a whole-day diff, so that start's time will be kept
|
|
}
|
|
}
|
|
else if (newDate.hasTime() === origDate.hasTime()) { // staying all-day or staying timed
|
|
delta = dayishDiff(newDate, origDate);
|
|
}
|
|
// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
|
|
|
|
// recalculate start/end
|
|
if (delta) {
|
|
newStart = start.clone().add(delta);
|
|
if (event.end === null) { // do we need to compute an end?
|
|
newEnd = null;
|
|
}
|
|
else {
|
|
newEnd = end.clone().add(delta);
|
|
}
|
|
}
|
|
else {
|
|
newStart = newDate;
|
|
newEnd = null; // end should be cleared
|
|
}
|
|
|
|
return { start: newStart, end: newEnd };
|
|
},
|
|
|
|
|
|
/* Resizing
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
// Called when the user does a mousedown on an event's resizer, which might lead to resizing.
|
|
// Generic enough to work with any type of Grid.
|
|
segResizeMousedown: function(seg, ev) {
|
|
var _this = this;
|
|
var view = this.view;
|
|
var el = seg.el;
|
|
var event = seg.event;
|
|
var start = event.start;
|
|
var end = view.calendar.getEventEnd(event);
|
|
var newEnd = null;
|
|
var dragListener;
|
|
|
|
function destroy() { // resets the rendering
|
|
_this.destroyResize();
|
|
view.showEvent(event);
|
|
}
|
|
|
|
// Tracks mouse movement over the *grid's* coordinate map
|
|
dragListener = new DragListener(this.coordMap, {
|
|
distance: 5,
|
|
dragStart: function(ev) {
|
|
_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
|
|
_this.isResizingSeg = true;
|
|
view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy
|
|
},
|
|
cellOver: function(cell, date) {
|
|
// compute the new end. don't allow it to go before the event's start
|
|
if (date < start) {
|
|
date = start;
|
|
}
|
|
newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end
|
|
|
|
if (newEnd.isSame(end)) {
|
|
newEnd = null;
|
|
destroy();
|
|
}
|
|
else {
|
|
_this.renderResize(start, newEnd, seg);
|
|
view.hideEvent(event);
|
|
}
|
|
},
|
|
cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
|
|
newEnd = null;
|
|
destroy();
|
|
},
|
|
dragStop: function(ev) {
|
|
_this.isResizingSeg = false;
|
|
destroy();
|
|
view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy
|
|
|
|
if (newEnd) {
|
|
view.eventResize(el[0], event, newEnd, ev); // will rerender all events...
|
|
}
|
|
}
|
|
});
|
|
|
|
dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
|
|
},
|
|
|
|
|
|
/* Rendering Utils
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
// Generic utility for generating the HTML classNames for an event segment's element
|
|
getSegClasses: function(seg, isDraggable, isResizable) {
|
|
var event = seg.event;
|
|
var classes = [
|
|
'fc-event',
|
|
seg.isStart ? 'fc-start' : 'fc-not-start',
|
|
seg.isEnd ? 'fc-end' : 'fc-not-end'
|
|
].concat(
|
|
event.className,
|
|
event.source ? event.source.className : []
|
|
);
|
|
|
|
if (isDraggable) {
|
|
classes.push('fc-draggable');
|
|
}
|
|
if (isResizable) {
|
|
classes.push('fc-resizable');
|
|
}
|
|
|
|
return classes;
|
|
},
|
|
|
|
|
|
// Utility for generating a CSS string with all the event skin-related properties
|
|
getEventSkinCss: function(event) {
|
|
var view = this.view;
|
|
var source = event.source || {};
|
|
var eventColor = event.color;
|
|
var sourceColor = source.color;
|
|
var optionColor = view.opt('eventColor');
|
|
var backgroundColor =
|
|
event.backgroundColor ||
|
|
eventColor ||
|
|
source.backgroundColor ||
|
|
sourceColor ||
|
|
view.opt('eventBackgroundColor') ||
|
|
optionColor;
|
|
var borderColor =
|
|
event.borderColor ||
|
|
eventColor ||
|
|
source.borderColor ||
|
|
sourceColor ||
|
|
view.opt('eventBorderColor') ||
|
|
optionColor;
|
|
var textColor =
|
|
event.textColor ||
|
|
source.textColor ||
|
|
view.opt('eventTextColor');
|
|
var statements = [];
|
|
if (backgroundColor) {
|
|
statements.push('background-color:' + backgroundColor);
|
|
}
|
|
if (borderColor) {
|
|
statements.push('border-color:' + borderColor);
|
|
}
|
|
if (textColor) {
|
|
statements.push('color:' + textColor);
|
|
}
|
|
return statements.join(';');
|
|
}
|
|
|
|
});
|
|
|
|
|
|
/* Event Segment Utilities
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
// A cmp function for determining which segments should take visual priority
|
|
function compareSegs(seg1, seg2) {
|
|
return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
|
|
seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
|
|
seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
|
|
(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
|
|
}
|
|
|