js-kompo-utils 1.0.27 → 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,42 +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._onViewportScroll = () => {
218
- if (this._rafId) return
219
- this._rafId = requestAnimationFrame(() => {
220
- this._rafId = null
221
- this.trackPosition()
222
- })
223
- }
224
- this._onViewportResize = () => this.refresh()
225
- window.visualViewport.addEventListener('scroll', this._onViewportScroll)
226
- window.visualViewport.addEventListener('resize', this._onViewportResize)
227
- }
228
-
229
- const opts = this.$refs.selectOptions
230
206
  const anchor = this.anchorEl()
231
207
  if (typeof ResizeObserver !== 'undefined') {
232
- this._panelObserver = new ResizeObserver(() => {
233
- if (this.isFocused) this.trackPosition()
234
- })
235
- if (opts) this._panelObserver.observe(opts)
236
208
  this._anchorObserver = new ResizeObserver((entries) => {
237
209
  const entry = entries[0]
238
210
  if (!entry) return
@@ -244,105 +216,142 @@ export default {
244
216
  })
245
217
  if (anchor) this._anchorObserver.observe(anchor)
246
218
  }
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
219
  this._listenersAttached = true
263
220
  },
264
221
  detachListeners() {
265
222
  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
- }
223
+ this.stopPositioning()
286
224
  if (this._anchorObserver) {
287
225
  this._anchorObserver.disconnect()
288
226
  this._anchorObserver = null
289
227
  }
290
- if (this._anchorIntersection) {
291
- this._anchorIntersection.disconnect()
292
- this._anchorIntersection = null
293
- }
294
228
  this._listenersAttached = false
295
229
  },
296
- trackPosition() {
230
+ startPositioning() {
297
231
  const opts = this.$refs.selectOptions
298
232
  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'
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
307
260
  },
308
- refresh() {
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'
271
+ },
272
+ resetPanelStyles() {
309
273
  const opts = this.$refs.selectOptions
310
- const anchor = this.anchorEl()
311
- if (!opts || !anchor) return
274
+ if (!opts) return
312
275
 
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',
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
326
297
  }
327
- 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
328
313
 
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
- }
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
345
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'
346
355
  })
347
356
  },
348
357
  },
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.28",
4
4
  "description": "",
5
5
  "main": "app.js",
6
6
  "scripts": {