2
0

InteractiveDateComponent.ts 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import * as $ from 'jquery'
  2. import * as moment from 'moment'
  3. import { getEvIsTouch, diffByUnit, diffDayTime } from '../util'
  4. import { listenBySelector, listenToHoverBySelector } from '../util/dom'
  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. // TODO: use event delegation for this? to prevent multiple calls because of bubbling?
  81. // attach a handler to the grid's root element.
  82. // jQuery will take care of unregistering them when removeElement gets called.
  83. el.addEventListener(name, (ev) => {
  84. if (
  85. !$(ev.target).is(
  86. this.segSelector + ':not(.fc-helper),' + // directly on an event element
  87. this.segSelector + ':not(.fc-helper) *,' + // within an event element
  88. '.fc-more,' + // a "more.." link
  89. 'a[data-goto]' // a clickable nav link
  90. )
  91. ) {
  92. return handler.call(this, ev)
  93. }
  94. })
  95. }
  96. bindAllSegHandlersToEl(el) {
  97. [
  98. this.eventPointing,
  99. this.eventDragging,
  100. this.eventResizing
  101. ].forEach(function(eventInteraction) {
  102. if (eventInteraction) {
  103. eventInteraction.bindToEl(el)
  104. }
  105. })
  106. }
  107. bindSegHandlerToEl(el, name, handler) {
  108. listenBySelector(
  109. el,
  110. name,
  111. this.segSelector,
  112. this.makeSegMouseHandler(handler)
  113. )
  114. }
  115. bindSegHoverHandlersToEl(el, onMouseEnter, onMouseLeave) {
  116. listenToHoverBySelector(
  117. el,
  118. this.segSelector,
  119. this.makeSegMouseHandler(onMouseEnter),
  120. this.makeSegMouseHandler(onMouseLeave)
  121. )
  122. }
  123. makeSegMouseHandler(handler) {
  124. return (ev, segEl) => {
  125. if (!segEl.classList.contains('fc-helper')) {
  126. let seg = (segEl as any).fcSeg // grab segment data. put there by View::renderEventsPayload
  127. if (seg && !this.shouldIgnoreEventPointing()) {
  128. return handler.call(this, seg, ev) // context will be the Grid
  129. }
  130. }
  131. }
  132. }
  133. shouldIgnoreMouse() {
  134. // HACK
  135. // This will still work even though bindDateHandlerToEl doesn't use GlobalEmitter.
  136. return GlobalEmitter.get().shouldIgnoreMouse()
  137. }
  138. shouldIgnoreTouch() {
  139. let view = this._getView()
  140. // On iOS (and Android?) when a new selection is initiated overtop another selection,
  141. // the touchend never fires because the elements gets removed mid-touch-interaction (my theory).
  142. // HACK: simply don't allow this to happen.
  143. // ALSO: prevent selection when an *event* is already raised.
  144. return view.isSelected || view.selectedEvent
  145. }
  146. shouldIgnoreEventPointing() {
  147. // only call the handlers if there is not a drag/resize in progress
  148. return (this.eventDragging && this.eventDragging.isDragging) ||
  149. (this.eventResizing && this.eventResizing.isResizing)
  150. }
  151. canStartSelection(seg, ev) {
  152. return getEvIsTouch(ev) &&
  153. !this.canStartResize(seg, ev) &&
  154. (this.isEventDefDraggable(seg.footprint.eventDef) ||
  155. this.isEventDefResizable(seg.footprint.eventDef))
  156. }
  157. canStartDrag(seg, ev) {
  158. return !this.canStartResize(seg, ev) &&
  159. this.isEventDefDraggable(seg.footprint.eventDef)
  160. }
  161. canStartResize(seg, ev) {
  162. let view = this._getView()
  163. let eventDef = seg.footprint.eventDef
  164. return (!getEvIsTouch(ev) || view.isEventDefSelected(eventDef)) &&
  165. this.isEventDefResizable(eventDef) &&
  166. $(ev.target).is('.fc-resizer')
  167. }
  168. // Kills all in-progress dragging.
  169. // Useful for when public API methods that result in re-rendering are invoked during a drag.
  170. // Also useful for when touch devices misbehave and don't fire their touchend.
  171. endInteractions() {
  172. [
  173. this.dateClicking,
  174. this.dateSelecting,
  175. this.eventPointing,
  176. this.eventDragging,
  177. this.eventResizing
  178. ].forEach(function(interaction) {
  179. if (interaction) {
  180. interaction.end()
  181. }
  182. })
  183. }
  184. // Event Drag-n-Drop
  185. // ---------------------------------------------------------------------------------------------------------------
  186. // Computes if the given event is allowed to be dragged by the user
  187. isEventDefDraggable(eventDef) {
  188. return this.isEventDefStartEditable(eventDef)
  189. }
  190. isEventDefStartEditable(eventDef) {
  191. let isEditable = eventDef.isStartExplicitlyEditable()
  192. if (isEditable == null) {
  193. isEditable = this.opt('eventStartEditable')
  194. if (isEditable == null) {
  195. isEditable = this.isEventDefGenerallyEditable(eventDef)
  196. }
  197. }
  198. return isEditable
  199. }
  200. isEventDefGenerallyEditable(eventDef) {
  201. let isEditable = eventDef.isExplicitlyEditable()
  202. if (isEditable == null) {
  203. isEditable = this.opt('editable')
  204. }
  205. return isEditable
  206. }
  207. // Event Resizing
  208. // ---------------------------------------------------------------------------------------------------------------
  209. // Computes if the given event is allowed to be resized from its starting edge
  210. isEventDefResizableFromStart(eventDef) {
  211. return this.opt('eventResizableFromStart') && this.isEventDefResizable(eventDef)
  212. }
  213. // Computes if the given event is allowed to be resized from its ending edge
  214. isEventDefResizableFromEnd(eventDef) {
  215. return this.isEventDefResizable(eventDef)
  216. }
  217. // Computes if the given event is allowed to be resized by the user at all
  218. isEventDefResizable(eventDef) {
  219. let isResizable = eventDef.isDurationExplicitlyEditable()
  220. if (isResizable == null) {
  221. isResizable = this.opt('eventDurationEditable')
  222. if (isResizable == null) {
  223. isResizable = this.isEventDefGenerallyEditable(eventDef)
  224. }
  225. }
  226. return isResizable
  227. }
  228. // Event Mutation / Constraints
  229. // ---------------------------------------------------------------------------------------------------------------
  230. // Diffs the two dates, returning a duration, based on granularity of the grid
  231. // TODO: port isTimeScale into this system?
  232. diffDates(a, b): moment.Duration {
  233. if (this.largeUnit) {
  234. return diffByUnit(a, b, this.largeUnit)
  235. } else {
  236. return diffDayTime(a, b)
  237. }
  238. }
  239. // is it allowed, in relation to the view's validRange?
  240. // NOTE: very similar to isExternalInstanceGroupAllowed
  241. isEventInstanceGroupAllowed(eventInstanceGroup) {
  242. let view = this._getView()
  243. let dateProfile = this.dateProfile
  244. let eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges())
  245. let i
  246. for (i = 0; i < eventFootprints.length; i++) {
  247. // TODO: just use getAllEventRanges directly
  248. if (!dateProfile.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
  249. return false
  250. }
  251. }
  252. return view.calendar.constraints.isEventInstanceGroupAllowed(eventInstanceGroup)
  253. }
  254. // NOTE: very similar to isEventInstanceGroupAllowed
  255. // when it's a completely anonymous external drag, no event.
  256. isExternalInstanceGroupAllowed(eventInstanceGroup) {
  257. let view = this._getView()
  258. let dateProfile = this.dateProfile
  259. let eventFootprints = this.eventRangesToEventFootprints(eventInstanceGroup.getAllEventRanges())
  260. let i
  261. for (i = 0; i < eventFootprints.length; i++) {
  262. if (!dateProfile.validUnzonedRange.containsRange(eventFootprints[i].componentFootprint.unzonedRange)) {
  263. return false
  264. }
  265. }
  266. for (i = 0; i < eventFootprints.length; i++) {
  267. // treat it as a selection
  268. // TODO: pass in eventInstanceGroup instead
  269. // because we don't want calendar's constraint system to depend on a component's
  270. // determination of footprints.
  271. if (!view.calendar.constraints.isSelectionFootprintAllowed(eventFootprints[i].componentFootprint)) {
  272. return false
  273. }
  274. }
  275. return true
  276. }
  277. }