calendar-tsx

Editing policies

Configure conversion, resource assignment, constraints, touch and edge scrolling.

Register only the policies and drivers you need. Factory configuration supplies defaults, and per-view features entries override them by registry alias.

core/interaction-policies.ts
const businessHours: Calendar_WeeklyRule[] = [
  {
    daysOfWeek: [1, 2, 3, 4, 5],
    startTime: Temporal.PlainTime.from('09:00'),
    endTime: Temporal.PlainTime.from('17:00'),
  },
];
export const features = calendarFeatures({
  interactions: interactionFeature(),
  pointer: pointerFeature({ distance: 4 }),
  touch: touchFeature({ delay: 500, tolerance: 8 }),
  scroll: autoScrollFeature({ edge: 40, speed: 600 }),
  movement: eventMoveFeature({
    relocationPolicy: {
      allowConversion: true,
      conversionDuration: 'reset',
      defaultTimedMinutes: 60,
      resourceMode: 'replace-all',
    },
  }),
  resizing: eventResizeFeature({ fromStart: true }),
  constraints: constraintsFeature({
    preventOverlap: true,
    businessHours,
    resourceAvailability: true,
  }),
});
export const views = calendarViews({
  features,
  views: {
    editable: timeGridView({ days: 1 }),
    readOnly: timeGridView({
      days: 1,
      features: {
        movement: { enabled: false },
        resizing: { enabled: false },
      },
    }),
  },
});

The read-only view disables its movement and resizing operations; their APIs stay installed. Settings can also arrive as live calendar options such as movement, resizing and constraints. They resolve in this order: factory defaults, live options, per-view features[alias], then direct per-view settings.

Interaction dependencies compare by reference, so keep rule arrays and predicates stable until their contents change. In React, declare them outside the component or memoize them; a business-hours array recreated on every render revalidates the draft each time.

Movement and resources

createEventRelocationPolicy types shared conversion and resource settings with inferred literals and returns the object unchanged; it installs no operation. Pass it once as the calendar's eventRelocationPolicy, and movement, external creation and transfer receiving inherit it. Each of those features accepts a partial relocationPolicy override.

core/interaction-placement.ts
const eventRelocationPolicy = createEventRelocationPolicy({
  allowConversion: true,
  conversionDuration: 'reset',
  defaultTimedMinutes: 60,
  allowResourceChange: true,
  resourceMode: 'replace-all',
});

export const features = calendarFeatures({
  interactions: interactionFeature(),
  movement: eventMoveFeature(),
  creation: externalDropFeature({ relocationPolicy: { allowConversion: false } }),
  transfers: eventTransferFeature({ relocationPolicy: { resourceMode: 'replace-source' } }),
});

export const calendar = createCalendar({
  features,
  views: calendarViews({ features, views: { day: timeGridView({ days: 1 }) } }),
  view: 'day',
  date: initialDate,
  timeZone: 'UTC',
  events: initialEvents,
  eventRelocationPolicy,
  id: 'bookings',
  onNavigate: (next): void => calendar['~update']({ ...calendar.options, view: next.view, date: next.date }),
});

Fields resolve from library defaults, then calendar policy, factory configuration, live feature settings, per-view features[alias] and direct view settings, with later defined values winning. Omitted and undefined fields inherit, and false overrides true. Policies merge by field, so overriding resource behavior keeps the inherited conversion settings.

Here, movement inherits the calendar policy, external creation rejects kind conversion, and transfer receiving replaces only the dragged resource. A transfer uses the receiving calendar's policy.

Changing the calendar policy or a feature override revalidates the active draft and its preview. If the proposal becomes unavailable, the preview clears and commit is refused; restoring permission revalidates the same session. A recreated policy with equal values doesn't revalidate.

enabled and canMove are movement settings. The other fields belong to eventRelocationPolicy or a feature's relocationPolicy.

SettingDefaultEffect
enabledtrueAllows movement while the feature is registered.
allowConversionfalseAllows timed and all-day kinds to change.
conversionDuration'reset'A conversion becomes one all-day date or one timed hour.
defaultTimedMinutes60Timed duration when conversion resets.
allowResourceChangetrueAllows a destination resource to replace assignments.
resourceMode'replace-all'Replaces the whole resource list with the destination.
canMove(event)UnrestrictedRejects events for which the predicate returns false.

Moves within a kind keep the elapsed timed duration or the inclusive all-day length. Moving a timed event in a day grid keeps its local start time. conversionDuration: 'preserve' keeps elapsed duration; timed-to-all-day conversion rounds down to whole dates, at least one.

resourceMode: 'replace-source' replaces only the dragged assignment. For multi-resource events, put its resourceId in the source descriptor or pass it to move.begin/move.evaluate. With allowResourceChange: false, a move to an unassigned resource is rejected and assignments stay as they were.

Resizing

Both edges resize by default; eventResizeFeature({ fromStart: false }) allows only the end. resizing.enabled disables the operation, and canResize(event, edge) rejects individual edits. The opposite edge stays fixed and the event kind never changes. A timed end must be after its start. All-day ends are inclusive, so equal start and end is one date.

Constraints

constraintsFeature checks selection, movement, resizing, creation and transfers, on every preview and again at commit.

SettingBehavior
preventOverlaptrue once registered. Set false to keep other rules and allow overlap.
allowedRangesBackground ranges, or a function of the proposed dates and time zone, containing the proposal.
businessHoursWeekly rules containing the proposal, in the calendar time zone.
resourceIdsAllowlist for assigned resource IDs.
resourceAvailabilityEach assigned resource must exist and its availability must contain the proposal.
allow(proposal, input)Synchronous predicate returning true, false or a rejection reason string.

Rules apply to the whole proposed interval, and adjacent allowed windows join. Overlap checks cover timed, all-day and background events. Events with disjoint nonempty resource lists don't conflict, and an edited event doesn't conflict with its own original.

Constraints check the concrete events you supply. They don't expand recurring definitions, and these operations can't edit generated occurrences; apply recurrence edits in your application. Shading a background doesn't enforce it: pass the same rules to constraintsFeature.

Touch and scrolling

pointerFeature activates after four pixels (distance). touchFeature adds touch input with a 500 ms hold (delay) and an eight-pixel tolerance; movement before activation scrolls the page as usual. autoScrollFeature scrolls nested containers and the window near their edges, with a 40 px edge zone and 600 px/s maximum speed.

Each driver reads per-view settings and accepts enabled: false. None of them mounts listeners alone (use the interaction bindings) or installs an operation: touch still needs selection, movement, resizing or a custom operation.

Time zones and DST

Timed hits snap to the view's slot interval on its fixed wall-clock axis. A nonexistent local time is unavailable. A repeated time keeps the source's offset when it matches, and otherwise takes the earlier occurrence. Hits keep the local clock and candidate instants for custom feedback.

Hidden dates, visible-hour windows and resource grouping shape the surfaces. Register only the date/time track so labels and gutters don't skew hit testing.

Back to selection and event editing for bindings and ownership.