create editor canvas toolbar
This commit is contained in:
@@ -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: [],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user