FullScheduler — Full Guide - Drag & Resize Calendar Events
Docs, demos, API
Learn how to enable drag and resize, how to validate/guard these actions, and how to finalize them safely using accept(), decline() or abort(). Since v1.0.35 every callback receives the api as the second argument (payload, api).
Move events (drag) and resize edges. The panel on the right logs both actions. Guard-rails include cross-day toggle, minimum duration and optional clamping to business hours.
Turn on interactions and finalize the change with accept(). Rule: if you call accept(), do not call updateEvents(...) for the same interaction — the scheduler already applied times.
import type { FullSchedulerApiInitOptions, FullSchedulerDrop, FullSchedulerResize } from 'fullscheduler'
const options: FullSchedulerApiInitOptions<HappyEvent> = {
draggableEvents: true,
resizableEvents: true,
onEventDropped: (d) => {
d.accept() // ✅ scheduler applies new start/end for this drop
},
onEventStartResized: (r) => {
// run quick validation...
r.accept?.() // ✅ scheduler applies new start
},
onEventEndResized: (r) => {
// run quick validation...
r.accept?.() // ✅ scheduler applies new end
}
}import type { FullSchedulerDrop, FullSchedulerResize, FullSchedulerUserApi } from 'fullscheduler'
// (payload, api) — since v1.0.35
onEventDropped: (d: FullSchedulerDrop<HappyEvent>, api: FullSchedulerUserApi<HappyEvent>) => {
d.accept()
// B) Or abort: d.abort?.()
},
onEventStartResized: (r: FullSchedulerResize<HappyEvent>, api: FullSchedulerUserApi<HappyEvent>) => {
// If valid → accept(); otherwise decline()/abort()
// r.accept?.()
// r.decline?.()
// r.abort?.()
},
onEventEndResized: (r: FullSchedulerResize<HappyEvent>, api: FullSchedulerUserApi<HappyEvent>) => {
// Example of reject-then-patch scenario:
// r.decline?.()
}Cross-day & minimum duration:
import type { FullSchedulerResize, FullSchedulerUserApi } from 'fullscheduler'
const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0,0,0,0)
const sameDay = (a: Date, b: Date) => startOfDay(a).getTime() === startOfDay(b).getTime()
const minutes = (ms: number) => Math.round(ms / 60000)
const allowCrossDay = false
const minDuration = 30, m = 60000
onEventEndResized: (r: FullSchedulerResize<HappyEvent>, api: FullSchedulerUserApi<HappyEvent>) => {
const candidate = { start: r.event.start, end: r.newDateTime }
if (!allowCrossDay && !sameDay(candidate.start, candidate.end)) {
r.decline?.()
return // ❌ cross-day not allowed
}
const dur = minutes(candidate.end.getTime() - candidate.start.getTime())
if (dur < minDuration) {
r.decline?.()
return // ❌ too short
}
r.accept?.() // ✅ valid → let scheduler apply
}Clamp to business hours (decline + patch):
import type { FullSchedulerResize, FullSchedulerUserApi } from 'fullscheduler'
const clampToBusiness = (d: Date) => {
const s = 8, e = 18
const c = new Date(d)
if (c.getHours() < s) c.setHours(s, 0, 0, 0)
if (c.getHours() > e) c.setHours(e, 0, 0, 0)
return c
}
onEventStartResized: (r: FullSchedulerResize<HappyEvent>, api: FullSchedulerUserApi<HappyEvent>) => {
const clamped = clampToBusiness(r.newDateTime)
if (clamped.getTime() !== r.newDateTime.getTime()) {
r.decline?.()
return
}
r.accept?.()
} Configure snapping to the time grid. You can set it at init (option) or adjust at runtime with api.setThreshold(...).
import type { FullSchedulerApiInitOptions, FullSchedulerUserApi } from 'fullscheduler'
// At init:
const options: FullSchedulerApiInitOptions<HappyEvent> = {
draggableEvents: true,
resizableEvents: true,
threshold: 15, // minutes
onFullSchedulerApiReady: (api: FullSchedulerUserApi<HappyEvent>) => {
// Or adjust later at runtime:
api.setThreshold?.(15)
}
}- One or the other:
accept()ordecline()/abort()+ API patch — never both. - Keep guard-rails small and focused; avoid heavy logic inside callbacks.
- For clamping/normalization, prefer
decline()and then patch a clean value viaapi.updateEvents. - Use
setThresholdto match your grid (e.g., 5/10/15 minutes). - Renderer is optional for interactions, but helps present actions (icons, buttons, hints).
