create editor canvas toolbar

This commit is contained in:
hamid zarghami
2026-05-06 11:17:35 +03:30
parent 1518e83c1d
commit e15dc76468
9 changed files with 353 additions and 9 deletions
+174 -2
View File
@@ -20,6 +20,17 @@ import type {
ToolType,
} from "./editorStore.types";
const MAX_OBJECT_HISTORY = 50;
/** اسنپ‌شات آرایٔهٔ اشیا برای undo؛ از انتشار مرج مشترک جلوگیری می‌شود */
const cloneObjectList = (objects: EditorObject[]): EditorObject[] =>
structuredClone(objects);
const objectListsEqual = (a: EditorObject[], b: EditorObject[]) =>
JSON.stringify(a) === JSON.stringify(b);
let objectGestureSnapshot: EditorObject[] | null = null;
export type {
EditorObject,
Page,
@@ -37,7 +48,7 @@ type EditorStoreType = {
addObject: (object: EditorObject) => void;
setGuides: (guides: PageGuide[]) => void;
updateObject: (id: string, updates: Partial<EditorObject>) => void;
deleteObject: (id: string) => void;
deleteObject: (id: string, options?: { skipHistory?: boolean }) => void;
duplicateObject: (id: string) => void;
copySelectedObjects: () => void;
pasteCopiedObjects: () => void;
@@ -108,6 +119,15 @@ type EditorStoreType = {
ungroupObjects: (groupId: string) => void;
loadPages: (pages: Page[]) => void;
clipboardObjects: EditorObject[];
objectHistoryPast: EditorObject[][];
objectHistoryFuture: EditorObject[][];
commitObjectHistoryBeforeChange: () => void;
beginObjectGesture: () => void;
endObjectGesture: () => void;
undoObjects: () => void;
redoObjects: () => void;
bringObjectForward: (id: string) => void;
sendObjectBackward: (id: string) => void;
};
export const useEditorStore = create<EditorStoreType>((set, get) => {
@@ -161,6 +181,135 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
objects: [],
guides: [],
clipboardObjects: [],
objectHistoryPast: [],
objectHistoryFuture: [],
commitObjectHistoryBeforeChange: () => {
const s = get();
const snap = cloneObjectList(s.objects);
set({
objectHistoryPast: [...s.objectHistoryPast, snap].slice(
-MAX_OBJECT_HISTORY,
),
objectHistoryFuture: [],
});
},
beginObjectGesture: () => {
if (objectGestureSnapshot !== null) return;
objectGestureSnapshot = cloneObjectList(get().objects);
},
endObjectGesture: () => {
const base = objectGestureSnapshot;
objectGestureSnapshot = null;
if (!base) return;
if (objectListsEqual(base, get().objects)) return;
const s = get();
set({
objectHistoryPast: [...s.objectHistoryPast, base].slice(
-MAX_OBJECT_HISTORY,
),
objectHistoryFuture: [],
});
},
undoObjects: () => {
const s = get();
if (s.objectHistoryPast.length === 0) return;
const previous = s.objectHistoryPast[s.objectHistoryPast.length - 1]!;
const newPast = s.objectHistoryPast.slice(0, -1);
const currentSnap = cloneObjectList(s.objects);
const next = cloneObjectList(previous);
const idSet = new Set(next.map((o) => o.id));
const nextSelIds = s.selectedObjectIds.filter((id) => idSet.has(id));
const nextTable =
s.selectedTableId && idSet.has(s.selectedTableId)
? s.selectedTableId
: null;
set({
objects: next,
objectHistoryPast: newPast,
objectHistoryFuture: [currentSnap, ...s.objectHistoryFuture].slice(
-MAX_OBJECT_HISTORY,
),
pages:
s.currentPageId != null
? s.pages.map((p) =>
p.id === s.currentPageId ? { ...p, objects: next } : p,
)
: s.pages,
selectedObjectIds: nextSelIds,
selectedObjectId: nextSelIds[0] ?? null,
selectedTableId: nextTable,
selectedCellId: null,
editingCell: null,
});
},
redoObjects: () => {
const s = get();
if (s.objectHistoryFuture.length === 0) return;
const forward = s.objectHistoryFuture[0]!;
const newFuture = s.objectHistoryFuture.slice(1);
const currentSnap = cloneObjectList(s.objects);
const next = cloneObjectList(forward);
const idSet = new Set(next.map((o) => o.id));
const nextSelIds = s.selectedObjectIds.filter((id) => idSet.has(id));
const nextTable =
s.selectedTableId && idSet.has(s.selectedTableId)
? s.selectedTableId
: null;
set({
objects: next,
objectHistoryPast: [...s.objectHistoryPast, currentSnap].slice(
-MAX_OBJECT_HISTORY,
),
objectHistoryFuture: newFuture,
pages:
s.currentPageId != null
? s.pages.map((p) =>
p.id === s.currentPageId ? { ...p, objects: next } : p,
)
: s.pages,
selectedObjectIds: nextSelIds,
selectedObjectId: nextSelIds[0] ?? null,
selectedTableId: nextTable,
selectedCellId: null,
editingCell: null,
});
},
bringObjectForward: (id) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const objs = [...state.objects];
const i = objs.findIndex((o) => o.id === id);
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,
),
});
return;
}
set({ objects: objs });
},
sendObjectBackward: (id) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const objs = [...state.objects];
const i = objs.findIndex((o) => o.id === id);
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,
),
});
return;
}
set({ objects: objs });
},
addObject: (object) => {
const state = get();
if (state.currentPageId) {
@@ -220,7 +369,10 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
),
}));
},
deleteObject: (id) => {
deleteObject: (id, options) => {
if (!options?.skipHistory) {
get().commitObjectHistoryBeforeChange();
}
const state = get();
if (state.currentPageId) {
set((state) => ({
@@ -246,6 +398,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
}));
},
duplicateObject: (id) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const objectToDuplicate = state.objects.find((obj) => obj.id === id);
if (!objectToDuplicate) return;
@@ -297,6 +450,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
const state = get();
if (state.clipboardObjects.length === 0) return;
get().commitObjectHistoryBeforeChange();
const pastedObjects = cloneObjectsForPaste(state.clipboardObjects);
const pastedIds = pastedObjects.map((obj) => obj.id);
@@ -323,6 +477,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
reorderObjects: (draggedId, targetId) => {
if (draggedId === targetId) return;
get().commitObjectHistoryBeforeChange();
const state = get();
if (state.currentPageId) {
set((state) => ({
@@ -411,6 +566,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
get().setSelectedTableId(tableId);
},
addRow: (tableId) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const obj = state.objects.find((o) => o.id === tableId);
if (!obj || !obj.tableData) return;
@@ -440,6 +596,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
});
},
addColumn: (tableId) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const obj = state.objects.find((o) => o.id === tableId);
if (!obj || !obj.tableData) return;
@@ -469,6 +626,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
});
},
removeRow: (tableId) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const obj = state.objects.find((o) => o.id === tableId);
if (!obj || !obj.tableData) return;
@@ -492,6 +650,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
});
},
removeColumn: (tableId) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const obj = state.objects.find((o) => o.id === tableId);
if (!obj || !obj.tableData) return;
@@ -515,6 +674,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
});
},
updateCellValue: (tableId, cellId, text) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const obj = state.objects.find((o) => o.id === tableId);
if (!obj || !obj.tableData) return;
@@ -536,6 +696,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
});
},
changeCellBackground: (tableId, cellId, color) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const obj = state.objects.find((o) => o.id === tableId);
if (!obj || !obj.tableData) return;
@@ -617,6 +778,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
objects: [],
guides: [],
selectedObjectId: null,
objectHistoryPast: [],
objectHistoryFuture: [],
}));
},
duplicatePage: (pageId) => {
@@ -658,6 +821,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
objects: newCurrentPage?.objects || [],
guides: newCurrentPage?.guides || [],
selectedObjectId: null,
objectHistoryPast: [],
objectHistoryFuture: [],
});
},
setCurrentPage: (pageId) => {
@@ -683,6 +848,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
objects: targetPageFromUpdated.objects,
guides: targetPageFromUpdated.guides || [],
selectedObjectId: null,
objectHistoryPast: [],
objectHistoryFuture: [],
});
},
updatePageName: (pageId, name) => {
@@ -691,6 +858,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
}));
},
toggleObjectVisibility: (id) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const obj = state.objects.find((o) => o.id === id);
if (!obj) return;
@@ -729,6 +897,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
}));
},
groupObjects: (objectIds) => {
get().commitObjectHistoryBeforeChange();
const state = get();
const result = buildGroupResult(state.objects, objectIds);
if (!result) return;
@@ -762,6 +931,7 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
}));
},
ungroupObjects: (groupId) => {
get().commitObjectHistoryBeforeChange();
const state = get();
if (state.currentPageId) {
@@ -803,6 +973,8 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
guides: firstPage.guides || [],
selectedObjectId: null,
selectedObjectIds: [],
objectHistoryPast: [],
objectHistoryFuture: [],
});
},
};