|
@@ -40,212 +40,233 @@ interface LayerUIProps {
|
40
|
40
|
onLockToggle: () => void;
|
41
|
41
|
}
|
42
|
42
|
|
43
|
|
-export const LayerUI = React.memo(
|
44
|
|
- ({
|
45
|
|
- actionManager,
|
46
|
|
- appState,
|
47
|
|
- setAppState,
|
48
|
|
- canvas,
|
49
|
|
- elements,
|
50
|
|
- onRoomCreate,
|
51
|
|
- onUsernameChange,
|
52
|
|
- onRoomDestroy,
|
53
|
|
- onLockToggle,
|
54
|
|
- }: LayerUIProps) => {
|
55
|
|
- const isMobile = useIsMobile();
|
|
43
|
+const LayerUI = ({
|
|
44
|
+ actionManager,
|
|
45
|
+ appState,
|
|
46
|
+ setAppState,
|
|
47
|
+ canvas,
|
|
48
|
+ elements,
|
|
49
|
+ onRoomCreate,
|
|
50
|
+ onUsernameChange,
|
|
51
|
+ onRoomDestroy,
|
|
52
|
+ onLockToggle,
|
|
53
|
+}: LayerUIProps) => {
|
|
54
|
+ const isMobile = useIsMobile();
|
56
|
55
|
|
57
|
|
- function renderExportDialog() {
|
58
|
|
- const createExporter = (type: ExportType): ExportCB => (
|
59
|
|
- exportedElements,
|
60
|
|
- scale,
|
61
|
|
- ) => {
|
62
|
|
- if (canvas) {
|
63
|
|
- exportCanvas(type, exportedElements, appState, canvas, {
|
64
|
|
- exportBackground: appState.exportBackground,
|
65
|
|
- name: appState.name,
|
66
|
|
- viewBackgroundColor: appState.viewBackgroundColor,
|
67
|
|
- scale,
|
68
|
|
- });
|
69
|
|
- }
|
70
|
|
- };
|
71
|
|
- return (
|
72
|
|
- <ExportDialog
|
73
|
|
- elements={elements}
|
74
|
|
- appState={appState}
|
75
|
|
- actionManager={actionManager}
|
76
|
|
- onExportToPng={createExporter("png")}
|
77
|
|
- onExportToSvg={createExporter("svg")}
|
78
|
|
- onExportToClipboard={createExporter("clipboard")}
|
79
|
|
- onExportToBackend={(exportedElements) => {
|
80
|
|
- if (canvas) {
|
81
|
|
- exportCanvas(
|
82
|
|
- "backend",
|
83
|
|
- exportedElements,
|
84
|
|
- {
|
85
|
|
- ...appState,
|
86
|
|
- selectedElementIds: {},
|
87
|
|
- },
|
88
|
|
- canvas,
|
89
|
|
- appState,
|
90
|
|
- );
|
91
|
|
- }
|
92
|
|
- }}
|
93
|
|
- />
|
94
|
|
- );
|
95
|
|
- }
|
96
|
|
-
|
97
|
|
- return isMobile ? (
|
98
|
|
- <MobileMenu
|
99
|
|
- appState={appState}
|
|
56
|
+ const renderExportDialog = () => {
|
|
57
|
+ const createExporter = (type: ExportType): ExportCB => (
|
|
58
|
+ exportedElements,
|
|
59
|
+ scale,
|
|
60
|
+ ) => {
|
|
61
|
+ if (canvas) {
|
|
62
|
+ exportCanvas(type, exportedElements, appState, canvas, {
|
|
63
|
+ exportBackground: appState.exportBackground,
|
|
64
|
+ name: appState.name,
|
|
65
|
+ viewBackgroundColor: appState.viewBackgroundColor,
|
|
66
|
+ scale,
|
|
67
|
+ });
|
|
68
|
+ }
|
|
69
|
+ };
|
|
70
|
+ return (
|
|
71
|
+ <ExportDialog
|
100
|
72
|
elements={elements}
|
|
73
|
+ appState={appState}
|
101
|
74
|
actionManager={actionManager}
|
102
|
|
- exportButton={renderExportDialog()}
|
103
|
|
- setAppState={setAppState}
|
104
|
|
- onUsernameChange={onUsernameChange}
|
105
|
|
- onRoomCreate={onRoomCreate}
|
106
|
|
- onRoomDestroy={onRoomDestroy}
|
107
|
|
- onLockToggle={onLockToggle}
|
|
75
|
+ onExportToPng={createExporter("png")}
|
|
76
|
+ onExportToSvg={createExporter("svg")}
|
|
77
|
+ onExportToClipboard={createExporter("clipboard")}
|
|
78
|
+ onExportToBackend={(exportedElements) => {
|
|
79
|
+ if (canvas) {
|
|
80
|
+ exportCanvas(
|
|
81
|
+ "backend",
|
|
82
|
+ exportedElements,
|
|
83
|
+ {
|
|
84
|
+ ...appState,
|
|
85
|
+ selectedElementIds: {},
|
|
86
|
+ },
|
|
87
|
+ canvas,
|
|
88
|
+ appState,
|
|
89
|
+ );
|
|
90
|
+ }
|
|
91
|
+ }}
|
108
|
92
|
/>
|
109
|
|
- ) : (
|
110
|
|
- <>
|
111
|
|
- {appState.isLoading && <LoadingMessage />}
|
112
|
|
- {appState.errorMessage && (
|
113
|
|
- <ErrorDialog
|
114
|
|
- message={appState.errorMessage}
|
115
|
|
- onClose={() => setAppState({ errorMessage: null })}
|
116
|
|
- />
|
117
|
|
- )}
|
118
|
|
- {appState.showShortcutsDialog && (
|
119
|
|
- <ShortcutsDialog
|
120
|
|
- onClose={() => setAppState({ showShortcutsDialog: null })}
|
121
|
|
- />
|
122
|
|
- )}
|
123
|
|
- <FixedSideContainer side="top">
|
124
|
|
- <HintViewer appState={appState} elements={elements} />
|
125
|
|
- <div className="App-menu App-menu_top">
|
126
|
|
- <Stack.Col gap={4}>
|
127
|
|
- <Section heading="canvasActions">
|
128
|
|
- {/* the zIndex ensures this menu has higher stacking order,
|
129
|
|
- see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
130
|
|
- <Island padding={4} style={{ zIndex: 1 }}>
|
131
|
|
- <Stack.Col gap={4}>
|
132
|
|
- <Stack.Row gap={1} justifyContent={"space-between"}>
|
133
|
|
- {actionManager.renderAction("loadScene")}
|
134
|
|
- {actionManager.renderAction("saveScene")}
|
135
|
|
- {renderExportDialog()}
|
136
|
|
- {actionManager.renderAction("clearCanvas")}
|
137
|
|
- <RoomDialog
|
138
|
|
- isCollaborating={appState.isCollaborating}
|
139
|
|
- collaboratorCount={appState.collaborators.size}
|
140
|
|
- username={appState.username}
|
141
|
|
- onUsernameChange={onUsernameChange}
|
142
|
|
- onRoomCreate={onRoomCreate}
|
143
|
|
- onRoomDestroy={onRoomDestroy}
|
|
93
|
+ );
|
|
94
|
+ };
|
|
95
|
+
|
|
96
|
+ const renderCanvasActions = () => (
|
|
97
|
+ <Section heading="canvasActions">
|
|
98
|
+ {/* the zIndex ensures this menu has higher stacking order,
|
|
99
|
+ see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
|
100
|
+ <Island padding={4} style={{ zIndex: 1 }}>
|
|
101
|
+ <Stack.Col gap={4}>
|
|
102
|
+ <Stack.Row gap={1} justifyContent="space-between">
|
|
103
|
+ {actionManager.renderAction("loadScene")}
|
|
104
|
+ {actionManager.renderAction("saveScene")}
|
|
105
|
+ {renderExportDialog()}
|
|
106
|
+ {actionManager.renderAction("clearCanvas")}
|
|
107
|
+ <RoomDialog
|
|
108
|
+ isCollaborating={appState.isCollaborating}
|
|
109
|
+ collaboratorCount={appState.collaborators.size}
|
|
110
|
+ username={appState.username}
|
|
111
|
+ onUsernameChange={onUsernameChange}
|
|
112
|
+ onRoomCreate={onRoomCreate}
|
|
113
|
+ onRoomDestroy={onRoomDestroy}
|
|
114
|
+ />
|
|
115
|
+ </Stack.Row>
|
|
116
|
+ {actionManager.renderAction("changeViewBackgroundColor")}
|
|
117
|
+ </Stack.Col>
|
|
118
|
+ </Island>
|
|
119
|
+ </Section>
|
|
120
|
+ );
|
|
121
|
+
|
|
122
|
+ const renderSelectedShapeActions = () => (
|
|
123
|
+ <Section heading="selectedShapeActions">
|
|
124
|
+ <Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={4}>
|
|
125
|
+ <SelectedShapeActions
|
|
126
|
+ appState={appState}
|
|
127
|
+ elements={elements}
|
|
128
|
+ renderAction={actionManager.renderAction}
|
|
129
|
+ elementType={appState.elementType}
|
|
130
|
+ />
|
|
131
|
+ </Island>
|
|
132
|
+ </Section>
|
|
133
|
+ );
|
|
134
|
+
|
|
135
|
+ const renderFixedSideContainer = () => {
|
|
136
|
+ const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
|
137
|
+ appState,
|
|
138
|
+ elements,
|
|
139
|
+ );
|
|
140
|
+ return (
|
|
141
|
+ <FixedSideContainer side="top">
|
|
142
|
+ <HintViewer appState={appState} elements={elements} />
|
|
143
|
+ <div className="App-menu App-menu_top">
|
|
144
|
+ <Stack.Col gap={4}>
|
|
145
|
+ {renderCanvasActions()}
|
|
146
|
+ {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
|
147
|
+ </Stack.Col>
|
|
148
|
+ <Section heading="shapes">
|
|
149
|
+ {(heading) => (
|
|
150
|
+ <Stack.Col gap={4} align="start">
|
|
151
|
+ <Stack.Row gap={1}>
|
|
152
|
+ <Island padding={1}>
|
|
153
|
+ {heading}
|
|
154
|
+ <Stack.Row gap={1}>
|
|
155
|
+ <ShapesSwitcher
|
|
156
|
+ elementType={appState.elementType}
|
|
157
|
+ setAppState={setAppState}
|
144
|
158
|
/>
|
145
|
159
|
</Stack.Row>
|
146
|
|
- {actionManager.renderAction("changeViewBackgroundColor")}
|
147
|
|
- </Stack.Col>
|
148
|
|
- </Island>
|
149
|
|
- </Section>
|
150
|
|
- {showSelectedShapeActions(appState, elements) && (
|
151
|
|
- <Section heading="selectedShapeActions">
|
152
|
|
- <Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={4}>
|
153
|
|
- <SelectedShapeActions
|
154
|
|
- appState={appState}
|
155
|
|
- elements={elements}
|
156
|
|
- renderAction={actionManager.renderAction}
|
157
|
|
- elementType={appState.elementType}
|
158
|
|
- />
|
159
|
160
|
</Island>
|
160
|
|
- </Section>
|
161
|
|
- )}
|
162
|
|
- </Stack.Col>
|
163
|
|
- <Section heading="shapes">
|
164
|
|
- {(heading) => (
|
165
|
|
- <Stack.Col gap={4} align="start">
|
166
|
|
- <Stack.Row gap={1}>
|
167
|
|
- <Island padding={1}>
|
168
|
|
- {heading}
|
169
|
|
- <Stack.Row gap={1}>
|
170
|
|
- <ShapesSwitcher
|
171
|
|
- elementType={appState.elementType}
|
172
|
|
- setAppState={setAppState}
|
173
|
|
- />
|
174
|
|
- </Stack.Row>
|
175
|
|
- </Island>
|
176
|
|
- <LockIcon
|
177
|
|
- checked={appState.elementLocked}
|
178
|
|
- onChange={onLockToggle}
|
179
|
|
- title={t("toolBar.lock")}
|
180
|
|
- />
|
181
|
|
- </Stack.Row>
|
182
|
|
- </Stack.Col>
|
183
|
|
- )}
|
184
|
|
- </Section>
|
185
|
|
- <div />
|
186
|
|
- </div>
|
187
|
|
- <div className="App-menu App-menu_bottom">
|
188
|
|
- <Stack.Col gap={2}>
|
189
|
|
- <Section heading="canvasActions">
|
190
|
|
- <Island padding={1}>
|
191
|
|
- <ZoomActions
|
192
|
|
- renderAction={actionManager.renderAction}
|
193
|
|
- zoom={appState.zoom}
|
|
161
|
+ <LockIcon
|
|
162
|
+ checked={appState.elementLocked}
|
|
163
|
+ onChange={onLockToggle}
|
|
164
|
+ title={t("toolBar.lock")}
|
194
|
165
|
/>
|
195
|
|
- </Island>
|
196
|
|
- </Section>
|
197
|
|
- </Stack.Col>
|
198
|
|
- </div>
|
199
|
|
- </FixedSideContainer>
|
200
|
|
- <aside>
|
201
|
|
- <GitHubCorner />
|
202
|
|
- </aside>
|
203
|
|
- <footer role="contentinfo">
|
204
|
|
- <LanguageList
|
205
|
|
- onChange={(lng) => {
|
206
|
|
- setLanguage(lng);
|
207
|
|
- setAppState({});
|
208
|
|
- }}
|
209
|
|
- languages={languages}
|
210
|
|
- floating
|
211
|
|
- />
|
212
|
|
- {actionManager.renderAction("toggleShortcuts")}
|
213
|
|
- {appState.scrolledOutside && (
|
214
|
|
- <button
|
215
|
|
- className="scroll-back-to-content"
|
216
|
|
- onClick={() => {
|
217
|
|
- setAppState({ ...calculateScrollCenter(elements) });
|
218
|
|
- }}
|
219
|
|
- >
|
220
|
|
- {t("buttons.scrollBackToContent")}
|
221
|
|
- </button>
|
222
|
|
- )}
|
223
|
|
- </footer>
|
224
|
|
- </>
|
|
166
|
+ </Stack.Row>
|
|
167
|
+ </Stack.Col>
|
|
168
|
+ )}
|
|
169
|
+ </Section>
|
|
170
|
+ <div />
|
|
171
|
+ </div>
|
|
172
|
+ <div className="App-menu App-menu_bottom">
|
|
173
|
+ <Stack.Col gap={2}>
|
|
174
|
+ <Section heading="canvasActions">
|
|
175
|
+ <Island padding={1}>
|
|
176
|
+ <ZoomActions
|
|
177
|
+ renderAction={actionManager.renderAction}
|
|
178
|
+ zoom={appState.zoom}
|
|
179
|
+ />
|
|
180
|
+ </Island>
|
|
181
|
+ </Section>
|
|
182
|
+ </Stack.Col>
|
|
183
|
+ </div>
|
|
184
|
+ </FixedSideContainer>
|
225
|
185
|
);
|
226
|
|
- },
|
227
|
|
- (prev, next) => {
|
228
|
|
- const getNecessaryObj = (appState: AppState): Partial<AppState> => {
|
229
|
|
- const {
|
230
|
|
- draggingElement,
|
231
|
|
- resizingElement,
|
232
|
|
- multiElement,
|
233
|
|
- editingElement,
|
234
|
|
- isResizing,
|
235
|
|
- cursorX,
|
236
|
|
- cursorY,
|
237
|
|
- ...ret
|
238
|
|
- } = appState;
|
239
|
|
- return ret;
|
240
|
|
- };
|
241
|
|
- const prevAppState = getNecessaryObj(prev.appState);
|
242
|
|
- const nextAppState = getNecessaryObj(next.appState);
|
|
186
|
+ };
|
243
|
187
|
|
244
|
|
- const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
|
188
|
+ const renderFooter = () => (
|
|
189
|
+ <footer role="contentinfo">
|
|
190
|
+ <LanguageList
|
|
191
|
+ onChange={(lng) => {
|
|
192
|
+ setLanguage(lng);
|
|
193
|
+ setAppState({});
|
|
194
|
+ }}
|
|
195
|
+ languages={languages}
|
|
196
|
+ floating
|
|
197
|
+ />
|
|
198
|
+ {actionManager.renderAction("toggleShortcuts")}
|
|
199
|
+ {appState.scrolledOutside && (
|
|
200
|
+ <button
|
|
201
|
+ className="scroll-back-to-content"
|
|
202
|
+ onClick={() => {
|
|
203
|
+ setAppState({ ...calculateScrollCenter(elements) });
|
|
204
|
+ }}
|
|
205
|
+ >
|
|
206
|
+ {t("buttons.scrollBackToContent")}
|
|
207
|
+ </button>
|
|
208
|
+ )}
|
|
209
|
+ </footer>
|
|
210
|
+ );
|
245
|
211
|
|
246
|
|
- return (
|
247
|
|
- prev.elements === next.elements &&
|
248
|
|
- keys.every((key) => prevAppState[key] === nextAppState[key])
|
249
|
|
- );
|
250
|
|
- },
|
251
|
|
-);
|
|
212
|
+ return isMobile ? (
|
|
213
|
+ <MobileMenu
|
|
214
|
+ appState={appState}
|
|
215
|
+ elements={elements}
|
|
216
|
+ actionManager={actionManager}
|
|
217
|
+ exportButton={renderExportDialog()}
|
|
218
|
+ setAppState={setAppState}
|
|
219
|
+ onUsernameChange={onUsernameChange}
|
|
220
|
+ onRoomCreate={onRoomCreate}
|
|
221
|
+ onRoomDestroy={onRoomDestroy}
|
|
222
|
+ onLockToggle={onLockToggle}
|
|
223
|
+ />
|
|
224
|
+ ) : (
|
|
225
|
+ <>
|
|
226
|
+ {appState.isLoading && <LoadingMessage />}
|
|
227
|
+ {appState.errorMessage && (
|
|
228
|
+ <ErrorDialog
|
|
229
|
+ message={appState.errorMessage}
|
|
230
|
+ onClose={() => setAppState({ errorMessage: null })}
|
|
231
|
+ />
|
|
232
|
+ )}
|
|
233
|
+ {appState.showShortcutsDialog && (
|
|
234
|
+ <ShortcutsDialog
|
|
235
|
+ onClose={() => setAppState({ showShortcutsDialog: null })}
|
|
236
|
+ />
|
|
237
|
+ )}
|
|
238
|
+ {renderFixedSideContainer()}
|
|
239
|
+ <aside>
|
|
240
|
+ <GitHubCorner />
|
|
241
|
+ </aside>
|
|
242
|
+ {renderFooter()}
|
|
243
|
+ </>
|
|
244
|
+ );
|
|
245
|
+};
|
|
246
|
+
|
|
247
|
+const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
|
248
|
+ const getNecessaryObj = (appState: AppState): Partial<AppState> => {
|
|
249
|
+ const {
|
|
250
|
+ draggingElement,
|
|
251
|
+ resizingElement,
|
|
252
|
+ multiElement,
|
|
253
|
+ editingElement,
|
|
254
|
+ isResizing,
|
|
255
|
+ cursorX,
|
|
256
|
+ cursorY,
|
|
257
|
+ ...ret
|
|
258
|
+ } = appState;
|
|
259
|
+ return ret;
|
|
260
|
+ };
|
|
261
|
+ const prevAppState = getNecessaryObj(prev.appState);
|
|
262
|
+ const nextAppState = getNecessaryObj(next.appState);
|
|
263
|
+
|
|
264
|
+ const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
|
265
|
+
|
|
266
|
+ return (
|
|
267
|
+ prev.elements === next.elements &&
|
|
268
|
+ keys.every((key) => prevAppState[key] === nextAppState[key])
|
|
269
|
+ );
|
|
270
|
+};
|
|
271
|
+
|
|
272
|
+export default React.memo(LayerUI, areEqual);
|