InteractiveDateComponent.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import * as moment from 'moment'
  2. import { diffByUnit, diffDayTime } from '../util/date'
  3. import { elementClosest } from '../util/dom-manip'
  4. import { getEvIsTouch, listenBySelector, listenToHoverBySelector } from '../util/dom-event'
  5. import DateComponent from './DateComponent'
  6. import GlobalEmitter from '../common/GlobalEmitter'
  7. export default abstract class InteractiveDateComponent extends DateComponent {
  8. dateClickingClass: any
  9. dateSelectingClass: any
  10. eventPointingClass: any
  11. eventDraggingClass: any
  12. eventResizingClass: any
  13. externalDroppingClass: any
  14. dateClicking: any
  15. dateSelecting: any
  16. eventPointing: any
  17. eventDragging: any
  18. eventResizing: any
  19. externalDropping: any
  20. // self-config, overridable by subclasses
  21. segSelector: string = '.fc-event-container > *' // what constitutes an event element?
  22. // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
  23. // of the date areas. if not defined, assumes to be day and time granularity.
  24. // TODO: port isTimeScale into same system?
  25. largeUnit: any
  26. constructor(_view?, _options?) {
  27. super(_view, _options)
  28. if (this.dateSelectingClass) {
  29. this.dateClicking = new this.dateClickingClass(this)
  30. }
  31. if (this.dateSelectingClass) {
  32. this.dateSelecting = new this.dateSelectingClass(this)
  33. }
  34. if (this.eventPointingClass) {
  35. this.eventPointing = new this.eventPointingClass(this)
  36. }
  37. if (this.eventDraggingClass && this.eventPointing) {
  38. this.eventDragging = new this.eventDraggingClass(this, this.eventPointing)
  39. }
  40. if (this.eventResizingClass && this.eventPointing) {
  41. this.eventResizing = new this.eventResizingClass(this, this.eventPointing)
  42. }
  43. if (this.externalDroppingClass) {
  44. this.externalDropping = new this.externalDroppingClass(this)
  45. }
  46. }
  47. // Sets the container element that the view should render inside of, does global DOM-related initializations,
  48. // and renders all the non-date-related content inside.
  49. setElement(el) {
  50. super.setElement(el)
  51. if (this.dateClicking) {
  52. this.dateClicking.bindToEl(el)
  53. }
  54. if (this.dateSelecting) {
  55. this.dateSelecting.bindToEl(el)
  56. }
  57. this.bindAllSegHandlersToEl(el)
  58. }
  59. removeElement() {
  60. this.endInteractions()
  61. super.removeElement()
  62. }
  63. executeEventUnrender() {
  64. this.endInteractions()
  65. super.executeEventUnrender()
  66. }
  67. bindGlobalHandlers() {
  68. super.bindGlobalHandlers()
  69. if (this.externalDropping) {
  70. this.externalDropping.bindToDocument()
  71. }
  72. }
  73. unbindGlobalHandlers() {
  74. super.unbindGlobalHandlers()
  75. if (this.externalDropping) {
  76. this.externalDropping.unbindFromDocument()
  77. }
  78. }
  79. bindDateHandlerToEl(el, name, handler) {
  80. el.addEventListener(name, (ev) => {
  81. if (
  82. !elementClosest(
  83. ev.target,
  84. this.segSelector + ':not(.fc-helper),' + // on or within an event segment
  85. '.fc-more,' + // a "more.." link
  86. 'a[data-goto]' // a clickable nav link
  87. )
  88. ) {
  89. return handler.call(this, ev)
  90. }
  91. })
  92. }
  93. bindAllSegHandlersToEl(el) {
  94. [
  95. this.eventPointing,
  96. this.eventDragging,
  97. this.eventResizing
  98. ].forEach(function(eventInteraction) {
  99. if (eventInteraction) {
  100. eventInteraction.bindToEl(el)
  101. }
  102. })
  103. }
  104. bindSegHandlerToEl(el, name, handler) {
  105. listenBySelector(
  106. el,
  107. name,
  108. this.segSelector,
  109. this.makeSegMouseHandler(handler)
  110. )
  111. }
  112. bindSegHoverHandlersToEl(el, onMouseEnter, onMouseLeave) {
  113. listenToHoverBySelector(
  114. el,
  115. this.segSelector,
  116. this.makeSegMouseHandler(onMouseEnter),
  117. this.makeSegMouseHandler(onMouseLeave)
  118. )
  119. }
  120. makeSegMouseHandler(handler) {
  121. return (ev, segEl) => {
  122. if (!segEl.classList.contains('fc-helper')) {
  123. let seg = (segEl as any).fcSeg // grab segment data. put there by View::renderEventsPayload
  124. if (seg && !this.shouldIgnoreEventPointing()) {
  125. return handler.call(this, seg, ev) // context will be the Grid
  126. }
  127. }
  128. }
  129. }
  130. shouldIgnoreMouse() {
  131. // HACK
  132. // This will still work even though bindDateHandlerToEl doesn't use GlobalEmitter.
  133. return GlobalEmitter.get().shouldIgnoreMouse()
  134. }
  135. shouldIgnoreTouch() {
  136. let view = this._getView()
  137. // On iOS (and Android?) when a new selection is initiated overtop another selection,
  138. // the touchend never fires because the elements gets removed mid-touch-interaction (my theory).
  139. // HACK: simply don't allow this to happen.
  140. // ALSO: prevent selection when an *event* is already raised.
  141. return view.isSelected || view.selectedEvent
  142. }
  143. shouldIgnoreEventPointing() {
  144. // only call the handlers if there is not a drag/resize in progress
  145. return (this.eventDragging && this.eventDragging.isDragging) ||
  146. (this.eventResizing && this.eventResizing.isResizing)
  147. }
  148. canStartSelection(seg, ev) {
  149. return getEvIsTouch(ev) &&
  150. !this.canStartResize(seg, ev) &&
  151. (this.isEventDefDraggable(seg.footprint.eventDef) ||
  152. this.isEventDefResizable(seg.footprint.eventDef))
  153. }
  154. canStartDrag(seg, ev) {
  155. return !this.canStartResize(seg, ev) &&
  156. this.isEventDefDraggable(seg.footprint.eventDef)
  157. }
  158. canStartResize(seg, ev) {
  159. let view = this._getView()
  160. let eventDef = seg.footprint.eventDef
  161. return (!getEvIsTouch(ev) || view.isEventDefSelected(eventDef)) &&
  162. this.isEventDefResizable(eventDef) &&
  163. ev.target.classList.contains('fc-resizer')
  164. }
  165. // Kills all in-progress dragging.
  166. // Useful for when public API methods that result in re-rendering are invoked during a drag.
  167. // Also useful for when touch devices misbehave and don't fire their touchend.
  168. endInteractions() {
  169. [
  170. this.dateClicking,
  171. this.dateSelecting,
  172. this.eventPointing,
  173. this.eventDragging,
  174. this.eventResizing
  175. ].forEach(function(interaction) {
  176. if (interaction) {
  177. interaction.end()
  178. }
  179. })
  180. }
  181. // Event Drag-n-Drop
  182. // ---------------------------------------------------------------------------------------------------------------
  183. // Computes if the given event is allowed to be dragged by the user
  184. isEventDefDraggable(eventDef) {
  185. return this.isEventDefStartEditable(eventDef)
  186. }
  187. isEventDefStartEditable(eventDef) {
  188. let isEditable = eventDef.isStartExplicitlyEditable()
  189. if (isEditable == null) {
  190. isEditable = this.opt('eventStartEditable')
  191. if (isEditable == null) {
  192. isEditable = this.isEventDefGenerallyEditable(eventDef)
  193. }
  194. }
  195. return isEditable
  196. }
  197. isEventDefGenerallyEditable(eventDef) {
  198. let isEditable = eventDef.isExplicitlyEditable()
  199. if (isEditable == null) {
  200. isEditable = this.opt('editable')
  201. }
  202. return isEditable
  203. }
  204. // Event Resizing
  205. // ---------------------------------------------------------------------------------------------------------------
  206. // Computes if the given event is allowed to be resized from its starting edge
  207. isEventDefResizableFromStart(eventDef) {
  208. return this.opt('eventResizableFromStart') && this.isEventDefResizable(eventDef)
  209. }
  210. // Computes if the given event is allowed to be resized from its ending edge
  211. isEventDefResizableFromEnd(eventDef) {
  212. return this.isEventDefResizable(eventDef)
  213. }
  214. // Computes if the given event is allowed to be resized by the user at all
  215. isEventDefResizable(eventDef) {
  216. let isResizable = eventDef.isDurationExplicitlyEditable()
  217. if (isResizable == null) {
  218. isResizable = this.opt('eventDurationEditable')
  219. if (isResizable == null) {
  220. isResizable = this.isEventDefGenerallyEditable(eventDef)
  221. }
  222. }
  223. return isResizable
  224. }
  225. // Event Mutation / Constraints
  226. // ---------------------------------------------------------------------------------------------------------------
  227. // Diffs the two dates, returning a duration, based on granularity of the grid
  228. // TODO: port isTimeScale into this system?
  229. diffDates(a, b): moment.Duration {
  230. if (this.largeUnit) {
  231. return diffByUnit(a, b, this.largeUnit)
  232. } else {
  233. return diffDayTime(a, b)
  234. }
  235. }
  236. // is it allowed, in relation to the view's validRange?
  237. // NOTE: very similar to isExternalInstanceGroupAllowed
  238. isEventInstanceGroupAllowed(eventInstanceGroup) {
  239. let view = this._getView()
  240. let dateProfile = this.dateProfile
  241. let eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges())
  242. let i
  243. for (i = 0; i < eventFootprints.length; i++) {
  244. // TODO: just use getAllEventRanges directly
  245. if (!dateProfile.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
  246. return false
  247. }
  248. }
  249. return view.calendar.constraints.isEventInstanceGroupAllowed(eventInstanceGroup)
  250. }
  251. // NOTE: very similar to isEventInstanceGroupAllowed
  252. // when it's a completely anonymous external drag, no event.
  253. isExternalInstanceGroupAllowed(eventInstanceGroup) {
  254. let view = this._getView()
  255. let dateProfile = this.dateProfile
  256. let eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges())
  257. let i
  258. for (i = 0; i < eventFootprints.length; i++) {
  259. if (!dateProfile.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
  260. return false
  261. }
  262. }
  263. for (i = 0; i < eventFootprints.length; i++) {
  264. // treat it as a selection
  265. // TODO: pass in eventInstanceGroup instead
  266. // because we don't want calendar's constraint system to depend on a component's
  267. // determination of footprints.
  268. if (!view.calendar.constraints.isSelectionFootprintAllowed(eventFootprints[i].componentFootprint)) {
  269. return false
  270. }
  271. }
  272. return true
  273. }
  274. }