Zum Inhalt springen
SuperScheduler

Pro-ModuleGilt fürSuperScheduler Pro

Rückgängig, Wiederholen und Verlauf

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.

Geprüft mit v0.1.0 · überarbeitet am 7. Oktober 2026.md

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.

src/UndoablePlanner.tsxtsx
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

OptionStandardWirkung
limit50Anzahl 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
equalsStart, Ende, Ressource, TextZwei Ereigniszustände, die als gleich gelten, erzeugen keinen Eintrag
fieldskeineZusätzliche Felder für den Standardvergleich
apply'control'Wer Rückgängig und Wiederholen anwendet: das Control oder Ihre Funktion
labelsEnglisch oder SpanischBeschriftung 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.

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.

src/ControlledUndo.tsxtsx
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.

Ä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.

src/BulkEditing.tsxtsx
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.

src/OptimisticPlanner.tsxtsx
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

Planung einer VideoproduktionEin Dreh dauert länger. Verschieben Sie den abhängigen Schnitt, verstehen Sie warum, und nehmen Sie es zurück. Ressourcenplanung für eine KreativagenturEine Designerin ist doppelt gebucht. Übergeben Sie zwei Aufgaben mit einem Ziehen, prüfen Sie, was sie freigeben, und nehmen Sie es zurück.