|
@@ -104,6 +104,7 @@ import { LanguageList } from "./components/LanguageList";
|
104
|
104
|
import { Point } from "roughjs/bin/geometry";
|
105
|
105
|
import { t, languages, setLanguage, getLanguage } from "./i18n";
|
106
|
106
|
import { HintViewer } from "./components/HintViewer";
|
|
107
|
+import useIsMobile, { IsMobileProvider } from "./is-mobile";
|
107
|
108
|
|
108
|
109
|
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
|
109
|
110
|
import { normalizeScroll } from "./scene/data";
|
|
@@ -135,6 +136,18 @@ const MOUSE_BUTTON = {
|
135
|
136
|
SECONDARY: 2,
|
136
|
137
|
};
|
137
|
138
|
|
|
139
|
+// Block pinch-zooming on iOS outside of the content area
|
|
140
|
+document.addEventListener(
|
|
141
|
+ "touchmove",
|
|
142
|
+ function(event) {
|
|
143
|
+ // @ts-ignore
|
|
144
|
+ if (event.scale !== 1) {
|
|
145
|
+ event.preventDefault();
|
|
146
|
+ }
|
|
147
|
+ },
|
|
148
|
+ { passive: false },
|
|
149
|
+);
|
|
150
|
+
|
138
|
151
|
let lastMouseUp: ((e: any) => void) | null = null;
|
139
|
152
|
|
140
|
153
|
export function viewportCoordsToSceneCoords(
|
|
@@ -211,64 +224,58 @@ const LayerUI = React.memo(
|
211
|
224
|
language,
|
212
|
225
|
setElements,
|
213
|
226
|
}: LayerUIProps) => {
|
214
|
|
- function renderCanvasActions() {
|
|
227
|
+ const isMobile = useIsMobile();
|
|
228
|
+
|
|
229
|
+ function renderExportDialog() {
|
215
|
230
|
return (
|
216
|
|
- <Stack.Col gap={4}>
|
217
|
|
- <Stack.Row justifyContent={"space-between"}>
|
218
|
|
- {actionManager.renderAction("loadScene")}
|
219
|
|
- {actionManager.renderAction("saveScene")}
|
220
|
|
- <ExportDialog
|
221
|
|
- elements={elements}
|
222
|
|
- appState={appState}
|
223
|
|
- actionManager={actionManager}
|
224
|
|
- onExportToPng={(exportedElements, scale) => {
|
225
|
|
- if (canvas) {
|
226
|
|
- exportCanvas("png", exportedElements, canvas, {
|
227
|
|
- exportBackground: appState.exportBackground,
|
228
|
|
- name: appState.name,
|
229
|
|
- viewBackgroundColor: appState.viewBackgroundColor,
|
230
|
|
- scale,
|
231
|
|
- });
|
232
|
|
- }
|
233
|
|
- }}
|
234
|
|
- onExportToSvg={(exportedElements, scale) => {
|
235
|
|
- if (canvas) {
|
236
|
|
- exportCanvas("svg", exportedElements, canvas, {
|
237
|
|
- exportBackground: appState.exportBackground,
|
238
|
|
- name: appState.name,
|
239
|
|
- viewBackgroundColor: appState.viewBackgroundColor,
|
240
|
|
- scale,
|
241
|
|
- });
|
242
|
|
- }
|
243
|
|
- }}
|
244
|
|
- onExportToClipboard={(exportedElements, scale) => {
|
245
|
|
- if (canvas) {
|
246
|
|
- exportCanvas("clipboard", exportedElements, canvas, {
|
247
|
|
- exportBackground: appState.exportBackground,
|
248
|
|
- name: appState.name,
|
249
|
|
- viewBackgroundColor: appState.viewBackgroundColor,
|
250
|
|
- scale,
|
251
|
|
- });
|
252
|
|
- }
|
253
|
|
- }}
|
254
|
|
- onExportToBackend={exportedElements => {
|
255
|
|
- if (canvas) {
|
256
|
|
- exportCanvas(
|
257
|
|
- "backend",
|
258
|
|
- exportedElements.map(element => ({
|
259
|
|
- ...element,
|
260
|
|
- isSelected: false,
|
261
|
|
- })),
|
262
|
|
- canvas,
|
263
|
|
- appState,
|
264
|
|
- );
|
265
|
|
- }
|
266
|
|
- }}
|
267
|
|
- />
|
268
|
|
- {actionManager.renderAction("clearCanvas")}
|
269
|
|
- </Stack.Row>
|
270
|
|
- {actionManager.renderAction("changeViewBackgroundColor")}
|
271
|
|
- </Stack.Col>
|
|
231
|
+ <ExportDialog
|
|
232
|
+ elements={elements}
|
|
233
|
+ appState={appState}
|
|
234
|
+ actionManager={actionManager}
|
|
235
|
+ onExportToPng={(exportedElements, scale) => {
|
|
236
|
+ if (canvas) {
|
|
237
|
+ exportCanvas("png", exportedElements, canvas, {
|
|
238
|
+ exportBackground: appState.exportBackground,
|
|
239
|
+ name: appState.name,
|
|
240
|
+ viewBackgroundColor: appState.viewBackgroundColor,
|
|
241
|
+ scale,
|
|
242
|
+ });
|
|
243
|
+ }
|
|
244
|
+ }}
|
|
245
|
+ onExportToSvg={(exportedElements, scale) => {
|
|
246
|
+ if (canvas) {
|
|
247
|
+ exportCanvas("svg", exportedElements, canvas, {
|
|
248
|
+ exportBackground: appState.exportBackground,
|
|
249
|
+ name: appState.name,
|
|
250
|
+ viewBackgroundColor: appState.viewBackgroundColor,
|
|
251
|
+ scale,
|
|
252
|
+ });
|
|
253
|
+ }
|
|
254
|
+ }}
|
|
255
|
+ onExportToClipboard={(exportedElements, scale) => {
|
|
256
|
+ if (canvas) {
|
|
257
|
+ exportCanvas("clipboard", exportedElements, canvas, {
|
|
258
|
+ exportBackground: appState.exportBackground,
|
|
259
|
+ name: appState.name,
|
|
260
|
+ viewBackgroundColor: appState.viewBackgroundColor,
|
|
261
|
+ scale,
|
|
262
|
+ });
|
|
263
|
+ }
|
|
264
|
+ }}
|
|
265
|
+ onExportToBackend={exportedElements => {
|
|
266
|
+ if (canvas) {
|
|
267
|
+ exportCanvas(
|
|
268
|
+ "backend",
|
|
269
|
+ exportedElements.map(element => ({
|
|
270
|
+ ...element,
|
|
271
|
+ isSelected: false,
|
|
272
|
+ })),
|
|
273
|
+ canvas,
|
|
274
|
+ appState,
|
|
275
|
+ );
|
|
276
|
+ }
|
|
277
|
+ }}
|
|
278
|
+ />
|
272
|
279
|
);
|
273
|
280
|
}
|
274
|
281
|
|
|
@@ -284,51 +291,49 @@ const LayerUI = React.memo(
|
284
|
291
|
}
|
285
|
292
|
|
286
|
293
|
return (
|
287
|
|
- <Island padding={4}>
|
288
|
|
- <div className="panelColumn">
|
289
|
|
- {actionManager.renderAction("changeStrokeColor")}
|
290
|
|
- {(hasBackground(elementType) ||
|
291
|
|
- targetElements.some(element => hasBackground(element.type))) && (
|
292
|
|
- <>
|
293
|
|
- {actionManager.renderAction("changeBackgroundColor")}
|
294
|
|
-
|
295
|
|
- {actionManager.renderAction("changeFillStyle")}
|
296
|
|
- </>
|
297
|
|
- )}
|
|
294
|
+ <div className="panelColumn">
|
|
295
|
+ {actionManager.renderAction("changeStrokeColor")}
|
|
296
|
+ {(hasBackground(elementType) ||
|
|
297
|
+ targetElements.some(element => hasBackground(element.type))) && (
|
|
298
|
+ <>
|
|
299
|
+ {actionManager.renderAction("changeBackgroundColor")}
|
|
300
|
+
|
|
301
|
+ {actionManager.renderAction("changeFillStyle")}
|
|
302
|
+ </>
|
|
303
|
+ )}
|
298
|
304
|
|
299
|
|
- {(hasStroke(elementType) ||
|
300
|
|
- targetElements.some(element => hasStroke(element.type))) && (
|
301
|
|
- <>
|
302
|
|
- {actionManager.renderAction("changeStrokeWidth")}
|
|
305
|
+ {(hasStroke(elementType) ||
|
|
306
|
+ targetElements.some(element => hasStroke(element.type))) && (
|
|
307
|
+ <>
|
|
308
|
+ {actionManager.renderAction("changeStrokeWidth")}
|
303
|
309
|
|
304
|
|
- {actionManager.renderAction("changeSloppiness")}
|
305
|
|
- </>
|
306
|
|
- )}
|
|
310
|
+ {actionManager.renderAction("changeSloppiness")}
|
|
311
|
+ </>
|
|
312
|
+ )}
|
307
|
313
|
|
308
|
|
- {(hasText(elementType) ||
|
309
|
|
- targetElements.some(element => hasText(element.type))) && (
|
310
|
|
- <>
|
311
|
|
- {actionManager.renderAction("changeFontSize")}
|
|
314
|
+ {(hasText(elementType) ||
|
|
315
|
+ targetElements.some(element => hasText(element.type))) && (
|
|
316
|
+ <>
|
|
317
|
+ {actionManager.renderAction("changeFontSize")}
|
312
|
318
|
|
313
|
|
- {actionManager.renderAction("changeFontFamily")}
|
314
|
|
- </>
|
315
|
|
- )}
|
|
319
|
+ {actionManager.renderAction("changeFontFamily")}
|
|
320
|
+ </>
|
|
321
|
+ )}
|
316
|
322
|
|
317
|
|
- {actionManager.renderAction("changeOpacity")}
|
|
323
|
+ {actionManager.renderAction("changeOpacity")}
|
318
|
324
|
|
319
|
|
- <fieldset>
|
320
|
|
- <legend>{t("labels.layers")}</legend>
|
321
|
|
- <div className="buttonList">
|
322
|
|
- {actionManager.renderAction("sendToBack")}
|
323
|
|
- {actionManager.renderAction("sendBackward")}
|
324
|
|
- {actionManager.renderAction("bringToFront")}
|
325
|
|
- {actionManager.renderAction("bringForward")}
|
326
|
|
- </div>
|
327
|
|
- </fieldset>
|
|
325
|
+ <fieldset>
|
|
326
|
+ <legend>{t("labels.layers")}</legend>
|
|
327
|
+ <div className="buttonList">
|
|
328
|
+ {actionManager.renderAction("sendToBack")}
|
|
329
|
+ {actionManager.renderAction("sendBackward")}
|
|
330
|
+ {actionManager.renderAction("bringToFront")}
|
|
331
|
+ {actionManager.renderAction("bringForward")}
|
|
332
|
+ </div>
|
|
333
|
+ </fieldset>
|
328
|
334
|
|
329
|
|
- {actionManager.renderAction("deleteSelectedElements")}
|
330
|
|
- </div>
|
331
|
|
- </Island>
|
|
335
|
+ {actionManager.renderAction("deleteSelectedElements")}
|
|
336
|
+ </div>
|
332
|
337
|
);
|
333
|
338
|
}
|
334
|
339
|
|
|
@@ -378,7 +383,125 @@ const LayerUI = React.memo(
|
378
|
383
|
);
|
379
|
384
|
}
|
380
|
385
|
|
381
|
|
- return (
|
|
386
|
+ const lockButton = (
|
|
387
|
+ <LockIcon
|
|
388
|
+ checked={appState.elementLocked}
|
|
389
|
+ onChange={() => {
|
|
390
|
+ setAppState({
|
|
391
|
+ elementLocked: !appState.elementLocked,
|
|
392
|
+ elementType: appState.elementLocked
|
|
393
|
+ ? "selection"
|
|
394
|
+ : appState.elementType,
|
|
395
|
+ });
|
|
396
|
+ }}
|
|
397
|
+ title={t("toolBar.lock")}
|
|
398
|
+ />
|
|
399
|
+ );
|
|
400
|
+
|
|
401
|
+ return isMobile ? (
|
|
402
|
+ <>
|
|
403
|
+ {appState.openedMenu === "canvas" ? (
|
|
404
|
+ <section
|
|
405
|
+ className="App-mobile-menu"
|
|
406
|
+ aria-labelledby="canvas-actions-title"
|
|
407
|
+ >
|
|
408
|
+ <h2 className="visually-hidden" id="canvas-actions-title">
|
|
409
|
+ {t("headings.canvasActions")}
|
|
410
|
+ </h2>
|
|
411
|
+ <div className="App-mobile-menu-scroller">
|
|
412
|
+ <Stack.Col gap={4}>
|
|
413
|
+ {actionManager.renderAction("loadScene")}
|
|
414
|
+ {actionManager.renderAction("saveScene")}
|
|
415
|
+ {renderExportDialog()}
|
|
416
|
+ {actionManager.renderAction("clearCanvas")}
|
|
417
|
+ {actionManager.renderAction("changeViewBackgroundColor")}
|
|
418
|
+ </Stack.Col>
|
|
419
|
+ </div>
|
|
420
|
+ </section>
|
|
421
|
+ ) : appState.openedMenu === "shape" ? (
|
|
422
|
+ <section
|
|
423
|
+ className="App-mobile-menu"
|
|
424
|
+ aria-labelledby="selected-shape-title"
|
|
425
|
+ >
|
|
426
|
+ <h2 className="visually-hidden" id="selected-shape-title">
|
|
427
|
+ {t("headings.selectedShapeActions")}
|
|
428
|
+ </h2>
|
|
429
|
+ <div className="App-mobile-menu-scroller">
|
|
430
|
+ {renderSelectedShapeActions(elements)}
|
|
431
|
+ </div>
|
|
432
|
+ </section>
|
|
433
|
+ ) : null}
|
|
434
|
+ <FixedSideContainer side="top">
|
|
435
|
+ <section aria-labelledby="shapes-title">
|
|
436
|
+ <Stack.Col gap={4} align="center">
|
|
437
|
+ <Stack.Row gap={1}>
|
|
438
|
+ <Island padding={1}>
|
|
439
|
+ <h2 className="visually-hidden" id="shapes-title">
|
|
440
|
+ {t("headings.shapes")}
|
|
441
|
+ </h2>
|
|
442
|
+ <Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
|
|
443
|
+ </Island>
|
|
444
|
+ </Stack.Row>
|
|
445
|
+ </Stack.Col>
|
|
446
|
+ </section>
|
|
447
|
+ </FixedSideContainer>
|
|
448
|
+ <footer className="App-toolbar">
|
|
449
|
+ <div className="App-toolbar-content">
|
|
450
|
+ <ToolButton
|
|
451
|
+ type="button"
|
|
452
|
+ icon={
|
|
453
|
+ <span style={{ fontSize: "2em", marginTop: "-0.15em" }}>☰</span>
|
|
454
|
+ }
|
|
455
|
+ aria-label={t("buttons.menu")}
|
|
456
|
+ onClick={() =>
|
|
457
|
+ setAppState(({ openedMenu }: any) => ({
|
|
458
|
+ openedMenu: openedMenu === "canvas" ? null : "canvas",
|
|
459
|
+ }))
|
|
460
|
+ }
|
|
461
|
+ />
|
|
462
|
+ {lockButton}
|
|
463
|
+ <div
|
|
464
|
+ style={{
|
|
465
|
+ visibility: isSomeElementSelected(elements)
|
|
466
|
+ ? "visible"
|
|
467
|
+ : "hidden",
|
|
468
|
+ }}
|
|
469
|
+ >
|
|
470
|
+ <ToolButton
|
|
471
|
+ type="button"
|
|
472
|
+ icon={
|
|
473
|
+ <span style={{ fontSize: "2em", marginTop: "-0.15em" }}>
|
|
474
|
+ ✎
|
|
475
|
+ </span>
|
|
476
|
+ }
|
|
477
|
+ aria-label={t("buttons.menu")}
|
|
478
|
+ onClick={() =>
|
|
479
|
+ setAppState(({ openedMenu }: any) => ({
|
|
480
|
+ openedMenu: openedMenu === "shape" ? null : "shape",
|
|
481
|
+ }))
|
|
482
|
+ }
|
|
483
|
+ />
|
|
484
|
+ </div>
|
|
485
|
+ <HintViewer
|
|
486
|
+ elementType={appState.elementType}
|
|
487
|
+ multiMode={appState.multiElement !== null}
|
|
488
|
+ isResizing={appState.isResizing}
|
|
489
|
+ elements={elements}
|
|
490
|
+ />
|
|
491
|
+ {appState.scrolledOutside && (
|
|
492
|
+ <button
|
|
493
|
+ className="scroll-back-to-content"
|
|
494
|
+ onClick={() => {
|
|
495
|
+ setAppState({ ...calculateScrollCenter(elements) });
|
|
496
|
+ }}
|
|
497
|
+ >
|
|
498
|
+ {t("buttons.scrollBackToContent")}
|
|
499
|
+ </button>
|
|
500
|
+ )}
|
|
501
|
+ </div>
|
|
502
|
+ </footer>
|
|
503
|
+ </>
|
|
504
|
+ ) : (
|
382
|
505
|
<>
|
383
|
506
|
<FixedSideContainer side="top">
|
384
|
507
|
<div className="App-menu App-menu_top">
|
|
@@ -390,7 +513,17 @@ const LayerUI = React.memo(
|
390
|
513
|
<h2 className="visually-hidden" id="canvas-actions-title">
|
391
|
514
|
{t("headings.canvasActions")}
|
392
|
515
|
</h2>
|
393
|
|
- <Island padding={4}>{renderCanvasActions()}</Island>
|
|
516
|
+ <Island padding={4}>
|
|
517
|
+ <Stack.Col gap={4}>
|
|
518
|
+ <Stack.Row justifyContent={"space-between"}>
|
|
519
|
+ {actionManager.renderAction("loadScene")}
|
|
520
|
+ {actionManager.renderAction("saveScene")}
|
|
521
|
+ {renderExportDialog()}
|
|
522
|
+ {actionManager.renderAction("clearCanvas")}
|
|
523
|
+ </Stack.Row>
|
|
524
|
+ {actionManager.renderAction("changeViewBackgroundColor")}
|
|
525
|
+ </Stack.Col>
|
|
526
|
+ </Island>
|
394
|
527
|
</section>
|
395
|
528
|
<section
|
396
|
529
|
className="App-right-menu"
|
|
@@ -399,7 +532,9 @@ const LayerUI = React.memo(
|
399
|
532
|
<h2 className="visually-hidden" id="selected-shape-title">
|
400
|
533
|
{t("headings.selectedShapeActions")}
|
401
|
534
|
</h2>
|
402
|
|
- {renderSelectedShapeActions(elements)}
|
|
535
|
+ <Island padding={4}>
|
|
536
|
+ {renderSelectedShapeActions(elements)}
|
|
537
|
+ </Island>
|
403
|
538
|
</section>
|
404
|
539
|
</Stack.Col>
|
405
|
540
|
<section aria-labelledby="shapes-title">
|
|
@@ -411,18 +546,7 @@ const LayerUI = React.memo(
|
411
|
546
|
</h2>
|
412
|
547
|
<Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
|
413
|
548
|
</Island>
|
414
|
|
- <LockIcon
|
415
|
|
- checked={appState.elementLocked}
|
416
|
|
- onChange={() => {
|
417
|
|
- setAppState({
|
418
|
|
- elementLocked: !appState.elementLocked,
|
419
|
|
- elementType: appState.elementLocked
|
420
|
|
- ? "selection"
|
421
|
|
- : appState.elementType,
|
422
|
|
- });
|
423
|
|
- }}
|
424
|
|
- title={t("toolBar.lock")}
|
425
|
|
- />
|
|
549
|
+ {lockButton}
|
426
|
550
|
</Stack.Row>
|
427
|
551
|
</Stack.Col>
|
428
|
552
|
</section>
|
|
@@ -2204,7 +2328,9 @@ class TopErrorBoundary extends React.Component {
|
2204
|
2328
|
|
2205
|
2329
|
ReactDOM.render(
|
2206
|
2330
|
<TopErrorBoundary>
|
2207
|
|
- <App />
|
|
2331
|
+ <IsMobileProvider>
|
|
2332
|
+ <App />
|
|
2333
|
+ </IsMobileProvider>
|
2208
|
2334
|
</TopErrorBoundary>,
|
2209
|
2335
|
rootElement,
|
2210
|
2336
|
);
|