# Editing and Interaction
URL: https://pro.rv-grid.com/guides/event-scheduler/event-editing/
Source: src/content/docs/guides/event-scheduler/event-editing.mdx
Description: Create, move, resize, delete, validate, select, copy, paste, and handle scheduler interactions with the RevoGrid Enterprise lifecycle.
Enable editing with the master `editable` flag and the specific `allow*` permissions your product needs.

Editing is checked in layers. The scheduler first applies `editable`, `allowCreate`, `allowMove`, `allowResize`, `allowDelete`, event/resource locks, date locks, and permission hooks. It then projects the next schedule and rejects the mutation when conflicts resolve to `severity: 'error'`. Use this to block edits outside working hours.

```ts
grid.eventScheduler = {
  view: 'resourceTimeline',
  weekStartDate: '2026-06-08',
  editable: true,
  allowCreate: true,
  allowMove: true,
  allowResize: true,
  allowDelete: true,
  conflicts: {
    enabled: true,
    rules: { 'outside-availability': 'error' },
  },
  createEventDraft: (context) => ({
    id: `event-${crypto.randomUUID()}`,
    resourceId: context.resourceId,
    title: 'New booking',
    startDateTime: context.startDateTime,
    endDateTime: context.endDateTime,
    status: 'draft',
  }),
  validateMutation: (detail) => {
    if (detail.event?.locked) {
      return 'Locked events cannot be changed.';
    }
  },
};
```

To enforce working hours, define them with `calendars` or `eventSchedulerAvailability`; then block the `outside-availability` conflict as shown above. `nonWorkingTime` can make closed cells visible, but it does not by itself reject create, move, resize, or editor changes.

Conflicts are evaluated against the projected next event array before the local mutation is committed. This means move, resize, create, edit, delete, bulk, template, paste, and recurrence actions can all be stopped by the same conflict configuration. Use `conflicts.policy: 'prevent'` when every detected conflict should block by default, or keep `policy: 'mark'` and promote only selected rules such as `overlap`, `outside-availability`, or `blocked-time` to `error`.

The before-change detail includes the projected `conflicts` array. Use it when the UI needs custom messages or business-specific decisions on top of configured conflict rules.

```ts
grid.addEventListener('event-scheduler-before-event-change', (event) => {
  const blockingConflict = event.detail.conflicts?.find((conflict) =>
    conflict.severity === 'error' &&
    event.detail.eventId !== null &&
    conflict.eventIds.includes(event.detail.eventId)
  );

  if (blockingConflict) {
    event.preventDefault();
  }
});
```

For one-off business rules that are not calendar rules, use `permissions.canCreate`, `permissions.canMutate`, or `validateMutation`. These hooks are useful for role checks, approval states, custom capacity rules, or backend-specific constraints.

If you prefer event-based validation, listen for `event-scheduler-before-event-change` and call `event.preventDefault()`. This is the same cancelable-before-event pattern used by RevoGrid editing events. Do not use `event-scheduler-event-changed` for cancellation; it fires after the scheduler has already accepted and applied the change.

```ts
grid.addEventListener('event-scheduler-before-event-change', (event) => {
  if (event.detail.action === 'move' && event.detail.event?.locked) {
    event.preventDefault();
  }
});
```

## Data ownership and save flows

Event Scheduler does not require one persistence model. Choose the data ownership flow that matches your app: upload all events as host state, save the final projected schedule, save every accepted mutation, or let remote hooks load and commit data.

Use `eventSchedulerEvents` to upload host-owned data into the scheduler:

```ts
grid.eventSchedulerResources = resources;
grid.eventSchedulerEvents = events;
```

The scheduler mutates its local accepted event list after permissions, before hooks, `validateMutation`, and conflict rules pass. Final events such as `event-scheduler-event-created`, `event-scheduler-event-changed`, and `event-scheduler-event-deleted` fire after that local state has already changed, so use them for persistence and state sync. Use before hooks or `validateMutation` for cancellation.

### Save the full projection after work

Use this flow when the user can make several local edits and your app saves the whole schedule later, for example from a toolbar Save button. Read `scheduler.getProjection()?.events`; do not persist scheduler rows, columns, or segments because those are render artifacts.

```ts
const plugins = await grid.getPlugins();
const scheduler = plugins.find((plugin) =>
  plugin.constructor?.name === 'EventSchedulerPlugin'
);

scheduler.createEvent({
  id: `event-${crypto.randomUUID()}`,
  resourceId: 'room-a',
  title: 'Design review',
  startDateTime: '2026-06-08T09:00:00.000Z',
  endDateTime: '2026-06-08T10:00:00.000Z',
});

scheduler.editEvent('booking-1', {
  resourceId: 'room-b',
  startDateTime: '2026-06-08T10:00:00.000Z',
  endDateTime: '2026-06-08T11:00:00.000Z',
});

scheduler.deleteEvent('hold-1');

const savedEvents = scheduler.getProjection()?.events ?? [];
await api.saveSchedule({ events: savedEvents });

// Later, or after clearing the scheduler, upload the saved data back.
grid.eventSchedulerEvents = savedEvents;
```

### Save incrementally while work happens

Use this flow when the host app owns canonical state and wants every accepted scheduler change to update that state immediately. Each final event carries `detail.events`, the full next accepted event list after the mutation.

```ts
let persistedEvents = [...initialEvents];

function persistSchedulerEvents(event: CustomEvent) {
  persistedEvents = event.detail.events;
  grid.eventSchedulerEvents = persistedEvents;
  void api.saveEvents(persistedEvents);
}

grid.addEventListener('event-scheduler-event-created', persistSchedulerEvents);
grid.addEventListener('event-scheduler-event-changed', persistSchedulerEvents);
grid.addEventListener('event-scheduler-event-deleted', persistSchedulerEvents);

// Later, upload the persisted host state into a clean scheduler.
grid.eventSchedulerEvents = persistedEvents;
```

The final event detail also includes action-specific context such as `eventId`, `event`, `previousEvent`, `action`, and `conflicts`. Use those fields for audit logs or targeted API calls, but keep `detail.events` as the simplest full-list state sync contract.

### Save through remote APIs

Use remote mode when the server owns the visible range or when loading the whole schedule up front is too expensive. Remote loaders upload visible data into the scheduler. `commitMutation` receives the accepted projected mutation request and can return canonical server events.

```ts
grid.eventScheduler = {
  view: 'resourceTimeline',
  weekStartDate: '2026-06-08',
  remote: {
    enabled: true,
    mode: 'remote',
    loadResources: ({ signal }) =>
      api.loadResources({ signal }),
    loadEvents: ({ dateRange, resourceIds, signal }) =>
      api.loadEvents({ dateRange, resourceIds, signal }),
    validateMutation: (request) =>
      api.validateScheduleChange(request),
    commitMutation: async (request) => {
      const result = await api.saveScheduleChange({
        action: request.action,
        eventId: request.eventId,
        event: request.event,
        previousEvent: request.previousEvent,
        changes: request.changes,
        events: request.events,
        conflicts: request.conflicts,
      });

      return {
        accepted: true,
        events: result.events,
      };
    },
  },
};

const plugins = await grid.getPlugins();
const scheduler = plugins.find((plugin) =>
  plugin.constructor?.name === 'EventSchedulerPlugin'
);

// Reload the current visible range into a clean scheduler state.
await scheduler.refreshVisibleRange();
```

Remote `commitMutation` runs after the local mutation has been accepted. If the backend rejects the change, return `{ accepted: false, message }` or throw; with the default rollback behavior the scheduler restores the previous event list. See [Remote Data](/guides/event-scheduler/remote-data/) for paging, state events, errors, and refresh controls.

`validateMutation` is the config-level equivalent of the cancelable before-change event. It receives the same detail shape and can return `false` or a string to reject.

```ts
grid.eventScheduler = {
  view: 'resourceTimeline',
  weekStartDate: '2026-06-08',
  validateMutation: ({ action, event, changes }) => {
    if (action === 'delete' && event?.status === 'completed') {
      return 'Completed events cannot be deleted.';
    }
    if (changes?.resourceId === 'locked-room') {
      return 'That room is locked.';
    }
  },
};
```

## Custom editor

Set `eventEditor: false` to disable the built-in editor, or provide a custom editor hook when the host app owns a drawer, modal, or approval flow:

```ts
grid.eventScheduler = {
  view: 'week',
  weekStartDate: '2026-06-08',
  eventEditor: (context) => {
    openProductDrawer({
      mode: context.mode,
      event: context.event,
      resources: context.resources,
      readOnly: context.readonly,
      validationMessage: context.validationMessage,
      onSave: context.submit,
      onDelete: context.delete,
      onClose: context.close,
    });
    return true;
  },
};
```

The editor helpers still route changes through permissions, conflicts, `event-scheduler-before-event-change`, local mutation, post-commit events, and remote mutation hooks.

## Interaction callbacks

Use named before hooks to validate or cancel user actions before local scheduler state changes. Return `false` to cancel with the default message, or return a string to cancel with a product-specific validation message.

| Before hook | Action |
| --- | --- |
| `beforeEventCreate` | Create from slot, editor create, template create, recurrence create |
| `beforeEventUpdate` | Editor/API updates and status changes |
| `beforeEventMove` | Drag move and bulk move |
| `beforeEventResize` | Drag resize |
| `beforeEventDelete` | Delete and bulk delete |
| `beforeEventDuplicate` | Duplicate selected events |
| `beforeEventLock` | Lock or unlock changes |
| `beforeSlotSelect` | Empty-slot selection/click before create behavior |

Final callbacks fire after the scheduler has committed local state and include enough context to update app state or persist to a backend.

| Final callback | Action |
| --- | --- |
| `onEventCreate` | Event created, including paste/duplicate/template-created events |
| `onEventUpdate` | Event updated or status changed |
| `onEventMove` | Event moved |
| `onEventResize` | Event resized |
| `onEventDelete` | Event deleted |
| `onEventDuplicate` | Duplicated event created |
| `onEventLock` | Event locked or unlocked |
| `onEventCopy` | Event copied into the scheduler clipboard |
| `onEventPaste` | Pasted event created |
| `onSelectionChange` | Event selection changed |
| `onSlotSelect` | Empty slot selected/clicked |
| `contextMenu.onContextMenuAction` | Scheduler context-menu item selected |

```ts
grid.eventScheduler = {
  view: 'week',
  weekStartDate: '2026-06-08',
  beforeEventMove: ({ event, changes }) => {
    if (event?.locked) return 'Locked events cannot be moved.';
    return validateMoveOnServerCache(event, changes);
  },
  beforeEventResize: ({ event, changes }) =>
    isOutsidePolicyWindow(event, changes) ? 'Outside policy window.' : undefined,
  beforeEventDelete: ({ event }) =>
    event?.metadata?.protected ? 'Protected events cannot be deleted.' : undefined,
  beforeSlotSelect: ({ date, startDateTime }) =>
    isClosedForBooking(date, startDateTime) ? 'This slot is closed.' : undefined,
  onEventMove: ({ event, previousEvent }) =>
    saveEventMove({ event, previousEvent }),
  onEventResize: ({ event, previousEvent }) =>
    saveEventResize({ event, previousEvent }),
  onEventDelete: ({ event }) =>
    archiveDeletedEvent(event),
  onSelectionChange: ({ eventIds }) =>
    setSelectedPlannerEvents(eventIds),
  contextMenu: {
    onContextMenuAction: ({ itemId, target, segment, slot }) =>
      trackSchedulerCommand({ itemId, target, eventId: segment?.event.id, slot }),
  },
};
```

The lower-level `event-scheduler-before-event-change`, `event-scheduler-before-event-select`, `event-scheduler-before-slot-select`, `event-scheduler-event-created`, `event-scheduler-event-changed`, `event-scheduler-event-deleted`, and `event-scheduler-event-selected` DOM events remain available for integrations that prefer event listeners. Call `preventDefault()` on the before events to cancel the pending scheduler action. `validateMutation` still runs in the same mutation pipeline for shared validation logic.

## Selection, clipboard, and keyboard

Scheduler selection supports selected events, selected slots, selected time ranges, multi-select, range select, copy, paste, duplicate, and keyboard-driven commands. Use the cancelable `event-scheduler-before-event-select` event when the host application owns selected event state: prevent the pending selection, update application state, then pass the accepted ids back through `selection.selectedEventIds`.

```ts
let selectedEventIds: readonly string[] = [];

grid.addEventListener('event-scheduler-before-event-select', (event) => {
  event.preventDefault();
  selectedEventIds = event.detail.eventIds;
  renderScheduler();
});

function renderScheduler() {
  grid.eventScheduler = {
    ...grid.eventScheduler,
    selectionMode: 'multiple',
    selection: {
      ...grid.eventScheduler?.selection,
      selectedEventIds,
      selectedSlot,
      selectedRange,
      clipboard: true,
      copyOffsetDays: 7,
      onChange: ({ eventIds }) => setSelectedEventIds(eventIds),
    },
    onSelectionChange: ({ eventIds, events }) =>
      syncInspectorSelection({ eventIds, events }),
    onSlotSelect: (slot) =>
      setSelectedSlot(slot),
  };
}
```

Accepted selections still emit `event-scheduler-event-selected`, `onSelectionChange`, and `selection.onChange`. A prevented selection does not emit final selection callbacks; the host re-applies its accepted state through the config.

```ts
grid.eventScheduler = {
  view: 'week',
  weekStartDate: '2026-06-08',
  beforeSlotSelect: ({ date }) =>
    isClosedForBooking(date) ? 'This slot is closed.' : undefined,
};

grid.addEventListener('event-scheduler-before-slot-select', (event) => {
  if (isReadOnlyResource(event.detail.resourceId)) {
    event.preventDefault();
  }
});
```

Use `clipboardAdapter` when copy/paste must integrate with product state, permissions, or a custom clipboard store. Paste remains disabled when there is no internal payload and `clipboardAdapter.hasPayload()` is false.

```ts
grid.eventScheduler = {
  view: 'resourceTimeline',
  weekStartDate: '2026-06-08',
  clipboardAdapter: {
    hasPayload: () => appClipboard.has('scheduler-events'),
    read: () => appClipboard.get('scheduler-events'),
    write: (payload) => appClipboard.set('scheduler-events', payload),
    clear: () => appClipboard.delete('scheduler-events'),
  },
  onCopy: ({ eventIds }) => trackCopy(eventIds),
  onPaste: ({ event }) => persistCreatedEvent(event),
  onDuplicate: ({ event }) => persistCreatedEvent(event),
};
```

Keyboard shortcuts can be disabled globally, disabled per command, or rebound. Use `Mod` for Cmd on macOS and Ctrl on Windows/Linux.

```ts
grid.eventScheduler = {
  view: 'week',
  weekStartDate: '2026-06-08',
  keyboardShortcuts: {
    enabled: true,
    helpPanel: true,
    shortcuts: {
      copy: 'Mod+c',
      paste: 'Mod+v',
      duplicate: 'Mod+Shift+d',
      delete: false,
      lock: false,
      today: ['t', 'Home'],
    },
  },
};
```

Available keyboard commands are `escape`, `help`, `today`, `search`, `previous`, `next`, `cyclePrevious`, `cycleNext`, `open`, `create`, `edit`, `lock`, `delete`, `copy`, `paste`, `duplicate`, and `viewWeek`.
