mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-08-11 11:18:47 +08:00
popover when user clicks on "more" link
This commit is contained in:
@@ -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",
|
||||
|
||||
+19
-12
@@ -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;
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
+14
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <tbody> 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 = '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
+119
-17
@@ -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" <td> 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 = $('<div/>').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 = $('<div/>').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 <div> wrapper around the <a>
|
||||
var options = {
|
||||
className: 'fc-more-popover',
|
||||
content: this.renderSegPopoverContent(date, segs),
|
||||
parentEl: this.el,
|
||||
top: this.rowEls.eq(cell.row).offset().top, // better than the <td>. 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 <td> 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 = $(
|
||||
'<div class="fc-header ' + view.widgetHeaderClass + '">' +
|
||||
'<span class="fc-close ' +
|
||||
(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
|
||||
'"></span>' +
|
||||
'<span class="fc-title">' +
|
||||
htmlEscape(title) +
|
||||
'</span>' +
|
||||
'<div class="fc-clear"/>' +
|
||||
'</div>' +
|
||||
'<div class="fc-body ' + view.widgetContentClass + '">' +
|
||||
'<div class="fc-event-container"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
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;
|
||||
|
||||
+157
-50
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 = $('<div class="fc-popover"/>')
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -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 = $('<div class="fc-content-skeleton"/>').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 <table> portion of the event-skeleton.
|
||||
// Returns an object with properties 'tbodyEl' and 'segs'.
|
||||
renderEventTable: function(events) {
|
||||
var view = this.view;
|
||||
var tableEl = $('<table><tr/></table>');
|
||||
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 = $('<div class="fc-event-container"/>');
|
||||
|
||||
// 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;
|
||||
|
||||
+10
-35
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -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
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
+74
-14
@@ -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 <button> tags, normalized to work cross-browser)
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
@@ -197,6 +211,55 @@ previous button's border...
|
||||
}
|
||||
|
||||
|
||||
/* Popover
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-popover {
|
||||
position: absolute;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.15);
|
||||
}
|
||||
|
||||
.fc-popover .fc-header {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.fc-popover .fc-header .fc-title {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.fc-popover .fc-header .fc-close {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fc-ltr .fc-popover .fc-header .fc-title,
|
||||
.fc-rtl .fc-popover .fc-header .fc-close {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.fc-rtl .fc-popover .fc-header .fc-title,
|
||||
.fc-ltr .fc-popover .fc-header .fc-close {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* unthemed */
|
||||
|
||||
.fc-unthemed .fc-popover {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.fc-unthemed .fc-popover .fc-header .fc-close {
|
||||
font-size: 25px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* jqui themed */
|
||||
|
||||
.fc-popover > .ui-widget-header + .ui-widget-content {
|
||||
border-top: 0; /* where they meet, let the header have the border */
|
||||
}
|
||||
|
||||
|
||||
/* Misc Reusable Components
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
@@ -383,16 +446,19 @@ temporary rendered events).
|
||||
|
||||
|
||||
/* DayGrid events
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
----------------------------------------------------------------------------------------------------
|
||||
We use the full "fc-day-grid-event" class instead of using descendants because the event won't
|
||||
be a descendant of the grid when it is being dragged.
|
||||
*/
|
||||
|
||||
.fc-day-grid .fc-event {
|
||||
.fc-day-grid-event {
|
||||
margin: 1px 1px 0; /* spacing between events and edges */
|
||||
}
|
||||
|
||||
/* events that are continuing to/from another week. kill rounded corners and butt up against edge */
|
||||
|
||||
.fc-ltr .fc-day-grid .fc-event.fc-not-start,
|
||||
.fc-rtl .fc-day-grid .fc-event.fc-not-end {
|
||||
.fc-ltr .fc-day-grid-event.fc-not-start,
|
||||
.fc-rtl .fc-day-grid-event.fc-not-end {
|
||||
margin-left: 0;
|
||||
border-left-width: 0;
|
||||
padding-left: 1px; /* replace the border with padding */
|
||||
@@ -400,8 +466,8 @@ temporary rendered events).
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.fc-ltr .fc-day-grid .fc-event.fc-not-end,
|
||||
.fc-rtl .fc-day-grid .fc-event.fc-not-start {
|
||||
.fc-ltr .fc-day-grid-event.fc-not-end,
|
||||
.fc-rtl .fc-day-grid-event.fc-not-start {
|
||||
margin-right: 0;
|
||||
border-right-width: 0;
|
||||
padding-right: 1px; /* replace the border with padding */
|
||||
@@ -409,12 +475,6 @@ temporary rendered events).
|
||||
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-day-grid-event".
|
||||
The below styles WILL be applied to dragged events.
|
||||
*/
|
||||
|
||||
.fc-day-grid-event > .fc-content { /* force events to be one-line tall */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
+61
-7
@@ -116,6 +116,20 @@ function matchCellWidths(els) {
|
||||
----------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
|
||||
function getScrollParent(el) {
|
||||
var position = el.css('position'),
|
||||
scrollParent = el.parents().filter(function() {
|
||||
var parent = $(this);
|
||||
return (/(auto|scroll)/).test(
|
||||
parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
|
||||
);
|
||||
}).eq(0);
|
||||
|
||||
return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
|
||||
}
|
||||
|
||||
|
||||
// Given a container element, return an object with the pixel values of the left/right scrollbars.
|
||||
// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.
|
||||
// PREREQUISITE: container element must have a single child with display:block
|
||||
@@ -143,6 +157,42 @@ function isPrimaryMouseButton(ev) {
|
||||
----------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
|
||||
// Expects all dates to be normalized to the same timezone beforehand.
|
||||
function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) {
|
||||
var segStart, segEnd;
|
||||
var isStart, isEnd;
|
||||
|
||||
if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all?
|
||||
|
||||
if (subjectStart >= intervalStart) {
|
||||
segStart = subjectStart.clone();
|
||||
isStart = true;
|
||||
}
|
||||
else {
|
||||
segStart = intervalStart.clone();
|
||||
isStart = false;
|
||||
}
|
||||
|
||||
if (subjectEnd <= intervalEnd) {
|
||||
segEnd = subjectEnd.clone();
|
||||
isEnd = true;
|
||||
}
|
||||
else {
|
||||
segEnd = intervalEnd.clone();
|
||||
isEnd = false;
|
||||
}
|
||||
|
||||
return {
|
||||
start: segStart,
|
||||
end: segEnd,
|
||||
isStart: isStart,
|
||||
isEnd: isEnd
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
|
||||
obj = obj || {};
|
||||
if (obj[name] !== undefined) {
|
||||
@@ -163,11 +213,20 @@ function smartProperty(obj, name) { // get a camel-cased/namespaced property of
|
||||
/* Date Utilities
|
||||
----------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
|
||||
|
||||
|
||||
// diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
|
||||
// Diffs the two moments into a Duration where only full-days are considered.
|
||||
// Moments will have their timezones normalized.
|
||||
function dayDiff(a, b) {
|
||||
return moment.duration({
|
||||
days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
|
||||
// Moments will have their timezones normalized.
|
||||
function dayishDiff(a, b) {
|
||||
return moment.duration({
|
||||
days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
|
||||
@@ -211,11 +270,6 @@ function extend(a, b) {
|
||||
}
|
||||
|
||||
|
||||
function flattenArray(a) { // flatten an array of arrays, one level deep
|
||||
return Array.prototype.concat.apply([], a);
|
||||
}
|
||||
|
||||
|
||||
function applyAll(functions, thisObj, args) {
|
||||
if ($.isFunction(functions)) {
|
||||
functions = [ functions ];
|
||||
|
||||
Reference in New Issue
Block a user