wave-ui 1.45.14 → 1.48.0

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.
@@ -3,20 +3,20 @@
3
3
  slot(name="activator" :on="activatorEventHandlers")
4
4
  transition(:name="transitionName" appear)
5
5
  .w-menu(
6
- v-if="custom && menuVisible"
7
- ref="menu"
8
- @click="hideOnMenuClick && closeMenu(true)"
6
+ v-if="custom && detachableVisible"
7
+ ref="detachable"
8
+ @click="hideOnMenuClick && close(true)"
9
9
  @mouseenter="showOnHover && (hoveringMenu = true)"
10
- @mouseleave="showOnHover && ((hoveringMenu = false), closeMenu())"
10
+ @mouseleave="showOnHover && ((hoveringMenu = false), close())"
11
11
  :class="classes"
12
12
  :style="styles")
13
13
  slot
14
14
  w-card.w-menu(
15
- v-else-if="menuVisible"
16
- ref="menu"
17
- @click.native="hideOnMenuClick && closeMenu(true)"
15
+ v-else-if="detachableVisible"
16
+ ref="detachable"
17
+ @click.native="hideOnMenuClick && close(true)"
18
18
  @mouseenter.native="showOnHover && (hoveringMenu = true)"
19
- @mouseleave.native="showOnHover && ((hoveringMenu = false), closeMenu())"
19
+ @mouseleave.native="showOnHover && ((hoveringMenu = false), close())"
20
20
  :tile="tile"
21
21
  :title-class="titleClasses"
22
22
  :content-class="contentClasses"
@@ -32,12 +32,12 @@
32
32
  w-overlay(
33
33
  v-if="overlay"
34
34
  ref="overlay"
35
- :value="menuVisible"
35
+ :value="detachableVisible"
36
36
  :persistent="persistent"
37
37
  :class="overlayClasses"
38
38
  v-bind="overlayProps"
39
39
  :z-index="(zIndex || 200) - 1"
40
- @input="menuVisible = false")
40
+ @input="detachableVisible = false")
41
41
  </template>
42
42
 
43
43
  <script>
@@ -51,12 +51,13 @@
51
51
  */
52
52
 
53
53
  import { objectifyClasses } from '../utils/index'
54
- import { consoleWarn } from '../utils/console'
54
+ import DetachableMixin from '../mixins/detachable'
55
55
 
56
56
  // const marginFromWindowSide = 4 // Amount of px from a window side, instead of overflowing.
57
57
 
58
58
  export default {
59
59
  name: 'w-menu',
60
+ mixins: [DetachableMixin],
60
61
 
61
62
  props: {
62
63
  value: {}, // Show or hide.
@@ -75,7 +76,8 @@ export default {
75
76
  contentClass: { type: [String, Object, Array] },
76
77
  // Position.
77
78
  arrow: { type: Boolean }, // The small triangle pointing toward the activator.
78
- detachTo: { type: [String, Boolean, Object] },
79
+ detachTo: { type: [String, Boolean, Object], deprecated: true },
80
+ appendTo: { type: [String, Boolean, Object] },
79
81
  fixed: { type: Boolean },
80
82
  top: { type: Boolean },
81
83
  bottom: { type: Boolean },
@@ -97,53 +99,31 @@ export default {
97
99
  emits: ['input', 'update:modelValue', 'open', 'close'],
98
100
 
99
101
  data: () => ({
100
- menuVisible: false,
102
+ detachableVisible: false,
101
103
  hoveringActivator: false,
102
104
  hoveringMenu: false,
103
105
  // The menu computed top & left coordinates.
104
- menuCoordinates: {
106
+ detachableCoords: {
105
107
  top: 0,
106
108
  left: 0
107
109
  },
108
110
  activatorEl: null,
109
111
  activatorWidth: 0,
110
- menuEl: null,
112
+ detachableEl: null,
111
113
  timeoutId: null
112
114
  }),
113
115
 
114
116
  computed: {
117
+ /**
118
+ * Other computed in the detachable mixin:
119
+ * - `appendToTarget`
120
+ * - `detachableParentEl`
121
+ **/
122
+
115
123
  transitionName () {
116
124
  return this.transition || 'scale-fade'
117
125
  },
118
126
 
119
- // DOM element to attach menu to.
120
- // ! \ This computed uses the DOM - NO SSR (only trigger from beforeMount and later).
121
- detachToTarget () {
122
- const defaultTarget = '.w-app'
123
-
124
- let target = this.detachTo || defaultTarget
125
- if (target === true) target = defaultTarget
126
- else if (target && !['object', 'string'].includes(typeof target)) target = defaultTarget
127
- else if (typeof target === 'object' && !target.nodeType) {
128
- target = defaultTarget
129
- consoleWarn('Invalid node provided in w-menu `detach-to`. Falling back to .w-app.', this)
130
- }
131
- if (typeof target === 'string') target = document.querySelector(target)
132
-
133
- if (!target) {
134
- consoleWarn(`Unable to locate ${this.detachTo ? `target ${this.detachTo}` : defaultTarget}`, this)
135
- target = document.querySelector(defaultTarget)
136
- }
137
-
138
- return target
139
- },
140
-
141
- // DOM element that will receive the menu.
142
- // ! \ This computed uses the DOM - NO SSR (only trigger from beforeMount and later).
143
- menuParentEl () {
144
- return this.detachToTarget
145
- },
146
-
147
127
  position () {
148
128
  return (
149
129
  (this.top && 'top') ||
@@ -154,21 +134,21 @@ export default {
154
134
  )
155
135
  },
156
136
 
157
- menuMinWidth () {
158
- if (this.minWidth === 'activator') return this.activatorWidth ? `${this.activatorWidth}px` : 0
159
- else return isNaN(this.minWidth) ? this.minWidth : (this.minWidth ? `${this.minWidth}px` : 0)
160
- },
161
-
162
137
  alignment () {
163
138
  return (
164
- ((this.top || this.bottom) && this.alignLeft && 'left') ||
165
- ((this.top || this.bottom) && this.alignRight && 'right') ||
166
- ((this.left || this.right) && this.alignTop && 'top') ||
167
- ((this.left || this.right) && this.alignBottom && 'bottom') ||
139
+ (['top', 'bottom'].includes(this.position) && this.alignLeft && 'left') ||
140
+ (['top', 'bottom'].includes(this.position) && this.alignRight && 'right') ||
141
+ (['left', 'right'].includes(this.position) && this.alignTop && 'top') ||
142
+ (['left', 'right'].includes(this.position) && this.alignBottom && 'bottom') ||
168
143
  ''
169
144
  )
170
145
  },
171
146
 
147
+ menuMinWidth () {
148
+ if (this.minWidth === 'activator') return this.activatorWidth ? `${this.activatorWidth}px` : 0
149
+ else return isNaN(this.minWidth) ? this.minWidth : (this.minWidth ? `${this.minWidth}px` : 0)
150
+ },
151
+
172
152
  menuClasses () {
173
153
  return objectifyClasses(this.menuClass)
174
154
  },
@@ -205,8 +185,8 @@ export default {
205
185
  styles () {
206
186
  return {
207
187
  zIndex: this.zIndex || this.zIndex === 0 || (this.overlay && !this.zIndex && 200) || null,
208
- top: (this.menuCoordinates.top && `${~~this.menuCoordinates.top}px`) || null,
209
- left: (this.menuCoordinates.left && `${~~this.menuCoordinates.left}px`) || null,
188
+ top: (this.detachableCoords.top && `${~~this.detachableCoords.top}px`) || null,
189
+ left: (this.detachableCoords.left && `${~~this.detachableCoords.left}px`) || null,
210
190
  minWidth: (this.minWidth && this.menuMinWidth) || null,
211
191
  '--w-menu-bg-color': this.arrow && this.$waveui.colors[this.bgColor || 'white']
212
192
  }
@@ -221,13 +201,13 @@ export default {
221
201
  blur: this.toggleMenu,
222
202
  mouseenter: e => {
223
203
  this.hoveringActivator = true
224
- this.openMenu(e)
204
+ this.open(e)
225
205
  },
226
206
  mouseleave: e => {
227
207
  this.hoveringActivator = false
228
208
  // Wait 10ms, the time to get the hoveringMenu updated on mouseenter on the menu.
229
209
  setTimeout(() => {
230
- if (!this.hoveringMenu) this.closeMenu()
210
+ if (!this.hoveringMenu) this.close()
231
211
  }, 10)
232
212
  }
233
213
  }
@@ -242,9 +222,19 @@ export default {
242
222
  },
243
223
 
244
224
  methods: {
225
+ /**
226
+ * Other methods in the `detachable` mixin:
227
+ * - `getActivatorCoordinates`
228
+ * - `computeDetachableCoords`
229
+ * - `onResize`
230
+ * - `onOutsideMousedown`
231
+ * - `insertInDOM`
232
+ * - `removeFromDOM`
233
+ **/
234
+
245
235
  // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
246
236
  toggleMenu (e) {
247
- let shouldShowMenu = this.menuVisible
237
+ let shouldShowMenu = this.detachableVisible
248
238
  if ('ontouchstart' in window && this.showOnHover && e.type === 'click') {
249
239
  shouldShowMenu = !shouldShowMenu
250
240
  }
@@ -261,25 +251,25 @@ export default {
261
251
  this.timeoutId = clearTimeout(this.timeoutId)
262
252
 
263
253
  if (shouldShowMenu) {
264
- this.$emit('update:modelValue', (this.menuVisible = true))
254
+ this.$emit('update:modelValue', (this.detachableVisible = true))
265
255
  this.$emit('input', true)
266
256
  this.$emit('open')
267
257
 
268
- this.openMenu(e)
258
+ this.open(e)
269
259
  }
270
- else this.closeMenu()
260
+ else this.close()
271
261
  },
272
262
 
273
263
  // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
274
- async openMenu (e) {
275
- this.menuVisible = true
276
- await this.insertMenu()
264
+ async open (e) {
265
+ this.detachableVisible = true
266
+ await this.insertInDOM()
277
267
 
278
268
  if (this.minWidth === 'activator') this.activatorWidth = this.activatorEl.offsetWidth
279
269
 
280
- if (!this.noPosition) this.computeMenuPosition(e)
270
+ if (!this.noPosition) this.computeDetachableCoords(e)
281
271
 
282
- // In `getCoordinates` accessing the menu computed styles takes a few ms (less than 10ms),
272
+ // In `getActivatorCoordinates` accessing the menu computed styles takes a few ms (less than 10ms),
283
273
  // if we don't postpone the Menu apparition it will start transition from a visible menu and
284
274
  // thus will not transition.
285
275
  this.timeoutId = setTimeout(() => {
@@ -303,170 +293,29 @@ export default {
303
293
  * But if hideOnMenuClick is also set to true, this should force close
304
294
  * even while hovering the menu.
305
295
  */
306
- async closeMenu (force = false) {
296
+ async close (force = false) {
307
297
  // Might be already closed.
308
298
  // E.g. showOnHover & hideOnMenuClick: on click, force hide then mouseleave is also firing.
309
- if (!this.menuVisible) return
299
+ if (!this.detachableVisible) return
310
300
 
311
301
  if (this.showOnHover && !force) {
312
302
  await new Promise(resolve => setTimeout(resolve, 10))
313
303
  if (this.showOnHover && (this.hoveringMenu || this.hoveringActivator)) return
314
304
  }
315
305
 
316
- this.$emit('update:modelValue', (this.menuVisible = false))
306
+ this.$emit('update:modelValue', (this.detachableVisible = false))
317
307
  this.$emit('input', false)
318
308
  this.$emit('close')
319
309
  // Remove the mousedown listener if the menu got closed without a mousedown outside of the menu.
320
310
  document.removeEventListener('mousedown', this.onOutsideMousedown)
321
311
  window.removeEventListener('resize', this.onResize)
322
- },
323
-
324
- // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
325
- onOutsideMousedown (e) {
326
- if (!this.menuEl.contains(e.target) && !this.activatorEl.contains(e.target)) {
327
- this.$emit('update:modelValue', (this.menuVisible = false))
328
- this.$emit('input', false)
329
- this.$emit('close')
330
- document.removeEventListener('mousedown', this.onOutsideMousedown)
331
- window.removeEventListener('resize', this.onResize)
332
- }
333
- },
334
-
335
- onResize () {
336
- if (this.minWidth === 'activator') this.activatorWidth = this.activatorEl.offsetWidth
337
- this.computeMenuPosition()
338
- },
339
-
340
- // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
341
- getCoordinates (e) {
342
- // Get the activator coordinates relative to window.
343
- const { top, left, width, height } = (e ? e.target : this.activatorEl).getBoundingClientRect()
344
- let coords = { top, left, width, height }
345
-
346
- // If absolute position, adjust top & left.
347
- if (!this.fixed) {
348
- const { top: targetTop, left: targetLeft } = this.menuParentEl.getBoundingClientRect()
349
- const computedStyles = window.getComputedStyle(this.menuParentEl, null)
350
- coords = {
351
- ...coords,
352
- top: top - targetTop + this.menuParentEl.scrollTop - parseInt(computedStyles.getPropertyValue('border-top-width')),
353
- left: left - targetLeft + this.menuParentEl.scrollLeft - parseInt(computedStyles.getPropertyValue('border-left-width'))
354
- }
355
- }
356
-
357
- return coords
358
- },
359
-
360
- // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
361
- computeMenuPosition (e) {
362
- // Get the activator coordinates.
363
- let { top, left, width, height } = this.getCoordinates(e)
364
-
365
- // 1. First display the menu but hide it (So we can get its dimension).
366
- // --------------------------------------------------
367
- this.menuEl.style.visibility = 'hidden'
368
- this.menuEl.style.display = 'flex'
369
- const computedStyles = window.getComputedStyle(this.menuEl, null)
370
-
371
- // 2. Position the menu top, left, right, bottom and apply chosen alignment.
372
- // --------------------------------------------------
373
- // Subtract half or full activator width or height and menu width or height according to the
374
- // menu alignment.
375
- // Note: the menu position relies on transform translate, the custom animation may override the
376
- // css transform property so do without it i.e. no translateX(-50%), and recalculate top & left
377
- // manually.
378
- switch (this.position) {
379
- case 'top': {
380
- top -= this.menuEl.offsetHeight
381
- if (this.alignRight) {
382
- // left: 100% of activator.
383
- left += width - this.menuEl.offsetWidth +
384
- parseInt(computedStyles.getPropertyValue('border-right-width'))
385
- }
386
- else if (!this.alignLeft) left += (width - this.menuEl.offsetWidth) / 2 // left: 50% of activator - half menu width.
387
- break
388
- }
389
- case 'bottom': {
390
- top += height
391
- if (this.alignRight) {
392
- // left: 100% of activator.
393
- left += width - this.menuEl.offsetWidth +
394
- parseInt(computedStyles.getPropertyValue('border-right-width'))
395
- }
396
- else if (!this.alignLeft) left += (width - this.menuEl.offsetWidth) / 2 // left: 50% of activator - half menu width.
397
- break
398
- }
399
- case 'left': {
400
- left -= this.menuEl.offsetWidth
401
- if (this.alignBottom) top += height - this.menuEl.offsetHeight
402
- else if (!this.alignTop) top += (height - this.menuEl.offsetHeight) / 2 // top: 50% of activator - half menu height.
403
- break
404
- }
405
- case 'right': {
406
- left += width
407
- if (this.alignBottom) {
408
- top += height - this.menuEl.offsetHeight +
409
- parseInt(computedStyles.getPropertyValue('margin-top'))
410
- }
411
- else if (!this.alignTop) {
412
- top += (height - this.menuEl.offsetHeight) / 2 + // top: 50% of activator - half menu height.
413
- parseInt(computedStyles.getPropertyValue('margin-top'))
414
- }
415
- break
416
- }
417
- }
418
-
419
- // 3. Keep fully in viewport.
420
- // @todo: do this.
421
- // --------------------------------------------------
422
- // if (this.position === 'top' && ((top - this.menuEl.offsetHeight) < 0)) {
423
- // const margin = - parseInt(computedStyles.getPropertyValue('margin-top'))
424
- // top -= top - this.menuEl.offsetHeight - margin - marginFromWindowSide
425
- // }
426
- // else if (this.position === 'left' && left - this.menuEl.offsetWidth < 0) {
427
- // const margin = - parseInt(computedStyles.getPropertyValue('margin-left'))
428
- // left -= left - this.menuEl.offsetWidth - margin - marginFromWindowSide
429
- // }
430
- // else if (this.position === 'right' && left + width + this.menuEl.offsetWidth > window.innerWidth) {
431
- // const margin = parseInt(computedStyles.getPropertyValue('margin-left'))
432
- // left -= left + width + this.menuEl.offsetWidth - window.innerWidth + margin + marginFromWindowSide
433
- // }
434
- // else if (this.position === 'bottom' && top + height + this.menuEl.offsetHeight > window.innerHeight) {
435
- // const margin = parseInt(computedStyles.getPropertyValue('margin-top'))
436
- // top -= top + height + this.menuEl.offsetHeight - window.innerHeight + margin + marginFromWindowSide
437
- // }
438
-
439
- // 4. Hide the menu again so the transition happens correctly.
440
- // --------------------------------------------------
441
- this.menuEl.style.visibility = null
442
-
443
- // The menu coordinates are also recalculated while resizing window with open menu: keep the menu visible.
444
- if (!this.menuVisible) this.menuEl.style.display = 'none'
445
-
446
- this.menuCoordinates = { top, left }
447
- },
448
-
449
- insertMenu () {
450
- return new Promise(resolve => {
451
- this.$nextTick(() => {
452
- this.menuEl = this.$refs.menu?.$el || this.$refs.menu
453
-
454
- // Move the menu elsewhere in the DOM.
455
- // wrapper.parentNode.insertBefore(this.menuEl, wrapper)
456
- this.detachToTarget.appendChild(this.menuEl)
457
- resolve()
458
- })
459
- })
460
- },
461
-
462
- removeMenu () {
463
- if (this.menuEl && this.menuEl.parentNode) this.menuEl.remove()
464
312
  }
465
313
  },
466
314
 
467
315
  mounted () {
468
316
  const wrapper = this.$el
469
317
  this.activatorEl = wrapper.firstElementChild
318
+
470
319
  // Unwrap the activator element.
471
320
  wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
472
321
 
@@ -480,18 +329,22 @@ export default {
480
329
  },
481
330
 
482
331
  beforeDestroy () {
483
- this.removeMenu()
332
+ this.removeFromDOM()
484
333
  if (this.overlay && this.overlayEl.parentNode) this.overlayEl.remove()
485
334
  if (this.activatorEl && this.activatorEl.parentNode) this.activatorEl.remove()
486
335
  },
487
336
 
488
337
  watch: {
489
338
  value (bool) {
490
- if (!!bool !== this.menuVisible) this.toggleMenu({ type: 'click', target: this.activatorEl })
339
+ if (!!bool !== this.detachableVisible) this.toggleMenu({ type: 'click', target: this.activatorEl })
491
340
  },
492
341
  detachTo () {
493
- this.removeMenu()
494
- this.insertMenu()
342
+ this.removeFromDOM()
343
+ this.insertInDOM()
344
+ },
345
+ appendTo () {
346
+ this.removeFromDOM()
347
+ this.insertInDOM()
495
348
  }
496
349
  }
497
350
  }
@@ -21,7 +21,7 @@ component(
21
21
  v-model="showMenu"
22
22
  :menu-class="`w-select__menu ${menuClass || ''}`"
23
23
  transition="slide-fade-down"
24
- :detach-to="(menuProps || {}).detachTo !== undefined ? (menuProps || {}).detachTo : '.w-app'"
24
+ :append-to="(menuProps || {}).appendTo !== undefined ? (menuProps || {}).appendTo : '.w-app'"
25
25
  align-left
26
26
  custom
27
27
  min-width="activator"
@@ -89,7 +89,6 @@ export default {
89
89
  background-color: rgba(255, 255, 255, 0.85);
90
90
  padding-left: 2 * $base-increment;
91
91
  padding-right: 2 * $base-increment;
92
- font-size: round(0.85 * $base-font-size);
93
92
  cursor: default;
94
93
  user-select: none;
95
94
 
@@ -100,23 +99,35 @@ export default {
100
99
  &--tile {border-radius: initial;}
101
100
  &--shadow {box-shadow: $box-shadow;}
102
101
 
103
- &.size--xs {font-size: round(0.7 * $base-font-size);}
102
+ &.size--xs {
103
+ $font-size: round(0.7 * $base-font-size);
104
+ font-size: $font-size;
105
+ line-height: $font-size + 2px;
106
+ }
104
107
  &.size--sm {
105
- font-size: round(0.82 * $base-font-size);
108
+ $font-size: round(0.82 * $base-font-size);
109
+ font-size: $font-size;
110
+ line-height: $font-size + 2px;
106
111
  padding: round(0.25 * $base-increment) $base-increment;
107
112
  }
108
113
  &.size--md {
109
- font-size: round(0.95 * $base-font-size);
114
+ $font-size: round(0.85 * $base-font-size);
115
+ font-size: $font-size;
116
+ line-height: $font-size + 4px;
110
117
  padding-top: round(0.25 * $base-increment);
111
118
  padding-bottom: round(0.25 * $base-increment);
112
119
  }
113
120
  &.size--lg {
114
- font-size: round(1.1 * $base-font-size);
121
+ $font-size: round(1.1 * $base-font-size);
122
+ font-size: $font-size;
123
+ line-height: $font-size + 4px;
115
124
  padding-top: round(0.5 * $base-increment);
116
125
  padding-bottom: round(0.5 * $base-increment);
117
126
  }
118
127
  &.size--xl {
119
- font-size: round(1.3 * $base-font-size);
128
+ $font-size: round(1.3 * $base-font-size);
129
+ font-size: $font-size;
130
+ line-height: $font-size + 4px;
120
131
  padding-top: round(1 * $base-increment);
121
132
  padding-bottom: round(1 * $base-increment);
122
133
  }