validation.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import { EventStore, expandRecurring, filterEventStoreDefs, parseEvents, createEmptyEventStore } from './structs/event-store'
  2. import Calendar from './Calendar'
  3. import { DateSpan, DateSpanApi } from './structs/date-span'
  4. import { rangeContainsRange, rangesIntersect, DateRange, OpenDateRange } from './datelib/date-range'
  5. import EventApi from './api/EventApi'
  6. import { compileEventUis } from './component/event-rendering'
  7. import { excludeInstances } from './reducers/eventStore'
  8. import { EventInput } from './structs/event'
  9. import { EventInteractionState } from './interactions/event-interaction-state'
  10. import { SplittableProps } from './component/event-splitting'
  11. import { mapHash } from './util/object'
  12. // TODO: rename to "criteria" ?
  13. export type ConstraintInput = 'businessHours' | string | EventInput | EventInput[]
  14. export type Constraint = 'businessHours' | string | EventStore | false // false means won't pass at all
  15. export type OverlapFunc = ((stillEvent: EventApi, movingEvent: EventApi | null) => boolean)
  16. export type AllowFunc = (span: DateSpanApi, movingEvent: EventApi | null) => boolean
  17. export type isPropsValidTester = (props: SplittableProps, calendar: Calendar) => boolean
  18. // high-level segmenting-aware tester functions
  19. // ------------------------------------------------------------------------------------------------------------------------
  20. export function isInteractionValid(interaction: EventInteractionState, calendar: Calendar) {
  21. return isNewPropsValid({ eventDrag: interaction }, calendar) // HACK: the eventDrag props is used for ALL interactions
  22. }
  23. export function isDateSelectionValid(dateSelection: DateSpan, calendar: Calendar) {
  24. return isNewPropsValid({ dateSelection }, calendar)
  25. }
  26. function isNewPropsValid(newProps, calendar: Calendar) {
  27. let view = calendar.view
  28. let props = {
  29. businessHours: view ? view.props.businessHours : createEmptyEventStore(), // why? yuck
  30. dateSelection: '',
  31. eventStore: calendar.state.eventStore,
  32. eventUiBases: calendar.eventUiBases,
  33. eventSelection: '',
  34. eventDrag: null,
  35. eventResize: null,
  36. ...newProps
  37. }
  38. return (calendar.pluginSystem.hooks.isPropsValid || isPropsValid)(props, calendar)
  39. }
  40. export function isPropsValid(state: SplittableProps, calendar: Calendar, dateSpanMeta = {}, filterConfig?): boolean {
  41. if (state.eventDrag && !isInteractionPropsValid(state, calendar, dateSpanMeta, filterConfig)) {
  42. return false
  43. }
  44. if (state.dateSelection && !isDateSelectionPropsValid(state, calendar, dateSpanMeta, filterConfig)) {
  45. return false
  46. }
  47. return true
  48. }
  49. // Moving Event Validation
  50. // ------------------------------------------------------------------------------------------------------------------------
  51. function isInteractionPropsValid(state: SplittableProps, calendar: Calendar, dateSpanMeta: any, filterConfig): boolean {
  52. let interaction = state.eventDrag // HACK: the eventDrag props is used for ALL interactions
  53. let subjectEventStore = interaction.mutatedEvents
  54. let subjectDefs = subjectEventStore.defs
  55. let subjectInstances = subjectEventStore.instances
  56. let subjectConfigs = compileEventUis(
  57. subjectDefs,
  58. interaction.isEvent ?
  59. state.eventUiBases :
  60. { '': calendar.selectionConfig } // if not a real event, validate as a selection
  61. )
  62. if (filterConfig) {
  63. subjectConfigs = mapHash(subjectConfigs, filterConfig)
  64. }
  65. let otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances) // exclude the subject events. TODO: exclude defs too?
  66. let otherDefs = otherEventStore.defs
  67. let otherInstances = otherEventStore.instances
  68. let otherConfigs = compileEventUis(otherDefs, state.eventUiBases)
  69. for (let subjectInstanceId in subjectInstances) {
  70. let subjectInstance = subjectInstances[subjectInstanceId]
  71. let subjectRange = subjectInstance.range
  72. let subjectConfig = subjectConfigs[subjectInstance.defId]
  73. let subjectDef = subjectDefs[subjectInstance.defId]
  74. // constraint
  75. if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, calendar)) {
  76. return false
  77. }
  78. // overlap
  79. let overlapFunc = calendar.opt('eventOverlap')
  80. if (typeof overlapFunc !== 'function') { overlapFunc = null }
  81. for (let otherInstanceId in otherInstances) {
  82. let otherInstance = otherInstances[otherInstanceId]
  83. // intersect! evaluate
  84. if (rangesIntersect(subjectRange, otherInstance.range)) {
  85. let otherOverlap = otherConfigs[otherInstance.defId].overlap
  86. // consider the other event's overlap. only do this if the subject event is a "real" event
  87. if (otherOverlap === false && interaction.isEvent) {
  88. return false
  89. }
  90. if (subjectConfig.overlap === false) {
  91. return false
  92. }
  93. if (overlapFunc && !overlapFunc(
  94. new EventApi(calendar, otherDefs[otherInstance.defId], otherInstance), // still event
  95. new EventApi(calendar, subjectDef, subjectInstance) // moving event
  96. )) {
  97. return false
  98. }
  99. }
  100. }
  101. // allow (a function)
  102. for (let subjectAllow of subjectConfig.allows) {
  103. let origDef = state.eventStore.defs[subjectDef.defId]
  104. let origInstance = state.eventStore.instances[subjectInstanceId]
  105. let subjectDateSpan: DateSpan = {
  106. ...dateSpanMeta,
  107. range: subjectInstance.range,
  108. allDay: subjectDef.allDay
  109. }
  110. if (!subjectAllow(
  111. calendar.buildDateSpanApi(subjectDateSpan),
  112. new EventApi(calendar, origDef, origInstance)
  113. )) {
  114. return false
  115. }
  116. }
  117. }
  118. return true
  119. }
  120. // Date Selection Validation
  121. // ------------------------------------------------------------------------------------------------------------------------
  122. function isDateSelectionPropsValid(state: SplittableProps, calendar: Calendar, dateSpanMeta: any, filterConfig): boolean {
  123. let relevantEventStore = state.eventStore
  124. let relevantDefs = relevantEventStore.defs
  125. let relevantInstances = relevantEventStore.instances
  126. let selection = state.dateSelection
  127. let selectionRange = selection.range
  128. let { selectionConfig } = calendar
  129. if (filterConfig) {
  130. selectionConfig = filterConfig(selectionConfig)
  131. }
  132. // constraint
  133. if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, calendar)) {
  134. return false
  135. }
  136. // overlap
  137. let overlapFunc = calendar.opt('selectOverlap')
  138. if (typeof overlapFunc !== 'function') { overlapFunc = null }
  139. for (let relevantInstanceId in relevantInstances) {
  140. let relevantInstance = relevantInstances[relevantInstanceId]
  141. // intersect! evaluate
  142. if (rangesIntersect(selectionRange, relevantInstance.range)) {
  143. if (selectionConfig.overlap === false) {
  144. return false
  145. }
  146. if (overlapFunc && !overlapFunc(
  147. new EventApi(calendar, relevantDefs[relevantInstance.defId], relevantInstance)
  148. )) {
  149. return false
  150. }
  151. }
  152. }
  153. // allow (a function)
  154. for (let selectionAllow of selectionConfig.allows) {
  155. let fullDateSpan = { ...dateSpanMeta, ...selection }
  156. if (!selectionAllow(
  157. calendar.buildDateSpanApi(fullDateSpan),
  158. null
  159. )) {
  160. return false
  161. }
  162. }
  163. return true
  164. }
  165. // Constraint Utils
  166. // ------------------------------------------------------------------------------------------------------------------------
  167. function allConstraintsPass(
  168. constraints: Constraint[],
  169. subjectRange: DateRange,
  170. otherEventStore: EventStore,
  171. businessHoursUnexpanded: EventStore,
  172. calendar: Calendar
  173. ): boolean {
  174. for (let constraint of constraints) {
  175. if (!anyRangesContainRange(
  176. constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, calendar),
  177. subjectRange
  178. )) {
  179. return false
  180. }
  181. }
  182. return true
  183. }
  184. function constraintToRanges(
  185. constraint: Constraint,
  186. subjectRange: DateRange, // for expanding a recurring constraint, or expanding business hours
  187. otherEventStore: EventStore, // for if constraint is an even group ID
  188. businessHoursUnexpanded: EventStore, // for if constraint is 'businessHours'
  189. calendar: Calendar // for expanding businesshours
  190. ): OpenDateRange[] {
  191. if (constraint === 'businessHours') {
  192. return eventStoreToRanges(
  193. expandRecurring(businessHoursUnexpanded, subjectRange, calendar)
  194. )
  195. } else if (typeof constraint === 'string') { // an group ID
  196. return eventStoreToRanges(
  197. filterEventStoreDefs(otherEventStore, function(eventDef) {
  198. return eventDef.groupId === constraint
  199. })
  200. )
  201. } else if (typeof constraint === 'object' && constraint) { // non-null object
  202. return eventStoreToRanges(
  203. expandRecurring(constraint, subjectRange, calendar)
  204. )
  205. }
  206. return [] // if it's false
  207. }
  208. // TODO: move to event-store file?
  209. function eventStoreToRanges(eventStore: EventStore): DateRange[] {
  210. let { instances } = eventStore
  211. let ranges: DateRange[] = []
  212. for (let instanceId in instances) {
  213. ranges.push(instances[instanceId].range)
  214. }
  215. return ranges
  216. }
  217. // TODO: move to geom file?
  218. function anyRangesContainRange(outerRanges: DateRange[], innerRange: DateRange): boolean {
  219. for (let outerRange of outerRanges) {
  220. if (rangeContainsRange(outerRange, innerRange)) {
  221. return true
  222. }
  223. }
  224. return false
  225. }
  226. // Parsing
  227. // ------------------------------------------------------------------------------------------------------------------------
  228. export function normalizeConstraint(input: ConstraintInput, calendar: Calendar): Constraint | null {
  229. if (Array.isArray(input)) {
  230. return parseEvents(input, '', calendar, true) // allowOpenRange=true
  231. } else if (typeof input === 'object' && input) { // non-null object
  232. return parseEvents([ input ], '', calendar, true) // allowOpenRange=true
  233. } else if (input != null) {
  234. return String(input)
  235. } else {
  236. return null
  237. }
  238. }