Skip to content

Commit

Permalink
feat: type-safe event handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
justingrant committed Apr 28, 2019
1 parent f827f23 commit 5d44510
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 60 deletions.
31 changes: 16 additions & 15 deletions src/core/Calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin
import OptionsManager from './OptionsManager'
import View from './View'
import Theme from './theme/Theme'
import { OptionsInput } from './types/input-types'
import { OptionsInput, EventHandlerName, EventHandlerArgs } from './types/input-types'
import { Locale, buildLocale, parseRawLocales, RawLocaleMap } from './datelib/locale'
import { DateEnv, DateInput } from './datelib/env'
import { DateMarker, startOfDay } from './datelib/marker'
Expand Down Expand Up @@ -702,13 +702,13 @@ export default class Calendar {
// -----------------------------------------------------------------------------------------------------------------


hasPublicHandlers(name: string): boolean {
hasPublicHandlers<T extends EventHandlerName>(name: T): boolean {
return this.hasHandlers(name) ||
this.opt(name) // handler specified in options
}


publiclyTrigger(name: string, args?) {
publiclyTrigger<T extends EventHandlerName>(name: T, args?: EventHandlerArgs<T>) {
let optHandler = this.opt(name)

this.triggerWith(name, this, args)
Expand All @@ -719,7 +719,7 @@ export default class Calendar {
}


publiclyTriggerAfterSizing(name, args) {
publiclyTriggerAfterSizing<T extends EventHandlerName>(name: T, args: EventHandlerArgs<T>) {
let { afterSizingTriggers } = this;

(afterSizingTriggers[name] || (afterSizingTriggers[name] = [])).push(args)
Expand All @@ -731,7 +731,7 @@ export default class Calendar {

for (let name in afterSizingTriggers) {
for (let args of afterSizingTriggers[name]) {
this.publiclyTrigger(name, args)
this.publiclyTrigger(name as EventHandlerName, args)
}
}

Expand Down Expand Up @@ -1048,11 +1048,11 @@ export default class Calendar {


triggerDateSelect(selection: DateSpan, pev?: PointerDragEvent) {
let arg = this.buildDateSpanApi(selection) as DateSelectionApi

arg.jsEvent = pev ? pev.origEvent : null
arg.view = this.view

const arg = {
...this.buildDateSpanApi(selection),
jsEvent: pev ? pev.origEvent as MouseEvent : null, // Is this always a mouse event? See #4655
view: this.view
}
this.publiclyTrigger('select', [ arg ])
}

Expand All @@ -1069,11 +1069,12 @@ export default class Calendar {

// TODO: receive pev?
triggerDateClick(dateSpan: DateSpan, dayEl: HTMLElement, view: View, ev: UIEvent) {
let arg = this.buildDatePointApi(dateSpan) as DateClickApi

arg.dayEl = dayEl
arg.jsEvent = ev
arg.view = view
const arg = {
...this.buildDatePointApi(dateSpan),
dayEl,
jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
view
}

this.publiclyTrigger('dateClick', [ arg ])
}
Expand Down
12 changes: 7 additions & 5 deletions src/core/component/DateComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import EventApi from '../api/EventApi'
import FgEventRenderer from './renderers/FgEventRenderer'
import FillRenderer from './renderers/FillRenderer'
import { EventInteractionState } from '../interactions/event-interaction-state'
import View from '../View'
import { EventHandlerName, EventHandlerArgs } from '../types/input-types'

export type DateComponentHash = { [uid: string]: DateComponent<any> }

Expand Down Expand Up @@ -153,21 +155,21 @@ export default class DateComponent<PropsType> extends Component<PropsType> {
// TODO: move to Calendar


publiclyTrigger(name, args) {
publiclyTrigger<T extends EventHandlerName>(name: T, args?: EventHandlerArgs<T>) {
let calendar = this.calendar

return calendar.publiclyTrigger(name, args)
}


publiclyTriggerAfterSizing(name, args) {
publiclyTriggerAfterSizing<T extends EventHandlerName>(name: T, args: EventHandlerArgs<T>) {
let calendar = this.calendar

return calendar.publiclyTriggerAfterSizing(name, args)
}


hasPublicHandlers(name) {
hasPublicHandlers<T extends EventHandlerName>(name: T) {
let calendar = this.calendar

return calendar.hasPublicHandlers(name)
Expand All @@ -191,7 +193,7 @@ export default class DateComponent<PropsType> extends Component<PropsType> {
isStart: seg.isStart,
isEnd: seg.isEnd,
el: seg.el,
view: this // ?
view: this as unknown as View // safe to cast because this method is only called on context.view
}
])
}
Expand Down Expand Up @@ -221,7 +223,7 @@ export default class DateComponent<PropsType> extends Component<PropsType> {
),
isMirror: isMirrors,
el: seg.el,
view: this // ?
view: this as unknown as View // safe to cast because this method is only called on context.view
}
])
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/component/event-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function hasBgRendering(def: EventDef) {
return def.rendering === 'background' || def.rendering === 'inverse-background'
}

export function filterSegsViaEls(view: View, segs: Seg[], isMirror) {
export function filterSegsViaEls(view: View, segs: Seg[], isMirror: boolean) {

if (view.hasPublicHandlers('eventRender')) {
segs = segs.filter(function(seg) {
Expand Down
2 changes: 1 addition & 1 deletion src/core/interactions/EventClicking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class EventClicking extends Interaction {
seg.eventRange.def,
seg.eventRange.instance
),
jsEvent: ev,
jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
view: component.view
}
])
Expand Down
4 changes: 2 additions & 2 deletions src/core/interactions/EventHovering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default class EventHovering extends Interaction {
}
}

triggerEvent(publicEvName: string, ev: Event | null, segEl: HTMLElement) {
triggerEvent(publicEvName: 'eventMouseEnter' | 'eventMouseLeave', ev: Event | null, segEl: HTMLElement) {
let { component } = this
let seg = getElSeg(segEl)!

Expand All @@ -67,7 +67,7 @@ export default class EventHovering extends Interaction {
seg.eventRange.def,
seg.eventRange.instance
),
jsEvent: ev,
jsEvent: ev as MouseEvent, // Is this always a mouse event? See #4655
view: component.view
}
])
Expand Down
3 changes: 2 additions & 1 deletion src/core/interactions/event-dragging.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Calendar from '../Calendar'
import { EventMutation } from '../structs/event-mutation'
import { Hit } from './hit'
import { EventHandlerArg } from '../types/input-types'

export type eventDragMutationMassager = (mutation: EventMutation, hit0: Hit, hit1: Hit) => void
export type EventDropTransformers = (mutation: EventMutation, calendar: Calendar) => void
export type EventDropTransformers = (mutation: EventMutation, calendar: Calendar) => Partial<EventHandlerArg<'eventDrop'>>
41 changes: 31 additions & 10 deletions src/core/types/input-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ export interface DropInfo {
end: Date
}

// TODO: refactor OptionsInputBase to split out event handlers into a separate interface,
// which will enable replacing the static list of event handlers below with a simpler
// `keyof OptionsInputBaseEventHandlers`
export type EventHandlerName =
'_init' | 'selectAllow' | 'eventAllow' | 'eventDataTransform' | 'datesRender' |
'datesDestroy' | 'dayRender' | 'windowResize' | 'dateClick' | 'eventClick' |
'eventMouseEnter' | 'eventMouseLeave' | 'select' | 'unselect' | 'loading' |
'eventRender' | 'eventPositioned' | '_eventsPositioned' | 'eventDestroy' |
'eventDragStart' | 'eventDragStop' | 'eventDrop' | '_destroyed' | 'drop' |
'eventResizeStart' | 'eventResizeStop' | 'eventResize' | 'eventReceive' |
'eventLeave' | 'viewSkeletonRender' | 'viewSkeletonDestroy' | '_noEventDrop' |
'_noEventResize' | 'eventLimitClick'

export type EventHandlerArgs<T extends EventHandlerName> =
Parameters<Extract<OptionsInput[T], (...args: any[]) => any>>
export type EventHandlerArg<T extends EventHandlerName> = EventHandlerArgs<T>[0]

export interface OptionsInputBase {
header?: boolean | ToolbarInput
footer?: boolean | ToolbarInput
Expand All @@ -93,7 +110,8 @@ export interface OptionsInputBase {
handleWindowResize?: boolean
windowResizeDelay?: number
eventLimit?: boolean | number
eventLimitClick?: 'popover' | 'week' | 'day' | string | ((cellinfo: CellInfo, jsevent: Event) => void)
eventLimitClick?: 'popover' | 'week' | 'day' | 'timeGridWeek' | 'timeGridDay' | string |
((arg: { date: Date, allDay: boolean, dayEl: HTMLElement, moreEl: HTMLElement, segs: any[], hiddenSegs: any[], jsEvent: MouseEvent, view: View }) => void),
timeZone?: string | boolean
now?: DateInput | (() => DateInput)
defaultView?: string
Expand Down Expand Up @@ -178,31 +196,34 @@ export interface OptionsInputBase {
timeGridEventMinHeight?: number
datesRender?(arg: { view: View, el: HTMLElement }): void
datesDestroy?(arg: { view: View, el: HTMLElement }): void
dayRender?(arg: { view: View, date: Date, allDay: boolean, el: HTMLElement }): void
dayRender?(arg: { view: View, date: Date, allDay?: boolean, el: HTMLElement }): void
windowResize?(view: View): void
dateClick?(arg: { date: Date, dateStr: string, allDay: boolean, resource: any, dayEl: HTMLElement, jsEvent: MouseEvent, view: View }): void // resource for Scheduler
dateClick?(arg: { date: Date, dateStr: string, allDay: boolean, resource?: any, dayEl: HTMLElement, jsEvent: MouseEvent, view: View }): void // resource for Scheduler
eventClick?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): boolean | void
eventMouseEnter?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): void
eventMouseLeave?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): void
select?(arg: { start: Date, end: Date, startStr: string, endStr: string, allDay: boolean, resource: any, jsEvent: MouseEvent, view: View }): void // resource for Scheduler
select?(arg: { start: Date, end: Date, startStr: string, endStr: string, allDay: boolean, resource?: any, jsEvent: MouseEvent, view: View }): void // resource for Scheduler
unselect?(arg: { view: View, jsEvent: Event }): void
loading?(isLoading: boolean, view: View): void
eventRender?(arg: { event: EventApi, el: HTMLElement, view: View }): void
eventPositioned?(arg: { event: EventApi, el: HTMLElement, view: View }): void
loading?(isLoading: boolean): void
eventRender?(arg: { isMirror: boolean, isStart: boolean, isEnd: boolean, event: EventApi, el: HTMLElement, view: View }): void
eventPositioned?(arg: { isMirror: boolean, isStart: boolean, isEnd: boolean, event: EventApi, el: HTMLElement, view: View }): void
_eventsPositioned?(arg: { view: View }): void
eventDestroy?(arg: { event: EventApi, el: HTMLElement, view: View }): void
eventDestroy?(arg: { isMirror: boolean, event: EventApi, el: HTMLElement, view: View }): void
eventDragStart?(arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: View }): void
eventDragStop?(arg: { event: EventApi, el: HTMLElement, jsEvent: MouseEvent, view: View }): void
eventDrop?(arg: { el: HTMLElement, event: EventApi, delta: Duration, revert: () => void, jsEvent: Event, view: View }): void
eventDrop?(arg: { el: HTMLElement, event: EventApi, oldEvent: EventApi, delta: Duration, revert: () => void, jsEvent: Event, view: View }): void
eventResizeStart?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): void
eventResizeStop?(arg: { el: HTMLElement, event: EventApi, jsEvent: MouseEvent, view: View }): void
eventResize?(arg: { el: HTMLElement, event: EventApi, delta: Duration, revert: () => void, jsEvent: Event, view: View }): void
eventResize?(arg: { el: HTMLElement, startDelta: Duration, endDelta: Duration, prevEvent: EventApi, event: EventApi, revert: () => void, jsEvent: Event, view: View }): void
drop?(arg: { date: Date, dateStr: string, allDay: boolean, draggedEl: HTMLElement, jsEvent: MouseEvent, view: View }): void
eventReceive?(arg: { event: EventApi, draggedEl: HTMLElement, view: View }): void
eventLeave?(arg: { draggedEl: HTMLElement, event: EventApi, view: View }): void
viewSkeletonRender?(arg: { el: HTMLElement, view: View }): void
viewSkeletonDestroy?(arg: { el: HTMLElement, view: View }): void
_destroyed?(): void
_init?(): void
_noEventDrop?(): void
_noEventResize?(): void
}

export interface ViewOptionsInput extends OptionsInputBase {
Expand Down
14 changes: 7 additions & 7 deletions src/interaction/interactions-external/ExternalElementDragging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,12 @@ export default class ExternalElementDragging {
let finalHit = this.hitDragging.finalHit!
let finalView = finalHit.component.view
let dragMeta = this.dragMeta!
let arg = receivingCalendar.buildDatePointApi(finalHit.dateSpan) as ExternalDropApi

arg.draggedEl = pev.subjectEl as HTMLElement
arg.jsEvent = pev.origEvent
arg.view = finalView

let arg = {
...receivingCalendar.buildDatePointApi(finalHit.dateSpan),
draggedEl: pev.subjectEl as HTMLElement,
jsEvent: pev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
view: finalView
}
receivingCalendar.publiclyTrigger('drop', [ arg ])

if (dragMeta.create) {
Expand All @@ -154,7 +154,7 @@ export default class ExternalElementDragging {
// signal that an external event landed
receivingCalendar.publiclyTrigger('eventReceive', [
{
draggedEl: pev.subjectEl,
draggedEl: pev.subjectEl as HTMLElement,
event: new EventApi(
receivingCalendar,
droppableEvent.def,
Expand Down
34 changes: 18 additions & 16 deletions src/interaction/interactions/EventDragging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import HitDragging, { isHitsEqual } from './HitDragging'
import FeaturefulElementDragging from '../dnd/FeaturefulElementDragging'
import { __assign } from 'tslib'
import { ExternalDropApi } from '../interactions-external/ExternalElementDragging'
import { EventDropTransformers } from '../../core/interactions/event-dragging'


export default class EventDragging extends Interaction { // TODO: rename to EventSelectingAndDragging
Expand Down Expand Up @@ -115,7 +115,7 @@ export default class EventDragging extends Interaction { // TODO: rename to Even
{
el: this.subjectSeg.el,
event: new EventApi(initialCalendar, eventRange.def, eventRange.instance),
jsEvent: ev.origEvent,
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
view: this.component.view
}
])
Expand Down Expand Up @@ -228,7 +228,7 @@ export default class EventDragging extends Interaction { // TODO: rename to Even
{
el: this.subjectSeg.el,
event: eventApi,
jsEvent: ev.origEvent,
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
view: initialView
}
])
Expand All @@ -243,14 +243,15 @@ export default class EventDragging extends Interaction { // TODO: rename to Even
eventStore: mutatedRelevantEvents
})

let eventDropArg = {}
let transformed: ReturnType<EventDropTransformers> = {}

for (let transformer of initialCalendar.pluginSystem.hooks.eventDropTransformers) {
__assign(eventDropArg, transformer(this.validMutation, initialCalendar))
__assign(transformed, transformer(this.validMutation, initialCalendar))
}

__assign(eventDropArg, {
el: ev.subjectEl,
const eventDropArg = {
...transformed, // don't use __assign here because it's not type-safe
el: ev.subjectEl as HTMLElement,
delta: this.validMutation.startDelta!,
oldEvent: eventApi,
event: new EventApi( // the data AFTER the mutation
Expand All @@ -266,7 +267,7 @@ export default class EventDragging extends Interaction { // TODO: rename to Even
},
jsEvent: ev.origEvent,
view: initialView
})
}

initialCalendar.publiclyTrigger('eventDrop', [ eventDropArg ])

Expand All @@ -275,7 +276,7 @@ export default class EventDragging extends Interaction { // TODO: rename to Even

initialCalendar.publiclyTrigger('eventLeave', [
{
draggedEl: ev.subjectEl,
draggedEl: ev.subjectEl as HTMLElement,
event: eventApi,
view: initialView
}
Expand All @@ -298,22 +299,23 @@ export default class EventDragging extends Interaction { // TODO: rename to Even
})
}

let dropArg = receivingCalendar.buildDatePointApi(finalHit.dateSpan) as ExternalDropApi
dropArg.draggedEl = ev.subjectEl as HTMLElement
dropArg.jsEvent = ev.origEvent
dropArg.view = finalHit.component as View // ?

let dropArg = {
...receivingCalendar.buildDatePointApi(finalHit.dateSpan),
draggedEl: ev.subjectEl as HTMLElement,
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
view: finalHit.component as View // should this be finalHit.component.view? See #4644
}
receivingCalendar.publiclyTrigger('drop', [ dropArg ])

receivingCalendar.publiclyTrigger('eventReceive', [
{
draggedEl: ev.subjectEl,
draggedEl: ev.subjectEl as HTMLElement,
event: new EventApi( // the data AFTER the mutation
receivingCalendar,
mutatedRelevantEvents.defs[eventDef.defId],
mutatedRelevantEvents.instances[eventInstance.instanceId]
),
view: finalHit.component
view: finalHit.component as View // should this be finalHit.component.view? See #4644
}
])
}
Expand Down
4 changes: 2 additions & 2 deletions src/interaction/interactions/EventResizing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default class EventDragging extends Interaction {
{
el: this.draggingSeg.el,
event: new EventApi(calendar, eventRange.def, eventRange.instance),
jsEvent: ev.origEvent,
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
view: this.component.view
}
])
Expand Down Expand Up @@ -163,7 +163,7 @@ export default class EventDragging extends Interaction {
{
el: this.draggingSeg.el,
event: eventApi,
jsEvent: ev.origEvent,
jsEvent: ev.origEvent as MouseEvent, // Is this always a mouse event? See #4655
view
}
])
Expand Down

0 comments on commit 5d44510

Please sign in to comment.