fix animation
This commit is contained in:
@@ -23,7 +23,7 @@ const Editor: FC = () => {
|
||||
}, [])
|
||||
const [isLayersPanelOpen, setIsLayersPanelOpen] = useState(false)
|
||||
const [isInitialContentReady, setIsInitialContentReady] = useState(false)
|
||||
const { loadPages, pages, documentSettings } = useEditorStore()
|
||||
const { loadPages, pages, documentSettings, objects, currentPageId } = useEditorStore()
|
||||
const { data, isFetched } = useGetCatalogById(id!)
|
||||
const { scheduleUpdate, isSaving } = useUpdateCatalog()
|
||||
|
||||
@@ -51,13 +51,19 @@ const Editor: FC = () => {
|
||||
if (!isInitialContentReady) return
|
||||
if (!pages || pages.length === 0) return
|
||||
|
||||
const pagesToSave = currentPageId
|
||||
? pages.map((p) =>
|
||||
p.id === currentPageId ? { ...p, objects } : p,
|
||||
)
|
||||
: pages
|
||||
|
||||
scheduleUpdate({
|
||||
id,
|
||||
content: JSON.stringify({ pages, documentSettings }),
|
||||
content: JSON.stringify({ pages: pagesToSave, documentSettings }),
|
||||
})
|
||||
// عمداً scheduleUpdate را در وابستگیها نیاوردیم تا از لوپ ذخیرهسازی جلوگیری شود
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, isInitialContentReady, pages, documentSettings])
|
||||
}, [id, isInitialContentReady, pages, documentSettings, objects, currentPageId])
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Select from "@/components/Select";
|
||||
import Input from "@/components/Input";
|
||||
import type { EditorObject } from "../../../store/editorStore";
|
||||
import { useEditorStore } from "../../../store/editorStore";
|
||||
import { ENTRANCE_ANIMATION_SELECT_ITEMS } from "../constants/entranceAnimationOptions";
|
||||
import {
|
||||
DEFAULT_ENTRANCE_DURATION_MS,
|
||||
@@ -13,10 +14,34 @@ type Props = {
|
||||
onUpdate: (id: string, updates: Partial<EditorObject>) => void;
|
||||
};
|
||||
|
||||
/** اعضای گروه (نه خود باکس group) برای تنظیم انیمیشن ورود */
|
||||
function getAnimationTargets(
|
||||
selectedObject: EditorObject,
|
||||
objects: EditorObject[],
|
||||
): EditorObject[] {
|
||||
if (selectedObject.type === "group") {
|
||||
return objects.filter(
|
||||
(o) => o.groupId === selectedObject.id && o.type !== "group",
|
||||
);
|
||||
}
|
||||
return [selectedObject];
|
||||
}
|
||||
|
||||
const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => {
|
||||
const anim = selectedObject.entranceAnimation ?? "none";
|
||||
const duration = selectedObject.entranceDurationMs ?? DEFAULT_ENTRANCE_DURATION_MS;
|
||||
const delayMs = selectedObject.entranceDelayMs ?? DEFAULT_ENTRANCE_DELAY_MS;
|
||||
const objects = useEditorStore((s) => s.objects);
|
||||
const targets = getAnimationTargets(selectedObject, objects);
|
||||
const primary = targets[0] ?? selectedObject;
|
||||
|
||||
const anim = primary.entranceAnimation ?? "none";
|
||||
|
||||
const duration = primary.entranceDurationMs ?? DEFAULT_ENTRANCE_DURATION_MS;
|
||||
const delayMs = primary.entranceDelayMs ?? DEFAULT_ENTRANCE_DELAY_MS;
|
||||
|
||||
const applyToTargets = (updates: Partial<EditorObject>) => {
|
||||
const ids =
|
||||
targets.length > 0 ? targets.map((t) => t.id) : [selectedObject.id];
|
||||
ids.forEach((id) => onUpdate(id, updates));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pt-4 flex flex-col gap-4 border-t border-border">
|
||||
@@ -26,7 +51,7 @@ const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => {
|
||||
value={anim}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value as EditorObject["entranceAnimation"];
|
||||
onUpdate(selectedObject.id, {
|
||||
applyToTargets({
|
||||
entranceAnimation: v === "none" ? undefined : v,
|
||||
...(v === "none" || !v
|
||||
? { entranceDurationMs: undefined, entranceDelayMs: undefined }
|
||||
@@ -35,19 +60,22 @@ const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => {
|
||||
}}
|
||||
/>
|
||||
{anim !== "none" && (
|
||||
<><Input
|
||||
label="مدت انیمیشن (میلیثانیه)"
|
||||
type="number"
|
||||
min={100}
|
||||
max={4000}
|
||||
value={duration}
|
||||
onChange={(e) => {
|
||||
const n = parseInt(e.target.value, 10);
|
||||
if (Number.isNaN(n)) return;
|
||||
onUpdate(selectedObject.id, {
|
||||
entranceDurationMs: Math.min(4000, Math.max(100, n)),
|
||||
});
|
||||
}} /><Input
|
||||
<>
|
||||
<Input
|
||||
label="مدت انیمیشن (میلیثانیه)"
|
||||
type="number"
|
||||
min={100}
|
||||
max={4000}
|
||||
value={duration}
|
||||
onChange={(e) => {
|
||||
const n = parseInt(e.target.value, 10);
|
||||
if (Number.isNaN(n)) return;
|
||||
applyToTargets({
|
||||
entranceDurationMs: Math.min(4000, Math.max(100, n)),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label="تأخیر قبل از شروع (میلیثانیه)"
|
||||
type="number"
|
||||
min={0}
|
||||
@@ -57,13 +85,15 @@ const EntranceAnimationSettings = ({ selectedObject, onUpdate }: Props) => {
|
||||
const n = parseInt(e.target.value, 10);
|
||||
if (Number.isNaN(n)) return;
|
||||
const clamped = Math.min(MAX_ENTRANCE_DELAY_MS, Math.max(0, n));
|
||||
onUpdate(selectedObject.id, {
|
||||
applyToTargets({
|
||||
entranceDelayMs: clamped === 0 ? undefined : clamped,
|
||||
});
|
||||
}} /></>
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EntranceAnimationSettings;
|
||||
export default EntranceAnimationSettings;
|
||||
@@ -30,6 +30,22 @@ const cloneObjectList = (objects: EditorObject[]): EditorObject[] =>
|
||||
const objectListsEqual = (a: EditorObject[], b: EditorObject[]) =>
|
||||
JSON.stringify(a) === JSON.stringify(b);
|
||||
|
||||
/** یک آرایهٔ objects برای state و صفحهٔ جاری در pages (جلوگیری از از دست رفتن فیلدهایی مثل entranceAnimation) */
|
||||
const withSyncedCurrentPageObjects = (
|
||||
state: { objects: EditorObject[]; pages: Page[]; currentPageId: string | null },
|
||||
nextObjects: EditorObject[],
|
||||
) => {
|
||||
if (!state.currentPageId) {
|
||||
return { objects: nextObjects };
|
||||
}
|
||||
return {
|
||||
objects: nextObjects,
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === state.currentPageId ? { ...p, objects: nextObjects } : p,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
let objectGestureSnapshot: EditorObject[] | null = null;
|
||||
|
||||
export type {
|
||||
@@ -248,7 +264,6 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
});
|
||||
},
|
||||
beginObjectGesture: () => {
|
||||
if (objectGestureSnapshot !== null) return;
|
||||
objectGestureSnapshot = cloneObjectList(get().objects);
|
||||
},
|
||||
endObjectGesture: () => {
|
||||
@@ -336,12 +351,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
if (i === -1 || i >= objs.length - 1) return;
|
||||
[objs[i], objs[i + 1]] = [objs[i + 1]!, objs[i]!];
|
||||
if (state.currentPageId) {
|
||||
set({
|
||||
objects: objs,
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === state.currentPageId ? { ...p, objects: objs } : p,
|
||||
),
|
||||
});
|
||||
set(withSyncedCurrentPageObjects(state, objs));
|
||||
return;
|
||||
}
|
||||
set({ objects: objs });
|
||||
@@ -354,12 +364,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
if (i <= 0) return;
|
||||
[objs[i], objs[i - 1]] = [objs[i - 1]!, objs[i]!];
|
||||
if (state.currentPageId) {
|
||||
set({
|
||||
objects: objs,
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === state.currentPageId ? { ...p, objects: objs } : p,
|
||||
),
|
||||
});
|
||||
set(withSyncedCurrentPageObjects(state, objs));
|
||||
return;
|
||||
}
|
||||
set({ objects: objs });
|
||||
@@ -398,30 +403,12 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
set({ guides });
|
||||
},
|
||||
updateObject: (id, updates) => {
|
||||
const state = get();
|
||||
if (state.currentPageId) {
|
||||
set((state) => ({
|
||||
objects: state.objects.map((obj) =>
|
||||
obj.id === id ? { ...obj, ...updates } : obj,
|
||||
),
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === state.currentPageId
|
||||
? {
|
||||
...p,
|
||||
objects: p.objects.map((obj) =>
|
||||
obj.id === id ? { ...obj, ...updates } : obj,
|
||||
),
|
||||
}
|
||||
: p,
|
||||
),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
set((state) => ({
|
||||
objects: state.objects.map((obj) =>
|
||||
set((state) => {
|
||||
const nextObjects = state.objects.map((obj) =>
|
||||
obj.id === id ? { ...obj, ...updates } : obj,
|
||||
),
|
||||
}));
|
||||
);
|
||||
return withSyncedCurrentPageObjects(state, nextObjects);
|
||||
});
|
||||
},
|
||||
deleteObject: (id, options) => {
|
||||
if (!options?.skipHistory) {
|
||||
@@ -535,17 +522,12 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
get().commitObjectHistoryBeforeChange();
|
||||
const state = get();
|
||||
if (state.currentPageId) {
|
||||
set((state) => ({
|
||||
objects: reorderByIds(state.objects, draggedId, targetId),
|
||||
pages: state.pages.map((page) =>
|
||||
page.id === state.currentPageId
|
||||
? {
|
||||
...page,
|
||||
objects: reorderByIds(page.objects, draggedId, targetId),
|
||||
}
|
||||
: page,
|
||||
set((state) =>
|
||||
withSyncedCurrentPageObjects(
|
||||
state,
|
||||
reorderByIds(state.objects, draggedId, targetId),
|
||||
),
|
||||
}));
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -962,24 +944,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
if (!result) return;
|
||||
|
||||
if (state.currentPageId) {
|
||||
const pageToGroup = state.pages.find(
|
||||
(p) => p.id === state.currentPageId,
|
||||
);
|
||||
if (!pageToGroup) return;
|
||||
const pageResult = buildGroupResult(
|
||||
pageToGroup.objects,
|
||||
objectIds,
|
||||
result.groupId,
|
||||
);
|
||||
if (!pageResult) return;
|
||||
|
||||
set((state) => ({
|
||||
objects: result.nextObjects,
|
||||
pages: state.pages.map((p) =>
|
||||
p.id === state.currentPageId
|
||||
? { ...p, objects: pageResult.nextObjects }
|
||||
: p,
|
||||
),
|
||||
...withSyncedCurrentPageObjects(state, result.nextObjects),
|
||||
selectedObjectId: result.groupId,
|
||||
selectedObjectIds: [result.groupId],
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user