# Ressourcenbäume, Spalten und Auswahl

> Ressourcen im Baum gruppieren, sortier- und verschiebbare Spalten im Zeilenkopf ergänzen, Zeilen und Ereignisse filtern und Ereignisse, Zeilen oder Zeiträume wählen.

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

Setzen Sie treeEnabled und verschachteln Sie Ressourcen in children; ein Elternknoten bleibt eingeklappt, sofern er nicht expanded: true hat. Spalten im Zeilenkopf ergänzen Sie mit rowHeaderColumns, deren Zellen über display aus Feldern der Ressource gefüllt werden. Filtern Sie mit control.rows.filter() und control.events.filter() zusammen mit onRowFilter und onEventFilter, und wählen Sie aus mit eventClickHandling: 'Select', Rechteckauswahl, rowClickHandling: 'Select' sowie den APIs multiselect und multirange.

Echte Planungen haben selten eine flache Liste von Ressourcen. Zimmer gehören zu Etagen, Techniker zu Regionen, Maschinen zu Linien. Dieser Leitfaden behandelt die Pro-Funktionen, die Zeilen ordnen und mit mehreren Elementen gleichzeitig arbeiten: Ressourcenbäume, Spalten im Zeilenkopf, Filter und die drei Arten der Auswahl (Ereignisse, Zeilen und Zeiträume).

Alles hier erfordert SuperScheduler Pro (`super-scheduler`). Die Edition Lite rendert eine flache Liste und lehnt `children` und `columns` an Ressourcen ab.

## Ressourcen in einem Baum gruppieren
Ein Baum besteht aus einfachen Daten: Eine Ressource mit einem `children`-Array wird zur Elternzeile. Zwei Dinge schalten ihn ein:

- `treeEnabled: true` am Planer. Ohne diese Option werden `children` ignoriert, und der Zeilenkopf hat keine Umschalter.
- `expanded: true` an jedem Elternknoten, der geöffnet starten soll. Elternknoten sind eingeklappt, sofern `expanded` nicht genau `true` ist.

```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>
  )
}
```
Sie sollten „North region“ geöffnet mit beiden Technikern sehen und „South region“ geschlossen; beide Buttons funktionieren, sobald der Planer gemountet ist (bis dahin ist `control` `null`).

Zugehörige Optionen:

| Option | Standard | Wirkung |
|---|---|---|
| `treeEnabled` | `false` | Baut Zeilen aus `children` auf. |
| `treeIndent` | `20` | Einrückung pro Ebene, in Pixeln. |
| `treePreventParentUsage` | `false` | Elternzeilen nehmen weder Ereignisse noch eine Zeitauswahl an. |
| `treeAutoExpand` | `true` | Ein eingeklappter Elternknoten öffnet sich, wenn eine halbe Sekunde lang ein Ereignis über ihn gezogen wird. |
| `rowFilterParentsAlwaysVisible` | `true` | Behält die Vorfahren einer Zeile, die einem Filter entspricht. |

Elternknoten können auch Ereignisse enthalten (eine Buchung für das „ganze Team“), sofern Sie nicht `treePreventParentUsage` setzen. Eine einzelne Ressource lehnt abgelegte Ereignisse mit `preventUsage: true` ab, und Ressourcen der obersten Ebene lassen sich mit `frozen: 'top'` oder `frozen: 'bottom'` über oder unter den scrollenden Zeilen fixieren, was sich für Summenzeilen eignet.

> **Behavior:**
> Wenn sich eine Zeile öffnet oder schließt, schreibt das Control den neuen Zustand in das Feld `expanded` dieses Ressourcenobjekts zurück. Übergibt Ihre Anwendung später ein neues `resources`-Array, gewinnen die `expanded`-Werte in diesem Array.

### Aus Code auf- und zuklappen
Das Control stellt den Baum über `control.rows` bereit:

- `control.rows.expandAll()` und `control.rows.collapseAll()`;
- `control.rows.expand(level)` öffnet die Elternknoten oberhalb einer Ebene (standardmäßig `1`, `-1` für alle Ebenen);
- `control.rows.find(id)` gibt eine Zeile zurück; `row.expand()`, `row.collapse()` und `row.toggle()` ändern sie.

`onResourceExpand` und `onResourceCollapse` melden ein Umschalten, nachdem es passiert ist, mit der Zeile als `args.resource`. Nutzen Sie sie, um sich zu merken, was der Nutzer geöffnet hat, oder um andere Teile Ihrer Oberfläche synchron zu halten.

> **Limitation:**
> Diese beiden Handler sind Benachrichtigungen: Ihr Argumenttyp enthält zwar `preventDefault()`, aber ein Aufruf verhindert das Umschalten der Zeile nicht. Das Nachladen von Kindern beim Öffnen eines Elternknotens (`onLoadNode`) und das Umsortieren von Zeilen per Ziehen (`rowMoveHandling`) sind reserviert und nicht implementiert. `treeAnimation` wird akzeptiert, hat aber keine Wirkung; Zeilen öffnen sich ohne Animation.

## Spalten im Zeilenkopf ergänzen
Ohne `rowHeaderColumns` zeigt der Zeilenkopf den `name` jeder Ressource. Mit der Option wird der Kopf zu einer kleinen Tabelle:

- `text` ist der Spaltentitel (`name` und `title` werden ebenfalls akzeptiert);
- `display` benennt ein Feld der Ressource, dessen Wert die Zelle füllt (ein gleichnamiges Feld in `tags` wird zuerst gelesen);
- die erste Spalte zeigt den `name` der Ressource, wenn sie kein `display` hat;
- `width` legt die Breite in Pixeln fest (Standard `rowHeaderColumnDefaultWidth`, 80);
- `sort` macht den Titel klickbar: Ein Klick sortiert die Zeilen nach diesem Feld, erst aufsteigend, dann absteigend, und setzt `aria-sort` am Titel.

Eine Ressource kann auch eigene `columns: [{ text }, { html }]` mitbringen. Diese Einträge werden nach Position zugeordnet, einer pro Spalte.

```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}
    />
  )
}
```
Um eine Zelle zu gestalten, verwenden Sie `onBeforeRowHeaderRender`. Mit Spalten wird das eigene `html` der Zeile ignoriert: Schreiben Sie stattdessen `args.row.columns[i].html`. Dieses Markup ist vertrauenswürdiges HTML; escapen Sie Nutzertext also mit `SuperScheduler.Util.escapeHtml`. Für React-Inhalte im Zeilenkopf siehe [React-Render-Slots](https://superscheduler.org/de/docs/react-render-slots/).

### Spaltenbreite ändern und Spalten umordnen
Die Breite von Spalten lässt sich standardmäßig ändern (`rowHeaderColumnsResizable: true`); `nonresizable: true` schaltet das für eine einzelne Spalte ab. Nach einer Größenänderung schreibt das Control die neue Breite in dieses Spaltenobjekt Ihres `rowHeaderColumns`-Arrays und ruft `onRowHeaderColumnResized({ column })` auf.

Das Umordnen ist standardmäßig aus. Mit `rowHeaderColumnsReorderable` ziehen Nutzer die Spaltentitel; die Ränder zur Größenänderung haben dabei Vorrang vor dem Ziehen. In `keyboardMode: 'Full'` verschieben Alt + Umschalt + Links und Alt + Umschalt + Rechts den fokussierten Titel und sagen die Änderung an. Aus Code verschiebt `control.moveRowHeaderColumn(from, to)` eine Spalte anhand ihres Index in der aktuellen Reihenfolge.

Jede Größenänderung und jedes Umordnen ruft `onRowHeaderColumnsChange` auf mit:

- `columns`: den Spalten in ihrer neuen Reihenfolge, mit ihren Breiten;
- `order`: `order[i]` ist der ursprüngliche Index (in der übergebenen Prop) der Spalte, die jetzt an Position `i` steht;
- `widths` und `reason` (`'resize'` oder `'reorder'`).

Halten Sie die Spalten wie im Snippet im React-State und übernehmen Sie `args.columns`. Andernfalls würde eine Elternkomponente, die mit einem neuen Array-Literal neu rendert, dem Control die ursprüngliche Reihenfolge zurückgeben.

> **Tip:**
> Füllen Sie umordenbare Spalten über `display`. Ein Wert, der aus einem Feld gelesen wird, folgt seiner Spalte; positionsbezogene Einträge in `resource.columns[i]` bleiben an ihrer Position. Um Breiten und Reihenfolge über Sitzungen hinweg zu behalten, speichern Sie sie mit [gespeicherten Ansichten](https://superscheduler.org/de/docs/panes-saved-views/).

## Zeilen und Ereignisse filtern
Filter bestehen aus einem Parameter und einem Callback. `control.rows.filter(param)` speichert den Parameter und ruft `onRowFilter` für jede Zeile auf; setzen Sie `args.visible = false`, um sie auszublenden. `control.events.filter(param)` und `onEventFilter` funktionieren für Ereignisse genauso. Ein Parameter, der falsy ist (`''`, `null`, `0`), hebt den Filter auf.

```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}
      />
    </>
  )
}
```
Die Eingabe „ben“ lässt „North region“ und „Ben Ortiz“ übrig: `rowFilterParentsAlwaysVisible` behält die Vorfahren jeder passenden Zeile. Die Wahl eines Status blendet die anderen Ereignisse aus, ohne die Zeilen anzutasten.

> **Behavior:**
> Das Control wertet den aktiven Filter erneut aus, wenn sich die Funktion `onRowFilter` oder `onEventFilter` ändert. Definieren Sie sie auf Modulebene oder memoisieren Sie sie. Die Bereichsauswahl mit Umschalt + Klick überspringt ausgeblendete Zeilen und herausgefilterte Ereignisse.

Filter laufen im Browser über die Daten, die das Control hält. Eine Suche über Daten, die nicht geladen sind (ein anderer Monat, ein anderer Standort), ist eine Abfrage an Ihr Backend, gefolgt von neuen `resources` oder `events`.

## Ereignisse auswählen
Standardmäßig führt ein Klick auf ein Ereignis nur `onEventClick` aus. Für die Auswahl braucht es `eventClickHandling: 'Select'`:

- Ein Klick schaltet die Auswahl des Ereignisses um und hebt die übrigen auf.
- Strg + Klick (Cmd + Klick unter macOS) schaltet es um und behält die übrigen, solange `allowMultiSelect` `true` ist (der Standard).
- Mit `eventMultiSelectRange` wählt Umschalt + Klick die Ereignisse zwischen dem zuletzt angeklickten und diesem aus, über die sichtbaren Zeilen hinweg; Strg/Cmd + Umschalt fügt sie hinzu.

`onEventSelect` läuft vor der Änderung und kann sie mit `args.preventDefault()` abbrechen; `onEventSelected` läuft danach.

Die API `control.multiselect` liest und ändert die Auswahl: `get()`, `add(e)`, `remove(e)`, `clear()`, `isSelected(e)` und `selectAll({ scope, filter })`. `selectAll` wählt standardmäßig die sichtbaren Ereignisse aus; `scope: 'all'` durchsucht jedes geladene Ereignis, mit Kosten proportional zur Größe des Speichers.

```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}
      />
    </>
  )
}
```
Der Zähler im Button sollte Klicks, Rechtecken und den Aktionen der Werkzeugleiste folgen, und ausgewählte Aufträge sollten durch dieses CSS umrandet sein:

```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:**
> In 0.1.0 erhält ein ausgewähltes Ereignis keine eigene visuelle Markierung: Der Auswahlstil des Stylesheets zielt auf ein Attribut `data-selected`, das Ereignisse nicht erhalten. Ergänzen Sie wie im Snippet eine Klasse in `onBeforeEventRender`; der Hook läuft für jedes Ereignis, dessen Auswahl sich ändert, erneut. Mit dem optionalen Präfix `theme` erhalten ausgewählte Ereignisse außerdem eine Klasse `{theme}_selected`.

> **Behavior:**
> Klicks werden über `onEventSelected` gemeldet. Rechteckauswahlen und Aufrufe von `multiselect` (`selectAll`, `clear`, `add`, `remove`) veröffentlichen das Thema `selection` von `super-scheduler/hooks`. Das Snippet hört auf beides, damit der Zähler immer aktuell ist.

Mit `allowMultiMove` verschiebt das Ziehen eines ausgewählten Ereignisses die ganze Auswahl; jedes Ereignis durchläuft dieselben Verschieberegeln (siehe [Ziehen, Dauer ändern und Geschäftsregeln](https://superscheduler.org/de/docs/drag-resize-rules/)). Im vollständigen Tastaturmodus wählt Strg/Cmd + A jedes sichtbare Ereignis aus.

### Rechteckauswahl
`rectangleSelectHandling` bestimmt, was ein aufgezogenes Rechteck bewirkt:

- `'EventSelect'` wählt die Ereignisse darin aus (und führt die Callbacks aus);
- `'Enabled'` führt nur die Callbacks aus, für Anwendungen, die selbst auf das Rechteck reagieren;
- `'Disabled'` (Standard) schaltet es ab.

Nutzer ziehen ein Rechteck auf, indem sie bei gedrückter Umschalttaste über das Raster ziehen. Um ohne Umschalttaste zu beginnen, setzen Sie `args.action = 'RectangleSelect'` in `onGridMouseDown` oder rufen vor dem nächsten Drücken `control.multiselect.startRectangle()` auf. `rectangleSelectMode: 'Row'` hält das Rechteck in der Zeile, in der das Drücken begann. Mit `rectangleSelectModifiers` fügt Umschalt zur aktuellen Auswahl hinzu und Alt entfernt daraus; ohne eine dieser Tasten ersetzt das Rechteck die Auswahl.

Die Callbacks sind `onRectangleSelecting` (jeder Frame; `args.visible = false` blendet das Rechteck aus), `onRectangleSelect` (abbrechbar, mit `events`, `start`, `end` und `resources`) und `onRectangleSelected`.

## Zeilen auswählen
`rowClickHandling: 'Select'` sorgt dafür, dass ein Klick auf den Zeilenkopf die Zeile auswählt. Strg/Cmd schaltet eine Zeile um, und Umschalt wählt alle Zeilen ab der zuletzt angeklickten aus. Ausgewählte Zeilen tragen `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` bietet `get()`, `add(row)`, `remove(row)`, `isSelected(row)` und `clear()`; diese Aufrufe heben die Zeilen sofort hervor und lösen keinen Callback aus, weshalb das Snippet nach `clear()` seinen eigenen State zurücksetzt. `onRowSelect` (abbrechbar) und `onRowSelected` laufen vor bzw. nach jeder Auswahl per Klick.

Die Prop `selectedRows` enthält die IDs der ausgewählten Ressourcen. Übergeben Sie sie, um mit einer Auswahl zu starten; das Control aktualisiert genau dieses Array, wenn sich die Auswahl ändert.

## Zeiträume auswählen
Ziehen über leere Zellen wählt einen Zeitraum aus und ruft beim Loslassen einmal `onTimeRangeSelected` auf, mit `start`, `end` (exklusiv), `resource` und `origin: 'drag'`. Weitere Ursprünge:

- `'click'`: ein einfacher Klick auf eine leere Zelle wählt diese Zelle aus;
- `'keyboard'`: bei aktivierter Tastatur die Eingabetaste auf einer fokussierten Zelle oder Umschalt + Links/Rechts, um einen Zeitraum zu erweitern;
- `'api'`: `control.selectTimeRange(start, end, resource)`.

Prüfen Sie `args.origin`, wenn nur ein Ziehen etwas anlegen soll. Die Auswahl bleibt nach dem Loslassen gezeichnet, bis zur nächsten Auswahl oder bis `control.clearSelection()`. Auch ein Klick an anderer Stelle hebt sie auf, außer mit `timeRangeSelectedHandling: 'HoldForever'`; `'Disabled'` schaltet die Zeitraumauswahl ab.

`onTimeRangeSelecting` läuft bei jeder Änderung während des Ziehens. Setzen Sie `args.allowed = false`, um einen Zeitraum abzulehnen (der Schatten wird markiert, und das Loslassen wählt nichts aus), oder schreiben Sie `args.start` und `args.end` um, um ihn zu begrenzen. Gesperrte Zellen und Elternknoten unter `treePreventParentUsage` lehnen Auswahlen ohne jeden Code ab.

### Mehrere Zeiträume gleichzeitig
Mit `allowMultiRange` kann ein neues Ziehen einen Zeitraum hinzufügen, statt den aktuellen zu ersetzen: solange Strg/Cmd gedrückt ist (`multiRangeMode: 'CtrlOrMeta'`, der Standard) oder immer (`'Always'`). `args.multirange` listet jeden ausgewählten Zeitraum auf, und `control.multirange` liest oder ändert sie: `get()`, `clear()` und `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}
      />
    </>
  )
}
```
Ziehen Sie über freie Tage eines Zimmers und dann bei gedrückter Strg-Taste (Cmd unter macOS) über die eines anderen: Der Button zeigt „Block 2 selected ranges“. Die Zeiträume erreichen `onBlock` als ISO-Werte in lokaler Uhrzeit mit exklusivem Ende, bereit zum Senden an Ihr Backend.

## Was Ihrer Anwendung gehört
Die Bibliothek hält Bäume, Filter und Auswahlen im Browser, für die aktuelle Sitzung. Ihre Anwendung entscheidet:

- woher der Baum kommt: Zeilen sind die `resources`, die Sie übergeben, und das Control fragt nie selbst nach Kindern (`onLoadNode` ist reserviert);
- was ein Filter bedeutet, wenn Daten nicht geladen sind, und wie die Suche das Backend erreicht;
- was eine Auswahl bewirkt: Massenaktionen, die Berechtigung, sie auszuführen, und ihre Speicherung;
- ob aufgeklappte Zeilen und Spaltenlayouts ein Neuladen überstehen (siehe [gespeicherte Ansichten](https://superscheduler.org/de/docs/panes-saved-views/)).

Auswahlen werden nirgends gespeichert. Der Aufklappzustand lebt in den übergebenen Ressourcenobjekten, bis Sie sie ersetzen.

## Siehe auch
→ https://superscheduler.org/de/examples/hotel-rooms/
→ https://superscheduler.org/de/examples/training-rooms/
→ https://superscheduler.org/de/examples/field-service-dispatch/
- [Tastatur, Barrierefreiheit und Touch](https://superscheduler.org/de/docs/keyboard-accessibility-touch/) listet die Tasten für die Auswahl auf.
- [Kontrollierte Ereignisse und Callbacks](https://superscheduler.org/de/docs/controlled-state/) erklärt, wie ausgewählte Ereignisse Ihren State erreichen.
- [Koordinierte Bereiche und gespeicherte Ansichten](https://superscheduler.org/de/docs/panes-saved-views/) speichert eingeklappte Zeilen und Spaltenbreiten.
