mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-06-27 16:10:13 +08:00
Adding files
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
|
||||
/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
|
||||
----------------------------------------------------------------------------------------------------------------------*/
|
||||
// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
|
||||
// Responsible for managing width/height.
|
||||
|
||||
setDefaults({
|
||||
allDaySlot: true,
|
||||
allDayText: 'all-day',
|
||||
|
||||
scrollTime: '06:00:00',
|
||||
|
||||
slotDuration: '00:30:00',
|
||||
|
||||
axisFormat: generateAgendaAxisFormat,
|
||||
timeFormat: {
|
||||
agenda: generateAgendaTimeFormat
|
||||
},
|
||||
|
||||
minTime: '00:00:00',
|
||||
maxTime: '24:00:00',
|
||||
slotEventOverlap: true
|
||||
});
|
||||
|
||||
var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
|
||||
|
||||
|
||||
function generateAgendaAxisFormat(options, langData) {
|
||||
return langData.longDateFormat('LT')
|
||||
.replace(':mm', '(:mm)')
|
||||
.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
|
||||
.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
|
||||
}
|
||||
|
||||
|
||||
function generateAgendaTimeFormat(options, langData) {
|
||||
return langData.longDateFormat('LT')
|
||||
.replace(/\s*a$/i, ''); // remove trailing AM/PM
|
||||
}
|
||||
|
||||
|
||||
function HAgendaView(calendar) {
|
||||
View.call(this, calendar); // call the super-constructor
|
||||
|
||||
this.timeGrid = new HTimeGrid(this);
|
||||
|
||||
if (this.opt('allDaySlot')) { // should we display the "all-day" area?
|
||||
this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view
|
||||
|
||||
// the coordinate grid will be a combination of both subcomponents' grids
|
||||
this.coordMap = new ComboCoordMap([
|
||||
this.dayGrid.coordMap,
|
||||
this.timeGrid.coordMap
|
||||
]);
|
||||
}
|
||||
else {
|
||||
this.coordMap = this.timeGrid.coordMap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HAgendaView.prototype = createObject(View.prototype); // define the super-class
|
||||
$.extend(HAgendaView.prototype, {
|
||||
|
||||
timeGrid: null, // the main time-grid subcomponent of this view
|
||||
dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
|
||||
|
||||
axisWidth: null, // the width of the time axis running down the side
|
||||
|
||||
noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
|
||||
|
||||
// when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
|
||||
bottomRuleEl: null,
|
||||
bottomRuleHeight: null,
|
||||
|
||||
|
||||
/* Rendering
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders the view into `this.el`, which has already been assigned.
|
||||
// `colCnt` has been calculated by a subclass and passed here.
|
||||
render: function(colCnt) {
|
||||
|
||||
// needed for cell-to-date and date-to-cell calculations in View
|
||||
this.rowCnt = 1;
|
||||
this.colCnt = colCnt;
|
||||
|
||||
this.el.addClass('fc-agenda-view').html(this.renderHtml());
|
||||
|
||||
// the element that wraps the time-grid that will probably scroll
|
||||
this.scrollerEl = this.el.find('.fc-time-grid-container');
|
||||
this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this
|
||||
|
||||
this.timeGrid.el = this.el.find('.fc-time-grid');
|
||||
this.timeGrid.render();
|
||||
|
||||
// the <hr> that sometimes displays under the time-grid
|
||||
this.bottomRuleEl = $('<hr class="' + this.widgetHeaderClass + '"/>')
|
||||
.appendTo(this.timeGrid.el); // inject it into the time-grid
|
||||
|
||||
if (this.dayGrid) {
|
||||
this.dayGrid.el = this.el.find('.fc-day-grid');
|
||||
this.dayGrid.render();
|
||||
|
||||
// have the day-grid extend it's coordinate area over the <hr> dividing the two grids
|
||||
this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
|
||||
}
|
||||
|
||||
this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
|
||||
|
||||
View.prototype.render.call(this); // call the super-method
|
||||
|
||||
this.resetScroll(); // do this after sizes have been set
|
||||
},
|
||||
|
||||
|
||||
// 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() {
|
||||
return '' +
|
||||
'<table>' +
|
||||
'<thead>' +
|
||||
'<tr>' +
|
||||
'<td class="' + this.widgetHeaderClass + '">' +
|
||||
this.timeGrid.headHtml() + // render the day-of-week headers
|
||||
'</td>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
'<tbody>' +
|
||||
'<tr>' +
|
||||
'<td class="' + this.widgetContentClass + '">' +
|
||||
(this.dayGrid ?
|
||||
'<div class="fc-day-grid"/>' +
|
||||
'<hr class="' + this.widgetHeaderClass + '"/>' :
|
||||
''
|
||||
) +
|
||||
'<div class="fc-time-grid-container">' +
|
||||
'<div class="fc-time-grid"/>' +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'</tr>' +
|
||||
'</tbody>' +
|
||||
'</table>';
|
||||
},
|
||||
|
||||
|
||||
// Generates the HTML that will go before the day-of week header cells.
|
||||
// Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.
|
||||
headIntroHtml: function() {
|
||||
var date;
|
||||
var weekNumber;
|
||||
var weekTitle;
|
||||
var weekText;
|
||||
|
||||
if (this.opt('weekNumbers')) {
|
||||
date = this.cellToDate(0, 0);
|
||||
weekNumber = this.calendar.calculateWeekNumber(date);
|
||||
weekTitle = this.opt('weekNumberTitle');
|
||||
|
||||
if (this.opt('isRTL')) {
|
||||
weekText = weekNumber + weekTitle;
|
||||
}
|
||||
else {
|
||||
weekText = weekTitle + weekNumber;
|
||||
}
|
||||
|
||||
return '' +
|
||||
'<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' +
|
||||
'<span>' + // needed for matchCellWidths
|
||||
htmlEscape(weekText) +
|
||||
'</span>' +
|
||||
'</th>';
|
||||
}
|
||||
else {
|
||||
return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Generates the HTML that goes before the all-day cells.
|
||||
// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
|
||||
dayIntroHtml: function() {
|
||||
return '' +
|
||||
'<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' +
|
||||
'<span>' + // needed for matchCellWidths
|
||||
(this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +
|
||||
'</span>' +
|
||||
'</td>';
|
||||
},
|
||||
|
||||
|
||||
// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
|
||||
slotBgIntroHtml: function() {
|
||||
return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>';
|
||||
},
|
||||
|
||||
|
||||
// Generates the HTML that goes before all other types of cells.
|
||||
// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
|
||||
// Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.
|
||||
introHtml: function() {
|
||||
return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>';
|
||||
},
|
||||
|
||||
|
||||
// Generates an HTML attribute string for setting the width of the axis, if it is known
|
||||
axisStyleAttr: function() {
|
||||
if (this.axisWidth !== null) {
|
||||
return 'style="width:' + this.axisWidth + 'px"';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
|
||||
/* Dimensions
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
updateSize: function(isResize) {
|
||||
if (isResize) {
|
||||
this.timeGrid.resize();
|
||||
}
|
||||
View.prototype.updateSize.call(this, isResize);
|
||||
},
|
||||
|
||||
|
||||
// Refreshes the horizontal dimensions of the view
|
||||
updateWidth: function() {
|
||||
// make all axis cells line up, and record the width so newly created axis cells will have it
|
||||
this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
|
||||
},
|
||||
|
||||
|
||||
// Adjusts the vertical dimensions of the view to the specified values
|
||||
setHeight: function(totalHeight, isAuto) {
|
||||
var eventLimit;
|
||||
var scrollerHeight;
|
||||
|
||||
if (this.bottomRuleHeight === null) {
|
||||
// calculate the height of the rule the very first time
|
||||
this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
|
||||
}
|
||||
this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
|
||||
|
||||
// reset all dimensions back to the original state
|
||||
this.scrollerEl.css('overflow', '');
|
||||
unsetScroller(this.scrollerEl);
|
||||
uncompensateScroll(this.noScrollRowEls);
|
||||
|
||||
// limit number of events in the all-day area
|
||||
if (this.dayGrid) {
|
||||
this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
|
||||
|
||||
eventLimit = this.opt('eventLimit');
|
||||
if (eventLimit && typeof eventLimit !== 'number') {
|
||||
eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
|
||||
}
|
||||
if (eventLimit) {
|
||||
this.dayGrid.limitRows(eventLimit);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
|
||||
|
||||
scrollerHeight = this.computeScrollerHeight(totalHeight);
|
||||
if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
|
||||
|
||||
// make the all-day and header rows lines up
|
||||
compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
|
||||
|
||||
// the scrollbar compensation might have changed text flow, which might affect height, so recalculate
|
||||
// and reapply the desired height to the scroller.
|
||||
scrollerHeight = this.computeScrollerHeight(totalHeight);
|
||||
this.scrollerEl.height(scrollerHeight);
|
||||
|
||||
this.restoreScroll();
|
||||
}
|
||||
else { // no scrollbars
|
||||
// still, force a height and display the bottom rule (marks the end of day)
|
||||
this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside
|
||||
this.bottomRuleEl.show();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it.
|
||||
resetScroll: function() {
|
||||
var _this = this;
|
||||
var scrollTime = moment.duration(this.opt('scrollTime'));
|
||||
var top = this.timeGrid.computeTimeTop(scrollTime);
|
||||
|
||||
// zoom can give weird floating-point values. rather scroll a little bit further
|
||||
top = Math.ceil(top);
|
||||
|
||||
if (top) {
|
||||
top++; // to overcome top border that slots beyond the first have. looks better
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
_this.scrollerEl.scrollTop(top);
|
||||
}
|
||||
|
||||
scroll();
|
||||
setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
|
||||
},
|
||||
|
||||
|
||||
/* Events
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders events onto the view and populates the View's segment array
|
||||
renderEvents: function(events) {
|
||||
var dayEvents = [];
|
||||
var timedEvents = [];
|
||||
var daySegs = [];
|
||||
var timedSegs;
|
||||
var i;
|
||||
|
||||
// separate the events into all-day and timed
|
||||
for (i = 0; i < events.length; i++) {
|
||||
if (events[i].allDay) {
|
||||
dayEvents.push(events[i]);
|
||||
}
|
||||
else {
|
||||
timedEvents.push(events[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// render the events in the subcomponents
|
||||
timedSegs = this.timeGrid.renderEvents(timedEvents);
|
||||
if (this.dayGrid) {
|
||||
daySegs = this.dayGrid.renderEvents(dayEvents);
|
||||
}
|
||||
|
||||
// the all-day area is flexible and might have a lot of events, so shift the height
|
||||
this.updateHeight();
|
||||
|
||||
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() {
|
||||
View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared
|
||||
|
||||
// if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly
|
||||
// after, so remember what the scroll value was so we can restore it.
|
||||
this.recordScroll();
|
||||
|
||||
// destroy the events in the subcomponents
|
||||
this.timeGrid.destroyEvents();
|
||||
if (this.dayGrid) {
|
||||
this.dayGrid.destroyEvents();
|
||||
}
|
||||
|
||||
// we DON'T need to call updateHeight() because:
|
||||
// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
|
||||
// B) in IE8, this causes a flash whenever events are rerendered
|
||||
},
|
||||
|
||||
|
||||
/* Event Dragging
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders a visual indication of an event being dragged over the view.
|
||||
// A returned value of `true` signals that a mock "helper" event has been rendered.
|
||||
renderDrag: function(start, end, seg) {
|
||||
if (start.hasTime()) {
|
||||
return this.timeGrid.renderDrag(start, end, seg);
|
||||
}
|
||||
else if (this.dayGrid) {
|
||||
return this.dayGrid.renderDrag(start, end, seg);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Unrenders a visual indications of an event being dragged over the view
|
||||
destroyDrag: function() {
|
||||
this.timeGrid.destroyDrag();
|
||||
if (this.dayGrid) {
|
||||
this.dayGrid.destroyDrag();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/* Selection
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders a visual indication of a selection
|
||||
renderSelection: function(start, end) {
|
||||
if (start.hasTime() || end.hasTime()) {
|
||||
this.timeGrid.renderSelection(start, end);
|
||||
}
|
||||
else if (this.dayGrid) {
|
||||
this.dayGrid.renderSelection(start, end);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Unrenders a visual indications of a selection
|
||||
destroySelection: function() {
|
||||
this.timeGrid.destroySelection();
|
||||
if (this.dayGrid) {
|
||||
this.dayGrid.destroySelection();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
/* A day view with an all-day cell area at the top, and a time grid below by resource
|
||||
----------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
fcViews.hResourceDay = HResourceDayView;
|
||||
|
||||
function HResourceDayView(calendar) { // TODO: make a ResourceView mixin
|
||||
HResourceView.call(this, calendar); // call the super-constructor
|
||||
}
|
||||
|
||||
HResourceDayView.prototype = createObject(HResourceView.prototype); // define the super-class
|
||||
$.extend(HResourceDayView.prototype, {
|
||||
|
||||
name: 'hResourceDay',
|
||||
|
||||
|
||||
incrementDate: function(date, delta) {
|
||||
var out = date.clone().stripTime().add(delta, 'days');
|
||||
out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);
|
||||
return out;
|
||||
},
|
||||
|
||||
|
||||
render: function(date) {
|
||||
this.start = this.intervalStart = date.clone().stripTime();
|
||||
this.end = this.intervalEnd = this.start.clone().add(1, 'days');
|
||||
|
||||
this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));
|
||||
|
||||
HResourceView.prototype.render.call(this, 12); // call the super-method
|
||||
},
|
||||
|
||||
// Computes HTML classNames for a single-day cell
|
||||
getDayClasses: function(date) {
|
||||
var view = this.view;
|
||||
var today = view.calendar.getNow().stripTime();
|
||||
var classes = [ 'fc-' + dayIDs[date.day()] ];
|
||||
|
||||
if (
|
||||
view.name === 'month' &&
|
||||
date.month() != view.intervalStart.month()
|
||||
) {
|
||||
classes.push('fc-other-month');
|
||||
}
|
||||
|
||||
if (date.isSame(today, 'day')) {
|
||||
classes.push(
|
||||
'fc-todaysss',
|
||||
view.highlightStateClass
|
||||
);
|
||||
}
|
||||
else if (date < today) {
|
||||
classes.push('fc-past');
|
||||
}
|
||||
else {
|
||||
classes.push('fc-future');
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
/* A component that renders one or more columns of vertical time slots
|
||||
----------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
function HResourceTimeGrid(view) {
|
||||
TimeGrid.call(this, view); // call the super-constructor
|
||||
}
|
||||
|
||||
|
||||
HResourceTimeGrid.prototype = createObject(TimeGrid.prototype); // define the super-class
|
||||
$.extend(HResourceTimeGrid.prototype, {
|
||||
// Slices up a date range into a segment for each column
|
||||
// Each column represents a resource. An event can be assigned to multiple resources
|
||||
// so we need to build segs accordingly
|
||||
rangeToSegs: function(rangeStart, rangeEnd, resourceIds) {
|
||||
var view = this.view;
|
||||
var segs = [];
|
||||
var seg;
|
||||
var col;
|
||||
var cellDate;
|
||||
var colStart, colEnd;
|
||||
|
||||
var resources = view.calendar.fetchResources();
|
||||
|
||||
// normalize
|
||||
rangeStart = rangeStart.clone().stripZone();
|
||||
rangeEnd = rangeEnd.clone().stripZone();
|
||||
|
||||
for (col = 0; col < view.colCnt; col++) {
|
||||
var resource = resources[col];
|
||||
if (resource && resourceIds && resourceIds.indexOf(resource.id) > -1){
|
||||
cellDate = view.cellToDate(0, col); // use the View's cell system for this
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return segs;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
|
||||
----------------------------------------------------------------------------------------------------------------------*/
|
||||
// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
|
||||
// Responsible for managing width/height.
|
||||
|
||||
setDefaults({
|
||||
allDaySlot: false,
|
||||
allDayText: 'all-day',
|
||||
|
||||
scrollTime: '06:00:00',
|
||||
|
||||
slotDuration: '00:30:00',
|
||||
|
||||
axisFormat: generateAgendaAxisFormat,
|
||||
timeFormat: {
|
||||
agenda: generateAgendaTimeFormat
|
||||
},
|
||||
|
||||
minTime: '00:00:00',
|
||||
maxTime: '24:00:00',
|
||||
slotEventOverlap: true
|
||||
});
|
||||
|
||||
function HResourceView(calendar) {
|
||||
View.call(this, calendar); // call the super-constructor
|
||||
|
||||
// overrides - the view.js should expose these on the prototype then we wouldn't have to do this.
|
||||
this.cellToDate = HResourceView.prototype.cellToDate;
|
||||
|
||||
this.timeGrid = new HResourceTimeGrid(this);
|
||||
|
||||
// if (this.opt('allDaySlot')) { // should we display the "all-day" area?
|
||||
// this.dayGrid = new ResourceDayGrid(this); // the all-day subcomponent of this view
|
||||
|
||||
// // the coordinate grid will be a combination of both subcomponents' grids
|
||||
// this.coordMap = new ComboCoordMap([
|
||||
// this.dayGrid.coordMap,
|
||||
// this.timeGrid.coordMap
|
||||
// ]);
|
||||
// }
|
||||
// else {
|
||||
this.coordMap = this.timeGrid.coordMap;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
HResourceView.prototype = createObject(HAgendaView.prototype); // define the super-class
|
||||
$.extend(HResourceView.prototype, {
|
||||
|
||||
cellToDate: function() {
|
||||
return this.start.clone();
|
||||
},
|
||||
|
||||
// fix the classes, etc.
|
||||
headCellHtml: function(row, col, date) {
|
||||
var view = this;
|
||||
var dateCell = date.add(col, 'hour').clone();
|
||||
|
||||
return '' +
|
||||
'<th class="fc-day-header ' + view.widgetHeaderClass + '">' +
|
||||
htmlEscape(dateCell.format('HH:mm')) +
|
||||
'</th>';
|
||||
}
|
||||
|
||||
// slotBgCellHtml: function(row, col, date) {
|
||||
// var view = this.view;
|
||||
// var classes = this.getDayClasses(date);
|
||||
|
||||
// classes.unshift('fc-day', view.widgetContentClass);
|
||||
|
||||
// return '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '"></td>';
|
||||
// }
|
||||
|
||||
// slotBgCellHtml: function(row, col, date) {
|
||||
// var view = this.view;
|
||||
// var classes = this.getDayClasses(date);
|
||||
|
||||
// classes.unshift('fc-day', view.widgetContentClass);
|
||||
|
||||
// return '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '"></td>';
|
||||
// }
|
||||
|
||||
});
|
||||
@@ -0,0 +1,462 @@
|
||||
|
||||
/* A component that renders one or more columns of vertical time slots
|
||||
----------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
function HTimeGrid(view) {
|
||||
Grid.call(this, view); // call the super-constructor
|
||||
}
|
||||
|
||||
|
||||
HTimeGrid.prototype = createObject(Grid.prototype); // define the super-class
|
||||
$.extend(HTimeGrid.prototype, {
|
||||
|
||||
slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
|
||||
snapDuration: null, // granularity of time for dragging and selecting
|
||||
|
||||
minTime: null, // Duration object that denotes the first visible time of any given day
|
||||
maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
|
||||
|
||||
dayEls: null, // cells elements in the day-row background
|
||||
slatEls: null, // elements running horizontally across all columns
|
||||
|
||||
slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
|
||||
|
||||
highlightEl: null, // cell skeleton element for rendering the highlight
|
||||
helperEl: null, // cell skeleton element for rendering the mock event "helper"
|
||||
|
||||
|
||||
// Renders the time grid into `this.el`, which should already be assigned.
|
||||
// Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
|
||||
render: function() {
|
||||
this.processOptions();
|
||||
|
||||
this.el.html(this.renderHtml());
|
||||
|
||||
this.dayEls = this.el.find('.fc-day');
|
||||
this.slatEls = this.el.find('.fc-slats tr');
|
||||
|
||||
this.computeSlatTops();
|
||||
|
||||
Grid.prototype.render.call(this); // call the super-method
|
||||
},
|
||||
|
||||
|
||||
// Renders the basic HTML skeleton for the grid
|
||||
renderHtml: function() {
|
||||
return '' +
|
||||
'<div class="fc-bg">' +
|
||||
'<table>' +
|
||||
this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
|
||||
'</table>' +
|
||||
'</div>' +
|
||||
'<div class="fc-slats">' +
|
||||
'<table>' +
|
||||
this.slatRowHtml() +
|
||||
'</table>' +
|
||||
'</div>';
|
||||
},
|
||||
|
||||
|
||||
// Renders the HTML for a vertical background cell behind the slots.
|
||||
// This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.
|
||||
slotBgCellHtml: function(row, col, date) {
|
||||
return this.bgCellHtml(row, col, date);
|
||||
},
|
||||
|
||||
|
||||
// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
|
||||
slatRowHtml: function() {
|
||||
debugger;
|
||||
var view = this.view;
|
||||
var calendar = view.calendar;
|
||||
var isRTL = view.opt('isRTL');
|
||||
var html = '';
|
||||
var slotNormal = this.slotDuration.asMinutes() % 15 === 0;
|
||||
var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
|
||||
var slotDate; // will be on the view's first day, but we only care about its time
|
||||
var minutes;
|
||||
var axisHtml;
|
||||
|
||||
// Calculate the time for each slot
|
||||
while (slotTime < this.maxTime) {
|
||||
slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues
|
||||
minutes = slotDate.minutes();
|
||||
|
||||
axisHtml =
|
||||
'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
|
||||
((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time
|
||||
'<span>' + // for matchCellWidths
|
||||
htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) +
|
||||
'</span>' :
|
||||
''
|
||||
) +
|
||||
'</td>';
|
||||
|
||||
html +=
|
||||
'<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' +
|
||||
(!isRTL ? axisHtml : '') +
|
||||
'<td class="' + view.widgetContentClass + '"/>' +
|
||||
(isRTL ? axisHtml : '') +
|
||||
"</tr>";
|
||||
|
||||
slotTime.add(this.slotDuration);
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
|
||||
// Parses various options into properties of this object
|
||||
processOptions: function() {
|
||||
var view = this.view;
|
||||
var slotDuration = view.opt('slotDuration');
|
||||
var snapDuration = view.opt('snapDuration');
|
||||
|
||||
slotDuration = moment.duration(slotDuration);
|
||||
snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
|
||||
|
||||
this.slotDuration = slotDuration;
|
||||
this.snapDuration = snapDuration;
|
||||
this.cellDuration = snapDuration; // important to assign this for Grid.events.js
|
||||
|
||||
this.minTime = moment.duration(view.opt('minTime'));
|
||||
this.maxTime = moment.duration(view.opt('maxTime'));
|
||||
},
|
||||
|
||||
|
||||
// Slices up a date range into a segment for each column
|
||||
rangeToSegs: function(rangeStart, rangeEnd) {
|
||||
var view = this.view;
|
||||
var segs = [];
|
||||
var seg;
|
||||
var col;
|
||||
var cellDate;
|
||||
var colStart, colEnd;
|
||||
|
||||
// normalize
|
||||
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().time(this.minTime);
|
||||
colEnd = cellDate.clone().time(this.maxTime);
|
||||
seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd);
|
||||
if (seg) {
|
||||
seg.col = col;
|
||||
segs.push(seg);
|
||||
}
|
||||
}
|
||||
|
||||
return segs;
|
||||
},
|
||||
|
||||
|
||||
/* Coordinates
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Called when there is a window resize/zoom and we need to recalculate coordinates for the grid
|
||||
resize: function() {
|
||||
this.computeSlatTops();
|
||||
this.updateSegVerticals();
|
||||
},
|
||||
|
||||
|
||||
// Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells.
|
||||
// "Snap" cells are different the slots because they might have finer granularity.
|
||||
buildCoords: function(rows, cols) {
|
||||
var colCnt = this.view.colCnt;
|
||||
var originTop = this.el.offset().top;
|
||||
var snapTime = moment.duration(+this.minTime);
|
||||
var p = null;
|
||||
var e, n;
|
||||
|
||||
this.dayEls.slice(0, colCnt).each(function(i, _e) {
|
||||
e = $(_e);
|
||||
n = e.offset().left;
|
||||
if (p) {
|
||||
p[1] = n;
|
||||
}
|
||||
p = [ n ];
|
||||
cols[i] = p;
|
||||
});
|
||||
p[1] = n + e.outerWidth();
|
||||
|
||||
p = null;
|
||||
while (snapTime < this.maxTime) {
|
||||
n = originTop + this.computeTimeTop(snapTime);
|
||||
if (p) {
|
||||
p[1] = n;
|
||||
}
|
||||
p = [ n ];
|
||||
rows.push(p);
|
||||
snapTime.add(this.snapDuration);
|
||||
}
|
||||
p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end
|
||||
},
|
||||
|
||||
|
||||
// Gets the datetime for the given slot cell
|
||||
getCellDate: function(cell) {
|
||||
var view = this.view;
|
||||
var calendar = view.calendar;
|
||||
|
||||
return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone
|
||||
view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column
|
||||
.time(this.minTime + this.snapDuration * cell.row)
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
// Gets the element that represents the whole-day the cell resides on
|
||||
getCellDayEl: function(cell) {
|
||||
return this.dayEls.eq(cell.col);
|
||||
},
|
||||
|
||||
|
||||
// Computes the top coordinate, relative to the bounds of the grid, of the given date.
|
||||
// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
|
||||
computeDateTop: function(date, startOfDayDate) {
|
||||
return this.computeTimeTop(
|
||||
moment.duration(
|
||||
date.clone().stripZone() - startOfDayDate.clone().stripTime()
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
|
||||
computeTimeTop: function(time) {
|
||||
var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
|
||||
var slatIndex;
|
||||
var slatRemainder;
|
||||
var slatTop;
|
||||
var slatBottom;
|
||||
|
||||
// constrain. because minTime/maxTime might be customized
|
||||
slatCoverage = Math.max(0, slatCoverage);
|
||||
slatCoverage = Math.min(this.slatEls.length, slatCoverage);
|
||||
|
||||
slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot
|
||||
slatRemainder = slatCoverage - slatIndex;
|
||||
slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot
|
||||
|
||||
if (slatRemainder) { // time spans part-way into the slot
|
||||
slatBottom = this.slatTops[slatIndex + 1];
|
||||
return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots
|
||||
}
|
||||
else {
|
||||
return slatTop;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.
|
||||
// Includes the the bottom of the last slat as the last item in the array.
|
||||
computeSlatTops: function() {
|
||||
var tops = [];
|
||||
var top;
|
||||
|
||||
this.slatEls.each(function(i, node) {
|
||||
top = $(node).position().top;
|
||||
tops.push(top);
|
||||
});
|
||||
|
||||
tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat
|
||||
|
||||
this.slatTops = tops;
|
||||
},
|
||||
|
||||
|
||||
/* Event Drag Visualization
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders a visual indication of an event being dragged over the specified date(s).
|
||||
// `end` and `seg` can be null. See View's documentation on renderDrag for more info.
|
||||
renderDrag: function(start, end, seg) {
|
||||
var opacity;
|
||||
|
||||
if (seg) { // if there is event information for this drag, render a helper event
|
||||
this.renderRangeHelper(start, end, seg);
|
||||
|
||||
opacity = this.view.opt('dragOpacity');
|
||||
if (opacity !== undefined) {
|
||||
this.helperEl.css('opacity', opacity);
|
||||
}
|
||||
|
||||
return true; // signal that a helper has been rendered
|
||||
}
|
||||
else {
|
||||
// otherwise, just render a highlight
|
||||
this.renderHighlight(
|
||||
start,
|
||||
end || this.view.calendar.getDefaultEventEnd(false, start)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Unrenders any visual indication of an event being dragged
|
||||
destroyDrag: function() {
|
||||
this.destroyHelper();
|
||||
this.destroyHighlight();
|
||||
},
|
||||
|
||||
|
||||
/* Event Resize Visualization
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders a visual indication of an event being resized
|
||||
renderResize: function(start, end, seg) {
|
||||
this.renderRangeHelper(start, end, seg);
|
||||
},
|
||||
|
||||
|
||||
// Unrenders any visual indication of an event being resized
|
||||
destroyResize: function() {
|
||||
this.destroyHelper();
|
||||
},
|
||||
|
||||
|
||||
/* Event Helper
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
|
||||
renderHelper: function(event, sourceSeg) {
|
||||
var res = this.renderEventTable([ event ]);
|
||||
var tableEl = res.tableEl;
|
||||
var segs = res.segs;
|
||||
var i, seg;
|
||||
var sourceEl;
|
||||
|
||||
// Try to make the segment that is in the same row as sourceSeg look the same
|
||||
for (i = 0; i < segs.length; i++) {
|
||||
seg = segs[i];
|
||||
if (sourceSeg && sourceSeg.col === seg.col) {
|
||||
sourceEl = sourceSeg.el;
|
||||
seg.el.css({
|
||||
left: sourceEl.css('left'),
|
||||
right: sourceEl.css('right'),
|
||||
'margin-left': sourceEl.css('margin-left'),
|
||||
'margin-right': sourceEl.css('margin-right')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.helperEl = $('<div class="fc-helper-skeleton"/>')
|
||||
.append(tableEl)
|
||||
.appendTo(this.el);
|
||||
},
|
||||
|
||||
|
||||
// Unrenders any mock helper event
|
||||
destroyHelper: function() {
|
||||
if (this.helperEl) {
|
||||
this.helperEl.remove();
|
||||
this.helperEl = null;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/* Selection
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
|
||||
renderSelection: function(start, end) {
|
||||
if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
|
||||
this.renderRangeHelper(start, end);
|
||||
}
|
||||
else {
|
||||
this.renderHighlight(start, end);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Unrenders any visual indication of a selection
|
||||
destroySelection: function() {
|
||||
this.destroyHelper();
|
||||
this.destroyHighlight();
|
||||
},
|
||||
|
||||
|
||||
/* Highlight
|
||||
------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
|
||||
renderHighlight: function(start, end) {
|
||||
this.highlightEl = $(
|
||||
this.highlightSkeletonHtml(start, end)
|
||||
).appendTo(this.el);
|
||||
},
|
||||
|
||||
|
||||
// Unrenders the emphasis on a date range
|
||||
destroyHighlight: function() {
|
||||
if (this.highlightEl) {
|
||||
this.highlightEl.remove();
|
||||
this.highlightEl = null;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Generates HTML for a table element with containers in each column, responsible for absolutely positioning the
|
||||
// highlight elements to cover the highlighted slots.
|
||||
highlightSkeletonHtml: function(start, end) {
|
||||
var view = this.view;
|
||||
var segs = this.rangeToSegs(start, end);
|
||||
var cellHtml = '';
|
||||
var col = 0;
|
||||
var i, seg;
|
||||
var dayDate;
|
||||
var top, bottom;
|
||||
|
||||
for (i = 0; i < segs.length; i++) { // loop through the segments. one per column
|
||||
seg = segs[i];
|
||||
|
||||
// need empty cells beforehand?
|
||||
if (col < seg.col) {
|
||||
cellHtml += '<td colspan="' + (seg.col - col) + '"/>';
|
||||
col = seg.col;
|
||||
}
|
||||
|
||||
// compute vertical position
|
||||
dayDate = view.cellToDate(0, col);
|
||||
top = this.computeDateTop(seg.start, dayDate);
|
||||
bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge
|
||||
|
||||
// generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the
|
||||
// bottom edge of the zero-height container.
|
||||
cellHtml +=
|
||||
'<td>' +
|
||||
'<div class="fc-highlight-container">' +
|
||||
'<div class="fc-highlight" style="top:' + top + 'px;bottom:-' + bottom + 'px"/>' +
|
||||
'</div>' +
|
||||
'</td>';
|
||||
|
||||
col++;
|
||||
}
|
||||
|
||||
// need empty cells after the last segment?
|
||||
if (col < view.colCnt) {
|
||||
cellHtml += '<td colspan="' + (view.colCnt - col) + '"/>';
|
||||
}
|
||||
|
||||
cellHtml = this.bookendCells(cellHtml, 'highlight');
|
||||
|
||||
return '' +
|
||||
'<div class="fc-highlight-skeleton">' +
|
||||
'<table>' +
|
||||
'<tr>' +
|
||||
cellHtml +
|
||||
'</tr>' +
|
||||
'</table>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
});
|
||||
@@ -41,6 +41,12 @@
|
||||
<script src='../src/agenda/AgendaDayView.js'></script>
|
||||
<script src='../src/resource/ResourceView.js'></script>
|
||||
<script src='../src/resource/ResourceDayView.js'></script>
|
||||
|
||||
<script src='../src/resource/gantt/HResourceTimeGrid.js'></script>
|
||||
<script src='../src/resource/gantt/HTimeGrid.js'></script>
|
||||
<script src='../src/resource/gantt/HAgendaView.js'></script>
|
||||
<script src='../src/resource/gantt/HResourceView.js'></script>
|
||||
<script src='../src/resource/gantt/HResourceDayView.js'></script>
|
||||
<script src='../dist/gcal.js'></script>
|
||||
<script>
|
||||
var cal, staticEvents;
|
||||
|
||||
Reference in New Issue
Block a user