# Full Calendar > Interactive calendar component with multiple views, event handling, and resource scheduling powered by FullCalendar --- .. toc:: .. llms_copy::Full Calendar `dash-fullcalendar` is a Dash component library that wraps the powerful `@fullcalendar/react` library, bringing professional calendar and scheduling capabilities to your Dash applications. It provides interactive calendar views with drag-and-drop event management, multiple view types (month, week, day, list, timeline), resource scheduling, custom event rendering, and comprehensive callback support for building sophisticated scheduling interfaces and event management systems. ### Installation [Visit GitHub Repo](https://github.com/pip-install-python/dash-fullcalendar) ⭐️ Star this component on GitHub! Stay up to date on new releases and browse the codebase. ```bash pip install dash-fullcalendar ``` --- ### Quick Start Create an interactive calendar with basic event handling, including date clicking, event clicking, and drag-and-drop functionality. .. exec::docs.full_calendar_component.introduction :code: false ```python # File: docs/full_calendar_component/introduction.py from datetime import date, datetime, time, timedelta import dash_fullcalendar as dcal import dash_mantine_components as dmc from dash import Input, Output, State, callback, callback_context, dcc, html, no_update from dash.exceptions import PreventUpdate DARK_CALENDAR_STYLE = { "--fc-page-bg-color": "#101113", "--fc-neutral-bg-color": "#1a1b1e", "--fc-neutral-text-color": "#f1f3f5", "--fc-border-color": "#2c2e33", "--fc-button-text-color": "#f1f3f5", "--fc-button-bg-color": "#2c2e33", "--fc-button-border-color": "#373a40", "--fc-event-text-color": "#f8f9fa", } LIGHT_CALENDAR_STYLE = { "--fc-page-bg-color": "#ffffff", "--fc-neutral-bg-color": "#f8f9fa", "--fc-neutral-text-color": "#212529", "--fc-border-color": "#dee2e6", "--fc-button-text-color": "#495057", "--fc-button-bg-color": "#f8f9fa", "--fc-button-border-color": "#dee2e6", "--fc-event-text-color": "#212529", } # Get today's date today = datetime.now() # Format the date formatted_date = today.strftime("%Y-%m-%d") def _parse_calendar_datetime(value): """Best-effort parser for FullCalendar ISO strings or datetime objects.""" if not value: return None if isinstance(value, datetime): return value if isinstance(value, time): return datetime.combine(datetime.now().date(), value) if isinstance(value, date): return datetime.combine(value, datetime.min.time()) if isinstance(value, (int, float)): return datetime.fromtimestamp(value) if isinstance(value, str): cleaned = value.strip() if cleaned.endswith("Z"): cleaned = cleaned[:-1] + "+00:00" try: parsed = datetime.fromisoformat(cleaned) except ValueError: try: parsed = datetime.strptime(cleaned, "%Y-%m-%d") except ValueError: return None if parsed.tzinfo: return parsed.astimezone().replace(tzinfo=None) return parsed return None def _coerce_date_value(value): if isinstance(value, datetime): return value.date() if isinstance(value, date): return value parsed = _parse_calendar_datetime(value) if parsed: return parsed.date() if isinstance(value, str): stripped = value.strip() for fmt in ("%Y-%m-%d", "%m/%d/%Y"): try: return datetime.strptime(stripped, fmt).date() except ValueError: continue return None def _coerce_time_value(value, date_hint): parsed = _parse_calendar_datetime(value) if parsed: return parsed date_hint = date_hint or datetime.now().date() if isinstance(value, time): return datetime.combine(date_hint, value) if isinstance(value, str): stripped = value.strip() for fmt in ("%H:%M:%S", "%H:%M"): try: t = datetime.strptime(stripped, fmt).time() return datetime.combine(date_hint, t) except ValueError: continue try: parsed = datetime.fromisoformat(stripped) return parsed.replace( year=date_hint.year, month=date_hint.month, day=date_hint.day, ) except ValueError: pass if isinstance(value, (int, float)): return datetime.fromtimestamp(value) return None def _extract_date_click_payload(payload): if not payload: return None, True if isinstance(payload, dict): date_value = payload.get("dateStr") or payload.get("date") view = payload.get("view") or {} view_type = view.get("type") all_day = payload.get("allDay") if all_day is None and view_type: all_day = view_type.startswith("dayGrid") or view_type.startswith("multiMonth") return date_value, bool(all_day) if all_day is not None else True return payload, True def _build_modal_response(start_dt, end_dt, *, all_day): start_dt = start_dt or datetime.now() if all_day: if not end_dt: end_dt = start_dt + timedelta(days=1) if end_dt <= start_dt: end_dt = start_dt + timedelta(days=1) # display end date inclusive end_display = end_dt - timedelta(days=1) if end_dt.date() > start_dt.date() else start_dt return ( True, start_dt.strftime("%Y-%m-%d"), end_display.strftime("%Y-%m-%d"), None, None, {"allDay": True}, ) end_dt = end_dt or (start_dt + timedelta(hours=1)) if end_dt <= start_dt: end_dt = start_dt + timedelta(hours=1) return ( True, start_dt.strftime("%Y-%m-%d"), end_dt.strftime("%Y-%m-%d"), start_dt.strftime("%H:%M:%S"), end_dt.strftime("%H:%M:%S"), {"allDay": False}, ) component = html.Div( [ dcc.Store(id="new_event_selection_meta", data={"allDay": False}), html.Div( dcal.FullCalendar( id="calendar", # Unique ID for the component initialView="dayGridMonth", # dayGridMonth, timeGridWeek, timeGridDay, listWeek, # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek headerToolbar={ "left": "prev,next today", "center": "", "right": "listWeek,timeGridDay,timeGridWeek,dayGridMonth", }, # Calendar header initialDate=f"{formatted_date}", # Start date for calendar editable=True, # Allow events to be edited selectable=True, # Allow dates to be selected events=[], nowIndicator=True, # Show current time indicator navLinks=True, # Allow navigation to other dates ), id="calendar-wrapper", className="dark-calendar", ), dmc.Modal( id="modal", size="xl", title="Event Details", zIndex=10000, centered=True, children=[ html.Div(id="modal_event_display_context"), dmc.Space(h=20), dmc.Group( [ dmc.Button( "Close", color="red", variant="outline", id="modal-close-button", ), ], align="right", ), ], ), dmc.Modal( id="add_modal", title="New Event", size="xl", centered=True, children=[ dmc.Grid( children=[ dmc.GridCol( html.Div( dmc.DatePickerInput( id="start_date", label="Start Date", value=datetime.now().date(), styles={"width": "100%"}, disabled=True, ), style={"width": "100%"}, ), span=6, ), dmc.GridCol( html.Div( dmc.TimeInput( label="Start Time", withSeconds=True, value=datetime.now(), # format="12", id="start_time", ), style={"width": "100%"}, ), span=6, ), ], gutter="xl", ), dmc.Grid( children=[ dmc.GridCol( html.Div( dmc.DatePickerInput( id="end_date", label="End Date", value=datetime.now().date(), styles={"width": "100%"}, ), style={"width": "100%"}, ), span=6, ), dmc.GridCol( html.Div( dmc.TimeInput( label="End Time", withSeconds=True, value=datetime.now(), # format="12", id="end_time", ), style={"width": "100%"}, ), span=6, ), ], gutter="xl", ), dmc.Grid( children=[ dmc.GridCol( span=6, children=[ dmc.TextInput( label="Event Title:", style={"width": "100%"}, id="event_name_input", required=True, ) ], ), dmc.GridCol( span=6, children=[ dmc.Select( label="Select event color", placeholder="Select one", id="event_color_select", value="ng", data=[ { "value": "bg-gradient-primary", "label": "bg-gradient-primary", }, { "value": "bg-gradient-secondary", "label": "bg-gradient-secondary", }, { "value": "bg-gradient-success", "label": "bg-gradient-success", }, { "value": "bg-gradient-info", "label": "bg-gradient-info", }, { "value": "bg-gradient-warning", "label": "bg-gradient-warning", }, { "value": "bg-gradient-danger", "label": "bg-gradient-danger", }, { "value": "bg-gradient-light", "label": "bg-gradient-light", }, { "value": "bg-gradient-dark", "label": "bg-gradient-dark", }, { "value": "bg-gradient-white", "label": "bg-gradient-white", }, ], style={"width": "100%", "marginBottom": 10}, required=True, ) ], ), ] ), dmc.RichTextEditor( id="rich_text_input", extensions=[ "StarterKit", {"Placeholder": {"placeholder": "Enter event details..."}}, ], toolbar={ "controlsGroups": [ ["Bold", "Italic", "Underline"], ["BulletList", "OrderedList"], ["Link"], ], }, ), dmc.Accordion( children=[ dmc.AccordionItem( [ dmc.AccordionControl("Raw HTML"), dmc.AccordionPanel( html.Div( id="rich_text_output", style={ "height": "300px", "overflowY": "scroll", }, ) ), ], value="raw_html", ), ], ), dmc.Space(h=20), dmc.Group( [ dmc.Button( "Submit", id="modal_submit_new_event_button", color="green", ), dmc.Button( "Close", color="red", variant="outline", id="modal_close_new_event_button", ), ], align="right", ), ], ), ], style={"padding": "1.5rem 0"}, ) @callback( Output("modal", "opened"), Output("modal", "title"), Output("modal_event_display_context", "children"), Input("modal-close-button", "n_clicks"), Input("calendar", "eventClick"), State("modal", "opened"), prevent_initial_call=True # Set this to True ) def open_event_modal(n, event_click, opened): ctx = callback_context if not ctx.triggered: raise PreventUpdate else: button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id == "calendar" and event_click is not None: event_title = event_click.get("title", "Selected event") extended_props = event_click.get("extendedProps") or {} event_context = extended_props.get("context", "No additional details provided.") return ( True, event_title, html.Div( dmc.RichTextEditor( id="input3", html=f"{event_context}", editable=False, extensions=["StarterKit"], ), style={"width": "100%", "overflowY": "auto"}, ), ) elif button_id == "modal-close-button" and n is not None: return False, no_update, no_update return opened, no_update, no_update @callback( Output("add_modal", "opened"), Output("start_date", "value"), Output("end_date", "value"), Output("start_time", "value"), Output("end_time", "value"), Output("new_event_selection_meta", "data"), Input("calendar", "dateClick"), Input("calendar", "select"), Input("modal_close_new_event_button", "n_clicks"), State("add_modal", "opened"), ) def open_add_modal(date_clicked, date_selected, close_clicks, opened): ctx = callback_context if not ctx.triggered: raise PreventUpdate trigger = ctx.triggered[0]["prop_id"] if trigger == "calendar.dateClick" and date_clicked is not None: date_value, is_all_day = _extract_date_click_payload(date_clicked) start_dt = _parse_calendar_datetime(date_value) or datetime.now() end_dt = start_dt + (timedelta(days=1) if is_all_day else timedelta(hours=1)) return _build_modal_response(start_dt, end_dt, all_day=is_all_day) if trigger == "calendar.select" and date_selected: selection = date_selected or {} start_dt = _parse_calendar_datetime(selection.get("start")) or datetime.now() end_dt = _parse_calendar_datetime(selection.get("end")) if not end_dt: end_dt = start_dt + timedelta(hours=1) is_all_day = bool(selection.get("allDay")) if is_all_day and end_dt <= start_dt: end_dt = start_dt + timedelta(days=1) return _build_modal_response(start_dt, end_dt, all_day=is_all_day) if trigger == "modal_close_new_event_button.n_clicks" and close_clicks is not None: return False, no_update, no_update, no_update, no_update, {"allDay": False} return opened, no_update, no_update, no_update, no_update, {"allDay": False} @callback( Output("calendar", "events"), Output("add_modal", "opened", allow_duplicate=True), Output("event_name_input", "value"), Output("event_color_select", "value"), Output("rich_text_input", "html"), Input("modal_submit_new_event_button", "n_clicks"), State("start_date", "value"), State("start_time", "value"), State("end_date", "value"), State("end_time", "value"), State("event_name_input", "value"), State("event_color_select", "value"), State("rich_text_output", "children"), State("calendar", "events"), State("new_event_selection_meta", "data"), prevent_initial_call=True # Set this to True ) def add_new_event( n, start_date, start_time, end_date, end_time, event_name, event_color, event_context, current_events, selection_meta, ): if n is None: raise PreventUpdate selection_meta = selection_meta or {} wants_all_day = bool(selection_meta.get("allDay")) has_time_inputs = bool(start_time or end_time) use_all_day = wants_all_day and not has_time_inputs safe_title = event_name or "Untitled event" safe_color = event_color or "bg-gradient-primary" safe_context = event_context or "" events = current_events or [] start_date_obj = _coerce_date_value(start_date) or datetime.now().date() end_date_obj = _coerce_date_value(end_date) or start_date_obj if end_date_obj < start_date_obj: end_date_obj = start_date_obj if use_all_day: new_event = { "title": safe_title, "start": start_date_obj.isoformat(), "end": (end_date_obj + timedelta(days=1)).isoformat(), "allDay": True, "className": safe_color, "extendedProps": {"context": safe_context}, } else: start_dt = _coerce_time_value(start_time, start_date_obj) or datetime.combine( start_date_obj, datetime.min.time() ) end_dt = _coerce_time_value(end_time, end_date_obj) or (start_dt + timedelta(hours=1)) if end_dt <= start_dt: end_dt = start_dt + timedelta(hours=1) new_event = { "title": safe_title, "start": start_dt.isoformat(), "end": end_dt.isoformat(), "className": safe_color, "extendedProps": {"context": safe_context}, } return events + [new_event], False, "", "bg-gradient-primary", "" @callback( Output("rich_text_output", "children"), Input("rich_text_input", "html"), ) def display_output(html_content): return html_content or "" @callback( Output("calendar-wrapper", "style"), Input("color-scheme-storage", "data"), ) def update_calendar_theme(theme): """Update calendar styling based on color scheme""" return DARK_CALENDAR_STYLE if theme == "dark" else LIGHT_CALENDAR_STYLE ``` --- ### Multiple Views & Layouts Switch between grid, list, multi-month, and Scheduler views by changing `initialView` or using navigation commands. FullCalendar supports various calendar layouts optimized for different use cases. .. exec::docs.full_calendar_component.section_renders :code: false ```python # File: docs/full_calendar_component/section_renders.py from datetime import datetime, timedelta import dash_fullcalendar as dcal import dash_mantine_components as dmc from dash import Input, Output, callback, html DARK_CALENDAR_STYLE = { "--fc-page-bg-color": "#101113", "--fc-neutral-bg-color": "#1a1b1e", "--fc-neutral-text-color": "#f1f3f5", "--fc-border-color": "#2c2e33", "--fc-button-text-color": "#f1f3f5", "--fc-button-bg-color": "#2c2e33", "--fc-button-border-color": "#373a40", "--fc-event-text-color": "#f8f9fa", } LIGHT_CALENDAR_STYLE = { "--fc-page-bg-color": "#ffffff", "--fc-neutral-bg-color": "#f8f9fa", "--fc-neutral-text-color": "#212529", "--fc-border-color": "#dee2e6", "--fc-button-text-color": "#495057", "--fc-button-bg-color": "#f8f9fa", "--fc-button-border-color": "#dee2e6", "--fc-event-text-color": "#212529", } BASE_VIEWS = [ ("dayGridMonth", "Monthly grid (dayGridMonth)"), ("timeGridWeek", "Weekly schedule (timeGridWeek)"), ("timeGridDay", "Single day (timeGridDay)"), ("listWeek", "Agenda list (listWeek)"), ("listDay", "Agenda day (listDay)"), ("multiMonthYear", "Multi-month year (multiMonthYear)"), ] HAS_RESOURCE_API = "resources" in getattr(dcal.FullCalendar, "available_properties", []) PREMIUM_VIEW_OPTIONS = [ ("resourceTimelineWeek", "Resource timeline week*"), ("resourceTimeGridDay", "Resource time grid day*"), ] VIEW_OPTIONS = BASE_VIEWS + (PREMIUM_VIEW_OPTIONS if HAS_RESOURCE_API else []) PREMIUM_VIEWS = {value for value, _ in PREMIUM_VIEW_OPTIONS} if HAS_RESOURCE_API else set() RESOURCES = [ {"id": "room-1", "title": "Room 101"}, {"id": "room-2", "title": "Room 202"}, {"id": "hybrid", "title": "Remote Crew"}, ] def _build_events(): base_day = datetime.now().date() def slot(title, day_offset, hour, duration, resource, color, context): start = datetime.combine(base_day + timedelta(days=day_offset), datetime.min.time()) + timedelta(hours=hour) end = start + timedelta(hours=duration) event = { "id": title.lower().replace(" ", "-"), "title": title, "start": start.strftime("%Y-%m-%dT%H:%M:%S"), "end": end.strftime("%Y-%m-%dT%H:%M:%S"), "className": color, "extendedProps": {"context": context}, } if resource: event["resourceId"] = resource return event return [ slot("Product Kickoff", 0, 9, 1.5, "room-1", "bg-gradient-success", "Launch plan and scope review."), slot("Design Review", 1, 11, 1, "room-2", "bg-gradient-info", "UX polish and acceptance."), slot("Customer Sync", 2, 14, 1, "hybrid", "bg-gradient-warning", "Weekly touchpoint with enterprise clients."), slot("Deployment Window", 3, 21, 2, "room-1", "bg-gradient-danger", "Late-night release window."), slot("Sprint Demo", 4, 13, 1.5, "room-2", "bg-gradient-primary", "Showcase progress to stakeholders."), ] component = dmc.Container( dmc.Grid( gutter={"base": "md", "sm": "lg", "lg": "xl"}, children=[ dmc.GridCol( dmc.Paper( html.Div(id="view-fcc"), id="intro-wrapper-fcc", withBorder=True, radius="md", p={"base": "sm", "sm": "md", "lg": "lg"}, ), span={"base": 12, "lg": 9}, ), dmc.GridCol( dmc.Paper( dmc.Stack( [ dmc.RadioGroup( label="Choose the initial view (premium views marked with *)", id="intro-view-fcc", value="dayGridMonth", children=dmc.Stack( [dmc.Radio(label=label, value=value) for value, label in VIEW_OPTIONS], gap="0.4rem", ), ), dmc.Text( "Resource views load automatically when Scheduler plugins and license keys are available." if HAS_RESOURCE_API else "Install the latest dash-fullcalendar (Scheduler build) to try the resource views.", size="sm", c="dimmed", ), ], gap="md", ), withBorder=True, radius="md", p={"base": "sm", "sm": "md", "lg": "lg"}, ), span={"base": 12, "lg": 3}, ), ], ), fluid=True, px={"base": "xs", "sm": "md"}, py={"base": "md", "sm": "xl"}, ) @callback( Output("view-fcc", "children"), Input("intro-view-fcc", "value"), Input("color-scheme-storage", "data"), ) def update_form(render: str, theme): events = _build_events() calendar_id = f"view-calendar-preview-{render}" calendar_kwargs = { "id": calendar_id, "initialView": render, "initialDate": events[0]["start"].split("T")[0], "headerToolbar": { "left": "prev,next today", "center": "title", "right": "dayGridMonth,timeGridWeek,timeGridDay,listWeek,listDay,multiMonthYear", }, "events": events, "navLinks": True, "weekends": True, "nowIndicator": True, "height": "750px", } if render in PREMIUM_VIEWS and HAS_RESOURCE_API: calendar_kwargs.update( { "schedulerLicenseKey": "GPL-My-Project-Is-Open-Source", "plugins": ["resourceTimeline", "resourceTimeGrid", "resource"], "resources": RESOURCES, } ) calendar_style = DARK_CALENDAR_STYLE if theme == "dark" else LIGHT_CALENDAR_STYLE return html.Div( dcal.FullCalendar(**calendar_kwargs), className="dark-calendar", style=calendar_style, ) ``` **Available Views:** - **Month Views**: `dayGridMonth`, `multiMonthYear` - **Week Views**: `timeGridWeek`, `dayGridWeek` - **Day Views**: `timeGridDay` - **List Views**: `listWeek`, `listMonth`, `listYear` - **Resource Views**: `resourceTimeline`, `resourceTimeGrid` (requires Scheduler) .. admonition:: Premium Scheduler plugins :icon: mdi:star-four-points :color: yellow Timeline and resource views require FullCalendar Scheduler. Use your commercial key or the open-source key (`GPL-My-Project-Is-Open-Source`) together with `plugins=["resourceTimeline","resourceTimeGrid","resource"]`. --- ### Header Toolbar & Navigation Configure the calendar header with custom buttons, navigation controls, and view switchers. The header toolbar is fully customizable through the `headerToolbar` prop. .. exec::docs.full_calendar_component.header_toolbar :code: false ```python # File: docs/full_calendar_component/header_toolbar.py from datetime import datetime, timedelta import dash_fullcalendar as dcal import dash_mantine_components as dmc from dash import Input, Output, callback, html DARK_CALENDAR_STYLE = { "--fc-page-bg-color": "#101113", "--fc-neutral-bg-color": "#1a1b1e", "--fc-neutral-text-color": "#f1f3f5", "--fc-border-color": "#2c2e33", "--fc-button-text-color": "#f1f3f5", "--fc-button-bg-color": "#2c2e33", "--fc-button-border-color": "#373a40", "--fc-event-text-color": "#f8f9fa", } LIGHT_CALENDAR_STYLE = { "--fc-page-bg-color": "#ffffff", "--fc-neutral-bg-color": "#f8f9fa", "--fc-neutral-text-color": "#212529", "--fc-border-color": "#dee2e6", "--fc-button-text-color": "#495057", "--fc-button-bg-color": "#f8f9fa", "--fc-button-border-color": "#dee2e6", "--fc-event-text-color": "#212529", } BASE_HEADER_CHOICES = [ {"value": "title", "label": "Title"}, {"value": "prev", "label": "Prev"}, {"value": "next", "label": "Next"}, {"value": "prevYear", "label": "Prev Year"}, {"value": "today", "label": "Today"}, {"value": "dayGridMonth", "label": "dayGridMonth"}, {"value": "timeGridWeek", "label": "timeGridWeek"}, {"value": "timeGridDay", "label": "timeGridDay"}, {"value": "listWeek", "label": "listWeek"}, {"value": "listDay", "label": "listDay"}, {"value": "multiMonthYear", "label": "multiMonthYear"}, ] HAS_RESOURCE_API = "resources" in getattr(dcal.FullCalendar, "available_properties", []) PREMIUM_CHOICES = [ {"value": "resourceTimelineWeek", "label": "resourceTimelineWeek*"}, {"value": "resourceTimeGridDay", "label": "resourceTimeGridDay*"}, ] HEADER_CHOICES = BASE_HEADER_CHOICES + (PREMIUM_CHOICES if HAS_RESOURCE_API else []) PREMIUM_KEYS = {choice["value"] for choice in PREMIUM_CHOICES} if HAS_RESOURCE_API else set() RESOURCES = [ {"id": "room-1", "title": "Room 101"}, {"id": "room-2", "title": "Room 202"}, {"id": "hybrid", "title": "Remote Crew"}, ] def _demo_events(): base_day = datetime.now().date() def block(title, day_offset, hour, duration, resource, color, context): start = datetime.combine(base_day + timedelta(days=day_offset), datetime.min.time()) + timedelta(hours=hour) end = start + timedelta(hours=duration) event = { "title": title, "start": start.strftime("%Y-%m-%dT%H:%M:%S"), "end": end.strftime("%Y-%m-%dT%H:%M:%S"), "className": color, "extendedProps": {"context": context}, } if resource: event["resourceId"] = resource return event return [ block("Roadmap sync", 0, 10, 1.5, "room-1", "bg-gradient-success", "Company-wide goals alignment."), block("Design pairing", 1, 13, 1, "room-2", "bg-gradient-info", "Working session with design."), block("Support rotation", 1, 16, 2, "hybrid", "bg-gradient-warning", "Handling premium support tickets."), block("Deploy", 3, 21, 1.5, "room-1", "bg-gradient-danger", "Nightly deployment window."), ] component = dmc.Container( dmc.Grid( gutter={"base": "md", "sm": "lg", "lg": "xl"}, children=[ dmc.GridCol( dmc.Paper( html.Div(id="view-fcc-2"), id="intro-wrapper-fcc-2", withBorder=True, radius="md", p={"base": "sm", "sm": "md", "lg": "lg"}, ), span={"base": 12, "lg": 9}, ), dmc.GridCol( dmc.Paper( dmc.Stack( [ dmc.MultiSelect( label="Left of the headerToolbar", placeholder="Pick buttons", id="fcc-headerToolbar-left-muti-select", value=["prev", "today", "next"], data=HEADER_CHOICES, ), dmc.MultiSelect( label="Center of the headerToolbar", placeholder="Pick buttons", id="fcc-headerToolbar-center-muti-select", value=["title"], data=HEADER_CHOICES, ), dmc.MultiSelect( label="Right of the headerToolbar", placeholder="Pick buttons", id="fcc-headerToolbar-right-muti-select", value=["dayGridMonth", "timeGridWeek", "timeGridDay", "listWeek"], data=HEADER_CHOICES, ), dmc.Text( "Buttons marked with * require Scheduler plugins and a license key." if HAS_RESOURCE_API else "Install the Scheduler build of dash-fullcalendar to unlock resource buttons (*).", size="sm", c="dimmed", ), ], gap="md", ), withBorder=True, radius="md", p={"base": "sm", "sm": "md", "lg": "lg"}, ), span={"base": 12, "lg": 3}, ), ], ), fluid=True, px={"base": "xs", "sm": "md"}, py={"base": "md", "sm": "xl"}, ) @callback( Output("view-fcc-2", "children"), Input("fcc-headerToolbar-left-muti-select", "value"), Input("fcc-headerToolbar-center-muti-select", "value"), Input("fcc-headerToolbar-right-muti-select", "value"), Input("color-scheme-storage", "data"), ) def update_form(left_buttons, center_buttons, right_buttons, theme): left = ",".join(left_buttons) if left_buttons else "" center = ",".join(center_buttons) if center_buttons else "" right = ",".join(right_buttons) if right_buttons else "" events = _demo_events() calendar_kwargs = { "id": "view-calendar-toolbar", "initialView": "dayGridMonth", "initialDate": events[0]["start"].split("T")[0], "headerToolbar": {"left": left, "center": center, "right": right}, "events": events, "editable": True, "selectable": True, "navLinks": True, "nowIndicator": True, "height": "650px", } selected = set(left_buttons + center_buttons + right_buttons) if HAS_RESOURCE_API and (selected & PREMIUM_KEYS): calendar_kwargs.update( { "schedulerLicenseKey": "GPL-My-Project-Is-Open-Source", "plugins": ["resourceTimeline", "resourceTimeGrid", "resource"], "resources": RESOURCES, } ) calendar_style = DARK_CALENDAR_STYLE if theme == "dark" else LIGHT_CALENDAR_STYLE return html.Div( dcal.FullCalendar(**calendar_kwargs), className="dark-calendar", style=calendar_style, ) ``` **Header Configuration:** The `headerToolbar` prop accepts an object with `left`, `center`, and `right` properties. Each can contain: - Navigation buttons: `prev`, `next`, `today` - View switcher buttons: `dayGridMonth`, `timeGridWeek`, `timeGridDay` - Custom buttons: defined in `customButtons` prop - Title: `title` displays the current date range --- ### Interactive Event Management Implement complete event management workflows with drag-and-drop, event resizing, date selection, and click handlers. This example demonstrates all interactive callback patterns. .. exec::docs.full_calendar_component.extra_fields :code: false ```python # File: docs/full_calendar_component/extra_fields.py from datetime import datetime, timedelta import dash_fullcalendar as dcal import dash_mantine_components as dmc from dash import Input, Output, callback, callback_context, html, no_update from dash.exceptions import PreventUpdate BUSINESS_HOURS = [ {"daysOfWeek": [1, 2, 3, 4, 5], "startTime": "09:00", "endTime": "17:00"}, ] CARD_STYLE = { "backgroundColor": "var(--mantine-color-dark-6)", "border": "1px solid var(--mantine-color-dark-4)", "color": "var(--mantine-color-gray-0)", } FIELDSET_STYLES = { "root": { **CARD_STYLE, "borderRadius": "var(--mantine-radius-md)", "padding": "var(--mantine-spacing-lg)", } } ALERT_STYLES = {"root": {"color": "var(--mantine-color-dark-9)"}} DARK_CALENDAR_STYLE = { "--fc-page-bg-color": "#101113", "--fc-neutral-bg-color": "#1a1b1e", "--fc-neutral-text-color": "#f1f3f5", "--fc-border-color": "#2c2e33", "--fc-button-text-color": "#f1f3f5", "--fc-button-bg-color": "#2c2e33", "--fc-button-border-color": "#373a40", "--fc-event-text-color": "#f8f9fa", } LIGHT_CALENDAR_STYLE = { "--fc-page-bg-color": "#ffffff", "--fc-neutral-bg-color": "#f8f9fa", "--fc-neutral-text-color": "#212529", "--fc-border-color": "#dee2e6", "--fc-button-text-color": "#495057", "--fc-button-bg-color": "#f8f9fa", "--fc-button-border-color": "#dee2e6", "--fc-event-text-color": "#212529", } def _slot(day_offset, hour, duration, title, color, context): day = datetime.now().date() + timedelta(days=day_offset) start = datetime.combine(day, datetime.min.time()) + timedelta(hours=hour) end = start + timedelta(hours=duration) return { "id": title.lower().replace(" ", "-"), "title": title, "start": start.strftime("%Y-%m-%dT%H:%M:%S"), "end": end.strftime("%Y-%m-%dT%H:%M:%S"), "className": color, "extendedProps": {"context": context}, } ADVANCED_EVENTS = [ _slot(0, 9, 1, "Stand-up", "bg-gradient-primary", "Daily scrum and blockers."), _slot(0, 13, 1, "Pairing Session", "bg-gradient-info", "Mob pairing on the dashboard."), _slot(1, 11, 1.5, "Customer Demo", "bg-gradient-success", "Demo latest release to customers."), _slot(2, 15, 2, "Deep Work", "bg-gradient-warning", "Heads-down time for migration tasks."), _slot(3, 20, 1, "Deploy", "bg-gradient-danger", "Blue/green switch-over."), ] component = dmc.Container( dmc.Grid( gutter={"base": "md", "sm": "lg", "lg": "xl"}, children=[ dmc.GridCol( dmc.Paper( dmc.Stack( [ dmc.Group( [ dmc.Button("Prev", id="advanced-command-prev", variant="light", size="compact-sm"), dmc.Button("Today", id="advanced-command-today", size="compact-sm"), dmc.Button("Next", id="advanced-command-next", variant="light", size="compact-sm"), ], justify="center", gap="xs", ), html.Div( dcal.FullCalendar( id="advanced-calendar", initialView="timeGridWeek", events=ADVANCED_EVENTS, selectable=True, selectMirror=True, editable=True, eventDurationEditable=True, weekends=True, businessHours=BUSINESS_HOURS, nowIndicator=True, height="780px", ), id="advanced-calendar-wrapper", className="dark-calendar", ), ], gap="md", ), radius="md", withBorder=True, p={"base": "sm", "sm": "md", "lg": "xl"}, style=CARD_STYLE, ), span={"base": 12, "lg": 8}, ), dmc.GridCol( dmc.Paper( dmc.Stack( [ dmc.Fieldset( legend="Toggle calendar behavior", children=dmc.Stack( [ dmc.Switch(label="Show weekends", id="toggle-weekends", checked=True, color="indigo"), dmc.Switch(label="Enforce business hours", id="toggle-business-hours", checked=True, color="indigo"), dmc.Switch(label="Allow dragging/resizing", id="toggle-editable", checked=True, color="indigo"), dmc.Switch(label="Allow range selection", id="toggle-selectable", checked=True, color="indigo"), ], gap="xs", ), styles=FIELDSET_STYLES, ), dmc.Divider(label="Live activity", variant="dashed"), dmc.Alert( "Click a date to capture quick notes.", id="calendar-click-output", color="indigo", variant="light", radius="md", styles=ALERT_STYLES, ), dmc.Alert( "Select a range to schedule a block.", id="calendar-select-output", color="teal", variant="light", radius="md", styles=ALERT_STYLES, ), dmc.Alert( "Move or resize an event to see the payload.", id="calendar-mutation-output", color="yellow", variant="light", radius="md", styles=ALERT_STYLES, ), ], gap="md", ), radius="md", withBorder=True, p={"base": "sm", "sm": "md", "lg": "xl"}, style=CARD_STYLE, ), span={"base": 12, "lg": 4}, ), ], ), fluid=True, px={"base": "xs", "sm": "md"}, py={"base": "md", "sm": "xl"}, ) @callback( Output("advanced-calendar", "command"), Input("advanced-command-prev", "n_clicks"), Input("advanced-command-today", "n_clicks"), Input("advanced-command-next", "n_clicks"), prevent_initial_call=True, ) def navigate_calendar(prev_clicks, today_clicks, next_clicks): ctx = callback_context if not ctx.triggered: raise PreventUpdate trigger = ctx.triggered[0]["prop_id"].split(".")[0] mapping = { "advanced-command-prev": "prev", "advanced-command-today": "today", "advanced-command-next": "next", } return {"type": mapping.get(trigger, "today")} @callback( Output("advanced-calendar", "weekends"), Output("advanced-calendar", "businessHours"), Output("advanced-calendar", "editable"), Output("advanced-calendar", "selectable"), Input("toggle-weekends", "checked"), Input("toggle-business-hours", "checked"), Input("toggle-editable", "checked"), Input("toggle-selectable", "checked"), ) def tune_calendar(weekends, business_hours, editable, selectable): return ( bool(weekends), BUSINESS_HOURS if business_hours else False, bool(editable), bool(selectable), ) @callback( Output("calendar-click-output", "children"), Output("calendar-select-output", "children"), Output("calendar-mutation-output", "children"), Input("advanced-calendar", "dateClick"), Input("advanced-calendar", "select"), Input("advanced-calendar", "eventDrop"), Input("advanced-calendar", "eventResize"), Input("advanced-calendar", "eventClick"), prevent_initial_call=True, ) def surface_activity(date_click, selection, event_drop, event_resize, event_click): ctx = callback_context if not ctx.triggered: raise PreventUpdate prop_id = ctx.triggered[0]["prop_id"] click_msg = no_update select_msg = no_update mutation_msg = no_update if prop_id == "advanced-calendar.dateClick" and date_click: click_msg = f"You clicked {date_click}." elif prop_id == "advanced-calendar.select" and selection: select_msg = f"Selected {selection['start']} → {selection['end']}." elif prop_id == "advanced-calendar.eventDrop" and event_drop: delta = event_drop["delta"] click_delta = delta.get("days", 0) or delta.get("milliseconds", 0) / (1000 * 60 * 60 * 24) mutation_msg = f"Moved {event_drop['event']['title']} by {click_delta:.1f} day(s)." elif prop_id == "advanced-calendar.eventResize" and event_resize: delta = event_resize["delta"] hours = delta.get("milliseconds", 0) / (1000 * 60 * 60) mutation_msg = f"Extended {event_resize['event']['title']} by {hours:.1f} hour(s)." elif prop_id == "advanced-calendar.eventClick" and event_click: context = (event_click.get("extendedProps") or {}).get("context", "No notes attached.") mutation_msg = f"{event_click.get('title', 'Event')}: {context}" return click_msg, select_msg, mutation_msg @callback( Output("advanced-calendar-wrapper", "style"), Input("color-scheme-storage", "data"), ) def update_calendar_theme(theme): """Update calendar styling based on color scheme""" return DARK_CALENDAR_STYLE if theme == "dark" else LIGHT_CALENDAR_STYLE ``` **Interactive Callback Properties:** - **`dateClick`**: Fires when a date/time is clicked, returns ISO datetime string - **`select`**: Fires when date range is selected, returns `{"start": str, "end": str, "allDay": bool}` - **`eventClick`**: Fires when event is clicked, returns complete event object - **`eventDrop`**: Fires when event is dragged to new time/date - **`eventResize`**: Fires when event duration is changed via resizing - **`command`**: Input prop to programmatically control the calendar (navigate, change view, etc.) --- ### Remote Data & API Integration Load events from REST APIs, databases, or real-time data sources. Events can be dynamically updated through Dash callbacks. .. exec::docs.full_calendar_component.api_example :code: false ```python # File: docs/full_calendar_component/api_example.py # import re # from datetime import datetime, timedelta # from html import unescape # from html.parser import HTMLParser # from urllib.parse import urlparse # # import dash_fullcalendar as dcal # from dash import Input, Output, State, callback, callback_context, dcc, html, no_update # import dash_mantine_components as dmc # from dash.exceptions import PreventUpdate # from data import api # # CALENDAR_STYLE = { # "--fc-page-bg-color": "#101113", # "--fc-neutral-bg-color": "#1a1b1e", # "--fc-neutral-text-color": "#f1f3f5", # "--fc-border-color": "#2c2e33", # "--fc-button-text-color": "#f1f3f5", # "--fc-button-bg-color": "#2c2e33", # "--fc-button-border-color": "#373a40", # "--fc-event-text-color": "#f8f9fa", # } # # CARD_STYLE = { # "backgroundColor": "var(--mantine-color-dark-6)", # "border": "1px solid var(--mantine-color-dark-4)", # "color": "var(--mantine-color-gray-0)", # } # # FIELDSET_STYLES = { # "root": { # **CARD_STYLE, # "borderRadius": "var(--mantine-radius-md)", # "padding": "var(--mantine-spacing-lg)", # }, # } # # CATEGORY_OPTIONS = [ # {"value": "plotly", "label": "Plotly"}, # {"value": "dash", "label": "Dash"}, # {"value": "python", "label": "Python"}, # ] # # VIEW_OPTIONS = [ # {"label": "Month", "value": "dayGridMonth"}, # {"label": "Week", "value": "timeGridWeek"}, # {"label": "Day", "value": "timeGridDay"}, # {"label": "List", "value": "listWeek"}, # ] # # DEFAULT_CONTEXT_PLACEHOLDER = "No description provided." # SAFE_PROTOCOLS = {"http", "https", "mailto", "tel", ""} # TAG_REGEX = re.compile(r"<[^>]+>") # # # class _SafeHTMLToMarkdown(HTMLParser): # """Convert limited HTML snippets into Markdown while stripping unsafe markup.""" # # _BLOCK_TAGS = { # "p", # "div", # "section", # "article", # "header", # "footer", # } # _SKIP_TAGS = {"script", "style"} # _BOLD_TAGS = {"strong", "b"} # _ITALIC_TAGS = {"em", "i"} # _CODE_TAGS = {"code", "kbd", "samp"} # # def __init__(self): # super().__init__(convert_charrefs=True) # self.fragments = [] # self.anchor_stack = [] # self.list_stack = [] # self.skip_depth = 0 # # def handle_starttag(self, tag, attrs): # tag = tag.lower() # if tag in self._SKIP_TAGS: # self.skip_depth += 1 # return # if self.skip_depth: # return # # if tag == "br": # self.fragments.append(" \n") # elif tag in self._BOLD_TAGS: # self.fragments.append("**") # elif tag in self._ITALIC_TAGS: # self.fragments.append("*") # elif tag in self._CODE_TAGS: # self.fragments.append("`") # elif tag == "a": # href = "" # for attr, value in attrs: # if attr.lower() == "href": # href = _sanitize_href(value) # break # self.anchor_stack.append(href) # self.fragments.append("[") # elif tag == "ul": # self.list_stack.append({"ordered": False, "count": 0}) # elif tag == "ol": # self.list_stack.append({"ordered": True, "count": 0}) # elif tag == "li": # if not self.list_stack: # self.list_stack.append({"ordered": False, "count": 0}) # entry = self.list_stack[-1] # entry["count"] += 1 # bullet = f"{entry['count']}." if entry["ordered"] else "-" # indent = " " * (len(self.list_stack) - 1) # if not self.fragments or not self.fragments[-1].endswith("\n"): # self.fragments.append("\n") # self.fragments.append(f"{indent}{bullet} ") # elif tag in self._BLOCK_TAGS: # if self.fragments and not self.fragments[-1].endswith("\n\n"): # if not self.fragments[-1].endswith("\n"): # self.fragments.append("\n") # if not self.fragments[-1].endswith("\n\n"): # self.fragments.append("\n") # # def handle_endtag(self, tag): # tag = tag.lower() # if tag in self._SKIP_TAGS: # if self.skip_depth: # self.skip_depth -= 1 # return # if self.skip_depth: # return # # if tag in self._BOLD_TAGS: # self.fragments.append("**") # elif tag in self._ITALIC_TAGS: # self.fragments.append("*") # elif tag in self._CODE_TAGS: # self.fragments.append("`") # elif tag == "a": # href = self.anchor_stack.pop() if self.anchor_stack else "" # if href: # self.fragments.append(f"]({href})") # else: # self.fragments.append("]") # elif tag == "li": # if not self.fragments or not self.fragments[-1].endswith("\n"): # self.fragments.append("\n") # elif tag in {"ul", "ol"}: # if self.list_stack: # self.list_stack.pop() # if not self.fragments or not self.fragments[-1].endswith("\n"): # self.fragments.append("\n") # elif tag in self._BLOCK_TAGS: # if not self.fragments or not self.fragments[-1].endswith("\n"): # self.fragments.append("\n") # if not self.fragments[-1].endswith("\n\n"): # self.fragments.append("\n") # # def handle_data(self, data): # if self.skip_depth or not data: # return # normalized = data.replace("\xa0", " ") # if not normalized.strip(): # if self.fragments and not self.fragments[-1].endswith((" ", "\n")): # self.fragments.append(" ") # return # normalized = re.sub(r"\s+", " ", normalized) # self.fragments.append(normalized) # # def get_value(self): # text = "".join(self.fragments) # text = re.sub(r"[ \t]+\n", "\n", text) # text = re.sub(r"\n{3,}", "\n\n", text) # return text.strip() # # # def _sanitize_href(value): # if not value: # return "" # candidate = value.strip() # if not candidate: # return "" # if candidate.lower().startswith("javascript:"): # return "" # try: # parsed = urlparse(candidate) # except ValueError: # return "" # scheme = (parsed.scheme or "").lower() # if scheme and scheme not in SAFE_PROTOCOLS: # return "" # return candidate # # # def _strip_tags(value): # if not value: # return "" # without_tags = TAG_REGEX.sub(" ", value) # without_tags = unescape(without_tags) # without_tags = re.sub(r"\s+", " ", without_tags) # return without_tags.strip() # # # def _to_markdown(value): # if not value: # return DEFAULT_CONTEXT_PLACEHOLDER # parser = _SafeHTMLToMarkdown() # try: # parser.feed(str(value)) # parser.close() # except Exception: # cleaned = _strip_tags(str(value)) # return cleaned or DEFAULT_CONTEXT_PLACEHOLDER # # cleaned = parser.get_value() # if cleaned: # return cleaned # cleaned = _strip_tags(str(value)) # return cleaned or DEFAULT_CONTEXT_PLACEHOLDER # # # def _render_description(markdown_text, *, style=None): # return dcc.Markdown(markdown_text, style=style or {}, link_target="_blank") # # # def _load_events(categories): # categories = categories or ["plotly"] # combined = [] # for cat in categories: # dataset = api.get_events_by_category(cat) or [] # for idx, event in enumerate(dataset): # normalized = { # "title": event.get("title") or f"{cat.title()} event", # "start": event.get("start"), # "end": event.get("end"), # "allDay": event.get("allDay", False), # "className": event.get("className", "bg-gradient-primary"), # "extendedProps": {**event.get("extendedProps", {}), "category": cat.title()}, # "id": event.get("id") or f"{cat}-{idx}", # } # combined.append(normalized) # combined.sort(key=lambda e: e.get("start") or "") # return _normalize_event_dates(combined) # # # def _normalize_event_dates(events): # if not events: # return events # # today = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0) # normalized = [] # for idx, event in enumerate(events): # start_iso = event.get("start") # end_iso = event.get("end") # # target_day = today + timedelta(days=idx) # # def parse_iso(value): # if not value: # return None # try: # cleaned = value.replace("Z", "") # return datetime.fromisoformat(cleaned) # except ValueError: # return None # # start_dt = parse_iso(start_iso) # end_dt = parse_iso(end_iso) # # if start_dt: # new_start = datetime.combine(target_day.date(), start_dt.time()) # else: # new_start = target_day # # if end_dt: # duration = end_dt - start_dt if start_dt else timedelta(hours=1) # new_end = new_start + duration # else: # new_end = new_start + timedelta(hours=1) # # event = {**event, "start": new_start.isoformat(), "end": new_end.isoformat()} # normalized.append(event) # # return normalized # # # formatted_date = datetime.now().strftime("%Y-%m-%d") # initial_events = _load_events(["plotly"]) # # # component = html.Div( # [ # dmc.MantineProvider( # theme={"colorScheme": "dark"}, # children=[ # dmc.Modal( # id="api_event_modal", # size="xl", # title="Event Details", # zIndex=10000, # centered=True, # children=[ # html.Div(id="api_event_modal_display_context"), # dmc.Space(h=20), # dmc.Group( # [ # dmc.Button( # "Close", # color="red", # variant="outline", # id="api_event_modal_close_button", # ), # ], # align="right", # ), # ], # ) # ], # ), # dmc.Stack( # [ # dmc.Fieldset( # legend="API Controls", # styles=FIELDSET_STYLES, # children=[ # dmc.MultiSelect( # label="Categories", # id="api_category_select", # data=CATEGORY_OPTIONS, # value=["plotly"], # clearable=False, # ), # dmc.Group( # [ # dmc.SegmentedControl( # id="api_view_control", # data=VIEW_OPTIONS, # value="dayGridMonth", # color="indigo", # ), # dmc.Button( # "Refresh events", # id="api_refresh_button", # variant="outline", # ), # ], # justify="space-between", # ), # dmc.Group( # [ # dmc.Switch(label="Show weekends", id="api_toggle_weekends", checked=True, color="indigo"), # dmc.Switch(label="Enable nav links", id="api_toggle_navlinks", checked=True, color="indigo"), # dmc.Switch(label="Allow selection", id="api_toggle_selectable", checked=True, color="indigo"), # ], # justify="flex-start", # ), # ], # ), # dmc.Alert( # "Select different data categories, change views, or toggle interactions to see how dash-fullcalendar reacts to API-fed data.", # color="indigo", # variant="light", # radius="md", # ), # dmc.Flex( # [ # dmc.Paper( # html.Div( # dcal.FullCalendar( # id="api_calendar", # initialView="dayGridMonth", # headerToolbar={"left": "prev,next today", "center": "", "right": ""}, # initialDate=formatted_date, # editable=True, # selectable=True, # events=initial_events, # nowIndicator=True, # navLinks=True, # ), # className="dark-calendar", # style=CALENDAR_STYLE, # ), # radius="md", # withBorder=True, # p="md", # style={**CARD_STYLE, "flex": 2}, # ), # dmc.Paper( # dmc.Stack( # [ # dmc.Badge(f"{len(initial_events)} events loaded", id="api_event_count_badge"), # dmc.ScrollArea( # html.Div(id="api_event_summary"), # h=320, # ), # dmc.Divider(label="Last clicked event"), # html.Div( # "Click an event on the calendar to preview its details here.", # id="api_event_preview", # style={"minHeight": "80px"}, # ), # ], # gap="md", # ), # radius="md", # withBorder=True, # p="md", # style={**CARD_STYLE, "flex": 1}, # ), # ], # gap="1.5rem", # direction={"base": "column", "lg": "row"}, # style={"width": "100%"}, # ), # ], # gap="md", # ), # ], # style={"padding": "1.5rem 0"}, # ) # # # @callback( # Output("api_calendar", "events"), # Output("api_event_summary", "children"), # Output("api_event_count_badge", "children"), # Input("api_category_select", "value"), # Input("api_refresh_button", "n_clicks"), # prevent_initial_call=False, # ) # def update_api_events(categories, _): # events = _load_events(categories) # summary_children = [] # for event in events[:15]: # summary_children.append( # dmc.Paper( # dmc.Group( # [ # dmc.Stack( # [ # dmc.Text(event["title"], fw=600), # dmc.Text( # f"{event.get('start', '')} → {event.get('end', 'open')}", # size="sm", # c="dimmed", # ), # ], # gap=2, # ), # dmc.Badge(event["extendedProps"].get("category", ""), color="violet", variant="light"), # ], # justify="space-between", # ), # withBorder=True, # radius="md", # p="sm", # ) # ) # summary = ( # dmc.Stack(summary_children, gap="sm") # if summary_children # else dmc.Text("No events returned from the API.", c="dimmed") # ) # badge_text = f"{len(events)} events loaded" # return events, summary, badge_text # # # @callback( # Output("api_calendar", "weekends"), # Output("api_calendar", "navLinks"), # Output("api_calendar", "selectable"), # Input("api_toggle_weekends", "checked"), # Input("api_toggle_navlinks", "checked"), # Input("api_toggle_selectable", "checked"), # ) # def toggle_calendar_behavior(weekends, nav_links, selectable): # return bool(weekends), bool(nav_links), bool(selectable) # # # @callback( # Output("api_calendar", "command"), # Input("api_view_control", "value"), # ) # def change_view(view_value): # if not view_value: # return no_update # return {"type": "changeView", "view": view_value} # # # @callback( # Output("api_event_modal", "opened"), # Output("api_event_modal", "title"), # Output("api_event_modal_display_context", "children"), # Output("api_event_preview", "children"), # Input("api_event_modal_close_button", "n_clicks"), # Input("api_calendar", "eventClick"), # State("api_event_modal", "opened"), # prevent_initial_call=True, # ) # def open__api_event_modal(n, event_click, opened): # ctx = callback_context # # if not ctx.triggered: # raise PreventUpdate # button_id = ctx.triggered[0]["prop_id"].split(".")[0] # # if button_id == "api_calendar" and event_click is not None: # event_title = event_click.get("title", "Selected event") # extended_props = event_click.get("extendedProps") or {} # event_context = extended_props.get("context", DEFAULT_CONTEXT_PLACEHOLDER) # category = extended_props.get("category", "API") # safe_description = _to_markdown(event_context or "") # preview = dmc.Stack( # [ # dmc.Text(event_title, fw=600), # dmc.Text(f"Category: {category}", size="sm", c="dimmed"), # _render_description(safe_description, style={"fontSize": "0.9rem"}), # ], # gap=4, # ) # # return ( # True, # event_title, # _render_description(safe_description), # preview, # ) # if button_id == "api_event_modal_close_button" and n is not None: # return False, no_update, no_update, no_update # # return opened, no_update, no_update, no_update from dash import html component = html.Div() ``` **Event Data Structure:** Events provided to the `events` prop should follow this structure: ```python { "title": "Event Title", # Required "start": "2024-01-15T10:00:00", # Required (ISO format) "end": "2024-01-15T12:00:00", # Optional (ISO format) "allDay": False, # Optional (boolean) "color": "#3788d8", # Optional (CSS color) "className": "important-event", # Optional (CSS class) "extendedProps": { # Optional (custom metadata) "department": "Marketing", "description": "Q1 Planning" } } ``` --- ### Working with Commands Programmatically control the calendar using the `command` prop. Pass command dictionaries to navigate, change views, or update calendar state. **Common Commands:** ```python # Navigation {"type": "next"} # Go to next period {"type": "prev"} # Go to previous period {"type": "today"} # Go to today # View Changes {"type": "changeView", "view": "timeGridWeek"} {"type": "changeView", "view": "dayGridMonth"} # Date Navigation {"type": "gotoDate", "date": "2024-01-15"} ``` Use these commands in callbacks to control the calendar based on user interactions or application state. --- ### Customization Options **Business Hours:** Define available/working hours on the calendar: ```python businessHours={ "daysOfWeek": [1, 2, 3, 4, 5], # Monday - Friday "startTime": "09:00", "endTime": "17:00" } ``` **Event Rendering:** Customize how events appear using `eventDisplay` options: - `"auto"`: Default display (block for all-day, list-item for timed) - `"block"`: Display as blocks - `"list-item"`: Display as list items - `"background"`: Display as background events - `"none"`: Hide the event (useful for custom rendering) **Theming:** The component automatically adapts to light and dark themes when integrated with Dash Mantine Components. Calendar colors and backgrounds update based on the active color scheme. --- ### Component Properties | Property | Type | Default | Description | | :----------------- | :---------- | :----------- | :-------------------------------------------------------------------------------------------------------------- | | **`id`** | `string` | **Required** | Unique identifier for the component used in Dash callbacks. | | `events` | `list` | `[]` | Array of event objects to display on the calendar. Each event should have `title` and `start` properties. | | `initialView` | `string` | `"dayGridMonth"` | Initial calendar view to display. Options: `"dayGridMonth"`, `"timeGridWeek"`, `"timeGridDay"`, etc. | | `initialDate` | `string` | (today) | Initial date to display in ISO format (e.g., `"2024-01-15"`). | | `headerToolbar` | `dict` | (default) | Configuration for header toolbar with `left`, `center`, `right` properties. | | `footerToolbar` | `dict` | `None` | Configuration for footer toolbar (same structure as `headerToolbar`). | | `plugins` | `list` | `[]` | Array of FullCalendar plugin names to enable (e.g., `["dayGrid", "timeGrid", "interaction"]`). | | `editable` | `bool` | `False` | Enable drag-and-drop and resizing for events. | | `selectable` | `bool` | `False` | Enable date range selection by dragging across dates. | | `selectMirror` | `bool` | `False` | Show selection preview while dragging. | | `dayMaxEvents` | `bool/int` | `False` | Maximum events per day. Use `True` for automatic calculation or number for specific limit. | | `weekends` | `bool` | `True` | Display weekends on the calendar. | | `businessHours` | `dict` | `None` | Define business/working hours. Object with `daysOfWeek`, `startTime`, `endTime` properties. | | `allDaySlot` | `bool` | `True` | Display the all-day slot in week/day views. | | `slotMinTime` | `string` | `"00:00:00"` | Earliest time slot to display (e.g., `"08:00:00"`). | | `slotMaxTime` | `string` | `"24:00:00"` | Latest time slot to display (e.g., `"18:00:00"`). | | `slotDuration` | `string` | `"00:30:00"` | Duration of each time slot (e.g., `"00:15:00"` for 15-minute slots). | | `slotLabelInterval`| `string` | (auto) | Interval for displaying time labels (e.g., `"01:00:00"` for hourly labels). | | `snapDuration` | `string` | (auto) | Granularity for event dragging/resizing (e.g., `"00:15:00"`). | | `nowIndicator` | `bool` | `False` | Display a marker for the current time in week/day views. | | `height` | `string/int`| `"auto"` | Calendar height. Can be CSS value (`"600px"`, `"100%"`) or number (pixels). | | `aspectRatio` | `number` | `1.35` | Width-to-height ratio when `height` is not set. | | `eventDisplay` | `string` | `"auto"` | How events are rendered. Options: `"auto"`, `"block"`, `"list-item"`, `"background"`, `"none"`. | | `eventColor` | `string` | `"#3788d8"` | Default color for events (CSS color value). | | `eventBackgroundColor` | `string` | (auto) | Default background color for events. | | `eventBorderColor` | `string` | (auto) | Default border color for events. | | `eventTextColor` | `string` | (auto) | Default text color for events. | | `resources` | `list` | `None` | Array of resource objects for resource scheduling (requires Scheduler plugins). | | `schedulerLicenseKey` | `string` | `None` | License key for FullCalendar Scheduler. Use `"GPL-My-Project-Is-Open-Source"` for open-source projects. | | `customButtons` | `dict` | `None` | Define custom toolbar buttons with click handlers. | | `command` | `dict` | `None` | Programmatic command to execute (e.g., navigation, view changes). Use in callbacks to control calendar. | | `dateClick` | `string` | (read-only) | ISO datetime string of clicked date. Updates when user clicks a date/time. | | `select` | `dict` | (read-only) | Object with `start`, `end`, `allDay` when date range is selected. | | `eventClick` | `dict` | (read-only) | Complete event object when event is clicked. | | `eventDrop` | `dict` | (read-only) | Object with `event`, `oldEvent`, `delta`, `relatedEvents` when event is dropped. | | `eventResize` | `dict` | (read-only) | Object with `event`, `oldEvent`, `startDelta`, `endDelta` when event is resized. | | `setProps` | `func` | (Dash Internal) | Callback function to update component properties. | | `loading_state` | `object` | (Dash Internal) | Object describing the loading state of the component or its props. | **Note:** All callback properties (`dateClick`, `select`, `eventClick`, `eventDrop`, `eventResize`) are read-only and update automatically when user interactions occur. Use these as `Input` in Dash callbacks to respond to calendar events. --- ### Contributing Contributions to dash-fullcalendar are welcome! Please refer to the project's issues on GitHub for any feature requests or bug reports. ### License This project is licensed under the MIT License. --- *Source: /pip/full_calendar_component* *Generated with dash-improve-my-llms*