haya-select 1.0.45 → 1.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/select/index.jsx +120 -75
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "haya-select",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,14 +1,16 @@
1
1
  import {anythingDifferent} from "set-state-compare/src/diff-utils"
2
- import config from "../config.js"
3
- import {dig, digg, digs} from "diggerize"
2
+ import {dig, digg} from "diggerize"
4
3
  import debounce from "debounce"
4
+ import FontAwesomeIcon from "react-native-vector-icons/FontAwesome"
5
5
  import idForComponent from "@kaspernj/api-maker/src/inputs/id-for-component.mjs"
6
6
  import nameForComponent from "@kaspernj/api-maker/src/inputs/name-for-component.mjs"
7
7
  import Option from "./option"
8
8
  import OptionGroup from "./option-group"
9
9
  import PropTypes from "prop-types"
10
10
  import propTypesExact from "prop-types-exact"
11
- import {memo, useRef} from "react"
11
+ import RenderHtml from "react-native-render-html"
12
+ import {Portal} from "react-native-portalize"
13
+ import {memo, useEffect, useRef} from "react"
12
14
  import {shapeComponent, ShapeComponent} from "set-state-compare/src/shape-component.js"
13
15
  import {Platform, Pressable, Text, TextInput, View} from "react-native"
14
16
  import useEventListener from "@kaspernj/api-maker/src/use-event-listener"
@@ -24,6 +26,8 @@ const nameForComponentWithMultiple = (component) => {
24
26
  export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
25
27
  static defaultProps = {
26
28
  multiple: false,
29
+ optionsAbsolute: true,
30
+ optionsWidth: null,
27
31
  search: false
28
32
  }
29
33
 
@@ -42,6 +46,8 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
42
46
  onChangeValue: PropTypes.func,
43
47
  onOptionsClosed: PropTypes.func,
44
48
  options: PropTypes.oneOfType([PropTypes.array, PropTypes.func]).isRequired,
49
+ optionsAbsolute: PropTypes.bool.isRequired,
50
+ optionsWidth: PropTypes.number,
45
51
  placeholder: PropTypes.node,
46
52
  search: PropTypes.bool.isRequired,
47
53
  selectContainerStyle: PropTypes.object,
@@ -55,6 +61,7 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
55
61
  values: PropTypes.array
56
62
  })
57
63
 
64
+ callOptionsPositionAboveIfOutsideScreen = false
58
65
  p = fetchingObject(() => this.props)
59
66
  s = fetchingObject(() => this.state)
60
67
  tt = fetchingObject(this)
@@ -73,27 +80,39 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
73
80
  this.setInstance({
74
81
  endOfSelectRef: useRef(),
75
82
  optionsContainerRef: useRef(),
76
- currentSelectedRef: useRef(),
77
83
  searchTextInputRef: useRef(),
84
+ selectContainerRef: useRef(),
78
85
  t
79
86
  })
80
87
  this.useStates({
81
88
  currentOptions: () => this.defaultCurrentOptions(),
89
+ selectContainerLayout: null,
90
+ endOfSelectLayout: null,
82
91
  loadedOptions: () => this.defaultLoadedOptions(),
83
92
  opened: false,
84
- optionsLeft: undefined,
93
+ optionsContainerLayout: null,
85
94
  optionsPlacement: undefined,
86
95
  optionsTop: undefined,
87
96
  optionsVisibility: undefined,
88
97
  optionsWidth: undefined,
98
+ scrollLeft: document.documentElement.scrollLeft,
99
+ scrollTop: document.documentElement.scrollTop,
89
100
  searchText: "",
90
- scrollLeft: undefined,
91
- scrollTop: undefined,
92
101
  toggled: () => this.defaultToggled()
93
102
  })
94
- useEventListener(window, "click", this.onWindowClicked)
95
- useEventListener(window, "resize", this.tt.onAnythingResizedDebounced)
96
- useEventListener(window, "scroll", this.tt.onAnythingScrolledDebounced)
103
+
104
+ if (Platform.OS == "web") {
105
+ useEventListener(window, "click", this.tt.onWindowClicked)
106
+ useEventListener(window, "resize", this.tt.onAnythingResizedDebounced)
107
+ useEventListener(window, "scroll", this.tt.onAnythingScrolledDebounced)
108
+ }
109
+
110
+ useEffect(() => {
111
+ if (this.tt.callOptionsPositionAboveIfOutsideScreen && this.s.optionsContainerLayout) {
112
+ this.callOptionsPositionAboveIfOutsideScreen = false
113
+ this.setOptionsPositionAboveIfOutsideScreen()
114
+ }
115
+ }, [this.tt.callOptionsPositionAboveIfOutsideScreen, this.s.optionsContainerLayout])
97
116
  }
98
117
 
99
118
  defaultCurrentOptions() {
@@ -126,7 +145,7 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
126
145
  getValues = () => ("values" in this.props) ? this.p.values : this.s.currentOptions.map((currentOption) => currentOption.value)
127
146
  getCurrentOptions = () => {
128
147
  if ("values" in this.props && typeof this.props.values != "undefined") {
129
- if (Array.isArray(this.props.options)) {
148
+ if (Array.isArray(this.props.options) && this.props.values) {
130
149
  return this.p.values.map((value) => this.p.options.find((option) => option.value == value))
131
150
  } else if (this.s.loadedOptions || typeof this.props.options == "function" && this.props.values) {
132
151
  if (this.s.loadedOptions) {
@@ -151,7 +170,7 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
151
170
  } else {
152
171
  this.setCurrentFromGivenValues()
153
172
  }
154
- } else {
173
+ } else if (this.props.values) {
155
174
  return this.p.values.map((value) => ({value}))
156
175
  }
157
176
  }
@@ -192,12 +211,10 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
192
211
  }
193
212
 
194
213
  render() {
195
- const {currentSelectedRef, endOfSelectRef} = this.tt
214
+ const {endOfSelectRef} = this.tt
196
215
  const {className, placeholder, toggleOptions} = this.props
197
216
  const {opened, optionsPlacement} = this.s
198
217
  const currentOptions = this.getCurrentOptions()
199
- const BodyPortal = config.getBodyPortal()
200
-
201
218
  const selectContainerStyleActual = this.stylingFor("selectContainer", {
202
219
  display: "flex",
203
220
  flexDirection: "row",
@@ -250,19 +267,15 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
250
267
  }}
251
268
  style={this.stylingFor("main")}
252
269
  >
253
- {opened &&
254
- <BodyPortal>
255
- {this.optionsContainer()}
256
- </BodyPortal>
257
- }
258
270
  <Pressable
259
271
  dataSet={{class: "select-container"}}
272
+ onLayout={this.tt.onSelectContainerLayout}
260
273
  onPress={this.tt.onSelectClicked}
274
+ ref={this.tt.selectContainerRef}
261
275
  style={selectContainerStyleActual}
262
276
  >
263
277
  <View
264
278
  dataSet={{class: "current-selected"}}
265
- ref={currentSelectedRef}
266
279
  style={this.stylingFor("currentSelected", {width: "calc(100% - 27px)", flexWrap: "wrap"})}
267
280
  >
268
281
  {opened &&
@@ -284,7 +297,7 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
284
297
  {!opened &&
285
298
  <>
286
299
  {currentOptions.length == 0 &&
287
- <Text style={this.stylingFor("nothingSelected", {color: "grey"})}>
300
+ <Text numberOfLines={1} style={this.stylingFor("nothingSelected", {color: "grey"})}>
288
301
  {placeholder || this.t(".nothing_selected")}
289
302
  </Text>
290
303
  }
@@ -348,7 +361,12 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
348
361
  </Text>
349
362
  </View>
350
363
  </Pressable>
351
- <View ref={endOfSelectRef} />
364
+ <View dataSet={{class: "end-of-select"}} onLayout={this.tt.onEndOfSelectLayout} ref={endOfSelectRef} />
365
+ {opened &&
366
+ <Portal dataSet={{class: "haya-select-portal"}}>
367
+ {this.optionsContainer()}
368
+ </Portal>
369
+ }
352
370
  </View>
353
371
  )
354
372
  }
@@ -425,6 +443,10 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
425
443
  this.setState({loadedOptions})
426
444
  }
427
445
 
446
+ onSelectContainerLayout = (e) => this.setState({selectContainerLayout: Object.assign({}, digg(e, "nativeEvent", "layout"))})
447
+ onEndOfSelectLayout = (e) => this.setState({endOfSelectLayout: Object.assign({}, digg(e, "nativeEvent", "layout"))})
448
+ onOptionsContainerLayout = (e) => this.setState({optionsContainerLayout: Object.assign({}, digg(e, "nativeEvent", "layout"))})
449
+
428
450
  onSelectClicked = (e) => {
429
451
  e.preventDefault()
430
452
  e.stopPropagation()
@@ -443,7 +465,8 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
443
465
  closeOptions() {
444
466
  this.setState({
445
467
  loadedOptions: undefined,
446
- opened: false
468
+ opened: false,
469
+ optionsContainerLayout: null
447
470
  })
448
471
 
449
472
  if (this.props.onOptionsClosed) {
@@ -456,11 +479,16 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
456
479
  openOptions() {
457
480
  this.setOptionsPositionBelow()
458
481
  this.loadOptions()
482
+ this.callOptionsPositionAboveIfOutsideScreen = true
459
483
  this.setState(
460
- {opened: true, searchText: ""},
484
+ {
485
+ opened: true,
486
+ scrollLeft: Platform.OS == "web" ? document.documentElement.scrollLeft : null,
487
+ scrollTop: Platform.OS == "web" ? document.documentElement.scrollTop : null,
488
+ searchText: ""
489
+ },
461
490
  () => {
462
491
  this.focusTextInput()
463
- this.setOptionsPositionAboveIfOutsideScreen()
464
492
  }
465
493
  )
466
494
  }
@@ -472,19 +500,17 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
472
500
  return // Debounce after un-mount handeling.
473
501
  }
474
502
 
503
+ this.callOptionsPositionAboveIfOutsideScreen = true
475
504
  this.setOptionsPositionBelow()
476
- this.setOptionsPositionAboveIfOutsideScreen()
477
505
  }
478
506
 
479
507
  setOptionsPositionAboveIfOutsideScreen() {
480
- const {optionsContainerRef} = this.tt
481
- const {optionsTop} = this.s
482
- const optionsHeight = digg(optionsContainerRef, "current", "offsetHeight")
483
- const scrollTop = document.documentElement.scrollTop
484
- const optionsTotalHeight = optionsHeight + optionsTop + scrollTop
485
- const windowHeightWithScroll = window.innerHeight + scrollTop
508
+ const {optionsContainerLayout} = this.s
509
+ const optionsTop = this.s.endOfSelectLayout.top
510
+ const optionsTotalBottomPosition = optionsContainerLayout.height + optionsTop
511
+ const windowHeightWithScroll = document.body.clientHeight + this.s.scrollTop
486
512
 
487
- if (windowHeightWithScroll < optionsTotalHeight) {
513
+ if (windowHeightWithScroll < optionsTotalBottomPosition) {
488
514
  this.setOptionsPositionAbove()
489
515
  } else {
490
516
  this.setState({optionsVisibility: "visible"})
@@ -492,49 +518,32 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
492
518
  }
493
519
 
494
520
  setOptionsPositionAbove() {
495
- const {optionsContainerRef, currentSelectedRef, endOfSelectRef} = this.tt
496
- const optionsHeight = digg(optionsContainerRef, "current", "offsetHeight")
497
- const position = currentSelectedRef.current.getBoundingClientRect()
498
- const {top} = digs(position, "top")
499
- const optionsTop = top - optionsHeight + 2
500
- const {left, width} = digs(endOfSelectRef.current.getBoundingClientRect(), "left", "width")
521
+ const {endOfSelectLayout} = this.s
501
522
 
502
523
  this.setState(
503
524
  {
504
525
  opened: true,
505
- optionsLeft: left,
506
526
  optionsPlacement: "above",
507
- optionsTop,
508
527
  optionsVisibility: "visible",
509
- optionsWidth: width,
510
- scrollLeft: document.documentElement.scrollLeft,
511
- scrollTop: document.documentElement.scrollTop
528
+ optionsWidth: endOfSelectLayout.width
512
529
  },
513
530
  () => this.focusTextInput()
514
531
  )
515
532
  }
516
533
 
517
534
  setOptionsPositionBelow() {
518
- const {endOfSelectRef} = this.tt
519
- const position = endOfSelectRef.current.getBoundingClientRect()
520
- const {left, top, width} = digs(position, "left", "top", "width")
521
-
522
535
  this.setState(
523
536
  {
524
537
  opened: true,
525
- optionsLeft: left,
526
538
  optionsPlacement: "below",
527
- optionsTop: top,
528
539
  optionsVisibility: "hidden",
529
- optionsWidth: width,
530
- scrollLeft: document.documentElement.scrollLeft,
531
- scrollTop: document.documentElement.scrollTop
540
+ optionsWidth: this.s.endOfSelectLayout.width
532
541
  },
533
542
  () => this.focusTextInput()
534
543
  )
535
544
  }
536
545
 
537
- onAnythingResized = (e) => {
546
+ onAnythingResized = () => {
538
547
  if (this.s.opened) {
539
548
  this.setOptionsPosition()
540
549
  }
@@ -542,8 +551,12 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
542
551
 
543
552
  onAnythingResizedDebounced = debounce(this.tt.onAnythingResized, 25)
544
553
 
545
- onAnythingScrolled = (e) => {
554
+ onAnythingScrolled = () => {
546
555
  if (this.s.opened) {
556
+ this.setState({
557
+ scrollLeft: document.documentElement.scrollLeft,
558
+ scrollTop: document.documentElement.scrollTop
559
+ })
547
560
  this.setOptionsPosition()
548
561
  }
549
562
  }
@@ -562,26 +575,55 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
562
575
 
563
576
  optionsContainer() {
564
577
  const {optionsContainerRef} = this.tt
565
- const {loadedOptions, optionsLeft, optionsTop, optionsVisibility, optionsWidth, scrollLeft, scrollTop} = this.s
578
+ const {optionsAbsolute} = this.p
579
+ const {selectContainerLayout, loadedOptions, endOfSelectLayout, optionsContainerLayout, optionsPlacement, optionsVisibility} = this.s
580
+ let top
581
+
582
+ const style = this.stylingFor("optionsContainer", {
583
+ position: "absolute",
584
+ zIndex: 99999,
585
+ elevation: 99999,
586
+ visibility: optionsVisibility,
587
+ width: this.p.optionsWidth || this.s.optionsWidth,
588
+ backgroundColor: "#fff",
589
+ border: "1px solid #999",
590
+ maxHeight: 300,
591
+ overflowY: "auto"
592
+ })
593
+
594
+ if (!optionsAbsolute) {
595
+ style.left = 0
596
+ style.bottom = 0
597
+ } else if (optionsPlacement == "below") {
598
+ if (Platform.OS == "web") {
599
+ // onLayout top value is sometimes negative so use browser JS to get it instead
600
+ top = digg(this.tt.endOfSelectRef.current.getBoundingClientRect(), "top")
601
+ } else {
602
+ top = selectContainerLayout.top
603
+ }
604
+
605
+ style.left = this.s.endOfSelectLayout.left
606
+ style.top = top - 2
607
+ } else if (optionsPlacement == "above") {
608
+ if (Platform.OS == "web") {
609
+ // onLayout top value is sometimes negative so use browser JS to get it instead
610
+ top = digg(this.tt.selectContainerRef.current.getBoundingClientRect(), "top")
611
+ } else {
612
+ top = selectContainerLayout.top
613
+ }
614
+
615
+ style.left = endOfSelectLayout.left
616
+ style.top = top - optionsContainerLayout.height + 1
617
+ } else {
618
+ throw new Error(`Unkonwn options placement: ${optionsPlacement}`)
619
+ }
566
620
 
567
621
  return (
568
622
  <View
569
623
  dataSet={{class: "options-container", id: idForComponent(this)}}
624
+ onLayout={this.tt.onOptionsContainerLayout}
570
625
  ref={optionsContainerRef}
571
- style={
572
- this.stylingFor("optionsContainer", {
573
- position: "absolute",
574
- left: optionsLeft + scrollLeft,
575
- top: optionsTop + scrollTop - 1,
576
- zIndex: 99999,
577
- visibility: optionsVisibility,
578
- width: optionsWidth,
579
- backgroundColor: "#fff",
580
- border: "1px solid #999",
581
- maxHeight: 300,
582
- overflowY: "auto"
583
- })
584
- }
626
+ style={style}
585
627
  >
586
628
  {loadedOptions?.map((loadedOption) =>
587
629
  this.hayaSelectOption({
@@ -720,18 +762,21 @@ export default memo(shapeComponent(class HayaSelect extends ShapeComponent {
720
762
  }}
721
763
  >
722
764
  {toggleOptions && !(currentValue.value in toggled) &&
723
- <i className="fa fa-fw" />
765
+ <View dataSet={{class: "toggle-icon-placeholder"}} style={{width: 25}} />
724
766
  }
725
767
  {toggleOptions && (currentValue.value in toggled) &&
726
- <i className={`fa fa-fw fa-${icon}`} />
768
+ <FontAwesomeIcon dataSet={{class: "toggle-icon"}} name={icon} style={{width: 25}} />
727
769
  }
728
770
  {currentValue.content}
729
771
  {!currentValue.content &&
730
- <Text className="option-presentation-text" style={this.stylingFor("optionPresentationText", {whiteSpace: "nowrap"})}>
772
+ <Text dataSet={{class: "option-presentation-text"}} style={this.stylingFor("optionPresentationText", {whiteSpace: "nowrap"})}>
731
773
  {currentValue.text}
732
774
  </Text>
733
775
  }
734
- {("html" in currentValue) &&
776
+ {("html" in currentValue) && Platform.OS != "web" &&
777
+ <RenderHtml source={{html: digg(currentValue, "html")}} />
778
+ }
779
+ {("html" in currentValue) && Platform.OS == "web" &&
735
780
  <div dangerouslySetInnerHTML={{__html: digg(currentValue, "html")}} />
736
781
  }
737
782
  </View>