mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 21:03:29 +00:00
feat: extend rundown shortcuts
This commit is contained in:
committed by
Carlos Valente
parent
d7af73d9ed
commit
967e92e2c8
@@ -13,6 +13,7 @@ import type { useEntryActions } from '../../../common/hooks/useEntryAction';
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
|
||||
type SelectionMode = 'shift' | 'click' | 'ctrl';
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
interface UseRundownCommandsOptions {
|
||||
entries: Rundown['entries'];
|
||||
@@ -22,6 +23,9 @@ interface UseRundownCommandsOptions {
|
||||
handleCollapseGroup: (collapsed: boolean, groupId: EntryId) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common operations for the rundown lists
|
||||
*/
|
||||
export function useRundownCommands({
|
||||
entries,
|
||||
order,
|
||||
@@ -29,7 +33,8 @@ export function useRundownCommands({
|
||||
setSelectedEvents,
|
||||
handleCollapseGroup,
|
||||
}: UseRundownCommandsOptions) {
|
||||
const { addEntry, clone, deleteEntry, move } = entryActions;
|
||||
const { addEntry, clone, deleteEntry, move, reorderEntry } = entryActions;
|
||||
|
||||
const deleteAtCursor = useCallback(
|
||||
(cursor: string | null) => {
|
||||
if (!cursor) return;
|
||||
@@ -45,7 +50,7 @@ export function useRundownCommands({
|
||||
const insertCopyAtId = useCallback(
|
||||
(atId: EntryId | null, above = false) => {
|
||||
// lazily get the value from the store
|
||||
const { entryCopyId } = useEntryCopy.getState();
|
||||
const { entryCopyId, entryCopyMode, setEntryCopyId } = useEntryCopy.getState();
|
||||
if (entryCopyId === null || !entries[entryCopyId]) {
|
||||
// we cant clone without selection
|
||||
return;
|
||||
@@ -60,13 +65,34 @@ export function useRundownCommands({
|
||||
normalisedAtId = refElement.parent;
|
||||
}
|
||||
|
||||
if (entryCopyMode === 'cut') {
|
||||
if (!normalisedAtId) {
|
||||
const firstId = order[0];
|
||||
if (!firstId || firstId === entryCopyId) {
|
||||
return;
|
||||
}
|
||||
reorderEntry(entryCopyId, firstId, 'before')
|
||||
.then(() => setEntryCopyId(null))
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (normalisedAtId === entryCopyId) {
|
||||
return;
|
||||
}
|
||||
const placement = above ? 'before' : 'after';
|
||||
reorderEntry(entryCopyId, normalisedAtId, placement)
|
||||
.then(() => setEntryCopyId(null))
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
clone(entryCopyId, {
|
||||
after: above ? undefined : normalisedAtId ?? undefined,
|
||||
// if we don't have a cursor add the new event on top
|
||||
before: above ? normalisedAtId ?? undefined : undefined,
|
||||
});
|
||||
},
|
||||
[entries, clone],
|
||||
[entries, order, clone, reorderEntry],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -86,7 +112,7 @@ export function useRundownCommands({
|
||||
const selectGroup = useCallback(
|
||||
(cursor: EntryId | null, direction: 'up' | 'down') => {
|
||||
if (order.length < 1) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
let newCursor = cursor;
|
||||
if (cursor === null) {
|
||||
@@ -95,13 +121,13 @@ export function useRundownCommands({
|
||||
|
||||
if (isOntimeGroup(selected)) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||
return;
|
||||
return selected.id;
|
||||
}
|
||||
newCursor = selected?.id ?? null;
|
||||
}
|
||||
|
||||
if (newCursor === null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// otherwise we select the next or previous
|
||||
@@ -112,7 +138,9 @@ export function useRundownCommands({
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
return selected.entry.id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[order, entries, setSelectedEvents],
|
||||
);
|
||||
@@ -123,7 +151,7 @@ export function useRundownCommands({
|
||||
const selectEntry = useCallback(
|
||||
(cursor: EntryId | null, direction: 'up' | 'down') => {
|
||||
if (order.length < 1) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cursor === null) {
|
||||
@@ -131,8 +159,9 @@ export function useRundownCommands({
|
||||
const selected = direction === 'up' ? getLastNormal(entries, order) : getFirstNormal(entries, order);
|
||||
if (selected !== null) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index: direction === 'up' ? order.length : 0 });
|
||||
return selected.id;
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// otherwise we select the next or previous
|
||||
@@ -141,7 +170,9 @@ export function useRundownCommands({
|
||||
|
||||
if (selected.entry !== null && selected.index !== null) {
|
||||
setSelectedEvents({ id: selected.entry.id, selectMode: 'click', index: selected.index });
|
||||
return selected.entry.id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[order, entries, setSelectedEvents],
|
||||
);
|
||||
@@ -161,12 +192,90 @@ export function useRundownCommands({
|
||||
[handleCollapseGroup, move],
|
||||
);
|
||||
|
||||
const cloneEntry = useCallback(
|
||||
(cursor: EntryId | null) => {
|
||||
if (!cursor) {
|
||||
return;
|
||||
}
|
||||
clone(cursor, { after: cursor });
|
||||
},
|
||||
[clone],
|
||||
);
|
||||
|
||||
const selectEdge = useCallback(
|
||||
(direction: 'top' | 'bottom') => {
|
||||
if (order.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selected = direction === 'top' ? getFirstNormal(entries, order) : getLastNormal(entries, order);
|
||||
if (!selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const index = order.indexOf(selected.id);
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index });
|
||||
return selected.id;
|
||||
},
|
||||
[entries, order, setSelectedEvents],
|
||||
);
|
||||
|
||||
const selectPage = useCallback(
|
||||
(cursor: EntryId | null, direction: 'up' | 'down') => {
|
||||
if (order.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cursor === null) {
|
||||
const selected = direction === 'down' ? getFirstNormal(entries, order) : getLastNormal(entries, order);
|
||||
if (!selected) {
|
||||
return null;
|
||||
}
|
||||
const index = order.indexOf(selected.id);
|
||||
if (index !== -1) {
|
||||
setSelectedEvents({ id: selected.id, selectMode: 'click', index });
|
||||
return selected.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let nextCursor = cursor;
|
||||
let target: { entry: OntimeEntry | null; index: number | null } | null = null;
|
||||
|
||||
for (let step = 0; step < PAGE_SIZE; step += 1) {
|
||||
const next =
|
||||
direction === 'down'
|
||||
? getNextNormal(entries, order, nextCursor)
|
||||
: getPreviousNormal(entries, order, nextCursor);
|
||||
if (next.entry === null || next.index === null) {
|
||||
break;
|
||||
}
|
||||
target = next;
|
||||
nextCursor = next.entry.id;
|
||||
}
|
||||
|
||||
if (target?.entry && target.index !== null) {
|
||||
setSelectedEvents({ id: target.entry.id, selectMode: 'click', index: target.index });
|
||||
return target.entry.id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[entries, order, setSelectedEvents],
|
||||
);
|
||||
|
||||
return {
|
||||
cloneEntry,
|
||||
deleteAtCursor,
|
||||
insertCopyAtId,
|
||||
insertAtId,
|
||||
selectGroup,
|
||||
selectEntry,
|
||||
moveEntry,
|
||||
selectEdge,
|
||||
selectPage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { type OntimeEntry, EntryId, SupportedEntry } from 'ontime-types';
|
||||
|
||||
import { useEntryCopy } from '../../../common/stores/entryCopyStore';
|
||||
import { useEventSelection } from '../useEventSelection';
|
||||
|
||||
interface UseRundownKeyboardOptions {
|
||||
cursor: EntryId | null;
|
||||
commands: {
|
||||
selectEntry: (cursor: EntryId | null, direction: 'up' | 'down') => void;
|
||||
selectGroup: (cursor: EntryId | null, direction: 'up' | 'down') => void;
|
||||
selectEntry: (cursor: EntryId | null, direction: 'up' | 'down') => EntryId | null;
|
||||
selectGroup: (cursor: EntryId | null, direction: 'up' | 'down') => EntryId | null;
|
||||
selectEdge: (direction: 'top' | 'bottom') => EntryId | null;
|
||||
selectPage: (cursor: EntryId | null, direction: 'up' | 'down') => EntryId | null;
|
||||
cloneEntry: (cursor: EntryId | null) => void;
|
||||
moveEntry: (cursor: EntryId | null, direction: 'up' | 'down') => void;
|
||||
deleteAtCursor: (cursor: EntryId | null) => void;
|
||||
insertAtId: (patch: Partial<OntimeEntry> & { type: SupportedEntry }, id: EntryId | null, above?: boolean) => void;
|
||||
insertCopyAtId: (atId: EntryId | null, above?: boolean) => void;
|
||||
};
|
||||
clearSelectedEvents: () => void;
|
||||
setEntryCopyId: (id: EntryId | null) => void;
|
||||
setEntryCopyId: (id: EntryId | null, mode?: 'copy' | 'cut') => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a keyboard event target is a text input element.
|
||||
* Use this to avoid intercepting browser-native copy/cut/paste shortcuts
|
||||
* while users are typing in form fields.
|
||||
*/
|
||||
function isEditableElement(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tagName = target.tagName.toLowerCase();
|
||||
return tagName === 'input' || tagName === 'textarea';
|
||||
}
|
||||
|
||||
export function useRundownKeyboard({
|
||||
@@ -21,18 +41,89 @@ export function useRundownKeyboard({
|
||||
clearSelectedEvents,
|
||||
setEntryCopyId,
|
||||
}: UseRundownKeyboardOptions) {
|
||||
const scrollToEntry = useEventSelection((state) => state.scrollToEntry);
|
||||
|
||||
useHotkeys([
|
||||
['alt + ArrowDown', () => commands.selectEntry(cursor, 'down'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + ArrowUp', () => commands.selectEntry(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
[
|
||||
'alt + ArrowDown',
|
||||
() => {
|
||||
const nextId = commands.selectEntry(cursor, 'down');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'alt + ArrowUp',
|
||||
() => {
|
||||
const nextId = commands.selectEntry(cursor, 'up');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
[
|
||||
'alt + shift + ArrowDown',
|
||||
() => commands.selectGroup(cursor, 'down'),
|
||||
() => {
|
||||
const nextId = commands.selectGroup(cursor, 'down');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'alt + shift + ArrowUp',
|
||||
() => commands.selectGroup(cursor, 'up'),
|
||||
() => {
|
||||
const nextId = commands.selectGroup(cursor, 'up');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
[
|
||||
'Home',
|
||||
() => {
|
||||
const nextId = commands.selectEdge('top');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'End',
|
||||
() => {
|
||||
const nextId = commands.selectEdge('bottom');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'PageUp',
|
||||
() => {
|
||||
const nextId = commands.selectPage(cursor, 'up');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'PageDown',
|
||||
() => {
|
||||
const nextId = commands.selectPage(cursor, 'down');
|
||||
if (nextId) {
|
||||
scrollToEntry(nextId);
|
||||
}
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
@@ -43,9 +134,16 @@ export function useRundownKeyboard({
|
||||
],
|
||||
['alt + mod + ArrowUp', () => commands.moveEntry(cursor, 'up'), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
['Escape', () => clearSelectedEvents(), { preventDefault: true, usePhysicalKeys: true }],
|
||||
[
|
||||
'Escape',
|
||||
() => {
|
||||
clearSelectedEvents();
|
||||
setEntryCopyId(null);
|
||||
},
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
['mod + Backspace', () => commands.deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
['alt + Backspace', () => commands.deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
[
|
||||
'alt + E',
|
||||
@@ -91,10 +189,50 @@ export function useRundownKeyboard({
|
||||
{ preventDefault: true, usePhysicalKeys: true },
|
||||
],
|
||||
|
||||
['mod + C', () => setEntryCopyId(cursor)],
|
||||
['mod + V', () => commands.insertCopyAtId(cursor)],
|
||||
['mod + shift + V', () => commands.insertCopyAtId(cursor, true), { preventDefault: true, usePhysicalKeys: true }],
|
||||
|
||||
['alt + backspace', () => commands.deleteAtCursor(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
[
|
||||
'mod + C',
|
||||
(event) => {
|
||||
if (cursor === null || isEditableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setEntryCopyId(cursor);
|
||||
},
|
||||
{ usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'mod + X',
|
||||
(event) => {
|
||||
if (cursor === null || isEditableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setEntryCopyId(cursor, 'cut');
|
||||
},
|
||||
{ usePhysicalKeys: true },
|
||||
],
|
||||
[
|
||||
'mod + V',
|
||||
(event) => {
|
||||
if (isEditableElement(event.target) || useEntryCopy.getState().entryCopyId === null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
commands.insertCopyAtId(cursor);
|
||||
},
|
||||
{ usePhysicalKeys: true },
|
||||
],
|
||||
['mod + D', () => commands.cloneEntry(cursor), { preventDefault: true, usePhysicalKeys: true }],
|
||||
[
|
||||
'mod + shift + V',
|
||||
(event) => {
|
||||
if (isEditableElement(event.target) || useEntryCopy.getState().entryCopyId === null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
commands.insertCopyAtId(cursor, true);
|
||||
},
|
||||
{ usePhysicalKeys: true },
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user