import * as $ from 'jquery'
import { htmlEscape, cssToStr } from '../util'
import EventRenderer from '../component/renderers/EventRenderer'
/* Event-rendering methods for the DayGrid class
----------------------------------------------------------------------------------------------------------------------*/
export default class DayGridEventRenderer extends EventRenderer {
dayGrid: any
rowStructs: any // an array of objects, each holding information about a row's foreground event-rendering
constructor(dayGrid, fillRenderer) {
super(dayGrid, fillRenderer)
this.dayGrid = dayGrid
}
renderBgRanges(eventRanges) {
// don't render timed background events
eventRanges = $.grep(eventRanges, function(eventRange: any) {
return eventRange.eventDef.isAllDay()
})
super.renderBgRanges(eventRanges)
}
// Renders the given foreground event segments onto the grid
renderFgSegs(segs) {
let rowStructs = this.rowStructs = this.renderSegRows(segs)
// append to each row's content skeleton
this.dayGrid.rowEls.each(function(i, rowNode) {
$(rowNode).find('.fc-content-skeleton > table').append(
rowStructs[i].tbodyEl
)
})
}
// Unrenders all currently rendered foreground event segments
unrenderFgSegs() {
let rowStructs = this.rowStructs || []
let rowStruct
while ((rowStruct = rowStructs.pop())) {
rowStruct.tbodyEl.remove()
}
this.rowStructs = null
}
// Uses the given events array to generate
elements that should be appended to each row's content skeleton.
// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
renderSegRows(segs) {
let rowStructs = []
let segRows
let row
segRows = this.groupSegRows(segs) // group into nested arrays
// iterate each row of segment groupings
for (row = 0; row < segRows.length; row++) {
rowStructs.push(
this.renderSegRow(row, segRows[row])
)
}
return rowStructs
}
// Given a row # and an array of segments all in the same row, render a element, a skeleton that contains
// the segments. Returns object with a bunch of internal data about how the render was calculated.
// NOTE: modifies rowSegs
renderSegRow(row, rowSegs) {
let colCnt = this.dayGrid.colCnt
let segLevels = this.buildSegLevels(rowSegs) // group into sub-arrays of levels
let levelCnt = Math.max(1, segLevels.length) // ensure at least one level
let tbody = $('')
let segMatrix = [] // lookup for which segments are rendered into which level+col cells
let cellMatrix = [] // lookup for all elements of the level+col matrix
let loneCellMatrix = [] // lookup for | elements that only take up a single column
let i
let levelSegs
let col
let tr
let j
let seg
let td
// populates empty cells from the current column (`col`) to `endCol`
function emptyCellsUntil(endCol) {
while (col < endCol) {
// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
td = (loneCellMatrix[i - 1] || [])[col]
if (td) {
td.attr(
'rowspan',
parseInt(td.attr('rowspan') || 1, 10) + 1
)
} else {
td = $(' | | ')
tr.append(td)
}
cellMatrix[i][col] = td
loneCellMatrix[i][col] = td
col++
}
}
for (i = 0; i < levelCnt; i++) { // iterate through all levels
levelSegs = segLevels[i]
col = 0
tr = $('
')
segMatrix.push([])
cellMatrix.push([])
loneCellMatrix.push([])
// levelCnt might be 1 even though there are no actual levels. protect against this.
// this single empty row is useful for styling.
if (levelSegs) {
for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
seg = levelSegs[j]
emptyCellsUntil(seg.leftCol)
// create a container that occupies or more columns. append the event element.
td = $(' | ').append(seg.el)
if (seg.leftCol !== seg.rightCol) {
td.attr('colspan', seg.rightCol - seg.leftCol + 1)
} else { // a single-column segment
loneCellMatrix[i][col] = td
}
while (col <= seg.rightCol) {
cellMatrix[i][col] = td
segMatrix[i][col] = seg
col++
}
tr.append(td)
}
}
emptyCellsUntil(colCnt) // finish off the row
this.dayGrid.bookendCells(tr)
tbody.append(tr)
}
return { // a "rowStruct"
row: row, // the row number
tbodyEl: tbody,
cellMatrix: cellMatrix,
segMatrix: segMatrix,
segLevels: segLevels,
segs: rowSegs
}
}
// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
// NOTE: modifies segs
buildSegLevels(segs) {
let levels = []
let i
let seg
let j
// Give preference to elements with certain criteria, so they have
// a chance to be closer to the top.
this.sortEventSegs(segs)
for (i = 0; i < segs.length; i++) {
seg = segs[i]
// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
for (j = 0; j < levels.length; j++) {
if (!isDaySegCollision(seg, levels[j])) {
break
}
}
// `j` now holds the desired subrow index
seg.level = j;
// create new level array if needed and append segment
(levels[j] || (levels[j] = [])).push(seg)
}
// order segments left-to-right. very important if calendar is RTL
for (j = 0; j < levels.length; j++) {
levels[j].sort(compareDaySegCols)
}
return levels
}
// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
groupSegRows(segs) {
let segRows = []
let i
for (i = 0; i < this.dayGrid.rowCnt; i++) {
segRows.push([])
}
for (i = 0; i < segs.length; i++) {
segRows[segs[i].row].push(segs[i])
}
return segRows
}
// Computes a default event time formatting string if `timeFormat` is not explicitly defined
computeEventTimeFormat() {
return this.opt('extraSmallTimeFormat') // like "6p" or "6:30p"
}
// Computes a default `displayEventEnd` value if one is not expliclty defined
computeDisplayEventEnd() {
return this.dayGrid.colCnt === 1 // we'll likely have space if there's only one day
}
// Builds the HTML to be used for the default element for an individual segment
fgSegHtml(seg, disableResizing) {
let view = this.view
let eventDef = seg.footprint.eventDef
let isAllDay = seg.footprint.componentFootprint.isAllDay
let isDraggable = view.isEventDefDraggable(eventDef)
let isResizableFromStart = !disableResizing && isAllDay &&
seg.isStart && view.isEventDefResizableFromStart(eventDef)
let isResizableFromEnd = !disableResizing && isAllDay &&
seg.isEnd && view.isEventDefResizableFromEnd(eventDef)
let classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd)
let skinCss = cssToStr(this.getSkinCss(eventDef))
let timeHtml = ''
let timeText
let titleHtml
classes.unshift('fc-day-grid-event', 'fc-h-event')
// Only display a timed events time if it is the starting segment
if (seg.isStart) {
timeText = this.getTimeText(seg.footprint)
if (timeText) {
timeHtml = '' + htmlEscape(timeText) + ''
}
}
titleHtml =
'' +
(htmlEscape(eventDef.title || '') || ' ') + // we always want one line of height
''
return '' +
'' +
(this.dayGrid.isRTL ?
titleHtml + ' ' + timeHtml : // put a natural space in between
timeHtml + ' ' + titleHtml //
) +
'
' +
(isResizableFromStart ?
'' :
''
) +
(isResizableFromEnd ?
'' :
''
) +
''
}
}
// Computes whether two segments' columns collide. They are assumed to be in the same row.
function isDaySegCollision(seg, otherSegs) {
let i
let otherSeg
for (i = 0; i < otherSegs.length; i++) {
otherSeg = otherSegs[i]
if (
otherSeg.leftCol <= seg.rightCol &&
otherSeg.rightCol >= seg.leftCol
) {
return true
}
}
return false
}
// A cmp function for determining the leftmost event
function compareDaySegCols(a, b) {
return a.leftCol - b.leftCol
}