# Glisser, redimensionner et règles métier

> Validez les déplacements au glisser, empêchez les chevauchements, bloquez du temps, verrouillez des événements, confirmez en asynchrone et expliquez chaque refus.

Source: https://superscheduler.org/fr/docs/drag-resize-rules/
Reviewed: 2026-10-07

Pilotez chaque frame du glisser dans onEventMoving et onEventResizing : définissez args.allowed = false et args.message pour refuser une position en donnant une explication. Empêchez les chevauchements avec allowEventOverlap={false} (ou frame par frame avec args.allowOverlap), bloquez du temps avec des cellules désactivées, verrouillez des événements isolés avec moveDisabled et resizeDisabled, et prenez la décision finale dans onEventMove ou onEventResize, en asynchrone si nécessaire avec args.async = true et args.loaded().

C’est au glisser qu’un planning prouve son utilité, et c’est là que vivent la plupart des règles métier : cette intervention nécessite un pont élévateur, ce séjour ne peut pas être déplacé dans le passé, le bloc opératoire ferme à midi. SuperScheduler Pro consulte votre code à deux moments. **Pendant le glisser**, à chaque changement de l’ombre, vous pouvez accepter, refuser ou ajuster la position, et dire pourquoi. **Au dépôt**, une seule fois, vous pouvez annuler, modifier ou confirmer le changement, y compris après un aller-retour avec votre serveur.

Ce guide construit ces règles sur le planning d’un atelier avec des postes de travail, puis traite des chevauchements, du temps fermé, des verrous, de la confirmation asynchrone, de la carte de glisser et de la création d’événements par sélection d’une plage. Tout ce qui suit nécessite Pro ; Lite est en lecture seule.

## Comment se décide un glisser
1. L’utilisateur saisit un événement. Les événements verrouillés (`moveDisabled`) ne déclenchent pas de glisser.
2. À chaque mouvement du pointeur qui change l’heure ou la ligne cible, **`onEventMoving`** s’exécute (`onEventResizing` pour un redimensionnement). Votre règle définit `args.allowed`, peut ajuster `args.start` et `args.end`, et définit `args.message`.
3. La bibliothèque applique ensuite ses propres contrôles : chevauchement avec d’autres événements quand `allowEventOverlap` vaut `false`, et cellules désactivées. Une ombre refusée est dessinée comme interdite et la carte de glisser affiche le motif.
4. Au relâchement sur une position refusée, il ne se passe rien : l’événement revient à sa place et aucun autre callback ne s’exécute.
5. Au relâchement sur une position acceptée, **`onEventMove`** (`onEventResize`) s’exécute une fois, avant la modification du stockage. Il peut annuler, modifier ou différer la validation.
6. Le stockage est mis à jour, **`onEventMoved`** (`onEventResized`) s’exécute, et `onEventsChange` suit à la microtâche suivante, comme décrit dans [Événements contrôlés et callbacks](https://superscheduler.org/fr/docs/controlled-state/#callback-order).

La même séquence de validation s’applique aux déplacements et redimensionnements effectués au clavier avec `keyboardMode: 'Full'`.

## Valider pendant le glisser
`onEventMoving` reçoit la position candidate et inscrit la décision dans ses arguments :

| Modifiable | Effet |
|---|---|
| `allowed` | `false` dessine l’ombre comme interdite ; un dépôt à cet endroit ne fait rien |
| `message` | Texte affiché par la carte de glisser tant que `allowed` vaut `false` |
| `start`, `end` | Ajustent l’ombre, par exemple pour conserver les horaires d’origine quand seule la ligne change |
| `allowOverlap` | Remplace `allowEventOverlap` pour cette frame uniquement |
| `cssClass`, `html` | Classe et contenu de l’ombre |

Il lit aussi le contexte : `args.e` (l’événement déplacé, avec vos données dans `args.e.data`), `args.resource` et `args.row` (la ligne cible), `args.duration`, `args.conflicts`, `args.external` (glissé depuis l’extérieur du planificateur) et les touches de modification. `onEventResizing` fonctionne de la même façon sur `start`, `end`, `allowed`, `message` et `allowOverlap`, et ajoute `args.what`, le bord en cours de déplacement (`'start'` ou `'end'`).

```tsx
// src/WorkshopPlanning.tsx
import { useMemo } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps } from 'super-scheduler'

const BAYS: SuperScheduler.ResourceData[] = [
  { id: 'bay-1', name: 'Bay 1 (lift)' },
  { id: 'bay-2', name: 'Bay 2 (lift)' },
  { id: 'bay-3', name: 'Bay 3' },
  { id: 'waiting', name: 'Waiting list' },
]
const BAYS_WITH_LIFT: ReadonlySet<SuperScheduler.ResourceId> = new Set(['bay-1', 'bay-2'])

const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
  { groupBy: 'Day', format: 'dddd d MMMM' },
  { groupBy: 'Hour', format: 'HH:mm' },
]

/** A custom field of the job data (see "Custom fields" in the data model guide). */
function needsLift(data: SuperScheduler.EventData): boolean {
  return 'needsLift' in data && data.needsLift === true
}

interface Props {
  readonly jobs: SuperScheduler.EventData[]
  readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
}

export function WorkshopPlanning({ jobs, onEventsChange }: Props) {
  const config = useMemo<SchedulerProps>(
    () => ({
      startDate: '2026-10-12',
      days: 5,
      scale: 'CellDuration',
      cellDuration: 30,
      cellWidth: 48,
      timeHeaders: TIME_HEADERS,
      businessBeginsHour: 8,
      businessEndsHour: 18,
      showNonBusiness: false,
      useEventBoxes: 'Never',
      allowEventOverlap: false,
      conflictHighlight: true,

      // Runs on every shadow change: keep it synchronous and cheap.
      onEventMoving: (args) => {
        if (args.start.getTime() < SuperScheduler.Date.now().getTime()) {
          args.allowed = false
          args.message = 'Jobs cannot be moved into the past.'
          return
        }
        if (needsLift(args.e.data) && !BAYS_WITH_LIFT.has(args.resource)) {
          args.allowed = false
          args.message = 'This job needs a bay with a lift.'
          return
        }
        // The waiting list may hold overlapping jobs; the bays may not (allowEventOverlap above).
        args.allowOverlap = args.resource === 'waiting'
      },

      onEventResizing: (args) => {
        const minutes = (args.end.getTime() - args.start.getTime()) / 60_000
        if (minutes < 30) {
          args.allowed = false
          args.message = 'A job takes at least 30 minutes.'
        }
      },
    }),
    [],
  )

  const owned = useMemo(() => jobs.slice(), [jobs])

  return (
    <SuperSchedulerComponent
      {...config}
      resources={BAYS}
      events={owned}
      onEventsChange={onEventsChange}
    />
  )
}
```
Vous devriez voir un planning de cinq jours en cellules d’une demi-heure, de 08:00 à 18:00. Faites glisser une intervention qui nécessite un pont élévateur sur Bay 3 : l’ombre devient interdite et la carte indique « This job needs a bay with a lift. ». Faites glisser n’importe quelle intervention sur une autre dans un poste : elle est refusée pour chevauchement. Déposez-la sur la liste d’attente : les deux interventions s’y empilent. Raccourcissez une intervention en dessous de 30 minutes : le redimensionnement est refusé.

> **Tip:**
> Ces handlers s’exécutent à chaque frame d’un glisser : gardez-les synchrones et peu coûteux. Consultez des ensembles et des tables précalculés, n’appelez jamais un serveur. Placez les vérifications lentes dans `onEventMove`, qui ne s’exécute qu’une fois.

## Empêcher les chevauchements
`allowEventOverlap={false}` refuse tout déplacement, redimensionnement ou sélection de plage qui chevaucherait un autre événement de la même ligne. Les intervalles sont semi-ouverts : des événements bout à bout (l’un se termine à 11:00, le suivant commence à 11:00) ne comptent jamais comme un chevauchement.

Deux outils affinent la règle selon la situation :

- **`args.allowOverlap`** dans `onEventMoving` et `onEventResizing` remplace l’option pour la frame en cours, comme le fait la liste d’attente ci-dessus. Il est réinitialisé à chaque appel.
- **`args.conflicts`** liste les événements existants avec lesquels l’ombre entre en collision dans la ligne cible (huit au maximum), sous forme d’objets enveloppes `SuperScheduler.Event`. Utilisez-le pour distinguer les conflits tolérables des conflits bloquants :

```ts
// src/soft-conflicts.ts
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

function isTentative(data: SuperScheduler.EventData): boolean {
  return 'status' in data && data.status === 'tentative'
}

/** Tentative jobs may be double-booked; confirmed ones may not. */
export const softConflicts: Pick<
  SchedulerProps,
  'allowEventOverlap' | 'conflictHighlight' | 'onEventMoving'
> = {
  allowEventOverlap: false,
  // Outlines the events the shadow collides with (data-conflict) while dragging.
  conflictHighlight: true,
  onEventMoving: (args) => {
    // Up to eight colliding events in the target row: feedback, not exhaustive validation.
    const hard = args.conflicts.find((event) => !isTentative(event.data))
    if (hard === undefined) {
      // For this frame only; the instance option stays false.
      args.allowOverlap = true
      return
    }
    args.allowed = false
    args.message = `Overlaps ${hard.text()}, which is confirmed.`
  },
}
```
`conflictHighlight` entoure d’un contour les événements en collision pendant le glisser (ils reçoivent un attribut `data-conflict` et un contour de couleur d’alerte) : l’utilisateur voit ce qui gêne, et pas seulement que quelque chose gêne.

> **Limitation:**
> `args.conflicts` est un échantillon limité destiné au retour visuel, pas une validation complète : en cas de nombreuses collisions, il en liste au plus huit. Les règles de chevauchement qui doivent valoir pour chaque événement ont aussi leur place sur votre serveur.

## Bloquer du temps avec des cellules désactivées
Une cellule désactivée est dessinée hachurée et refuse les déplacements, redimensionnements et sélections de plage qui la touchent. Il y a deux façons de désactiver des cellules :

- **Une ligne entière :** `cellsDisabled: true` sur la ressource, pour un poste en réparation ou une chambre hors service.
- **N’importe quelle cellule :** définissez `args.cell.properties.disabled = true` dans `onBeforeCellRender`, pour les pauses déjeuner, les jours fériés ou les horaires d’ouverture propres à chaque ressource.

```tsx
// src/ClosedTime.tsx
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerBeforeCellRenderArgs, SuperScheduler } from 'super-scheduler'

const BAYS: SuperScheduler.ResourceData[] = [
  { id: 'bay-1', name: 'Bay 1' },
  { id: 'bay-2', name: 'Bay 2' },
  // Closed for refurbishment: every cell of the row is disabled.
  { id: 'bay-3', name: 'Bay 3', cellsDisabled: true },
]

/**
 * Module level, so its identity never changes: a new function per render would invalidate the
 * per-cell cache. Disabled cells are hatched and reject drops, resizes and range selection.
 */
function closeLunchBreak(args: SchedulerBeforeCellRenderArgs): void {
  if (args.cell.start.getHours() === 13) {
    args.cell.properties.disabled = true
    args.cell.properties.cssClass = 'lunch-break'
  }
}

export function ClosedTime({ jobs }: { jobs: SuperScheduler.EventData[] }) {
  const owned = useMemo(() => jobs.slice(), [jobs])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-12"
      days={5}
      scale="CellDuration"
      cellDuration={30}
      resources={BAYS}
      events={owned}
      onBeforeCellRender={closeLunchBreak}
    />
  )
}
```
Vous devriez voir Bay 3 hachuré sur toute sa ligne et tous les postes hachurés de 13:00 à 14:00. Les interventions ne peuvent pas être déposées ni étirées à travers la pause déjeuner.

> **Behavior:**
> Les résultats de `onBeforeCellRender` sont mis en cache par cellule. Si le résultat dépend des événements (par exemple des cellules « complètes »), définissez `cellsAutoUpdated: true` sur la ressource pour que ses cellules soient recalculées quand ses événements changent, ou appelez `control.update()` après la modification. Gardez l’identité du handler stable, comme ci-dessus, sinon chaque rendu vide le cache.

Autres façons d’exprimer le temps fermé : le masquer de l’axe avec `showNonBusiness={false}` ou `onIncludeTimeCell` (voir [Heures, minutes, jours et zoom](https://superscheduler.org/fr/docs/time-scales-zoom/#business-hours)), ou le représenter par des événements qui ne peuvent être ni déplacés ni redimensionnés (`moveDisabled`, `resizeDisabled`), comme un blocage de maintenance, qui comptent aussi comme chevauchements quand `allowEventOverlap` vaut `false`.

## Verrouiller des événements isolés
| Champ de l’événement | Effet |
|---|---|
| `moveDisabled` | L’événement ne peut pas être déplacé |
| `resizeDisabled` | L’événement ne peut pas être redimensionné |
| `moveHDisabled` | Il peut changer de ligne mais pas d’horaire |
| `moveVDisabled` | Il peut changer d’horaire mais pas de ligne |
| `clickDisabled`, `deleteDisabled` | Il ignore les clics, ou n’a pas de bouton de suppression |

Pour l’ensemble du planificateur, `eventMoveHandling="Disabled"` et `eventResizeHandling="Disabled"` désactivent les gestes. Les règles qui dépendent de la personne qui regarde (un réceptionniste peut déplacer, un client non) sont des permissions : calculez ces champs à partir du rôle de l’utilisateur avant de passer les événements.

## Confirmer au dépôt, y compris en asynchrone
`onEventMove` et `onEventResize` s’exécutent une fois par dépôt, avant toute modification. Ils peuvent :

- **Annuler** avec `args.preventDefault()`.
- **Modifier** le résultat en affectant `args.newStart`, `args.newEnd` ou `args.newResource`.
- **Différer** avec `args.async = true`, puis appeler `args.loaded()` une fois la réponse obtenue. Appeler `args.preventDefault()` avant `loaded()` annule le dépôt.

```ts
// src/confirm-move.ts
import type {
  SchedulerEventMoveArgs,
  SchedulerEventResizeArgs,
  SuperScheduler,
} from 'super-scheduler'

function inProgress(data: SuperScheduler.EventData): boolean {
  return 'status' in data && data.status === 'inProgress'
}

/**
 * onEventMove: called once on drop, before the store changes. The library never awaits a
 * handler, so an asynchronous decision defers the drop with `async` and finishes it with `loaded()`.
 */
export function confirmMove(args: SchedulerEventMoveArgs): void {
  // A synchronous veto needs no async: cancel and return. This final check also covers moves
  // made with the keyboard.
  if (inProgress(args.e.data) && args.newResource !== args.e.resource()) {
    args.preventDefault()
    args.control.message('A job in progress stays in its bay.')
    return
  }

  args.async = true
  const resource = String(args.newResource)
  void (async () => {
    try {
      const question = `Move ${args.e.text()} to ${resource}, ${args.newStart.toString('ddd d MMM HH:mm')}?`
      if (!(await confirmWithUser(question))) {
        args.preventDefault()
        return
      }
      await saveBooking({
        id: String(args.e.id()),
        resource,
        start: args.newStart.value,
        end: args.newEnd.value,
      })
    } catch {
      args.preventDefault()
      args.control.message('The move could not be saved.')
    } finally {
      // Always: completes the drop, or cancels it when preventDefault() was called first.
      args.loaded()
    }
  })()
}

/** The same protocol for resizing; `what` tells which edge moved. */
export function confirmResize(args: SchedulerEventResizeArgs): void {
  args.async = true
  void saveBooking({
    id: String(args.e.id()),
    resource: String(args.e.resource()),
    start: args.newStart.value,
    end: args.newEnd.value,
  })
    .catch(() => args.preventDefault())
    .finally(() => args.loaded())
}
```
Branchez-les avec `onEventMove={confirmMove}` et `onEventResize={confirmResize}`. Tant que la décision est en attente, l’événement reste à sa position d’origine ; il se déplace quand `loaded()` termine le dépôt, ou reste en place si le dépôt a été annulé.

> **Behavior:**
> Les handlers sont typés comme renvoyant `void`, et la bibliothèque ne les attend jamais. Un handler `async` passe la vérification de types, mais le dépôt est validé dès qu’il renvoie sa promesse. Pour les décisions asynchrones, définissez `args.async = true` de façon synchrone et appelez `args.loaded()` exactement une fois, dans un bloc `finally`, pour qu’une requête en échec ne puisse jamais laisser un dépôt en attente.

## La carte de glisser
Pendant le glisser, une carte à côté du pointeur affiche les dates cibles, la durée (en nuits pour les plages en jours entiers, en heures et minutes sinon), la ligne cible et la raison d’un refus : votre `args.message`, ou l’événement chevauché. Un repère de date apparaît aussi dans l’en-tête de temps (`headerMarker`). Les deux sont activés par défaut.

Configurez la carte avec un objet stable :

```ts
// src/drag-card.ts
import { SuperScheduler } from 'super-scheduler'

// One stable object at module level: a new object per render would reconfigure the card.
export const DRAG_CARD: SuperScheduler.DragCardOptions = {
  // Intraday work: show the time of both edges.
  dateFormat: 'ddd d MMM HH:mm',
  movingDateFormat: 'ddd d MMM HH:mm',
  // Whole-day ranges count nights by default; other ranges show hours and minutes.
  duration: 'auto',
  row: true,
  labels: { overlapping: 'Overlaps', forbidden: 'Not allowed' },
}

// Replace the content when a refusal needs more room. The string is trusted HTML.
export const DRAG_CARD_WITH_REASON: SuperScheduler.DragCardOptions = {
  ...DRAG_CARD,
  html: (info) => {
    if (info.refusal === null) return null // null keeps the default content
    const reason = SuperScheduler.Util.escapeHtml(info.refusal)
    const row = SuperScheduler.Util.escapeHtml(info.rowName ?? '')
    return `<strong>${reason}</strong><br>${row}`
  },
}
```
Passez `dragCard={DRAG_CARD}`, ou `dragCard={false}` pour la supprimer. Les libellés par défaut sont traduits en anglais, espagnol, catalan, basque, galicien, allemand, français, italien et portugais, selon la `locale` du planificateur ; `labels` les remplace. L’option `html` reçoit les dates, le nom de la ligne, le premier conflit, le message de refus et la position de l’événement avant le glisser.

## Créer des événements en sélectionnant du temps
Faire glisser sur des cellules vides sélectionne une plage de temps ; `onTimeRangeSelected` la signale avec `start`, `end` (exclusif), `resource` et `origin`. Y créer un événement est une décision de votre application :

```tsx
// src/CreateOnSelect.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type {
  SchedulerEventsChangeArgs,
  SchedulerTimeRangeSelectedArgs,
  SchedulerTimeRangeSelectingArgs,
  SuperScheduler,
} from 'super-scheduler'

const BAYS: SuperScheduler.ResourceData[] = [
  { id: 'bay-1', name: 'Bay 1' },
  { id: 'bay-2', name: 'Bay 2' },
]

/** Steers the selection while it is drawn, as onEventMoving does for moves: four hours at most. */
function limitToFourHours(args: SchedulerTimeRangeSelectingArgs): void {
  args.allowed = args.end.getTime() - args.start.getTime() <= 4 * 3_600_000
}

export function CreateOnSelect() {
  const [jobs, setJobs] = useState<SuperScheduler.EventData[]>([])
  const owned = useMemo(() => jobs.slice(), [jobs])
  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => setJobs([...args.events]),
    [],
  )

  const onTimeRangeSelected = useCallback((args: SchedulerTimeRangeSelectedArgs) => {
    // The selection shadow stays until cleared.
    args.control.clearSelection()
    // A plain click on an empty cell also selects it (origin 'click'): create only on a drag.
    if (args.origin !== 'drag') return
    setJobs((current) => [
      ...current,
      {
        id: crypto.randomUUID(),
        resource: args.resource,
        // Store strings: start and end arrive as SuperScheduler.Date (end exclusive).
        start: args.start.value,
        end: args.end.value,
        text: 'New job',
      },
    ])
  }, [])

  return (
    <SuperSchedulerComponent
      startDate="2026-10-12"
      days={5}
      scale="CellDuration"
      cellDuration={30}
      resources={BAYS}
      events={owned}
      allowEventOverlap={false}
      onTimeRangeSelecting={limitToFourHours}
      onTimeRangeSelected={onTimeRangeSelected}
      onEventsChange={onEventsChange}
    />
  )
}
```
Vous devriez voir une ombre de sélection suivre le pointeur, refuser de dépasser quatre heures ou de recouvrir une autre intervention, puis devenir un événement « New job » au relâchement.

Trois comportements à connaître :

- **Un simple clic est aussi une sélection.** Cliquer sur une cellule vide déclenche `onTimeRangeSelected` avec `origin: 'click'` et une seule cellule. Vérifiez `origin === 'drag'` si un clic ne doit rien créer.
- **La sélection reste visible** après le relâchement, jusqu’à la sélection suivante ou jusqu’à `args.control.clearSelection()`.
- **Les sélections suivent les mêmes règles que les déplacements :** elles ne peuvent traverser ni des cellules désactivées, ni du temps occupé quand `allowEventOverlap` vaut `false`. `onTimeRangeSelecting` les pilote frame par frame avec `args.allowed`.

## Ce qui reste dans votre application
La bibliothèque fait respecter ce que vous configurez et signale ce qui se passe. Votre application possède les règles elles-mêmes (quelle ressource accepte quel travail, qui peut modifier quoi), la validation côté serveur de chaque modification, ainsi que tout placement automatique ou toute optimisation. Déplacer une réservation ne replanifie jamais les autres : si votre métier exige des modifications en cascade, calculez-les et mettez à jour les événements vous-même.

→ https://superscheduler.org/fr/examples/clinic-appointments/
→ https://superscheduler.org/fr/examples/manufacturing-orders/
→ https://superscheduler.org/fr/examples/sports-club-courts/
## Étapes suivantes
- Enregistrez les modifications acceptées par ces règles : [Événements contrôlés et callbacks](https://superscheduler.org/fr/docs/controlled-state/#persistence).
- Permettez aux utilisateurs d’annuler un déplacement : [Annuler et rétablir](https://superscheduler.org/fr/docs/undo-redo/).
- Appliquez les mêmes règles depuis le clavier : [Clavier, accessibilité et tactile](https://superscheduler.org/fr/docs/keyboard-accessibility-touch/).
