js-kompo-utils 1.0.21 → 1.0.23

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.
@@ -63,6 +63,9 @@ export default {
63
63
  shouldShowMenu(visible) {
64
64
  if (visible) {
65
65
  this.updatePosition()
66
+ this.attachListeners()
67
+ } else {
68
+ this.detachListeners()
66
69
  }
67
70
  if (!visible && this.openOnClick) {
68
71
  this.open = false
@@ -147,11 +150,10 @@ export default {
147
150
  },
148
151
  mounted() {
149
152
  this.teleportMenu()
150
- this.setupListeners()
151
153
  },
152
154
  beforeDestroy() {
153
155
  this.cleanupMenu()
154
- this.removeListeners()
156
+ this.detachListeners()
155
157
  },
156
158
  methods:{
157
159
  teleportMenu() {
@@ -182,27 +184,19 @@ export default {
182
184
  clearTimeout(this._closeTimeout)
183
185
  }
184
186
  },
185
- setupListeners() {
186
- this._scrollHandler = () => {
187
- if (this.shouldShowMenu) {
188
- requestAnimationFrame(() => this.updatePosition())
189
- }
190
- }
191
- this._resizeHandler = () => {
192
- if (this.shouldShowMenu) {
193
- this.updatePosition()
194
- }
195
- }
187
+ attachListeners() {
188
+ if (this._listenersAttached) return
189
+ this._scrollHandler = () => requestAnimationFrame(() => this.updatePosition())
190
+ this._resizeHandler = () => this.updatePosition()
196
191
  window.addEventListener('scroll', this._scrollHandler, true)
197
192
  window.addEventListener('resize', this._resizeHandler, { passive: true })
193
+ this._listenersAttached = true
198
194
  },
199
- removeListeners() {
200
- if (this._scrollHandler) {
201
- window.removeEventListener('scroll', this._scrollHandler, true)
202
- }
203
- if (this._resizeHandler) {
204
- window.removeEventListener('resize', this._resizeHandler)
205
- }
195
+ detachListeners() {
196
+ if (!this._listenersAttached) return
197
+ window.removeEventListener('scroll', this._scrollHandler, true)
198
+ window.removeEventListener('resize', this._resizeHandler)
199
+ this._listenersAttached = false
206
200
  },
207
201
  updatePosition() {
208
202
  const root = this.$refs.dropdownRoot
@@ -1,31 +1,42 @@
1
1
  <template>
2
2
  <vl-form-field v-bind="$_wrapperAttributes">
3
- <vlTaggableInput
4
- v-click-out="blur"
5
- v-bind="$_taggableInputAttributes"
6
- v-on="$_taggableInputEvents">
7
- <template v-slot:prepend v-if="$_icon">
8
- <div v-bind="prependIcon" />
9
- </template>
10
- <input
11
- class="vlFormControl"
12
- v-bind="$_attributes"
13
- v-on="$_events"
14
- v-model="inputValue"
15
- @keydown.enter.prevent="() => {}"
16
- ref="input"
17
- type="search"
18
- autocomplete="off"
19
- />
20
- <template v-slot:append v-if="appendIcon && !$_readOnly">
21
- <div v-html="appendIcon"/>
22
- </template>
23
- </vlTaggableInput>
24
- <div class="vlOptions"
3
+ <div :style="useNativeMobile ? { position: 'relative' } : null">
4
+ <vlTaggableInput
5
+ v-click-out="blur"
6
+ v-bind="$_taggableInputAttributes"
7
+ v-on="$_taggableInputEvents"
8
+ ref="taggableInput">
9
+ <template v-slot:prepend v-if="$_icon">
10
+ <div v-bind="prependIcon" />
11
+ </template>
12
+ <input
13
+ class="vlFormControl"
14
+ v-bind="$_attributes"
15
+ v-on="$_events"
16
+ v-model="inputValue"
17
+ @keydown.enter.prevent="() => {}"
18
+ ref="input"
19
+ type="search"
20
+ autocomplete="off"
21
+ />
22
+ <template v-slot:append v-if="appendIcon && !$_readOnly">
23
+ <div v-html="appendIcon"/>
24
+ </template>
25
+ </vlTaggableInput>
26
+ <select v-if="useNativeMobile"
27
+ class="vlNativeSelect"
28
+ :value="nativeValue"
29
+ @change="onNativeChange"
30
+ :style="nativeSelectStyle">
31
+ <option value="">{{ nativePlaceholder }}</option>
32
+ <option v-for="opt in options" :key="opt.value" :value="opt.value">{{ nativeLabel(opt) }}</option>
33
+ </select>
34
+ </div>
35
+ <div class="vlOptions vlOptionsTeleported"
25
36
  :key="optionsKey"
26
37
  ref="selectOptions"
27
38
  :style="optionsStyle"
28
- :class="{ 'vlOptionsFlipUp': flipUp }">
39
+ :class="{ 'vlOptionsFlipUp': flipUp, 'vlOptionsVisible': isFocused }">
29
40
  <template v-if="filteredOptions.length">
30
41
  <div v-for="(option,key) in filteredOptions" :key="option.value" :ref="key"
31
42
  class="vlOption"
@@ -58,7 +69,8 @@ export default {
58
69
  data() {
59
70
  return {
60
71
  flipUp: false,
61
- overlayStyle: null,
72
+ panelStyle: null,
73
+ isTouchDevice: false,
62
74
  }
63
75
  },
64
76
  computed: {
@@ -69,51 +81,135 @@ export default {
69
81
  return !!this.$_config('openUpward')
70
82
  },
71
83
  optionsStyle() {
72
- if (this.overlayStyle) return this.overlayStyle
73
- if (this.flipUp) {
74
- return { top: 'auto', bottom: '100%', borderRadius: '5px 5px 0 0' }
84
+ if (!this.isFocused || !this.panelStyle) return { display: 'none' }
85
+ return this.panelStyle
86
+ },
87
+ useNativeMobile() {
88
+ if (this.$_config('disableNativeMobile')) return false
89
+ return this.isTouchDevice && !this.$_multiple && !this.ajaxOptions
90
+ },
91
+ nativeValue() {
92
+ return this.$_value && this.$_value.length ? this.$_value[0].value : ''
93
+ },
94
+ nativePlaceholder() {
95
+ return this.$_config('placeholder') || ''
96
+ },
97
+ nativeSelectStyle() {
98
+ return {
99
+ position: 'absolute',
100
+ top: 0, left: 0, right: 0, bottom: 0,
101
+ width: '100%', height: '100%',
102
+ opacity: 0,
103
+ appearance: 'none',
104
+ border: 0,
105
+ background: 'transparent',
106
+ zIndex: 2,
75
107
  }
76
- return {}
77
108
  },
78
109
  },
79
110
  watch: {
80
111
  isFocused(focused) {
81
112
  if (focused) {
82
113
  this.$nextTick(() => this.refresh())
114
+ this.attachListeners()
83
115
  } else {
84
116
  this.flipUp = false
85
- this.overlayStyle = null
117
+ this.panelStyle = null
118
+ this.detachListeners()
86
119
  }
87
120
  },
88
121
  filteredOptions() {
89
122
  if (this.isFocused) this.$nextTick(() => this.refresh())
90
123
  },
91
124
  },
125
+ mounted() {
126
+ this.teleportOptions()
127
+ if (typeof window !== 'undefined' && window.matchMedia) {
128
+ this.isTouchDevice = window.matchMedia('(pointer: coarse)').matches
129
+ }
130
+ },
131
+ beforeDestroy() {
132
+ this.cleanupOptions()
133
+ this.detachListeners()
134
+ },
92
135
  methods: {
136
+ onNativeChange(e) {
137
+ const v = e.target.value
138
+ if (v === '') return
139
+ const opt = this.options.find(o => String(o.value) === String(v))
140
+ if (opt) this.select(opt)
141
+ },
142
+ nativeLabel(opt) {
143
+ const l = opt.label
144
+ if (typeof l === 'string') return l
145
+ if (l && typeof l === 'object') return l.label || l.text || l.search || ''
146
+ return String(opt.value ?? '')
147
+ },
148
+ teleportOptions() {
149
+ const opts = this.$refs.selectOptions
150
+ if (opts && opts.parentNode !== document.body) {
151
+ document.body.appendChild(opts)
152
+ }
153
+ },
154
+ cleanupOptions() {
155
+ const opts = this.$refs.selectOptions
156
+ if (opts && opts.parentNode === document.body) {
157
+ document.body.removeChild(opts)
158
+ }
159
+ },
160
+ anchorEl() {
161
+ return this.$refs.taggableInput?.$el || this.$refs.input?.parentElement
162
+ },
163
+ attachListeners() {
164
+ if (this._listenersAttached) return
165
+ this._onScroll = (e) => {
166
+ const opts = this.$refs.selectOptions
167
+ if (opts && e.target instanceof Node && opts.contains(e.target)) return
168
+ this.blur()
169
+ }
170
+ this._onResize = () => this.refresh()
171
+ window.addEventListener('scroll', this._onScroll, true)
172
+ window.addEventListener('resize', this._onResize, { passive: true })
173
+ this._listenersAttached = true
174
+ },
175
+ detachListeners() {
176
+ if (!this._listenersAttached) return
177
+ window.removeEventListener('scroll', this._onScroll, true)
178
+ window.removeEventListener('resize', this._onResize)
179
+ this._listenersAttached = false
180
+ },
93
181
  refresh() {
94
182
  const opts = this.$refs.selectOptions
95
- const anchor = opts && opts.parentElement
183
+ const anchor = this.anchorEl()
96
184
  if (!opts || !anchor) return
97
185
 
98
186
  const rect = anchor.getBoundingClientRect()
99
- const optsHeight = opts.offsetHeight
100
- const spaceBelow = window.innerHeight - rect.bottom
101
- const spaceAbove = rect.top
102
- this.flipUp = this.forceUp || (spaceBelow < optsHeight && spaceAbove > spaceBelow)
187
+ const inOverlay = !!anchor.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l')
188
+ const baseStyle = {
189
+ position: 'fixed',
190
+ left: rect.left + 'px',
191
+ width: rect.width + 'px',
192
+ zIndex: inOverlay ? 5001 : 1000,
193
+ top: rect.bottom + 'px',
194
+ bottom: 'auto',
195
+ }
196
+ this.panelStyle = baseStyle
103
197
 
104
- if (opts.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l')) {
105
- this.overlayStyle = {
106
- position: 'fixed',
107
- left: rect.left + 'px',
108
- width: rect.width + 'px',
109
- zIndex: 5001,
110
- ...(this.flipUp
111
- ? { top: 'auto', bottom: (window.innerHeight - rect.top) + 'px', borderRadius: '5px 5px 0 0' }
112
- : { top: rect.bottom + 'px', bottom: 'auto' }),
198
+ this.$nextTick(() => {
199
+ const optsHeight = opts.offsetHeight
200
+ const spaceBelow = window.innerHeight - rect.bottom
201
+ const spaceAbove = rect.top
202
+ const shouldFlip = this.forceUp || (spaceBelow < optsHeight && spaceAbove > spaceBelow)
203
+ this.flipUp = shouldFlip
204
+ if (shouldFlip) {
205
+ this.panelStyle = {
206
+ ...baseStyle,
207
+ top: 'auto',
208
+ bottom: (window.innerHeight - rect.top) + 'px',
209
+ borderRadius: '5px 5px 0 0',
210
+ }
113
211
  }
114
- } else {
115
- this.overlayStyle = null
116
- }
212
+ })
117
213
  },
118
214
  },
119
215
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-kompo-utils",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "",
5
5
  "main": "app.js",
6
6
  "scripts": {