---
title: "React Timeline Multi-Select & Bulk Event Actions Example | Mobiscroll"
component: "Timeline"
framework: "React"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/react/timeline/event-bulk-actions-edit-delete-update"
---

# Multi-Select & Bulk Event Actions - React Timeline demo

Select multiple events on a React Timeline by keyboard shortcut or button, then apply bulk delete or update — a side panel tracks the selection. For React and Next.js.

- [Live demo](https://demo.mobiscroll.com/react/timeline/event-bulk-actions-edit-delete-update)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/eventcalendar/timeline/event-bulk-actions-edit-delete-update)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/eventcalendar/timeline/event-bulk-actions-edit-delete-update)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/timeline/event-bulk-actions-edit-delete-update.md), [jQuery](https://demo.mobiscroll.com/jquery/timeline/event-bulk-actions-edit-delete-update.md), [Angular](https://demo.mobiscroll.com/angular/timeline/event-bulk-actions-edit-delete-update.md), [Vue](https://demo.mobiscroll.com/vue/timeline/event-bulk-actions-edit-delete-update.md)

## Description

Multiple event selection can be turned on with the [`selectMultipleEvents`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-selectMultipleEvents) option. Selecting multiple events can be done through user interaction with `CTRL`/`SHIFT`/`CMD` + click or programmatically eg. click of a button or 'select all' checkbox.

Bulk operations like delete, update can be applied the selected events. Things like deleting with the `backspace` or `delete` buttons work out of the box but custom actions can be also applied. The selection can be easily retrieved and updated with the [`selectedEvents`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-selectedEvents) option and the [`onSelectedEventsChange`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onSelectedEventsChange) event.

Custom actions can be performed with external buttons or with context menu activated on right-click.

## What this demo shows

- A desktop weekly timeline where days are arranged horizontally and resources are listed as rows on the left.
- **Header navigation** The month and year label opens date navigation, while the previous and next arrows move between weeks, and the Today button returns to the current date.
- **Week view** The strip below the header shows the selected week from Sunday to Saturday, with the current date highlighted.
- **Time grid** The timeline displays hourly columns from 12 AM to 12 PM.
- **Resources** Multiple resources are shown as separate timeline rows.
- **Current time** A vertical blue line with a time label marks the current time.
- **Hover behavior** Hovering over the time grid shows a time indicator that follows the cursor in 15-minute increments.
- **Bulk action panel** A side panel next to the timeline contains `Select all from view`, `Reset selection`, and `Update selected` actions.
- **Select all from view** Clicking this action selects all events currently visible, highlights their titles in the week view, and shows a bottom-centered toast message: `All events selected from the view`.
- **Reset selection** Clicking this action clears the current selection and shows a bottom-centered toast message: `Selection cleared`.
- **Update selected** Clicking this action changes the color of all selected events to orange and shows a bottom-centered toast message: `All selected event's color changed to orange`.
- **Currently selected** The side panel includes a `Currently selected` section that is empty by default and lists the titles of all selected events when one or more events are selected.
- **Multi-selection** Users can select multiple events with `CTRL`/`SHIFT`/`CMD` + click.
- **Context menu** Right-clicking an event opens a small popup with `Update` and `Delete` actions for that event.
- **Event cards** Events appear as colored cards with a colored stripe on the left, the event title in bold, and the exact start and end time displayed below the title.
- **Variable event height** Timeline events grow to fit their custom event content instead of forcing every event into the same fixed height.
- **Resource row height** Resource rows adjust dynamically based on the height of the events they contain.
- **Date positioning** Events are positioned by their assigned resource and exact date range.
- **Event interaction** Events highlight on hover and show drag and resize handles for repositioning or changing duration.
- **Event selection** Clicking an event selects and highlights it.
- **Event creation** New events can be created by double-clicking the timeline or by clicking and dragging across a time range.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through time and resources.

## Best for

- **Bulk schedule updates** Selecting several events and applying the same change, such as updating the color or status of multiple scheduled items at once.
- **Resource planning workflows** Managing schedules where events are grouped by resource and users need to act on several assignments in the same timeline view.
- **Operational cleanup** Reviewing a week of visible events and clearing, updating, or deleting selected items without editing each event individually.
- **Multi-event review** Tracking the current selection in a side panel so users can confirm which events will be affected before running a bulk action.
- **Context-specific actions** Giving users quick access to update and delete actions from a right-click menu while still supporting toolbar-style bulk actions.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-themeVariant) - option
- [`getEvents`](https://mobiscroll.com/docs/react/eventcalendar/api#method-getEvents) - method
- [`clickToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-dragToResize) - option
- [`eventDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-eventDelete) - option
- [`view`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-view) - option
- [`data`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-resources) - option
- [`selectMultipleEvents`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-selectMultipleEvents) - option
- [`selectedEvents`](https://mobiscroll.com/docs/react/eventcalendar/api#opt-selectedEvents) - option
- [`onEventDelete`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventDelete) - event
- [`onEventUpdate`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventUpdate) - event
- [`onPageLoading`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onPageLoading) - event
- [`onSelectedEventsChange`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onSelectedEventsChange) - event
- [`onEventRightClick`](https://mobiscroll.com/docs/react/eventcalendar/api#event-onEventRightClick) - event
- [`inputProps`](https://mobiscroll.com/docs/react/select/api#opt-inputProps) - option
- [`display`](https://mobiscroll.com/docs/react/select/api#opt-display) - option
- [`touchUi`](https://mobiscroll.com/docs/react/select/api#opt-touchUi) - option
- [`anchor`](https://mobiscroll.com/docs/react/select/api#opt-anchor) - option
- [`data`](https://mobiscroll.com/docs/react/select/api#opt-data) - option
- [`isOpen`](https://mobiscroll.com/docs/react/select/api#opt-isOpen) - option
- [`onChange`](https://mobiscroll.com/docs/react/select/api#event-onChange) - event
- [`onClose`](https://mobiscroll.com/docs/react/select/api#event-onClose) - event
- [`onClose`](https://mobiscroll.com/docs/react/notifications/toast#event-onClose) - event
- [`onClose`](https://mobiscroll.com/docs/react/notifications/confirm#event-onClose) - event

## Code

### JSX

```jsx
import { Button, Confirm, Eventcalendar, formatDate, getJson, Page, Select, setOptions, Toast } from '@mobiscroll/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

const contextMenu = [
  {
    text: 'Update',
    value: 'update',
  },
  {
    text: 'Delete',
    value: 'delete',
  },
];

function App() {
  const [myEvents, setMyEvents] = useState([]);
  const [mySelectedEvents, setSelectedEvents] = useState([]);
  const [eventTitles, setEventTitles] = useState([]);

  const [firstDay, setFirstDay] = useState();
  const [lastDay, setLastDay] = useState();
  const [menuAnchor, setMenuAnchor] = useState();
  const [menuOpen, setMenuOpen] = useState(false);
  const [selectedValue, setSelected] = useState(false);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [toastMessage, setToastMessage] = useState('');
  const [isToastOpen, setToastOpen] = useState(false);
  const calRef = useRef();

  const myView = useMemo(() => ({ timeline: { type: 'week' } }), []);
  const myResources = useMemo(
    () => [
      {
        id: 1,
        name: 'Ryan',
        color: '#fdf500',
      },
      {
        id: 2,
        name: 'Kate',
        color: '#ff4600',
      },
      {
        id: 3,
        name: 'John',
        color: '#ff0101',
      },
      {
        id: 4,
        name: 'Mark',
        color: '#239a21',
      },
      {
        id: 5,
        name: 'Sharon',
        color: '#8f1ed6',
      },
      {
        id: 6,
        name: 'Ashley',
        color: '#01adff',
      },
    ],
    [],
  );

  const getSelectedEventTitles = useCallback((events) => {
    let titles = [];

    for (const event of events) {
      titles = [...titles, event.title];
    }
    return titles;
  }, []);

  const refreshSelectedEvents = useCallback(
    (events) => {
      setSelectedEvents(events);
      setEventTitles(getSelectedEventTitles(events));
    },
    [getSelectedEventTitles],
  );

  const deleteSelectedEvents = useCallback(() => {
    setConfirmOpen(true);
  }, []);

  const updateSelectedEvents = useCallback(() => {
    const events = mySelectedEvents.length === 0 ? [mySelectedEvents] : mySelectedEvents;
    let eventsToUpdate = [...myEvents];

    for (const event of events) {
      if (event.recurring) {
        const origEvent = event.original;
        const newEvent = {
          ...event,
          recurring: undefined,
          color: 'orange',
          id: event.id + '_' + formatDate('YYYY-MM-DD', event.start),
        };
        eventsToUpdate = [...eventsToUpdate, newEvent];
        const updatedOrigEvent = {
          ...origEvent,
          recurringException: [...(origEvent.recurringException || []), event.start],
        };

        // Update the event in the list
        const index = eventsToUpdate.findIndex((x) => x.id === updatedOrigEvent.id);
        eventsToUpdate.splice(index, 1, updatedOrigEvent);
      } else {
        const newEv = { ...event };
        newEv.color = 'orange';
        const index = eventsToUpdate.findIndex((x) => x.id === newEv.id);
        eventsToUpdate.splice(index, 1, newEv);
      }
    }
    setToastMessage("All selected event's color changed to orange");
    setToastOpen(true);
    setMyEvents(eventsToUpdate);
    refreshSelectedEvents([]);
  }, [myEvents, mySelectedEvents, refreshSelectedEvents]);

  const handleEventUpdate = useCallback(
    (args) => {
      if (args.isDelete) {
        if (!confirmOpen) {
          deleteSelectedEvents();
        }
        return false;
      }
    },
    [confirmOpen, deleteSelectedEvents],
  );

  const handleEventDelete = useCallback(() => {
    if (!confirmOpen) {
      deleteSelectedEvents();
      return false;
    }
  }, [confirmOpen, deleteSelectedEvents]);

  const handlePageLoading = useCallback((args) => {
    setTimeout(() => {
      setFirstDay(args.firstDay);
      setLastDay(args.lastDay);
    });
  }, []);

  const handleSelectedEventsChange = useCallback(
    (args) => {
      refreshSelectedEvents(args.events);
    },
    [refreshSelectedEvents],
  );

  const handleEventRightClick = useCallback((args) => {
    args.domEvent.preventDefault();
    setMenuAnchor(args.domEvent.target);
    setTimeout(() => {
      setMenuOpen(true);
    });
  }, []);

  const selectAllEvents = useCallback(() => {
    const selectedEvents = calRef.current.getEvents(firstDay, lastDay);
    refreshSelectedEvents(selectedEvents);
    setToastMessage('All events selected from view');
    setToastOpen(true);
  }, [firstDay, lastDay, refreshSelectedEvents]);

  const resetSelection = useCallback(() => {
    refreshSelectedEvents([]);
    setToastMessage('Selection cleared');
    setToastOpen(true);
  }, [refreshSelectedEvents]);

  const selectChange = useCallback(
    (args) => {
      setSelected(args.value);
      if (args.value === 'update') {
        updateSelectedEvents();
      } else if (args.value === 'delete') {
        deleteSelectedEvents();
      }
    },
    [deleteSelectedEvents, updateSelectedEvents],
  );

  const menuClose = useCallback(() => {
    setSelected('');
    setMenuOpen(false);
  }, []);

  const handleCloseToast = useCallback(() => setToastOpen(false), []);

  const handleConfirmClose = useCallback(
    (result) => {
      if (result) {
        let eventsToUpdate = [...myEvents];

        for (const event of mySelectedEvents) {
          if (event.recurring) {
            const origEvent = event.original;
            const updatedOrigEvent = {
              ...origEvent,
              recurringException: [...(origEvent.recurringException || []), event.start],
            };

            // Update the event in the list
            const index = eventsToUpdate.findIndex((x) => x.id === updatedOrigEvent.id);
            eventsToUpdate.splice(index, 1, updatedOrigEvent);
          } else {
            eventsToUpdate = eventsToUpdate.filter((ev) => ev.id !== event.id);
          }
        }

        setMyEvents(eventsToUpdate);
        refreshSelectedEvents([]);

        setToastMessage('Deleted');
        setToastOpen(true);
      }
      setConfirmOpen(false);
    },
    [myEvents, mySelectedEvents, refreshSelectedEvents],
  );

  useEffect(() => {
    getJson(
      'https://trial.mobiscroll.com/timeline-events/',
      (events) => {
        setMyEvents(events);
      },
      'jsonp',
    );
    document.querySelector('.md-bulk-operations').addEventListener('keydown', (ev) => {
      if (!confirmOpen && (ev.keyCode === 8 || ev.keyCode === 46)) {
        deleteSelectedEvents();
      }
    });
  }, [confirmOpen, deleteSelectedEvents]);

  return (
    <Page className="md-bulk-operations">
      <div className="mbsc-grid mbsc-no-padding">
        <div className="mbsc-row">
          <div className="mbsc-col-sm-9 mbsc-push-sm-3">
            <Eventcalendar
              className="md-bulk-operations-border"
              clickToCreate={true}
              dragToCreate={true}
              dragToMove={true}
              dragToResize={true}
              eventDelete={true}
              ref={calRef}
              view={myView}
              data={myEvents}
              resources={myResources}
              selectMultipleEvents={true}
              selectedEvents={mySelectedEvents}
              onEventDelete={handleEventDelete}
              onEventUpdate={handleEventUpdate}
              onPageLoading={handlePageLoading}
              onSelectedEventsChange={handleSelectedEventsChange}
              onEventRightClick={handleEventRightClick}
            />
            <Select
              inputProps={{ type: 'hidden' }}
              display="anchored"
              touchUi={false}
              anchor={menuAnchor}
              data={contextMenu}
              value={selectedValue}
              isOpen={menuOpen}
              onChange={selectChange}
              onClose={menuClose}
            />
          </div>
          <div className="mbsc-col-sm-3 mbsc-pull-sm-9">
            <div className="mbsc-form-group">
              <div className="mbsc-button-group-block">
                <Button onClick={selectAllEvents}>Select all this month</Button>
                <Button onClick={resetSelection}>Reset selection</Button>
                <Button onClick={updateSelectedEvents}>Update selected</Button>
              </div>
            </div>
            <div className="mbsc-form-group-title">Currently selected</div>
            <div className="mbsc-padding md-selected-event-list">
              <ul>
                {eventTitles.map((title, index) => (
                  <li key={index}>{title}</li>
                ))}
              </ul>
            </div>
          </div>
        </div>
      </div>
      <Toast message={toastMessage} isOpen={isToastOpen} onClose={handleCloseToast} />
      <Confirm
        isOpen={confirmOpen}
        title="Are you sure you want to delete the following events?"
        message={getSelectedEventTitles(mySelectedEvents).join(', ')}
        okText="Delete"
        onClose={handleConfirmClose}
      />
    </Page>
  );
}

export default App;
```

### TSX

```tsx
import {
  Button,
  Confirm,
  Eventcalendar,
  formatDate,
  getJson,
  MbscCalendarEvent,
  MbscEventcalendarView,
  MbscEventClickEvent,
  MbscEventUpdateEvent,
  MbscPageLoadingEvent,
  MbscResource,
  MbscSelectChangeEvent,
  MbscSelectedEventsChangeEvent,
  Page,
  Select,
  setOptions,
  Toast,
} from '@mobiscroll/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

const contextMenu = [
  {
    text: 'Update',
    value: 'update',
  },
  {
    text: 'Delete',
    value: 'delete',
  },
];

function App() {
  const [myEvents, setMyEvents] = useState<MbscCalendarEvent[]>([]);
  const [mySelectedEvents, setSelectedEvents] = useState<MbscCalendarEvent[]>([]);
  const [eventTitles, setEventTitles] = useState<string[]>([]);
  const [firstDay, setFirstDay] = useState<Date>();
  const [lastDay, setLastDay] = useState<Date>();
  const [menuAnchor, setMenuAnchor] = useState<HTMLElement>();
  const [menuOpen, setMenuOpen] = useState<boolean>(false);
  const [selectedValue, setSelected] = useState<string>();
  const [confirmOpen, setConfirmOpen] = useState<boolean>(false);
  const [toastMessage, setToastMessage] = useState<string>('');
  const [isToastOpen, setToastOpen] = useState<boolean>(false);
  const calRef = useRef<Eventcalendar>(null);

  const myView = useMemo<MbscEventcalendarView>(() => ({ timeline: { type: 'week' } }), []);
  const myResources = useMemo<MbscResource[]>(
    () => [
      {
        id: 1,
        name: 'Ryan',
        color: '#fdf500',
      },
      {
        id: 2,
        name: 'Kate',
        color: '#ff4600',
      },
      {
        id: 3,
        name: 'John',
        color: '#ff0101',
      },
      {
        id: 4,
        name: 'Mark',
        color: '#239a21',
      },
      {
        id: 5,
        name: 'Sharon',
        color: '#8f1ed6',
      },
      {
        id: 6,
        name: 'Ashley',
        color: '#01adff',
      },
    ],
    [],
  );

  const getSelectedEventTitles = useCallback((events: MbscCalendarEvent[]) => {
    let titles: Array<string> = [];

    for (const event of events) {
      titles = [...titles, event.title!];
    }
    return titles;
  }, []);

  const refreshSelectedEvents = useCallback(
    (events: MbscCalendarEvent[]) => {
      setSelectedEvents(events);
      setEventTitles(getSelectedEventTitles(events));
    },
    [getSelectedEventTitles],
  );

  const deleteSelectedEvents = useCallback(() => {
    setConfirmOpen(true);
  }, []);

  const updateSelectedEvents = useCallback(() => {
    const events = mySelectedEvents;
    let eventsToUpdate = [...myEvents];

    for (const event of events) {
      if (event.recurring) {
        const origEvent = event.original!;
        const newEvent = {
          ...event,
          recurring: undefined,
          color: 'orange',
          id: event.id + '_' + formatDate('YYYY-MM-DD', new Date(event.start as string)),
        };
        eventsToUpdate = [...eventsToUpdate, newEvent];
        const updatedOrigEvent = {
          ...origEvent,
          recurringException: [...(((origEvent.recurringException as Array<string>) || [])), event.start as string],
        };

        // Update the event in the list
        const index = eventsToUpdate.findIndex((x) => x.id === origEvent.id);
        eventsToUpdate.splice(index, 1, updatedOrigEvent);
      } else {
        const newEv = { ...event };
        newEv.color = 'orange';
        const index = eventsToUpdate.findIndex((x) => x.id === newEv.id);
        eventsToUpdate.splice(index, 1, newEv);
      }
    }
    setToastMessage("All selected event's color changed to orange");
    setToastOpen(true);

    setMyEvents(eventsToUpdate);
    refreshSelectedEvents([]);
  }, [myEvents, mySelectedEvents, refreshSelectedEvents]);

  const handleEventUpdate = useCallback(
    (args: MbscEventUpdateEvent) => {
      if (args.isDelete) {
        if (!confirmOpen) {
          deleteSelectedEvents();
        }
        return false;
      }
    },
    [confirmOpen, deleteSelectedEvents],
  );

  const handleEventDelete = useCallback(() => {
    if (!confirmOpen) {
      deleteSelectedEvents();
      return false;
    }
  }, [confirmOpen, deleteSelectedEvents]);

  const handlePageLoading = useCallback((args: MbscPageLoadingEvent) => {
    setTimeout(() => {
      setFirstDay(args.firstDay);
      setLastDay(args.lastDay);
    });
  }, []);

  const handleSelectedEventsChange = useCallback(
    (args: MbscSelectedEventsChangeEvent) => {
      refreshSelectedEvents(args.events);
    },
    [refreshSelectedEvents],
  );

  const handleEventRightClick = useCallback((args: MbscEventClickEvent) => {
    args.domEvent.preventDefault();
    setMenuAnchor(args.domEvent.target);
    setTimeout(() => {
      setMenuOpen(true);
    });
  }, []);

  const selectAllEvents = useCallback(() => {
    const selectedEvents = calRef.current!.getEvents(firstDay, lastDay);
    refreshSelectedEvents(selectedEvents);
    setToastMessage('All events selected from view');
    setToastOpen(true);
  }, [firstDay, lastDay, refreshSelectedEvents]);

  const resetSelection = useCallback(() => {
    refreshSelectedEvents([]);
    setToastMessage('Selection cleared');
    setToastOpen(true);
  }, [refreshSelectedEvents]);

  const selectChange = useCallback(
    (args: MbscSelectChangeEvent) => {
      setSelected(args.value);
      if (args.value === 'update') {
        updateSelectedEvents();
      } else if (args.value === 'delete') {
        deleteSelectedEvents();
      }
    },
    [deleteSelectedEvents, updateSelectedEvents],
  );

  const menuClose = useCallback(() => {
    setSelected('');
    setMenuOpen(false);
  }, []);

  const handleCloseToast = useCallback(() => setToastOpen(false), []);

  const handleConfirmClose = useCallback(
    (result: boolean) => {
      if (result) {
        let eventsToUpdate = [...myEvents];

        for (const event of mySelectedEvents) {
          if (event.recurring) {
            const origEvent = event.original!;
            const updatedOrigEvent = {
              ...origEvent,
              recurringException: [...(((origEvent.recurringException as Array<string>) || [])), event.start as string],
            };

            // Update the event in the list
            const index = eventsToUpdate.findIndex((x) => x.id === origEvent.id);
            eventsToUpdate.splice(index, 1, updatedOrigEvent);
          } else {
            eventsToUpdate = eventsToUpdate.filter((ev) => ev.id !== event.id);
          }
        }

        setMyEvents(eventsToUpdate);
        refreshSelectedEvents([]);

        setToastMessage('Deleted');
        setToastOpen(true);
      }
      setConfirmOpen(false);
    },
    [myEvents, mySelectedEvents, refreshSelectedEvents],
  );

  const handleDeleteKey = useCallback(
    (ev: { keyCode: number }) => {
      if (!confirmOpen && (ev.keyCode === 8 || ev.keyCode === 46)) {
        deleteSelectedEvents();
      }
    },
    [confirmOpen, deleteSelectedEvents],
  );

  useEffect(() => {
    getJson(
      'https://trial.mobiscroll.com/timeline-events/',
      (events) => {
        setMyEvents(events);
      },
      'jsonp',
    );
  }, []);

  return (
    <Page className="md-bulk-operations">
      <div className="mbsc-grid mbsc-no-padding" onKeyDown={handleDeleteKey}>
        <div className="mbsc-row">
          <div className="mbsc-col-sm-9 mbsc-push-sm-3">
            <Eventcalendar
              className="md-bulk-operations-border"
              ref={calRef}
              view={myView}
              data={myEvents}
              resources={myResources}
              clickToCreate={true}
              selectMultipleEvents={true}
              selectedEvents={mySelectedEvents}
              onEventDelete={handleEventDelete}
              onPageLoading={handlePageLoading}
              onEventUpdate={handleEventUpdate}
              onSelectedEventsChange={handleSelectedEventsChange}
              onEventRightClick={handleEventRightClick}
            />
            <Select
              inputProps={{ type: 'hidden' }}
              display="anchored"
              touchUi={false}
              anchor={menuAnchor}
              data={contextMenu}
              value={selectedValue}
              isOpen={menuOpen}
              onChange={selectChange}
              onClose={menuClose}
            />
          </div>
          <div className="mbsc-col-sm-3 mbsc-pull-sm-9">
            <div className="mbsc-form-group">
              <div className="mbsc-button-group-block">
                <Button onClick={selectAllEvents}>Select all this month</Button>
                <Button onClick={resetSelection}>Reset selection</Button>
                <Button onClick={updateSelectedEvents}>Update selected</Button>
              </div>
            </div>
            <div className="mbsc-form-group-title">Currently selected</div>
            <div className="mbsc-padding md-selected-event-list">
              <ul>
                {eventTitles.map((title, index) => (
                  <li key={index}>{title}</li>
                ))}
              </ul>
            </div>
          </div>
        </div>
      </div>
      <Toast message={toastMessage} isOpen={isToastOpen} onClose={handleCloseToast} />
      <Confirm
        isOpen={confirmOpen}
        title="Are you sure you want to delete the following events?"
        message={getSelectedEventTitles(mySelectedEvents).join(', ')}
        okText="Delete"
        onClose={handleConfirmClose}
      />
    </Page>
  );
}

export default App;
```

### CSS

```css
.md-bulk-operations-border {
  border-left: 1px solid #ccc;
}

.md-selected-event-list {
  height: 500px;
  overflow: auto;
  padding-top: 0;
  padding-bottom: 0;
}
```

## Implementation notes

- Use `timeline: { type: 'week' }` - a weekly view.
- Set `selectMultipleEvents: true` to enable Ctrl/Shift/Cmd+click multi-selection.
- Define 6 named resources (Ryan, Kate, John, Mark, Sharon, Ashley) each with `id`, `name`, and `color`.
- Load events asynchronously from `https://trial.mobiscroll.com/timeline-events/` via JSONP; set them on the calendar after load.
- Use a two-column layout: the calendar occupies 9/12 columns (pushed right), the sidebar 3/12 (pulled left). The sidebar contains three action buttons ("Select all from view", "Reset selection", "Update selected") and a scrollable list of currently selected event titles.
- Track `firstDay` and `lastDay` via `onPageLoading` (`args.firstDay` / `args.lastDay`). These are used by "Select all from view".
- `onSelectedEventsChange` → update the current selection and refresh the displayed titles list. React/Angular/Vue: the selection is tracked via the bound `selectedEvents` prop/binding (two-way); this handler just mirrors it into local state for the titles list.
- **"Select all from view"** → call `calendar.getEvents(firstDay, lastDay)` to get the visible events, then update the selection and the titles list. React/Angular/Vue: assign the result to the state/binding feeding the `selectedEvents` prop. JS/jQuery: pass the result to `calendar.setSelectedEvents()`. Shows toast "All events selected from view".
- **"Reset selection"** → clear the selection and the titles list. React/Angular/Vue: set the `selectedEvents` state/binding to `[]`. JS/jQuery: call `calendar.setSelectedEvents([])`. Shows toast "Selection cleared".
- **"Update selected"** → `updateSelectedEvents()`: get the currently selected events - React/Angular/Vue: read them from the bound `selectedEvents` state/binding; JS/jQuery: call `calendar.getSelectedEvents()` - then iterate and change color to orange. For **recurring occurrences**: detach the occurrence - set `recurring = undefined`, assign a new `id` by appending `_` + `formatDate('YYYY-MM-DD', event.start)`, add the occurrence's start to `origEvent.recurringException`. React/Angular/Vue: replace `origEvent` in place and append `newEvent` in the array bound to `data`. JS/jQuery: call `calendar.updateEvent(origEvent)` and `calendar.addEvent(newEvent)`. For **non-recurring**: set `color = 'orange'`. React/Angular/Vue: replace the event in place in the array bound to `data`. JS/jQuery: call `calendar.updateEvent(event)`. After: clear the selection (React/Angular/Vue: set the `selectedEvents` state/binding to `[]`; JS/jQuery: call `calendar.setSelectedEvents([])`), show toast "All selected event's color changed to orange".
- **`deleteSelectedEvents()`** → set `confirmOpen = true`, show a confirm dialog ("Are you sure you want to delete the following events?") with the selected event titles joined as the message and "Delete" as the ok button. On confirm: for **recurring** events, push `event.start` into `origEvent.recurringException` - React/Angular/Vue: replace `origEvent` in place in the array bound to `data`; JS/jQuery: call `calendar.updateEvent(origEvent)`. For **non-recurring**, collect them, then remove them - React/Angular/Vue: filter `eventsToDelete` out of the array bound to `data`; JS/jQuery: call `calendar.removeEvent(eventsToDelete)`. After: clear the selection (React/Angular/Vue: set the `selectedEvents` state/binding to `[]`; JS/jQuery: call `calendar.setSelectedEvents([])`), show toast "Deleted", set `confirmOpen = false`.
- Three entry points trigger `deleteSelectedEvents()`: (1) **`onEventDelete`** - fired by the Delete/Backspace key; use the `confirmOpen` flag to prevent double-triggering, then `return false`; (2) **`onEventUpdate` with `args.isDelete === true`** - fired when Mobiscroll attempts a keyboard delete on a multi-selection; same guard and `return false`; (3) **context menu "Delete" option**.
- **Right-click context menu**: `onEventRightClick` → store `selectedEvent = args.event` as a fallback (used if no events are selected), `args.domEvent.preventDefault()`, set the menu anchor to `args.domEvent.target`, open an anchored `Select` popup (`display: 'anchored'`, `touchUi: false`, no buttons) with two options ("Update" / "Delete"). On selection dispatch to `updateSelectedEvents()` or `deleteSelectedEvents()`; `updateSelectedEvents` / `deleteSelectedEvents` fall back to `[selectedEvent]` when the current selection is empty (React/Angular/Vue: the bound `selectedEvents` state/binding; JS/jQuery: `getSelectedEvents()`). On menu close, clear the selected value.
- **Keyboard delete**: attach a `keydown` listener on the `.md-bulk-operations` container; if Delete or Backspace is pressed and no confirm dialog is open, call `deleteSelectedEvents()`.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-react.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config react
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config react --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/react/getting-started/installation

## Related demos

- Previous: [Block Past Event Creation](https://demo.mobiscroll.com/react/timeline/disallow-past-event-creation.md) - Keep past dates read-only on a React resource timeline — block creation, moving, and resizing, and show a toast for blocked actions.
