js-kompo-utils 1.0.27 → 1.0.29

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.
@@ -34,11 +34,15 @@
34
34
  <option v-for="opt in nativeOptionsList" :key="opt.value" :value="opt.value">{{ nativeLabel(opt) }}</option>
35
35
  </select>
36
36
  </div>
37
- <div class="vlOptions vlOptionsTeleported"
37
+ <div class="vlOptions"
38
38
  :key="optionsKey"
39
39
  ref="selectOptions"
40
40
  :style="optionsStyle"
41
- :class="{ 'vlOptionsFlipUp': flipUp, 'vlOptionsVisible': isFocused }">
41
+ :class="{
42
+ 'vlOptionsTeleported': useFloatingOptions,
43
+ 'vlOptionsFlipUp': useFloatingOptions && flipUp,
44
+ 'vlOptionsVisible': useFloatingOptions && isFocused
45
+ }">
42
46
  <template v-if="filteredOptions.length">
43
47
  <div v-for="(option,key) in filteredOptions" :key="option.value" :ref="key"
44
48
  class="vlOption"
@@ -64,6 +68,7 @@
64
68
  </template>
65
69
 
66
70
  <script>
71
+ import { autoUpdate, computePosition, flip, hide, offset, shift, size } from '@floating-ui/dom'
67
72
  import Select from 'vue-kompo/js/form/form-fields/Select.vue'
68
73
 
69
74
  export default {
@@ -71,7 +76,6 @@ export default {
71
76
  data() {
72
77
  return {
73
78
  flipUp: false,
74
- panelStyle: null,
75
79
  isTouchDevice: false,
76
80
  }
77
81
  },
@@ -82,9 +86,18 @@ export default {
82
86
  forceUp() {
83
87
  return !!this.$_config('openUpward')
84
88
  },
89
+ forceDown() {
90
+ return !!this.$_config('openDownward') || this.$_config('autoOptionsPlacement') === false
91
+ },
92
+ useFloatingOptions() {
93
+ const explicitFloating = this.$_config('floatingOptions')
94
+ if (explicitFloating !== undefined && explicitFloating !== null) return !!explicitFloating
95
+
96
+ return !!this.$_config('teleportedOptions') || this.forceUp
97
+ },
85
98
  optionsStyle() {
86
- if (!this.isFocused || !this.panelStyle) return { display: 'none' }
87
- return this.panelStyle
99
+ if (!this.useFloatingOptions) return null
100
+ return this.isFocused ? null : { display: 'none' }
88
101
  },
89
102
  useNativeMobile() {
90
103
  if (!this.$_config('native_on_mobile')) return false
@@ -114,31 +127,39 @@ export default {
114
127
  },
115
128
  watch: {
116
129
  isFocused(focused) {
130
+ if (!this.useFloatingOptions) {
131
+ if (!focused) {
132
+ this.flipUp = false
133
+ this.detachListeners()
134
+ this.resetPanelStyles()
135
+ }
136
+ return
137
+ }
117
138
  if (focused) {
118
- this.$nextTick(() => this.refresh())
119
139
  this.attachListeners()
140
+ this.$nextTick(() => {
141
+ this.teleportOptions()
142
+ this.startPositioning()
143
+ })
120
144
  } else {
121
145
  this.flipUp = false
122
- this.panelStyle = null
123
146
  this.detachListeners()
147
+ this.resetPanelStyles()
124
148
  }
125
149
  },
126
150
  filteredOptions() {
127
- if (this.isFocused) this.$nextTick(() => this.refresh())
151
+ if (this.useFloatingOptions && this.isFocused) this.$nextTick(() => this.queuePositionUpdate())
128
152
  },
129
153
  optionsKey() {
154
+ if (!this.useFloatingOptions) return
130
155
  this.$nextTick(() => {
131
156
  this.teleportOptions()
132
- if (this._panelObserver && this.$refs.selectOptions) {
133
- this._panelObserver.disconnect()
134
- this._panelObserver.observe(this.$refs.selectOptions)
135
- }
136
- if (this.isFocused) this.refresh()
157
+ if (this.isFocused) this.startPositioning()
137
158
  })
138
159
  },
139
160
  },
140
161
  mounted() {
141
- this.teleportOptions()
162
+ if (this.useFloatingOptions) this.teleportOptions()
142
163
  if (typeof window !== 'undefined' && window.matchMedia) {
143
164
  this.isTouchDevice = window.matchMedia('(pointer: coarse)').matches
144
165
  }
@@ -182,57 +203,61 @@ export default {
182
203
  tmp.innerHTML = str
183
204
  return (tmp.textContent || tmp.innerText || '').trim()
184
205
  },
185
- teleportOptions() {
206
+ teleportOptions(anchor = this.anchorEl()) {
207
+ if (!this.useFloatingOptions) return
186
208
  const opts = this.$refs.selectOptions
187
- if (opts && opts.parentNode !== document.body) {
188
- document.body.appendChild(opts)
209
+ const context = this.floatingContext(anchor)
210
+ const root = context?.root
211
+ if (opts && root && opts.parentNode !== root) {
212
+ root.appendChild(opts)
189
213
  }
190
214
  },
191
215
  cleanupOptions() {
216
+ if (!this.useFloatingOptions) return
192
217
  const opts = this.$refs.selectOptions
193
- if (opts && opts.parentNode === document.body) {
194
- document.body.removeChild(opts)
218
+ if (opts && opts.parentNode && this.$el && !this.$el.contains(opts)) {
219
+ opts.parentNode.removeChild(opts)
195
220
  }
196
221
  },
197
222
  anchorEl() {
198
223
  return this.$refs.taggableInput?.$el || this.$refs.input?.parentElement
199
224
  },
200
- attachListeners() {
201
- if (this._listenersAttached) return
202
- this._rafId = null
203
- this._onScroll = (e) => {
204
- const opts = this.$refs.selectOptions
205
- if (opts && e.target instanceof Node && opts.contains(e.target)) return
206
- if (this._rafId) return
207
- this._rafId = requestAnimationFrame(() => {
208
- this._rafId = null
209
- this.trackPosition()
210
- })
225
+ overlayAncestor(anchor = this.anchorEl()) {
226
+ return anchor?.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l, .vl-sidebar-r') || null
227
+ },
228
+ floatingContext(anchor = this.anchorEl()) {
229
+ if (!anchor || typeof document === 'undefined') return null
230
+
231
+ const drawerOrSidebar = anchor.closest('.kompoDrawer, .vl-sidebar-l, .vl-sidebar-r')
232
+ if (drawerOrSidebar && typeof window !== 'undefined') {
233
+ const position = window.getComputedStyle(drawerOrSidebar).position
234
+ if (position && position !== 'static') {
235
+ return {
236
+ root: drawerOrSidebar,
237
+ strategy: 'absolute',
238
+ zIndex: 5001,
239
+ }
240
+ }
211
241
  }
212
- this._onResize = () => this.refresh()
213
- window.addEventListener('scroll', this._onScroll, true)
214
- window.addEventListener('resize', this._onResize, { passive: true })
215
242
 
216
- if (window.visualViewport) {
217
- this._onViewportScroll = () => {
218
- if (this._rafId) return
219
- this._rafId = requestAnimationFrame(() => {
220
- this._rafId = null
221
- this.trackPosition()
222
- })
243
+ if (this.overlayAncestor(anchor)) {
244
+ return {
245
+ root: document.body,
246
+ strategy: 'fixed',
247
+ zIndex: 5001,
223
248
  }
224
- this._onViewportResize = () => this.refresh()
225
- window.visualViewport.addEventListener('scroll', this._onViewportScroll)
226
- window.visualViewport.addEventListener('resize', this._onViewportResize)
227
249
  }
228
250
 
229
- const opts = this.$refs.selectOptions
251
+ return {
252
+ root: document.body,
253
+ strategy: 'absolute',
254
+ zIndex: 1000,
255
+ }
256
+ },
257
+ attachListeners() {
258
+ if (this._listenersAttached) return
230
259
  const anchor = this.anchorEl()
231
260
  if (typeof ResizeObserver !== 'undefined') {
232
- this._panelObserver = new ResizeObserver(() => {
233
- if (this.isFocused) this.trackPosition()
234
- })
235
- if (opts) this._panelObserver.observe(opts)
236
261
  this._anchorObserver = new ResizeObserver((entries) => {
237
262
  const entry = entries[0]
238
263
  if (!entry) return
@@ -244,105 +269,157 @@ export default {
244
269
  })
245
270
  if (anchor) this._anchorObserver.observe(anchor)
246
271
  }
247
- const inOverlay = !!(anchor && anchor.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l'))
248
- if (inOverlay && typeof IntersectionObserver !== 'undefined' && anchor) {
249
- let intersectionInitialised = false
250
- this._anchorIntersection = new IntersectionObserver((entries) => {
251
- const entry = entries[entries.length - 1]
252
- if (!entry) return
253
- if (!intersectionInitialised) {
254
- intersectionInitialised = true
255
- return
256
- }
257
- if (entry.intersectionRatio === 0) this.blur()
258
- }, { threshold: 0 })
259
- this._anchorIntersection.observe(anchor)
260
- }
261
-
262
272
  this._listenersAttached = true
263
273
  },
264
274
  detachListeners() {
265
275
  if (!this._listenersAttached) return
266
- if (this._rafId) {
267
- cancelAnimationFrame(this._rafId)
268
- this._rafId = null
269
- }
270
- window.removeEventListener('scroll', this._onScroll, true)
271
- window.removeEventListener('resize', this._onResize)
272
- if (window.visualViewport) {
273
- if (this._onViewportScroll) {
274
- window.visualViewport.removeEventListener('scroll', this._onViewportScroll)
275
- this._onViewportScroll = null
276
- }
277
- if (this._onViewportResize) {
278
- window.visualViewport.removeEventListener('resize', this._onViewportResize)
279
- this._onViewportResize = null
280
- }
281
- }
282
- if (this._panelObserver) {
283
- this._panelObserver.disconnect()
284
- this._panelObserver = null
285
- }
276
+ this.stopPositioning()
286
277
  if (this._anchorObserver) {
287
278
  this._anchorObserver.disconnect()
288
279
  this._anchorObserver = null
289
280
  }
290
- if (this._anchorIntersection) {
291
- this._anchorIntersection.disconnect()
292
- this._anchorIntersection = null
293
- }
294
281
  this._listenersAttached = false
295
282
  },
296
- trackPosition() {
283
+ startPositioning() {
297
284
  const opts = this.$refs.selectOptions
298
285
  const anchor = this.anchorEl()
299
- if (!opts || !anchor) return
300
- const rect = anchor.getBoundingClientRect()
301
- const x = Math.round(rect.left)
302
- const y = this.flipUp
303
- ? Math.round(rect.top) - opts.offsetHeight + 1
304
- : Math.round(rect.bottom) - 1
305
- opts.style.transform = `translate3d(${x}px, ${y}px, 0)`
306
- opts.style.width = Math.round(rect.width) + 'px'
286
+ if (!opts || !anchor || !this.isFocused) return
287
+
288
+ this.stopPositioning()
289
+ this.teleportOptions(anchor)
290
+ const context = this.floatingContext(anchor)
291
+ if (!context) return
292
+ this.applyBasePanelStyles(opts, anchor, context)
293
+ opts.style.visibility = 'hidden'
294
+ opts.style.transform = 'translate3d(0px, 0px, 0)'
295
+
296
+ const updatePosition = () => this.queuePositionUpdate()
297
+ this._autoUpdateCleanup = autoUpdate(anchor, opts, updatePosition, {
298
+ ancestorScroll: true,
299
+ ancestorResize: true,
300
+ elementResize: true,
301
+ layoutShift: true,
302
+ animationFrame: true,
303
+ })
304
+
305
+ this.queuePositionUpdate(true)
306
+ },
307
+ stopPositioning() {
308
+ if (this._positionRafId) {
309
+ cancelAnimationFrame(this._positionRafId)
310
+ this._positionRafId = null
311
+ }
312
+ this._positionVersion = (this._positionVersion || 0) + 1
313
+ if (!this._autoUpdateCleanup) return
314
+ this._autoUpdateCleanup()
315
+ this._autoUpdateCleanup = null
316
+ },
317
+ applyBasePanelStyles(opts, anchor, context = this.floatingContext(anchor)) {
318
+ if (!context) return
319
+
320
+ opts.style.position = context.strategy
321
+ opts.style.top = '0px'
322
+ opts.style.left = '0px'
323
+ opts.style.bottom = 'auto'
324
+ opts.style.width = Math.round(anchor.getBoundingClientRect().width) + 'px'
325
+ opts.style.maxHeight = '240px'
326
+ opts.style.zIndex = String(context.zIndex)
327
+ opts.style.willChange = 'transform'
328
+ },
329
+ resetPanelStyles() {
330
+ const opts = this.$refs.selectOptions
331
+ if (!opts) return
332
+
333
+ opts.style.position = ''
334
+ opts.style.top = ''
335
+ opts.style.left = ''
336
+ opts.style.bottom = ''
337
+ opts.style.transform = ''
338
+ opts.style.width = ''
339
+ opts.style.maxHeight = ''
340
+ opts.style.zIndex = ''
341
+ opts.style.willChange = ''
342
+ opts.style.visibility = ''
343
+ opts.style.borderRadius = ''
307
344
  },
308
- refresh() {
345
+ queuePositionUpdate(immediate = false) {
346
+ if (!this.isFocused) return
347
+ if (immediate) {
348
+ if (this._positionRafId) {
349
+ cancelAnimationFrame(this._positionRafId)
350
+ this._positionRafId = null
351
+ }
352
+ this.updatePosition()
353
+ return
354
+ }
355
+ if (this._positionRafId) return
356
+ this._positionRafId = requestAnimationFrame(() => {
357
+ this._positionRafId = null
358
+ this.updatePosition()
359
+ })
360
+ },
361
+ placementConfig() {
362
+ if (this.forceUp) return { placement: 'top-start', auto: false }
363
+ if (this.forceDown) return { placement: 'bottom-start', auto: false }
364
+ return { placement: 'bottom-start', auto: true }
365
+ },
366
+ updatePosition() {
309
367
  const opts = this.$refs.selectOptions
310
368
  const anchor = this.anchorEl()
311
- if (!opts || !anchor) return
369
+ if (!opts || !anchor || !this.isFocused) return
370
+
371
+ const floatingEl = opts
372
+ const referenceEl = anchor
373
+ const positionVersion = this._positionVersion || 0
374
+ const { placement, auto } = this.placementConfig()
375
+ const context = this.floatingContext(referenceEl)
376
+ if (!context) return
312
377
 
313
- const rect = anchor.getBoundingClientRect()
314
- const inOverlay = !!anchor.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l')
315
- const x = Math.round(rect.left)
316
- const yBelow = Math.round(rect.bottom) - 1
317
- const baseStyle = {
318
- position: 'fixed',
319
- top: '0px',
320
- left: '0px',
321
- bottom: 'auto',
322
- width: Math.round(rect.width) + 'px',
323
- zIndex: inOverlay ? 5001 : 1000,
324
- transform: `translate3d(${x}px, ${yBelow}px, 0)`,
325
- willChange: 'transform',
378
+ if (floatingEl.parentNode !== context.root) {
379
+ this.$nextTick(() => {
380
+ if (this.isFocused) this.startPositioning()
381
+ })
382
+ return
326
383
  }
327
- this.panelStyle = baseStyle
328
384
 
329
- this.$nextTick(() => {
330
- const optsHeight = opts.offsetHeight
331
- const vv = window.visualViewport
332
- const viewportHeight = vv ? vv.height : window.innerHeight
333
- const viewportTop = vv ? vv.offsetTop : 0
334
- const spaceBelow = (viewportTop + viewportHeight) - rect.bottom
335
- const spaceAbove = rect.top - viewportTop
336
- const shouldFlip = this.forceUp || (spaceBelow < optsHeight && spaceAbove > spaceBelow)
337
- this.flipUp = shouldFlip
338
- if (shouldFlip) {
339
- const yAbove = Math.round(rect.top) - optsHeight + 1
340
- this.panelStyle = {
341
- ...baseStyle,
342
- transform: `translate3d(${x}px, ${yAbove}px, 0)`,
343
- borderRadius: '5px 5px 0 0',
344
- }
385
+ this.applyBasePanelStyles(floatingEl, referenceEl, context)
386
+
387
+ const middleware = [
388
+ offset(-1),
389
+ ...(auto ? [flip({ fallbackPlacements: ['top-start'], padding: 8 })] : []),
390
+ shift({ padding: 8, crossAxis: true }),
391
+ size({
392
+ padding: 8,
393
+ apply: ({ availableHeight, rects, elements }) => {
394
+ const maxHeight = Math.max(0, Math.min(240, Math.floor(availableHeight)))
395
+ elements.floating.style.width = Math.round(rects.reference.width) + 'px'
396
+ elements.floating.style.maxHeight = maxHeight + 'px'
397
+ },
398
+ }),
399
+ hide(),
400
+ ]
401
+
402
+ computePosition(referenceEl, floatingEl, {
403
+ strategy: context.strategy,
404
+ placement,
405
+ middleware,
406
+ }).then(({ x, y, placement, middlewareData }) => {
407
+ if (positionVersion !== (this._positionVersion || 0)) return
408
+ if (!this.isFocused) return
409
+ if (floatingEl !== this.$refs.selectOptions || referenceEl !== this.anchorEl()) {
410
+ this.$nextTick(() => {
411
+ if (this.isFocused) this.startPositioning()
412
+ })
413
+ return
345
414
  }
415
+
416
+ const hidden = middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped
417
+ const flipsUp = placement.startsWith('top')
418
+
419
+ this.flipUp = flipsUp
420
+ floatingEl.style.transform = `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`
421
+ floatingEl.style.borderRadius = flipsUp ? '5px 5px 0 0' : '0 0 5px 5px'
422
+ floatingEl.style.visibility = hidden ? 'hidden' : 'visible'
346
423
  })
347
424
  },
348
425
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-kompo-utils",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "",
5
5
  "main": "app.js",
6
6
  "scripts": {