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