diff --git a/apps/client/src/features/rundown/Rundown.tsx b/apps/client/src/features/rundown/Rundown.tsx
index c3f0eee53..04745544d 100644
--- a/apps/client/src/features/rundown/Rundown.tsx
+++ b/apps/client/src/features/rundown/Rundown.tsx
@@ -2,8 +2,15 @@ import { Fragment, lazy, useCallback, useEffect, useRef, useState } from 'react'
import { closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useHotkeys } from '@mantine/hooks';
-import { isOntimeEvent, MaybeNumber, Playback, RundownCached, SupportedEvent } from 'ontime-types';
-import { getFirstNormal, getLastNormal, getNextNormal, getPreviousNormal } from 'ontime-utils';
+import { isOntimeBlock, isOntimeEvent, MaybeNumber, Playback, RundownCached, SupportedEvent } from 'ontime-types';
+import {
+ getFirstNormal,
+ getLastNormal,
+ getNextBlockNormal,
+ getNextNormal,
+ getPreviousBlockNormal,
+ getPreviousNormal,
+} from 'ontime-utils';
import { useEventAction } from '../../common/hooks/useEventAction';
import useFollowComponent from '../../common/hooks/useFollowComponent';
@@ -98,28 +105,61 @@ export default function Rundown({ data }: RundownProps) {
[rundown, order, addEvent],
);
+ const selectBlock = useCallback(
+ (cursor: string | null, direction: 'up' | 'down') => {
+ if (order.length < 1) {
+ return;
+ }
+ let newCursor = cursor;
+ if (cursor === null) {
+ // there is no cursor, we select the first or last depending on direction
+ const selected = direction === 'up' ? getLastNormal(rundown, order) : getFirstNormal(rundown, order);
+
+ if (isOntimeBlock(selected)) {
+ setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
+ return;
+ }
+ newCursor = selected?.id ?? null;
+ }
+
+ if (newCursor === null) {
+ return;
+ }
+
+ // otherwise we select the next or previous
+ const selected =
+ direction === 'up'
+ ? getPreviousBlockNormal(rundown, order, newCursor)
+ : getNextBlockNormal(rundown, order, newCursor);
+
+ if (selected.entry !== null && selected.index !== null) {
+ setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
+ }
+ },
+ [order, rundown, setSelectedEvents],
+ );
+
const selectEntry = useCallback(
(cursor: string | null, direction: 'up' | 'down') => {
if (order.length < 1) {
return;
}
- let newCursor: string | null;
- let newIndex: number | null;
+
if (cursor === null) {
// there is no cursor, we select the first or last depending on direction if it exists
- newCursor =
- (direction === 'up' ? getLastNormal(rundown, order)?.id : getFirstNormal(rundown, order)?.id) ?? null;
- newIndex = direction === 'up' ? order.length : 0;
- } else {
- // otherwise we select the next or previous
- const selected =
- direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
- newCursor = selected.entry?.id ?? null;
- newIndex = selected.index;
+ const selected = direction === 'up' ? getLastNormal(rundown, order) : getFirstNormal(rundown, order);
+ if (selected !== null) {
+ setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
+ }
+ return;
}
- if (newCursor && newIndex !== null) {
- setSelectedEvents({ id: newCursor, selectMode: 'click', index: newIndex });
+ // otherwise we select the next or previous
+ const selected =
+ direction === 'up' ? getPreviousNormal(rundown, order, cursor) : getNextNormal(rundown, order, cursor);
+
+ if (selected.entry !== null && selected.index !== null) {
+ setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
}
},
[order, rundown, setSelectedEvents],
@@ -145,6 +185,10 @@ export default function Rundown({ data }: RundownProps) {
useHotkeys([
['alt + ArrowDown', () => selectEntry(cursor, 'down'), { preventDefault: true }],
['alt + ArrowUp', () => selectEntry(cursor, 'up'), { preventDefault: true }],
+
+ ['alt + shift + ArrowDown', () => selectBlock(cursor, 'down'), { preventDefault: true }],
+ ['alt + shift + ArrowUp', () => selectBlock(cursor, 'up'), { preventDefault: true }],
+
['alt + mod + ArrowDown', () => moveEntry(cursor, 'down'), { preventDefault: true }],
['alt + mod + ArrowUp', () => moveEntry(cursor, 'up'), { preventDefault: true }],
diff --git a/apps/client/src/features/rundown/event-editor/EventEditorEmpty.module.scss b/apps/client/src/features/rundown/event-editor/EventEditorEmpty.module.scss
index 4fdcb8fb2..9f443ccc9 100644
--- a/apps/client/src/features/rundown/event-editor/EventEditorEmpty.module.scss
+++ b/apps/client/src/features/rundown/event-editor/EventEditorEmpty.module.scss
@@ -28,6 +28,7 @@
}
td:nth-child(even) {
text-align: right;
+ white-space: nowrap;
}
}
}
diff --git a/apps/client/src/features/rundown/event-editor/EventEditorEmpty.tsx b/apps/client/src/features/rundown/event-editor/EventEditorEmpty.tsx
index c6dbc762b..7905f278e 100644
--- a/apps/client/src/features/rundown/event-editor/EventEditorEmpty.tsx
+++ b/apps/client/src/features/rundown/event-editor/EventEditorEmpty.tsx
@@ -24,6 +24,18 @@ function EventEditorEmpty() {
↓
+
+ | Select block |
+
+ {deviceAlt}
+ +
+ Shift
+ +
+ ↑
+ /
+ ↓
+ |
+
| Deselect entry |
diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts
index 5eba5af9a..dce23f606 100644
--- a/apps/server/src/stores/runtimeState.ts
+++ b/apps/server/src/stores/runtimeState.ts
@@ -11,7 +11,7 @@ import {
TimerPhase,
TimerState,
} from 'ontime-types';
-import { calculateDuration, checkIsNow, dayInMs, filterTimedEvents, getRelevantBlock } from 'ontime-utils';
+import { calculateDuration, checkIsNow, dayInMs, filterTimedEvents, getPreviousBlock } from 'ontime-utils';
import { clock } from '../services/Clock.js';
import { RestorePoint } from '../services/RestoreService.js';
@@ -664,7 +664,7 @@ function loadBlock(rundown: OntimeRundown) {
return;
}
- const newCurrentBlock = getRelevantBlock(rundown, runtimeState.eventNow.id);
+ const newCurrentBlock = getPreviousBlock(rundown, runtimeState.eventNow.id);
// update time only if the block has changed
if (newCurrentBlock === null || newCurrentBlock.id !== runtimeState.currentBlock.block?.id) {
diff --git a/packages/utils/index.ts b/packages/utils/index.ts
index 623666d6b..faaf9b656 100644
--- a/packages/utils/index.ts
+++ b/packages/utils/index.ts
@@ -8,9 +8,9 @@ export { sanitiseCue } from './src/cue-utils/cueUtils.js';
export { getCueCandidate } from './src/cue-utils/cueUtils.js';
export { generateId } from './src/generate-id/generateId.js';
export {
- getEventWithId,
filterPlayable,
filterTimedEvents,
+ getEventWithId,
getFirst,
getFirstEvent,
getFirstEventNormal,
@@ -19,6 +19,7 @@ export {
getLastEventNormal,
getLastNormal,
getNext,
+ getNextBlockNormal,
getNextEvent,
getNextEventNormal,
getNextNormal,
@@ -26,7 +27,8 @@ export {
getPreviousEvent,
getPreviousEventNormal,
getPreviousNormal,
- getRelevantBlock,
+ getPreviousBlock,
+ getPreviousBlockNormal,
swapEventData,
} from './src/rundown-utils/rundownUtils.js';
diff --git a/packages/utils/src/rundown-utils/rundownUtils.test.ts b/packages/utils/src/rundown-utils/rundownUtils.test.ts
index c8ee9e29c..bafa7f52b 100644
--- a/packages/utils/src/rundown-utils/rundownUtils.test.ts
+++ b/packages/utils/src/rundown-utils/rundownUtils.test.ts
@@ -8,8 +8,8 @@ import {
getNext,
getNextEvent,
getPrevious,
+ getPreviousBlock,
getPreviousEvent,
- getRelevantBlock,
swapEventData,
} from './rundownUtils';
@@ -278,23 +278,23 @@ describe('getLastEvent', () => {
];
it('returns the relevant block', () => {
- const block = getRelevantBlock(testRundown as unknown as OntimeRundown, 'h');
+ const block = getPreviousBlock(testRundown as unknown as OntimeRundown, 'h');
expect(block?.id).toBe('g');
});
it('returns the relevant block', () => {
- const block = getRelevantBlock(testRundown as unknown as OntimeRundown, 'f');
+ const block = getPreviousBlock(testRundown as unknown as OntimeRundown, 'f');
expect(block?.id).toBe('e');
});
it('returns the relevant block', () => {
- const block = getRelevantBlock(testRundown as unknown as OntimeRundown, 'a');
+ const block = getPreviousBlock(testRundown as unknown as OntimeRundown, 'a');
expect(block).toBeNull();
});
it('also works on index 0', () => {
testRundown.unshift({ id: '0', type: SupportedEvent.Block });
- const block = getRelevantBlock(testRundown as unknown as OntimeRundown, 'a');
+ const block = getPreviousBlock(testRundown as unknown as OntimeRundown, 'a');
expect(block?.id).toBe('0');
});
});
diff --git a/packages/utils/src/rundown-utils/rundownUtils.ts b/packages/utils/src/rundown-utils/rundownUtils.ts
index 8dc4b7056..7b33b01ae 100644
--- a/packages/utils/src/rundown-utils/rundownUtils.ts
+++ b/packages/utils/src/rundown-utils/rundownUtils.ts
@@ -302,7 +302,53 @@ export function getEventWithId(rundown: OntimeRundown, id: string): OntimeRundow
/**
* Gets relevant block element for a given ID
*/
-export function getRelevantBlock(rundown: OntimeRundown, currentId: string): OntimeBlock | null {
+export function getPreviousBlockNormal(rundown: NormalisedRundown, order: string[], currentId: string): IndexAndEntry {
+ let foundCurrentEvent = false;
+ // Iterate backwards through the rundown to find the current event
+ for (let index = order.length - 1; index >= 0; index--) {
+ const id = order[index];
+ if (!foundCurrentEvent && id === currentId) {
+ // set the flag when the current event is found
+ foundCurrentEvent = true;
+ continue;
+ }
+ // the first block before the current event is the relevant one
+ const entry = rundown[id];
+ if (foundCurrentEvent && isOntimeBlock(entry)) {
+ return { entry, index };
+ }
+ }
+ // no blocks exist before current event
+ return { entry: null, index: null };
+}
+
+/**
+ * Gets next block element for a given ID
+ */
+export function getNextBlockNormal(rundown: NormalisedRundown, order: string[], currentId: string): IndexAndEntry {
+ let foundCurrentEvent = false;
+ // Iterate backwards through the rundown to find the current event
+ for (let index = 0; index < order.length; index++) {
+ const id = order[index];
+ if (!foundCurrentEvent && id === currentId) {
+ // set the flag when the current event is found
+ foundCurrentEvent = true;
+ continue;
+ }
+ // the first block before the current event is the relevant one
+ const entry = rundown[id];
+ if (foundCurrentEvent && isOntimeBlock(entry)) {
+ return { entry, index };
+ }
+ }
+ // no blocks exist before current event
+ return { entry: null, index: null };
+}
+
+/**
+ * Gets relevant block element for a given ID
+ */
+export function getPreviousBlock(rundown: OntimeRundown, currentId: string): OntimeBlock | null {
let foundCurrentEvent = false;
// Iterate backwards through the rundown to find the current event
for (let i = rundown.length - 1; i >= 0; i--) {
|