@plone-collective/volto-image-editor 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.changelog.draft +19 -0
  2. package/.release-it.json +34 -0
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +209 -0
  5. package/babel.config.js +17 -0
  6. package/locales/de/LC_MESSAGES/volto.po +187 -0
  7. package/locales/en/LC_MESSAGES/volto.po +187 -0
  8. package/locales/es/LC_MESSAGES/volto.po +194 -0
  9. package/locales/pt_BR/LC_MESSAGES/volto.po +192 -0
  10. package/locales/volto.pot +189 -0
  11. package/news/.gitkeep +0 -0
  12. package/package.json +50 -0
  13. package/public/.gitkeep +0 -0
  14. package/src/components/.gitkeep +0 -0
  15. package/src/components/ImageEditor/ImageEditor.scss +59 -0
  16. package/src/components/ImageEditor/ImageEditor.tsx +290 -0
  17. package/src/components/ImageEditor/ImageEditorWrapper.scss +13 -0
  18. package/src/components/ImageEditor/ImageEditorWrapper.tsx +91 -0
  19. package/src/components/ImageEditor/components/AdjustableCropperBackground.tsx +60 -0
  20. package/src/components/ImageEditor/components/AdjustableImage.scss +3 -0
  21. package/src/components/ImageEditor/components/AdjustableImage.tsx +85 -0
  22. package/src/components/ImageEditor/components/AdjustablePreviewBackground.tsx +59 -0
  23. package/src/components/ImageEditor/components/Button.scss +30 -0
  24. package/src/components/ImageEditor/components/Button.tsx +27 -0
  25. package/src/components/ImageEditor/components/Navigation.scss +38 -0
  26. package/src/components/ImageEditor/components/Navigation.tsx +153 -0
  27. package/src/components/ImageEditor/components/SettingsModal.scss +369 -0
  28. package/src/components/ImageEditor/components/SettingsModal.tsx +582 -0
  29. package/src/components/ImageEditor/components/Slider.scss +231 -0
  30. package/src/components/ImageEditor/components/Slider.tsx +322 -0
  31. package/src/components/ImageEditor/types/ImageSettings.ts +23 -0
  32. package/src/components/Widgets/FileWidget.scss +14 -0
  33. package/src/components/Widgets/FileWidget.tsx +239 -0
  34. package/src/config/settings.ts +5 -0
  35. package/src/customizations/volto/components/manage/Widgets/FileWidget.jsx +3 -0
  36. package/src/icons/AspectRatioIcons.tsx +39 -0
  37. package/src/icons/BrightnessIcon.tsx +19 -0
  38. package/src/icons/ContrastIcon.tsx +19 -0
  39. package/src/icons/CropIcon.tsx +19 -0
  40. package/src/icons/DownloadIcon.tsx +20 -0
  41. package/src/icons/FlipHorizontalIcon.tsx +21 -0
  42. package/src/icons/FlipVerticalIcon.tsx +27 -0
  43. package/src/icons/HueIcon.tsx +20 -0
  44. package/src/icons/ResetIcon.tsx +19 -0
  45. package/src/icons/ResetRotationIcon.tsx +20 -0
  46. package/src/icons/RestrictionIcons.tsx +19 -0
  47. package/src/icons/RotateLeftIcon.tsx +21 -0
  48. package/src/icons/RotateRightIcon.tsx +21 -0
  49. package/src/icons/SaturationIcon.tsx +20 -0
  50. package/src/icons/SettingsIcon.tsx +21 -0
  51. package/src/icons/StencilIcons.tsx +8 -0
  52. package/src/index.ts +10 -0
  53. package/src/theme/constants.scss +13 -0
  54. package/src/theme/custom.css +82 -0
  55. package/src/theme/mixins.scss +91 -0
  56. package/towncrier.toml +33 -0
  57. package/tsconfig.json +30 -0
@@ -0,0 +1,582 @@
1
+ /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/label-has-associated-control */
2
+ import React, { FC } from 'react';
3
+ import cn from 'classnames';
4
+ import { defineMessages, useIntl } from 'react-intl';
5
+ import { Button } from './Button';
6
+ import { SettingsIcon } from '@plone-collective/volto-image-editor/icons/SettingsIcon';
7
+ import type { ImageSettings } from '../types/ImageSettings';
8
+ import {
9
+ FreeIcon,
10
+ SquareIcon,
11
+ PortraitIcon,
12
+ LandscapeIcon,
13
+ PhotoIcon,
14
+ MonitorIcon,
15
+ WidescreenIcon,
16
+ MobileIcon,
17
+ UltraWideIcon,
18
+ } from '@plone-collective/volto-image-editor/icons/AspectRatioIcons';
19
+ import {
20
+ FitAreaIcon,
21
+ FillAreaIcon,
22
+ StencilIcon,
23
+ NoRestrictionIcon,
24
+ } from '@plone-collective/volto-image-editor/icons/RestrictionIcons';
25
+ import {
26
+ RectangleStencilIcon,
27
+ CircleStencilIcon,
28
+ } from '@plone-collective/volto-image-editor/icons/StencilIcons';
29
+ import './SettingsModal.scss';
30
+
31
+ const messages = defineMessages({
32
+ editorSettings: {
33
+ id: 'Editor Settings',
34
+ defaultMessage: 'Editor Settings',
35
+ },
36
+ aspectRatio: {
37
+ id: 'Aspect Ratio',
38
+ defaultMessage: 'Aspect Ratio',
39
+ },
40
+ freeAspectRatio: {
41
+ id: 'Free aspect ratio',
42
+ defaultMessage: 'Free',
43
+ },
44
+ square: {
45
+ id: 'Square (1:1)',
46
+ defaultMessage: '1:1',
47
+ },
48
+ photo: {
49
+ id: 'Photo (3:4)',
50
+ defaultMessage: '3:4',
51
+ },
52
+ monitor: {
53
+ id: 'Monitor (4:3)',
54
+ defaultMessage: '4:3',
55
+ },
56
+ widescreen: {
57
+ id: 'Widescreen (16:9)',
58
+ defaultMessage: '16:9',
59
+ },
60
+ mobile: {
61
+ id: 'Mobile (9:16)',
62
+ defaultMessage: '9:16',
63
+ },
64
+ landscape: {
65
+ id: 'Landscape (2:1)',
66
+ defaultMessage: '2:1',
67
+ },
68
+ ultraWide: {
69
+ id: 'Ultra Wide (21:9)',
70
+ defaultMessage: '21:9',
71
+ },
72
+ portrait: {
73
+ id: 'Portrait (1:2)',
74
+ defaultMessage: '1:2',
75
+ },
76
+ imageRestriction: {
77
+ id: 'Image Restriction',
78
+ defaultMessage: 'Image Restriction',
79
+ },
80
+ fitArea: {
81
+ id: 'Fit Area',
82
+ defaultMessage: 'Fit',
83
+ },
84
+ fillArea: {
85
+ id: 'Fill Area',
86
+ defaultMessage: 'Fill',
87
+ },
88
+ stencilRestriction: {
89
+ id: 'Stencil',
90
+ defaultMessage: 'Stencil',
91
+ },
92
+ noRestriction: {
93
+ id: 'No Restriction',
94
+ defaultMessage: 'Free',
95
+ },
96
+ stencilType: {
97
+ id: 'Stencil Type',
98
+ defaultMessage: 'Stencil Type',
99
+ },
100
+ rectangle: {
101
+ id: 'Rectangle',
102
+ defaultMessage: 'Rectangle',
103
+ },
104
+ circle: {
105
+ id: 'Circle',
106
+ defaultMessage: 'Circle',
107
+ },
108
+ minDimensions: {
109
+ id: 'Minimum Dimensions',
110
+ defaultMessage: 'Minimum Dimensions',
111
+ },
112
+ minWidth: {
113
+ id: 'Min width',
114
+ defaultMessage: 'Min width',
115
+ },
116
+ minHeight: {
117
+ id: 'Min height',
118
+ defaultMessage: 'Min height',
119
+ },
120
+ maxCropDimensions: {
121
+ id: 'Maximum Crop Dimensions',
122
+ defaultMessage: 'Maximum Crop Dimensions',
123
+ },
124
+ maxWidth: {
125
+ id: 'Max width',
126
+ defaultMessage: 'Max width',
127
+ },
128
+ maxHeight: {
129
+ id: 'Max height',
130
+ defaultMessage: 'Max height',
131
+ },
132
+ stencilGrid: {
133
+ id: 'Stencil Grid',
134
+ defaultMessage: 'Stencil Grid',
135
+ },
136
+ });
137
+
138
+ interface Props {
139
+ className?: string;
140
+ settings: ImageSettings;
141
+ onChange: (settings: ImageSettings) => void;
142
+ isOpen: boolean;
143
+ onToggle: () => void;
144
+ }
145
+
146
+ export const SettingsModal: FC<Props> = ({
147
+ className,
148
+ settings,
149
+ onChange,
150
+ isOpen,
151
+ onToggle,
152
+ }) => {
153
+ const intl = useIntl();
154
+
155
+ const handleSettingChange = (key: keyof ImageSettings, value: any) => {
156
+ onChange({
157
+ ...settings,
158
+ [key]: value,
159
+ });
160
+ };
161
+
162
+ const handleBackdropClick = (e: React.MouseEvent) => {
163
+ if (e.target === e.currentTarget) {
164
+ onToggle();
165
+ }
166
+ };
167
+
168
+ return (
169
+ <>
170
+ <Button
171
+ className={cn('image-editor-settings-modal__toggle', className)}
172
+ onClick={onToggle}
173
+ active={isOpen}
174
+ >
175
+ <SettingsIcon />
176
+ </Button>
177
+
178
+ {isOpen && (
179
+ <div
180
+ className="image-editor-settings-modal__overlay"
181
+ tabIndex={-1}
182
+ onClick={handleBackdropClick}
183
+ onKeyDown={(e) => {
184
+ if (e.key === 'Escape') {
185
+ onToggle();
186
+ }
187
+ }}
188
+ >
189
+ <div
190
+ className="image-editor-settings-modal__content"
191
+ role="dialog"
192
+ aria-modal="true"
193
+ aria-labelledby="settings-modal-title"
194
+ >
195
+ <div className="image-editor-settings-modal__header">
196
+ <h2
197
+ id="settings-modal-title"
198
+ className="image-editor-settings-modal__title"
199
+ >
200
+ {intl.formatMessage(messages.editorSettings)}
201
+ </h2>
202
+ <Button
203
+ className="image-editor-settings-modal__close"
204
+ onClick={onToggle}
205
+ >
206
+ ✕
207
+ </Button>
208
+ </div>
209
+
210
+ <div className="image-editor-settings-modal__body">
211
+ {/* Aspect Ratio */}
212
+ <div className="image-editor-settings-modal__section">
213
+ <div
214
+ className="image-editor-settings-modal__label"
215
+ id="aspect-ratio-label"
216
+ >
217
+ {intl.formatMessage(messages.aspectRatio)}
218
+ </div>
219
+ <div
220
+ className="image-editor-settings-modal__icon-scroll"
221
+ role="group"
222
+ aria-labelledby="aspect-ratio-label"
223
+ >
224
+ <div
225
+ className={cn(
226
+ 'image-editor-settings-modal__icon-option',
227
+ settings.aspectRatio === 'free' && 'active',
228
+ )}
229
+ role="button"
230
+ tabIndex={0}
231
+ aria-label={intl.formatMessage(messages.freeAspectRatio)}
232
+ aria-pressed={settings.aspectRatio === 'free'}
233
+ onClick={() => handleSettingChange('aspectRatio', 'free')}
234
+ onKeyDown={(e) => {
235
+ if (e.key === 'Enter' || e.key === ' ') {
236
+ e.preventDefault();
237
+ handleSettingChange('aspectRatio', 'free');
238
+ }
239
+ }}
240
+ >
241
+ <FreeIcon />
242
+ <span>{intl.formatMessage(messages.freeAspectRatio)}</span>
243
+ </div>
244
+ <div
245
+ className={cn(
246
+ 'image-editor-settings-modal__icon-option',
247
+ settings.aspectRatio === '1:1' && 'active',
248
+ )}
249
+ role="button"
250
+ tabIndex={0}
251
+ aria-label={intl.formatMessage(messages.square)}
252
+ aria-pressed={settings.aspectRatio === '1:1'}
253
+ onClick={() => handleSettingChange('aspectRatio', '1:1')}
254
+ onKeyDown={(e) => {
255
+ if (e.key === 'Enter' || e.key === ' ') {
256
+ e.preventDefault();
257
+ handleSettingChange('aspectRatio', '1:1');
258
+ }
259
+ }}
260
+ >
261
+ <SquareIcon />
262
+ <span>{intl.formatMessage(messages.square)}</span>
263
+ </div>
264
+ <div
265
+ className={cn(
266
+ 'image-editor-settings-modal__icon-option',
267
+ settings.aspectRatio === '3:4' && 'active',
268
+ )}
269
+ role="button"
270
+ tabIndex={0}
271
+ aria-label={intl.formatMessage(messages.photo)}
272
+ aria-pressed={settings.aspectRatio === '3:4'}
273
+ onClick={() => handleSettingChange('aspectRatio', '3:4')}
274
+ onKeyDown={(e) => {
275
+ if (e.key === 'Enter' || e.key === ' ') {
276
+ e.preventDefault();
277
+ handleSettingChange('aspectRatio', '3:4');
278
+ }
279
+ }}
280
+ >
281
+ <PhotoIcon />
282
+ <span>{intl.formatMessage(messages.photo)}</span>
283
+ </div>
284
+ <div
285
+ className={cn(
286
+ 'image-editor-settings-modal__icon-option',
287
+ settings.aspectRatio === '4:3' && 'active',
288
+ )}
289
+ role="button"
290
+ tabIndex={0}
291
+ aria-label={intl.formatMessage(messages.monitor)}
292
+ aria-pressed={settings.aspectRatio === '4:3'}
293
+ onClick={() => handleSettingChange('aspectRatio', '4:3')}
294
+ onKeyDown={(e) => {
295
+ if (e.key === 'Enter' || e.key === ' ') {
296
+ e.preventDefault();
297
+ handleSettingChange('aspectRatio', '4:3');
298
+ }
299
+ }}
300
+ >
301
+ <MonitorIcon />
302
+ <span>{intl.formatMessage(messages.monitor)}</span>
303
+ </div>
304
+ <div
305
+ className={cn(
306
+ 'image-editor-settings-modal__icon-option',
307
+ settings.aspectRatio === '16:9' && 'active',
308
+ )}
309
+ role="button"
310
+ tabIndex={0}
311
+ aria-label={intl.formatMessage(messages.widescreen)}
312
+ aria-pressed={settings.aspectRatio === '16:9'}
313
+ onClick={() => handleSettingChange('aspectRatio', '16:9')}
314
+ onKeyDown={(e) => {
315
+ if (e.key === 'Enter' || e.key === ' ') {
316
+ e.preventDefault();
317
+ handleSettingChange('aspectRatio', '16:9');
318
+ }
319
+ }}
320
+ >
321
+ <WidescreenIcon />
322
+ <span>{intl.formatMessage(messages.widescreen)}</span>
323
+ </div>
324
+ <div
325
+ className={cn(
326
+ 'image-editor-settings-modal__icon-option',
327
+ settings.aspectRatio === '9:16' && 'active',
328
+ )}
329
+ role="button"
330
+ tabIndex={0}
331
+ aria-label={intl.formatMessage(messages.mobile)}
332
+ aria-pressed={settings.aspectRatio === '9:16'}
333
+ onClick={() => handleSettingChange('aspectRatio', '9:16')}
334
+ onKeyDown={(e) => {
335
+ if (e.key === 'Enter' || e.key === ' ') {
336
+ e.preventDefault();
337
+ handleSettingChange('aspectRatio', '9:16');
338
+ }
339
+ }}
340
+ >
341
+ <MobileIcon />
342
+ <span>{intl.formatMessage(messages.mobile)}</span>
343
+ </div>
344
+ <div
345
+ className={cn(
346
+ 'image-editor-settings-modal__icon-option',
347
+ settings.aspectRatio === '2:1' && 'active',
348
+ )}
349
+ role="button"
350
+ tabIndex={0}
351
+ aria-label={intl.formatMessage(messages.landscape)}
352
+ aria-pressed={settings.aspectRatio === '2:1'}
353
+ onClick={() => handleSettingChange('aspectRatio', '2:1')}
354
+ onKeyDown={(e) => {
355
+ if (e.key === 'Enter' || e.key === ' ') {
356
+ e.preventDefault();
357
+ handleSettingChange('aspectRatio', '2:1');
358
+ }
359
+ }}
360
+ >
361
+ <LandscapeIcon />
362
+ <span>{intl.formatMessage(messages.landscape)}</span>
363
+ </div>
364
+ <div
365
+ className={cn(
366
+ 'image-editor-settings-modal__icon-option',
367
+ settings.aspectRatio === '21:9' && 'active',
368
+ )}
369
+ role="button"
370
+ tabIndex={0}
371
+ aria-label={intl.formatMessage(messages.ultraWide)}
372
+ aria-pressed={settings.aspectRatio === '21:9'}
373
+ onClick={() => handleSettingChange('aspectRatio', '21:9')}
374
+ onKeyDown={(e) => {
375
+ if (e.key === 'Enter' || e.key === ' ') {
376
+ e.preventDefault();
377
+ handleSettingChange('aspectRatio', '21:9');
378
+ }
379
+ }}
380
+ >
381
+ <UltraWideIcon />
382
+ <span>{intl.formatMessage(messages.ultraWide)}</span>
383
+ </div>
384
+ <div
385
+ className={cn(
386
+ 'image-editor-settings-modal__icon-option',
387
+ settings.aspectRatio === '1:2' && 'active',
388
+ )}
389
+ role="button"
390
+ tabIndex={0}
391
+ aria-label={intl.formatMessage(messages.portrait)}
392
+ aria-pressed={settings.aspectRatio === '1:2'}
393
+ onClick={() => handleSettingChange('aspectRatio', '1:2')}
394
+ onKeyDown={(e) => {
395
+ if (e.key === 'Enter' || e.key === ' ') {
396
+ e.preventDefault();
397
+ handleSettingChange('aspectRatio', '1:2');
398
+ }
399
+ }}
400
+ >
401
+ <PortraitIcon />
402
+ <span>{intl.formatMessage(messages.portrait)}</span>
403
+ </div>
404
+ </div>
405
+ </div>
406
+
407
+ <div className="image-editor-settings-modal__section">
408
+ <label className="image-editor-settings-modal__label">
409
+ {intl.formatMessage(messages.imageRestriction)}
410
+ </label>
411
+ <div className="image-editor-settings-modal__icon-scroll">
412
+ <div
413
+ className={cn(
414
+ 'image-editor-settings-modal__icon-option',
415
+ settings.imageRestriction === 'fit-area' && 'active',
416
+ )}
417
+ onClick={() =>
418
+ handleSettingChange('imageRestriction', 'fit-area')
419
+ }
420
+ >
421
+ <FitAreaIcon />
422
+ <span>{intl.formatMessage(messages.fitArea)}</span>
423
+ </div>
424
+ <div
425
+ className={cn(
426
+ 'image-editor-settings-modal__icon-option',
427
+ settings.imageRestriction === 'fill-area' && 'active',
428
+ )}
429
+ onClick={() =>
430
+ handleSettingChange('imageRestriction', 'fill-area')
431
+ }
432
+ >
433
+ <FillAreaIcon />
434
+ <span>{intl.formatMessage(messages.fillArea)}</span>
435
+ </div>
436
+ <div
437
+ className={cn(
438
+ 'image-editor-settings-modal__icon-option',
439
+ settings.imageRestriction === 'stencil' && 'active',
440
+ )}
441
+ onClick={() =>
442
+ handleSettingChange('imageRestriction', 'stencil')
443
+ }
444
+ >
445
+ <StencilIcon />
446
+ <span>
447
+ {intl.formatMessage(messages.stencilRestriction)}
448
+ </span>
449
+ </div>
450
+ <div
451
+ className={cn(
452
+ 'image-editor-settings-modal__icon-option',
453
+ settings.imageRestriction === 'none' && 'active',
454
+ )}
455
+ onClick={() =>
456
+ handleSettingChange('imageRestriction', 'none')
457
+ }
458
+ >
459
+ <NoRestrictionIcon />
460
+ <span>{intl.formatMessage(messages.noRestriction)}</span>
461
+ </div>
462
+ </div>
463
+ </div>
464
+
465
+ <div className="image-editor-settings-modal__section">
466
+ <label className="image-editor-settings-modal__label">
467
+ {intl.formatMessage(messages.stencilType)}
468
+ </label>
469
+ <div className="image-editor-settings-modal__icon-scroll">
470
+ <div
471
+ className={cn(
472
+ 'image-editor-settings-modal__icon-option',
473
+ settings.stencilType === 'rectangle' && 'active',
474
+ )}
475
+ onClick={() =>
476
+ handleSettingChange('stencilType', 'rectangle')
477
+ }
478
+ >
479
+ <RectangleStencilIcon />
480
+ <span>{intl.formatMessage(messages.rectangle)}</span>
481
+ </div>
482
+ <div
483
+ className={cn(
484
+ 'image-editor-settings-modal__icon-option',
485
+ settings.stencilType === 'circle' && 'active',
486
+ )}
487
+ onClick={() => handleSettingChange('stencilType', 'circle')}
488
+ >
489
+ <CircleStencilIcon />
490
+ <span>{intl.formatMessage(messages.circle)}</span>
491
+ </div>
492
+ </div>
493
+ </div>
494
+
495
+ <div className="image-editor-settings-modal__section">
496
+ <label className="image-editor-settings-modal__label">
497
+ {intl.formatMessage(messages.minDimensions)}
498
+ </label>
499
+ <div className="image-editor-settings-modal__row">
500
+ <input
501
+ type="number"
502
+ value={settings.minWidth}
503
+ onChange={(e) =>
504
+ handleSettingChange(
505
+ 'minWidth',
506
+ parseInt(e.target.value) || 0,
507
+ )
508
+ }
509
+ className="image-editor-settings-modal__input"
510
+ placeholder={intl.formatMessage(messages.minWidth)}
511
+ min="0"
512
+ />
513
+ <input
514
+ type="number"
515
+ value={settings.minHeight}
516
+ onChange={(e) =>
517
+ handleSettingChange(
518
+ 'minHeight',
519
+ parseInt(e.target.value) || 0,
520
+ )
521
+ }
522
+ className="image-editor-settings-modal__input"
523
+ placeholder={intl.formatMessage(messages.minHeight)}
524
+ min="0"
525
+ />
526
+ </div>
527
+ </div>
528
+
529
+ <div className="image-editor-settings-modal__section">
530
+ <label className="image-editor-settings-modal__label">
531
+ {intl.formatMessage(messages.maxCropDimensions)}
532
+ </label>
533
+ <div className="image-editor-settings-modal__row">
534
+ <input
535
+ type="number"
536
+ value={settings.maxCropWidth || ''}
537
+ onChange={(e) =>
538
+ handleSettingChange(
539
+ 'maxCropWidth',
540
+ e.target.value ? parseInt(e.target.value) : undefined,
541
+ )
542
+ }
543
+ className="image-editor-settings-modal__input"
544
+ placeholder={intl.formatMessage(messages.maxWidth)}
545
+ min="1"
546
+ />
547
+ <input
548
+ type="number"
549
+ value={settings.maxCropHeight || ''}
550
+ onChange={(e) =>
551
+ handleSettingChange(
552
+ 'maxCropHeight',
553
+ e.target.value ? parseInt(e.target.value) : undefined,
554
+ )
555
+ }
556
+ className="image-editor-settings-modal__input"
557
+ placeholder={intl.formatMessage(messages.maxHeight)}
558
+ min="1"
559
+ />
560
+ </div>
561
+ </div>
562
+
563
+ <div className="image-editor-settings-modal__section">
564
+ <label className="image-editor-settings-modal__label">
565
+ <input
566
+ type="checkbox"
567
+ checked={settings.stencilGrid}
568
+ onChange={(e) =>
569
+ handleSettingChange('stencilGrid', e.target.checked)
570
+ }
571
+ className="image-editor-settings-modal__checkbox"
572
+ />
573
+ {intl.formatMessage(messages.stencilGrid)}
574
+ </label>
575
+ </div>
576
+ </div>
577
+ </div>
578
+ </div>
579
+ )}
580
+ </>
581
+ );
582
+ };