BasicView.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import { htmlEscape } from '../util/html'
  2. import { createElement, findElements } from '../util/dom-manip'
  3. import {
  4. matchCellWidths,
  5. uncompensateScroll,
  6. compensateScroll,
  7. subtractInnerElHeight,
  8. distributeHeight,
  9. undistributeHeight
  10. } from '../util/misc'
  11. import { createFormatter } from '../datelib/formatting'
  12. import ScrollComponent from '../common/ScrollComponent'
  13. import View from '../View'
  14. import BasicViewDateProfileGenerator from './BasicViewDateProfileGenerator'
  15. import DayGrid from './DayGrid'
  16. import { DateProfile } from '../DateProfileGenerator'
  17. import { buildGotoAnchorHtml } from '../component/date-rendering'
  18. import { DateComponentProps } from '../component/DateComponent'
  19. const WEEK_NUM_FORMAT = createFormatter({ week: 'numeric' })
  20. /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
  21. ----------------------------------------------------------------------------------------------------------------------*/
  22. // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
  23. // It is responsible for managing width/height.
  24. export default class BasicView extends View {
  25. // initialized after class
  26. dayGridClass: any // class the dayGrid will be instantiated from (overridable by subclasses)
  27. scroller: ScrollComponent
  28. dayGrid: DayGrid // the main subcomponent that does most of the heavy lifting
  29. colWeekNumbersVisible: boolean = false
  30. weekNumberWidth: any // width of all the week-number cells running down the side
  31. initialize() {
  32. this.el.classList.add('fc-basic-view')
  33. this.el.innerHTML = this.renderSkeletonHtml()
  34. this.scroller = new ScrollComponent(
  35. 'hidden', // overflow x
  36. 'auto' // overflow y
  37. )
  38. let dayGridContainerEl = this.scroller.el
  39. this.el.querySelector('.fc-body > tr > td').appendChild(dayGridContainerEl)
  40. dayGridContainerEl.classList.add('fc-day-grid-container')
  41. let dayGridEl = createElement('div', { className: 'fc-day-grid' })
  42. dayGridContainerEl.appendChild(dayGridEl)
  43. this.dayGrid = this.instantiateDayGrid(
  44. this.el.querySelector('.fc-head-container'),
  45. dayGridEl
  46. )
  47. this.dayGrid.isRigid = this.hasRigidRows()
  48. if (this.opt('weekNumbers')) {
  49. if (this.opt('weekNumbersWithinDays')) {
  50. this.dayGrid.cellWeekNumbersVisible = true
  51. this.colWeekNumbersVisible = false
  52. } else {
  53. this.dayGrid.cellWeekNumbersVisible = false
  54. this.colWeekNumbersVisible = true
  55. }
  56. }
  57. }
  58. destroy() {
  59. super.destroy()
  60. this.dayGrid.destroy()
  61. this.scroller.destroy()
  62. }
  63. // Generates the DayGrid object this view needs. Draws from this.dayGridClass
  64. instantiateDayGrid(headerContainerEl: HTMLElement, el: HTMLElement) {
  65. // generate a subclass on the fly with BasicView-specific behavior
  66. // TODO: cache this subclass
  67. let subclass: any = makeDayGridSubclass(this.dayGridClass)
  68. return new subclass(this.context, headerContainerEl, el)
  69. }
  70. render(props: DateComponentProps) {
  71. super.render(props)
  72. this.dayGrid.receiveProps(props)
  73. }
  74. renderDates(dateProfile: DateProfile) {
  75. this.dayGrid.breakOnWeeks = /year|month|week/.test(
  76. this.props.dateProfile.currentRangeUnit
  77. )
  78. super.renderDates(dateProfile)
  79. }
  80. // Builds the HTML skeleton for the view.
  81. // The day-grid component will render inside of a container defined by this HTML.
  82. renderSkeletonHtml() {
  83. let { theme } = this
  84. return '' +
  85. '<table class="' + theme.getClass('tableGrid') + '">' +
  86. (this.opt('columnHeader') ?
  87. '<thead class="fc-head">' +
  88. '<tr>' +
  89. '<td class="fc-head-container ' + theme.getClass('widgetHeader') + '">&nbsp;</td>' +
  90. '</tr>' +
  91. '</thead>' :
  92. ''
  93. ) +
  94. '<tbody class="fc-body">' +
  95. '<tr>' +
  96. '<td class="' + theme.getClass('widgetContent') + '"></td>' +
  97. '</tr>' +
  98. '</tbody>' +
  99. '</table>'
  100. }
  101. // Generates an HTML attribute string for setting the width of the week number column, if it is known
  102. weekNumberStyleAttr() {
  103. if (this.weekNumberWidth != null) {
  104. return 'style="width:' + this.weekNumberWidth + 'px"'
  105. }
  106. return ''
  107. }
  108. // Determines whether each row should have a constant height
  109. hasRigidRows() {
  110. let eventLimit = this.opt('eventLimit')
  111. return eventLimit && typeof eventLimit !== 'number'
  112. }
  113. /* Dimensions
  114. ------------------------------------------------------------------------------------------------------------------*/
  115. // Refreshes the horizontal dimensions of the view
  116. updateHeight(totalHeight, isAuto, isResize) {
  117. super.updateHeight(totalHeight, isAuto, isResize)
  118. let { dayGrid } = this
  119. let eventLimit = this.opt('eventLimit')
  120. let headRowEl =
  121. dayGrid.headerContainerEl ?
  122. dayGrid.headerContainerEl.querySelector('.fc-row') as HTMLElement :
  123. null
  124. let scrollerHeight
  125. let scrollbarWidths
  126. // hack to give the view some height prior to dayGrid's columns being rendered
  127. // TODO: separate setting height from scroller VS dayGrid.
  128. if (!dayGrid.rowEls) {
  129. if (!isAuto) {
  130. scrollerHeight = this.computeScrollerHeight(totalHeight)
  131. this.scroller.setHeight(scrollerHeight)
  132. }
  133. return
  134. }
  135. if (this.colWeekNumbersVisible) {
  136. // Make sure all week number cells running down the side have the same width.
  137. // Record the width for cells created later.
  138. this.weekNumberWidth = matchCellWidths(
  139. findElements(this.el, '.fc-week-number')
  140. )
  141. }
  142. // reset all heights to be natural
  143. this.scroller.clear()
  144. if (headRowEl) {
  145. uncompensateScroll(headRowEl)
  146. }
  147. dayGrid.removeSegPopover() // kill the "more" popover if displayed
  148. // is the event limit a constant level number?
  149. if (eventLimit && typeof eventLimit === 'number') {
  150. dayGrid.limitRows(eventLimit) // limit the levels first so the height can redistribute after
  151. }
  152. // distribute the height to the rows
  153. // (totalHeight is a "recommended" value if isAuto)
  154. scrollerHeight = this.computeScrollerHeight(totalHeight)
  155. this.setGridHeight(scrollerHeight, isAuto)
  156. // is the event limit dynamically calculated?
  157. if (eventLimit && typeof eventLimit !== 'number') {
  158. dayGrid.limitRows(eventLimit) // limit the levels after the grid's row heights have been set
  159. }
  160. if (!isAuto) { // should we force dimensions of the scroll container?
  161. this.scroller.setHeight(scrollerHeight)
  162. scrollbarWidths = this.scroller.getScrollbarWidths()
  163. if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars?
  164. if (headRowEl) {
  165. compensateScroll(headRowEl, scrollbarWidths)
  166. }
  167. // doing the scrollbar compensation might have created text overflow which created more height. redo
  168. scrollerHeight = this.computeScrollerHeight(totalHeight)
  169. this.scroller.setHeight(scrollerHeight)
  170. }
  171. // guarantees the same scrollbar widths
  172. this.scroller.lockOverflow(scrollbarWidths)
  173. }
  174. }
  175. updateSize(isResize: boolean) {
  176. this.dayGrid.updateSize(isResize)
  177. }
  178. // given a desired total height of the view, returns what the height of the scroller should be
  179. computeScrollerHeight(totalHeight) {
  180. return totalHeight -
  181. subtractInnerElHeight(this.el, this.scroller.el) // everything that's NOT the scroller
  182. }
  183. // Sets the height of just the DayGrid component in this view
  184. setGridHeight(height, isAuto) {
  185. if (isAuto) {
  186. undistributeHeight(this.dayGrid.rowEls) // let the rows be their natural height with no expanding
  187. } else {
  188. distributeHeight(this.dayGrid.rowEls, height, true) // true = compensate for height-hogging rows
  189. }
  190. }
  191. /* Scroll
  192. ------------------------------------------------------------------------------------------------------------------*/
  193. computeInitialDateScroll() {
  194. return { top: 0 }
  195. }
  196. queryDateScroll() {
  197. return { top: this.scroller.getScrollTop() }
  198. }
  199. applyDateScroll(scroll) {
  200. if (scroll.top !== undefined) {
  201. this.scroller.setScrollTop(scroll.top)
  202. }
  203. }
  204. }
  205. BasicView.dateProfileGeneratorClass = BasicViewDateProfileGenerator
  206. BasicView.prototype.dayGridClass = DayGrid
  207. // customize the rendering behavior of BasicView's dayGrid
  208. function makeDayGridSubclass(SuperClass) {
  209. return class SubClass extends SuperClass {
  210. // Generates the HTML that will go before the day-of week header cells
  211. renderHeadIntroHtml(this: DayGrid) {
  212. let { view, theme } = this
  213. if ((view as BasicView).colWeekNumbersVisible) {
  214. return '' +
  215. '<th class="fc-week-number ' + theme.getClass('widgetHeader') + '" ' + (view as BasicView).weekNumberStyleAttr() + '>' +
  216. '<span>' + // needed for matchCellWidths
  217. htmlEscape(this.opt('weekLabel')) +
  218. '</span>' +
  219. '</th>'
  220. }
  221. return ''
  222. }
  223. // Generates the HTML that will go before content-skeleton cells that display the day/week numbers
  224. renderNumberIntroHtml(this: DayGrid, row) {
  225. let { view, dateEnv } = this
  226. let weekStart = this.getCellDate(row, 0)
  227. if ((view as BasicView).colWeekNumbersVisible) {
  228. return '' +
  229. '<td class="fc-week-number" ' + (view as BasicView).weekNumberStyleAttr() + '>' +
  230. buildGotoAnchorHtml( // aside from link, important for matchCellWidths
  231. view,
  232. { date: weekStart, type: 'week', forceOff: this.colCnt === 1 },
  233. dateEnv.format(weekStart, WEEK_NUM_FORMAT) // inner HTML
  234. ) +
  235. '</td>'
  236. }
  237. return ''
  238. }
  239. // Generates the HTML that goes before the day bg cells for each day-row
  240. renderBgIntroHtml(this: DayGrid) {
  241. let { view, theme } = this
  242. if ((view as BasicView).colWeekNumbersVisible) {
  243. return '<td class="fc-week-number ' + theme.getClass('widgetContent') + '" ' +
  244. (view as BasicView).weekNumberStyleAttr() + '></td>'
  245. }
  246. return ''
  247. }
  248. // Generates the HTML that goes before every other type of row generated by DayGrid.
  249. // Affects mirror-skeleton and highlight-skeleton rows.
  250. renderIntroHtml(this: DayGrid) {
  251. let { view } = this
  252. if ((view as BasicView).colWeekNumbersVisible) {
  253. return '<td class="fc-week-number" ' + (view as BasicView).weekNumberStyleAttr() + '></td>'
  254. }
  255. return ''
  256. }
  257. getIsNumbersVisible(this: DayGrid) {
  258. let { view } = this
  259. return DayGrid.prototype.getIsNumbersVisible.apply(this, arguments) ||
  260. (view as BasicView).colWeekNumbersVisible
  261. }
  262. }
  263. }