haya-select 1.0.78 → 1.0.80

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.
@@ -1,873 +0,0 @@
1
- import {anythingDifferent} from "set-state-compare/src/diff-utils"
2
- import config from "../config.js"
3
- import {dig, digg} from "diggerize"
4
- import {Dimensions, Platform, Pressable, Text, TextInput, View} from "react-native"
5
- import React, {memo, useEffect, useRef} from "react"
6
- import {shapeComponent, ShapeComponent} from "set-state-compare/src/shape-component.js"
7
- import debounce from "debounce"
8
- import FontAwesomeIcon from "react-native-vector-icons/FontAwesome"
9
- import idForComponent from "@kaspernj/api-maker/build/inputs/id-for-component"
10
- import nameForComponent from "@kaspernj/api-maker/build/inputs/name-for-component"
11
- import Option from "./option"
12
- import OptionGroup from "./option-group"
13
- import PropTypes from "prop-types"
14
- import propTypesExact from "prop-types-exact"
15
- import RenderHtml from "react-native-render-html"
16
- import {Portal} from "conjointment"
17
- import useEventListener from "@kaspernj/api-maker/build/use-event-listener"
18
- import usePressOutside from "outside-eye/build/use-press-outside"
19
-
20
- const nameForComponentWithMultiple = (component) => {
21
- let name = nameForComponent(component)
22
-
23
- const currentOptions = component.getCurrentOptions()
24
-
25
- if (component.props.multiple && name && currentOptions.length > 0) {
26
- name += "[]"
27
- }
28
-
29
- return name
30
- }
31
-
32
- export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
33
- static defaultProps = {
34
- multiple: false,
35
- onBlur: null,
36
- onFocus: null,
37
- optionsAbsolute: true,
38
- optionsPortal: true,
39
- optionsWidth: null,
40
- search: false,
41
- transparent: false
42
- }
43
-
44
- static propTypes = propTypesExact({
45
- attribute: PropTypes.string,
46
- className: PropTypes.string,
47
- defaultToggled: PropTypes.object,
48
- defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
49
- defaultValues: PropTypes.array,
50
- defaultValuesFromOptions: PropTypes.array,
51
- id: PropTypes.node,
52
- model: PropTypes.object,
53
- multiple: PropTypes.bool.isRequired,
54
- name: PropTypes.string,
55
- onBlur: PropTypes.func,
56
- onChange: PropTypes.func,
57
- onChangeValue: PropTypes.func,
58
- onFocus: PropTypes.func,
59
- onOptionsClosed: PropTypes.func,
60
- options: PropTypes.oneOfType([PropTypes.array, PropTypes.func]).isRequired,
61
- optionsAbsolute: PropTypes.bool.isRequired,
62
- optionsPortal: PropTypes.bool.isRequired,
63
- optionsWidth: PropTypes.number,
64
- placeholder: PropTypes.node,
65
- search: PropTypes.bool.isRequired,
66
- styles: PropTypes.object,
67
- toggled: PropTypes.object,
68
- toggleOptions: PropTypes.arrayOf(PropTypes.shape({
69
- icon: PropTypes.string.isRequired,
70
- label: PropTypes.string.isRequired,
71
- value: PropTypes.string.isRequired
72
- })),
73
- transparent: PropTypes.bool.isRequired,
74
- values: PropTypes.array
75
- })
76
-
77
- callOptionsPositionAboveIfOutsideScreen = false
78
- windowWidth = Dimensions.get("window").width
79
- windowHeight = Dimensions.get("window").height
80
-
81
- setup() {
82
- const {t} = config.getUseTranslate()()
83
-
84
- if (this.props.values) {
85
- for (const value of this.props.values) {
86
- if (typeof value == "undefined") {
87
- throw new Error("HayaSelect: Undefined given as value")
88
- }
89
- }
90
- }
91
-
92
- this.setInstance({
93
- endOfSelectRef: useRef(),
94
- optionsContainerRef: useRef(),
95
- searchTextInputRef: useRef(),
96
- selectContainerRef: useRef(),
97
- t
98
- })
99
- this.useStates({
100
- currentOptions: () => this.defaultCurrentOptions(),
101
- selectContainerLayout: null,
102
- endOfSelectLayout: null,
103
- height: null,
104
- loadedOptions: () => this.defaultLoadedOptions(),
105
- opened: false,
106
- optionsContainerLayout: null,
107
- optionsPlacement: undefined,
108
- optionsTop: undefined,
109
- optionsVisibility: undefined,
110
- optionsWidth: undefined,
111
- scrollLeft: Platform.OS == "web" ? document.documentElement.scrollLeft : null,
112
- scrollTop: Platform.OS == "web" ? document.documentElement.scrollTop : null,
113
- searchText: "",
114
- toggled: () => this.defaultToggled()
115
- })
116
-
117
- useEventListener(Dimensions, "change", this.tt.onDimensionsChange)
118
- usePressOutside(this.tt.optionsContainerRef, this.tt.onPressOutsideOptions)
119
-
120
- if (Platform.OS == "web") {
121
- useEventListener(window, "resize", this.tt.onAnythingResizedDebounced)
122
- useEventListener(window, "scroll", this.tt.onAnythingScrolledDebounced)
123
- }
124
-
125
- useEffect(() => {
126
- if (this.tt.callOptionsPositionAboveIfOutsideScreen && this.s.optionsContainerLayout) {
127
- this.callOptionsPositionAboveIfOutsideScreen = false
128
- this.setOptionsPositionAboveIfOutsideScreen()
129
- }
130
- }, [this.tt.callOptionsPositionAboveIfOutsideScreen, this.s.optionsContainerLayout])
131
-
132
- useEffect(() => {
133
- const currentOptionIds = this.s.currentOptions?.map((currentOption) => currentOption.value)
134
-
135
- if (this.props.values && anythingDifferent(currentOptionIds, this.props.values) && typeof this.props.options == "function") {
136
- this.setCurrentFromGivenValues()
137
- }
138
- }, [this.props.values])
139
- }
140
-
141
- translate(msgID) {
142
- if (msgID.startsWith(".")) {
143
- return this.t(`haya_select${msgID}`)
144
- } else {
145
- return this.t(msgID)
146
- }
147
- }
148
-
149
- defaultCurrentOptions() {
150
- const {defaultValue, defaultValues, values} = this.props
151
- const {options} = this.p
152
-
153
- if (!Array.isArray(options)) return []
154
-
155
- return options.filter(({value}) =>
156
- (defaultValue && value == defaultValue) ||
157
- (defaultValues && defaultValues.includes(value)) ||
158
- (values && values.includes(value))
159
- )
160
- }
161
-
162
- defaultLoadedOptions() {
163
- const {options} = this.p
164
-
165
- if (typeof options == "function") {
166
- return undefined
167
- } else if (Array.isArray(options)) {
168
- return options
169
- }
170
-
171
- throw new Error(`Unknown type of options: ${typeof options}`)
172
- }
173
-
174
- defaultToggled = () => ("toggled" in this.props) ? this.p.toggled : this.props.defaultToggled || {}
175
- getToggled = () => ("toggled" in this.props) ? this.p.toggled : this.s.toggled
176
- getValues = () => ("values" in this.props) ? this.p.values : this.s.currentOptions.map((currentOption) => currentOption.value)
177
- getCurrentOptions = () => {
178
- if ("values" in this.props && typeof this.props.values != "undefined") {
179
- if (Array.isArray(this.props.options) && this.props.values) {
180
- return this.p.values.map((value) => this.p.options.find((option) => option.value == value))
181
- } else if (this.s.loadedOptions && this.props.values) {
182
- return this.p.values.map((value) => {
183
- let foundOption = this.s.loadedOptions.find((option) => option.value == value)
184
-
185
- if (!foundOption) {
186
- foundOption = this.s.currentOptions.find((option) => option.value == value)
187
-
188
- if (!foundOption) {
189
- console.error(
190
- `Couldn't find option: ${value} in loadedOptions or state currentOptions`,
191
- {stateLoadedOptions: this.s.loadedOptions, stateCurrentOptions: this.s.currentOptions, propsOptions: this.props.options}
192
- )
193
- }
194
- }
195
-
196
- return foundOption
197
- })
198
- } else if (typeof this.props.options == "function" && this.props.values) {
199
- // Options haven't been loaded yet.
200
- } else if (this.props.values) {
201
- return this.p.values.map((value) => ({value}))
202
- }
203
- }
204
-
205
- return this.s.currentOptions
206
- }
207
-
208
- getCurrentOptionValues() {
209
- if ("values" in this.props) {
210
- return this.p.values
211
- }
212
-
213
- return this.getCurrentOptions().map((option) => option.value)
214
- }
215
-
216
- componentDidMount() {
217
- const {attribute, defaultValue, defaultValues, defaultValuesFromOptions, model, options} = this.props
218
-
219
- if (((defaultValue || defaultValues || defaultValuesFromOptions) || (attribute && model)) && typeof options == "function") {
220
- this.loadDefaultValuesFromOptionsCallback()
221
- }
222
- }
223
-
224
- componentDidUpdate() {
225
- const newState = {}
226
-
227
- if ("toggled" in this.props) {
228
- const newToggled = this.p.toggled
229
-
230
- if (anythingDifferent(this.state.toggled, newToggled)) {
231
- newState.toggled = newToggled
232
- }
233
- }
234
-
235
- if (Object.keys(newState).length > 0) {
236
- this.setState(newState)
237
- }
238
- }
239
-
240
- render() {
241
- const {endOfSelectRef} = this.tt
242
- const {transparent} = this.p
243
- const {className, placeholder, toggleOptions} = this.props
244
- const {opened, optionsPlacement} = this.s
245
- const currentOptions = this.getCurrentOptions()
246
- const id = idForComponent(this)
247
-
248
- const selectContainerStyleActual = this.stylingFor("selectContainer", {
249
- flexDirection: "row",
250
- alignItems: "center",
251
- backgroundColor: transparent ? undefined : "#fff",
252
- border: transparent ? undefined : "1px solid #999",
253
- borderRadius: transparent ? undefined : 4,
254
- color: "#000",
255
- cursor: "pointer",
256
- paddingTop: 5,
257
- paddingBottom: 5,
258
- paddingLeft: 5
259
- }, [transparent])
260
-
261
- if (opened) {
262
- // Prevent select from changing size once the content is replaced with search text once opened
263
- selectContainerStyleActual.height = this.s.height
264
-
265
- if (optionsPlacement == "above") {
266
- selectContainerStyleActual.borderTopLeftRadius = 0
267
- selectContainerStyleActual.borderTopRightRadius = 0
268
- selectContainerStyleActual.borderBottomRightRadius = selectContainerStyleActual.borderRadius
269
- selectContainerStyleActual.borderBottomLeftRadius = selectContainerStyleActual.borderRadius
270
-
271
- delete selectContainerStyleActual.borderRadius
272
- } else if (optionsPlacement == "below") {
273
- selectContainerStyleActual.borderTopLeftRadius = selectContainerStyleActual.borderRadius
274
- selectContainerStyleActual.borderTopRightRadius = selectContainerStyleActual.borderRadius
275
- selectContainerStyleActual.borderBottomRightRadius = 0
276
- selectContainerStyleActual.borderBottomLeftRadius = 0
277
-
278
- delete selectContainerStyleActual.borderRadius
279
- }
280
- }
281
-
282
- return (
283
- <View
284
- dataSet={this.cache("rootViewDataSet", {
285
- class: className,
286
- component: "haya-select",
287
- id,
288
- opened,
289
- optionsPlacement,
290
- toggles: Boolean(toggleOptions)
291
- }, [className, id, opened, optionsPlacement, Boolean(toggleOptions)])}
292
- style={this.stylingFor("main")}
293
- >
294
- <Pressable
295
- dataSet={this.selectContainerDataSet ||= {class: "select-container"}}
296
- onLayout={this.tt.onSelectContainerLayout}
297
- onPress={this.tt.onSelectClicked}
298
- ref={this.tt.selectContainerRef}
299
- style={selectContainerStyleActual}
300
- >
301
- <View
302
- dataSet={this.currentSelectedDataSet ||= {class: "current-selected"}}
303
- style={this.stylingFor("currentSelected", this.currentSelectedStyle ||= {width: "calc(100% - 25px)", flexWrap: "wrap", overflow: "hidden"})}
304
- >
305
- {opened &&
306
- <TextInput
307
- dataSet={this.searchTextInputDataSet ||= {class: "search-text-input"}}
308
- onChange={this.tt.onSearchTextInputChangedDebounced}
309
- onChangeText={this.tt.onChangeSearchText}
310
- placeholder={this.translate(".search_dot_dot_dot")}
311
- ref={this.tt.searchTextInputRef}
312
- style={this.stylingFor("searchTextInput", this.searchTextInputStyle ||= {
313
- width: "100%",
314
- border: 0,
315
- outline: "none",
316
- padding: 0
317
- })}
318
- value={this.state.searchText}
319
- />
320
- }
321
- {!opened &&
322
- <>
323
- {currentOptions.length == 0 &&
324
- <Text numberOfLines={1} style={this.stylingFor("nothingSelected", this.nothingSelectedStyle ||= {color: "grey"})}>
325
- {placeholder || this.translate(".nothing_selected")}
326
- </Text>
327
- }
328
- {currentOptions.length == 0 && Platform.OS == "web" &&
329
- <input
330
- id={idForComponent(this)}
331
- name={nameForComponentWithMultiple(this)}
332
- type="hidden"
333
- value=""
334
- />
335
- }
336
- {currentOptions.map((currentOption) =>
337
- <View
338
- dataSet={this.currentOptionDataSet ||= {class: "current-option"}}
339
- key={currentOption.key || `current-value-${currentOption.value}`}
340
- style={this.stylingFor("currentOption", {marginRight: 6})}
341
- >
342
- {currentOption.type == "group" &&
343
- <View style={this.stylingFor("currentOptionGroup", this.currentOptionGroupStyle ||= {fontWeight: "bold"})}>
344
- <Text style={this.stylingFor("currentOptionGroupText")}>
345
- {currentOption.text}
346
- </Text>
347
- </View>
348
- }
349
- {currentOption.type != "group" &&
350
- <>
351
- {Platform.OS == "web" && nameForComponentWithMultiple(this) &&
352
- <input
353
- id={idForComponent(this)}
354
- name={nameForComponentWithMultiple(this)}
355
- type="hidden"
356
- value={digg(currentOption, "value")}
357
- />
358
- }
359
- {this.presentOption(currentOption, "current")}
360
- </>
361
- }
362
- </View>
363
- )}
364
- </>
365
- }
366
- </View>
367
- <View
368
- dataSet={this.chevronContainerDataSet ||= {class: "chevron-container"}}
369
- style={this.stylingFor("chevronContainer", this.chevronContainerStyle ||= {
370
- alignItems: "center",
371
- justifyContent: "center",
372
- height: "100%",
373
- marginLeft: "auto",
374
- marginRight: 8
375
- })}
376
- >
377
- <FontAwesomeIcon name={opened ? "chevron-up" : "chevron-down"} style={this.stylingFor("chevron", this.chevronStyle ||= {fontSize: 12})} />
378
- </View>
379
- </Pressable>
380
- <View
381
- dataSet={this.endOfSelectDataSet ||= {class: "end-of-select"}}
382
- onLayout={this.tt.onEndOfSelectLayout}
383
- ref={endOfSelectRef}
384
- />
385
- {opened && this.p.optionsPortal &&
386
- <Portal>
387
- {this.optionsContainer()}
388
- </Portal>
389
- }
390
- {opened && !this.p.optionsPortal &&
391
- <View>
392
- {this.optionsContainer()}
393
- </View>
394
- }
395
- </View>
396
- )
397
- }
398
-
399
- defaultValues () {
400
- const {attribute, defaultValue, defaultValues, defaultValuesFromOptions, model} = this.props
401
-
402
- if (defaultValuesFromOptions) return defaultValuesFromOptions
403
- if (defaultValue) return defaultValue
404
- if (defaultValues) return defaultValues
405
-
406
- if (attribute && model) {
407
- if (!(attribute in model)) throw new Error(`No such attribute on ${model.modelClassData().name}: ${attribute}`)
408
-
409
- return model[attribute]()
410
- }
411
- }
412
-
413
- isActive() {
414
- if (this.tt.endOfSelectRef.current) {
415
- return true
416
- }
417
-
418
- return false
419
- }
420
-
421
- async loadDefaultValuesFromOptionsCallback() {
422
- const defaultValues = this.defaultValues()
423
-
424
- if (!defaultValues) return
425
-
426
- const result = await this.props.options({
427
- searchValue: this.s.searchText,
428
- values: defaultValues
429
- })
430
-
431
- this.setState({currentOptions: this.state.currentOptions.concat(result)})
432
- }
433
-
434
- loadOptions = async () => {
435
- const {options} = this.p
436
- const searchValue = this.s.searchText
437
-
438
- if (Array.isArray(options)) {
439
- return this.loadOptionsFromArray(options, searchValue)
440
- }
441
-
442
- const loadedOptions = await options({searchValue})
443
-
444
- this.setState({loadedOptions})
445
- }
446
-
447
- hayaSelectOption({key, loadedOption}) {
448
- if (loadedOption.type == "group") {
449
- return <OptionGroup key={key} option={loadedOption} />
450
- }
451
-
452
- return (
453
- <Option
454
- currentOptionValues={this.getCurrentOptionValues()}
455
- icon={this.iconForOption(loadedOption)}
456
- key={key}
457
- option={loadedOption}
458
- onOptionClicked={this.tt.onOptionClicked}
459
- presentOption={this.tt.presentOption}
460
- />
461
- )
462
- }
463
-
464
- loadOptionsFromArray(options, searchValue) {
465
- const lowerSearchValue = searchValue?.toLowerCase()
466
- const loadedOptions = options.filter(({text}) => !lowerSearchValue || text?.toLowerCase()?.includes(lowerSearchValue))
467
-
468
- this.setState({loadedOptions})
469
- }
470
-
471
- onDimensionsChange = ({window}) => {
472
- this.windowWidth = window.width
473
- this.windowHeight = window.height
474
- }
475
-
476
- onSelectContainerLayout = (e) => this.setState({selectContainerLayout: Object.assign({}, digg(e, "nativeEvent", "layout"))})
477
- onEndOfSelectLayout = (e) => this.setState({endOfSelectLayout: Object.assign({}, digg(e, "nativeEvent", "layout"))})
478
- onOptionsContainerLayout = (e) => this.setState({optionsContainerLayout: Object.assign({}, digg(e, "nativeEvent", "layout"))})
479
-
480
- onSelectClicked = (e) => {
481
- e.preventDefault()
482
- e.stopPropagation()
483
-
484
- const {opened} = this.s
485
-
486
- if (opened) {
487
- this.closeOptions()
488
- } else {
489
- this.openOptions()
490
- }
491
- }
492
-
493
- onSearchTextInputChangedDebounced = debounce(this.tt.loadOptions, 200)
494
-
495
- closeOptions() {
496
- this.setState({
497
- height: null,
498
- loadedOptions: undefined,
499
- opened: false,
500
- optionsContainerLayout: null
501
- })
502
-
503
- if (this.props.onOptionsClosed) {
504
- this.props.onOptionsClosed({options: this.getCurrentOptions()})
505
- }
506
-
507
- if (this.p.onBlur) this.p.onBlur()
508
- }
509
-
510
- onChangeSearchText = (searchText) => this.setState({searchText})
511
-
512
- openOptions() {
513
- this.setOptionsPositionBelow()
514
- this.loadOptions()
515
- this.callOptionsPositionAboveIfOutsideScreen = true
516
- this.setState(
517
- {
518
- height: this.s.selectContainerLayout.height,
519
- opened: true,
520
- scrollLeft: Platform.OS == "web" ? document.documentElement.scrollLeft : null,
521
- scrollTop: Platform.OS == "web" ? document.documentElement.scrollTop : null,
522
- searchText: ""
523
- },
524
- () => {
525
- this.focusTextInput()
526
- }
527
- )
528
-
529
- if (this.p.onFocus) this.p.onFocus()
530
- }
531
-
532
- focusTextInput = () => digg(this.tt.searchTextInputRef, "current")?.focus()
533
-
534
- setOptionsPosition() {
535
- if (!this.isActive()) {
536
- return // Debounce after un-mount handeling.
537
- }
538
-
539
- this.callOptionsPositionAboveIfOutsideScreen = true
540
- this.setOptionsPositionBelow()
541
- }
542
-
543
- setOptionsPositionAboveIfOutsideScreen() {
544
- const {windowHeight} = this.tt
545
- const {optionsContainerLayout} = this.s
546
- const optionsTop = this.s.endOfSelectLayout.top
547
- const optionsTotalBottomPosition = optionsContainerLayout.height + optionsTop
548
- const windowHeightWithScroll = windowHeight + this.s.scrollTop
549
-
550
- if (windowHeightWithScroll < optionsTotalBottomPosition) {
551
- this.setOptionsPositionAbove()
552
- } else {
553
- this.setState({optionsVisibility: "visible"})
554
- }
555
- }
556
-
557
- setOptionsPositionAbove() {
558
- const {endOfSelectLayout} = this.s
559
-
560
- this.setState(
561
- {
562
- opened: true,
563
- optionsPlacement: "above",
564
- optionsVisibility: "visible",
565
- optionsWidth: endOfSelectLayout.width
566
- },
567
- () => this.focusTextInput()
568
- )
569
- }
570
-
571
- setOptionsPositionBelow() {
572
- this.setState(
573
- {
574
- opened: true,
575
- optionsPlacement: "below",
576
- optionsVisibility: "hidden",
577
- optionsWidth: this.s.endOfSelectLayout.width
578
- },
579
- () => this.focusTextInput()
580
- )
581
- }
582
-
583
- onAnythingResized = () => {
584
- if (this.s.opened) {
585
- this.setOptionsPosition()
586
- }
587
- }
588
-
589
- onAnythingResizedDebounced = debounce(this.tt.onAnythingResized, 25)
590
-
591
- onAnythingScrolled = () => {
592
- if (this.s.opened) {
593
- this.setState({
594
- scrollLeft: Platform.OS == "web" ? document.documentElement.scrollLeft : null,
595
- scrollTop: Platform.OS == "web" ? document.documentElement.scrollTop : null
596
- })
597
- this.setOptionsPosition()
598
- }
599
- }
600
-
601
- onAnythingScrolledDebounced = debounce(this.tt.onAnythingScrolled, 25)
602
-
603
- onPressOutsideOptions = () => {
604
- // If options are open and a click is made outside of the options container
605
- if (this.s.opened) {
606
- this.closeOptions()
607
- }
608
- }
609
-
610
- optionsContainer() {
611
- const {selectContainerLayout, loadedOptions, endOfSelectLayout, optionsContainerLayout, optionsPlacement, optionsVisibility} = this.s
612
- let left, top
613
-
614
- let style = {
615
- position: "absolute",
616
- zIndex: 99999,
617
- elevation: 99999,
618
- visibility: optionsVisibility,
619
- width: this.p.optionsWidth || this.s.optionsWidth,
620
- backgroundColor: "#fff",
621
- border: "1px solid #999",
622
- maxHeight: 300,
623
- overflowY: "auto"
624
- }
625
-
626
- if (!this.p.optionsPortal) {
627
- style.top = 0
628
- style.left = 0
629
- } else if (!this.p.optionsAbsolute) {
630
- style.left = 0
631
- style.bottom = 0
632
- } else if (optionsPlacement == "below") {
633
- if (Platform.OS == "web") {
634
- // onLayout top value is sometimes negative so use browser JS to get it instead
635
- top = digg(this.tt.endOfSelectRef.current.getBoundingClientRect(), "top") + document.documentElement.scrollTop + 1
636
-
637
- // onLayout left values doesn't always update when changed
638
- left = digg(this.tt.endOfSelectRef.current.getBoundingClientRect(), "left") + document.documentElement.scrollLeft
639
- } else {
640
- left = this.s.endOfSelectLayout.left
641
- top = selectContainerLayout.top
642
- }
643
-
644
- style.left = left
645
- style.top = top - 2
646
- } else if (optionsPlacement == "above") {
647
- if (Platform.OS == "web") {
648
- // onLayout top value is sometimes negative so use browser JS to get it instead
649
- top = digg(this.tt.selectContainerRef.current.getBoundingClientRect(), "top") + document.documentElement.scrollTop
650
-
651
- // onLayout left values doesn't always update when changed
652
- left = digg(this.tt.selectContainerRef.current.getBoundingClientRect(), "left") + document.documentElement.scrollLeft
653
- } else {
654
- left = endOfSelectLayout.left
655
- top = selectContainerLayout.top
656
- }
657
-
658
- style.left = left
659
- style.top = top - optionsContainerLayout.height + 1
660
- } else {
661
- throw new Error(`Unkonwn options placement: ${optionsPlacement}`)
662
- }
663
-
664
- style = this.stylingFor("optionsContainer", style, [left, top, style.visibility, style.width])
665
-
666
- const id = idForComponent(this)
667
-
668
- return (
669
- <View
670
- dataSet={this.cache("optionsContainerDataSet", {class: "options-container", id, role: "dialog"}, [id])}
671
- onLayout={this.tt.onOptionsContainerLayout}
672
- ref={this.tt.optionsContainerRef}
673
- style={style}
674
- >
675
- {loadedOptions?.map((loadedOption) =>
676
- this.hayaSelectOption({
677
- key: loadedOption.key || `loaded-option-${loadedOption.value}`,
678
- loadedOption
679
- })
680
- )}
681
- {loadedOptions?.length === 0 &&
682
- <View dataSet={this.noOptionsContainerDataSet ||= {class: "no-options-container"}}>
683
- <Text>{this.translate(".no_options_found")}</Text>
684
- </View>
685
- }
686
- </View>
687
- )
688
- }
689
-
690
- onOptionClicked = (event, loadedOption) => {
691
- event.preventDefault()
692
- event.stopPropagation()
693
-
694
- const {onChange, toggleOptions} = this.props
695
- const {multiple} = this.p
696
- const currentOptions = this.getCurrentOptions()
697
- const toggled = this.getToggled()
698
- const newState = {}
699
- const existingOption = currentOptions.find((currentOption) => currentOption.value == loadedOption.value)
700
- const newToggled = {...toggled}
701
- let newCurrentOptions
702
-
703
- if (existingOption) {
704
- if (toggleOptions) {
705
- const currentToggle = toggled[loadedOption.value]
706
- const currentIndex = toggleOptions.findIndex((element) => element.value == currentToggle)
707
-
708
- if (currentIndex >= (toggleOptions.length - 1)) {
709
- // No next toggled - remove toggled and option
710
- delete newToggled[loadedOption.value]
711
-
712
- newCurrentOptions = currentOptions.filter((currentOption) => currentOption.value != loadedOption.value)
713
- } else {
714
- // Already toggled - set to next toggle
715
- newToggled[loadedOption.value] = digg(toggleOptions, currentIndex + 1, "value")
716
- }
717
-
718
- newState.toggled = newToggled
719
- } else {
720
- // Remove from current options
721
- newCurrentOptions = currentOptions.filter((currentOption) => currentOption.value != loadedOption.value)
722
- }
723
- } else {
724
- // Don't do anything if the clicked option is disabled
725
- if (loadedOption.disabled) return
726
-
727
- if (toggleOptions) {
728
- // Set fresh toggle
729
- newToggled[loadedOption.value] = toggleOptions[0].value
730
- newState.toggled = newToggled
731
- }
732
-
733
- if (multiple || toggleOptions) {
734
- newCurrentOptions = currentOptions.concat([loadedOption])
735
- } else {
736
- newCurrentOptions = [loadedOption]
737
- }
738
- }
739
-
740
- if ("values" in this.props && this.props.values !== undefined) {
741
- // currentOptions are controlled and a useMemo callback is handeling setting current options.
742
- } else if (newCurrentOptions) {
743
- newState.currentOptions = newCurrentOptions
744
- }
745
-
746
- const options = newCurrentOptions || currentOptions
747
-
748
- if (onChange) {
749
- onChange({
750
- event,
751
- options,
752
- toggles: newToggled
753
- })
754
- }
755
-
756
- if (this.props.onChangeValue) {
757
- let optionValue
758
-
759
- if (multiple) {
760
- optionValue = options.map((option) => option.value)
761
- } else {
762
- optionValue = dig(options, 0, "value")
763
- }
764
-
765
- this.p.onChangeValue(optionValue)
766
- }
767
-
768
- if (!multiple) this.closeOptions()
769
- this.setState(newState)
770
- }
771
-
772
- stylingFor(stylingName, style = {}, caches = []) {
773
- let customStyling = dig(this, "props", "styles", stylingName)
774
-
775
- if (typeof customStyling == "function") customStyling = customStyling({state: this.state, style})
776
-
777
- if (customStyling) {
778
- return Object.assign(style, customStyling)
779
- }
780
-
781
- return this.cache(`stylingFor-${stylingName}`, style, caches)
782
- }
783
-
784
- iconForOption(option) {
785
- const {toggleOptions} = this.props || {}
786
- const toggled = this.getToggled()
787
-
788
- if (toggleOptions && (option.value in toggled)) {
789
- const toggledValue = toggled[option.value]
790
- const toggledOption = toggleOptions.find((element) => element.value == toggledValue)
791
-
792
- if (!toggledOption) {
793
- throw new Error(`Couldn't find a toggle option for value: ${toggledValue}`)
794
- }
795
-
796
- return toggledOption.icon
797
- }
798
- }
799
-
800
- presentOption = (option, mode) => {
801
- const {toggleOptions} = this.props || {}
802
- const toggled = this.getToggled()
803
- const icon = this.iconForOption(option)
804
- const toggleValue = toggled[option.value]
805
- const toggleOption = toggleOptions?.find((toggleOptionI) => toggleOptionI.value == toggleValue)
806
- let style
807
-
808
- if (mode == "current") {
809
- style = this.stylingFor("currentOptionPresentationText", {flex: 1, whiteSpace: "nowrap"})
810
- } else {
811
- style = this.stylingFor("optionPresentationText", {flex: 1, whiteSpace: "nowrap"})
812
- }
813
-
814
- return (
815
- <View
816
- dataSet={{
817
- class: "option-presentation",
818
- text: option.text,
819
- toggleIcon: toggleOption?.icon,
820
- toggleValue: toggleOption?.value,
821
- value: option.value
822
- }}
823
- style={this.optionPresentationStyle ||= {flexDirection: "row"}}
824
- >
825
- {toggleOptions && !(option.value in toggled) &&
826
- <View
827
- dataSet={this.toggleIconPlaceholderDataSet ||= {class: "toggle-icon-placeholder"}}
828
- style={this.toggleIconPlaceholderStyle ||= {width: 20}}
829
- />
830
- }
831
- {toggleOptions && (option.value in toggled) &&
832
- <View style={this.toggleIconContainerStyle ||= {alignItems: "center", justifyContent: "center", width: 20}}>
833
- <FontAwesomeIcon
834
- dataSet={this.toggleIconDataSet ||= {class: "toggle-icon"}}
835
- name={icon}
836
- />
837
- </View>
838
- }
839
- {(() => {
840
- if (mode == "current" && option.currentContent) {
841
- return option.currentContent()
842
- } else if (option.content) {
843
- return option.content()
844
- } else if ("html" in option && Platform.OS != "web") {
845
- return <RenderHtml source={{html: digg(option, "html")}} />
846
- } else if ("html" in option && Platform.OS == "web") {
847
- return <div dangerouslySetInnerHTML={{__html: digg(option, "html")}} />
848
- } else {
849
- return (
850
- <Text
851
- dataSet={this.optionPresentationTextDataSet ||= {class: "option-presentation-text"}}
852
- style={style}
853
- >
854
- {option.text}
855
- </Text>
856
- )
857
- }
858
- })()}
859
- </View>
860
- )
861
- }
862
-
863
- async setCurrentFromGivenValues() {
864
- const {options, values} = this.p
865
- const currentOptions = await options({values})
866
- const currentValues = currentOptions?.map((currentOption) => currentOption.value)
867
- const stateValues = this.s.currentOptions?.map((currentOption) => currentOption.value)
868
-
869
- if (anythingDifferent(currentValues, stateValues)) {
870
- this.setState({currentOptions})
871
- }
872
- }
873
- }))