@codeleap/mobile 4.0.1 → 4.1.0

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.
@@ -0,0 +1,152 @@
1
+ import React, { useCallback } from 'react'
2
+ import { AnyRecord, AppIcon, IJSX, StyledComponentProps, useCompositionStyles } from '@codeleap/styles'
3
+ import { useStylesFor } from '../../hooks'
4
+ import { Text } from '../Text'
5
+ import { View } from '../View'
6
+ import { PlaceItem, PlacesAutocompleteProps } from './types'
7
+ import { MobileStyleRegistry } from '../../Registry'
8
+ import { TextInput } from '../TextInput'
9
+ import { List } from '../List'
10
+ import { Touchable } from '../Touchable'
11
+ import { EmptyPlaceholder } from '../EmptyPlaceholder'
12
+ import { ActivityIndicator } from '../ActivityIndicator'
13
+ import { usePlacesAutocompleteUtils } from '@codeleap/common'
14
+
15
+ export * from './styles'
16
+ export * from './types'
17
+
18
+ const DefaultPlaceRow: PlacesAutocompleteProps['renderPlaceRow'] = (props) => {
19
+ const { item, onPress, styles } = props
20
+
21
+ if (item?.content) {
22
+ return item?.content
23
+ }
24
+
25
+ const isLatLng = !!item?.formatted_address
26
+
27
+ const mainTitle = isLatLng ? item?.formatted_address : item?.description
28
+
29
+ return (
30
+ <Touchable onPress={() => onPress(mainTitle, item)} debugName={`PlaceRow ${item?.place_id}`} style={styles.placeRowWrapper}>
31
+ <Text text={`${mainTitle}`} style={styles.placeRowText} />
32
+ </Touchable>
33
+ )
34
+ }
35
+
36
+ export const PlacesAutocomplete = (props: PlacesAutocompleteProps) => {
37
+ const {
38
+ style,
39
+ itemRow,
40
+ data = [],
41
+ customData = [],
42
+ onPress,
43
+ onValueChange,
44
+ showClearIcon,
45
+ showEmptyPlaceholder,
46
+ clearIcon,
47
+ textInputProps,
48
+ listProps,
49
+ emptyPlaceholderProps,
50
+ placeRow = null,
51
+ renderPlaceRow: PlaceRow,
52
+ activityIndicatorProps,
53
+ debounce,
54
+ isLoading,
55
+ persistResultsOnBlur,
56
+ ...rest
57
+ } = props
58
+
59
+ const [isFocused, setIsFocused] = React.useState(false)
60
+
61
+ const styles = useStylesFor(PlacesAutocomplete.styleRegistryName, style)
62
+ const compositionStyles = useCompositionStyles(['input', 'list', 'loader'], styles)
63
+
64
+ const {
65
+ handleChangeAddress,
66
+ handlePressAddress,
67
+ handleClearAddress,
68
+ address,
69
+ isTyping,
70
+ setIsTyping,
71
+ } = usePlacesAutocompleteUtils<PlaceItem>({
72
+ onValueChange,
73
+ onPress,
74
+ })
75
+
76
+ const _showEmptyPlaceholder = !!address && !isTyping && showEmptyPlaceholder && !isLoading
77
+
78
+ const showResults = isFocused || persistResultsOnBlur
79
+
80
+ const _showClearIcon = showClearIcon && !!address?.trim?.()
81
+
82
+ const rightIcon = _showClearIcon ? {
83
+ name: clearIcon,
84
+ onPress: handleClearAddress,
85
+ } : textInputProps?.rightIcon
86
+
87
+ const _data = customData?.length > 0 && address ? [...customData, ...data] : data
88
+ const hasCustomValue = !!textInputProps?.value
89
+
90
+ const renderItem = useCallback((props) => {
91
+ return (
92
+ placeRow ? placeRow : <PlaceRow onPress={handlePressAddress} styles={styles} {...props} />
93
+ )
94
+ }, [placeRow])
95
+
96
+ return (
97
+ <View style={styles.wrapper} {...rest}>
98
+ <TextInput
99
+ style={compositionStyles.input}
100
+ onChangeText={(value) => {
101
+ setIsTyping(true)
102
+ handleChangeAddress(value)
103
+ }}
104
+ onBlur={() => {
105
+ setIsFocused(false)
106
+ }}
107
+ onFocus={() => {
108
+ setIsFocused(true)
109
+ }}
110
+ {...textInputProps}
111
+ value={hasCustomValue ? textInputProps?.value : address}
112
+ rightIcon={rightIcon}
113
+ />
114
+ {isTyping ? (
115
+ <View style={styles.loadingWrapper}>
116
+ <ActivityIndicator style={compositionStyles.loader} {...activityIndicatorProps} />
117
+ </View>
118
+ ) : (
119
+ showResults ? (
120
+ <List
121
+ data={_data}
122
+ renderItem={renderItem}
123
+ ListEmptyComponent={
124
+ _showEmptyPlaceholder ? <EmptyPlaceholder {...emptyPlaceholderProps} /> : null
125
+ }
126
+ style={compositionStyles.list}
127
+ separators
128
+ {...listProps}
129
+ />
130
+ ) : null
131
+ )
132
+ }
133
+ </View>
134
+ )
135
+ }
136
+
137
+ PlacesAutocomplete.styleRegistryName = 'PlacesAutocomplete'
138
+ PlacesAutocomplete.elements = ['wrapper', 'input', 'list', 'loader', 'placeRow', 'loadingWrapper']
139
+ PlacesAutocomplete.withVariantTypes = <S extends AnyRecord>(styles: S) => {
140
+ return PlacesAutocomplete as (props: StyledComponentProps<PlacesAutocompleteProps, typeof styles>) => IJSX
141
+ }
142
+
143
+ PlacesAutocomplete.defaultProps = {
144
+ showClearIcon: false,
145
+ showEmptyPlaceholder: true,
146
+ clearIcon: 'x' as AppIcon,
147
+ placeRowComponent: DefaultPlaceRow,
148
+ renderPlaceRow: DefaultPlaceRow,
149
+ debounce: 250,
150
+ }
151
+
152
+ MobileStyleRegistry.registerComponent(PlacesAutocomplete)
@@ -0,0 +1,12 @@
1
+ import { ActivityIndicatorComposition } from '../ActivityIndicator'
2
+ import { ListComposition } from '../List'
3
+ import { TextInputComposition } from '../TextInput'
4
+
5
+ export type PlacesAutocompleteComposition =
6
+ `input${Capitalize<TextInputComposition>}` |
7
+ `list${Capitalize<ListComposition>}` |
8
+ `loader${Capitalize<ActivityIndicatorComposition>}` |
9
+ 'placeRowWrapper' |
10
+ 'placeRowText' |
11
+ 'wrapper' |
12
+ 'loadingWrapper'
@@ -0,0 +1,42 @@
1
+ import { AppIcon, ICSS, StyledProp } from '@codeleap/styles'
2
+ import { PlacesAutocompleteComposition } from './styles'
3
+ import { TextInputProps } from '../TextInput'
4
+ import { FlatListProps } from '../List'
5
+ import { EmptyPlaceholderProps } from '../EmptyPlaceholder'
6
+ import { ActivityIndicatorProps } from '../ActivityIndicator'
7
+ import { PlaceAddress, PlaceLatLng } from '@codeleap/common'
8
+
9
+ export type CustomData = {
10
+ item?: Partial<PlaceAddress> & Partial<PlaceLatLng>
11
+ content?: JSX.Element
12
+ }
13
+
14
+ export type PlaceItem = PlaceAddress & PlaceLatLng & { content?: JSX.Element }
15
+
16
+ export type PlaceRowProps = {
17
+ item?: PlaceItem
18
+ styles?: Record<PlacesAutocompleteComposition, ICSS>
19
+ onPress?: PlacesAutocompleteProps['onPress']
20
+ }
21
+
22
+ export type PlacesAutocompleteProps = {
23
+ style?: StyledProp<PlacesAutocompleteComposition>
24
+ itemRow?: (props: any) => JSX.Element
25
+ textInputProps?: TextInputProps
26
+ emptyPlaceholderProps?: EmptyPlaceholderProps
27
+ listProps?: FlatListProps
28
+ data: PlaceAddress[] | PlaceLatLng[]
29
+ customData?: CustomData[]
30
+ onPress?: (address: string, place: PlaceItem) => void
31
+ onValueChange?: (address: string) => void
32
+ showClearIcon?: boolean
33
+ showEmptyPlaceholder?: boolean
34
+ clearIcon?: AppIcon
35
+ placeRowComponent?: React.ComponentType<PlaceRowProps>
36
+ renderPlaceRow?: (props: PlaceRowProps) => React.ReactElement
37
+ placeRow?: React.ReactElement
38
+ debounce?: number
39
+ activityIndicatorProps?: ActivityIndicatorProps
40
+ persistResultsOnBlur?: boolean
41
+ isLoading?: boolean
42
+ }
@@ -6,7 +6,7 @@ import {
6
6
  onUpdate,
7
7
  usePrevious,
8
8
  useSearch,
9
- useBooleanToggle,
9
+ useConditionalState,
10
10
  } from '@codeleap/common'
11
11
  import React, { useCallback, useMemo } from 'react'
12
12
  import { List } from '../List'
@@ -35,10 +35,11 @@ const defaultFilterFunction = (search: string, options: FormTypes.Options<any>)
35
35
 
36
36
  const defaultGetLabel = (option) => {
37
37
  if (TypeGuards.isArray(option)) {
38
- if (option.length === 0) return null
39
-
40
- return option.map(o => o.label).join(', ')
38
+ if (option?.length === 0) return null
41
39
 
40
+ const labels = option?.map(option => option?.label)?.filter(value => !!value)
41
+
42
+ return labels?.join(', ')
42
43
  } else {
43
44
  if (!option) return null
44
45
  return option?.label
@@ -140,7 +141,7 @@ export const Select = <T extends string | number = string, Multi extends boolean
140
141
  onLoadOptionsError,
141
142
  })
142
143
 
143
- const [visible, toggle] = TypeGuards.isBoolean(_visible) && !!_toggle ? [_visible, _toggle] : useBooleanToggle(false)
144
+ const [visible, toggle] = useConditionalState(_visible, _toggle, { initialValue: false, isBooleanToggle: true })
144
145
 
145
146
  const currentValueLabel = useMemo(() => {
146
147
  const _options = (multiple ? labelOptions : labelOptions?.[0]) as Multi extends true ? FormTypes.Options<T> : FormTypes.Options<T>[number]
@@ -174,19 +175,15 @@ export const Select = <T extends string | number = string, Multi extends boolean
174
175
 
175
176
  const select = useCallback((selectedValue) => {
176
177
  let newValue = null
177
-
178
178
  let newOption = null
179
179
  let removedIndex = null
180
180
 
181
181
  if (multiple && isValueArray) {
182
-
183
182
  if (value.includes(selectedValue)) {
184
183
  removedIndex = value.findIndex(v => v === selectedValue)
185
184
 
186
185
  newValue = value.filter((v, i) => i !== removedIndex)
187
-
188
186
  } else {
189
-
190
187
  if (TypeGuards.isNumber(limit) && value.length >= limit) {
191
188
  return
192
189
  }
@@ -195,7 +192,6 @@ export const Select = <T extends string | number = string, Multi extends boolean
195
192
 
196
193
  newValue = [...value, selectedValue]
197
194
  }
198
-
199
195
  } else {
200
196
  newValue = selectedValue
201
197
  newOption = currentOptions.find(o => o.value === selectedValue)
@@ -209,7 +205,8 @@ export const Select = <T extends string | number = string, Multi extends boolean
209
205
  newOptions.splice(removedIndex, 1)
210
206
  setLabelOptions(newOptions)
211
207
  } else {
212
- setLabelOptions([...labelOptions, newOption])
208
+ const newLabels = [...labelOptions, newOption]
209
+ setLabelOptions(newLabels)
213
210
  }
214
211
  } else {
215
212
  setLabelOptions([newOption])
@@ -218,7 +215,7 @@ export const Select = <T extends string | number = string, Multi extends boolean
218
215
  if (closeOnSelect) {
219
216
  close?.()
220
217
  }
221
- }, [isValueArray, (isValueArray ? value : [value]), limit, multiple])
218
+ }, [isValueArray, (isValueArray ? value : [value]), limit, multiple, labelOptions, currentOptions])
222
219
 
223
220
  const renderListItem = useCallback(({ item, index }) => {
224
221
  let selected = false
@@ -335,7 +332,6 @@ export const Select = <T extends string | number = string, Multi extends boolean
335
332
  </>
336
333
  }
337
334
 
338
-
339
335
  Select.styleRegistryName = 'Select'
340
336
  Select.elements = [...Modal.elements, 'input', 'list', 'item', 'searchInput']
341
337
  Select.rootElement = 'inputWrapper'
@@ -0,0 +1,170 @@
1
+ import { AnyRecord, IJSX, StyledComponentProps, useNestedStylesByKey } from '@codeleap/styles'
2
+ import { FileInput } from '../FileInput'
3
+ import { Icon } from '../Icon'
4
+ import { Image } from '../Image'
5
+ import { Dimensions, View } from 'react-native'
6
+ import { DragSortableView } from 'react-native-drag-sort'
7
+ import { SortableItemProps, SortablePhoto, SortablePhotosProps } from './types'
8
+ import { useSortablePhotos } from './useSortablePhotos'
9
+ import { useStylesFor } from '../../hooks'
10
+ import { MobileStyleRegistry } from '../../Registry'
11
+ import { ActivityIndicator } from '../ActivityIndicator'
12
+
13
+ export * from './styles'
14
+ export * from './types'
15
+
16
+ const DefaultItem = <T extends SortablePhoto>(props: SortableItemProps<T>) => {
17
+ const { photo, width, height, styles, emptyIcon } = props
18
+
19
+ return (
20
+ <View style={[{ width, height }, styles.photoWrapper]}>
21
+ {
22
+ !!photo?.filename
23
+ ? <Image resizeMode='cover' source={{ uri: photo?.file }} style={styles.photoImage} />
24
+ : <Icon name={emptyIcon} style={styles.photoEmptyIcon} />
25
+ }
26
+ </View>
27
+ )
28
+ }
29
+
30
+ const defaultGetFilename = (file: string) => {
31
+ if (!file) return null
32
+
33
+ const filenameWithExtension = file?.split?.('/').pop()
34
+
35
+ if (filenameWithExtension) {
36
+ return filenameWithExtension?.split('.').slice(0, -1).join('.')
37
+ }
38
+
39
+ return new Date().toISOString()
40
+ }
41
+
42
+ const screenWidth = Dimensions.get('screen').width
43
+
44
+ export const SortablePhotos = <T extends SortablePhoto>(props: SortablePhotosProps<T>) => {
45
+ const allProps = {
46
+ ...SortablePhotos.defaultProps,
47
+ ...props,
48
+ }
49
+
50
+ const {
51
+ numColumns,
52
+ renderPhoto: RenderItem,
53
+ gap,
54
+ multiple,
55
+ pickerConfig,
56
+ emptyIcon,
57
+ disableDragDropEmptyItems,
58
+ itemWidth: _itemWidth,
59
+ itemHeight: _itemHeight,
60
+ width: _parentWidth,
61
+ style,
62
+ loading,
63
+ ...rest
64
+ } = allProps
65
+
66
+ const styles = useStylesFor(SortablePhotos.styleRegistryName, style)
67
+
68
+ const loaderStyles = useNestedStylesByKey('loader', styles)
69
+
70
+ const {
71
+ input,
72
+ handlePressPhoto,
73
+ numberPhotosMissing,
74
+ emptyIndexes,
75
+ onChangePhotosOrder,
76
+ data,
77
+ } = useSortablePhotos<T>(allProps)
78
+
79
+ const defaultParentWidth = screenWidth - (gap * 2)
80
+ const defaultItemWidth = (defaultParentWidth / numColumns) - gap
81
+
82
+ const itemWidth = _itemWidth ?? defaultItemWidth
83
+ const itemHeight = _itemHeight ?? itemWidth
84
+ const parentWidth = _parentWidth ?? defaultParentWidth
85
+
86
+ const childrenMargin = gap / 2
87
+
88
+ const fileInputPickerOptions = {
89
+ ...SortablePhotos.defaultProps.pickerConfig,
90
+ ...pickerConfig,
91
+ multiple,
92
+ maxFiles: numberPhotosMissing,
93
+ }
94
+
95
+ if (loading) {
96
+ return (
97
+ <View style={[styles.wrapper, styles['wrapper:loading']]}>
98
+ <ActivityIndicator style={loaderStyles} />
99
+ </View>
100
+ )
101
+ }
102
+
103
+ return (
104
+ <View style={styles.wrapper}>
105
+ <FileInput
106
+ mode='hidden'
107
+ ref={input.ref}
108
+ pickerOptions={fileInputPickerOptions}
109
+ />
110
+
111
+ <DragSortableView
112
+ dataSource={data}
113
+ childrenHeight={itemHeight}
114
+ childrenWidth={itemWidth}
115
+ parentWidth={parentWidth}
116
+ marginChildrenBottom={childrenMargin}
117
+ marginChildrenLeft={childrenMargin}
118
+ marginChildrenRight={childrenMargin}
119
+ marginChildrenTop={childrenMargin}
120
+ onDataChange={onChangePhotosOrder}
121
+ onClickItem={handlePressPhoto}
122
+ fixedItems={disableDragDropEmptyItems ? emptyIndexes : undefined}
123
+ {...rest}
124
+ renderItem={(item, order) => (
125
+ <RenderItem
126
+ width={itemWidth}
127
+ height={itemHeight}
128
+ photo={item}
129
+ order={order}
130
+ styles={styles}
131
+ emptyIcon={emptyIcon}
132
+ />
133
+ )}
134
+ />
135
+ </View>
136
+ )
137
+ }
138
+
139
+ SortablePhotos.styleRegistryName = 'SortablePhotos'
140
+ SortablePhotos.elements = ['wrapper', 'photo', 'loader']
141
+ SortablePhotos.rootElement = 'wrapper'
142
+
143
+ SortablePhotos.withVariantTypes = <S extends AnyRecord>(styles: S) => {
144
+ return SortablePhotos as <T extends SortablePhoto>(props: StyledComponentProps<SortablePhotosProps<T>, typeof styles>) => IJSX
145
+ }
146
+
147
+ SortablePhotos.defaultProps = {
148
+ numPhotos: 9,
149
+ numColumns: 3,
150
+ renderPhoto: DefaultItem,
151
+ multiple: true,
152
+ disableDragDropEmptyItems: true,
153
+ gap: 16,
154
+ emptyIcon: 'plus',
155
+ modalTitle: 'Photos',
156
+ modalBody: null,
157
+ modalLibraryText: 'Choose from gallery',
158
+ modalCameraText: 'Take a photo',
159
+ modalDeleteText: 'Remove photo',
160
+ getFilename: defaultGetFilename,
161
+ pickerConfig: {
162
+ cropping: true,
163
+ showCropFrame: true,
164
+ compressImageMaxHeight: 1700,
165
+ compressImageMaxWidth: 1700,
166
+ compressImageQuality: 0.8,
167
+ },
168
+ } as Partial<SortablePhotosProps<any>>
169
+
170
+ MobileStyleRegistry.registerComponent(SortablePhotos)
@@ -0,0 +1,9 @@
1
+ import { ActivityIndicatorComposition } from '../ActivityIndicator'
2
+
3
+ export type SortablePhotosComposition =
4
+ 'wrapper' |
5
+ 'wrapper:loading' |
6
+ 'photoWrapper' |
7
+ 'photoEmptyIcon' |
8
+ 'photoImage' |
9
+ `loader${Capitalize<ActivityIndicatorComposition>}`
@@ -0,0 +1,58 @@
1
+ import { AppIcon, ICSS, StyledProp } from '@codeleap/styles'
2
+ import { ReactElement } from 'react'
3
+ import { SortablePhotosComposition } from './styles'
4
+
5
+ export type SortablePhoto = {
6
+ filename: string | null
7
+ file: string | null
8
+ }
9
+
10
+ export type SortableItemProps<T extends SortablePhoto> = {
11
+ photo: T
12
+ order: number
13
+ height: number
14
+ width: number
15
+ styles: Record<SortablePhotosComposition, ICSS>
16
+ emptyIcon: AppIcon
17
+ }
18
+
19
+ export type SortablePhotosPickerConfig = {
20
+ cropping?: boolean
21
+ compressImageMaxHeight?: number
22
+ compressImageMaxWidth?: number
23
+ compressImageQuality?: number
24
+ showCropFrame?: boolean
25
+ }
26
+
27
+ export type SortablePhotosProps<T extends SortablePhoto> = {
28
+ numColumns?: number
29
+ numPhotos?: number
30
+ renderPhoto?: (props: SortableItemProps<T>) => ReactElement
31
+ photos: T[]
32
+ onChangePhotos: (newPhotos: T[]) => void
33
+ gap?: number
34
+ itemHeight?: number
35
+ itemWidth?: number
36
+ width?: number
37
+ onPressPhoto?: (data: T[], photo: T, order: number) => void
38
+ onDragStart?: (fromIndex: number) => void
39
+ onDragEnd?: (fromIndex: number, toIndex: number) => void
40
+ keyExtractor?: (photo: T, order: number) => any
41
+ delayLongPress?: number
42
+ pickerConfig?: SortablePhotosPickerConfig
43
+ multiple?: boolean
44
+ maxScale?: number
45
+ minOpacity?: number
46
+ scaleDuration?: number
47
+ slideDuration?: number
48
+ emptyIcon?: AppIcon
49
+ disableDragDropEmptyItems?: boolean
50
+ style?: StyledProp<SortablePhotosComposition>
51
+ modalTitle?: string
52
+ modalBody?: string
53
+ modalLibraryText?: string
54
+ modalCameraText?: string
55
+ modalDeleteText?: string
56
+ getFilename?: (file: string) => string
57
+ loading?: boolean
58
+ }
@@ -0,0 +1,174 @@
1
+ import { CreateOSAlert, useEffect, useGlobalContext, useMemo, useState } from '@codeleap/common'
2
+ import { FileInputImageSource, useFileInput } from '../FileInput'
3
+ import { SortablePhoto, SortablePhotosProps } from './types'
4
+
5
+ const SortableAlert = CreateOSAlert()
6
+
7
+ export const useSortablePhotos = <T extends SortablePhoto>(props: SortablePhotosProps<T>) => {
8
+ const {
9
+ onChangePhotos,
10
+ onPressPhoto,
11
+ modalBody,
12
+ modalTitle,
13
+ modalCameraText,
14
+ modalDeleteText,
15
+ modalLibraryText,
16
+ getFilename,
17
+ numPhotos,
18
+ loading,
19
+ photos: currentPhotos
20
+ } = props
21
+
22
+ const input = useFileInput()
23
+ const { logger } = useGlobalContext()
24
+
25
+ const [data, setData] = useState<T[]>([])
26
+
27
+ const onChange = (photos: T[]) => {
28
+ const { newPhotos, sortedPhotos } = sortPhotos(photos)
29
+
30
+ setData(sortedPhotos)
31
+ onChangePhotos(newPhotos)
32
+ }
33
+
34
+ useEffect(() => {
35
+ if (!loading && data?.length < numPhotos) {
36
+ const currentLength = currentPhotos?.length ?? 0
37
+ const length = Math.abs(numPhotos - currentLength)
38
+ const fillPhotos = Array(length).fill({ filename: null, file: null }) as T[]
39
+
40
+ const newPhotos = currentPhotos.concat(fillPhotos)
41
+
42
+ setData(newPhotos)
43
+ onChangePhotos(currentPhotos)
44
+ }
45
+ }, [loading])
46
+
47
+ const { emptyIndexes, numberPhotosMissing } = useMemo(() => {
48
+ const copyPhotos = [...data]
49
+
50
+ const emptyIndexes = copyPhotos.reduce((indexes, photo, index) => {
51
+ if (!photo?.filename) {
52
+ indexes.push(index)
53
+ }
54
+ return indexes
55
+ }, [])
56
+
57
+ const numberPhotosMissing = emptyIndexes?.length
58
+
59
+ return {
60
+ emptyIndexes,
61
+ numberPhotosMissing,
62
+ }
63
+ }, [JSON.stringify(data)])
64
+
65
+ const sortPhotos = (_unorderedPhotos: T[]) => {
66
+ const unorderedPhotos = [..._unorderedPhotos]
67
+
68
+ const [newPhotos, emptyPhotos] = unorderedPhotos.reduce(
69
+ ([newPhotos, emptyPhotos], photo) => {
70
+ !!photo?.filename ? newPhotos.push(photo) : emptyPhotos.push(photo)
71
+ return [newPhotos, emptyPhotos]
72
+ },
73
+ [[], []] as [T[], T[]]
74
+ )
75
+
76
+ const sortedPhotos: T[] = newPhotos.concat(emptyPhotos)
77
+
78
+ return {
79
+ sortedPhotos,
80
+ newPhotos,
81
+ }
82
+ }
83
+
84
+ const handleOpenPicker = async (pickerType: FileInputImageSource, photo: T, order: number) => {
85
+ let files = []
86
+
87
+ const isEdit = !!photo?.filename
88
+
89
+ try {
90
+ files = await input?.openFilePicker(pickerType, {
91
+ multiple: isEdit ? false : props?.multiple
92
+ })
93
+ } catch (error) {
94
+ logger.error('Error opening file picker:', error)
95
+ }
96
+
97
+ if (files?.length <= 0) return null
98
+
99
+ const isMultiple = files?.length > 1 && !isEdit
100
+
101
+ const newPhotos = [...data]
102
+
103
+ if (isMultiple) {
104
+ for (const fileIndex in files) {
105
+ const file = files?.[fileIndex]
106
+ const order = emptyIndexes[fileIndex]
107
+ const uri = file?.file?.uri
108
+ const filename = getFilename(uri)
109
+
110
+ newPhotos[order] = {
111
+ ...newPhotos[order],
112
+ filename,
113
+ file: uri,
114
+ } as T
115
+ }
116
+ } else {
117
+ const file = files?.[0]
118
+ const uri = file?.file?.uri
119
+ const filename = getFilename(uri)
120
+
121
+ newPhotos[order] = {
122
+ ...newPhotos[order],
123
+ filename,
124
+ file: uri,
125
+ } as T
126
+ }
127
+
128
+ onChange(newPhotos)
129
+ }
130
+
131
+ const handleDeletePhoto = (photo: T, order: number) => {
132
+ const newPhotos = [...data]
133
+
134
+ newPhotos[order] = {
135
+ ...newPhotos[order],
136
+ filename: null,
137
+ file: null,
138
+ } as T
139
+
140
+ onChange(newPhotos)
141
+ }
142
+
143
+ const handlePressPhoto = (currentData: T[], photo: T, order: number) => {
144
+ SortableAlert.custom({
145
+ title: modalTitle,
146
+ body: modalBody,
147
+ options: [
148
+ { text: modalLibraryText, onPress: () => handleOpenPicker('library', photo, order) },
149
+ { text: modalCameraText, onPress: () => handleOpenPicker('camera', photo, order) },
150
+ !!photo?.filename && { text: modalDeleteText, onPress: () => handleDeletePhoto(photo, order) },
151
+ ],
152
+ // @ts-expect-error
153
+ isRow: false,
154
+ })
155
+
156
+ onPressPhoto?.(currentData, photo, order)
157
+ }
158
+
159
+ const onChangePhotosOrder = (newData: T[]) => {
160
+ onChange(newData)
161
+ }
162
+
163
+ return {
164
+ input,
165
+ handlePressPhoto,
166
+ handleDeletePhoto,
167
+ handleOpenPicker,
168
+ sortPhotos,
169
+ numberPhotosMissing,
170
+ onChangePhotosOrder,
171
+ emptyIndexes,
172
+ data,
173
+ }
174
+ }