wave-ui 1.45.15 → 1.49.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.
@@ -73,102 +74,53 @@ export default {
73
74
  menuClass: { type: [String, Object, Array] },
74
75
  titleClass: { type: [String, Object, Array] },
75
76
  contentClass: { type: [String, Object, Array] },
76
- // Position.
77
77
  arrow: { type: Boolean }, // The small triangle pointing toward the activator.
78
- detachTo: { type: [String, Boolean, Object] },
79
- fixed: { type: Boolean },
80
- top: { type: Boolean },
81
- bottom: { type: Boolean },
82
- left: { type: Boolean },
83
- right: { type: Boolean },
84
- alignTop: { type: Boolean },
85
- alignBottom: { type: Boolean },
86
- alignLeft: { type: Boolean },
87
- alignRight: { type: Boolean },
88
- zIndex: { type: [Number, String, Boolean] },
89
78
  minWidth: { type: [Number, String] }, // can be like: `40`, `5em`, `activator`.
90
79
  overlay: { type: Boolean },
91
80
  overlayClass: { type: [String, Object, Array] },
92
81
  overlayProps: { type: Object }, // Allow passing down an object of props to the w-overlay component.
93
82
  persistent: { type: Boolean },
94
- noPosition: { type: Boolean }
83
+ delay: { type: Number }
84
+ // Other props in the detachable mixin:
85
+ // detachTo, appendTo, fixed, top, bottom, left, right, alignTop, alignBottom, alignLeft,
86
+ // alignRight, noPosition, zIndex, activator.
95
87
  },
96
88
 
97
89
  emits: ['input', 'update:modelValue', 'open', 'close'],
98
90
 
99
91
  data: () => ({
100
- menuVisible: false,
92
+ detachableVisible: false,
101
93
  hoveringActivator: false,
102
94
  hoveringMenu: false,
103
95
  // The menu computed top & left coordinates.
104
- menuCoordinates: {
96
+ detachableCoords: {
105
97
  top: 0,
106
98
  left: 0
107
99
  },
108
- activatorEl: null,
109
100
  activatorWidth: 0,
110
- menuEl: null,
101
+ detachableEl: null,
111
102
  timeoutId: null
112
103
  }),
113
104
 
114
105
  computed: {
106
+ /**
107
+ * Other computed in the detachable mixin:
108
+ * - `appendToTarget`
109
+ * - `detachableParentEl`
110
+ * - `activatorEl`
111
+ * - `position`
112
+ * - `alignment`
113
+ **/
114
+
115
115
  transitionName () {
116
116
  return this.transition || 'scale-fade'
117
117
  },
118
118
 
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
- position () {
148
- return (
149
- (this.top && 'top') ||
150
- (this.bottom && 'bottom') ||
151
- (this.left && 'left') ||
152
- (this.right && 'right') ||
153
- 'bottom'
154
- )
155
- },
156
-
157
119
  menuMinWidth () {
158
120
  if (this.minWidth === 'activator') return this.activatorWidth ? `${this.activatorWidth}px` : 0
159
121
  else return isNaN(this.minWidth) ? this.minWidth : (this.minWidth ? `${this.minWidth}px` : 0)
160
122
  },
161
123
 
162
- alignment () {
163
- 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') ||
168
- ''
169
- )
170
- },
171
-
172
124
  menuClasses () {
173
125
  return objectifyClasses(this.menuClass)
174
126
  },
@@ -205,8 +157,8 @@ export default {
205
157
  styles () {
206
158
  return {
207
159
  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,
160
+ top: (this.detachableCoords.top && `${~~this.detachableCoords.top}px`) || null,
161
+ left: (this.detachableCoords.left && `${~~this.detachableCoords.left}px`) || null,
210
162
  minWidth: (this.minWidth && this.menuMinWidth) || null,
211
163
  '--w-menu-bg-color': this.arrow && this.$waveui.colors[this.bgColor || 'white']
212
164
  }
@@ -217,34 +169,44 @@ export default {
217
169
 
218
170
  if (this.showOnHover) {
219
171
  handlers = {
220
- focus: this.toggleMenu,
221
- blur: this.toggleMenu,
172
+ focus: this.toggle,
173
+ blur: this.toggle,
222
174
  mouseenter: e => {
223
175
  this.hoveringActivator = true
224
- this.openMenu(e)
176
+ this.open(e)
225
177
  },
226
178
  mouseleave: e => {
227
179
  this.hoveringActivator = false
228
180
  // Wait 10ms, the time to get the hoveringMenu updated on mouseenter on the menu.
229
181
  setTimeout(() => {
230
- if (!this.hoveringMenu) this.closeMenu()
182
+ if (!this.hoveringMenu) this.close()
231
183
  }, 10)
232
184
  }
233
185
  }
234
186
  // Check the window exists: SSR-proof.
235
187
  if (typeof window !== 'undefined' && 'ontouchstart' in window) {
236
- handlers.click = this.toggleMenu
188
+ handlers.click = this.toggle
237
189
  }
238
190
  }
239
- else handlers = { click: this.toggleMenu }
191
+ else handlers = { click: this.toggle }
240
192
  return handlers
241
193
  }
242
194
  },
243
195
 
244
196
  methods: {
197
+ /**
198
+ * Other methods in the `detachable` mixin:
199
+ * - `getActivatorCoordinates`
200
+ * - `computeDetachableCoords`
201
+ * - `onResize`
202
+ * - `onOutsideMousedown`
203
+ * - `insertInDOM`
204
+ * - `removeFromDOM`
205
+ **/
206
+
245
207
  // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
246
- toggleMenu (e) {
247
- let shouldShowMenu = this.menuVisible
208
+ toggle (e) {
209
+ let shouldShowMenu = this.detachableVisible
248
210
  if ('ontouchstart' in window && this.showOnHover && e.type === 'click') {
249
211
  shouldShowMenu = !shouldShowMenu
250
212
  }
@@ -260,26 +222,29 @@ export default {
260
222
 
261
223
  this.timeoutId = clearTimeout(this.timeoutId)
262
224
 
263
- if (shouldShowMenu) {
264
- this.$emit('update:modelValue', (this.menuVisible = true))
265
- this.$emit('input', true)
266
- this.$emit('open')
267
-
268
- this.openMenu(e)
269
- }
270
- else this.closeMenu()
225
+ if (shouldShowMenu) this.open(e)
226
+ else this.close()
271
227
  },
272
228
 
273
229
  // ! \ 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()
230
+ async open (e) {
231
+ // A tiny delay may help positioning the detachable correctly in case of multiple activators
232
+ // with different menu contents.
233
+ if (this.delay) await new Promise(resolve => setTimeout(resolve, this.delay))
234
+
235
+ this.detachableVisible = true
236
+
237
+ // If the activator is external, there might be multiple,
238
+ // so on open, the activator will be set to the event target.
239
+ if (this.activator) this.activatorEl = e.target
240
+
241
+ await this.insertInDOM()
277
242
 
278
243
  if (this.minWidth === 'activator') this.activatorWidth = this.activatorEl.offsetWidth
279
244
 
280
- if (!this.noPosition) this.computeMenuPosition(e)
245
+ if (!this.noPosition) this.computeDetachableCoords(e)
281
246
 
282
- // In `getCoordinates` accessing the menu computed styles takes a few ms (less than 10ms),
247
+ // In `getActivatorCoordinates` accessing the menu computed styles takes a few ms (less than 10ms),
283
248
  // if we don't postpone the Menu apparition it will start transition from a visible menu and
284
249
  // thus will not transition.
285
250
  this.timeoutId = setTimeout(() => {
@@ -303,197 +268,26 @@ export default {
303
268
  * But if hideOnMenuClick is also set to true, this should force close
304
269
  * even while hovering the menu.
305
270
  */
306
- async closeMenu (force = false) {
271
+ async close (force = false) {
307
272
  // Might be already closed.
308
273
  // E.g. showOnHover & hideOnMenuClick: on click, force hide then mouseleave is also firing.
309
- if (!this.menuVisible) return
274
+ if (!this.detachableVisible) return
310
275
 
311
276
  if (this.showOnHover && !force) {
312
277
  await new Promise(resolve => setTimeout(resolve, 10))
313
278
  if (this.showOnHover && (this.hoveringMenu || this.hoveringActivator)) return
314
279
  }
315
280
 
316
- this.$emit('update:modelValue', (this.menuVisible = false))
281
+ this.$emit('update:modelValue', (this.detachableVisible = false))
317
282
  this.$emit('input', false)
318
283
  this.$emit('close')
319
284
  // Remove the mousedown listener if the menu got closed without a mousedown outside of the menu.
320
285
  document.removeEventListener('mousedown', this.onOutsideMousedown)
321
286
  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
- }
465
- },
466
-
467
- mounted () {
468
- const wrapper = this.$el
469
- this.activatorEl = wrapper.firstElementChild
470
- // Unwrap the activator element.
471
- wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
472
-
473
- // Unwrap the overlay.
474
- if (this.overlay) {
475
- this.overlayEl = this.$refs.overlay?.$el
476
- wrapper.parentNode.insertBefore(this.overlayEl, wrapper)
477
- }
478
-
479
- if (this.value) this.toggleMenu({ type: 'click', target: this.activatorEl })
480
- },
481
-
482
- beforeDestroy () {
483
- this.removeMenu()
484
- if (this.overlay && this.overlayEl.parentNode) this.overlayEl.remove()
485
- if (this.activatorEl && this.activatorEl.parentNode) this.activatorEl.remove()
486
- },
487
-
488
- watch: {
489
- value (bool) {
490
- if (!!bool !== this.menuVisible) this.toggleMenu({ type: 'click', target: this.activatorEl })
491
- },
492
- detachTo () {
493
- this.removeMenu()
494
- this.insertMenu()
495
287
  }
496
288
  }
289
+
290
+ // watch, mounted & beforeDestroy hooks are set in the detachable.js mixin.
497
291
  }
498
292
  </script>
499
293
 
@@ -11,8 +11,8 @@ transition-group(
11
11
  :key="notif._uid"
12
12
  v-model="notif._value"
13
13
  @close="notifManager.dismiss(notif._uid)"
14
- v-bind="notif")
15
- | {{ notif.message }}
14
+ v-bind="notifProps(notif)")
15
+ div(v-html="notif.message")
16
16
  </template>
17
17
 
18
18
  <script>
@@ -42,6 +42,13 @@ export default {
42
42
  }
43
43
  },
44
44
 
45
+ methods: {
46
+ notifProps (notif) {
47
+ const { _value, _uid, message, timeout, ...props } = notif
48
+ return props
49
+ }
50
+ },
51
+
45
52
  created () {
46
53
  this.notifManager = new NotificationManager()
47
54
  },
@@ -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
  }