wave-ui 1.45.12 → 1.46.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.
@@ -1,8 +1,8 @@
1
1
  <template lang="pug">
2
- .w-tooltip-wrap(:class="{ 'w-tooltip-wrap--attached': !detachTo }")
2
+ .w-tooltip-wrap
3
3
  slot(name="activator" :on="eventHandlers")
4
4
  transition(:name="transitionName" appear)
5
- .w-tooltip(ref="tooltip" v-show="showTooltip" :class="classes" :style="styles")
5
+ .w-tooltip(v-if="detachableVisible" ref="detachable" :class="classes" :style="styles")
6
6
  slot
7
7
  </template>
8
8
 
@@ -17,12 +17,13 @@
17
17
  */
18
18
 
19
19
  import { objectifyClasses } from '../utils/index'
20
- import { consoleWarn } from '../utils/console'
20
+ import DetachableMixin from '../mixins/detachable'
21
21
 
22
- const marginFromWindowSide = 4 // Amount of px from a window side, instead of overflowing.
22
+ // const marginFromWindowSide = 4 // Amount of px from a window side, instead of overflowing.
23
23
 
24
24
  export default {
25
25
  name: 'w-tooltip',
26
+ mixins: [DetachableMixin],
26
27
 
27
28
  props: {
28
29
  value: {},
@@ -36,32 +37,44 @@ export default {
36
37
  transition: { type: String },
37
38
  tooltipClass: { type: [String, Object, Array] },
38
39
  // Position.
39
- detachTo: {},
40
+ detachTo: { type: [String, Boolean, Object], deprecated: true },
41
+ appendTo: { type: [String, Boolean, Object] },
40
42
  fixed: { type: Boolean },
41
43
  top: { type: Boolean },
42
44
  bottom: { type: Boolean },
43
45
  left: { type: Boolean },
44
46
  right: { type: Boolean },
45
- zIndex: { type: [Number, String, Boolean] }
47
+ alignTop: { type: Boolean },
48
+ alignBottom: { type: Boolean },
49
+ alignLeft: { type: Boolean },
50
+ alignRight: { type: Boolean },
51
+ zIndex: { type: [Number, String, Boolean] },
52
+ persistent: { type: Boolean },
53
+ noPosition: { type: Boolean }
46
54
  },
47
55
 
48
56
  emits: ['input', 'update:modelValue', 'open', 'close'],
49
57
 
50
58
  data: () => ({
51
- showTooltip: false,
59
+ detachableVisible: false,
60
+ hoveringActivator: false,
52
61
  // The activator coordinates.
53
- coordinates: {
62
+ detachableCoords: {
54
63
  top: 0,
55
- left: 0,
56
- width: 0,
57
- height: 0
64
+ left: 0
58
65
  },
59
66
  activatorEl: null,
60
- tooltipEl: null,
67
+ detachableEl: null,
61
68
  timeoutId: null
62
69
  }),
63
70
 
64
71
  computed: {
72
+ /**
73
+ * Other computed in the detachable mixin:
74
+ * - `appendToTarget`
75
+ * - `detachableParentEl`
76
+ **/
77
+
65
78
  tooltipClasses () {
66
79
  return objectifyClasses(this.tooltipClass)
67
80
  },
@@ -71,29 +84,6 @@ export default {
71
84
  return this.transition || `w-tooltip-slide-fade-${direction}`
72
85
  },
73
86
 
74
- detachToTarget () {
75
- let target = this.detachTo || '.w-app'
76
- if (target === true) target = '.w-app'
77
- else if (target && !['object', 'string'].includes(typeof target)) target = '.w-app'
78
- else if (typeof target === 'object' && !target.nodeType) {
79
- target = '.w-app'
80
- consoleWarn('Invalid node provided in w-tooltip `attach-to`. Falling back to .w-app.', this)
81
- }
82
- if (typeof target === 'string') target = document.querySelector(target)
83
-
84
- if (!target) {
85
- consoleWarn(`Unable to locate ${this.detachTo ? `target ${this.detachTo}` : '.w-app'}`, this)
86
- target = document.querySelector('.w-app')
87
- }
88
-
89
- return target
90
- },
91
-
92
- // DOM element that will receive the tooltip.
93
- tooltipParentEl () {
94
- return this.detachTo ? this.detachToTarget : this.$el
95
- },
96
-
97
87
  position () {
98
88
  return (
99
89
  (this.top && 'top') ||
@@ -104,34 +94,14 @@ export default {
104
94
  )
105
95
  },
106
96
 
107
- tooltipCoordinates () {
108
- const coords = {}
109
- const { top, left, width, height } = this.coordinates
110
-
111
- switch (this.position) {
112
- case 'top': {
113
- coords.top = top
114
- coords.left = left + width / 2 // left: 50%.
115
- break
116
- }
117
- case 'bottom': {
118
- coords.top = top + height
119
- coords.left = left + width / 2 // left: 50%.
120
- break
121
- }
122
- case 'left': {
123
- coords.top = top + height / 2 // top: 50%.
124
- coords.left = left
125
- break
126
- }
127
- case 'right': {
128
- coords.top = top + height / 2 // top: 50%.
129
- coords.left = left + width
130
- break
131
- }
132
- }
133
-
134
- return coords
97
+ alignment () {
98
+ return (
99
+ (['top', 'bottom'].includes(this.position) && this.alignLeft && 'left') ||
100
+ (['top', 'bottom'].includes(this.position) && this.alignRight && 'right') ||
101
+ (['left', 'right'].includes(this.position) && this.alignTop && 'top') ||
102
+ (['left', 'right'].includes(this.position) && this.alignBottom && 'bottom') ||
103
+ ''
104
+ )
135
105
  },
136
106
 
137
107
  classes () {
@@ -139,12 +109,12 @@ export default {
139
109
  [this.color]: this.color,
140
110
  [`${this.bgColor}--bg`]: this.bgColor,
141
111
  ...this.tooltipClasses,
142
- [`w-tooltip--${this.position}`]: true,
112
+ [`w-tooltip--${this.position}`]: !this.noPosition,
113
+ [`w-tooltip--align-${this.alignment}`]: !this.noPosition && this.alignment,
143
114
  'w-tooltip--tile': this.tile,
144
115
  'w-tooltip--round': this.round,
145
116
  'w-tooltip--shadow': this.shadow,
146
117
  'w-tooltip--fixed': this.fixed,
147
- 'w-tooltip--active': this.showTooltip,
148
118
  'w-tooltip--no-border': this.noBorder || this.bgColor,
149
119
  'w-tooltip--custom-transition': this.transition
150
120
  }
@@ -154,8 +124,8 @@ export default {
154
124
  styles () {
155
125
  return {
156
126
  zIndex: this.zIndex || this.zIndex === 0 || null,
157
- top: (this.tooltipCoordinates.top && `${~~this.tooltipCoordinates.top}px`) || null,
158
- left: (this.tooltipCoordinates.left && `${~~this.tooltipCoordinates.left}px`) || null,
127
+ top: (this.detachableCoords.top && `${~~this.detachableCoords.top}px`) || null,
128
+ left: (this.detachableCoords.left && `${~~this.detachableCoords.left}px`) || null,
159
129
  '--w-tooltip-bg-color': this.$waveui.colors[this.bgColor || 'white']
160
130
  }
161
131
  },
@@ -167,20 +137,38 @@ export default {
167
137
  handlers = {
168
138
  focus: this.toggle,
169
139
  blur: this.toggle,
170
- mouseenter: this.toggle,
171
- mouseleave: this.toggle
140
+ mouseenter: e => {
141
+ this.hoveringActivator = true
142
+ this.open(e)
143
+ },
144
+ mouseleave: e => {
145
+ this.hoveringActivator = false
146
+ this.close()
147
+ }
172
148
  }
173
149
 
174
- if ('ontouchstart' in window) handlers.click = this.toggle
150
+ // Check the window exists: SSR-proof.
151
+ if (typeof window !== 'undefined' && 'ontouchstart' in window) handlers.click = this.toggle
175
152
  }
176
153
  return handlers
177
154
  }
178
155
  },
179
156
 
180
157
  methods: {
158
+ /**
159
+ * Other methods in the `detachable` mixin:
160
+ * - `getActivatorCoordinates`
161
+ * - `computeDetachableCoords`
162
+ * - `onResize`
163
+ * - `onOutsideMousedown`
164
+ * - `insertInDOM`
165
+ * - `removeFromDOM`
166
+ **/
167
+
168
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
181
169
  toggle (e) {
182
- let shouldShowTooltip = this.showTooltip
183
- if ('ontouchstart' in window) {
170
+ let shouldShowTooltip = this.detachableVisible
171
+ if (typeof window !== 'undefined' && 'ontouchstart' in window) {
184
172
  if (e.type === 'click') shouldShowTooltip = !shouldShowTooltip
185
173
  }
186
174
  else if (e.type === 'click' && this.showOnClick) shouldShowTooltip = !shouldShowTooltip
@@ -189,130 +177,101 @@ export default {
189
177
 
190
178
  this.timeoutId = clearTimeout(this.timeoutId)
191
179
  if (shouldShowTooltip) {
192
- this.coordinates = this.getCoordinates(e)
193
- // In `getCoordinates` accessing the tooltip computed styles takes a few ms (less than 10ms),
194
- // if we don't postpone the tooltip apparition it will start transition from a visible tooltip and
195
- // thus will not transition.
196
- this.timeoutId = setTimeout(() => {
197
- this.showTooltip = true
198
- this.$emit('update:modelValue', true)
199
- this.$emit('input', true)
200
- this.$emit('open')
201
- }, 10)
202
- }
203
- else {
204
- this.showTooltip = false
205
- this.$emit('update:modelValue', false)
206
- this.$emit('input', false)
207
- this.$emit('close')
180
+ this.$emit('update:modelValue', (this.detachableVisible = true))
181
+ this.$emit('input', true)
182
+ this.$emit('open')
183
+
184
+ this.open(e)
208
185
  }
186
+ else this.close()
209
187
  },
210
188
 
211
- getCoordinates () {
212
- const { top, left, width, height } = this.activatorEl.getBoundingClientRect()
213
- let coords = { top, left, width, height }
189
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
190
+ async open (e) {
191
+ this.detachableVisible = true
192
+ await this.insertInDOM()
214
193
 
215
- if (!this.fixed) {
216
- const { top: targetTop, left: targetLeft } = this.tooltipParentEl.getBoundingClientRect()
217
- coords = { ...coords, top: top - targetTop, left: left - targetLeft }
218
- }
194
+ if (this.minWidth === 'activator') this.activatorWidth = this.activatorEl.offsetWidth
219
195
 
220
- const tooltipEl = this.$refs.tooltip
196
+ if (!this.noPosition) this.computeDetachableCoords(e)
221
197
 
222
- // 1. First display the tooltip but hide it (So we can get its dimension).
223
- tooltipEl.style.visibility = 'hidden'
224
- tooltipEl.style.display = 'table'
225
- const computedStyles = window.getComputedStyle(tooltipEl, null)
198
+ // In `getActivatorCoordinates` accessing the tooltip computed styles takes a few ms (less than 10ms),
199
+ // if we don't postpone the Tooltip apparition it will start transition from a visible tooltip and
200
+ // thus will not transition.
201
+ this.timeoutId = setTimeout(() => {
202
+ this.$emit('update:modelValue', true)
203
+ this.$emit('input', true)
204
+ this.$emit('open')
205
+ }, 0)
226
206
 
227
- // Keep fully in viewport.
228
- // --------------------------------------------------
229
- if (this.position === 'top' && ((top - tooltipEl.offsetHeight) < 0)) {
230
- const margin = -parseInt(computedStyles.getPropertyValue('margin-top'))
231
- coords.top -= top - tooltipEl.offsetHeight - margin - marginFromWindowSide
232
- }
233
- else if (this.position === 'left' && left - tooltipEl.offsetWidth < 0) {
234
- const margin = -parseInt(computedStyles.getPropertyValue('margin-left'))
235
- coords.left -= left - tooltipEl.offsetWidth - margin - marginFromWindowSide
236
- }
237
- else if (this.position === 'right' && left + width + tooltipEl.offsetWidth > window.innerWidth) {
238
- const margin = parseInt(computedStyles.getPropertyValue('margin-left'))
239
- coords.left -= left + width + tooltipEl.offsetWidth - window.innerWidth + margin + marginFromWindowSide
240
- }
241
- else if (this.position === 'bottom' && top + height + tooltipEl.offsetHeight > window.innerHeight) {
242
- const margin = parseInt(computedStyles.getPropertyValue('margin-top'))
243
- coords.top -= top + height + tooltipEl.offsetHeight - window.innerHeight + margin + marginFromWindowSide
244
- }
245
- // --------------------------------------------------
246
-
247
- // 2. Update left & top if there is a custom transition.
248
- // Tooltip position relies on transform translate, the custom animation may override the transform
249
- // property so do without it and subtract half width or height manually.
250
- if (this.transition) {
251
- // If tooltip is on top or bottom.
252
- if (['top', 'bottom'].includes(this.position)) coords.left -= tooltipEl.offsetWidth / 2
253
- // If tooltip is on left or right.
254
- if (['left', 'right'].includes(this.position)) coords.top -= tooltipEl.offsetHeight / 2
255
-
256
- if (this.position === 'left') coords.left -= tooltipEl.offsetWidth
257
- if (this.position === 'top') coords.top -= tooltipEl.offsetHeight
258
- }
259
-
260
- // 3. Hide the tooltip again so the transition happens correctly.
261
- tooltipEl.style.visibility = null
262
- tooltipEl.style.display = 'none'
263
-
264
- return coords
207
+ if (!this.persistent) document.addEventListener('mousedown', this.onOutsideMousedown)
208
+ if (!this.noPosition) window.addEventListener('resize', this.onResize)
265
209
  },
266
210
 
267
- insertTooltip () {
268
- const wrapper = this.$el
269
- this.tooltipEl = this.$refs.tooltip.$el || this.$refs.tooltip
270
-
271
- // Unwrap the activator element.
272
- wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
273
-
274
- // Move the tooltip elsewhere in the DOM.
275
- // wrapper.parentNode.insertBefore(this.$refs.tooltip, wrapper)
276
- this.detachToTarget.appendChild(this.$refs.tooltip)
277
- },
211
+ /**
212
+ * Closes the tooltip. Can happen on:
213
+ * - click of activator
214
+ * - hover outside if showOnHover
215
+ * - click inside tooltip if hideOnTooltipClick.
216
+ * / ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
217
+ *
218
+ * @param {Boolean} force when showOnHover is set to true, hovering tooltip should keep it open.
219
+ * But if hideOnTooltipClick is also set to true, this should force close
220
+ * even while hovering the tooltip.
221
+ */
222
+ async close (force = false) {
223
+ // Might be already closed.
224
+ // E.g. showOnHover & hideOnTooltipClick: on click, force hide then mouseleave is also firing.
225
+ if (!this.detachableVisible) return
226
+
227
+ if (this.showOnHover && !force) {
228
+ await new Promise(resolve => setTimeout(resolve, 10))
229
+ if (this.showOnHover && this.hoveringActivator) return
230
+ }
278
231
 
279
- removeTooltip () {
280
- if (this.tooltipEl && this.tooltipEl.parentNode) this.tooltipEl.remove()
232
+ this.$emit('update:modelValue', (this.detachableVisible = false))
233
+ this.$emit('input', false)
234
+ this.$emit('close')
235
+ // Remove the mousedown listener if the tooltip got closed without a mousedown outside of the tooltip.
236
+ document.removeEventListener('mousedown', this.onOutsideMousedown)
237
+ window.removeEventListener('resize', this.onResize)
281
238
  }
282
-
283
239
  },
284
240
 
285
241
  mounted () {
286
- this.activatorEl = this.$el.firstElementChild
287
- if (this.detachTo) this.insertTooltip()
242
+ const wrapper = this.$el
243
+ this.activatorEl = wrapper.firstElementChild
244
+
245
+ // Unwrap the activator element.
246
+ wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
288
247
 
289
248
  if (this.value) this.toggle({ type: 'click', target: this.activatorEl })
290
249
  },
291
250
 
292
251
  beforeDestroy () {
293
- this.removeTooltip()
252
+ this.removeFromDOM()
294
253
 
295
254
  if (this.activatorEl && this.activatorEl.parentNode) this.activatorEl.remove()
296
255
  },
297
256
 
298
257
  watch: {
299
258
  value (bool) {
300
- if (bool !== this.showTooltip) this.toggle({ type: 'click', target: this.activatorEl })
259
+ if (bool !== this.detachableVisible) this.toggle({ type: 'click', target: this.activatorEl })
301
260
  },
302
261
  detachTo () {
303
- this.removeTooltip()
304
- this.insertTooltip()
262
+ this.removeFromDOM()
263
+ this.insertInDOM()
264
+ },
265
+ appendTo () {
266
+ this.removeFromDOM()
267
+ this.insertInDOM()
305
268
  }
306
269
  }
307
270
  }
308
271
  </script>
309
272
 
310
273
  <style lang="scss">
311
- .w-tooltip-wrap {
312
- display: none;
313
-
314
- &--attached {display: inline-block;position: relative;}
315
- }
274
+ .w-tooltip-wrap {display: none;}
316
275
 
317
276
  .w-tooltip {
318
277
  // Fix Safari where `width: max-content` does not take padding and border into consideration.
@@ -340,115 +299,22 @@ export default {
340
299
  &--shadow {box-shadow: $box-shadow;}
341
300
  &--no-border {border: none;}
342
301
 
343
- &--top {
344
- transform: translate(-50%, -100%);
345
- margin-top: -3 * $base-increment;
346
- }
347
- &--bottom {
348
- transform: translateX(-50%);
349
- margin-top: 3 * $base-increment;
350
- }
351
- &--left {
352
- transform: translate(-100%, -50%);
353
- margin-left: -3 * $base-increment;
354
- }
355
- &--right {
356
- transform: translateY(-50%);
357
- margin-left: 3 * $base-increment;
358
- }
302
+ &--top {margin-top: -3 * $base-increment;}
303
+ &--bottom {margin-top: 3 * $base-increment;}
304
+ &--left {margin-left: -3 * $base-increment;}
305
+ &--right {margin-left: 3 * $base-increment;}
359
306
 
360
307
  &--custom-transition {transform: none;}
361
308
 
362
- // &:after {
363
- // content: '';
364
- // position: absolute;
365
- // width: 0;
366
- // height: 0;
367
- // border: 6px solid transparent;
368
- // }
369
- // &--top:after {
370
- // top: 100%;
371
- // left: 50%;
372
- // border-top-color: $tooltip-bg-color;
373
- // transform: translateX(-50%);
374
- // margin-top: 1px;
375
- // }
376
- // &--bottom:after {
377
- // bottom: 100%;
378
- // left: 50%;
379
- // border-bottom-color: $tooltip-bg-color;
380
- // transform: translateX(-50%);
381
- // margin-bottom: 1px;
382
- // }
383
- // &--left:after {
384
- // left: 100%;
385
- // top: 50%;
386
- // border-left-color: $tooltip-bg-color;
387
- // transform: translateY(-50%);
388
- // margin-left: 1px;
389
- // }
390
- // &--right:after {
391
- // right: 100%;
392
- // top: 50%;
393
- // border-right-color: $tooltip-bg-color;
394
- // transform: translateY(-50%);
395
- // margin-right: 1px;
396
- // }
397
-
398
309
  // Tooltip without border.
399
- // --------------------------------------------------------
400
310
  &--no-border {
401
311
  @include triangle(var(--w-tooltip-bg-color), '.w-tooltip', 7px, 0);
402
312
  }
313
+
314
+ // Tooltip with border.
403
315
  &:not(&--no-border) {
404
316
  @include triangle(var(--w-tooltip-bg-color), '.w-tooltip', 7px);
405
317
  }
406
-
407
- // Tooltip with border.
408
- // --------------------------------------------------------
409
- // &:not(&--no-border).w-tooltip--top:after {margin-top: -1px;}
410
- // &:not(&--no-border).w-tooltip--bottom:after {margin-bottom: -1px;}
411
- // &:not(&--no-border).w-tooltip--left:after {margin-left: -1px;}
412
- // &:not(&--no-border).w-tooltip--right:after {margin-right: -1px;}
413
-
414
- // &:not(&--no-border) {
415
- // &:before {
416
- // content: '';
417
- // position: absolute;
418
- // width: 0;
419
- // height: 0;
420
- // border: 7px solid transparent;
421
- // }
422
- // &.w-tooltip--top:before {
423
- // top: 100%;
424
- // left: 50%;
425
- // border-top-color: inherit;
426
- // transform: translateX(-50%);
427
- // margin-top: 0;
428
- // }
429
- // &.w-tooltip--bottom:before {
430
- // bottom: 100%;
431
- // left: 50%;
432
- // border-bottom-color: inherit;
433
- // transform: translateX(-50%);
434
- // margin-bottom: 0;
435
- // }
436
- // &.w-tooltip--left:before {
437
- // left: 100%;
438
- // top: 50%;
439
- // border-left-color: inherit;
440
- // transform: translateY(-50%);
441
- // margin-left: 0;
442
- // }
443
- // &.w-tooltip--right:before {
444
- // right: 100%;
445
- // top: 50%;
446
- // border-right-color: inherit;
447
- // transform: translateY(-50%);
448
- // margin-right: 0;
449
- // }
450
- // }
451
- // --------------------------------------------------------
452
318
  }
453
319
 
454
320
  // Transitions.