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.
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.
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
| Feld | Standard | Wirkung |
|---|---|---|
id | erforderlich | Identifiziert den Bereich in Handlern (args.pane), in panesRef und im DOM (data-pane) |
resources | Die Zeilen dieses Bereichs | |
rowFilter | Wä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 |
minSize | 48 | Kleinste Höhe in Pixeln; reicht die Gesamthöhe nicht, haben die Mindestwerte Vorrang |
hidden | false | Blendet den Bereich aus, lässt ihn aber gemountet, sodass erneutes Einblenden nichts kostet |
props | Props 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
| Prop | Standard | Wirkung |
|---|---|---|
height | erforderlich | Gesamthö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' |
splitter | true | { 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:
eventsplusonEventsChange. Der Handler erhält die vollständige, zusammengeführte Liste inargs.eventssowieargs.panefü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, undcontrol(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.
Selbst platzierte Planer koppeln
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:
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 include | Gespeicherte Felder | Hinweise |
|---|---|---|
'zoom' | cellWidth, zoomLevel | zoomLevel ist der Index der aktiven Stufe in zoomLevels; halten Sie deren Reihenfolge daher stabil |
'scroll' | anchorDate, topRowId, topOffset | Das Datum am linken Rand und die oberste Zeile, per ID, mit dem Versatz innerhalb dieser Zeile |
'density' | density | Nur wenn Sie die Prop density setzen |
'collapsed' | collapsed | IDs der zugeklappten Elternelemente im Baum |
'columns' | columnWidths, columnOrder | Breiten und Reihenfolge der Spalten im Zeilenkopf |
Jeder Zustand hat v: 1. Ohne include werden alle fünf Schlüssel erfasst.
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 })
}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 vontimeout(standardmäßig 5.000 ms), wird das Promise mitfalseaufgelöst.when: 'now'wendet sofort an; existiert die gespeicherte oberste Zeile nicht mehr, wird der gespeicherte Versatz als absolute Scrollposition verwendet.animate: trueanimiert die Zoomänderung.- In
collapsedaufgefü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 mitfalseaufgelöst. Ein Zustand mit einer anderen Version als 1 wird ebenfalls mitfalseaufgelö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.
localStoragefü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
vvor 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
topRowIdundcollapsedfunktionieren. - Bereichsgrößen und Auswahl. Beides ist nicht Teil einer Ansicht; speichern Sie Bereichsgrößen aus
onPaneResize, wenn Sie sie wiederhaben möchten.
Verwandte Themen
- Ressourcenbäume, Spalten und Auswahl zu den zugeklappten Zeilen und Spalten, die eine Ansicht speichert.
- Zeitskalen und Zoom zu den Zoomstufen, die eine Ansicht wiederherstellt.