Zum Inhalt springen
SuperScheduler

Pro-ModuleGilt fürSuperScheduler Pro

Gekoppelte Bereiche und gespeicherte Ansichten

Ersetzen Sie SuperSchedulerComponent durch SchedulerPanes aus super-scheduler/panes und beschreiben Sie jeden Bereich mit einer id plus resources oder einem rowFilter; die Bereiche teilen horizontales Scrollen, Zoom und die Breite des Zeilenkopfs, scrollen vertikal unabhängig voneinander, und Ereignisse lassen sich zwischen ihnen ziehen. Für gespeicherte Ansichten gibt getViewState(control) ein JSON-taugliches Objekt mit Zoom, Scrollposition, Dichte, zugeklappten Zeilen und Spalten zurück, und applyViewState stellt es wieder her; wo es gespeichert wird, entscheidet Ihre Anwendung.

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

Zwei Anforderungen tauchen in jedem großen Planungsbildschirm auf. Die erste: einen Teil der Zeilen im Blick behalten, während der Rest scrollt, etwa eine Ablage „nicht zugewiesen“ unter den Zimmern oder ein Team über seinen Maschinen. Die zweite: später zur selben Ansicht zurückkehren, also zum Zoom, zum Datum und zu den Zeilen, die der Nutzer gerade angesehen hat. super-scheduler/panes und super-scheduler/views decken beides ab, und beide erfordern SuperScheduler Pro.

Eine Zeitleiste in Bereiche teilen

SchedulerPanes rendert mehrere Planer übereinander auf einer gemeinsamen Zeitleiste. Sie teilen die horizontale Scrollposition, den Zoom und die Breite des Zeilenkopfs; jeder Bereich scrollt vertikal für sich und hat seine eigene Höhe. Trennleisten zwischen den Bereichen ändern deren Größe.

Die Komponente ersetzt SuperSchedulerComponent: Sie übergeben dieselben Planer-Props einmal, dazu ein panes-Array und eine Gesamthöhe height.

src/RoomsWithTray.tsxtsx
import { useCallback, useMemo, useRef, useState } from 'react'
import type { SchedulerEventsChangeArgs, SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SchedulerPanes } from 'super-scheduler/panes'
import type { SchedulerPane, SchedulerPanesHandle } from 'super-scheduler/panes'
import 'super-scheduler/styles.css'

const isTray = (resource: SuperScheduler.ResourceData) => resource.kind === 'tray'

// Module-level (or memoized): a new `panes` array resets the sizes the user dragged.
const PANES: SchedulerPane[] = [
  { id: 'rooms', rowFilter: (resource) => !isTray(resource), minSize: 200 },
  {
    id: 'tray',
    rowFilter: isTray,
    size: 140,
    minSize: 96,
    // Per-pane overrides: smaller events in the unassigned tray.
    props: { eventHeight: 28 },
  },
]

export function RoomsWithTray(props: {
  resources: SuperScheduler.ResourceData[]
  initial: SuperScheduler.EventData[]
}) {
  const [events, setEvents] = useState(props.initial)
  const panesRef = useRef<SchedulerPanesHandle>(null)

  // One list for every pane: each event appears in the pane that holds its resource.
  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => setEvents([...args.events]),
    [],
  )

  const shared = useMemo<Partial<SchedulerProps>>(
    () => ({
      onEventMove: (args) => {
        // `pane` is where the event lands; `sourcePane` is set only for a move between panes.
        if (args.sourcePane !== 'rooms' || args.pane !== 'tray') return
        // Unassigning a booking asks first; the drop waits for the answer.
        args.async = true
        void confirmWithUser(`Unassign ${args.e.text()}?`).then((ok) => {
          if (!ok) args.preventDefault()
          args.loaded()
        })
      },
    }),
    [],
  )

  return (
    <>
      <button type="button" onClick={() => panesRef.current?.scrollTo('2026-10-01', 'left')}>
        Go to 1 October
      </button>
      <SchedulerPanes
        {...shared}
        panesRef={panesRef}
        panes={PANES}
        // Total height of every pane, the splitter and the shared header.
        height={640}
        resources={props.resources}
        events={events}
        onEventsChange={onEventsChange}
        splitter={{ size: 6, step: 8 }}
        startDate="2026-10-01"
        days={60}
        scale="Day"
        cellWidth={44}
      />
    </>
  )
}

Sie sollten oben die Zimmer sehen und darunter eine 140 px hohe Ablage „nicht zugewiesen“, mit einem einzigen Zeitkopf ganz oben. Scrollen Sie einen der Bereiche seitwärts, und der andere folgt. Ziehen Sie eine Buchung aus der Ablage in ein Zimmer, und sie wandert dorthin; ziehen Sie eine aus einem Zimmer in die Ablage, und die Anwendung fragt vorher nach.

Optionen der Bereiche

FeldStandardWirkung
iderforderlichIdentifiziert den Bereich in Handlern (args.pane), in panesRef und im DOM (data-pane)
resourcesDie Zeilen dieses Bereichs
rowFilterWählt die Zeilen dieses Bereichs aus den gemeinsamen resources; verwenden Sie entweder dies oder resources
size'auto'Pixel, ein Prozentsatz der freien Höhe ('30%') oder 'auto' für einen Anteil am Rest
minSize48Kleinste Höhe in Pixeln; reicht die Gesamthöhe nicht, haben die Mindestwerte Vorrang
hiddenfalseBlendet den Bereich aus, lässt ihn aber gemountet, sodass erneutes Einblenden nichts kostet
propsProps nur für diesen Bereich; Handler hier ersetzen die gemeinsamen

Zeilen werden nach Ressourcen der obersten Ebene zugeordnet: Eine Elternressource nimmt ihre Kinder in ihren Bereich mit. Der erste Bereich ohne resources und ohne rowFilter erhält jede Ressource der obersten Ebene, die kein anderer Bereich übernommen hat.

Layout und Trennleiste

PropStandardWirkung
heighterforderlichGesamthöhe in Pixeln: alle Bereiche, die Trennleisten und der gemeinsame Kopf
timeHeader'first''first' zeigt den Zeitkopf nur im ersten sichtbaren Bereich; 'all' in jedem Bereich
scrollbar'last'Horizontale Bildlaufleiste nur im letzten Bereich, oder 'all'
splittertrue{ size, step } legt ihre Dicke (6 px) und den Tastaturschritt (8 px) fest; false entfernt sie
onPaneResize{ sizes } nach einer übernommenen Größenänderung, nach Bereichs-ID

Die Trennleiste ist fokussierbar und hat role="separator"; ihr Wert ist die Höhe des Bereichs darunter. Pfeil oben und Pfeil unten verschieben sie um step, Umschalt+Pfeil oben und Umschalt+Pfeil unten um 40 px, Pos1 und Ende führen zu den Grenzen, und Eingabe oder ein Doppelklick stellt die deklarierten Größen wieder her. Während des Ziehens wird eine Vorschau der Bereiche gezeigt; ihre Höhen ändern sich beim Loslassen. In 0.1.0 ist ihr barrierefreier Name das englische „Pane size“, ohne Option zur Übersetzung.

Ereignisse in Bereichen

Übergeben Sie alle Ereignisse einmal. Jeder Bereich zeigt die Ereignisse, deren resource zu seinen Zeilen gehört, und ein Ereignis wechselt in einen anderen Bereich, wenn seine Ressource das tut.

  • Kontrolliert: events plus onEventsChange. Der Handler erhält die vollständige, zusammengeführte Liste in args.events sowie args.pane für den Bereich, in dem die Änderung stattfand. Übernehmen Sie sie wie unter kontrollierter Zustand beschrieben.
  • Unkontrolliert: defaultEvents; die Bereiche verwalten die Liste dann selbst.

Wenn Sie control.events.list eines Bereichs direkt ändern, wird das nicht mit den anderen Bereichen geteilt; gehen Sie über den State oder die API control.events.

Verschieben zwischen Bereichen

Ziehen zwischen Bereichen ist standardmäßig aktiv (crossPaneMove: true); false hält jedes Ereignis in seinem Bereich. Mit dem Standard eventMoveHandling: 'Update' wird eine Verschiebung zwischen Bereichen einmal gemeldet, als Änderung 'move' in onEventsChange.

Jeder gemeinsame Handler erhält args.pane. Bei einer Verschiebung zwischen Bereichen erhalten onEventMove und onEventMoved zusätzlich args.sourcePane, sodass eine Regel von der Richtung abhängen kann: Das Snippet verlangt nur bei Verschiebungen von den Zimmern in die Ablage eine Bestätigung, mit args.async und args.loaded(). Eine abgebrochene oder abgelehnte Verschiebung lässt die Daten unverändert.

Auf das Control jedes Bereichs zugreifen

SchedulerPanes erzeugt die Planer und gibt Ihnen deren Controls über panesRef:

  • controls: eine Map von Bereichs-ID zu Control, und control(id) für einen einzelnen Bereich;
  • forEach(run), um etwas in jedem Bereich aufzurufen;
  • scrollTo(date, position), um alle gemeinsam zu scrollen;
  • update(options), um Optionen auf jeden Bereich anzuwenden.

Um die React-Render-Slots in Bereichen zu nutzen, übergeben Sie die Komponente dieses Einstiegspunkts: component={SuperSchedulerComponent}, importiert aus super-scheduler/react-render. Das Bereichsmodul importiert sie nur, wenn Sie das tun.

Wenn die Planer nicht übereinander liegen (ein Personalplan oben auf der Seite und ein Raumplan weiter unten), behalten Sie Ihre eigenen Komponenten und koppeln deren Controls mit linkPanes:

src/LinkedBoards.tsxtsx
import { useEffect, useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { linkPanes } from 'super-scheduler/panes'

// Two schedulers placed by your own layout (here, a page section apart) that move together.
export function LinkedBoards(props: {
  staff: SuperScheduler.ResourceData[]
  rooms: SuperScheduler.ResourceData[]
  shifts: SuperScheduler.EventData[]
  bookings: SuperScheduler.EventData[]
}) {
  const staff = useSchedulerControl()
  const rooms = useSchedulerControl()
  const shifts = useMemo(() => props.shifts.slice(), [props.shifts])
  const bookings = useMemo(() => props.bookings.slice(), [props.bookings])

  useEffect(() => {
    if (staff.control === null || rooms.control === null) return
    // Horizontal scroll always; zoom and row header width too unless turned off.
    const link = linkPanes([staff.control, rooms.control], { zoom: true, rowHeaderWidth: true })
    return () => link.dispose()
  }, [staff.control, rooms.control])

  return (
    <>
      <h2>Staff</h2>
      <SuperSchedulerComponent
        controlRef={staff.controlRef}
        startDate="2026-10-01"
        days={30}
        scale="Day"
        resources={props.staff}
        events={shifts}
      />
      <h2>Rooms</h2>
      <SuperSchedulerComponent
        controlRef={rooms.controlRef}
        startDate="2026-10-01"
        days={30}
        scale="Day"
        resources={props.rooms}
        events={bookings}
      />
    </>
  )
}

Horizontales Scrollen wird immer geteilt. Zoom und Breite des Zeilenkopfs werden geteilt, sofern Sie nicht zoom: false oder rowHeaderWidth: false übergeben. Rufen Sie dispose() auf, um die Kopplung aufzuheben.

Eine Ansicht speichern und wiederherstellen

Eine Ansicht beschreibt, wie der Nutzer auf die Daten blickt, nicht die Daten selbst. getViewState(control, include?) erfasst sie als kleines, JSON-taugliches Objekt; applyViewState(control, state, options?) stellt sie wieder her.

Schlüssel in includeGespeicherte FelderHinweise
'zoom'cellWidth, zoomLevelzoomLevel ist der Index der aktiven Stufe in zoomLevels; halten Sie deren Reihenfolge daher stabil
'scroll'anchorDate, topRowId, topOffsetDas Datum am linken Rand und die oberste Zeile, per ID, mit dem Versatz innerhalb dieser Zeile
'density'densityNur wenn Sie die Prop density setzen
'collapsed'collapsedIDs der zugeklappten Elternelemente im Baum
'columns'columnWidths, columnOrderBreiten und Reihenfolge der Spalten im Zeilenkopf

Jeder Zustand hat v: 1. Ohne include werden alle fünf Schlüssel erfasst.

src/savedView.tsts
import type { SuperScheduler } from 'super-scheduler'
import { applyViewState, getViewState } from 'super-scheduler/views'
import type { SchedulerViewState, ViewStateKey } from 'super-scheduler/views'

// What this application restores from the view. Density and columns stay in React state here.
const KEYS: readonly ViewStateKey[] = ['zoom', 'scroll', 'collapsed']

const storageKey = (user: string, view: string) => `planning-view:${user}:${view}`

/** Saves the current view. The application owns storage: here localStorage, per user. */
export function saveView(control: SuperScheduler.Scheduler, user: string, view: string): void {
  const state = getViewState(control, KEYS)
  try {
    localStorage.setItem(storageKey(user, view), JSON.stringify(state))
  } catch {
    // Storage can be full or disabled; a view is a convenience, not data.
  }
}

/** Stored values are untrusted input: check the shape before using them. */
function isViewState(value: unknown): value is SchedulerViewState {
  return typeof value === 'object' && value !== null && (value as { v?: unknown }).v === 1
}

/** Restores a saved view. Resolves false when nothing was saved or the rows never appeared. */
export async function restoreView(
  control: SuperScheduler.Scheduler,
  user: string,
  view: string,
): Promise<boolean> {
  let saved: unknown = null
  try {
    saved = JSON.parse(localStorage.getItem(storageKey(user, view)) ?? 'null')
  } catch {
    return false
  }
  if (!isViewState(saved)) return false
  // Waits (up to 5 s) for the rows and the saved top row, for data that loads after mount.
  return applyViewState(control, saved, { when: 'rows', timeout: 5000 })
}
src/PlannerWithViews.tsxtsx
import { useEffect, useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import { restoreView, saveView } from './saved-view'

const ZOOM_LEVELS: SuperScheduler.ZoomLevel[] = [
  { id: 'weeks', properties: { scale: 'Week', cellWidth: 120 } },
  { id: 'days', properties: { scale: 'Day', cellWidth: 44 } },
]

export function PlannerWithViews(props: {
  user: string
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const { controlRef, control } = useSchedulerControl()
  const events = useMemo(() => props.events.slice(), [props.events])

  // Restore once the control exists; keep row ids stable so the top row can be found again.
  useEffect(() => {
    if (control !== null) void restoreView(control, props.user, 'default')
  }, [control, props.user])

  return (
    <>
      <button
        type="button"
        disabled={control === null}
        onClick={() => control && saveView(control, props.user, 'default')}
      >
        Save this view
      </button>
      <SuperSchedulerComponent
        controlRef={controlRef}
        startDate="2026-01-01"
        days={365}
        zoomLevels={ZOOM_LEVELS}
        zoom="days"
        treeEnabled
        resources={props.resources}
        events={events}
      />
    </>
  )
}

Scrollen Sie zu einem Datum, klappen Sie eine Etage zu, drücken Sie „Save this view“ und laden Sie die Seite neu: Der Planer kehrt zum selben Datum und zur selben Zeile zurück, mit zugeklappter Etage.

So funktioniert das Wiederherstellen:

  • when: 'rows' (Standard) wartet, bis die Zeilen und die gespeicherte oberste Zeile existieren; das deckt Daten ab, die erst nach dem Mount eintreffen. Erscheinen sie nicht innerhalb von timeout (standardmäßig 5.000 ms), wird das Promise mit false aufgelöst.
  • when: 'now' wendet sofort an; existiert die gespeicherte oberste Zeile nicht mehr, wird der gespeicherte Versatz als absolute Scrollposition verwendet.
  • animate: true animiert die Zoomänderung.
  • In collapsed aufgeführte Elternelemente werden zugeklappt, alle anderen Elternelemente aufgeklappt.
  • Passen die gespeicherten Spalten nicht mehr zu rowHeaderColumns (andere Spaltenzahl), wird nichts angewendet, und das Promise wird mit false aufgelöst. Ein Zustand mit einer anderen Version als 1 wird ebenfalls mit false aufgelöst.
  • Das Wiederherstellen verschiebt den Tastaturfokus nicht.

Mit Bereichen speichern und wiederherstellen Sie über das Control eines einzelnen Bereichs (panesRef.current?.control('rooms')): Zoom und horizontales Scrollen werden geteilt, während vertikales Scrollen und zugeklappte Zeilen zu diesem Bereich gehören.

Was Ihre Anwendung verantwortet

  • Speicherung. localStorage für einen Browser oder Ihr Backend, damit die Ansicht dem Nutzer auf alle Geräte folgt. Die Bibliothek speichert nie etwas.
  • Benennen und Teilen. Benannte Ansichten, Standards pro Team, Links, die eine Ansicht öffnen.
  • Validierung. Gespeicherte Ansichten sind nicht vertrauenswürdige Eingaben: Prüfen Sie Struktur und v vor dem Anwenden und verwerfen Sie Ansichten, die die Prüfung nicht bestehen.
  • Stabile IDs. Zeilen-IDs müssen von einer Sitzung zur nächsten dieselben Zeilen bezeichnen, damit topRowId und collapsed funktionieren.
  • Bereichsgrößen und Auswahl. Beides ist nicht Teil einer Ansicht; speichern Sie Bereichsgrößen aus onPaneResize, wenn Sie sie wiederhaben möchten.

Planung von SchulungsräumenDie Anmeldungen sprengen den Raum. Beide Termine wählen, sehen, was für beide frei ist, zusammen verschieben und die Ansicht behalten.