# Rückgängig, Wiederholen und Verlauf

> Gesten mit super-scheduler/history aufzeichnen, Rückgängig auf kontrollierten Zustand anwenden, Änderungen bündeln, eigene Befehle anlegen, Abgelehntes zurückrollen.

Source: https://superscheduler.org/de/docs/undo-redo/
Reviewed: 2026-10-07

Erzeugen Sie mit createHistory() einen einzigen Verlauf und übergeben Sie ihn als history-Prop: Verschieben und Dauer ändern werden aufgezeichnet, und Ctrl/Cmd+Z, Ctrl/Cmd+Umschalt+Z sowie Ctrl+Y funktionieren, solange der Fokus im Planer liegt. Bei kontrollierten Ereignissen übernehmen Sie die Änderungen mit dem Grund 'history' aus onEventsChange oder geben createHistory eine apply-Funktion, die Ihren Zustand aktualisiert. Nutzen Sie push für eigene Befehle, batch zum Bündeln von Änderungen, revert, um eine vom Server abgelehnte Änderung rückgängig zu machen, und subscribe oder onHistoryChange für die Schaltflächen Rückgängig und Wiederholen.

Direkte Manipulation lädt zu Fehlern ein: ein Ereignis, das eine Zeile zu tief abgelegt wurde, eine Dauer, die einen Tag zu weit gezogen wurde. `super-scheduler/history` führt einen Rückgängig-Stapel für Änderungen an Ereignissen und für Ihre eigenen Befehle, mit Tastenkürzeln, Beschriftungen für Schaltflächen, Bündelung und einem Rollback für Änderungen, die Ihr Server ablehnt. Der Verlauf lebt für die Dauer der Sitzung im Speicher; etwas dauerhaft zu speichern ist Aufgabe Ihrer Anwendung.

Der Verlauf erfordert SuperScheduler Pro.

## Einen Verlauf anbinden
`createHistory()` gibt ein Verlaufsobjekt zurück. Übergeben Sie es dem Planer als `history` (oder innerhalb von `extensions`). Erzeugen Sie es nur einmal: Props werden über ihre Identität verglichen, und ein während des Renderns erzeugter Verlauf wäre bei jedem Rendern ein neuer, leerer.

```tsx
// src/UndoablePlanner.tsx
import { useEffect, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerHistoryChangeArgs, SuperScheduler } from 'super-scheduler'
import { createHistory } from 'super-scheduler/history'
import type { SchedulerHistory } from 'super-scheduler/history'
import 'super-scheduler/styles.css'

type HistoryState = Pick<
  SchedulerHistoryChangeArgs,
  'canUndo' | 'canRedo' | 'undoLabel' | 'redoLabel'
>

/** Mirrors the history in React: one render per change, never per gesture frame. */
export function useHistoryState(history: SchedulerHistory): HistoryState {
  const [state, setState] = useState<HistoryState>(() => ({
    canUndo: history.canUndo,
    canRedo: history.canRedo,
    undoLabel: history.undoLabel,
    redoLabel: history.redoLabel,
  }))
  useEffect(() => history.subscribe(setState), [history])
  return state
}

export function UndoablePlanner(props: {
  resources: SuperScheduler.ResourceData[]
  initialEvents: SuperScheduler.EventData[]
}) {
  // Created once: `history` is compared by identity, like every prop.
  const [history] = useState(() =>
    createHistory({
      limit: 100,
      // Default: moves and resizes. Mod+Z, Mod+Shift+Z and Ctrl+Y work while focus is in the grid.
      record: ['move', 'resize'],
      keys: 'root',
      labels: { move: 'move', resize: 'resize' },
    }),
  )
  const [initial] = useState(() => props.initialEvents.slice())
  const state = useHistoryState(history)

  return (
    <>
      <div role="toolbar" aria-label="History">
        <button type="button" disabled={!state.canUndo} onClick={() => history.undo()}>
          {state.undoLabel === null ? 'Undo' : `Undo ${state.undoLabel}`}
        </button>
        <button type="button" disabled={!state.canRedo} onClick={() => history.redo()}>
          {state.redoLabel === null ? 'Redo' : `Redo ${state.redoLabel}`}
        </button>
      </div>
      <SuperSchedulerComponent
        history={history}
        // Uncontrolled: the control owns the events after mount.
        defaultEvents={initial}
        resources={props.resources}
        startDate="2026-10-01"
        days={31}
        scale="Day"
      />
    </>
  )
}
```
Ziehen Sie ein Ereignis, und die erste Schaltfläche lautet „Undo move“. Klicken Sie darauf oder drücken Sie Cmd+Z (Ctrl+Z unter Windows und Linux), nachdem Sie ein Ereignis angeklickt haben, und das Ereignis springt zurück; „Redo move“ stellt die Verschiebung wieder her.

Ein Verlauf kann mehreren Planern dienen, zum Beispiel den Bereichen einer geteilten Ansicht. Rückgängig folgt dann der Reihenfolge der Änderungen über alle Planer hinweg.

## Optionen
| Option | Standard | Wirkung |
|---|---|---|
| `limit` | `50` | Anzahl der behaltenen Einträge; darüber hinaus fällt der älteste weg. `0` zeichnet nichts auf. |
| `record` | `['move', 'resize']` | Automatisch aufgezeichnete Änderungen: `'move'`, `'resize'`, `'create'`, `'remove'`, `'update'` |
| `keys` | `'root'` | Wo Tastenkürzel greifen: im Planer, im ganzen `'document'` oder mit `false` nirgends |
| `equals` | Start, Ende, Ressource, Text | Zwei Ereigniszustände, die als gleich gelten, erzeugen keinen Eintrag |
| `fields` | keine | Zusätzliche Felder für den Standardvergleich |
| `apply` | `'control'` | Wer Rückgängig und Wiederholen anwendet: das Control oder Ihre Funktion |
| `labels` | Englisch oder Spanisch | Beschriftung pro Art: `move`, `resize`, `create`, `remove`, `update`, `command` |

Was aufgezeichnet wird:

- **Gesten.** `'move'` und `'resize'` decken Verschieben und Dauer ändern per Zeiger und per Tastatur ab. Ein Eintrag entsteht, nachdem die Änderung bestätigt wurde: Eine Verschiebung, die Ihre Regeln ablehnen oder die während einer asynchronen Bestätigung abgebrochen wird, hinterlässt keinen Eintrag.
- **API-Änderungen.** `'create'`, `'remove'` und `'update'` zeichnen Aufrufe von `control.events.add()`, `remove()` und `update()` auf.
- **Nie.** Ladevorgänge: ein neues `events`-Array aus React und Ereignisse, die beim bereichsweisen Laden eintreffen. Rückgängig und Wiederholen selbst werden nicht erneut aufgezeichnet.

Die Tastenkürzel sind Cmd+Z und Cmd+Umschalt+Z unter macOS, sonst Ctrl+Z, Ctrl+Umschalt+Z und Ctrl+Y. Sie werden ignoriert in Eingabefeldern, Textbereichen und bearbeitbaren Elementen, bei gedrückter Alt-Taste und wenn ein anderer Handler die Taste bereits verarbeitet hat. Bei `keys: 'root'` muss der Fokus im Planer liegen: Ein Klick auf ein Ereignis oder das Ziehen eines Ereignisses setzt ihn dorthin, ebenso die Tab-Taste, wenn die Tastatur aktiviert ist. `'document'` wirkt überall auf der Seite; verwenden Sie es daher nur, wenn kein anderer Teil der Seite ein eigenes Rückgängig hat.

> **Behavior:**
> Der Verlauf vergleicht das gespeicherte Ereignisobjekt vor und nach einer Änderung. Die Setter des Wrappers (`e.start(value)`, `e.end(value)`, `e.text(value)`) ändern das gespeicherte Objekt direkt, sodass ein anschließendes `control.events.update(e)` nichts Wiederherstellbares hinterlässt und keinen Eintrag erzeugt. Übergeben Sie stattdessen ein neues Objekt: `control.events.update({ ...e.data, start, end })`.

## Den Rückgängig-Zustand anzeigen
Das Verlaufsobjekt stellt `canUndo`, `canRedo`, `undoLabel` und `redoLabel` bereit sowie `undo()` und `redo()`, die `false` zurückgeben, wenn es nichts zu tun gibt. Es gibt drei Wege, Änderungen zu verfolgen:

- `history.subscribe(listener)` gibt eine Funktion zum Abmelden zurück und passt damit natürlich in einen `useEffect` (das `useHistoryState` im Snippet);
- die Prop `onHistoryChange` erhält denselben Zustand, mit `this` auf das Control gesetzt;
- `useScheduler({ track: ['history'] })` aus `super-scheduler/hooks` stellt ihn als React-State bereit.

Jede Benachrichtigung enthält eine `cause`: `'record'`, `'undo'`, `'redo'`, `'clear'` oder `'revert'`. Die Beschriftungen sind Ihr `label` oder der Standard für die jeweilige Art, auf Englisch oder auf Spanisch, wenn die `locale` des Planers mit `es` beginnt. Für andere Sprachen übergeben Sie `labels`.

## Kontrollierte Ereignisse
Wenn React-State die Ereignisse verwaltet (`events` plus `onEventsChange`), gibt es zwei Wege, Rückgängig anzuwenden.

Mit dem Standard `apply: 'control'` ändert Rückgängig die Ereignisse des Controls über `control.events.*`, und das Ergebnis erreicht `onEventsChange` mit `reason: 'history'`. Wenn Sie dort ohnehin jede Änderung übernehmen, funktioniert Rückgängig ohne weiteren Code.

Mit einer `apply`-Funktion übergibt Ihnen der Verlauf die Operationen, und Sie aktualisieren Ihren Zustand; die neue `events`-Prop erreicht dann das Control. Das passt zu Stores, Reducern und Apps, die jede Änderung über einen einzigen Codepfad speichern.

```tsx
// src/ControlledUndo.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs } from 'super-scheduler'
import { createHistory } from 'super-scheduler/history'
import type { HistoryOp } from 'super-scheduler/history'

type EventData = SuperScheduler.EventData

/** Applies history operations to React state. They arrive in order (reversed for undo). */
function applyOps(
  events: EventData[],
  ops: readonly HistoryOp[],
  direction: 'undo' | 'redo',
): EventData[] {
  let next = events
  for (const op of ops) {
    const target = direction === 'undo' ? op.before : op.after
    const id = (target ?? op.before ?? op.after)?.id
    if (id === undefined) continue
    const index = next.findIndex((event) => event.id === id)
    if (target === null) next = next.filter((event) => event.id !== id)
    else if (index >= 0) next = next.map((event, i) => (i === index ? target : event))
    else next = [...next, target]
  }
  return next
}

export function ControlledUndo(props: {
  resources: SuperScheduler.ResourceData[]
  initial: EventData[]
}) {
  const [events, setEvents] = useState(props.initial)
  // The control adopts the array it receives and mutates it: give it a copy.
  const owned = useMemo(() => events.slice(), [events])

  const [history] = useState(() =>
    createHistory({
      record: ['move', 'resize'],
      // Undo and redo update React state; the new `events` prop then reaches the control.
      apply: (ops, direction) => setEvents((current) => applyOps(current, ops, direction)),
    }),
  )

  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => setEvents([...args.events]),
    [],
  )

  const onTimeRangeSelected = useCallback(
    (args: SuperScheduler.SchedulerTimeRangeSelectedArgs) => {
      if (args.origin !== 'drag') return
      args.control.clearSelection()
      const booking: EventData = {
        id: SuperScheduler.guid(),
        resource: args.resource,
        start: args.start,
        end: args.end,
        text: 'New booking',
      }
      setEvents((current) => [...current, booking])
      // A change made through state is a data load for the control, not a gesture:
      // record it explicitly so it can be undone.
      history.record({
        kind: 'create',
        label: 'new booking',
        ops: [{ before: null, after: booking }],
      })
    },
    [history],
  )

  return (
    <SuperSchedulerComponent
      history={history}
      events={owned}
      onEventsChange={onEventsChange}
      onTimeRangeSelected={onTimeRangeSelected}
      resources={props.resources}
      startDate="2026-10-01"
      days={31}
      scale="Day"
    />
  )
}
```
Jede Operation hat die Form `{ before, after }`. `before: null` bedeutet, dass das Ereignis erzeugt wurde, `after: null`, dass es entfernt wurde. Die Operationen kommen in der Reihenfolge an, in der sie anzuwenden sind, für Rückgängig bereits umgekehrt.

> **Tip:**
> Eine Änderung, die Ihre Anwendung durch Setzen von State vornimmt, ist für das Control ein Ladevorgang und keine Geste; nichts zeichnet sie auf. Zeichnen Sie sie selbst mit `history.record({ kind, label, ops })` auf, wie es das Snippet beim Anlegen einer Buchung tut. Wenn ein Verlauf mehreren Planern dient, übergeben Sie zusätzlich `control`.

## Änderungen bündeln und Befehle ergänzen
`history.batch(label, run)` fasst alles, was während der Ausführung von `run` aufgezeichnet wird, zu einem einzigen Eintrag zusammen. Sammelaktionen lassen sich dann in einem Schritt rückgängig machen. Bündel lassen sich verschachteln; die äußere Beschriftung gewinnt.

`history.push({ label, undo, redo })` fügt einen eigenen Befehl hinzu, für Änderungen außerhalb der Ereignisse des Planers: einen gesperrten Tag, eine Ressourceneinstellung, eine Abhängigkeit zwischen Ereignissen. Rückgängig und Wiederholen rufen Ihre Funktionen auf.

```tsx
// src/BulkEditing.tsx
import { useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { createHistory } from 'super-scheduler/history'

export function BulkEditing(props: {
  resources: SuperScheduler.ResourceData[]
  initial: SuperScheduler.EventData[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const [initial] = useState(() => props.initial.slice())
  const [frozenDays, setFrozenDays] = useState<ReadonlySet<string>>(new Set())
  // 'update' also records changes made through control.events.update().
  const [history] = useState(() => createHistory({ record: ['move', 'resize', 'update'] }))

  // One undo step for the whole operation.
  const shiftSelected = (days: number) => {
    if (control === null) return
    const selected = control.multiselect.get()
    history.batch(`shift ${selected.length} jobs`, () => {
      for (const e of selected) {
        // A new object: the stored one stays intact as the state undo restores.
        // (The wrapper setters e.start(...) edit the stored object in place.)
        control.events.update({
          ...e.data,
          start: e.start().addDays(days),
          end: e.end().addDays(days),
        })
      }
    })
  }

  // A change outside the scheduler's events, undone through the same history.
  const freezeDay = (day: string) => {
    const add = () => setFrozenDays((current) => new Set(current).add(day))
    const remove = () =>
      setFrozenDays((current) => new Set([...current].filter((item) => item !== day)))
    add()
    history.push({ label: `freeze ${day}`, undo: remove, redo: add })
  }

  return (
    <>
      <button type="button" onClick={() => shiftSelected(1)}>
        Move selection one day later
      </button>
      <button type="button" onClick={() => freezeDay('2026-10-12')}>
        Freeze 12 October
      </button>
      <p>Frozen days: {[...frozenDays].join(', ') || 'none'}</p>
      <SuperSchedulerComponent
        controlRef={controlRef}
        history={history}
        defaultEvents={initial}
        eventClickHandling="Select"
        resources={props.resources}
        startDate="2026-10-01"
        days={31}
        scale="Day"
      />
    </>
  )
}
```
Wählen Sie zwei Ereignisse aus (Klick, dann Cmd+Klick), drücken Sie „Move selection one day later“ und dann „Freeze 12 October“. Das erste Rückgängig hebt die Sperre des Tages auf; das zweite verschiebt beide Ereignisse auf einmal zurück.

## Vom Server bestätigte Abläufe
Die Bibliothek ruft nie Ihr Backend auf. Zwei Muster decken die meisten Anwendungen ab.

**Optimistisch.** Lassen Sie die Verschiebung geschehen und aufzeichnen, speichern Sie sie in `onEventMoved` und rollen Sie zurück, wenn der Server ablehnt. `history.revert(eventId)` stellt das Ereignis auf den Zustand vor seinem letzten Eintrag zurück und verwirft diesen Eintrag, sodass ein späteres Rückgängig die abgelehnte Änderung nicht wiederbelebt.

```tsx
// src/OptimisticPlanner.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { createHistory } from 'super-scheduler/history'

export function OptimisticPlanner(props: {
  resources: SuperScheduler.ResourceData[]
  initial: SuperScheduler.EventData[]
}) {
  const [events, setEvents] = useState(props.initial)
  const owned = useMemo(() => events.slice(), [events])
  const [history] = useState(() => createHistory())

  const config = useMemo<SchedulerProps>(
    () => ({
      history,
      onEventsChange: ({ events: next }) => setEvents([...next]),
      // The move is already applied and recorded: save it, and roll back if the server refuses.
      onEventMoved: (args) => {
        const id = args.e.id()
        saveBooking({
          id: String(id),
          resource: String(args.newResource),
          start: args.newStart.value,
          end: args.newEnd.value,
        }).catch(() => {
          // Restores the event's previous state and drops that history entry.
          history.revert(id)
          args.control.message('The move could not be saved and was undone.')
        })
      },
    }),
    [history],
  )

  const undo = useCallback(() => history.undo(), [history])

  return (
    <>
      <button type="button" onClick={undo}>
        Undo
      </button>
      <SuperSchedulerComponent
        {...config}
        events={owned}
        resources={props.resources}
        startDate="2026-10-01"
        days={31}
        scale="Day"
      />
    </>
  )
}
```
Wenn das Speichern fehlschlägt, springt das Ereignis zurück, und die Meldungsleiste erklärt den Grund.

**Erst bestätigen.** Fragen Sie den Server (oder den Nutzer), bevor die Änderung angewendet wird: Setzen Sie `args.async = true` in `onEventMove` und rufen Sie dann `args.loaded()` auf, um anzunehmen, oder `args.preventDefault()` und `args.loaded()`, um abzulehnen. Der Verlauf zeichnet die Verschiebung erst auf, wenn sie angenommen ist. Für Abläufe, in denen die bestätigte Änderung von der Geste abweicht (ein Dialog, der das Ergebnis bearbeitet), setzen Sie `record: []` und fügen nach der Bestätigung durch den Server mit `push` einen Befehl hinzu.

Rückgängig und Wiederholen sind ebenfalls Änderungen: Speichern Sie sie auf dieselbe Weise, entweder in Ihrer `apply`-Funktion oder wenn `onEventsChange` `reason: 'history'` meldet. Zu erkennen, dass jemand anderes das Ereignis zwischenzeitlich geändert hat (Versionen, Konflikte), ist Aufgabe Ihres Backends.

## Was der Verlauf nicht leistet
> **Limitation:**
> Der Verlauf liegt im Speicher, pro Seite: Er übersteht kein Neuladen und wird weder zwischen Tabs noch zwischen Nutzern geteilt. Er zeichnet nur Ereignisse auf; Ressourcen, Verknüpfungen, Auswahl, Zoom und Scrollposition werden nicht aufgezeichnet, es sei denn, Sie fügen eigene Befehle hinzu (zu Zoom und Scrollposition siehe [gespeicherte Ansichten](https://superscheduler.org/de/docs/panes-saved-views/)). Rufen Sie `history.clear()` auf, wenn Sie einen anderen Datenbestand laden, damit Rückgängig keine alten Zustände auf neue Daten anwenden kann.

## Verwandte Themen
→ https://superscheduler.org/de/examples/video-production/
→ https://superscheduler.org/de/examples/agency-campaigns/
- [Kontrollierte Ereignisse und Callbacks](https://superscheduler.org/de/docs/controlled-state/) zu `onEventsChange` und seinen Gründen.
- [Ziehen, Dauer ändern und Geschäftsregeln](https://superscheduler.org/de/docs/drag-resize-rules/) zur asynchronen Bestätigung.
