@riebel/react-native-multiple-select 0.6.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,591 @@
1
+ /*!
2
+ * react-native-multi-select
3
+ * Copyright(c) 2017 Mustapha Babatunde Oluwaleke
4
+ * MIT Licensed
5
+ */
6
+
7
+ import React, { useState, useCallback, useImperativeHandle, forwardRef } from 'react'
8
+ import {
9
+ Text,
10
+ View,
11
+ TextInput,
12
+ TouchableWithoutFeedback,
13
+ TouchableOpacity,
14
+ FlatList,
15
+ UIManager
16
+ } from 'react-native'
17
+ import styles, { colorPack, selectorViewStyle } from './styles'
18
+ import type { MultiSelectProps, MultiSelectItem, MultiSelectRef } from './types'
19
+
20
+ // set UIManager LayoutAnimationEnabledExperimental
21
+ if (UIManager.setLayoutAnimationEnabledExperimental) {
22
+ UIManager.setLayoutAnimationEnabledExperimental(true)
23
+ }
24
+
25
+ const getDisplayValue = (item: MultiSelectItem, key: string): string => {
26
+ const val = item[key]
27
+ if (val == null) return ''
28
+ if (typeof val === 'string') return val
29
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val)
30
+ return ''
31
+ }
32
+
33
+ const MultiSelect = forwardRef<MultiSelectRef, MultiSelectProps>(
34
+ (
35
+ {
36
+ single = false,
37
+ selectedItems = [],
38
+ items,
39
+ iconComponent: Icon,
40
+ iconNames,
41
+ uniqueKey = '_id',
42
+ displayKey = 'name',
43
+ tagBorderColor = colorPack.primary,
44
+ tagTextColor = colorPack.primary,
45
+ tagRemoveIconColor = colorPack.danger,
46
+ tagContainerStyle,
47
+ fontFamily = '',
48
+ selectedItemFontFamily = '',
49
+ selectedItemTextColor = colorPack.primary,
50
+ selectedItemIconColor = colorPack.primary,
51
+ itemFontFamily = '',
52
+ itemTextColor = colorPack.textPrimary,
53
+ itemFontSize = 16,
54
+ searchIcon,
55
+ searchInputPlaceholderText = 'Search',
56
+ searchInputStyle = { color: colorPack.textPrimary },
57
+ selectText = 'Select',
58
+ selectedText = 'selected',
59
+ altFontFamily = '',
60
+ fontSize = 14,
61
+ textColor = colorPack.textPrimary,
62
+ fixedHeight = false,
63
+ hideTags = false,
64
+ hideSubmitButton = false,
65
+ hideDropdown = false,
66
+ submitButtonColor = '#CCC',
67
+ submitButtonText = 'Submit',
68
+ canAddItems = false,
69
+ removeSelected = false,
70
+ noItemsText = 'No items to display.',
71
+ filterMethod = 'partial',
72
+ onSelectedItemsChange,
73
+ onAddItem,
74
+ onChangeInput,
75
+ onClearSelector: onClearSelectorProp,
76
+ onToggleList,
77
+ textInputProps,
78
+ flatListProps,
79
+ styleDropdownMenu,
80
+ styleDropdownMenuSubsection,
81
+ styleInputGroup,
82
+ styleItemsContainer,
83
+ styleListContainer,
84
+ styleMainWrapper,
85
+ styleRowList,
86
+ styleSelectorContainer,
87
+ styleTextDropdown,
88
+ styleTextDropdownSelected,
89
+ styleTextTag,
90
+ styleIndicator
91
+ },
92
+ ref
93
+ ) => {
94
+ const [selector, setSelector] = useState(false)
95
+ const [searchTerm, setSearchTerm] = useState('')
96
+
97
+ const names = React.useMemo(
98
+ () => ({
99
+ search: iconNames?.search ?? 'magnify',
100
+ close: iconNames?.close ?? 'close-circle',
101
+ check: iconNames?.check ?? 'check',
102
+ arrowDown: iconNames?.arrowDown ?? 'menu-down',
103
+ arrowRight: iconNames?.arrowRight ?? 'menu-right',
104
+ arrowLeft: iconNames?.arrowLeft ?? 'arrow-left'
105
+ }),
106
+ [iconNames]
107
+ )
108
+
109
+ const resolvedSearchIcon = searchIcon ?? (
110
+ <Icon
111
+ name={names.search}
112
+ size={20}
113
+ color={colorPack.placeholderTextColor}
114
+ style={styles.searchIconMargin}
115
+ />
116
+ )
117
+
118
+ const getItemKey = useCallback(
119
+ (item: MultiSelectItem): string => getDisplayValue(item, uniqueKey),
120
+ [uniqueKey]
121
+ )
122
+
123
+ const findItem = useCallback(
124
+ (itemKey: string): MultiSelectItem =>
125
+ items.find((singleItem) => singleItem[uniqueKey] === itemKey) ?? {},
126
+ [items, uniqueKey]
127
+ )
128
+
129
+ const itemSelected = useCallback(
130
+ (item: MultiSelectItem): boolean => selectedItems.indexOf(getItemKey(item)) !== -1,
131
+ [selectedItems, getItemKey]
132
+ )
133
+
134
+ const handleChangeInput = useCallback(
135
+ (value: string) => {
136
+ onChangeInput?.(value)
137
+ setSearchTerm(value)
138
+ },
139
+ [onChangeInput]
140
+ )
141
+
142
+ const getSelectLabel = useCallback((): string => {
143
+ if (selectedItems.length === 0) {
144
+ return selectText
145
+ }
146
+ if (single) {
147
+ const foundItem = findItem(selectedItems[0])
148
+ return getDisplayValue(foundItem, displayKey) || selectText
149
+ }
150
+ return `${selectText} (${String(selectedItems.length)} ${selectedText})`
151
+ }, [selectedItems, selectText, single, findItem, displayKey, selectedText])
152
+
153
+ const clearSearchTerm = useCallback(() => {
154
+ setSearchTerm('')
155
+ }, [])
156
+
157
+ const toggleSelector = useCallback(() => {
158
+ setSelector((prev) => !prev)
159
+ onToggleList?.()
160
+ }, [onToggleList])
161
+
162
+ const submitSelection = useCallback(() => {
163
+ toggleSelector()
164
+ clearSearchTerm()
165
+ }, [toggleSelector, clearSearchTerm])
166
+
167
+ const clearSelectorCallback = useCallback(() => {
168
+ setSelector(false)
169
+ onClearSelectorProp?.()
170
+ }, [onClearSelectorProp])
171
+
172
+ const removeItem = useCallback(
173
+ (item: MultiSelectItem) => {
174
+ const key = getItemKey(item)
175
+ const newItems = selectedItems.filter((singleItem) => key !== singleItem)
176
+ onSelectedItemsChange(newItems)
177
+ },
178
+ [selectedItems, getItemKey, onSelectedItemsChange]
179
+ )
180
+
181
+ const toggleItem = useCallback(
182
+ (item: MultiSelectItem) => {
183
+ const key = getItemKey(item)
184
+ if (single) {
185
+ submitSelection()
186
+ onSelectedItemsChange([key])
187
+ } else {
188
+ const isSelected = itemSelected(item)
189
+ const newItems = isSelected
190
+ ? selectedItems.filter((singleItem) => key !== singleItem)
191
+ : [...selectedItems, key]
192
+ onSelectedItemsChange(newItems)
193
+ }
194
+ },
195
+ [
196
+ single,
197
+ getItemKey,
198
+ selectedItems,
199
+ onSelectedItemsChange,
200
+ itemSelected,
201
+ submitSelection
202
+ ]
203
+ )
204
+
205
+ const addItem = useCallback(() => {
206
+ const newItemName = searchTerm
207
+ if (newItemName) {
208
+ const newItemId = newItemName
209
+ .split(' ')
210
+ .filter((word) => word.length)
211
+ .join('-')
212
+ const newItems = [...items, { [uniqueKey]: newItemId, name: newItemName }]
213
+ const newSelectedItems = [...selectedItems, newItemId]
214
+ onAddItem?.(newItems)
215
+ onSelectedItemsChange(newSelectedItems)
216
+ clearSearchTerm()
217
+ }
218
+ }, [
219
+ searchTerm,
220
+ items,
221
+ uniqueKey,
222
+ selectedItems,
223
+ onAddItem,
224
+ onSelectedItemsChange,
225
+ clearSearchTerm
226
+ ])
227
+
228
+ const itemStyle = useCallback(
229
+ (item: MultiSelectItem) => {
230
+ const isSelected = itemSelected(item)
231
+ const ff: { fontFamily?: string } = {}
232
+ if (isSelected && selectedItemFontFamily) {
233
+ ff.fontFamily = selectedItemFontFamily
234
+ } else if (!isSelected && itemFontFamily) {
235
+ ff.fontFamily = itemFontFamily
236
+ }
237
+ const color = isSelected
238
+ ? { color: selectedItemTextColor }
239
+ : { color: itemTextColor }
240
+ return { ...ff, ...color, fontSize: itemFontSize }
241
+ },
242
+ [
243
+ itemSelected,
244
+ selectedItemFontFamily,
245
+ itemFontFamily,
246
+ selectedItemTextColor,
247
+ itemTextColor,
248
+ itemFontSize
249
+ ]
250
+ )
251
+
252
+ const displaySelectedItems = useCallback(
253
+ (optionalSelectedItems?: string[]) => {
254
+ const actualSelectedItems = optionalSelectedItems ?? selectedItems
255
+ return actualSelectedItems.map((singleSelectedItem) => {
256
+ const item = findItem(singleSelectedItem)
257
+ const label = getDisplayValue(item, displayKey)
258
+ if (!label) return null
259
+ return (
260
+ <View
261
+ style={[
262
+ styles.selectedItem,
263
+ styles.selectedItemLayout,
264
+ {
265
+ width: label.length * 8 + 60,
266
+ borderColor: tagBorderColor
267
+ },
268
+ tagContainerStyle ?? {}
269
+ ]}
270
+ key={getItemKey(item)}
271
+ >
272
+ <Text
273
+ style={[
274
+ styles.tagLabel,
275
+ { color: tagTextColor },
276
+ styleTextTag ?? {},
277
+ fontFamily ? { fontFamily } : {}
278
+ ]}
279
+ numberOfLines={1}
280
+ >
281
+ {label}
282
+ </Text>
283
+ <TouchableOpacity
284
+ onPress={() => {
285
+ removeItem(item)
286
+ }}
287
+ >
288
+ <Icon
289
+ name={names.close}
290
+ style={[styles.tagRemoveIcon, { color: tagRemoveIconColor }]}
291
+ />
292
+ </TouchableOpacity>
293
+ </View>
294
+ )
295
+ })
296
+ },
297
+ [
298
+ Icon,
299
+ names,
300
+ selectedItems,
301
+ findItem,
302
+ displayKey,
303
+ getItemKey,
304
+ tagBorderColor,
305
+ tagTextColor,
306
+ tagRemoveIconColor,
307
+ tagContainerStyle,
308
+ styleTextTag,
309
+ fontFamily,
310
+ removeItem
311
+ ]
312
+ )
313
+
314
+ useImperativeHandle(
315
+ ref,
316
+ () => ({
317
+ getSelectedItemsExt: (optionalItems?: string[]) => (
318
+ <View style={styles.rowWrap}>{displaySelectedItems(optionalItems)}</View>
319
+ )
320
+ }),
321
+ [displaySelectedItems]
322
+ )
323
+
324
+ const filterItemsPartial = useCallback(
325
+ (term: string): MultiSelectItem[] => {
326
+ const parts = term.trim().split(/[ \-:]+/)
327
+ const regex = new RegExp(`(${parts.join('|')})`, 'ig')
328
+ return items.filter((item) => regex.test(getDisplayValue(item, displayKey)))
329
+ },
330
+ [items, displayKey]
331
+ )
332
+
333
+ const filterItemsFull = useCallback(
334
+ (term: string): MultiSelectItem[] => {
335
+ const lower = term.trim().toLowerCase()
336
+ return items.filter(
337
+ (item) => getDisplayValue(item, displayKey).toLowerCase().indexOf(lower) >= 0
338
+ )
339
+ },
340
+ [items, displayKey]
341
+ )
342
+
343
+ const filterItems = useCallback(
344
+ (term: string): MultiSelectItem[] => {
345
+ if (filterMethod === 'full') return filterItemsFull(term)
346
+ return filterItemsPartial(term)
347
+ },
348
+ [filterMethod, filterItemsFull, filterItemsPartial]
349
+ )
350
+
351
+ const getRow = useCallback(
352
+ (item: MultiSelectItem) => (
353
+ <TouchableOpacity
354
+ disabled={Boolean(item.disabled)}
355
+ onPress={() => {
356
+ toggleItem(item)
357
+ }}
358
+ style={[styleRowList, styles.rowPadding]}
359
+ >
360
+ <View>
361
+ <View style={styles.rowAlignCenter}>
362
+ <Text
363
+ style={[
364
+ styles.rowItemText,
365
+ itemStyle(item),
366
+ item.disabled ? styles.disabledText : {}
367
+ ]}
368
+ >
369
+ {getDisplayValue(item, displayKey)}
370
+ </Text>
371
+ {itemSelected(item) ? (
372
+ <Icon
373
+ name={names.check}
374
+ style={[styles.checkIcon, { color: selectedItemIconColor }]}
375
+ />
376
+ ) : null}
377
+ </View>
378
+ </View>
379
+ </TouchableOpacity>
380
+ ),
381
+ [
382
+ Icon,
383
+ names,
384
+ toggleItem,
385
+ styleRowList,
386
+ itemStyle,
387
+ displayKey,
388
+ itemSelected,
389
+ selectedItemIconColor
390
+ ]
391
+ )
392
+
393
+ const getRowNew = useCallback(
394
+ (item: MultiSelectItem) => (
395
+ <TouchableOpacity
396
+ disabled={Boolean(item.disabled)}
397
+ onPress={() => {
398
+ addItem()
399
+ }}
400
+ style={styles.rowPadding}
401
+ >
402
+ <View>
403
+ <View style={styles.rowAlignCenter}>
404
+ <Text
405
+ style={[
406
+ styles.rowItemText,
407
+ itemStyle(item),
408
+ item.disabled ? styles.disabledText : {}
409
+ ]}
410
+ >
411
+ Add {getDisplayValue(item, 'name')} (tap or press return)
412
+ </Text>
413
+ </View>
414
+ </View>
415
+ </TouchableOpacity>
416
+ ),
417
+ [addItem, itemStyle]
418
+ )
419
+
420
+ const renderItems = useCallback(() => {
421
+ let renderList = searchTerm ? filterItems(searchTerm) : items
422
+
423
+ if (removeSelected) {
424
+ renderList = renderList.filter(
425
+ (item) => !selectedItems.includes(getItemKey(item))
426
+ )
427
+ }
428
+
429
+ let itemList: React.ReactNode = null
430
+ let searchTermMatch = false
431
+
432
+ if (renderList.length) {
433
+ itemList = (
434
+ <FlatList
435
+ data={renderList}
436
+ extraData={selectedItems}
437
+ keyExtractor={(_item: MultiSelectItem, index: number) => index.toString()}
438
+ renderItem={(rowData: { item: MultiSelectItem }) => getRow(rowData.item)}
439
+ {...flatListProps}
440
+ nestedScrollEnabled
441
+ />
442
+ )
443
+ searchTermMatch = renderList.some((item) => item.name === searchTerm)
444
+ } else if (!canAddItems) {
445
+ itemList = (
446
+ <View style={styles.noItemsRow}>
447
+ <Text style={[styles.noItemsText, fontFamily ? { fontFamily } : {}]}>
448
+ {noItemsText}
449
+ </Text>
450
+ </View>
451
+ )
452
+ }
453
+
454
+ let addItemRow: React.ReactNode = null
455
+ if (canAddItems && !searchTermMatch && searchTerm.length) {
456
+ addItemRow = getRowNew({ name: searchTerm })
457
+ }
458
+
459
+ return (
460
+ <View style={styleListContainer}>
461
+ {itemList}
462
+ {addItemRow}
463
+ </View>
464
+ )
465
+ }, [
466
+ searchTerm,
467
+ filterItems,
468
+ items,
469
+ removeSelected,
470
+ selectedItems,
471
+ getItemKey,
472
+ getRow,
473
+ flatListProps,
474
+ canAddItems,
475
+ fontFamily,
476
+ noItemsText,
477
+ getRowNew,
478
+ styleListContainer
479
+ ])
480
+
481
+ return (
482
+ <View style={styleMainWrapper}>
483
+ {selector ? (
484
+ <View style={[selectorViewStyle(fixedHeight), styleSelectorContainer]}>
485
+ <View style={[styles.inputGroup, styleInputGroup]}>
486
+ {resolvedSearchIcon}
487
+ <TextInput
488
+ autoFocus
489
+ onChangeText={handleChangeInput}
490
+ onSubmitEditing={addItem}
491
+ placeholder={searchInputPlaceholderText}
492
+ placeholderTextColor={colorPack.placeholderTextColor}
493
+ underlineColorAndroid="transparent"
494
+ style={[searchInputStyle, styles.searchInputFlex]}
495
+ value={searchTerm}
496
+ {...textInputProps}
497
+ />
498
+ {hideSubmitButton && (
499
+ <TouchableOpacity onPress={submitSelection}>
500
+ <Icon
501
+ name={names.arrowDown}
502
+ style={[styles.indicator, styles.indicatorPadded, styleIndicator]}
503
+ />
504
+ </TouchableOpacity>
505
+ )}
506
+ {!hideDropdown && (
507
+ <Icon
508
+ name={names.arrowLeft}
509
+ size={20}
510
+ onPress={clearSelectorCallback}
511
+ color={colorPack.placeholderTextColor}
512
+ style={styles.backArrowMargin}
513
+ />
514
+ )}
515
+ </View>
516
+ <View style={styles.selectorContent}>
517
+ <View style={styleItemsContainer}>{renderItems()}</View>
518
+ {!single && !hideSubmitButton && (
519
+ <TouchableOpacity
520
+ onPress={submitSelection}
521
+ style={[styles.button, { backgroundColor: submitButtonColor }]}
522
+ >
523
+ <Text style={[styles.buttonText, fontFamily ? { fontFamily } : {}]}>
524
+ {submitButtonText}
525
+ </Text>
526
+ </TouchableOpacity>
527
+ )}
528
+ </View>
529
+ </View>
530
+ ) : (
531
+ <View>
532
+ <View style={[styles.dropdownView, styleDropdownMenu]}>
533
+ <View
534
+ style={[
535
+ styles.subSection,
536
+ styles.subSectionPadded,
537
+ styleDropdownMenuSubsection
538
+ ]}
539
+ >
540
+ <TouchableWithoutFeedback onPress={toggleSelector}>
541
+ <View style={styles.dropdownTouchable}>
542
+ <Text
543
+ style={
544
+ selectedItems.length === 0
545
+ ? [
546
+ styles.dropdownLabelBase,
547
+ {
548
+ fontSize,
549
+ color: textColor || colorPack.placeholderTextColor
550
+ },
551
+ styleTextDropdown,
552
+ altFontFamily
553
+ ? { fontFamily: altFontFamily }
554
+ : fontFamily
555
+ ? { fontFamily }
556
+ : {}
557
+ ]
558
+ : [
559
+ styles.dropdownLabelBase,
560
+ {
561
+ fontSize,
562
+ color: textColor || colorPack.placeholderTextColor
563
+ },
564
+ styleTextDropdownSelected
565
+ ]
566
+ }
567
+ numberOfLines={1}
568
+ >
569
+ {getSelectLabel()}
570
+ </Text>
571
+ <Icon
572
+ name={hideSubmitButton ? names.arrowRight : names.arrowDown}
573
+ style={[styles.indicator, styleIndicator]}
574
+ />
575
+ </View>
576
+ </TouchableWithoutFeedback>
577
+ </View>
578
+ </View>
579
+ {!single && !hideTags && selectedItems.length ? (
580
+ <View style={styles.rowWrap}>{displaySelectedItems()}</View>
581
+ ) : null}
582
+ </View>
583
+ )}
584
+ </View>
585
+ )
586
+ }
587
+ )
588
+
589
+ MultiSelect.displayName = 'MultiSelect'
590
+
591
+ export default MultiSelect