import { EntryId, MaybeNumber, ProjectRundownsList, Rundown, isOntimeEvent } from 'ontime-types'; import { MouseEvent } from 'react'; import { create } from 'zustand'; import { PROJECT_RUNDOWNS, getRundownQueryKey } from '../../common/api/constants'; import { ontimeQueryClient } from '../../common/queryClient'; import { isMacOS } from '../../common/utils/deviceUtils'; export type SelectionMode = 'shift' | 'click' | 'ctrl'; interface EventSelectionStore { selectedEvents: Set; anchoredIndex: MaybeNumber; cursor: EntryId | null; entryMode: 'event' | 'single' | null; scrollHandler: ((id: EntryId) => void) | null; setSingleEntrySelection: (selectionArgs: { id: EntryId }) => void; setSelectedEvents: (selectionArgs: { id: EntryId; index: number; selectMode: SelectionMode }) => void; clearSelectedEvents: () => void; clearMultiSelect: () => void; unselect: (id: EntryId) => void; setScrollHandler: (handler: ((id: EntryId) => void) | null) => void; scrollToEntry: (id: EntryId) => void; } /** * Keeps track of the selected entries and selection mode * Provides methods to update the selection based on user interactions */ export const useEventSelection = create()((set, get) => ({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null, scrollHandler: null, setSingleEntrySelection: ({ id }) => { set({ selectedEvents: new Set([id]), anchoredIndex: null, cursor: id, entryMode: 'single' }); }, setSelectedEvents: ({ id, index, selectMode }) => { const { selectedEvents, anchoredIndex, entryMode } = get(); // if we are in single mode, we replace the selection and change the mode if (entryMode === 'single') { return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' }); } // on click, we replace selection with event if (selectMode === 'click') { return set({ selectedEvents: new Set([id]), anchoredIndex: index, cursor: id, entryMode: 'event' }); } // on ctrl + click, we toggle the selection of that event if (selectMode === 'ctrl') { const rundownData = getLoadedRundownData(); if (!rundownData) return; // if it doesnt exist, simply add to the list and set an anchor if (!selectedEvents.has(id)) { return set({ selectedEvents: selectedEvents.add(id), anchoredIndex: index, cursor: id, entryMode: 'event', }); } // if event is already selected, we remove it from selection // and set the anchor to the event after selectedEvents.delete(id); const nextIndex = rundownData.order.findIndex( (eventId, i) => i > index && isOntimeEvent(rundownData.entries[eventId]) && selectedEvents.has(eventId), ); // if we didnt find anything after, set the anchor to the last event return set({ selectedEvents, anchoredIndex: nextIndex < 0 ? rundownData.order.length - 1 : nextIndex, entryMode: 'event', }); } // on shift + click, we select a range of events up to the clicked event if (selectMode === 'shift') { const rundownData = getLoadedRundownData(); if (!rundownData) return; // get list of rundown with only ontime events const eventIds: EntryId[] = []; rundownData.flatOrder.forEach((eventId) => { const event = rundownData.entries[eventId]; if (isOntimeEvent(event)) { eventIds.push(event.id); } }); const start = anchoredIndex === null ? 0 : Math.min(anchoredIndex, index); const end = anchoredIndex === null ? index : Math.max(anchoredIndex, index + 1); // create new set with range of ids from start to end const selectedEventIds = eventIds.slice(start, end); return set({ selectedEvents: new Set([...selectedEvents, ...selectedEventIds]), anchoredIndex: index, entryMode: 'event', }); } }, clearSelectedEvents: () => set({ selectedEvents: new Set(), anchoredIndex: null, cursor: null, entryMode: null }), clearMultiSelect: () => { const { selectedEvents } = get(); const [firstSelected] = selectedEvents; set({ selectedEvents: new Set(firstSelected ? [firstSelected] : []), anchoredIndex: null, entryMode: null, }); }, unselect: (id: string) => { const { entryMode, selectedEvents } = get(); selectedEvents.delete(id); set({ selectedEvents, entryMode: selectedEvents.size === 0 ? null : entryMode, }); }, // Sets the scroll handler for programmatic scrolling to entries setScrollHandler: (handler) => set({ scrollHandler: handler }), // Scrolls to the specified entry using the registered scroll handler scrollToEntry: (id: EntryId) => { const handler = get().scrollHandler; if (handler) { handler(id); } }, })); function getLoadedRundownData() { const rundownId = ontimeQueryClient.getQueryData(PROJECT_RUNDOWNS)?.loaded; if (!rundownId) return undefined; return ontimeQueryClient.getQueryData(getRundownQueryKey(rundownId)); } export function getSelectionMode(event: MouseEvent): SelectionMode { if ((isMacOS() && event.metaKey) || event.ctrlKey) { return 'ctrl'; } if (event.shiftKey) { return 'shift'; } return 'click'; }