Zum Inhalt springen
SuperScheduler

AnpassungGilt fürSuperScheduler Pro

React-Render-Slots und Hover-Karten

Importieren Sie SuperSchedulerComponent aus super-scheduler/react-render statt aus dem Paket-Root und übergeben Sie dann renderEvent, renderRowHeader, renderTimeHeader, renderCorner, renderCell oder renderArea; jede dieser Funktionen liefert den React-Inhalt einer Art von Slot. Der HTML- oder Text-Fallback wird zuerst gezeichnet, und React ersetzt ihn in kurzen Batches, wenn der Browser untätig ist; das Scrollen wartet also nie auf React. Ergänzen Sie eventHover für Hover-Karten, die Nutzer anheften können.

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

SuperScheduler zeichnet sein Raster mit eigenem DOM-Code; genau das hält das Scrollen bei Tausenden von Zeilen und Ereignissen flüssig. Wenn der Inhalt eines Ereignisses oder eines Kopfs aus Ihren React-Komponenten kommen soll (Ihr Designsystem, Icons, Avatare, formatierte Werte), bindet der Einstiegspunkt super-scheduler/react-render React-Inhalte in die Slots des Planers ein, ohne React die Kontrolle über das Raster zu geben.

React-Render-Slots und Hover-Karten erfordern SuperScheduler Pro.

Zur React-Render-Komponente wechseln

super-scheduler/react-render exportiert ein eigenes SuperSchedulerComponent. Es akzeptiert jede Prop der Hauptkomponente, stellt dieselben ref.current.control und controlRef bereit und ergänzt die render*-Props, eventHover, renderOptions sowie die Handler onBefore*DomAdd / onBefore*DomRemove.

Die Komponente aus dem Paket-Root akzeptiert diese Props ebenfalls, warnt aber nur einmal (needs the component from "super-scheduler/react-render") und rendert daraus nichts. Weil die React-Mechanik in einem eigenen Einstiegspunkt liegt, laden Seiten, die sie nicht verwenden, sie auch nicht.

Die Slots

PropArgumenteErsetzt
renderEventcontrol, e, data, row, width, lodDen Inhalt eines Ereigniskastens
renderRowHeadercontrol, row, columnDen Inhalt einer Zelle im Zeilenkopf (column ist der Spaltenindex, 0 ohne Spalten)
renderTimeHeadercontrol, header (start, end, level)Den Inhalt einer Zelle im Zeitkopf
renderCornercontrolDie linke obere Ecke
renderCellcontrol, cellDen Inhalt einer Rasterzelle
renderAreacontrol, area, sourceEine Area, die mit render: true deklariert ist

Die Engine behält die Teile, die ihr gehören: den Ereigniskasten und seine Position, den Dauerbalken, die Griffe zum Ändern der Dauer, gewöhnliche Areas, den Umschalter des Baums und die Rasterlinien. Ein Slot ist der Inhalt darin.

Inhalte von Ereignissen rendern

src/CampaignBoard.tsxtsx
import { memo, useMemo } from 'react'
import { SuperScheduler } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'
import 'super-scheduler/styles.css'

type Campaign = { client: string; channel: 'social' | 'print' | 'video'; progress: number }

const CHANNEL_LABEL: Record<Campaign['channel'], string> = {
  social: 'Social',
  print: 'Print',
  video: 'Video',
}

const CampaignContent = memo(function CampaignContent(props: {
  title: string
  campaign: Campaign
  compact: boolean
}) {
  const { title, campaign, compact } = props
  if (compact) return <strong className="campaign__title">{title}</strong>
  return (
    <span className="campaign">
      <strong className="campaign__title">{title}</strong>
      <span className="campaign__meta">
        {campaign.client} · {CHANNEL_LABEL[campaign.channel]} ·{' '}
        {Math.round(campaign.progress * 100)}%
      </span>
    </span>
  )
})

// Module-level functions keep their identity: a new function re-renders every slot.
const renderEvent: NonNullable<SchedulerProps['renderEvent']> = ({ e, data, width, lod }) => {
  // `data` is the event after onBeforeEventRender; custom fields need a cast.
  const campaign = data as SuperScheduler.EventRenderData<Campaign>
  // `width` comes in 8 px steps and changes only when a gesture ends.
  return (
    <CampaignContent title={e.text()} campaign={campaign} compact={width < 160 || lod !== 'full'} />
  )
}

// The HTML fallback paints first and stays if the React content fails.
const onBeforeEventRender: NonNullable<SchedulerProps['onBeforeEventRender']> = (args) => {
  args.data.html = SuperScheduler.Util.escapeHtml(args.data.text)
}

export function CampaignBoard(props: {
  resources: SuperScheduler.ResourceData[]
  campaigns: SuperScheduler.EventData<Campaign>[]
}) {
  const events = useMemo(() => props.campaigns.slice(), [props.campaigns])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={61}
      scale="Day"
      cellWidth={36}
      eventHeight={44}
      resources={props.resources}
      events={events}
      onBeforeEventRender={onBeforeEventRender}
      renderEvent={renderEvent}
    />
  )
}

Sie sollten jede Kampagne mit Kunde, Kanal und Fortschritt sehen und nur den Titel, wenn das Ereignis schmaler als 160 px oder herausgezoomt ist.

Die Argumente im Detail:

  • e ist der Ereignis-Wrapper: e.id(), e.text(), e.start(), e.end() und e.data für das gespeicherte Objekt.
  • data ist das Ereignis in dem Zustand, in dem onBeforeEventRender es hinterlassen hat, mit start und end als SuperScheduler.Date-Werten. Eigene Felder brauchen einen Cast, wie im Snippet.
  • width ist die gerenderte Breite in Schritten von 8 px, aktualisiert am Ende einer Geste statt in jedem Frame.
  • lod ist die Detailstufe ('full', 'compact' oder 'overview') zum Zeitpunkt des Renderns.

Köpfe, Ecke, Zellen und Areas

src/TeamBoard.tsxtsx
import { useMemo } from 'react'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'

const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [{ groupBy: 'Month' }, { groupBy: 'Day' }]

// Scheduler dates are civil values: format their native Date in UTC to keep the wall clock.
const WEEKDAY = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' })

// Every slot of a kind gets the function's result: return content for each case
// (a null result leaves that slot empty rather than showing the fallback).
const SLOTS: SchedulerProps = {
  renderRowHeader: ({ row }) => {
    const role = typeof row.data.role === 'string' ? row.data.role : ''
    return (
      <span className="person">
        <span className="person__initials" aria-hidden="true">
          {row.name.slice(0, 1)}
        </span>
        <span className="person__name">{row.name}</span>
        {role !== '' && <span className="person__role">{role}</span>}
      </span>
    )
  },
  renderTimeHeader: ({ header }) =>
    header.level === 0 ? (
      <span>{header.start.toString('MMMM yyyy')}</span>
    ) : (
      <span className="day">
        <small>{WEEKDAY.format(header.start.toDate())}</small> {header.start.toString('d')}
      </span>
    ),
  renderCorner: () => <span className="corner">Team</span>,
  // Only areas declared with `render: true` reach renderArea.
  renderArea: ({ area }) =>
    area.id === 'approval' ? <span className="badge">Needs approval</span> : null,
  onBeforeEventRender: (args) => {
    if (args.data.status === 'draft') {
      args.data.areas = [
        { id: 'approval', render: true, right: 4, top: 4, width: 96, height: 16, action: 'None' },
      ]
    }
  },
}

export function TeamBoard(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const events = useMemo(() => props.events.slice(), [props.events])
  return (
    <SuperSchedulerComponent
      {...SLOTS}
      startDate="2026-10-01"
      days={31}
      scale="Day"
      timeHeaders={TIME_HEADERS}
      rowHeaderWidth={200}
      resources={props.resources}
      events={events}
      // Keeps React work bounded on large boards (defaults shown).
      renderOptions={{ sliceMs: 8 }}
    />
  )
}

Eine Render-Funktion besitzt jeden Slot ihrer Art. Liefern Sie für jeden Fall Inhalt: Ein Ergebnis null lässt den Slot leer, statt den Fallback zu zeigen. renderArea ist in der Praxis die Ausnahme, weil nur Areas, die Sie mit render: true deklariert haben, die Funktion erreichen.

Hinweise pro Slot:

  • Zeilenköpfe. Der Umschalter des Baums bleibt an seinem Platz. Mit rowHeaderColumns läuft die Funktion einmal pro Spalte und erhält deren Index in column.
  • Zeitköpfe. header.level ist der Index in timeHeaders (0 ist die oberste Zeile). Datumsangaben des Planers sind bürgerliche Zeitwerte: Um sie mit Intl zu formatieren, übergeben Sie date.toDate() und timeZone: 'UTC', wie im Snippet.
  • Zellen. renderCell mountet einen React-Root pro gemounteter Zelle und keinen, solange Zellen schmaler als 24 px sind. Eine Ansicht mit 40 Zeilen mal 30 Tagen mountet bereits 1.200 davon: Für Verfügbarkeit, Preise oder Schattierung setzen Sie stattdessen html, cssClass oder backColor in onBeforeCellRender.
  • Areas. Deklarieren Sie die Area am Ereignis (oder an Zeile, Zelle, Kopf) mit render: true und ihrer Position; source sagt Ihnen, zu welchem Element die Area gehört.

Fallbacks, Batches und Lebenszyklus

React-Inhalte blockieren nie das Zeichnen:

  1. Der Planer zeichnet zuerst den HTML- oder Text-Fallback: das html oder den text, den Ihre Daten und die onBefore*Render-Hooks liefern.
  2. Wenn der Browser untätig ist, werden React-Inhalte in Batches übernommen, die renderOptions.sliceMs anpeilen (standardmäßig 8 ms). Jeder Slot blendet seinen Fallback aus, sobald sein Inhalt bereit ist.
  3. Während Scrollen, Zoomen und Ziehen bewegen sich vorhandene Inhalte mit dem Raster. Neue Slots und Änderungen am Renderer warten, bis die Geste endet.
  4. Wirft eine Render-Funktion einen Fehler, behält dieser Slot seinen Fallback, und der Fehler wird einmal pro Slot über reportError des Browsers gemeldet (ein globales error-Event, das Ihr Error-Tracking abfangen kann).

Inhalte, die aus dem sichtbaren Bereich scrollen, werden abgelöst aufbewahrt, damit sie ohne erneutes Rendern zurückkommen können: bis zu renderOptions.retain Elemente, standardmäßig das Doppelte der gemounteten Anzahl, höchstens 2.000. Der lokale State eines aufbewahrten Elements bleibt erhalten; ein verdrängtes Element beginnt von vorn. retain: 0 schaltet die Aufbewahrung ab.

Slot-Inhalte werden über Portale gerendert und sehen daher Ihre Provider: Theme, Übersetzungen, Router, Daten-Clients. CSS kann [data-super-scheduler-slot], [data-super-scheduler-slot-ready] und [data-super-scheduler-fallback] ansprechen.

Auf dem Server rendert die Komponente ein leeres <div>; Slots erscheinen, nachdem der Planer im Client gemountet ist. Siehe Server-Rendering und Prerendering.

Hover-Karten

eventHover zeigt neben einem Ereignis eine React-Karte, nachdem der Zeiger darauf verweilt hat. Ohne eventHover erscheint keine Karte.

OptionStandardWirkung
render(args)PflichtInhalt der Karte; args hat control, e, row, anchor (den Kasten des Ereignisses), pinned und close()
delay350Millisekunden, die der Zeiger verweilt, bevor sich die Karte öffnet
leaveGrace180Millisekunden bis zum Schließen, nachdem der Zeiger das Ereignis oder die Karte verlassen hat
placement'auto''auto', 'above', 'below', 'start' oder 'end'
pinfalse'click' oder 'dblclick' heftet die Karte an, damit Nutzer mit ihr interagieren können
glidetrueWechselt der Zeiger zu einem anderen Ereignis, wandert die offene Karte mit, statt sich neu zu öffnen
src/BookingsWithCards.tsxtsx
import { useMemo } from 'react'
import type { SchedulerEventHoverOptions, SuperScheduler } from 'super-scheduler'
import { SuperSchedulerComponent } from 'super-scheduler/react-render'

// Module-level: the options object keeps its identity across renders.
const BOOKING_CARD: SchedulerEventHoverOptions = {
  delay: 350,
  leaveGrace: 180,
  placement: 'auto',
  // A click pins the card as a non-modal dialog; on touch screens a tap does it.
  pin: 'click',
  render: ({ e, row, pinned, close }) => (
    <article className="booking-card">
      <h3>{e.text()}</h3>
      <p>{row.name}</p>
      <p>
        {e.start().toString('d MMM, HH:mm')} to {e.end().toString('d MMM, HH:mm')}
      </p>
      {pinned && (
        <button type="button" onClick={close}>
          Close
        </button>
      )}
    </article>
  ),
}

export function BookingsWithCards(props: {
  resources: SuperScheduler.ResourceData[]
  events: SuperScheduler.EventData[]
}) {
  const events = useMemo(() => props.events.slice(), [props.events])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={14}
      scale="Day"
      resources={props.resources}
      events={events}
      eventHover={BOOKING_CARD}
    />
  )
}

So verhält sich die Karte:

  • Eine nicht angeheftete Karte ist ein role="tooltip"; eine angeheftete Karte ist ein nicht modaler role="dialog", der den Fokus übernimmt. Esc oder ein Klick außerhalb schließt eine angeheftete Karte und gibt den Fokus an das Ereignis zurück.
  • Bewegt sich der Zeiger in die Karte, bleibt sie offen. Scrollen, Zoomen, Ziehen und Auswählen blenden sie sofort aus.
  • Die Karte wird beim Öffnen platziert, klappt um oder schrumpft, damit sie in den Viewport passt, und berücksichtigt reduzierte Bewegung.
  • Sie lebt in document.body und trägt das Theme des Planers. Gestalten Sie sie mit --super-scheduler-hover-padding, -hover-border, -hover-radius, -hover-bg, -hover-color, -hover-shadow und --super-scheduler-z-hover.
  • Touchscreens kennen kein Hover: Mit pin: 'click' öffnet ein Tippen eine angeheftete Karte.

Hover-Karten sind unabhängig von den HTML-Blasen (bubble, bubbleHtml). Blasen können keine React-Inhalte aufnehmen; verwenden Sie dafür eventHover.

Performance

  • Stabile Funktionen. Definieren Sie Render-Funktionen und Optionsobjekte auf Modulebene oder memoisieren Sie sie. Eine neue Funktionsidentität rendert jeden Slot dieser Art neu.
  • Günstige Renderings. sliceMs ist ein Zielwert für Batches, keine Begrenzung Ihres Codes: Eine einzige langsame Render-Funktion verzögert ihren Batch. Lesen Sie in Render-Funktionen kein Layout und messen Sie kein DOM; verwenden Sie width und lod.
  • Memoisierte Komponenten. Umschließen Sie Slot-Komponenten mit memo und übergeben Sie primitive Props, wie im Ereignis-Snippet.
  • Context. Ein Context-Wert, der sich oft ändert, rendert jeden Slot neu, der ihn liest. Halten Sie schnell wechselnden State (Zeigerposition, Timer) aus Contexts heraus, die Slots konsumieren.
  • Zellen. Bevorzugen Sie bei großen Rastern Strings aus onBeforeCellRender gegenüber renderCell.
  • Kein State pro Frame. Setzen Sie keinen React-State aus onScroll oder Zieh-Handlern; die Bibliothek erledigt ihre Arbeit pro Frame ohne React-Renderings.

Messen Sie Ihre eigenen Inhalte mit dem React Profiler: Die Bibliothek kann eine teure Komponente nicht günstig machen.

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. Gerätebuchung im LaborWer ein Gerät bucht, bekommt die Kalibrierung mit. Eine Sitzung aus dem Servicefenster holen, dann den eigenen Lauf verlängern.