|
|
@@ -0,0 +1,368 @@
|
|
|
+
|
|
|
+/*
|
|
|
+A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
|
|
|
+Prerequisite: the object being mixed into needs to be a *Grid*
|
|
|
+*/
|
|
|
+var DayTableMixin = fc.DayTableMixin = {
|
|
|
+
|
|
|
+ breakOnWeeks: false, // should create a new row for each week?
|
|
|
+ dayDates: null, // whole-day dates for each column. left to right
|
|
|
+ dayIndices: null, // for each day from start, the offset
|
|
|
+ daysPerRow: null,
|
|
|
+ rowCnt: null,
|
|
|
+ colCnt: null,
|
|
|
+ colHeadFormat: null,
|
|
|
+
|
|
|
+
|
|
|
+ // Populates internal variables used for date calculation and rendering
|
|
|
+ updateDayTable: function() {
|
|
|
+ var view = this.view;
|
|
|
+ var date = this.start.clone();
|
|
|
+ var dayIndex = -1;
|
|
|
+ var dayIndices = [];
|
|
|
+ var dayDates = [];
|
|
|
+ var daysPerRow;
|
|
|
+ var firstDay;
|
|
|
+ var rowCnt;
|
|
|
+
|
|
|
+ while (date.isBefore(this.end)) { // loop each day from start to end
|
|
|
+ if (view.isHiddenDay(date)) {
|
|
|
+ dayIndices.push(dayIndex + 0.5); // mark that it's between indices
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ dayIndex++;
|
|
|
+ dayIndices.push(dayIndex);
|
|
|
+ dayDates.push(date.clone());
|
|
|
+ }
|
|
|
+ date.add(1, 'days');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.breakOnWeeks) {
|
|
|
+ // count columns until the day-of-week repeats
|
|
|
+ firstDay = dayDates[0].day();
|
|
|
+ for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) {
|
|
|
+ if (dayDates[daysPerRow].day() == firstDay) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ rowCnt = Math.ceil(dayDates.length / daysPerRow);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ rowCnt = 1;
|
|
|
+ daysPerRow = dayDates.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.dayDates = dayDates;
|
|
|
+ this.dayIndices = dayIndices;
|
|
|
+ this.daysPerRow = daysPerRow;
|
|
|
+ this.rowCnt = rowCnt;
|
|
|
+
|
|
|
+ this.updateDayTableCols();
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // Computes and assigned the colCnt property and updates any options that may be computed from it
|
|
|
+ updateDayTableCols: function() {
|
|
|
+ this.colCnt = this.computeColCnt();
|
|
|
+ this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat();
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // Determines how many columns there should be in the table
|
|
|
+ computeColCnt: function() {
|
|
|
+ return this.daysPerRow;
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // Computes the ambiguously-timed moment for the given cell
|
|
|
+ getCellDate: function(row, col) {
|
|
|
+ return this.dayDates[
|
|
|
+ this.getCellDayIndex(row, col)
|
|
|
+ ].clone();
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // Computes the ambiguously-timed date range for the given cell
|
|
|
+ getCellRange: function(row, col) {
|
|
|
+ var start = this.getCellDate(row, col);
|
|
|
+ var end = start.clone().add(1, 'days');
|
|
|
+
|
|
|
+ return { start: start, end: end };
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // Returns the number of day cells, chronologically, from the first of the grid (0-based)
|
|
|
+ getCellDayIndex: function(row, col) {
|
|
|
+ return row * this.daysPerRow + this.getColDayIndex(col);
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // Returns the numner of day cells, chronologically, from the first cell in *any given row*
|
|
|
+ getColDayIndex: function(col) {
|
|
|
+ if (this.isRTL) {
|
|
|
+ return this.colCnt - 1 - col;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ return col;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // Given a date, returns its chronolocial cell-index from the first cell of the grid.
|
|
|
+ // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
|
|
|
+ // If before the first offset, returns a negative number.
|
|
|
+ // If after the last offset, returns an offset past the last cell offset.
|
|
|
+ // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
|
|
|
+ getDateDayIndex: function(date) {
|
|
|
+ var dayIndices = this.dayIndices;
|
|
|
+ var dayOffset = date.diff(this.start, 'days');
|
|
|
+
|
|
|
+ if (dayOffset < 0) {
|
|
|
+ return dayIndices[0] - 1;
|
|
|
+ }
|
|
|
+ else if (dayOffset >= dayIndices.length) {
|
|
|
+ return dayIndices[dayIndices.length - 1] + 1;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ return dayIndices[dayOffset];
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ /* Options
|
|
|
+ ------------------------------------------------------------------------------------------------------------------*/
|
|
|
+
|
|
|
+
|
|
|
+ // Computes a default column header formatting string if `colFormat` is not explicitly defined
|
|
|
+ computeColHeadFormat: function() {
|
|
|
+ // if more than one week row, or if there are a lot of columns with not much space,
|
|
|
+ // put just the day numbers will be in each cell
|
|
|
+ if (this.rowCnt > 1 || this.colCnt > 10) {
|
|
|
+ return 'ddd'; // "Sat"
|
|
|
+ }
|
|
|
+ // multiple days, so full single date string WON'T be in title text
|
|
|
+ else if (this.colCnt > 1) {
|
|
|
+ return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
|
|
|
+ }
|
|
|
+ // single day, so full single date string will probably be in title text
|
|
|
+ else {
|
|
|
+ return 'dddd'; // "Saturday"
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ /* Slicing
|
|
|
+ ------------------------------------------------------------------------------------------------------------------*/
|
|
|
+
|
|
|
+
|
|
|
+ // Slices up a date range into a segment for every week-row it intersects with
|
|
|
+ sliceRangeByRow: function(range) {
|
|
|
+ var daysPerRow = this.daysPerRow;
|
|
|
+ var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
|
|
|
+ var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
|
|
|
+ var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
|
|
|
+ var segs = [];
|
|
|
+ var row;
|
|
|
+ var rowFirst, rowLast; // inclusive day-index range for current row
|
|
|
+ var segFirst, segLast; // inclusive day-index range for segment
|
|
|
+
|
|
|
+ for (row = 0; row < this.rowCnt; row++) {
|
|
|
+ rowFirst = row * daysPerRow;
|
|
|
+ rowLast = rowFirst + daysPerRow - 1;
|
|
|
+
|
|
|
+ // intersect segment's offset range with the row's
|
|
|
+ segFirst = Math.max(rangeFirst, rowFirst);
|
|
|
+ segLast = Math.min(rangeLast, rowLast);
|
|
|
+
|
|
|
+ // deal with in-between indices
|
|
|
+ segFirst = Math.ceil(segFirst); // in-between starts round to next cell
|
|
|
+ segLast = Math.floor(segLast); // in-between ends round to prev cell
|
|
|
+
|
|
|
+ if (segFirst <= segLast) { // was there any intersection with the current row?
|
|
|
+ segs.push({
|
|
|
+ row: row,
|
|
|
+
|
|
|
+ // normalize to start of row
|
|
|
+ firstRowDayIndex: segFirst - rowFirst,
|
|
|
+ lastRowDayIndex: segLast - rowFirst,
|
|
|
+
|
|
|
+ // must be matching integers to be the segment's start/end
|
|
|
+ isStart: segFirst === rangeFirst,
|
|
|
+ isEnd: segLast === rangeLast
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return segs;
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // Slices up a date range into a segment for every day-cell it intersects with.
|
|
|
+ // TODO: make more DRY with sliceRangeByRow somehow.
|
|
|
+ sliceRangeByDay: function(range) {
|
|
|
+ var daysPerRow = this.daysPerRow;
|
|
|
+ var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
|
|
|
+ var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
|
|
|
+ var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
|
|
|
+ var segs = [];
|
|
|
+ var row;
|
|
|
+ var rowFirst, rowLast; // inclusive day-index range for current row
|
|
|
+ var i;
|
|
|
+ var segFirst, segLast; // inclusive day-index range for segment
|
|
|
+
|
|
|
+ for (row = 0; row < this.rowCnt; row++) {
|
|
|
+ rowFirst = row * daysPerRow;
|
|
|
+ rowLast = rowFirst + daysPerRow - 1;
|
|
|
+
|
|
|
+ for (i = rowFirst; i <= rowLast; i++) {
|
|
|
+
|
|
|
+ // intersect segment's offset range with the row's
|
|
|
+ segFirst = Math.max(rangeFirst, i);
|
|
|
+ segLast = Math.min(rangeLast, i);
|
|
|
+
|
|
|
+ // deal with in-between indices
|
|
|
+ segFirst = Math.ceil(segFirst); // in-between starts round to next cell
|
|
|
+ segLast = Math.floor(segLast); // in-between ends round to prev cell
|
|
|
+
|
|
|
+ if (segFirst <= segLast) { // was there any intersection with the current row?
|
|
|
+ segs.push({
|
|
|
+ row: row,
|
|
|
+
|
|
|
+ // normalize to start of row
|
|
|
+ firstRowDayIndex: segFirst - rowFirst,
|
|
|
+ lastRowDayIndex: segLast - rowFirst,
|
|
|
+
|
|
|
+ // must be matching integers to be the segment's start/end
|
|
|
+ isStart: segFirst === rangeFirst,
|
|
|
+ isEnd: segLast === rangeLast
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return segs;
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ /* Header Rendering
|
|
|
+ ------------------------------------------------------------------------------------------------------------------*/
|
|
|
+
|
|
|
+
|
|
|
+ getHeadHtml: function() {
|
|
|
+ var view = this.view;
|
|
|
+
|
|
|
+ return '' +
|
|
|
+ '<div class="fc-row ' + view.widgetHeaderClass + '">' +
|
|
|
+ '<table>' +
|
|
|
+ '<thead>' +
|
|
|
+ this.getHeadTrHtml() +
|
|
|
+ '</thead>' +
|
|
|
+ '</table>' +
|
|
|
+ '</div>';
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ getHeadIntroHtml: function() {
|
|
|
+ return this.getIntroHtml(); // fall back to generic
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ getHeadTrHtml: function() {
|
|
|
+ return '' +
|
|
|
+ '<tr>' +
|
|
|
+ (this.isRTL ? '' : this.getHeadIntroHtml()) +
|
|
|
+ this.getHeadDateCellsHtml() +
|
|
|
+ (this.isRTL ? this.getHeadIntroHtml() : '') +
|
|
|
+ '</tr>';
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ getHeadDateCellsHtml: function() {
|
|
|
+ var htmls = [];
|
|
|
+ var col, date;
|
|
|
+
|
|
|
+ for (col = 0; col < this.colCnt; col++) {
|
|
|
+ date = this.getCellDate(0, col);
|
|
|
+ htmls.push(this.getHeadDateCellHtml(date));
|
|
|
+ }
|
|
|
+
|
|
|
+ return htmls.join('');
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ getHeadDateCellHtml: function(date, colspan) {
|
|
|
+ var view = this.view;
|
|
|
+
|
|
|
+ return '' +
|
|
|
+ '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '"' +
|
|
|
+ (colspan > 1 ? ' colspan="' + colspan + '"' : '') +
|
|
|
+ '>' +
|
|
|
+ htmlEscape(date.format(this.colHeadFormat)) +
|
|
|
+ '</th>';
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ /* Background Rendering
|
|
|
+ ------------------------------------------------------------------------------------------------------------------*/
|
|
|
+
|
|
|
+
|
|
|
+ getBgTrHtml: function(row) {
|
|
|
+ return '' +
|
|
|
+ '<tr>' +
|
|
|
+ (this.isRTL ? '' : this.getBgIntroHtml(row)) +
|
|
|
+ this.getBgCellsHtml(row) +
|
|
|
+ (this.isRTL ? this.getBgIntroHtml(row) : '') +
|
|
|
+ '</tr>';
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ getBgIntroHtml: function(row) {
|
|
|
+ return this.getIntroHtml(); // fall back to generic
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ getBgCellsHtml: function(row) {
|
|
|
+ var htmls = [];
|
|
|
+ var col, date;
|
|
|
+
|
|
|
+ for (col = 0; col < this.colCnt; col++) {
|
|
|
+ date = this.getCellDate(row, col);
|
|
|
+ htmls.push(this.getBgCellHtml(date));
|
|
|
+ }
|
|
|
+
|
|
|
+ return htmls.join('');
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ getBgCellHtml: function(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('YYYY-MM-DD') + '"' + // if date has a time, won't format it
|
|
|
+ '></td>';
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ /* Utils
|
|
|
+ ------------------------------------------------------------------------------------------------------------------*/
|
|
|
+
|
|
|
+
|
|
|
+ // Applies the generic "intro" and "outro" HTML to the given cells.
|
|
|
+ // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
|
|
|
+ bookendCells: function(trEl) {
|
|
|
+ var introHtml = this.getIntroHtml();
|
|
|
+
|
|
|
+ if (introHtml) {
|
|
|
+ if (this.isRTL) {
|
|
|
+ trEl.append(introHtml);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ trEl.prepend(introHtml);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+};
|