Files
OpenCut/apps/web/src/hooks/timeline/element/use-element-selection.ts
T

138 lines
3.5 KiB
TypeScript

import { useCallback } from "react";
import { useEditor } from "@/hooks/use-editor";
import type { ElementRef } from "@/lib/timeline/types";
export function useElementSelection() {
const editor = useEditor();
const selectedElements = useEditor((e) => e.selection.getSelectedElements());
const isElementSelected = useCallback(
({ trackId, elementId }: ElementRef) =>
selectedElements.some(
(element) =>
element.trackId === trackId && element.elementId === elementId,
),
[selectedElements],
);
const selectElement = useCallback(
({ trackId, elementId }: ElementRef) => {
editor.selection.setSelectedElements({
elements: [{ trackId, elementId }],
});
},
[editor],
);
const addElementToSelection = useCallback(
({ trackId, elementId }: ElementRef) => {
const alreadySelected = selectedElements.some(
(element) =>
element.trackId === trackId && element.elementId === elementId,
);
if (alreadySelected) return;
editor.selection.setSelectedElements({
elements: [...selectedElements, { trackId, elementId }],
});
},
[selectedElements, editor],
);
const removeElementFromSelection = useCallback(
({ trackId, elementId }: ElementRef) => {
editor.selection.setSelectedElements({
elements: selectedElements.filter(
(element) =>
!(element.trackId === trackId && element.elementId === elementId),
),
});
},
[selectedElements, editor],
);
const toggleElementSelection = useCallback(
({ trackId, elementId }: ElementRef) => {
const alreadySelected = selectedElements.some(
(element) =>
element.trackId === trackId && element.elementId === elementId,
);
if (alreadySelected) {
removeElementFromSelection({ trackId, elementId });
} else {
addElementToSelection({ trackId, elementId });
}
},
[selectedElements, addElementToSelection, removeElementFromSelection],
);
const clearElementSelection = useCallback(() => {
editor.selection.clearSelection();
}, [editor]);
const setElementSelection = useCallback(
({ elements }: { elements: ElementRef[] }) => {
editor.selection.setSelectedElements({ elements });
},
[editor],
);
/**
* Merges elements into the current selection, deduplicating by identity.
* Used for additive box-select where the pre-drag selection is preserved.
*/
const mergeElementsIntoSelection = useCallback(
({ elements }: { elements: ElementRef[] }) => {
const merged = [
...selectedElements.filter(
(selectedElement) =>
!elements.some(
(element) =>
element.trackId === selectedElement.trackId &&
element.elementId === selectedElement.elementId,
),
),
...elements,
];
editor.selection.setSelectedElements({ elements: merged });
},
[selectedElements, editor],
);
/**
* Handles click interaction on an element.
* - Regular click: select only this element
* - Multi-key click (Ctrl/Cmd): toggle this element in selection
*/
const handleElementClick = useCallback(
({
trackId,
elementId,
isMultiKey,
}: ElementRef & { isMultiKey: boolean }) => {
if (isMultiKey) {
toggleElementSelection({ trackId, elementId });
} else {
selectElement({ trackId, elementId });
}
},
[toggleElementSelection, selectElement],
);
return {
selectedElements,
isElementSelected,
selectElement,
setElementSelection,
mergeElementsIntoSelection,
addElementToSelection,
removeElementFromSelection,
toggleElementSelection,
clearElementSelection,
handleElementClick,
};
}