diff --git a/lumbar.json b/lumbar.json
index f3a14b8..2330fbd 100644
--- a/lumbar.json
+++ b/lumbar.json
@@ -22,6 +22,7 @@
"src/util.js",
"src/moment-ext.js",
"src/date-formatting.js",
+ "src/common/Popover.js",
"src/common/CoordMap.js",
"src/common/DragListener.js",
"src/common/MouseFollower.js",
diff --git a/src/Calendar.js b/src/Calendar.js
index 6c0d443..e8e79f9 100644
--- a/src/Calendar.js
+++ b/src/Calendar.js
@@ -52,7 +52,7 @@ function Calendar(element, instanceOptions) {
t.today = today;
t.gotoDate = gotoDate;
t.incrementDate = incrementDate;
- t.zoomToDay = zoomToDay;
+ t.zoomTo = zoomTo;
t.getDate = getDate;
t.getCalendar = getCalendar;
t.getView = getView;
@@ -424,6 +424,7 @@ function Calendar(element, instanceOptions) {
}
ignoreWindowResize++;
+ currentView.recordScroll(); // for keeping the scroll value (if any) before refreshing dimensions
currentView.updateHeight(); // will poll getSuggestedViewHeight() and isHeightAuto()
currentView.updateWidth();
ignoreWindowResize--;
@@ -625,19 +626,25 @@ function Calendar(element, instanceOptions) {
}
- // Forces navigation to a day-view on the given date. `viewName` is the name of an explicit view to go to.
- // If not specified, or 'auto', it will guess the best view based on which buttons are in the header toolbar.
- function zoomToDay(newDate, viewName) {
- var viewsWithButtons;
+ // Forces navigation to a view for the given date.
+ // `viewName` can be a specific view name or a generic one like "week" or "day".
+ function zoomTo(newDate, viewName) {
+ var viewStr;
+ var match;
- if (fcViews[viewName] === undefined) { // not an available view, or 'auto'
- viewsWithButtons = header.getViewsWithButtons();
- if ($.inArray('basicDay', viewsWithButtons) !== -1) {
- viewName = 'basicDay';
- }
- else {
- viewName = 'agendaDay'; // the fallback
+ if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto"
+ viewName = viewName || 'day';
+ viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header
+
+ // try to match a general view name, like "week", against a specific one, like "agendaWeek"
+ match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName)));
+
+ // fall back to the day view being used in the header
+ if (!match) {
+ match = viewStr.match(/\w+Day/);
}
+
+ viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay
}
date = newDate;
diff --git a/src/agenda/AgendaView.js b/src/agenda/AgendaView.js
index ead2169..98dadc7 100644
--- a/src/agenda/AgendaView.js
+++ b/src/agenda/AgendaView.js
@@ -115,6 +115,16 @@ $.extend(AgendaView.prototype, {
},
+ // Make subcomponents ready for cleanup
+ destroy: function() {
+ this.timeGrid.destroy();
+ if (this.dayGrid) {
+ this.dayGrid.destroy();
+ }
+ View.prototype.destroy.call(this); // call the super-method
+ },
+
+
// Builds the HTML skeleton for the view.
// The day-grid and time-grid components will render inside containers defined by this HTML.
renderHtml: function() {
@@ -317,12 +327,18 @@ $.extend(AgendaView.prototype, {
// the all-day area is flexible and might have a lot of events, so shift the height
this.updateHeight();
- this.segs = daySegs.concat(timedSegs); // needed by the View super-class
-
View.prototype.renderEvents.call(this, events); // call the super-method
},
+ // Retrieves all segment objects that are rendered in the view
+ getSegs: function() {
+ return this.timeGrid.getSegs().concat(
+ this.dayGrid ? this.dayGrid.getSegs() : []
+ );
+ },
+
+
// Unrenders all event elements and clears internal segment data
destroyEvents: function() {
@@ -340,7 +356,7 @@ $.extend(AgendaView.prototype, {
// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
// B) in IE8, this causes a flash whenever events are rerendered
- View.prototype.destroyEvents.call(this); // call the super-method. will kill `this.segs`
+ View.prototype.destroyEvents.call(this); // call the super-method
},
diff --git a/src/agenda/agenda.css b/src/agenda/agenda.css
index 6aadf84..81fbc0e 100644
--- a/src/agenda/agenda.css
+++ b/src/agenda/agenda.css
@@ -126,9 +126,12 @@
/* TimeGrid Event Styling
---------------------------------------------------------------------------------------------------*/
+----------------------------------------------------------------------------------------------------
+We use the full "fc-time-grid-event" class instead of using descendants because the event won't
+be a descendant of the grid when it is being dragged.
+*/
-.fc-time-grid .fc-event.fc-not-start { /* events that are continuing from another day */
+.fc-time-grid-event.fc-not-start { /* events that are continuing from another day */
/* replace space made by the top border with padding */
border-top-width: 0;
padding-top: 1px;
@@ -138,7 +141,7 @@
border-top-right-radius: 0;
}
-.fc-time-grid .fc-event.fc-not-end {
+.fc-time-grid-event.fc-not-end {
/* replace space made by the top border with padding */
border-bottom-width: 0;
padding-bottom: 1px;
@@ -148,12 +151,6 @@
border-bottom-right-radius: 0;
}
-/*
-The above event styles will not apply to events that are being dragged. Dragged events are attached
-to an outer parent not part of the .fc-view, thus we need the className "fc-time-grid-event".
-The below styles WILL be applied to dragged events.
-*/
-
.fc-time-grid-event {
overflow: hidden; /* don't let the bg flow over rounded corners */
}
diff --git a/src/basic/BasicView.js b/src/basic/BasicView.js
index 3dbfb95..11dc6d9 100644
--- a/src/basic/BasicView.js
+++ b/src/basic/BasicView.js
@@ -50,6 +50,13 @@ $.extend(BasicView.prototype, {
},
+ // Make subcomponents ready for cleanup
+ destroy: function() {
+ this.dayGrid.destroy();
+ View.prototype.destroy.call(this); // call the super-method
+ },
+
+
// Builds the HTML skeleton for the view.
// The day-grid component will render inside of a container defined by this HTML.
renderHtml: function() {
@@ -214,7 +221,7 @@ $.extend(BasicView.prototype, {
// Renders the given events onto the view and populates the segments array
renderEvents: function(events) {
- this.segs = this.dayGrid.renderEvents(events);
+ this.dayGrid.renderEvents(events);
this.updateHeight(); // must compensate for events that overflow the row
@@ -222,6 +229,12 @@ $.extend(BasicView.prototype, {
},
+ // Retrieves all segment objects that are rendered in the view
+ getSegs: function() {
+ return this.dayGrid.getSegs();
+ },
+
+
// Unrenders all event elements and clears internal segment data
destroyEvents: function() {
this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand
diff --git a/src/basic/MonthView.js b/src/basic/MonthView.js
index ca0a75d..47a0cb6 100644
--- a/src/basic/MonthView.js
+++ b/src/basic/MonthView.js
@@ -6,7 +6,7 @@ setDefaults({
fixedWeekCount: true,
eventLimit: false,
eventLimitText: 'more',
- eventLimitClick: 'auto'
+ eventLimitClick: 'popover'
});
fcViews.month = MonthView; // register the view
@@ -68,6 +68,8 @@ $.extend(MonthView.prototype, {
height *= this.rowCnt / 6;
}
+ this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
+
// is the event limit a constant level number?
if (eventLimit && typeof eventLimit === 'number') {
this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
diff --git a/src/basic/basic.css b/src/basic/basic.css
index 1c9b84d..d78e73d 100644
--- a/src/basic/basic.css
+++ b/src/basic/basic.css
@@ -75,3 +75,18 @@ a.fc-more:hover {
.fc-limited { /* rows and cells that are hidden because of a "more" link */
display: none;
}
+
+/* popover that appears when "more" link is clicked */
+
+.fc-day-grid .fc-row {
+ z-index: 1; /* make the "more" popover one higher than this */
+}
+
+.fc-more-popover {
+ z-index: 2;
+ width: 220px;
+}
+
+.fc-more-popover .fc-event-container {
+ padding: 10px;
+}
diff --git a/src/common/DayGrid.events.js b/src/common/DayGrid.events.js
index 4908e4f..bb45de6 100644
--- a/src/common/DayGrid.events.js
+++ b/src/common/DayGrid.events.js
@@ -4,24 +4,32 @@
$.extend(DayGrid.prototype, {
-
+ segs: null,
rowStructs: null, // an array of objects, each holding information about a row's event-rendering
// Render the given events onto the Grid and return the rendered segments
renderEvents: function(events) {
var rowStructs = this.rowStructs = this.renderEventRows(events);
- var allSegs = [];
+ var segs = [];
// append to each row's content skeleton
this.rowEls.each(function(i, rowNode) {
$(rowNode).find('.fc-content-skeleton > table').append(
rowStructs[i].tbodyEl
);
- allSegs.push.apply(allSegs, rowStructs[i].segs);
+ segs.push.apply(segs, rowStructs[i].segs);
});
- return allSegs; // return segment objects. for the view
+ this.segs = segs;
+ },
+
+
+ // Retrieves all segment objects that have been rendered
+ getSegs: function() {
+ return (this.segs || []).concat(
+ this.popoverSegs || [] // segs rendered in the "more" events popover
+ );
},
@@ -33,38 +41,27 @@ $.extend(DayGrid.prototype, {
while ((rowStruct = rowStructs.pop())) {
rowStruct.tbodyEl.remove();
}
+
+ this.segs = null;
+ this.destroySegPopover(); // removes the "more.." events popover
},
// Uses the given events array to generate
elements that should be appended to each row's content skeleton.
// Returns an array of rowStruct objects (see the bottom of `renderEventRow`).
renderEventRows: function(events) {
- var view = this.view;
- var allSegs = this.eventsToSegs(events);
- var segRows = this.groupSegRows(allSegs); // group into nested arrays
- var html = '';
+ var segs = this.eventsToSegs(events);
var rowStructs = [];
- var i;
+ var segRows;
var row;
- var rowSegs;
- // build a large concatenation of event segment HTML
- for (i = 0; i < allSegs.length; i++) {
- html += this.renderSegHtml(allSegs[i]);
- }
-
- // 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) {
- allSegs[i].el = view.resolveEventEl(allSegs[i].event, $(node));
- });
+ segs = this.renderSegs(segs); // returns a new array with only visible segments
+ segRows = this.groupSegRows(segs); // group into nested arrays
// iterate each row of segment groupings
for (row = 0; row < segRows.length; row++) {
- rowSegs = segRows[row];
- rowSegs = $.grep(rowSegs, renderedSegFilter); // filter out non-rendered segments
rowStructs.push(
- this.renderEventRow(row, rowSegs)
+ this.renderEventRow(row, segRows[row])
);
}
@@ -73,12 +70,12 @@ $.extend(DayGrid.prototype, {
// Builds the HTML to be used for the default element for an individual segment
- renderSegHtml: function(seg) {
+ renderSegHtml: function(seg, disableResizing) {
var view = this.view;
var isRTL = view.opt('isRTL');
var event = seg.event;
var isDraggable = view.isEventDraggable(event);
- var isResizable = event.allDay && seg.isEnd && view.isEventResizable(event); // only on endings of timed events
+ var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event);
var classes = this.getSegClasses(seg, isDraggable, isResizable);
var skinCss = this.getEventSkinCss(event);
var timeHtml = '';
diff --git a/src/common/DayGrid.js b/src/common/DayGrid.js
index 7605b85..dd29987 100644
--- a/src/common/DayGrid.js
+++ b/src/common/DayGrid.js
@@ -46,6 +46,11 @@ $.extend(DayGrid.prototype, {
},
+ destroy: function() {
+ this.destroySegPopover();
+ },
+
+
// Generates the HTML for a single row. `row` is the row number.
dayRowHtml: function(row, isRigid) {
var view = this.view;
diff --git a/src/common/DayGrid.limit.js b/src/common/DayGrid.limit.js
index f4f0cd3..c736598 100644
--- a/src/common/DayGrid.limit.js
+++ b/src/common/DayGrid.limit.js
@@ -5,6 +5,17 @@
$.extend(DayGrid.prototype, {
+ segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
+ popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
+
+
+ destroySegPopover: function() {
+ if (this.segPopover) {
+ this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs`
+ }
+ },
+
+
// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
// `levelLimit` can be false (don't limit), a number, or true (should be computed).
limitRows: function(levelLimit) {
@@ -73,7 +84,7 @@ $.extend(DayGrid.prototype, {
var td, rowspan;
var segMoreNodes; // array of "more"
cells that will stand-in for the current seg's cell
var j;
- var moreTd, moreLink;
+ var moreTd, moreWrap, moreLink;
// Iterates through empty level cells and places "more" links inside if need be
function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
@@ -83,8 +94,9 @@ $.extend(DayGrid.prototype, {
if (segsBelow.length) {
td = cellMatrix[levelLimit - 1][col];
moreLink = _this.renderMoreLink(cell, segsBelow);
- td.append(moreLink);
- moreNodes.push(moreLink[0]);
+ moreWrap = $('').append(moreLink);
+ td.append(moreWrap);
+ moreNodes.push(moreWrap[0]);
}
col++;
}
@@ -124,7 +136,8 @@ $.extend(DayGrid.prototype, {
segsBelow = colSegsBelow[j];
cell = { row: row, col: seg.leftCol + j };
moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
- moreTd.append(moreLink);
+ moreWrap = $('').append(moreLink);
+ moreTd.append(moreWrap);
segMoreNodes.push(moreTd[0]);
moreNodes.push(moreTd[0]);
}
@@ -168,27 +181,116 @@ $.extend(DayGrid.prototype, {
.text(
this.getMoreLinkText(hiddenSegs.length)
)
- .on('click', function() {
- var date = view.cellToDate(cell);
+ .on('click', function(ev) {
var clickOption = view.opt('eventLimitClick');
+ var date = view.cellToDate(cell);
+ var moreEl = $(this);
+ var dayEl = _this.getCellDayEl(cell);
+ var allSegs = _this.getCellSegs(cell);
- if (typeof clickOption === 'string') { // a view name or 'auto'
- view.calendar.zoomToDay(date, clickOption);
+ // rescope the segments to be within the cell's date
+ var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
+ var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
+
+ if (clickOption === 'popover') {
+ _this.showSegPopover(date, cell, moreEl, reslicedAllSegs);
}
- else {
- view.trigger(
- 'eventLimitClick',
- null,
- date,
- _this.getCellDayEl(cell), // element for the cell's day
- hiddenSegs,
- _this.getCellSegs(cell) // all segments on that day
- );
+ else if (typeof clickOption === 'string') { // a view name
+ view.calendar.zoomTo(date, clickOption);
+ }
+ else if (typeof clickOption === 'function') {
+ view.trigger('eventLimitClick', null, {
+ date: date,
+ dayEl: dayEl,
+ moreEl: moreEl,
+ segs: reslicedAllSegs,
+ hiddenSegs: reslicedHiddenSegs
+ }, ev);
}
});
},
+ // Reveals the popover that displays all events within a cell
+ showSegPopover: function(date, cell, moreLink, segs) {
+ var _this = this;
+ var view = this.view;
+ var moreWrap = moreLink.parent(); // the
. no border confusion
+ autoHide: true, // when the user clicks elsewhere, hide the popover
+ hide: function() {
+ // destroy everything when the popover is hidden
+ _this.segPopover.destroy();
+ _this.segPopover = null;
+ _this.popoverSegs = null;
+ }
+ };
+
+ // Determine horizontal coordinate.
+ // We use the moreWrap instead of the
to avoid border confusion.
+ if (view.opt('isRTL')) {
+ options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
+ }
+ else {
+ options.left = moreWrap.offset().left - 1; // -1 to be over cell border
+ }
+
+ this.segPopover = new Popover(options);
+ this.segPopover.show();
+ },
+
+
+ // Builds the inner DOM contents of the segment popover
+ renderSegPopoverContent: function(date, segs) {
+ var view = this.view;
+ var isTheme = view.opt('theme');
+ var title = date.format('LL'); // TODO: make this an option somehow
+ var content = $(
+ '
'
+ );
+ var segContainer = content.find('.fc-event-container');
+ var i;
+
+ // render each seg's `el` and only return the visible segs
+ segs = this.renderSegs(segs, true); // disableResizing=true
+ this.popoverSegs = segs;
+
+ for (i = 0; i < segs.length; i++) {
+ segs[i].isDetached = true; // signals the segment doesn't live in a cell. needed for event DnD
+ segContainer.append(segs[i].el);
+ }
+
+ return content;
+ },
+
+
+ // Given the events within an array of segment objects, reslice them to be in a single day
+ resliceDaySegs: function(segs, dayDate) {
+ var events = $.map(segs, function(seg) {
+ return seg.event;
+ });
+ var dayStart = dayDate.clone().stripTime();
+ var dayEnd = dayStart.clone().add('days', 1);
+
+ return this.eventsToSegs(events, dayStart, dayEnd);
+ },
+
+
// Generates the text that should be inside a "more" link, given the number of events it represents
getMoreLinkText: function(num) {
var view = this.view;
diff --git a/src/common/Grid.events.js b/src/common/Grid.events.js
index c29665e..1a92577 100644
--- a/src/common/Grid.events.js
+++ b/src/common/Grid.events.js
@@ -4,6 +4,7 @@
$.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?
@@ -14,29 +15,79 @@ $.extend(Grid.prototype, {
},
+ // 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) {
+ eventsToSegs: function(events, intervalStart, intervalEnd) {
var _this = this;
return $.map(events, function(event) {
- return _this.eventToSegs(event); // $.map flattens all returned arrays together
+ return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together
});
},
- // Slices a single event into an array of event segments
- eventToSegs: function(event) {
+ // 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 = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass
+ 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];
@@ -49,6 +100,10 @@ $.extend(Grid.prototype, {
},
+ /* Handlers
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
// Attaches event-element-related handlers to the container element and leverage bubbling
bindSegHandlers: function() {
var _this = this;
@@ -57,10 +112,10 @@ $.extend(Grid.prototype, {
$.each(
{
mouseenter: function(seg, ev) {
- view.trigger('eventMouseover', this, seg.event, ev);
+ _this.triggerSegMouseover(seg, ev);
},
mouseleave: function(seg, ev) {
- view.trigger('eventMouseout', this, seg.event, ev);
+ _this.triggerSegMouseout(seg, ev);
},
click: function(seg, ev) {
return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
@@ -76,11 +131,11 @@ $.extend(Grid.prototype, {
},
function(name, func) {
// attach the handler to the container element and only listen for real event elements via bubbling
- _this.el.on(name, '.fc-content-skeleton .fc-event-container > *', function(ev) {
+ _this.el.on(name, '.fc-event-container > *', function(ev) {
var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
- if (seg /*&& !_this.isDraggingSeg && !_this.isResizingSeg*/) {
- // needs more work if we want eventMouseout to fire correctly
+ // 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
}
});
@@ -89,6 +144,28 @@ $.extend(Grid.prototype, {
},
+ // 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) {
@@ -96,9 +173,7 @@ $.extend(Grid.prototype, {
var view = this.view;
var el = seg.el;
var event = seg.event;
- var start = event.start;
- var end = view.calendar.getEventEnd(event);
- var newStart = null;
+ var newStart, newEnd;
// A clone of the original element that will move with the mouse
var mouseFollower = new MouseFollower(seg.el, {
@@ -117,39 +192,21 @@ $.extend(Grid.prototype, {
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 origDate = dragListener.origDate;
- var delta;
- var newEnd;
+ var res = _this.computeDraggedEventDates(seg, dragListener.origDate, date);
+ newStart = res.start;
+ newEnd = res.end;
- if (origDate) { // must start out on a cell (weird accident if it didn't)
-
- if (date.hasTime() === origDate.hasTime()) { // staying all-day or staying timed
- delta = dayishDiff(date, origDate);
- 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 { // switching from all-day to timed, or vice versa
- newStart = date;
- newEnd = null; // end should be cleared
- }
-
- 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();
- }
+ 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
@@ -158,14 +215,13 @@ $.extend(Grid.prototype, {
mouseFollower.show(); // show in case we are moving out of all cells
},
dragStop: function(ev) {
- var hasChanged = newStart && !newStart.isSame(start);
+ 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) {
@@ -182,6 +238,60 @@ $.extend(Grid.prototype, {
},
+ // 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) {
@@ -203,8 +313,8 @@ $.extend(Grid.prototype, {
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) {
@@ -230,7 +340,6 @@ $.extend(Grid.prototype, {
dragStop: function(ev) {
_this.isResizingSeg = false;
destroy();
-
view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy
if (newEnd) {
@@ -243,6 +352,10 @@ $.extend(Grid.prototype, {
},
+ /* Rendering Utils
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
// Generic utility for generating the HTML classNames for an event segment's element
getSegClasses: function(seg, isDraggable, isResizable) {
var event = seg.event;
@@ -319,9 +432,3 @@ function compareSegs(seg1, seg2) {
(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
}
-
-// Returns `true` if the segment has a rendered element and `false` otherwise
-function renderedSegFilter(seg) {
- return !!seg.el;
-}
-
diff --git a/src/common/Grid.js b/src/common/Grid.js
index ae04fc7..3858f9a 100644
--- a/src/common/Grid.js
+++ b/src/common/Grid.js
@@ -23,6 +23,12 @@ $.extend(Grid.prototype, {
},
+ // Called when the grid's resources need to be cleaned up
+ destroy: function() {
+ // subclasses can implement
+ },
+
+
/* Coordinates & Cells
------------------------------------------------------------------------------------------------------------------*/
@@ -62,7 +68,10 @@ $.extend(Grid.prototype, {
var _this = this;
this.el.on('mousedown', function(ev) {
- if (!$(ev.target).is('.fc-event-container *, .fc-more')) { // not an event element or "more" link
+ if (
+ !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
+ !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
+ ) {
_this.dayMousedown(ev);
}
});
diff --git a/src/common/Popover.js b/src/common/Popover.js
new file mode 100644
index 0000000..d7bcb43
--- /dev/null
+++ b/src/common/Popover.js
@@ -0,0 +1,165 @@
+
+/* A rectangular panel that is absolutely positioned over other content
+------------------------------------------------------------------------------------------------------------------------
+Options:
+ - className (string)
+ - content (HTML string or jQuery element set)
+ - parentEl
+ - top
+ - left
+ - right (the x coord of where the right edge should be. not a "CSS" right)
+ - autoHide (boolean)
+ - show (callback)
+ - hide (callback)
+*/
+
+function Popover(options) {
+ this.options = options || {};
+}
+
+
+Popover.prototype = {
+
+ isHidden: true,
+ options: null,
+ el: null, // the container element for the popover. generated by this object
+ documentMousedownProxy: null, // document mousedown handler bound to `this`
+ margin: 10, // the space required between the popover and the edges of the scroll container
+
+
+ // Shows the popover on the specified position. Renders it if not already
+ show: function() {
+ if (this.isHidden) {
+ if (!this.el) {
+ this.render();
+ }
+ this.el.show();
+ this.position();
+ this.isHidden = false;
+ this.trigger('show');
+ }
+ },
+
+
+ // Hides the popover, through CSS, but does not remove it from the DOM
+ hide: function() {
+ if (!this.isHidden) {
+ this.el.hide();
+ this.isHidden = true;
+ this.trigger('hide');
+ }
+ },
+
+
+ // Creates `this.el` and renders content inside of it
+ render: function() {
+ var _this = this;
+ var options = this.options;
+
+ this.el = $('')
+ .addClass(options.className || '')
+ .css({
+ // position initially to the top left to avoid creating scrollbars
+ top: 0,
+ left: 0
+ })
+ .append(options.content)
+ .appendTo(options.parentEl);
+
+ // when a click happens on anything inside with a 'fc-close' className, hide the popover
+ this.el.on('click', '.fc-close', function() {
+ _this.hide();
+ });
+
+ if (options.autoHide) {
+ $(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown'));
+ }
+ },
+
+
+ // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
+ documentMousedown: function(ev) {
+ // only hide the popover if the click happened outside the popover
+ if (this.el && !$(ev.target).closest(this.el).length) {
+ this.hide();
+ }
+ },
+
+
+ // Hides and unregisters any handlers
+ destroy: function() {
+ this.hide();
+
+ if (this.el) {
+ this.el.remove();
+ this.el = null;
+ }
+
+ $(document).off('mousedown', this.documentMousedownProxy);
+ },
+
+
+ // Positions the popover optimally, using the top/left/right options
+ position: function() {
+ var options = this.options;
+ var origin = this.el.offsetParent().offset();
+ var width = this.el.outerWidth();
+ var height = this.el.outerHeight();
+ var windowEl = $(window);
+ var viewportEl = getScrollParent(this.el);
+ var viewportTop;
+ var viewportLeft;
+ var viewportOffset;
+ var top; // the "position" (not "offset") values for the popover
+ var left; //
+
+ // compute top and left
+ top = options.top || 0;
+ if (options.left !== undefined) {
+ left = options.left;
+ }
+ else if (options.right !== undefined) {
+ left = options.right - width; // derive the left value from the right value
+ }
+ else {
+ left = 0;
+ }
+
+ if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
+ viewportEl = windowEl;
+ viewportTop = 0; // the window is always at the top left
+ viewportLeft = 0; // (and .offset() won't work if called here)
+ }
+ else {
+ viewportOffset = viewportEl.offset();
+ viewportTop = viewportOffset.top;
+ viewportLeft = viewportOffset.left;
+ }
+
+ // if the window is scrolled, it causes the visible area to be further down
+ viewportTop += windowEl.scrollTop();
+ viewportLeft += windowEl.scrollLeft();
+
+ // constrain to the view port. if constrained by two edges, give precedence to top/left
+ top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
+ top = Math.max(top, viewportTop + this.margin);
+ left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
+ left = Math.max(left, viewportLeft + this.margin);
+
+ this.el.css({
+ top: top - origin.top,
+ left: left - origin.left
+ });
+ },
+
+
+ // Triggers a callback. Calls a function in the option hash of the same name.
+ // Arguments beyond the first `name` are forwarded on.
+ // TODO: better code reuse for this. Repeat code
+ trigger: function(name) {
+ if (this.options[name]) {
+ this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
+ }
+ }
+
+};
diff --git a/src/common/TimeGrid.events.js b/src/common/TimeGrid.events.js
index 85c2e21..56fc892 100644
--- a/src/common/TimeGrid.events.js
+++ b/src/common/TimeGrid.events.js
@@ -4,6 +4,7 @@
$.extend(TimeGrid.prototype, {
+ segs: null, // segment objects rendered in the component. null of events haven't been rendered yet
eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
@@ -14,7 +15,13 @@ $.extend(TimeGrid.prototype, {
this.eventSkeletonEl = $('').append(res.tableEl);
this.el.append(this.eventSkeletonEl);
- return res.segs; // return segment objects. for the view
+ this.segs = res.segs;
+ },
+
+
+ // Retrieves rendered segment objects
+ getSegs: function() {
+ return this.segs || [];
},
@@ -24,49 +31,41 @@ $.extend(TimeGrid.prototype, {
this.eventSkeletonEl.remove();
this.eventSkeletonEl = null;
}
+
+ this.segs = null;
},
// Renders and returns the
portion of the event-skeleton.
// Returns an object with properties 'tbodyEl' and 'segs'.
renderEventTable: function(events) {
- var view = this.view;
var tableEl = $('
');
var trEl = tableEl.find('tr');
- var allSegs = this.eventsToSegs(events);
- var segCols = this.groupSegCols(allSegs); // groups into sub-arrays, and assigns 'col' to each seg
- var html = ''; // html string with default HTML for all events, concatenated together
+ var segs = this.eventsToSegs(events);
+ var segCols;
var i, seg;
- var col, segs;
+ var col, colSegs;
var containerEl;
- // build the combined HTML string. and compute top/bottom
- for (i = 0; i < allSegs.length; i++) {
- seg = allSegs[i];
- html += this.renderSegHtml(seg);
+ segs = this.renderSegs(segs); // returns only the visible segs
+ segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
+ // compute vertical coordinates
+ for (i = 0; i < segs.length; i++) {
+ seg = segs[i];
seg.top = this.computeDateTop(seg.start, seg.start);
seg.bottom = this.computeDateTop(seg.end, seg.start);
}
- // 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) {
- allSegs[i].el = view.resolveEventEl(allSegs[i].event, $(node));
- });
-
for (col = 0; col < segCols.length; col++) { // iterate each column grouping
- segs = segCols[col];
-
- segs = $.grep(segs, renderedSegFilter); // filter out unrendered segments
- placeSlotSegs(segs); // compute horizontal coordinates, z-index's, and reorder the array
- segCols[col] = segs; // assign back
+ colSegs = segCols[col];
+ placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array
containerEl = $('');
// assign positioning CSS and insert into container
- for (i = 0; i < segs.length; i++) {
- seg = segs[i];
+ for (i = 0; i < colSegs.length; i++) {
+ seg = colSegs[i];
seg.el.css(this.generateSegPositionCss(seg));
containerEl.append(seg.el);
}
@@ -78,17 +77,17 @@ $.extend(TimeGrid.prototype, {
return {
tableEl: tableEl,
- segs: flattenArray(segCols) // will contain only segments with rendered els
+ segs: segs
};
},
// Renders the HTML for a single event segment's default rendering
- renderSegHtml: function(seg) {
+ renderSegHtml: function(seg, disableResizing) {
var view = this.view;
var event = seg.event;
var isDraggable = view.isEventDraggable(event);
- var isResizable = seg.isEnd && view.isEventResizable(event);
+ var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event);
var classes = this.getSegClasses(seg, isDraggable, isResizable);
var skinCss = this.getEventSkinCss(event);
var timeText;
diff --git a/src/common/TimeGrid.js b/src/common/TimeGrid.js
index f54cc60..53104ad 100644
--- a/src/common/TimeGrid.js
+++ b/src/common/TimeGrid.js
@@ -124,51 +124,26 @@ $.extend(TimeGrid.prototype, {
// Slices up a date range into a segment for each column
- rangeToSegs: function(start, end) {
+ rangeToSegs: function(rangeStart, rangeEnd) {
var view = this.view;
var segs = [];
+ var seg;
var col;
var cellDate;
var colStart, colEnd;
- var segStart, segEnd;
- var isStart, isEnd;
// normalize
- start = start.clone().stripZone();
- end = end.clone().stripZone();
+ rangeStart = rangeStart.clone().stripZone();
+ rangeEnd = rangeEnd.clone().stripZone();
for (col = 0; col < view.colCnt; col++) {
cellDate = view.cellToDate(0, col); // use the View's cell system for this
- colStart = cellDate.clone().stripZone().time(this.minTime); // normalize and calculate
- colEnd = cellDate.clone().stripZone().time(this.maxTime); // normalize and calculate
-
- if (end > colStart && start < colEnd) { // in bounds at all?
-
- if (start >= colStart) {
- segStart = start.clone();
- isStart = true;
- }
- else {
- segStart = colStart; // don't need to clone
- isStart = false;
- }
-
- if (end <= colEnd) {
- segEnd = end.clone();
- isEnd = true;
- }
- else {
- segEnd = colEnd; // don't need to clone
- isEnd = false;
- }
-
- segs.push({
- col: col,
- start: segStart,
- end: segEnd,
- isStart: isStart,
- isEnd: isEnd
- });
+ colStart = cellDate.clone().time(this.minTime);
+ colEnd = cellDate.clone().time(this.maxTime);
+ seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd);
+ if (seg) {
+ seg.col = col;
+ segs.push(seg);
}
}
diff --git a/src/common/View.js b/src/common/View.js
index 9c69412..fc60f15 100644
--- a/src/common/View.js
+++ b/src/common/View.js
@@ -19,8 +19,6 @@ View.prototype = {
rowCnt: null, // # of weeks
colCnt: null, // # of days displayed in a week
- segs: null, // array of rendered event segment objects
-
isSelected: false, // boolean whether cells are user-selected or not
// subclasses can optionally use a scroll container
@@ -124,7 +122,9 @@ View.prototype = {
// Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently
// change the scroll of the container.
recordScroll: function() {
- this.scrollTop = this.scrollerEl.scrollTop();
+ if (this.scrollerEl) {
+ this.scrollTop = this.scrollerEl.scrollTop();
+ }
},
@@ -143,10 +143,9 @@ View.prototype = {
// Renders the events onto the view.
- // Should be overriden by subclasses. Subclasses should assign `this.segs` and call the super-method afterwards.
+ // Should be overriden by subclasses. Subclasses should call the super-method afterwards.
renderEvents: function(events) {
this.segEach(function(seg) {
- seg.el.data('fc-seg', seg); // store info about the segment object. used by handlers
this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
});
this.trigger('eventAfterAllRender');
@@ -159,7 +158,6 @@ View.prototype = {
this.segEach(function(seg) {
this.trigger('eventDestroy', seg.event, seg.event, seg.el);
});
- this.segs = [];
},
@@ -199,7 +197,7 @@ View.prototype = {
// If the optional `event` argument is specified, only iterates through segments linked to that event.
// The `this` value of the callback function will be the view.
segEach: function(func, event) {
- var segs = this.segs || [];
+ var segs = this.getSegs();
var i;
for (i = 0; i < segs.length; i++) {
@@ -210,6 +208,12 @@ View.prototype = {
},
+ // Retrieves all the rendered segment objects for the view
+ getSegs: function() {
+ // subclasses must implement
+ },
+
+
/* Event Drag Visualization
------------------------------------------------------------------------------------------------------------------*/
diff --git a/src/common/common.css b/src/common/common.css
index 0a859a4..71e595b 100644
--- a/src/common/common.css
+++ b/src/common/common.css
@@ -27,14 +27,24 @@ body .fc { /* extra precedence to overcome jqui */
.fc-unthemed hr,
.fc-unthemed thead,
.fc-unthemed tbody,
-.fc-unthemed .fc-row {
+.fc-unthemed .fc-row,
+.fc-unthemed .fc-popover {
border-color: #ddd;
}
-.fc-unthemed hr {
+.fc-unthemed .fc-popover {
+ background-color: #fff;
+}
+
+.fc-unthemed hr,
+.fc-unthemed .fc-popover .fc-header {
background: #eee;
}
+.fc-unthemed .fc-popover .fc-header .fc-close {
+ color: #666;
+}
+
.fc-unthemed .fc-today {
background: #fcf8e3;
}
@@ -75,6 +85,10 @@ body .fc { /* extra precedence to overcome jqui */
content: "\000BB";
}
+.fc-icon-x:after {
+ content: "\000D7";
+}
+
/* Buttons (styled