# Arborescences de ressources, colonnes et sélection

> Groupez les ressources en arborescence, ajoutez des colonnes d’en-tête triables et redimensionnables, filtrez lignes et événements, et sélectionnez dans Pro.

Source: https://superscheduler.org/fr/docs/trees-columns-selection/
Reviewed: 2026-10-07

Activez treeEnabled et imbriquez les ressources dans children ; un parent reste replié sauf s’il a expanded: true. Ajoutez des colonnes d’en-tête de ligne avec rowHeaderColumns, en remplissant les cellules à partir des champs de ressource via display. Filtrez avec control.rows.filter() et control.events.filter() plus onRowFilter et onEventFilter, et sélectionnez avec eventClickHandling: 'Select', la sélection rectangulaire, rowClickHandling: 'Select' et les API multiselect et multirange.

Les vrais plannings ont rarement une liste plate de ressources. Les chambres appartiennent à des étages, les techniciens à des régions, les machines à des lignes de production. Ce guide présente les fonctionnalités Pro qui organisent les lignes et permettent d’agir sur plusieurs éléments à la fois : arborescences de ressources, colonnes d’en-tête de ligne, filtres, et les trois types de sélection (événements, lignes et plages de temps).

Tout ce qui suit nécessite SuperScheduler Pro (`super-scheduler`). L’édition Lite affiche une liste plate et refuse les `children` et `columns` de ressource.

## Grouper les ressources en arborescence
Une arborescence, ce sont de simples données : une ressource avec un tableau `children` devient une ligne parente. Deux réglages l’activent :

- `treeEnabled: true` sur le planificateur. Sans lui, les `children` sont ignorés et l’en-tête de ligne n’a pas de bouton de dépliage.
- `expanded: true` sur chaque parent qui doit être ouvert au départ. Les parents sont repliés sauf si `expanded` vaut exactement `true`.

```tsx
// src/TeamPlanner.tsx
import { useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'

// Regions are parent rows and technicians their children. A parent stays collapsed
// unless its resource says `expanded: true`.
const RESOURCES: SuperScheduler.ResourceData[] = [
  {
    id: 'north',
    name: 'North region',
    expanded: true,
    children: [
      { id: 'ana', name: 'Ana Ruiz' },
      { id: 'ben', name: 'Ben Ortiz' },
    ],
  },
  {
    id: 'south',
    name: 'South region',
    children: [
      { id: 'cai', name: 'Cai Moreno' },
      { id: 'dee', name: 'Dee Patel' },
    ],
  },
]

const JOBS: SuperScheduler.EventData[] = [
  {
    id: 'job-1',
    resource: 'ana',
    start: '2026-10-05T00:00:00',
    end: '2026-10-07T00:00:00',
    text: 'Panel upgrade',
  },
  {
    id: 'job-2',
    resource: 'cai',
    start: '2026-10-06T00:00:00',
    end: '2026-10-09T00:00:00',
    text: 'Boiler service',
  },
]

export function TeamPlanner() {
  const { controlRef, control } = useSchedulerControl()
  // The control adopts the events array and mutates it: hand it a copy.
  const events = useMemo(() => JOBS.slice(), [])

  return (
    <section aria-labelledby="team-plan-title">
      <h2 id="team-plan-title">Field teams</h2>
      <div role="toolbar" aria-label="Rows">
        <button type="button" disabled={control === null} onClick={() => control?.rows.expandAll()}>
          Expand all
        </button>
        <button
          type="button"
          disabled={control === null}
          onClick={() => control?.rows.collapseAll()}
        >
          Collapse all
        </button>
      </div>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        cellWidth={56}
        treeEnabled
        treeIndent={16}
        // Region rows group technicians: no jobs and no time selection on them.
        treePreventParentUsage
        resources={RESOURCES}
        events={events}
      />
    </section>
  )
}
```
Vous devriez voir « North region » ouvert avec ses deux techniciens, « South region » fermé, et les deux boutons fonctionnels une fois le planificateur monté (`control` vaut `null` jusque-là).

Options associées :

| Option | Défaut | Effet |
|---|---|---|
| `treeEnabled` | `false` | Construit les lignes à partir de `children`. |
| `treeIndent` | `20` | Indentation par niveau, en pixels. |
| `treePreventParentUsage` | `false` | Les lignes parentes n’acceptent ni événements ni sélection de temps. |
| `treeAutoExpand` | `true` | Un parent replié s’ouvre quand un événement est glissé au-dessus de lui pendant une demi-seconde. |
| `rowFilterParentsAlwaysVisible` | `true` | Conserve les ancêtres d’une ligne qui correspond à un filtre. |

Les parents peuvent aussi porter des événements (une réservation pour « toute l’équipe »), sauf si vous activez `treePreventParentUsage`. Une ressource isolée refuse les événements déposés avec `preventUsage: true`, et les ressources de premier niveau peuvent être épinglées au-dessus ou en dessous des lignes défilantes avec `frozen: 'top'` ou `frozen: 'bottom'`, ce qui convient aux lignes de synthèse.

> **Behavior:**
> Quand une ligne s’ouvre ou se ferme, le contrôle réécrit le nouvel état dans le champ `expanded` de l’objet ressource correspondant. Si votre application passe plus tard un nouveau tableau `resources`, les valeurs `expanded` de ce tableau l’emportent.

### Déplier et replier depuis le code
Le contrôle expose l’arborescence via `control.rows` :

- `control.rows.expandAll()` et `control.rows.collapseAll()` ;
- `control.rows.expand(level)` ouvre les parents situés au-dessus d’un niveau (`1` par défaut, `-1` pour tous les niveaux) ;
- `control.rows.find(id)` renvoie une ligne ; `row.expand()`, `row.collapse()` et `row.toggle()` la modifient.

`onResourceExpand` et `onResourceCollapse` signalent un dépliage ou un repliage une fois qu’il a eu lieu, avec la ligne dans `args.resource`. Utilisez-les pour mémoriser ce que l’utilisateur a ouvert ou pour synchroniser d’autres parties de votre interface.

> **Limitation:**
> Ces deux handlers sont des notifications : le type de leurs arguments inclut `preventDefault()`, mais l’appeler n’empêche pas la ligne de basculer. Le chargement des enfants à l’ouverture d’un parent (`onLoadNode`) et le glisser de lignes pour les réordonner (`rowMoveHandling`) sont réservés et non implémentés. `treeAnimation` est accepté mais sans effet ; les lignes s’ouvrent sans animation.

## Ajouter des colonnes d’en-tête de ligne
Sans `rowHeaderColumns`, l’en-tête de ligne affiche le `name` de chaque ressource. Avec cette option, l’en-tête devient un petit tableau :

- `text` est le titre de la colonne (`name` et `title` sont aussi acceptés) ;
- `display` désigne un champ de la ressource dont la valeur remplit la cellule (un champ de même nom dans `tags` est lu en premier) ;
- la première colonne affiche le `name` de la ressource quand elle n’a pas de `display` ;
- `width` définit la largeur en pixels (par défaut `rowHeaderColumnDefaultWidth`, soit 80) ;
- `sort` rend le titre cliquable : il trie les lignes selon ce champ, par ordre croissant puis décroissant, et définit `aria-sort` sur le titre.

Une ressource peut aussi porter ses propres `columns: [{ text }, { html }]`. Ces entrées sont associées par position, une par colonne.

```tsx
// src/TechnicianColumns.tsx
import { useState } from 'react'
import { type SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import 'super-scheduler/styles.css'

type Status = 'available' | 'off'

const TECHNICIANS: SuperScheduler.ResourceData[] = [
  { id: 'ana', name: 'Ana Ruiz', skill: 'Electrical', shift: 'Early', status: 'available' },
  { id: 'ben', name: 'Ben Ortiz', skill: 'HVAC', shift: 'Late', status: 'off' },
  { id: 'cai', name: 'Cai Moreno', skill: 'Plumbing', shift: 'Early', status: 'available' },
]

// `display` reads a resource field, so a column keeps its values when it is reordered.
// `sort` makes the title clickable: it sorts the rows by that field, ascending then descending.
const INITIAL_COLUMNS: SuperScheduler.RowHeaderColumnData[] = [
  { text: 'Technician', width: 150, sort: 'name' },
  { text: 'Skill', display: 'skill', width: 110, sort: 'skill' },
  { text: 'Shift', display: 'shift', width: 70, nonresizable: true },
  { text: 'Status', display: 'status', width: 100 },
]

const STATUS_LABEL: Record<Status, string> = { available: 'Available', off: 'Day off' }

// Runs with `this` = the control, whose rowHeaderColumns are always in the current order.
function decorateStatus(
  this: SuperScheduler.SchedulerApi,
  args: SuperScheduler.SchedulerBeforeRowHeaderRenderArgs,
): void {
  const index = this.rowHeaderColumns?.findIndex((column) => column.display === 'status') ?? -1
  const cell = args.row.columns[index]
  const status = args.row.data.status
  if (cell === undefined || (status !== 'available' && status !== 'off')) return
  // With rowHeaderColumns, row.html is ignored: write the column's own html (trusted markup).
  cell.html = `<span class="tech-status tech-status--${status}">${STATUS_LABEL[status]}</span>`
}

export function TechnicianColumns({ events }: { events: SuperScheduler.EventData[] }) {
  // Kept in state so a parent re-render never hands the control the original order again.
  const [columns, setColumns] = useState(INITIAL_COLUMNS)

  return (
    <SuperSchedulerComponent
      startDate="2026-10-05"
      days={14}
      scale="Day"
      cellWidth={56}
      resources={TECHNICIANS}
      events={events}
      rowHeaderColumns={columns}
      rowHeaderColumnsReorderable
      onRowHeaderColumnsChange={(args) => setColumns([...args.columns])}
      onBeforeRowHeaderRender={decorateStatus}
    />
  )
}
```
Pour décorer une cellule, utilisez `onBeforeRowHeaderRender`. Avec des colonnes, le `html` propre à la ligne est ignoré : écrivez plutôt `args.row.columns[i].html`. Ce balisage est du HTML de confiance : échappez donc le texte saisi par les utilisateurs avec `SuperScheduler.Util.escapeHtml`. Pour du contenu React dans l’en-tête de ligne, voir [Slots de rendu React](https://superscheduler.org/fr/docs/react-render-slots/).

### Redimensionner et réordonner les colonnes
Les colonnes sont redimensionnables par défaut (`rowHeaderColumnsResizable: true`) ; `nonresizable: true` le désactive pour une colonne. Après un redimensionnement, le contrôle écrit la nouvelle largeur dans l’objet colonne correspondant de votre tableau `rowHeaderColumns` et appelle `onRowHeaderColumnResized({ column })`.

Le réordonnancement est désactivé par défaut. Avec `rowHeaderColumnsReorderable`, les utilisateurs font glisser les titres de colonnes ; les bords de redimensionnement restent prioritaires sur le glisser. En `keyboardMode: 'Full'`, Alt+Maj+Flèche gauche et Alt+Maj+Flèche droite déplacent le titre qui a le focus et annoncent le changement. Depuis le code, `control.moveRowHeaderColumn(from, to)` déplace une colonne d’après son index dans l’ordre actuel.

Chaque redimensionnement et chaque réordonnancement appellent `onRowHeaderColumnsChange` avec :

- `columns` : les colonnes dans leur nouvel ordre, avec leurs largeurs ;
- `order` : `order[i]` est l’index d’origine (dans la prop que vous avez passée) de la colonne désormais en position `i` ;
- `widths` et `reason` (`'resize'` ou `'reorder'`).

Gardez les colonnes dans le state React, comme le fait le snippet, et adoptez `args.columns`. Sinon, un parent qui refait son rendu avec un nouveau littéral de tableau redonnerait l’ordre d’origine au contrôle.

> **Tip:**
> Remplissez les colonnes réordonnables via `display`. Une valeur lue depuis un champ suit sa colonne ; les entrées positionnelles `resource.columns[i]` restent à leur position. Pour conserver largeurs et ordre d’une session à l’autre, enregistrez-les avec les [vues enregistrées](https://superscheduler.org/fr/docs/panes-saved-views/).

## Filtrer lignes et événements
Un filtre, c’est un paramètre plus un callback. `control.rows.filter(param)` stocke le paramètre et appelle `onRowFilter` pour chaque ligne ; définissez `args.visible = false` pour la masquer. `control.events.filter(param)` et `onEventFilter` fonctionnent de la même façon pour les événements. Un paramètre falsy (`''`, `null`, `0`) efface le filtre.

```tsx
// src/FilteredJobs.tsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'

type JobStatus = 'scheduled' | 'done' | 'cancelled'
type Job = { status: JobStatus }

// Module-level handlers keep their identity: a new onRowFilter or onEventFilter
// function makes the control evaluate the active filter again.
const FILTERS: Pick<SchedulerProps, 'onRowFilter' | 'onEventFilter'> = {
  onRowFilter: (args) => {
    const query = String(args.filterParam).toLowerCase()
    args.visible = args.row.name.toLowerCase().includes(query)
  },
  onEventFilter: (args) => {
    const job = args.e.data as SuperScheduler.EventData<Job>
    args.visible = job.status === args.filterParam
  },
}

export function FilteredJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const [query, setQuery] = useState('')
  const [status, setStatus] = useState<JobStatus | ''>('')
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // A falsy parameter ('' or null) clears the filter.
  useEffect(() => control?.rows.filter(query.trim() || null), [control, query])
  useEffect(() => control?.events.filter(status || null), [control, status])

  return (
    <>
      <label>
        Technician <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} />
      </label>
      <label>
        Status
        <select value={status} onChange={(e) => setStatus(e.target.value as JobStatus | '')}>
          <option value="">All</option>
          <option value="scheduled">Scheduled</option>
          <option value="done">Done</option>
          <option value="cancelled">Cancelled</option>
        </select>
      </label>
      <SuperSchedulerComponent
        {...FILTERS}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        treeEnabled
        // Keeps the region row of every matching technician (default true).
        rowFilterParentsAlwaysVisible
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
Saisir « ben » laisse « North region » et « Ben Ortiz » : `rowFilterParentsAlwaysVisible` conserve les ancêtres de chaque ligne correspondante. Choisir un statut masque les autres événements sans toucher aux lignes.

> **Behavior:**
> Le contrôle réévalue le filtre actif quand la fonction `onRowFilter` ou `onEventFilter` change. Définissez-les au niveau du module ou mémoïsez-les. La sélection de plage par Maj+clic ignore les lignes masquées et les événements filtrés.

Les filtres s’exécutent dans le navigateur, sur les données que détient le contrôle. Une recherche portant sur des données non chargées (un autre mois, un autre site) est une requête à votre backend, suivie de nouvelles `resources` ou de nouveaux `events`.

## Sélectionner des événements
Par défaut, un clic sur un événement exécute seulement `onEventClick`. La sélection nécessite `eventClickHandling: 'Select'` :

- un clic sélectionne ou désélectionne l’événement et désélectionne les autres ;
- Ctrl+clic (Cmd+clic sur macOS) le sélectionne ou le désélectionne en conservant les autres, tant que `allowMultiSelect` vaut `true` (par défaut) ;
- avec `eventMultiSelectRange`, Maj+clic sélectionne les événements situés entre le dernier événement cliqué et celui-ci, à travers les lignes visibles ; Ctrl/Cmd+Maj les ajoute.

`onEventSelect` s’exécute avant le changement et peut l’annuler avec `args.preventDefault()` ; `onEventSelected` s’exécute après.

L’API `control.multiselect` lit et modifie la sélection : `get()`, `add(e)`, `remove(e)`, `clear()`, `isSelected(e)` et `selectAll({ scope, filter })`. `selectAll` sélectionne par défaut les événements visibles ; `scope: 'all'` parcourt tous les événements chargés, pour un coût proportionnel à la taille du stockage.

```tsx
// src/SelectableJobs.tsx
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { subscribeScheduler } from 'super-scheduler/hooks'

type Job = { status: 'scheduled' | 'done' }

export function SelectableJobs(props: {
  resources: SuperScheduler.ResourceData[]
  jobs: SuperScheduler.EventData<Job>[]
  onReschedule: (ids: SuperScheduler.EventId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [count, setCount] = useState(0)
  const events = useMemo(() => props.jobs.slice(), [props.jobs])

  // Rectangle selections and multiselect calls (selectAll, clear) publish the
  // 'selection' topic; clicks are reported by onEventSelected below.
  useEffect(() => {
    if (control === null) return
    return subscribeScheduler(control, 'selection', (selected) => setCount(selected.length))
  }, [control])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click toggles the event; Ctrl+click (Cmd+click on macOS) keeps the others.
      eventClickHandling: 'Select',
      // Shift+click selects every event between the last clicked one and this one.
      eventMultiSelectRange: true,
      // Shift+drag over the grid selects the events inside the rectangle.
      rectangleSelectHandling: 'EventSelect',
      // With the rectangle: Shift adds to the selection, Alt removes from it.
      rectangleSelectModifiers: true,
      onEventSelected() {
        setCount(this.multiselect.get().length)
      },
      // Marks selected events for CSS. The hook runs again for every event whose
      // selection changes.
      onBeforeEventRender: (args) => {
        const e = args.control.events.find(args.data.id)
        if (e !== null && args.control.multiselect.isSelected(e)) {
          args.data.cssClass = [args.data.cssClass, 'is-selected'].filter(Boolean).join(' ')
        }
      },
    }),
    [],
  )

  const selectScheduled = () =>
    control?.multiselect.selectAll({
      scope: 'view',
      filter: (e) => (e.data as SuperScheduler.EventData<Job>).status === 'scheduled',
    })

  const reschedule = () => {
    const ids = control?.multiselect.get().map((e) => e.id()) ?? []
    if (ids.length > 0) props.onReschedule(ids)
  }

  return (
    <>
      <div role="toolbar" aria-label="Selection">
        <button type="button" onClick={selectScheduled}>
          Select scheduled in view
        </button>
        <button type="button" onClick={() => control?.multiselect.clear()}>
          Clear
        </button>
        <button type="button" disabled={count === 0} onClick={reschedule}>
          Reschedule {count} {count === 1 ? 'job' : 'jobs'}
        </button>
      </div>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
Le compteur du bouton devrait suivre les clics, les rectangles et les actions de la barre d’outils, et les interventions sélectionnées devraient être entourées d’un contour grâce à ce CSS :

```css
.super-scheduler__event.is-selected {
  z-index: 2;
  box-shadow: 0 0 0 2px var(--super-scheduler-accent), var(--super-scheduler-shadow-2);
}
```

> **Limitation:**
> Dans la version 0.1.0, un événement sélectionné ne reçoit aucune marque visuelle propre : le style de sélection de la feuille de style cible un attribut `data-selected` que les événements ne reçoivent pas. Ajoutez une classe dans `onBeforeEventRender`, comme le fait le snippet ; le hook s’exécute de nouveau pour chaque événement dont la sélection change. Avec le préfixe facultatif `theme`, les événements sélectionnés reçoivent aussi une classe `{theme}_selected`.

> **Behavior:**
> Les clics sont signalés par `onEventSelected`. Les sélections rectangulaires et les appels à `multiselect` (`selectAll`, `clear`, `add`, `remove`) publient le sujet `selection` de `super-scheduler/hooks`. Le snippet écoute les deux pour que le compteur soit toujours à jour.

Avec `allowMultiMove`, faire glisser un événement sélectionné déplace toute la sélection ; chaque événement passe par les mêmes règles de déplacement (voir [glisser, redimensionner et règles métier](https://superscheduler.org/fr/docs/drag-resize-rules/)). En mode clavier complet, Ctrl/Cmd+A sélectionne tous les événements visibles.

### Sélection rectangulaire
`rectangleSelectHandling` détermine ce que fait un rectangle tracé au glisser :

- `'EventSelect'` sélectionne les événements qu’il contient (et exécute les callbacks) ;
- `'Enabled'` exécute seulement les callbacks, pour les applications qui agissent elles-mêmes sur le rectangle ;
- `'Disabled'` (par défaut) le désactive.

Les utilisateurs tracent un rectangle avec Maj+glisser sur la grille. Pour en démarrer un sans Maj, définissez `args.action = 'RectangleSelect'` dans `onGridMouseDown`, ou appelez `control.multiselect.startRectangle()` avant l’appui suivant. `rectangleSelectMode: 'Row'` maintient le rectangle dans la ligne où l’appui a commencé. Avec `rectangleSelectModifiers`, Maj ajoute à la sélection courante et Alt en retire ; sans l’une ni l’autre touche, le rectangle remplace la sélection.

Les callbacks sont `onRectangleSelecting` (à chaque frame ; `args.visible = false` masque le rectangle), `onRectangleSelect` (annulable, avec `events`, `start`, `end` et `resources`) et `onRectangleSelected`.

## Sélectionner des lignes
`rowClickHandling: 'Select'` fait qu’un clic sur un en-tête de ligne sélectionne la ligne. Ctrl/Cmd sélectionne ou désélectionne une ligne, et Maj sélectionne une plage depuis la dernière ligne cliquée. Les lignes sélectionnées portent `aria-selected="true"`.

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

export function RosterRows(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onNotify: (resourceIds: SuperScheduler.ResourceId[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [selected, setSelected] = useState<readonly SuperScheduler.ResourceId[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // A click on a row header selects the row; Ctrl/Cmd toggles, Shift selects a range.
      rowClickHandling: 'Select',
      onRowSelected() {
        setSelected(this.rows.selection.get().map((row) => row.id))
      },
    }),
    [],
  )

  return (
    <>
      <button
        type="button"
        disabled={selected.length === 0}
        onClick={() => props.onNotify([...selected])}
      >
        Notify {selected.length} technicians
      </button>
      <button
        type="button"
        onClick={() => {
          control?.rows.selection.clear()
          setSelected([])
        }}
      >
        Clear rows
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
`control.rows.selection` propose `get()`, `add(row)`, `remove(row)`, `isSelected(row)` et `clear()` ; ces appels mettent les lignes en évidence immédiatement et ne déclenchent aucun callback, c’est pourquoi le snippet réinitialise son propre state après `clear()`. `onRowSelect` (annulable) et `onRowSelected` s’exécutent autour de chaque sélection faite par un clic.

La prop `selectedRows` contient les ids des ressources sélectionnées. Passez-la pour démarrer avec une sélection ; le contrôle met à jour ce même tableau quand la sélection change.

## Sélectionner des plages de temps
Faire glisser sur des cellules vides sélectionne une plage de temps et appelle `onTimeRangeSelected` une seule fois, au relâchement, avec `start`, `end` (exclusif), `resource` et `origin: 'drag'`. Autres origines :

- `'click'` : un simple clic sur une cellule vide sélectionne cette cellule ;
- `'keyboard'` : avec le clavier activé, Entrée sur une cellule qui a le focus, ou Maj+Flèche gauche/droite pour étendre une plage ;
- `'api'` : `control.selectTimeRange(start, end, resource)`.

Vérifiez `args.origin` quand seul un glisser doit créer quelque chose. La sélection reste dessinée après le relâchement, jusqu’à la sélection suivante ou jusqu’à `control.clearSelection()`. Un clic ailleurs l’efface aussi, sauf avec `timeRangeSelectedHandling: 'HoldForever'` ; `'Disabled'` désactive la sélection de plages.

`onTimeRangeSelecting` s’exécute à chaque changement pendant le glisser. Définissez `args.allowed = false` pour refuser une plage (l’ombre est marquée et le relâchement ne sélectionne rien), ou réécrivez `args.start` et `args.end` pour la borner. Les cellules désactivées et les parents soumis à `treePreventParentUsage` refusent les sélections sans aucun code.

### Plusieurs plages à la fois
Avec `allowMultiRange`, un nouveau glisser peut ajouter une plage au lieu de remplacer la plage courante : tant que Ctrl/Cmd est maintenu (`multiRangeMode: 'CtrlOrMeta'`, par défaut) ou toujours (`'Always'`). `args.multirange` liste toutes les plages sélectionnées, et `control.multirange` les lit ou les modifie : `get()`, `clear()` et `add(new SuperScheduler.Selection(start, end, resource))`.

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

export interface BlockRequest {
  readonly resource: SuperScheduler.ResourceId
  /** ISO wall-clock values; `end` is exclusive. */
  readonly start: string
  readonly end: string
}

export function RangeBlocker(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
  onBlock: (requests: BlockRequest[]) => void
}) {
  const { controlRef, control } = useSchedulerControl()
  const [ranges, setRanges] = useState<readonly SuperScheduler.SchedulerRange[]>([])
  const events = useMemo(() => props.events.slice(), [props.events])

  const config = useMemo<SchedulerProps>(
    () => ({
      // Ctrl/Cmd + drag adds a range instead of replacing the current one.
      allowMultiRange: true,
      multiRangeMode: 'CtrlOrMeta',
      onTimeRangeSelecting: (args) => {
        // Past days cannot be blocked: the shadow turns red and the drop is refused.
        args.allowed = args.start.getTime() >= SuperScheduler.Date.today().getTime()
      },
      onTimeRangeSelected: (args) => {
        // A plain click on an empty cell also selects it (origin 'click').
        setRanges([...args.multirange])
      },
    }),
    [],
  )

  const block = () => {
    props.onBlock(
      ranges.map((range) => ({
        resource: range.resource,
        start: range.start.value,
        end: range.end.value,
      })),
    )
    control?.clearSelection()
    setRanges([])
  }

  return (
    <>
      <button type="button" disabled={ranges.length === 0} onClick={block}>
        Block {ranges.length} selected {ranges.length === 1 ? 'range' : 'ranges'}
      </button>
      <SuperSchedulerComponent
        {...config}
        controlRef={controlRef}
        startDate="2026-10-05"
        days={14}
        scale="Day"
        resources={props.resources}
        events={events}
      />
    </>
  )
}
```
Faites glisser sur des jours libres d’une chambre, puis d’une autre en maintenant Ctrl (Cmd sur macOS) : le bouton indique « Block 2 selected ranges ». Les plages parviennent à `onBlock` sous forme de valeurs ISO en heure locale avec fins exclusives, prêtes à être envoyées à votre backend.

## Ce que possède votre application
La bibliothèque conserve arborescences, filtres et sélections dans le navigateur, pour la session en cours. Votre application décide :

- d’où vient l’arborescence : les lignes sont les `resources` que vous passez, et le contrôle ne demande jamais d’enfants de lui-même (`onLoadNode` est réservé) ;
- ce que signifie un filtre quand les données ne sont pas chargées, et comment la recherche atteint le backend ;
- ce que fait une sélection : actions groupées, permissions pour les exécuter et persistance ;
- si les lignes dépliées et la disposition des colonnes survivent à un rechargement (voir [vues enregistrées](https://superscheduler.org/fr/docs/panes-saved-views/)).

Les sélections ne sont enregistrées nulle part. L’état déplié vit dans les objets ressources que vous avez passés, jusqu’à ce que vous les remplaciez.

## Voir aussi
→ https://superscheduler.org/fr/examples/hotel-rooms/
→ https://superscheduler.org/fr/examples/training-rooms/
→ https://superscheduler.org/fr/examples/field-service-dispatch/
- [Clavier, accessibilité et tactile](https://superscheduler.org/fr/docs/keyboard-accessibility-touch/) liste les touches de sélection.
- [Événements contrôlés et callbacks](https://superscheduler.org/fr/docs/controlled-state/) explique comment les événements sélectionnés parviennent à votre state.
- [Volets coordonnés et vues enregistrées](https://superscheduler.org/fr/docs/panes-saved-views/) conserve les lignes repliées et les largeurs de colonnes.
