InteraktionGilt fürSuperScheduler Pro
Ziehen, Dauer ändern und Geschäftsregeln
Steuern Sie jeden Frame eines Ziehvorgangs in onEventMoving und onEventResizing: Setzen Sie args.allowed = false und args.message, um eine Position mit Begründung abzulehnen. Verhindern Sie Überlappungen mit allowEventOverlap={false} (oder pro Frame mit args.allowOverlap), sperren Sie Zeiten mit gesperrten Zellen, fixieren Sie einzelne Ereignisse mit moveDisabled und resizeDisabled, und treffen Sie die endgültige Entscheidung in onEventMove oder onEventResize, bei Bedarf asynchron mit args.async = true und args.loaded().
Beim Ziehen beweist eine Planungstafel ihren Wert, und hier stecken die meisten Geschäftsregeln: Dieser Auftrag braucht eine Hebebühne, jener Aufenthalt darf nicht in die Vergangenheit verschoben werden, der OP-Saal ist über Mittag geschlossen. SuperScheduler Pro fragt Ihren Code zu zwei Zeitpunkten. Während der Nutzer zieht, bei jeder Änderung des Schattens, können Sie die Position akzeptieren, ablehnen oder anpassen und den Grund nennen. Beim Ablegen, einmalig, können Sie die Änderung abbrechen, abwandeln oder bestätigen, auch nach einer Rückfrage bei Ihrem Server.
Dieser Leitfaden baut diese Regeln an einer Werkstattplanung mit Arbeitsplätzen auf und behandelt danach Überlappungen, gesperrte Zeiten, Sperren, asynchrone Bestätigung, die Drag-Karte und das Anlegen von Ereignissen durch Auswahl eines Zeitraums. Alles hier erfordert Pro; Lite ist schreibgeschützt.
Wie über einen Ziehvorgang entschieden wird
- Der Nutzer greift ein Ereignis. Gesperrte Ereignisse (
moveDisabled) starten keinen Ziehvorgang. - Bei jeder Zeigerbewegung, die Zielzeit oder Zielzeile ändert, läuft
onEventMoving(onEventResizingbeim Ändern der Dauer). Ihre Regel setztargs.allowed, darfargs.startundargs.endanpassen und setztargs.message. - Danach wendet die Bibliothek ihre eigenen Prüfungen an: Überlappung mit anderen Ereignissen, wenn
allowEventOverlapfalseist, und gesperrte Zellen. Ein abgelehnter Schatten wird als verboten gezeichnet, und die Drag-Karte zeigt den Grund. - Beim Loslassen über einer abgelehnten Position passiert nichts: Das Ereignis kehrt zurück, und kein weiterer Callback läuft.
- Beim Loslassen über einer akzeptierten Position läuft
onEventMove(onEventResize) einmal, bevor sich der Speicher ändert. Es kann das Übernehmen abbrechen, abwandeln oder aufschieben. - Der Speicher wird aktualisiert,
onEventMoved(onEventResized) läuft, undonEventsChangefolgt im nächsten Microtask, wie in Kontrollierte Ereignisse und Callbacks beschrieben.
Dieselbe Abfolge beim Übernehmen gilt für Verschiebungen und Dauer-Änderungen per Tastatur mit keyboardMode: 'Full'.
Beim Ziehen prüfen
onEventMoving erhält die vorgeschlagene Position und schreibt die Entscheidung in seine Argumente zurück:
| Beschreibbar | Wirkung |
|---|---|
allowed | false zeichnet den Schatten als verboten; Ablegen dort bewirkt nichts |
message | Text, den die Drag-Karte zeigt, solange allowed false ist |
start, end | Passen den Schatten an, zum Beispiel um die ursprünglichen Zeiten zu behalten, wenn sich nur die Zeile ändert |
allowOverlap | Überschreibt allowEventOverlap nur für diesen Frame |
cssClass, html | Klasse und Inhalt des Schattens |
Außerdem liest es den Kontext: args.e (das gezogene Ereignis, Ihre Daten in args.e.data), args.resource und args.row (die Zielzeile), args.duration, args.conflicts, args.external (von außerhalb des Planers gezogen) und die Modifikatortasten. onEventResizing arbeitet genauso mit start, end, allowed, message und allowOverlap und ergänzt args.what, den gezogenen Rand ('start' oder 'end').
import { useMemo } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps } from 'super-scheduler'
const BAYS: SuperScheduler.ResourceData[] = [
{ id: 'bay-1', name: 'Bay 1 (lift)' },
{ id: 'bay-2', name: 'Bay 2 (lift)' },
{ id: 'bay-3', name: 'Bay 3' },
{ id: 'waiting', name: 'Waiting list' },
]
const BAYS_WITH_LIFT: ReadonlySet<SuperScheduler.ResourceId> = new Set(['bay-1', 'bay-2'])
const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
{ groupBy: 'Day', format: 'dddd d MMMM' },
{ groupBy: 'Hour', format: 'HH:mm' },
]
/** A custom field of the job data (see "Custom fields" in the data model guide). */
function needsLift(data: SuperScheduler.EventData): boolean {
return 'needsLift' in data && data.needsLift === true
}
interface Props {
readonly jobs: SuperScheduler.EventData[]
readonly onEventsChange: (args: SchedulerEventsChangeArgs) => void
}
export function WorkshopPlanning({ jobs, onEventsChange }: Props) {
const config = useMemo<SchedulerProps>(
() => ({
startDate: '2026-10-12',
days: 5,
scale: 'CellDuration',
cellDuration: 30,
cellWidth: 48,
timeHeaders: TIME_HEADERS,
businessBeginsHour: 8,
businessEndsHour: 18,
showNonBusiness: false,
useEventBoxes: 'Never',
allowEventOverlap: false,
conflictHighlight: true,
// Runs on every shadow change: keep it synchronous and cheap.
onEventMoving: (args) => {
if (args.start.getTime() < SuperScheduler.Date.now().getTime()) {
args.allowed = false
args.message = 'Jobs cannot be moved into the past.'
return
}
if (needsLift(args.e.data) && !BAYS_WITH_LIFT.has(args.resource)) {
args.allowed = false
args.message = 'This job needs a bay with a lift.'
return
}
// The waiting list may hold overlapping jobs; the bays may not (allowEventOverlap above).
args.allowOverlap = args.resource === 'waiting'
},
onEventResizing: (args) => {
const minutes = (args.end.getTime() - args.start.getTime()) / 60_000
if (minutes < 30) {
args.allowed = false
args.message = 'A job takes at least 30 minutes.'
}
},
}),
[],
)
const owned = useMemo(() => jobs.slice(), [jobs])
return (
<SuperSchedulerComponent
{...config}
resources={BAYS}
events={owned}
onEventsChange={onEventsChange}
/>
)
}Sie sollten eine Fünf-Tage-Planung in Halbstundenzellen von 08:00 bis 18:00 Uhr sehen. Ziehen Sie einen Auftrag, der eine Hebebühne braucht, auf Bay 3: Der Schatten wird als verboten markiert, und die Karte zeigt „This job needs a bay with a lift.“. Ziehen Sie einen beliebigen Auftrag auf einem Arbeitsplatz über einen anderen: Das wird als Überlappung abgelehnt. Legen Sie ihn auf der Warteliste ab: Dort stapeln sich beide Aufträge. Kürzen Sie einen Auftrag auf unter 30 Minuten: Die Dauer-Änderung wird abgelehnt.
Überlappungen verhindern
allowEventOverlap={false} lehnt jede Verschiebung, Dauer-Änderung oder Zeitraumauswahl ab, die sich mit einem anderen Ereignis in derselben Zeile überlappen würde. Intervalle sind halboffen; direkt aufeinanderfolgende Ereignisse (eines endet um 11:00 Uhr, das nächste beginnt um 11:00 Uhr) gelten also nie als Überlappung.
Zwei Werkzeuge verfeinern die Regel je nach Situation:
args.allowOverlapinonEventMovingundonEventResizingüberschreibt die Option für den aktuellen Frame, wie es die Warteliste oben tut. Der Wert wird bei jedem Aufruf zurückgesetzt.args.conflictslistet die bestehenden Ereignisse, mit denen der Schatten in der Zielzeile kollidiert (bis zu acht), alsSuperScheduler.Event-Wrapper. Damit unterscheiden Sie weiche von harten Konflikten:
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
function isTentative(data: SuperScheduler.EventData): boolean {
return 'status' in data && data.status === 'tentative'
}
/** Tentative jobs may be double-booked; confirmed ones may not. */
export const softConflicts: Pick<
SchedulerProps,
'allowEventOverlap' | 'conflictHighlight' | 'onEventMoving'
> = {
allowEventOverlap: false,
// Outlines the events the shadow collides with (data-conflict) while dragging.
conflictHighlight: true,
onEventMoving: (args) => {
// Up to eight colliding events in the target row: feedback, not exhaustive validation.
const hard = args.conflicts.find((event) => !isTentative(event.data))
if (hard === undefined) {
// For this frame only; the instance option stays false.
args.allowOverlap = true
return
}
args.allowed = false
args.message = `Overlaps ${hard.text()}, which is confirmed.`
},
}conflictHighlight umrandet die kollidierenden Ereignisse, während der Nutzer zieht (sie erhalten ein Attribut data-conflict und eine Umrandung in der Warnfarbe). So sieht der Nutzer, was im Weg ist, und nicht nur, dass etwas im Weg ist.
Zeiten mit gesperrten Zellen blockieren
Eine gesperrte Zelle wird schraffiert gezeichnet und lehnt Verschiebungen, Dauer-Änderungen und Zeitraumauswahlen ab, die sie berühren. Zellen lassen sich auf zwei Arten sperren:
- Eine ganze Zeile:
cellsDisabled: truean der Ressource, für einen Arbeitsplatz in Reparatur oder ein Zimmer außer Betrieb. - Eine beliebige Zelle: Setzen Sie
args.cell.properties.disabled = trueinonBeforeCellRender, für Mittagspausen, Feiertage oder Öffnungszeiten pro Ressource.
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerBeforeCellRenderArgs, SuperScheduler } from 'super-scheduler'
const BAYS: SuperScheduler.ResourceData[] = [
{ id: 'bay-1', name: 'Bay 1' },
{ id: 'bay-2', name: 'Bay 2' },
// Closed for refurbishment: every cell of the row is disabled.
{ id: 'bay-3', name: 'Bay 3', cellsDisabled: true },
]
/**
* Module level, so its identity never changes: a new function per render would invalidate the
* per-cell cache. Disabled cells are hatched and reject drops, resizes and range selection.
*/
function closeLunchBreak(args: SchedulerBeforeCellRenderArgs): void {
if (args.cell.start.getHours() === 13) {
args.cell.properties.disabled = true
args.cell.properties.cssClass = 'lunch-break'
}
}
export function ClosedTime({ jobs }: { jobs: SuperScheduler.EventData[] }) {
const owned = useMemo(() => jobs.slice(), [jobs])
return (
<SuperSchedulerComponent
startDate="2026-10-12"
days={5}
scale="CellDuration"
cellDuration={30}
resources={BAYS}
events={owned}
onBeforeCellRender={closeLunchBreak}
/>
)
}Sie sollten Bay 3 über die ganze Zeile schraffiert sehen und jeden Arbeitsplatz von 13:00 bis 14:00 Uhr. Aufträge lassen sich weder in die Mittagspause ablegen noch über sie hinweg verlängern.
Weitere Möglichkeiten, geschlossene Zeiten auszudrücken: Blenden Sie sie mit showNonBusiness={false} oder onIncludeTimeCell aus der Achse aus (siehe Stunden, Minuten, Tage und Zoom), oder stellen Sie sie als Ereignisse dar, die sich weder verschieben noch in der Dauer ändern lassen (moveDisabled, resizeDisabled), etwa einen Wartungsblock; solche Ereignisse zählen ebenfalls als Überlappung, wenn allowEventOverlap false ist.
Einzelne Ereignisse sperren
| Feld des Ereignisses | Wirkung |
|---|---|
moveDisabled | Das Ereignis lässt sich nicht verschieben |
resizeDisabled | Die Dauer des Ereignisses lässt sich nicht ändern |
moveHDisabled | Es kann die Zeile wechseln, aber nicht die Zeit |
moveVDisabled | Es kann die Zeit wechseln, aber nicht die Zeile |
clickDisabled, deleteDisabled | Es ignoriert Klicks bzw. hat keinen Lösch-Button |
Für den ganzen Planer schalten eventMoveHandling="Disabled" und eventResizeHandling="Disabled" die Gesten ab. Regeln, die davon abhängen, wer hinsieht (die Rezeption darf verschieben, ein Gast nicht), sind Berechtigungen: Berechnen Sie diese Felder aus der Rolle des Nutzers, bevor Sie die Ereignisse übergeben.
Beim Ablegen bestätigen, auch asynchron
onEventMove und onEventResize laufen einmal pro Ablegen, bevor sich etwas ändert. Sie können:
- Abbrechen mit
args.preventDefault(). - Das Ergebnis abwandeln, indem Sie
args.newStart,args.newEndoderargs.newResourcezuweisen. - Aufschieben mit
args.async = trueund dannargs.loaded()aufrufen, sobald Sie eine Antwort haben. Ein Aufruf vonargs.preventDefault()vorloaded()bricht das Ablegen ab.
import type {
SchedulerEventMoveArgs,
SchedulerEventResizeArgs,
SuperScheduler,
} from 'super-scheduler'
function inProgress(data: SuperScheduler.EventData): boolean {
return 'status' in data && data.status === 'inProgress'
}
/**
* onEventMove: called once on drop, before the store changes. The library never awaits a
* handler, so an asynchronous decision defers the drop with `async` and finishes it with `loaded()`.
*/
export function confirmMove(args: SchedulerEventMoveArgs): void {
// A synchronous veto needs no async: cancel and return. This final check also covers moves
// made with the keyboard.
if (inProgress(args.e.data) && args.newResource !== args.e.resource()) {
args.preventDefault()
args.control.message('A job in progress stays in its bay.')
return
}
args.async = true
const resource = String(args.newResource)
void (async () => {
try {
const question = `Move ${args.e.text()} to ${resource}, ${args.newStart.toString('ddd d MMM HH:mm')}?`
if (!(await confirmWithUser(question))) {
args.preventDefault()
return
}
await saveBooking({
id: String(args.e.id()),
resource,
start: args.newStart.value,
end: args.newEnd.value,
})
} catch {
args.preventDefault()
args.control.message('The move could not be saved.')
} finally {
// Always: completes the drop, or cancels it when preventDefault() was called first.
args.loaded()
}
})()
}
/** The same protocol for resizing; `what` tells which edge moved. */
export function confirmResize(args: SchedulerEventResizeArgs): void {
args.async = true
void saveBooking({
id: String(args.e.id()),
resource: String(args.e.resource()),
start: args.newStart.value,
end: args.newEnd.value,
})
.catch(() => args.preventDefault())
.finally(() => args.loaded())
}Binden Sie sie als onEventMove={confirmMove} und onEventResize={confirmResize} ein. Solange die Entscheidung aussteht, bleibt das Ereignis an seiner ursprünglichen Position; es bewegt sich, wenn loaded() das Ablegen abschließt, oder bleibt, wo es ist, wenn das Ablegen abgebrochen wurde.
Die Drag-Karte
Während des Ziehens zeigt eine Karte neben dem Zeiger die Zieldaten, die Dauer (Nächte bei ganztägigen Zeiträumen, sonst Stunden und Minuten), die Zielzeile und den Grund, warum eine Position abgelehnt wird: Ihre args.message oder das Ereignis, mit dem sie sich überlappt. Zusätzlich erscheint eine Datumsmarke im Zeitkopf (headerMarker). Beides ist standardmäßig aktiv.
Konfigurieren Sie die Karte mit einem einzigen stabilen Objekt:
import { SuperScheduler } from 'super-scheduler'
// One stable object at module level: a new object per render would reconfigure the card.
export const DRAG_CARD: SuperScheduler.DragCardOptions = {
// Intraday work: show the time of both edges.
dateFormat: 'ddd d MMM HH:mm',
movingDateFormat: 'ddd d MMM HH:mm',
// Whole-day ranges count nights by default; other ranges show hours and minutes.
duration: 'auto',
row: true,
labels: { overlapping: 'Overlaps', forbidden: 'Not allowed' },
}
// Replace the content when a refusal needs more room. The string is trusted HTML.
export const DRAG_CARD_WITH_REASON: SuperScheduler.DragCardOptions = {
...DRAG_CARD,
html: (info) => {
if (info.refusal === null) return null // null keeps the default content
const reason = SuperScheduler.Util.escapeHtml(info.refusal)
const row = SuperScheduler.Util.escapeHtml(info.rowName ?? '')
return `<strong>${reason}</strong><br>${row}`
},
}Übergeben Sie dragCard={DRAG_CARD} oder dragCard={false}, um sie zu entfernen. Die Standardbeschriftungen sind ins Englische, Spanische, Katalanische, Baskische, Galicische, Deutsche, Französische, Italienische und Portugiesische übersetzt und folgen der locale des Planers; labels überschreibt sie. Die Option html erhält die Daten, den Zeilennamen, den ersten Konflikt, die Ablehnungsmeldung und die Position des Ereignisses vor dem Ziehen.
Ereignisse durch Auswahl von Zeit anlegen
Ziehen über leere Zellen wählt einen Zeitraum aus; onTimeRangeSelected meldet ihn mit start, end (exklusiv), resource und origin. Ob dort ein Ereignis entsteht, entscheidet Ihre Anwendung:
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type {
SchedulerEventsChangeArgs,
SchedulerTimeRangeSelectedArgs,
SchedulerTimeRangeSelectingArgs,
SuperScheduler,
} from 'super-scheduler'
const BAYS: SuperScheduler.ResourceData[] = [
{ id: 'bay-1', name: 'Bay 1' },
{ id: 'bay-2', name: 'Bay 2' },
]
/** Steers the selection while it is drawn, as onEventMoving does for moves: four hours at most. */
function limitToFourHours(args: SchedulerTimeRangeSelectingArgs): void {
args.allowed = args.end.getTime() - args.start.getTime() <= 4 * 3_600_000
}
export function CreateOnSelect() {
const [jobs, setJobs] = useState<SuperScheduler.EventData[]>([])
const owned = useMemo(() => jobs.slice(), [jobs])
const onEventsChange = useCallback(
(args: SchedulerEventsChangeArgs) => setJobs([...args.events]),
[],
)
const onTimeRangeSelected = useCallback((args: SchedulerTimeRangeSelectedArgs) => {
// The selection shadow stays until cleared.
args.control.clearSelection()
// A plain click on an empty cell also selects it (origin 'click'): create only on a drag.
if (args.origin !== 'drag') return
setJobs((current) => [
...current,
{
id: crypto.randomUUID(),
resource: args.resource,
// Store strings: start and end arrive as SuperScheduler.Date (end exclusive).
start: args.start.value,
end: args.end.value,
text: 'New job',
},
])
}, [])
return (
<SuperSchedulerComponent
startDate="2026-10-12"
days={5}
scale="CellDuration"
cellDuration={30}
resources={BAYS}
events={owned}
allowEventOverlap={false}
onTimeRangeSelecting={limitToFourHours}
onTimeRangeSelected={onTimeRangeSelected}
onEventsChange={onEventsChange}
/>
)
}Sie sollten sehen, wie ein Auswahlschatten dem Zeiger folgt, nicht über vier Stunden hinaus oder über einen anderen Auftrag wächst und beim Loslassen zu einem Ereignis „New job“ wird.
Drei Verhaltensweisen sollten Sie kennen:
- Ein einfacher Klick ist ebenfalls eine Auswahl. Ein Klick auf eine leere Zelle löst
onTimeRangeSelectedmitorigin: 'click'und einer Zelle aus. Prüfen Sieorigin === 'drag', wenn ein Klick nichts anlegen darf. - Die Auswahl bleibt nach dem Loslassen sichtbar, bis zur nächsten Auswahl oder bis
args.control.clearSelection(). - Auswahlen folgen denselben Regeln wie Verschiebungen: Sie können weder gesperrte Zellen überqueren noch belegte Zeit, wenn
allowEventOverlapfalseist.onTimeRangeSelectingsteuert sie Frame für Frame mitargs.allowed.
Was in Ihrer Anwendung bleibt
Die Bibliothek setzt durch, was Sie konfigurieren, und meldet, was passiert. Ihre Anwendung verantwortet die Regeln selbst (welche Ressource welche Arbeit annimmt, wer was ändern darf), die serverseitige Validierung jeder Änderung und jede automatische Platzierung oder Optimierung. Das Verschieben einer Buchung plant nie die anderen um: Braucht Ihr Geschäft kaskadierende Änderungen, berechnen Sie sie und aktualisieren die Ereignisse selbst.
Termine in der PhysiotherapiepraxisEin Patient kann um 10 Uhr nicht. Finden Sie den nächsten Slot, der Pausen und Reinigung respektiert. Planung von FertigungsaufträgenDie Wartung wurde vorgezogen. Verschieben Sie den Auftrag und halten Sie die Arbeitsfolge ein. Platzbuchung im SportclubAm Vormittag reißt ein Padel-Netz. Kurs verschieben, Platz sperren und jeden Coach in seinem Dienst lassen.
Nächste Schritte
- Die Änderungen speichern, die diese Regeln akzeptieren: Kontrollierte Ereignisse und Callbacks.
- Nutzern erlauben, eine Verschiebung rückgängig zu machen: Rückgängig machen und Wiederholen.
- Dieselben Regeln per Tastatur nutzen: Tastatur, Barrierefreiheit und Touch.
Verwandte Beispiele
- FormaTermine in der PhysiotherapiepraxisEin Patient kann um 10 Uhr nicht. Finden Sie den nächsten Slot, der Pausen und Reinigung respektiert.
- ForgePlanung von FertigungsaufträgenDie Wartung wurde vorgezogen. Verschieben Sie den Auftrag und halten Sie die Arbeitsfolge ein.
- Court ClubPlatzbuchung im SportclubAm Vormittag reißt ein Padel-Netz. Kurs verschieben, Platz sperren und jeden Coach in seinem Dienst lassen.
- FieldworkDisposition im technischen AußendienstEin dringender Auftrag kommt herein. Finden Sie das Team, das ihn rechtzeitig übernehmen kann.