js-kompo-utils 1.0.26 → 1.0.28

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.
@@ -64,6 +64,7 @@
64
64
  </template>
65
65
 
66
66
  <script>
67
+ import { autoUpdate, computePosition, flip, hide, offset, shift, size } from '@floating-ui/dom'
67
68
  import Select from 'vue-kompo/js/form/form-fields/Select.vue'
68
69
 
69
70
  export default {
@@ -71,7 +72,6 @@ export default {
71
72
  data() {
72
73
  return {
73
74
  flipUp: false,
74
- panelStyle: null,
75
75
  isTouchDevice: false,
76
76
  }
77
77
  },
@@ -82,9 +82,11 @@ export default {
82
82
  forceUp() {
83
83
  return !!this.$_config('openUpward')
84
84
  },
85
+ forceDown() {
86
+ return !!this.$_config('openDownward') || this.$_config('autoOptionsPlacement') === false
87
+ },
85
88
  optionsStyle() {
86
- if (!this.isFocused || !this.panelStyle) return { display: 'none' }
87
- return this.panelStyle
89
+ return this.isFocused ? null : { display: 'none' }
88
90
  },
89
91
  useNativeMobile() {
90
92
  if (!this.$_config('native_on_mobile')) return false
@@ -115,25 +117,24 @@ export default {
115
117
  watch: {
116
118
  isFocused(focused) {
117
119
  if (focused) {
118
- this.$nextTick(() => this.refresh())
119
120
  this.attachListeners()
121
+ this.$nextTick(() => {
122
+ this.teleportOptions()
123
+ this.startPositioning()
124
+ })
120
125
  } else {
121
126
  this.flipUp = false
122
- this.panelStyle = null
123
127
  this.detachListeners()
128
+ this.resetPanelStyles()
124
129
  }
125
130
  },
126
131
  filteredOptions() {
127
- if (this.isFocused) this.$nextTick(() => this.refresh())
132
+ if (this.isFocused) this.$nextTick(() => this.queuePositionUpdate())
128
133
  },
129
134
  optionsKey() {
130
135
  this.$nextTick(() => {
131
136
  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()
137
+ if (this.isFocused) this.startPositioning()
137
138
  })
138
139
  },
139
140
  },
@@ -197,41 +198,13 @@ export default {
197
198
  anchorEl() {
198
199
  return this.$refs.taggableInput?.$el || this.$refs.input?.parentElement
199
200
  },
201
+ overlayAncestor(anchor = this.anchorEl()) {
202
+ return anchor?.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l') || null
203
+ },
200
204
  attachListeners() {
201
205
  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
- })
211
- }
212
- this._onResize = () => this.refresh()
213
- window.addEventListener('scroll', this._onScroll, true)
214
- window.addEventListener('resize', this._onResize, { passive: true })
215
-
216
- if (window.visualViewport) {
217
- this._onViewportChange = () => {
218
- if (this._rafId) return
219
- this._rafId = requestAnimationFrame(() => {
220
- this._rafId = null
221
- this.refresh()
222
- })
223
- }
224
- window.visualViewport.addEventListener('resize', this._onViewportChange)
225
- window.visualViewport.addEventListener('scroll', this._onViewportChange)
226
- }
227
-
228
- const opts = this.$refs.selectOptions
229
206
  const anchor = this.anchorEl()
230
207
  if (typeof ResizeObserver !== 'undefined') {
231
- this._panelObserver = new ResizeObserver(() => {
232
- if (this.isFocused) this.trackPosition()
233
- })
234
- if (opts) this._panelObserver.observe(opts)
235
208
  this._anchorObserver = new ResizeObserver((entries) => {
236
209
  const entry = entries[0]
237
210
  if (!entry) return
@@ -243,93 +216,142 @@ export default {
243
216
  })
244
217
  if (anchor) this._anchorObserver.observe(anchor)
245
218
  }
246
- if (typeof IntersectionObserver !== 'undefined' && anchor) {
247
- this._anchorIntersection = new IntersectionObserver((entries) => {
248
- const entry = entries[entries.length - 1]
249
- if (entry && entry.intersectionRatio === 0) this.blur()
250
- }, { threshold: 0 })
251
- this._anchorIntersection.observe(anchor)
252
- }
253
-
254
219
  this._listenersAttached = true
255
220
  },
256
221
  detachListeners() {
257
222
  if (!this._listenersAttached) return
258
- if (this._rafId) {
259
- cancelAnimationFrame(this._rafId)
260
- this._rafId = null
261
- }
262
- window.removeEventListener('scroll', this._onScroll, true)
263
- window.removeEventListener('resize', this._onResize)
264
- if (this._onViewportChange && window.visualViewport) {
265
- window.visualViewport.removeEventListener('resize', this._onViewportChange)
266
- window.visualViewport.removeEventListener('scroll', this._onViewportChange)
267
- this._onViewportChange = null
268
- }
269
- if (this._panelObserver) {
270
- this._panelObserver.disconnect()
271
- this._panelObserver = null
272
- }
223
+ this.stopPositioning()
273
224
  if (this._anchorObserver) {
274
225
  this._anchorObserver.disconnect()
275
226
  this._anchorObserver = null
276
227
  }
277
- if (this._anchorIntersection) {
278
- this._anchorIntersection.disconnect()
279
- this._anchorIntersection = null
280
- }
281
228
  this._listenersAttached = false
282
229
  },
283
- trackPosition() {
230
+ startPositioning() {
284
231
  const opts = this.$refs.selectOptions
285
232
  const anchor = this.anchorEl()
286
- if (!opts || !anchor) return
287
- const rect = anchor.getBoundingClientRect()
288
- const x = Math.round(rect.left)
289
- const y = this.flipUp
290
- ? Math.round(rect.top) - opts.offsetHeight + 1
291
- : Math.round(rect.bottom) - 1
292
- opts.style.transform = `translate3d(${x}px, ${y}px, 0)`
293
- opts.style.width = Math.round(rect.width) + 'px'
233
+ if (!opts || !anchor || !this.isFocused) return
234
+
235
+ this.stopPositioning()
236
+ this.applyBasePanelStyles(opts, anchor)
237
+ opts.style.visibility = 'hidden'
238
+ opts.style.transform = 'translate3d(0px, 0px, 0)'
239
+
240
+ const updatePosition = () => this.queuePositionUpdate()
241
+ this._autoUpdateCleanup = autoUpdate(anchor, opts, updatePosition, {
242
+ ancestorScroll: true,
243
+ ancestorResize: true,
244
+ elementResize: true,
245
+ layoutShift: true,
246
+ animationFrame: true,
247
+ })
248
+
249
+ this.queuePositionUpdate(true)
250
+ },
251
+ stopPositioning() {
252
+ if (this._positionRafId) {
253
+ cancelAnimationFrame(this._positionRafId)
254
+ this._positionRafId = null
255
+ }
256
+ this._positionVersion = (this._positionVersion || 0) + 1
257
+ if (!this._autoUpdateCleanup) return
258
+ this._autoUpdateCleanup()
259
+ this._autoUpdateCleanup = null
260
+ },
261
+ applyBasePanelStyles(opts, anchor) {
262
+ const inOverlay = !!this.overlayAncestor(anchor)
263
+ opts.style.position = 'fixed'
264
+ opts.style.top = '0px'
265
+ opts.style.left = '0px'
266
+ opts.style.bottom = 'auto'
267
+ opts.style.width = Math.round(anchor.getBoundingClientRect().width) + 'px'
268
+ opts.style.maxHeight = '240px'
269
+ opts.style.zIndex = inOverlay ? '5001' : '1000'
270
+ opts.style.willChange = 'transform'
294
271
  },
295
- refresh() {
272
+ resetPanelStyles() {
296
273
  const opts = this.$refs.selectOptions
297
- const anchor = this.anchorEl()
298
- if (!opts || !anchor) return
274
+ if (!opts) return
299
275
 
300
- const rect = anchor.getBoundingClientRect()
301
- const inOverlay = !!anchor.closest('.kompoModal, .kompoDrawer, .vl-sidebar-l')
302
- const x = Math.round(rect.left)
303
- const yBelow = Math.round(rect.bottom) - 1
304
- const baseStyle = {
305
- position: 'fixed',
306
- top: '0px',
307
- left: '0px',
308
- bottom: 'auto',
309
- width: Math.round(rect.width) + 'px',
310
- zIndex: inOverlay ? 5001 : 1000,
311
- transform: `translate3d(${x}px, ${yBelow}px, 0)`,
312
- willChange: 'transform',
276
+ opts.style.position = ''
277
+ opts.style.top = ''
278
+ opts.style.left = ''
279
+ opts.style.bottom = ''
280
+ opts.style.transform = ''
281
+ opts.style.width = ''
282
+ opts.style.maxHeight = ''
283
+ opts.style.zIndex = ''
284
+ opts.style.willChange = ''
285
+ opts.style.visibility = ''
286
+ opts.style.borderRadius = ''
287
+ },
288
+ queuePositionUpdate(immediate = false) {
289
+ if (!this.isFocused) return
290
+ if (immediate) {
291
+ if (this._positionRafId) {
292
+ cancelAnimationFrame(this._positionRafId)
293
+ this._positionRafId = null
294
+ }
295
+ this.updatePosition()
296
+ return
313
297
  }
314
- this.panelStyle = baseStyle
298
+ if (this._positionRafId) return
299
+ this._positionRafId = requestAnimationFrame(() => {
300
+ this._positionRafId = null
301
+ this.updatePosition()
302
+ })
303
+ },
304
+ placementConfig() {
305
+ if (this.forceUp) return { placement: 'top-start', auto: false }
306
+ if (this.forceDown) return { placement: 'bottom-start', auto: false }
307
+ return { placement: 'bottom-start', auto: true }
308
+ },
309
+ updatePosition() {
310
+ const opts = this.$refs.selectOptions
311
+ const anchor = this.anchorEl()
312
+ if (!opts || !anchor || !this.isFocused) return
315
313
 
316
- this.$nextTick(() => {
317
- const optsHeight = opts.offsetHeight
318
- const vv = window.visualViewport
319
- const viewportHeight = vv ? vv.height : window.innerHeight
320
- const viewportTop = vv ? vv.offsetTop : 0
321
- const spaceBelow = (viewportTop + viewportHeight) - rect.bottom
322
- const spaceAbove = rect.top - viewportTop
323
- const shouldFlip = this.forceUp || (spaceBelow < optsHeight && spaceAbove > spaceBelow)
324
- this.flipUp = shouldFlip
325
- if (shouldFlip) {
326
- const yAbove = Math.round(rect.top) - optsHeight + 1
327
- this.panelStyle = {
328
- ...baseStyle,
329
- transform: `translate3d(${x}px, ${yAbove}px, 0)`,
330
- borderRadius: '5px 5px 0 0',
331
- }
314
+ const floatingEl = opts
315
+ const referenceEl = anchor
316
+ const positionVersion = this._positionVersion || 0
317
+ const { placement, auto } = this.placementConfig()
318
+
319
+ const middleware = [
320
+ offset(-1),
321
+ ...(auto ? [flip({ fallbackPlacements: ['top-start'], padding: 8 })] : []),
322
+ shift({ padding: 8, crossAxis: true }),
323
+ size({
324
+ padding: 8,
325
+ apply: ({ availableHeight, rects, elements }) => {
326
+ const maxHeight = Math.max(0, Math.min(240, Math.floor(availableHeight)))
327
+ elements.floating.style.width = Math.round(rects.reference.width) + 'px'
328
+ elements.floating.style.maxHeight = maxHeight + 'px'
329
+ },
330
+ }),
331
+ hide(),
332
+ ]
333
+
334
+ computePosition(referenceEl, floatingEl, {
335
+ strategy: 'fixed',
336
+ placement,
337
+ middleware,
338
+ }).then(({ x, y, placement, middlewareData }) => {
339
+ if (positionVersion !== (this._positionVersion || 0)) return
340
+ if (!this.isFocused) return
341
+ if (floatingEl !== this.$refs.selectOptions || referenceEl !== this.anchorEl()) {
342
+ this.$nextTick(() => {
343
+ if (this.isFocused) this.startPositioning()
344
+ })
345
+ return
332
346
  }
347
+
348
+ const hidden = middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped
349
+ const flipsUp = placement.startsWith('top')
350
+
351
+ this.flipUp = flipsUp
352
+ floatingEl.style.transform = `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`
353
+ floatingEl.style.borderRadius = flipsUp ? '5px 5px 0 0' : '0 0 5px 5px'
354
+ floatingEl.style.visibility = hidden ? 'hidden' : 'visible'
333
355
  })
334
356
  },
335
357
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-kompo-utils",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "",
5
5
  "main": "app.js",
6
6
  "scripts": {