mirror of
https://github.com/wassname/fullcalendar.git
synced 2026-07-23 12:50:32 +08:00
541 lines
15 KiB
JavaScript
541 lines
15 KiB
JavaScript
|
|
|
|
function View(element, calendar, viewName) {
|
|
var t = this;
|
|
|
|
|
|
// exports
|
|
t.element = element;
|
|
t.calendar = calendar;
|
|
t.name = viewName;
|
|
t.opt = opt;
|
|
t.trigger = trigger;
|
|
t.isEventDraggable = isEventDraggable;
|
|
t.isEventResizable = isEventResizable;
|
|
t.setEventData = setEventData;
|
|
t.clearEventData = clearEventData;
|
|
t.eventEnd = eventEnd;
|
|
t.reportEventElement = reportEventElement;
|
|
t.triggerEventDestroy = triggerEventDestroy;
|
|
t.eventElementHandlers = eventElementHandlers;
|
|
t.showEvents = showEvents;
|
|
t.hideEvents = hideEvents;
|
|
t.eventDrop = eventDrop;
|
|
t.eventResize = eventResize;
|
|
// t.title
|
|
// t.start, t.end
|
|
// t.visStart, t.visEnd
|
|
|
|
|
|
// imports
|
|
var defaultEventEnd = t.defaultEventEnd;
|
|
var normalizeEvent = calendar.normalizeEvent; // in EventManager
|
|
var reportEventChange = calendar.reportEventChange;
|
|
|
|
|
|
// locals
|
|
var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events)
|
|
var eventElementsByID = {}; // eventID mapped to array of jQuery elements
|
|
var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system
|
|
var options = calendar.options;
|
|
|
|
|
|
|
|
function opt(name, viewNameOverride) {
|
|
var v = options[name];
|
|
if ($.isPlainObject(v)) {
|
|
return smartProperty(v, viewNameOverride || viewName);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
|
|
function trigger(name, thisObj) {
|
|
return calendar.trigger.apply(
|
|
calendar,
|
|
[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
|
|
);
|
|
}
|
|
|
|
|
|
|
|
/* Event Editable Boolean Calculations
|
|
------------------------------------------------------------------------------*/
|
|
|
|
|
|
function isEventDraggable(event) {
|
|
var source = event.source || {};
|
|
return firstDefined(
|
|
event.startEditable,
|
|
source.startEditable,
|
|
opt('eventStartEditable'),
|
|
event.editable,
|
|
source.editable,
|
|
opt('editable')
|
|
)
|
|
&& !opt('disableDragging'); // deprecated
|
|
}
|
|
|
|
|
|
function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
|
|
var source = event.source || {};
|
|
return firstDefined(
|
|
event.durationEditable,
|
|
source.durationEditable,
|
|
opt('eventDurationEditable'),
|
|
event.editable,
|
|
source.editable,
|
|
opt('editable')
|
|
)
|
|
&& !opt('disableResizing'); // deprecated
|
|
}
|
|
|
|
|
|
|
|
/* Event Data
|
|
------------------------------------------------------------------------------*/
|
|
|
|
|
|
function setEventData(events) { // events are already normalized at this point
|
|
eventsByID = {};
|
|
var i, len=events.length, event;
|
|
for (i=0; i<len; i++) {
|
|
event = events[i];
|
|
if (eventsByID[event._id]) {
|
|
eventsByID[event._id].push(event);
|
|
}else{
|
|
eventsByID[event._id] = [event];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function clearEventData() {
|
|
eventsByID = {};
|
|
eventElementsByID = {};
|
|
eventElementCouples = [];
|
|
}
|
|
|
|
|
|
// returns a Date object for an event's end
|
|
function eventEnd(event) {
|
|
return event.end ? cloneDate(event.end) : defaultEventEnd(event);
|
|
}
|
|
|
|
|
|
|
|
/* Event Elements
|
|
------------------------------------------------------------------------------*/
|
|
|
|
|
|
// report when view creates an element for an event
|
|
function reportEventElement(event, element) {
|
|
eventElementCouples.push({ event: event, element: element });
|
|
if (eventElementsByID[event._id]) {
|
|
eventElementsByID[event._id].push(element);
|
|
}else{
|
|
eventElementsByID[event._id] = [element];
|
|
}
|
|
}
|
|
|
|
|
|
function triggerEventDestroy() {
|
|
$.each(eventElementCouples, function(i, couple) {
|
|
t.trigger('eventDestroy', couple.event, couple.event, couple.element);
|
|
});
|
|
}
|
|
|
|
|
|
// attaches eventClick, eventMouseover, eventMouseout
|
|
function eventElementHandlers(event, eventElement) {
|
|
eventElement
|
|
.click(function(ev) {
|
|
if (!eventElement.hasClass('ui-draggable-dragging') &&
|
|
!eventElement.hasClass('ui-resizable-resizing')) {
|
|
return trigger('eventClick', this, event, ev);
|
|
}
|
|
})
|
|
.hover(
|
|
function(ev) {
|
|
trigger('eventMouseover', this, event, ev);
|
|
},
|
|
function(ev) {
|
|
trigger('eventMouseout', this, event, ev);
|
|
}
|
|
);
|
|
// TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
|
|
// TODO: same for resizing
|
|
}
|
|
|
|
|
|
function showEvents(event, exceptElement) {
|
|
eachEventElement(event, exceptElement, 'show');
|
|
}
|
|
|
|
|
|
function hideEvents(event, exceptElement) {
|
|
eachEventElement(event, exceptElement, 'hide');
|
|
}
|
|
|
|
|
|
function eachEventElement(event, exceptElement, funcName) {
|
|
// NOTE: there may be multiple events per ID (repeating events)
|
|
// and multiple segments per event
|
|
var elements = eventElementsByID[event._id],
|
|
i, len = elements.length;
|
|
for (i=0; i<len; i++) {
|
|
if (!exceptElement || elements[i][0] != exceptElement[0]) {
|
|
elements[i][funcName]();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* Event Modification Reporting
|
|
---------------------------------------------------------------------------------*/
|
|
|
|
|
|
function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
|
|
var oldAllDay = event.allDay;
|
|
var eventId = event._id;
|
|
moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
|
|
trigger(
|
|
'eventDrop',
|
|
e,
|
|
event,
|
|
dayDelta,
|
|
minuteDelta,
|
|
allDay,
|
|
function() {
|
|
// TODO: investigate cases where this inverse technique might not work
|
|
moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
|
|
reportEventChange(eventId);
|
|
},
|
|
ev,
|
|
ui
|
|
);
|
|
reportEventChange(eventId);
|
|
}
|
|
|
|
|
|
function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
|
|
var eventId = event._id;
|
|
elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
|
|
trigger(
|
|
'eventResize',
|
|
e,
|
|
event,
|
|
dayDelta,
|
|
minuteDelta,
|
|
function() {
|
|
// TODO: investigate cases where this inverse technique might not work
|
|
elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
|
|
reportEventChange(eventId);
|
|
},
|
|
ev,
|
|
ui
|
|
);
|
|
reportEventChange(eventId);
|
|
}
|
|
|
|
|
|
|
|
/* Event Modification Math
|
|
---------------------------------------------------------------------------------*/
|
|
|
|
|
|
function moveEvents(events, dayDelta, minuteDelta, allDay) {
|
|
minuteDelta = minuteDelta || 0;
|
|
for (var e, len=events.length, i=0; i<len; i++) {
|
|
e = events[i];
|
|
if (allDay !== undefined) {
|
|
e.allDay = allDay;
|
|
}
|
|
addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
|
|
if (e.end) {
|
|
e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
|
|
}
|
|
normalizeEvent(e, options);
|
|
}
|
|
}
|
|
|
|
|
|
function elongateEvents(events, dayDelta, minuteDelta) {
|
|
minuteDelta = minuteDelta || 0;
|
|
for (var e, len=events.length, i=0; i<len; i++) {
|
|
e = events[i];
|
|
e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
|
|
normalizeEvent(e, options);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ====================================================================================================
|
|
// Utilities for day "cells"
|
|
// ====================================================================================================
|
|
// The "basic" views are completely made up of day cells.
|
|
// The "agenda" views have day cells at the top "all day" slot.
|
|
// This was the obvious common place to put these utilities, but they should be abstracted out into
|
|
// a more meaningful class (like DayEventRenderer).
|
|
// ====================================================================================================
|
|
|
|
|
|
// For determining how a given "cell" translates into a "date":
|
|
//
|
|
// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
|
|
// Keep in mind that column indices are inverted with isRTL. This is taken into account.
|
|
//
|
|
// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
|
|
//
|
|
// 3. Convert the "day offset" into a "date" (a JavaScript Date object).
|
|
//
|
|
// The reverse transformation happens when transforming a date into a cell.
|
|
|
|
|
|
// exports
|
|
t.isHiddenDay = isHiddenDay;
|
|
t.skipHiddenDays = skipHiddenDays;
|
|
t.getCellsPerWeek = getCellsPerWeek;
|
|
t.dateToCell = dateToCell;
|
|
t.dateToDayOffset = dateToDayOffset;
|
|
t.dayOffsetToCellOffset = dayOffsetToCellOffset;
|
|
t.cellOffsetToCell = cellOffsetToCell;
|
|
t.cellToDate = cellToDate;
|
|
t.cellToCellOffset = cellToCellOffset;
|
|
t.cellOffsetToDayOffset = cellOffsetToDayOffset;
|
|
t.dayOffsetToDate = dayOffsetToDate;
|
|
t.rangeToSegments = rangeToSegments;
|
|
|
|
|
|
// internals
|
|
var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
|
|
var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
|
|
var cellsPerWeek;
|
|
var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
|
|
var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
|
|
var isRTL = opt('isRTL');
|
|
|
|
|
|
// initialize important internal variables
|
|
(function() {
|
|
|
|
if (opt('weekends') === false) {
|
|
hiddenDays.push(0, 6); // 0=sunday, 6=saturday
|
|
}
|
|
|
|
// Loop through a hypothetical week and determine which
|
|
// days-of-week are hidden. Record in both hashes (one is the reverse of the other).
|
|
for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
|
|
dayToCellMap[dayIndex] = cellIndex;
|
|
isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
|
|
if (!isHiddenDayHash[dayIndex]) {
|
|
cellToDayMap[cellIndex] = dayIndex;
|
|
cellIndex++;
|
|
}
|
|
}
|
|
|
|
cellsPerWeek = cellIndex;
|
|
if (!cellsPerWeek) {
|
|
throw 'invalid hiddenDays'; // all days were hidden? bad.
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
// Is the current day hidden?
|
|
// `day` is a day-of-week index (0-6), or a Date object
|
|
function isHiddenDay(day) {
|
|
if (typeof day == 'object') {
|
|
day = day.getDay();
|
|
}
|
|
return isHiddenDayHash[day];
|
|
}
|
|
|
|
|
|
function getCellsPerWeek() {
|
|
return cellsPerWeek;
|
|
}
|
|
|
|
|
|
// Keep incrementing the current day until it is no longer a hidden day.
|
|
// If the initial value of `date` is not a hidden day, don't do anything.
|
|
// Pass `isExclusive` as `true` if you are dealing with an end date.
|
|
// `inc` defaults to `1` (increment one day forward each time)
|
|
function skipHiddenDays(date, inc, isExclusive) {
|
|
inc = inc || 1;
|
|
while (
|
|
isHiddenDayHash[ ( date.getDay() + (isExclusive ? inc : 0) + 7 ) % 7 ]
|
|
) {
|
|
addDays(date, inc);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// TRANSFORMATIONS: cell -> cell offset -> day offset -> date
|
|
//
|
|
|
|
// cell -> date (combines all transformations)
|
|
// Possible arguments:
|
|
// - row, col
|
|
// - { row:#, col: # }
|
|
function cellToDate() {
|
|
var cellOffset = cellToCellOffset.apply(null, arguments);
|
|
var dayOffset = cellOffsetToDayOffset(cellOffset);
|
|
var date = dayOffsetToDate(dayOffset);
|
|
return date;
|
|
}
|
|
|
|
// cell -> cell offset
|
|
// Possible arguments:
|
|
// - row, col
|
|
// - { row:#, col:# }
|
|
function cellToCellOffset(row, col) {
|
|
var colCnt = t.getColCnt();
|
|
|
|
// rtl variables. wish we could pre-populate these. but where?
|
|
var dis = isRTL ? -1 : 1;
|
|
var dit = isRTL ? colCnt - 1 : 0;
|
|
|
|
if (typeof row == 'object') {
|
|
col = row.col;
|
|
row = row.row;
|
|
}
|
|
var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
|
|
|
|
return cellOffset;
|
|
}
|
|
|
|
// cell offset -> day offset
|
|
function cellOffsetToDayOffset(cellOffset) {
|
|
var day0 = t.visStart.getDay(); // first date's day of week
|
|
cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
|
|
return Math.floor(cellOffset / cellsPerWeek) * 7 // # of days from full weeks
|
|
+ cellToDayMap[ // # of days from partial last week
|
|
(cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
|
|
]
|
|
- day0; // adjustment for beginning-of-week normalization
|
|
}
|
|
|
|
// day offset -> date (JavaScript Date object)
|
|
function dayOffsetToDate(dayOffset) {
|
|
var date = cloneDate(t.visStart);
|
|
addDays(date, dayOffset);
|
|
return date;
|
|
}
|
|
|
|
|
|
//
|
|
// TRANSFORMATIONS: date -> day offset -> cell offset -> cell
|
|
//
|
|
|
|
// date -> cell (combines all transformations)
|
|
function dateToCell(date) {
|
|
var dayOffset = dateToDayOffset(date);
|
|
var cellOffset = dayOffsetToCellOffset(dayOffset);
|
|
var cell = cellOffsetToCell(cellOffset);
|
|
return cell;
|
|
}
|
|
|
|
// date -> day offset
|
|
function dateToDayOffset(date) {
|
|
return dayDiff(date, t.visStart);
|
|
}
|
|
|
|
// day offset -> cell offset
|
|
function dayOffsetToCellOffset(dayOffset) {
|
|
var day0 = t.visStart.getDay(); // first date's day of week
|
|
dayOffset += day0; // normalize dayOffset to beginning-of-week
|
|
return Math.floor(dayOffset / 7) * cellsPerWeek // # of cells from full weeks
|
|
+ dayToCellMap[ // # of cells from partial last week
|
|
(dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
|
|
]
|
|
- dayToCellMap[day0]; // adjustment for beginning-of-week normalization
|
|
}
|
|
|
|
// cell offset -> cell (object with row & col keys)
|
|
function cellOffsetToCell(cellOffset) {
|
|
var colCnt = t.getColCnt();
|
|
|
|
// rtl variables. wish we could pre-populate these. but where?
|
|
var dis = isRTL ? -1 : 1;
|
|
var dit = isRTL ? colCnt - 1 : 0;
|
|
|
|
var row = Math.floor(cellOffset / colCnt);
|
|
var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
|
|
return {
|
|
row: row,
|
|
col: col
|
|
};
|
|
}
|
|
|
|
|
|
//
|
|
// Converts a date range into an array of segment objects.
|
|
// "Segments" are horizontal stretches of time, sliced up by row.
|
|
// A segment object has the following properties:
|
|
// - row
|
|
// - cols
|
|
// - isStart
|
|
// - isEnd
|
|
//
|
|
function rangeToSegments(startDate, endDate) {
|
|
var rowCnt = t.getRowCnt();
|
|
var colCnt = t.getColCnt();
|
|
var segments = []; // array of segments to return
|
|
|
|
// day offset for given date range
|
|
var rangeDayOffsetStart = dateToDayOffset(startDate);
|
|
var rangeDayOffsetEnd = dateToDayOffset(endDate); // exclusive
|
|
|
|
// first and last cell offset for the given date range
|
|
// "last" implies inclusivity
|
|
var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
|
|
var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
|
|
|
|
// loop through all the rows in the view
|
|
for (var row=0; row<rowCnt; row++) {
|
|
|
|
// first and last cell offset for the row
|
|
var rowCellOffsetFirst = row * colCnt;
|
|
var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
|
|
|
|
// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
|
|
var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
|
|
var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
|
|
|
|
// make sure segment's offsets are valid and in view
|
|
if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
|
|
|
|
// translate to cells
|
|
var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
|
|
var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
|
|
|
|
// view might be RTL, so order by leftmost column
|
|
var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
|
|
|
|
// Determine if segment's first/last cell is the beginning/end of the date range.
|
|
// We need to compare "day offset" because "cell offsets" are often ambiguous and
|
|
// can translate to multiple days, and an edge case reveals itself when we the
|
|
// range's first cell is hidden (we don't want isStart to be true).
|
|
var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
|
|
var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; // +1 for comparing exclusively
|
|
|
|
segments.push({
|
|
row: row,
|
|
leftCol: cols[0],
|
|
rightCol: cols[1],
|
|
isStart: isStart,
|
|
isEnd: isEnd
|
|
});
|
|
}
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
|
|
}
|