mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): activate selected element set state
Use selectedElementIds as the live selection set with selectedElementId as anchor. Keep single-click selection as a one-element set and clear both fields together.
This commit is contained in:
@@ -222,6 +222,77 @@ describe("usePlayerStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectedElementIds", () => {
|
||||
it("sets a multi-id selection with a coherent anchor", () => {
|
||||
usePlayerStore.getState().setSelection(["el-1", "el-2", "el-3"], "el-2");
|
||||
|
||||
const state = usePlayerStore.getState();
|
||||
expect([...state.selectedElementIds]).toEqual(["el-1", "el-2", "el-3"]);
|
||||
expect(state.selectedElementId).toBe("el-2");
|
||||
});
|
||||
|
||||
it("falls back to the first selected id when the anchor is outside the set", () => {
|
||||
usePlayerStore.getState().setSelection(["el-1", "el-2"], "missing");
|
||||
|
||||
const state = usePlayerStore.getState();
|
||||
expect([...state.selectedElementIds]).toEqual(["el-1", "el-2"]);
|
||||
expect(state.selectedElementId).toBe("el-1");
|
||||
});
|
||||
|
||||
it("single-click selection replaces the set with the selected id", () => {
|
||||
const store = usePlayerStore.getState();
|
||||
store.setSelection(["el-1", "el-2"], "el-2");
|
||||
store.setSelectedElementId("el-3");
|
||||
|
||||
const state = usePlayerStore.getState();
|
||||
expect([...state.selectedElementIds]).toEqual(["el-3"]);
|
||||
expect(state.selectedElementId).toBe("el-3");
|
||||
});
|
||||
|
||||
it("clearing single selection empties the set", () => {
|
||||
const store = usePlayerStore.getState();
|
||||
store.setSelection(["el-1", "el-2"], "el-2");
|
||||
store.setSelectedElementId(null);
|
||||
|
||||
const state = usePlayerStore.getState();
|
||||
expect([...state.selectedElementIds]).toEqual([]);
|
||||
expect(state.selectedElementId).toBeNull();
|
||||
});
|
||||
|
||||
it("toggle adds and removes members while keeping the anchor in the set", () => {
|
||||
const store = usePlayerStore.getState();
|
||||
store.setSelectedElementId("el-1");
|
||||
store.toggleSelectedElementId("el-2");
|
||||
|
||||
let state = usePlayerStore.getState();
|
||||
expect([...state.selectedElementIds]).toEqual(["el-1", "el-2"]);
|
||||
expect(state.selectedElementId).toBe("el-1");
|
||||
|
||||
store.toggleSelectedElementId("el-1");
|
||||
|
||||
state = usePlayerStore.getState();
|
||||
expect([...state.selectedElementIds]).toEqual(["el-2"]);
|
||||
expect(state.selectedElementId).toBe("el-2");
|
||||
});
|
||||
|
||||
it("clearSelection and clearSelectedElementIds empty the set and anchor", () => {
|
||||
const store = usePlayerStore.getState();
|
||||
store.setSelection(["el-1", "el-2"], "el-2");
|
||||
store.clearSelection();
|
||||
|
||||
let state = usePlayerStore.getState();
|
||||
expect([...state.selectedElementIds]).toEqual([]);
|
||||
expect(state.selectedElementId).toBeNull();
|
||||
|
||||
store.setSelection(["el-3"], "el-3");
|
||||
store.clearSelectedElementIds();
|
||||
|
||||
state = usePlayerStore.getState();
|
||||
expect([...state.selectedElementIds]).toEqual([]);
|
||||
expect(state.selectedElementId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateElement", () => {
|
||||
it("updates the start time of a specific element", () => {
|
||||
usePlayerStore.getState().setElements([
|
||||
|
||||
@@ -75,6 +75,23 @@ export interface TimelineElement {
|
||||
export type ZoomMode = "fit" | "manual";
|
||||
type TimelineTool = "select" | "razor";
|
||||
|
||||
function resolveElementSelection(
|
||||
ids: Iterable<string>,
|
||||
anchor?: string | null,
|
||||
): { selectedElementIds: Set<string>; selectedElementId: string | null } {
|
||||
const selectedElementIds = new Set(ids);
|
||||
if (selectedElementIds.size === 0) {
|
||||
return { selectedElementIds, selectedElementId: null };
|
||||
}
|
||||
if (anchor && selectedElementIds.has(anchor)) {
|
||||
return { selectedElementIds, selectedElementId: anchor };
|
||||
}
|
||||
return {
|
||||
selectedElementIds,
|
||||
selectedElementId: selectedElementIds.values().next().value ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
interface PlayerState {
|
||||
isPlaying: boolean;
|
||||
currentTime: number;
|
||||
@@ -125,7 +142,10 @@ interface PlayerState {
|
||||
|
||||
/** Multi-select: additional selected elements beyond selectedElementId. */
|
||||
selectedElementIds: Set<string>;
|
||||
setSelection: (ids: Iterable<string>, anchor?: string | null) => void;
|
||||
addSelectedElementId: (id: string) => void;
|
||||
toggleSelectedElementId: (id: string) => void;
|
||||
clearSelection: () => void;
|
||||
clearSelectedElementIds: () => void;
|
||||
|
||||
/** Keyframe data per element id, populated from parsed GSAP animations. */
|
||||
@@ -265,14 +285,22 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
setAutoKeyframeEnabled: (enabled) => set({ autoKeyframeEnabled: enabled }),
|
||||
|
||||
selectedElementIds: new Set<string>(),
|
||||
setSelection: (ids, anchor) => set(resolveElementSelection(ids, anchor)),
|
||||
addSelectedElementId: (id: string) =>
|
||||
set((s) => {
|
||||
const next = new Set(s.selectedElementIds);
|
||||
next.add(id);
|
||||
return resolveElementSelection(next, s.selectedElementId);
|
||||
}),
|
||||
toggleSelectedElementId: (id: string) =>
|
||||
set((s) => {
|
||||
const next = new Set(s.selectedElementIds);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return { selectedElementIds: next };
|
||||
return resolveElementSelection(next, s.selectedElementId);
|
||||
}),
|
||||
clearSelectedElementIds: () => set({ selectedElementIds: new Set() }),
|
||||
clearSelection: () => set({ selectedElementId: null, selectedElementIds: new Set() }),
|
||||
clearSelectedElementIds: () => set({ selectedElementId: null, selectedElementIds: new Set() }),
|
||||
|
||||
keyframeCache: new Map(),
|
||||
setKeyframeCache: (elementId, data) =>
|
||||
@@ -390,8 +418,13 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
// to "modify" a keyframe on the new element. A diamond click sets the pct AFTER
|
||||
// calling setSelectedElementId, so this never clobbers a genuine keyframe select.
|
||||
id !== s.selectedElementId
|
||||
? { selectedElementId: id, activeKeyframePct: null, motionPathArmed: false }
|
||||
: { selectedElementId: id },
|
||||
? {
|
||||
selectedElementId: id,
|
||||
selectedElementIds: id ? new Set([id]) : new Set(),
|
||||
activeKeyframePct: null,
|
||||
motionPathArmed: false,
|
||||
}
|
||||
: { selectedElementId: id, selectedElementIds: id ? new Set([id]) : new Set() },
|
||||
),
|
||||
updateElement: (elementId, updates) =>
|
||||
set((state) => ({
|
||||
|
||||
Reference in New Issue
Block a user