@mgonza02/goal-image-gallery 1.2.1 → 1.2.4
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.
- package/CHANGELOG.md +23 -0
- package/README.md +10 -0
- package/lib/GoalImageGallery.js +54 -7
- package/lib/components/ImageGalleryGrid.js +38 -8
- package/lib/components/ImageGalleryItem.js +43 -2
- package/lib/hooks/use-image-gallery.js +71 -16
- package/lib/index.d.ts +54 -0
- package/lib/index.js +17 -21
- package/lib/utils/image-utils.js +83 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to the GoalImageGallery project will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.4] - 2026-10-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Floating Add Button**: Replaced the static '+' grid item with a floating action button (Fab) over the image gallery when `canAdd` is active.
|
|
12
|
+
|
|
13
|
+
## [1.2.3] - 2026-10-12
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- **`defaultImageCode`**: Highlights the default image in the grid.
|
|
17
|
+
- **`onSetAsDefault(imageCode)`**: Star action when `multiple` and at least two images (requires handler).
|
|
18
|
+
- **`onDelete(imageCode)`**, **`onUpdate(oldCode, newCode)`**, **`onAdd(imageCode)`**: Fired after successful gallery operations.
|
|
19
|
+
|
|
20
|
+
## [1.2.2] - 2026-10-12
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **`maxItems`**: Cap gallery size; hides the add tile and blocks uploads beyond the limit.
|
|
24
|
+
- **`allowDelete` / `minItems`**: Configurable delete in single- and multi-image mode (delete was previously limited to multi with 2+ images).
|
|
25
|
+
- **`minImageWidth` / `minImageHeight`**: Minimum dimension constraints, passed to custom selectors and `validateImageFile`.
|
|
26
|
+
- **`validateImageFile`**, **`validateImageFileSync`**, **`countGalleryImages`** exported from `image-utils`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Delete API payload includes `action: 'delete'`, `image_codes`, and `deletedImageCode` for backends that branch on operation type.
|
|
30
|
+
|
|
8
31
|
## [1.2.1] - 2026-05-22
|
|
9
32
|
|
|
10
33
|
### 🐛 Bug Fixes
|
package/README.md
CHANGED
|
@@ -312,6 +312,16 @@ const CustomImageSelector = ({
|
|
|
312
312
|
| `imageHandlerApi` | `function` | `undefined` | No | API function for image operations (required for editing) |
|
|
313
313
|
| `afterUpload` | `function` | `undefined` | No | Callback function called after successful upload |
|
|
314
314
|
| `multiple` | `boolean` | `false` | No | Allow multiple image selection and upload |
|
|
315
|
+
| `maxItems` | `number` | `undefined` | No | Maximum number of images in the gallery (hides add control when reached) |
|
|
316
|
+
| `allowDelete` | `boolean` | `true` | No | Show delete action when `canEdit` is enabled |
|
|
317
|
+
| `minItems` | `number` | `0` | No | Minimum images that must remain (disables delete at this count) |
|
|
318
|
+
| `minImageWidth` | `number` | `undefined` | No | Minimum image width in pixels (forwarded to selector / `validateImageFile`) |
|
|
319
|
+
| `minImageHeight` | `number` | `undefined` | No | Minimum image height in pixels (forwarded to selector / `validateImageFile`) |
|
|
320
|
+
| `defaultImageCode` | `string` | `undefined` | No | Marks which image is the default in the grid |
|
|
321
|
+
| `onSetAsDefault` | `(imageCode) => void` | `undefined` | No | User chose default (only when `multiple` and 2+ images) |
|
|
322
|
+
| `onDelete` | `(imageCode) => void` | `undefined` | No | After successful delete |
|
|
323
|
+
| `onUpdate` | `(oldImageCode, newImageCode) => void` | `undefined` | No | After successful replace |
|
|
324
|
+
| `onAdd` | `(imageCode) => void` | `undefined` | No | After each new image is added |
|
|
315
325
|
|
|
316
326
|
### Display Props
|
|
317
327
|
|
package/lib/GoalImageGallery.js
CHANGED
|
@@ -5,7 +5,7 @@ import { useMediaQuery } from "@mui/material";
|
|
|
5
5
|
import ImageGalleryGrid from "./components/ImageGalleryGrid";
|
|
6
6
|
import ImageGalleryModal from "./components/ImageGalleryModal";
|
|
7
7
|
import { useImageGallery, useImageZoom, useClipboard, useKeyboardNavigation } from "./hooks/use-image-gallery";
|
|
8
|
-
import { downloadImage } from "./utils/image-utils";
|
|
8
|
+
import { countGalleryImages, downloadImage } from "./utils/image-utils";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* GoalImageGallery - Advanced React Image Gallery Component
|
|
@@ -233,7 +233,17 @@ const GoalImageGallery = _ref => {
|
|
|
233
233
|
hasPermission = () => true,
|
|
234
234
|
currentCompany = 0,
|
|
235
235
|
slot = {},
|
|
236
|
-
slotProps = {}
|
|
236
|
+
slotProps = {},
|
|
237
|
+
maxItems,
|
|
238
|
+
allowDelete = true,
|
|
239
|
+
minItems = 0,
|
|
240
|
+
minImageWidth,
|
|
241
|
+
minImageHeight,
|
|
242
|
+
defaultImageCode,
|
|
243
|
+
onSetAsDefault,
|
|
244
|
+
onDelete,
|
|
245
|
+
onUpdate,
|
|
246
|
+
onAdd
|
|
237
247
|
} = _ref;
|
|
238
248
|
const theme = useTheme();
|
|
239
249
|
const [updatable, setUpdatable] = useState(false);
|
|
@@ -241,8 +251,21 @@ const GoalImageGallery = _ref => {
|
|
|
241
251
|
selector: ImageSelector
|
|
242
252
|
} = slot;
|
|
243
253
|
const {
|
|
244
|
-
selector: imageSelectorProps
|
|
254
|
+
selector: imageSelectorProps = {}
|
|
245
255
|
} = slotProps;
|
|
256
|
+
const currentImageCount = countGalleryImages((Array.isArray(imageCodes) ? imageCodes : (imageCodes || "").split(/[\s,]+/).filter(Boolean)).map((code, index) => ({
|
|
257
|
+
id: index,
|
|
258
|
+
image_code: code
|
|
259
|
+
})));
|
|
260
|
+
const mergedSelectorProps = {
|
|
261
|
+
...imageSelectorProps,
|
|
262
|
+
maxItems,
|
|
263
|
+
currentImageCount,
|
|
264
|
+
minImageWidth,
|
|
265
|
+
minImageHeight,
|
|
266
|
+
minWidth: imageSelectorProps.minWidth ?? minImageWidth,
|
|
267
|
+
minHeight: imageSelectorProps.minHeight ?? minImageHeight
|
|
268
|
+
};
|
|
246
269
|
|
|
247
270
|
// Initialize permissions
|
|
248
271
|
useEffect(() => {
|
|
@@ -259,12 +282,15 @@ const GoalImageGallery = _ref => {
|
|
|
259
282
|
zoomLevel,
|
|
260
283
|
loading,
|
|
261
284
|
removable,
|
|
285
|
+
canSetDefault,
|
|
286
|
+
canAdd,
|
|
262
287
|
setZoomLevel,
|
|
263
288
|
handleOpenImage,
|
|
264
289
|
handleCloseImage,
|
|
265
290
|
handleImageSelect,
|
|
266
291
|
handleCancel,
|
|
267
292
|
handleRemoveImage,
|
|
293
|
+
handleSetAsDefault,
|
|
268
294
|
handleAfterUpload
|
|
269
295
|
} = useImageGallery({
|
|
270
296
|
imageCodes,
|
|
@@ -275,7 +301,14 @@ const GoalImageGallery = _ref => {
|
|
|
275
301
|
ownerEntity,
|
|
276
302
|
currentCompany,
|
|
277
303
|
onRefresh,
|
|
278
|
-
showError
|
|
304
|
+
showError,
|
|
305
|
+
maxItems,
|
|
306
|
+
allowDelete,
|
|
307
|
+
minItems,
|
|
308
|
+
onSetAsDefault,
|
|
309
|
+
onDelete,
|
|
310
|
+
onUpdate,
|
|
311
|
+
onAdd
|
|
279
312
|
});
|
|
280
313
|
|
|
281
314
|
// Zoom and navigation handlers
|
|
@@ -338,7 +371,7 @@ const GoalImageGallery = _ref => {
|
|
|
338
371
|
multiple: multiple,
|
|
339
372
|
title: multiple ? "Seleccionar Imágenes" : "Seleccionar Imagen",
|
|
340
373
|
uploadMessage: multiple ? "Arrastra imágenes aquí o haz clic para seleccionar" : "Arrastra una imagen aquí o haz clic para seleccionar",
|
|
341
|
-
...
|
|
374
|
+
...mergedSelectorProps
|
|
342
375
|
}) : /*#__PURE__*/_jsxs("div", {
|
|
343
376
|
style: {
|
|
344
377
|
padding: "20px",
|
|
@@ -380,7 +413,11 @@ const GoalImageGallery = _ref => {
|
|
|
380
413
|
onImageClick: handleOpenImage,
|
|
381
414
|
onImageSelect: handleImageSelect,
|
|
382
415
|
onRemoveImage: handleRemoveImage,
|
|
383
|
-
|
|
416
|
+
onSetAsDefault: handleSetAsDefault,
|
|
417
|
+
onDownloadImage: handleDownloadImage,
|
|
418
|
+
canSetDefault: canSetDefault,
|
|
419
|
+
canAdd: canAdd,
|
|
420
|
+
defaultImageCode: defaultImageCode
|
|
384
421
|
}), /*#__PURE__*/_jsx(ImageGalleryModal, {
|
|
385
422
|
open: openImage !== null,
|
|
386
423
|
imageUrl: openImage,
|
|
@@ -413,6 +450,16 @@ GoalImageGallery.propTypes = {
|
|
|
413
450
|
hasPermission: PropTypes.func,
|
|
414
451
|
currentCompany: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
415
452
|
slot: PropTypes.object,
|
|
416
|
-
slotProps: PropTypes.object
|
|
453
|
+
slotProps: PropTypes.object,
|
|
454
|
+
maxItems: PropTypes.number,
|
|
455
|
+
allowDelete: PropTypes.bool,
|
|
456
|
+
minItems: PropTypes.number,
|
|
457
|
+
minImageWidth: PropTypes.number,
|
|
458
|
+
minImageHeight: PropTypes.number,
|
|
459
|
+
defaultImageCode: PropTypes.string,
|
|
460
|
+
onSetAsDefault: PropTypes.func,
|
|
461
|
+
onDelete: PropTypes.func,
|
|
462
|
+
onUpdate: PropTypes.func,
|
|
463
|
+
onAdd: PropTypes.func
|
|
417
464
|
};
|
|
418
465
|
export default GoalImageGallery;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import PropTypes from 'prop-types';
|
|
2
|
-
import { Grid, Card, CardContent } from '@mui/material';
|
|
2
|
+
import { Grid, Card, CardContent, Fab, Tooltip } from '@mui/material';
|
|
3
|
+
import { Add as AddIcon } from '@mui/icons-material';
|
|
3
4
|
import { useTheme } from '@mui/material/styles';
|
|
4
5
|
import ImageGalleryItem from './ImageGalleryItem';
|
|
5
6
|
import ImageGalleryEmptyState from './ImageGalleryEmptyState';
|
|
@@ -8,7 +9,7 @@ import { StyledImageGridContainer } from '../styles/styled-components';
|
|
|
8
9
|
/**
|
|
9
10
|
* Image grid component for displaying gallery images
|
|
10
11
|
*/
|
|
11
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
13
|
const ImageGalleryGrid = _ref => {
|
|
13
14
|
let {
|
|
14
15
|
imageList,
|
|
@@ -25,7 +26,11 @@ const ImageGalleryGrid = _ref => {
|
|
|
25
26
|
onImageClick,
|
|
26
27
|
onImageSelect,
|
|
27
28
|
onRemoveImage,
|
|
28
|
-
|
|
29
|
+
onSetAsDefault,
|
|
30
|
+
onDownloadImage,
|
|
31
|
+
canSetDefault = false,
|
|
32
|
+
canAdd = false,
|
|
33
|
+
defaultImageCode
|
|
29
34
|
} = _ref;
|
|
30
35
|
const theme = useTheme();
|
|
31
36
|
if (imageList.length === 0) {
|
|
@@ -41,9 +46,12 @@ const ImageGalleryGrid = _ref => {
|
|
|
41
46
|
})
|
|
42
47
|
});
|
|
43
48
|
}
|
|
44
|
-
return /*#__PURE__*/
|
|
49
|
+
return /*#__PURE__*/_jsxs(Card, {
|
|
45
50
|
elevation: 2,
|
|
46
|
-
|
|
51
|
+
sx: {
|
|
52
|
+
position: 'relative'
|
|
53
|
+
},
|
|
54
|
+
children: [/*#__PURE__*/_jsx(CardContent, {
|
|
47
55
|
children: /*#__PURE__*/_jsx(StyledImageGridContainer, {
|
|
48
56
|
children: /*#__PURE__*/_jsx(Grid, {
|
|
49
57
|
container: true,
|
|
@@ -70,12 +78,30 @@ const ImageGalleryGrid = _ref => {
|
|
|
70
78
|
onImageClick: onImageClick,
|
|
71
79
|
onImageSelect: onImageSelect,
|
|
72
80
|
onRemoveImage: onRemoveImage,
|
|
73
|
-
|
|
81
|
+
onSetAsDefault: onSetAsDefault,
|
|
82
|
+
onDownloadImage: onDownloadImage,
|
|
83
|
+
canSetDefault: canSetDefault,
|
|
84
|
+
isDefault: !!defaultImageCode && image.image_code === defaultImageCode
|
|
74
85
|
})
|
|
75
86
|
}, image.id))
|
|
76
87
|
})
|
|
77
88
|
})
|
|
78
|
-
})
|
|
89
|
+
}), canAdd && /*#__PURE__*/_jsx(Tooltip, {
|
|
90
|
+
title: "Agregar imagen",
|
|
91
|
+
children: /*#__PURE__*/_jsx(Fab, {
|
|
92
|
+
color: "primary",
|
|
93
|
+
size: "small",
|
|
94
|
+
"aria-label": "Agregar imagen",
|
|
95
|
+
onClick: () => onImageSelect(null),
|
|
96
|
+
sx: {
|
|
97
|
+
position: 'absolute',
|
|
98
|
+
bottom: 16,
|
|
99
|
+
right: 16,
|
|
100
|
+
zIndex: 3
|
|
101
|
+
},
|
|
102
|
+
children: /*#__PURE__*/_jsx(AddIcon, {})
|
|
103
|
+
})
|
|
104
|
+
})]
|
|
79
105
|
});
|
|
80
106
|
};
|
|
81
107
|
ImageGalleryGrid.propTypes = {
|
|
@@ -93,6 +119,10 @@ ImageGalleryGrid.propTypes = {
|
|
|
93
119
|
onImageClick: PropTypes.func.isRequired,
|
|
94
120
|
onImageSelect: PropTypes.func.isRequired,
|
|
95
121
|
onRemoveImage: PropTypes.func.isRequired,
|
|
96
|
-
|
|
122
|
+
onSetAsDefault: PropTypes.func,
|
|
123
|
+
onDownloadImage: PropTypes.func.isRequired,
|
|
124
|
+
canSetDefault: PropTypes.bool,
|
|
125
|
+
canAdd: PropTypes.bool,
|
|
126
|
+
defaultImageCode: PropTypes.string
|
|
97
127
|
};
|
|
98
128
|
export default ImageGalleryGrid;
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { Box, IconButton, Skeleton, Tooltip, Typography, Button } from '@mui/material';
|
|
4
4
|
import { alpha, useTheme } from '@mui/material/styles';
|
|
5
|
-
import { ChangeCircle as ChangeIcon, Download as DownloadIcon, Visibility as ViewIcon, Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
|
5
|
+
import { ChangeCircle as ChangeIcon, Download as DownloadIcon, Visibility as ViewIcon, Add as AddIcon, Delete as DeleteIcon, Star as StarIcon, StarBorder as StarBorderIcon } from '@mui/icons-material';
|
|
6
6
|
import { getImageContainerStyles, getImageOverlayStyles } from '../styles/styled-components';
|
|
7
7
|
import { handleImageError } from '../utils/image-utils';
|
|
8
8
|
|
|
@@ -21,9 +21,12 @@ const ImageGalleryItem = _ref => {
|
|
|
21
21
|
removable = false,
|
|
22
22
|
allowDownload = false,
|
|
23
23
|
showImageInfo = false,
|
|
24
|
+
canSetDefault = false,
|
|
25
|
+
isDefault = false,
|
|
24
26
|
onImageClick,
|
|
25
27
|
onImageSelect,
|
|
26
28
|
onRemoveImage,
|
|
29
|
+
onSetAsDefault,
|
|
27
30
|
onDownloadImage
|
|
28
31
|
} = _ref;
|
|
29
32
|
const theme = useTheme();
|
|
@@ -65,7 +68,31 @@ const ImageGalleryItem = _ref => {
|
|
|
65
68
|
}
|
|
66
69
|
return /*#__PURE__*/_jsxs(Box, {
|
|
67
70
|
sx: getImageContainerStyles(theme),
|
|
68
|
-
children: [
|
|
71
|
+
children: [isDefault && /*#__PURE__*/_jsxs(Box, {
|
|
72
|
+
sx: {
|
|
73
|
+
position: 'absolute',
|
|
74
|
+
top: 8,
|
|
75
|
+
left: 8,
|
|
76
|
+
zIndex: 2,
|
|
77
|
+
display: 'flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
gap: 0.5,
|
|
80
|
+
px: 1,
|
|
81
|
+
py: 0.25,
|
|
82
|
+
borderRadius: 1,
|
|
83
|
+
backgroundColor: alpha(theme.palette.warning.main, 0.92),
|
|
84
|
+
color: theme.palette.warning.contrastText
|
|
85
|
+
},
|
|
86
|
+
children: [/*#__PURE__*/_jsx(StarIcon, {
|
|
87
|
+
sx: {
|
|
88
|
+
fontSize: 16
|
|
89
|
+
}
|
|
90
|
+
}), /*#__PURE__*/_jsx(Typography, {
|
|
91
|
+
variant: "caption",
|
|
92
|
+
fontWeight: 600,
|
|
93
|
+
children: "Predeterminada"
|
|
94
|
+
})]
|
|
95
|
+
}), loading && /*#__PURE__*/_jsx(Skeleton, {
|
|
69
96
|
variant: "rectangular",
|
|
70
97
|
width: "100%",
|
|
71
98
|
height: 200,
|
|
@@ -111,6 +138,17 @@ const ImageGalleryItem = _ref => {
|
|
|
111
138
|
},
|
|
112
139
|
children: /*#__PURE__*/_jsx(DownloadIcon, {})
|
|
113
140
|
})
|
|
141
|
+
}), canSetDefault && !isDefault && /*#__PURE__*/_jsx(Tooltip, {
|
|
142
|
+
title: "Usar como predeterminada",
|
|
143
|
+
children: /*#__PURE__*/_jsx(IconButton, {
|
|
144
|
+
size: "small",
|
|
145
|
+
onClick: e => {
|
|
146
|
+
e.stopPropagation();
|
|
147
|
+
onSetAsDefault?.(image);
|
|
148
|
+
},
|
|
149
|
+
color: "warning",
|
|
150
|
+
children: /*#__PURE__*/_jsx(StarBorderIcon, {})
|
|
151
|
+
})
|
|
114
152
|
}), updatable && /*#__PURE__*/_jsx(Tooltip, {
|
|
115
153
|
title: "Cambiar imagen",
|
|
116
154
|
children: /*#__PURE__*/_jsx(IconButton, {
|
|
@@ -163,9 +201,12 @@ ImageGalleryItem.propTypes = {
|
|
|
163
201
|
removable: PropTypes.bool,
|
|
164
202
|
allowDownload: PropTypes.bool,
|
|
165
203
|
showImageInfo: PropTypes.bool,
|
|
204
|
+
canSetDefault: PropTypes.bool,
|
|
205
|
+
isDefault: PropTypes.bool,
|
|
166
206
|
onImageClick: PropTypes.func.isRequired,
|
|
167
207
|
onImageSelect: PropTypes.func.isRequired,
|
|
168
208
|
onRemoveImage: PropTypes.func.isRequired,
|
|
209
|
+
onSetAsDefault: PropTypes.func,
|
|
169
210
|
onDownloadImage: PropTypes.func.isRequired
|
|
170
211
|
};
|
|
171
212
|
export default ImageGalleryItem;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import { countGalleryImages } from '../utils/image-utils';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Custom hook for managing image gallery state
|
|
@@ -13,7 +14,14 @@ export const useImageGallery = _ref => {
|
|
|
13
14
|
ownerEntity,
|
|
14
15
|
currentCompany,
|
|
15
16
|
onRefresh,
|
|
16
|
-
showError
|
|
17
|
+
showError,
|
|
18
|
+
maxItems,
|
|
19
|
+
allowDelete = true,
|
|
20
|
+
minItems = 0,
|
|
21
|
+
onSetAsDefault,
|
|
22
|
+
onDelete,
|
|
23
|
+
onUpdate,
|
|
24
|
+
onAdd
|
|
17
25
|
} = _ref;
|
|
18
26
|
const [openImage, setOpenImage] = useState(null);
|
|
19
27
|
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
|
@@ -23,6 +31,8 @@ export const useImageGallery = _ref => {
|
|
|
23
31
|
const [zoomLevel, setZoomLevel] = useState(1);
|
|
24
32
|
const [loading, setLoading] = useState(false);
|
|
25
33
|
const [removable, setRemovable] = useState(false);
|
|
34
|
+
const [canSetDefault, setCanSetDefault] = useState(false);
|
|
35
|
+
const [canAdd, setCanAdd] = useState(false);
|
|
26
36
|
|
|
27
37
|
// Process image codes into image list
|
|
28
38
|
useEffect(() => {
|
|
@@ -40,17 +50,14 @@ export const useImageGallery = _ref => {
|
|
|
40
50
|
} else {
|
|
41
51
|
newList = [];
|
|
42
52
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
id: newList.length
|
|
50
|
-
});
|
|
51
|
-
}
|
|
53
|
+
const imageCount = newList.length;
|
|
54
|
+
const canRemoveMore = imageCount > minItems;
|
|
55
|
+
setRemovable(!!(updatable && allowDelete && imageCount > 0 && canRemoveMore));
|
|
56
|
+
const atMaxItems = maxItems != null && imageCount >= maxItems;
|
|
57
|
+
setCanAdd(!!(updatable && multiple && !atMaxItems));
|
|
58
|
+
setCanSetDefault(!!(multiple && imageCount >= 2 && onSetAsDefault));
|
|
52
59
|
setImageList(newList);
|
|
53
|
-
}, [imageCodes, canEdit, multiple, updatable]);
|
|
60
|
+
}, [imageCodes, canEdit, multiple, updatable, maxItems, allowDelete, minItems, onSetAsDefault]);
|
|
54
61
|
|
|
55
62
|
// Image handlers
|
|
56
63
|
const handleOpenImage = useCallback(function (imageUrl) {
|
|
@@ -64,9 +71,16 @@ export const useImageGallery = _ref => {
|
|
|
64
71
|
setZoomLevel(1);
|
|
65
72
|
}, []);
|
|
66
73
|
const handleImageSelect = useCallback(image => {
|
|
74
|
+
if (image === null && maxItems != null) {
|
|
75
|
+
const currentCount = countGalleryImages(imageList);
|
|
76
|
+
if (currentCount >= maxItems) {
|
|
77
|
+
showError?.(`Máximo ${maxItems} imágenes permitidas`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
67
81
|
setSelectedImage(image);
|
|
68
82
|
setSelectingImage(true);
|
|
69
|
-
}, []);
|
|
83
|
+
}, [imageList, maxItems, showError]);
|
|
70
84
|
const handleCancel = useCallback(() => {
|
|
71
85
|
setSelectingImage(false);
|
|
72
86
|
}, []);
|
|
@@ -83,9 +97,13 @@ export const useImageGallery = _ref => {
|
|
|
83
97
|
...ownerEntity,
|
|
84
98
|
companyId: currentCompany,
|
|
85
99
|
image_code: imagesToSave,
|
|
86
|
-
|
|
100
|
+
image_codes: imagesToSave,
|
|
101
|
+
company_id: currentCompany,
|
|
102
|
+
action: 'delete',
|
|
103
|
+
deletedImageCode: image.image_code
|
|
87
104
|
});
|
|
88
105
|
if (result.success) {
|
|
106
|
+
onDelete?.(image.image_code);
|
|
89
107
|
onRefresh?.(result);
|
|
90
108
|
} else {
|
|
91
109
|
showError(result.message);
|
|
@@ -95,7 +113,13 @@ export const useImageGallery = _ref => {
|
|
|
95
113
|
} finally {
|
|
96
114
|
setLoading(false);
|
|
97
115
|
}
|
|
98
|
-
}, [ownerEntity, currentCompany, imageList, onRefresh, showError, imageHandlerApi]);
|
|
116
|
+
}, [ownerEntity, currentCompany, imageList, onRefresh, onDelete, showError, imageHandlerApi]);
|
|
117
|
+
const handleSetAsDefault = useCallback(image => {
|
|
118
|
+
if (!image?.image_code || image.image_code === '+') {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
onSetAsDefault?.(image.image_code);
|
|
122
|
+
}, [onSetAsDefault]);
|
|
99
123
|
|
|
100
124
|
/**
|
|
101
125
|
* Handles the upload completion for single or multiple images
|
|
@@ -149,19 +173,47 @@ export const useImageGallery = _ref => {
|
|
|
149
173
|
}
|
|
150
174
|
});
|
|
151
175
|
}
|
|
176
|
+
if (maxItems != null && imagesToSave.length > maxItems) {
|
|
177
|
+
showError?.(`Máximo ${maxItems} imágenes permitidas (se intentaron guardar ${imagesToSave.length})`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const existingCodes = imageList.reduce((acc, img) => {
|
|
181
|
+
if (img.image_code !== '+') {
|
|
182
|
+
acc.push(img.image_code);
|
|
183
|
+
}
|
|
184
|
+
return acc;
|
|
185
|
+
}, []);
|
|
152
186
|
const result = await imageHandlerApi({
|
|
153
187
|
...ownerEntity,
|
|
154
188
|
companyId: currentCompany,
|
|
155
189
|
company_id: currentCompany,
|
|
156
|
-
image_code: imagesToSave
|
|
190
|
+
image_code: imagesToSave,
|
|
191
|
+
image_codes: imagesToSave
|
|
157
192
|
});
|
|
158
193
|
if (result.success) {
|
|
194
|
+
if (currentImageCode) {
|
|
195
|
+
const newPrimaryCode = imagesArray[0]?.code;
|
|
196
|
+
if (newPrimaryCode && newPrimaryCode !== currentImageCode) {
|
|
197
|
+
onUpdate?.(currentImageCode, newPrimaryCode);
|
|
198
|
+
}
|
|
199
|
+
imagesArray.slice(1).forEach(image => {
|
|
200
|
+
if (image?.code) {
|
|
201
|
+
onAdd?.(image.code);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
imagesArray.forEach(image => {
|
|
206
|
+
if (image?.code && !existingCodes.includes(image.code)) {
|
|
207
|
+
onAdd?.(image.code);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
159
211
|
setSelectingImage(false);
|
|
160
212
|
onRefresh?.(result);
|
|
161
213
|
} else {
|
|
162
214
|
showError(result.message);
|
|
163
215
|
}
|
|
164
|
-
}, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, showError]);
|
|
216
|
+
}, [ownerEntity, currentCompany, imageList, selectedImage, imageHandlerApi, onRefresh, onAdd, onUpdate, showError, maxItems]);
|
|
165
217
|
return {
|
|
166
218
|
openImage,
|
|
167
219
|
selectedImageIndex,
|
|
@@ -171,12 +223,15 @@ export const useImageGallery = _ref => {
|
|
|
171
223
|
zoomLevel,
|
|
172
224
|
loading,
|
|
173
225
|
removable,
|
|
226
|
+
canSetDefault,
|
|
227
|
+
canAdd,
|
|
174
228
|
setZoomLevel,
|
|
175
229
|
handleOpenImage,
|
|
176
230
|
handleCloseImage,
|
|
177
231
|
handleImageSelect,
|
|
178
232
|
handleCancel,
|
|
179
233
|
handleRemoveImage,
|
|
234
|
+
handleSetAsDefault,
|
|
180
235
|
handleAfterUpload
|
|
181
236
|
};
|
|
182
237
|
};
|
package/lib/index.d.ts
CHANGED
|
@@ -23,6 +23,26 @@ export interface GoalImageGalleryProps {
|
|
|
23
23
|
slotProps?: {
|
|
24
24
|
selector?: object;
|
|
25
25
|
};
|
|
26
|
+
/** Maximum number of images allowed in the gallery */
|
|
27
|
+
maxItems?: number;
|
|
28
|
+
/** When false, hides delete actions */
|
|
29
|
+
allowDelete?: boolean;
|
|
30
|
+
/** Minimum images that must remain (delete disabled at or below this count) */
|
|
31
|
+
minItems?: number;
|
|
32
|
+
/** Minimum image width in pixels (passed to selector and validateImageFile) */
|
|
33
|
+
minImageWidth?: number;
|
|
34
|
+
/** Minimum image height in pixels (passed to selector and validateImageFile) */
|
|
35
|
+
minImageHeight?: number;
|
|
36
|
+
/** Image code marked as default (badge + star state) */
|
|
37
|
+
defaultImageCode?: string;
|
|
38
|
+
/** Called when user sets an image as default (multi-image galleries with 2+ images) */
|
|
39
|
+
onSetAsDefault?: (imageCode: string) => void;
|
|
40
|
+
/** Called after a successful delete */
|
|
41
|
+
onDelete?: (imageCode: string) => void;
|
|
42
|
+
/** Called after a successful replace upload */
|
|
43
|
+
onUpdate?: (oldImageCode: string, newImageCode: string) => void;
|
|
44
|
+
/** Called after each new image is added */
|
|
45
|
+
onAdd?: (imageCode: string) => void;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
// Image item interface
|
|
@@ -54,6 +74,13 @@ export interface UseImageGalleryOptions {
|
|
|
54
74
|
currentCompany?: string;
|
|
55
75
|
onRefresh?: (result: any) => void;
|
|
56
76
|
showError?: (message: string) => void;
|
|
77
|
+
maxItems?: number;
|
|
78
|
+
allowDelete?: boolean;
|
|
79
|
+
minItems?: number;
|
|
80
|
+
onSetAsDefault?: (imageCode: string) => void;
|
|
81
|
+
onDelete?: (imageCode: string) => void;
|
|
82
|
+
onUpdate?: (oldImageCode: string, newImageCode: string) => void;
|
|
83
|
+
onAdd?: (imageCode: string) => void;
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
export interface UseImageGalleryReturn {
|
|
@@ -65,12 +92,15 @@ export interface UseImageGalleryReturn {
|
|
|
65
92
|
zoomLevel: number;
|
|
66
93
|
loading: boolean;
|
|
67
94
|
removable: boolean;
|
|
95
|
+
canSetDefault: boolean;
|
|
96
|
+
canAdd: boolean;
|
|
68
97
|
setZoomLevel: (level: number) => void;
|
|
69
98
|
handleOpenImage: (imageUrl: string, index?: number) => void;
|
|
70
99
|
handleCloseImage: () => void;
|
|
71
100
|
handleImageSelect: (image: ImageItem | null) => void;
|
|
72
101
|
handleCancel: () => void;
|
|
73
102
|
handleRemoveImage: (image: ImageItem) => Promise<void>;
|
|
103
|
+
handleSetAsDefault: (image: ImageItem) => void;
|
|
74
104
|
handleAfterUpload: (images: UploadedImage | UploadedImage[]) => Promise<void>;
|
|
75
105
|
}
|
|
76
106
|
|
|
@@ -105,7 +135,11 @@ export interface ImageGalleryGridProps {
|
|
|
105
135
|
onImageClick: (imageUrl: string, index: number) => void;
|
|
106
136
|
onImageSelect: (image: ImageItem | null) => void;
|
|
107
137
|
onRemoveImage: (image: ImageItem) => void;
|
|
138
|
+
onSetAsDefault?: (image: ImageItem) => void;
|
|
108
139
|
onDownloadImage: (imageCode: string) => void;
|
|
140
|
+
canSetDefault?: boolean;
|
|
141
|
+
canAdd?: boolean;
|
|
142
|
+
defaultImageCode?: string;
|
|
109
143
|
}
|
|
110
144
|
|
|
111
145
|
export interface ImageGalleryModalProps {
|
|
@@ -135,7 +169,10 @@ export interface ImageGalleryItemProps {
|
|
|
135
169
|
onImageClick: (imageUrl: string, index: number) => void;
|
|
136
170
|
onImageSelect: (image: ImageItem) => void;
|
|
137
171
|
onRemoveImage: (image: ImageItem) => void;
|
|
172
|
+
onSetAsDefault?: (image: ImageItem) => void;
|
|
138
173
|
onDownloadImage: (imageCode: string) => void;
|
|
174
|
+
canSetDefault?: boolean;
|
|
175
|
+
isDefault?: boolean;
|
|
139
176
|
}
|
|
140
177
|
|
|
141
178
|
export interface ImageGalleryEmptyStateProps {
|
|
@@ -200,3 +237,20 @@ export declare const debounce: Debounce;
|
|
|
200
237
|
export declare const isTouchDevice: IsTouchDevice;
|
|
201
238
|
export declare const getImageDimensions: GetImageDimensions;
|
|
202
239
|
export declare const createImagePreview: CreateImagePreview;
|
|
240
|
+
export declare const countGalleryImages: (imageList: ImageItem[]) => number;
|
|
241
|
+
export declare const validateImageFileSync: (
|
|
242
|
+
file: File,
|
|
243
|
+
options?: {
|
|
244
|
+
maxSize?: number;
|
|
245
|
+
allowedTypes?: string[];
|
|
246
|
+
}
|
|
247
|
+
) => { isValid: boolean; errors: string[] };
|
|
248
|
+
export declare const validateImageFile: (
|
|
249
|
+
file: File,
|
|
250
|
+
options?: {
|
|
251
|
+
maxSize?: number;
|
|
252
|
+
allowedTypes?: string[];
|
|
253
|
+
minWidth?: number;
|
|
254
|
+
minHeight?: number;
|
|
255
|
+
}
|
|
256
|
+
) => Promise<{ isValid: boolean; errors: string[]; width?: number; height?: number }>;
|
package/lib/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export { useImageGallery, useImageZoom, useClipboard, useKeyboardNavigation } fr
|
|
|
20
20
|
export { StyledImageContainer, ImageOverlay, StyledModalContainer, StyledModalHeader, StyledModalBody, StyledModalFooter, StyledEmptyStateContainer, StyledImageGridContainer } from './styles/styled-components';
|
|
21
21
|
|
|
22
22
|
// Utility exports
|
|
23
|
-
export { handleImageError, downloadImage, processImageCodes, getGridColumns, isValidImageFile, formatFileSize, generateId, debounce, isTouchDevice, getImageDimensions, createImagePreview } from './utils/image-utils';
|
|
23
|
+
export { handleImageError, downloadImage, processImageCodes, getGridColumns, isValidImageFile, formatFileSize, generateId, debounce, isTouchDevice, getImageDimensions, createImagePreview, countGalleryImages, validateImageFile, validateImageFileSync } from './utils/image-utils';
|
|
24
24
|
|
|
25
25
|
// Examples and documentation
|
|
26
26
|
export { default as GoalImageGalleryExamples } from './examples';
|
|
@@ -66,25 +66,9 @@ export const utils = {
|
|
|
66
66
|
* @param {Object} options - Validation options
|
|
67
67
|
* @returns {Object} - Validation result
|
|
68
68
|
*/
|
|
69
|
-
validateImageFile
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
maxSize = 5 * 1024 * 1024,
|
|
73
|
-
// 5MB
|
|
74
|
-
allowedTypes = FEATURES.SUPPORTED_FORMATS
|
|
75
|
-
} = options;
|
|
76
|
-
const errors = [];
|
|
77
|
-
if (file.size > maxSize) {
|
|
78
|
-
errors.push(`File size exceeds ${maxSize / 1024 / 1024}MB limit`);
|
|
79
|
-
}
|
|
80
|
-
if (!allowedTypes.includes(file.type)) {
|
|
81
|
-
errors.push(`File type ${file.type} is not supported`);
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
isValid: errors.length === 0,
|
|
85
|
-
errors
|
|
86
|
-
};
|
|
87
|
-
},
|
|
69
|
+
validateImageFile,
|
|
70
|
+
validateImageFileSync,
|
|
71
|
+
countGalleryImages,
|
|
88
72
|
/**
|
|
89
73
|
* Create image handler API wrapper
|
|
90
74
|
* @param {Function} uploadFunction - Upload function
|
|
@@ -136,8 +120,20 @@ export const CONFIG = {
|
|
|
136
120
|
};
|
|
137
121
|
|
|
138
122
|
// Export version info
|
|
139
|
-
export const VERSION = '1.2.
|
|
123
|
+
export const VERSION = '1.2.4';
|
|
140
124
|
export const CHANGELOG = [{
|
|
125
|
+
version: '1.2.4',
|
|
126
|
+
date: '2026-10-12',
|
|
127
|
+
changes: ['Replaced grid add placeholder tile with a floating action button over the image gallery when canAdd is true']
|
|
128
|
+
}, {
|
|
129
|
+
version: '1.2.3',
|
|
130
|
+
date: '2026-10-12',
|
|
131
|
+
changes: ['Added defaultImageCode, onSetAsDefault, onDelete, onUpdate, and onAdd lifecycle callbacks', 'Set-as-default control shown only when multiple is true and there are at least two images']
|
|
132
|
+
}, {
|
|
133
|
+
version: '1.2.2',
|
|
134
|
+
date: '2026-10-12',
|
|
135
|
+
changes: ['Added allowDelete, maxItems, minItems, minImageWidth, and minImageHeight props', 'Delete is available for single and multiple galleries when editing (respects minItems)', 'Exported validateImageFile async helper with minimum dimension checks']
|
|
136
|
+
}, {
|
|
141
137
|
version: '1.2.1',
|
|
142
138
|
date: '2026-05-22',
|
|
143
139
|
changes: ['Fixed warning about invalid alignItems prop being passed to DOM elements by Stack components']
|
package/lib/utils/image-utils.js
CHANGED
|
@@ -47,13 +47,94 @@ export const getGridColumns = (isMobile, isTablet, multiple) => {
|
|
|
47
47
|
if (isTablet) return multiple ? 4 : 6; // 3 or 2 columns on tablet
|
|
48
48
|
return multiple ? 3 : 4; // 4 or 3 columns on desktop
|
|
49
49
|
};
|
|
50
|
+
const DEFAULT_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* Validate image file type
|
|
53
54
|
*/
|
|
54
55
|
export const isValidImageFile = file => {
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
return DEFAULT_IMAGE_TYPES.includes(file.type);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Count gallery images (excludes the "+" add placeholder).
|
|
61
|
+
*/
|
|
62
|
+
export const countGalleryImages = function () {
|
|
63
|
+
let imageList = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
64
|
+
return imageList.filter(img => img?.image_code && img.image_code !== '+').length;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Validate image file size and type (sync). Use validateImageFile for dimension checks.
|
|
69
|
+
*/
|
|
70
|
+
export const validateImageFileSync = function (file) {
|
|
71
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
72
|
+
const {
|
|
73
|
+
maxSize = 25 * 1024 * 1024,
|
|
74
|
+
allowedTypes = DEFAULT_IMAGE_TYPES
|
|
75
|
+
} = options;
|
|
76
|
+
const errors = [];
|
|
77
|
+
if (!file) {
|
|
78
|
+
errors.push('No se ha seleccionado ningún archivo');
|
|
79
|
+
} else {
|
|
80
|
+
if (maxSize && file.size > maxSize) {
|
|
81
|
+
errors.push(`El tamaño del archivo supera el límite de ${formatFileSize(maxSize)}`);
|
|
82
|
+
}
|
|
83
|
+
if (allowedTypes.length && !allowedTypes.includes(file.type)) {
|
|
84
|
+
errors.push(`El tipo de archivo ${file.type || 'desconocido'} no está permitido`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
isValid: errors.length === 0,
|
|
89
|
+
errors
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate image file including optional minimum width and height (async).
|
|
95
|
+
*/
|
|
96
|
+
export const validateImageFile = async function (file) {
|
|
97
|
+
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
98
|
+
const {
|
|
99
|
+
minWidth,
|
|
100
|
+
minHeight,
|
|
101
|
+
minImageWidth,
|
|
102
|
+
minImageHeight,
|
|
103
|
+
...syncOptions
|
|
104
|
+
} = options;
|
|
105
|
+
const resolvedMinWidth = minWidth ?? minImageWidth;
|
|
106
|
+
const resolvedMinHeight = minHeight ?? minImageHeight;
|
|
107
|
+
const syncResult = validateImageFileSync(file, syncOptions);
|
|
108
|
+
if (!syncResult.isValid || !resolvedMinWidth && !resolvedMinHeight) {
|
|
109
|
+
return syncResult;
|
|
110
|
+
}
|
|
111
|
+
const objectUrl = URL.createObjectURL(file);
|
|
112
|
+
try {
|
|
113
|
+
const {
|
|
114
|
+
width,
|
|
115
|
+
height
|
|
116
|
+
} = await getImageDimensions(objectUrl);
|
|
117
|
+
const errors = [...syncResult.errors];
|
|
118
|
+
if (resolvedMinWidth && width < resolvedMinWidth) {
|
|
119
|
+
errors.push(`El ancho mínimo es ${resolvedMinWidth}px (la imagen tiene ${width}px)`);
|
|
120
|
+
}
|
|
121
|
+
if (resolvedMinHeight && height < resolvedMinHeight) {
|
|
122
|
+
errors.push(`El alto mínimo es ${resolvedMinHeight}px (la imagen tiene ${height}px)`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
isValid: errors.length === 0,
|
|
126
|
+
errors,
|
|
127
|
+
width,
|
|
128
|
+
height
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
return {
|
|
132
|
+
isValid: false,
|
|
133
|
+
errors: [...syncResult.errors, 'No se pudieron leer las dimensiones de la imagen']
|
|
134
|
+
};
|
|
135
|
+
} finally {
|
|
136
|
+
URL.revokeObjectURL(objectUrl);
|
|
137
|
+
}
|
|
57
138
|
};
|
|
58
139
|
|
|
59
140
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mgonza02/goal-image-gallery",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Advanced, accessible, and highly customizable React image gallery component with multi-method upload, responsive design, and extensive TypeScript support",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|