wave-ui 2.29.0 → 2.31.2

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.
@@ -0,0 +1,321 @@
1
+ /**
2
+ * A detachable element is an element that can be appended to another DOM node
3
+ * (but keeping data-driven Vue DOM refreshes).
4
+ * This mixin is used by w-tooltip & w-menu.
5
+ */
6
+
7
+ import { consoleWarn } from '../utils/console'
8
+
9
+ export default {
10
+ props: {
11
+ // Position.
12
+ detachTo: { type: [String, Boolean, Object], deprecated: true },
13
+ appendTo: { type: [String, Boolean, Object] },
14
+ fixed: { type: Boolean },
15
+ top: { type: Boolean },
16
+ bottom: { type: Boolean },
17
+ left: { type: Boolean },
18
+ right: { type: Boolean },
19
+ alignTop: { type: Boolean },
20
+ alignBottom: { type: Boolean },
21
+ alignLeft: { type: Boolean },
22
+ alignRight: { type: Boolean },
23
+ noPosition: { type: Boolean },
24
+ zIndex: { type: [Number, String, Boolean] },
25
+ activator: { type: String } // Optionally designate an external activator.
26
+ },
27
+
28
+ data: () => ({
29
+ // The event listeners handlers have to be removed the exact same way they have been attached.
30
+ // Since the handler functions have variables that change after hot-reload, keep them exactly
31
+ // as is in an array so we can delete them on destroy.
32
+ // This only applies to the activatorEventHandlers, the other events listeners can be removed
33
+ // normally.
34
+ docAEventListenersHandlers: []
35
+ }),
36
+
37
+ computed: {
38
+ // DOM element to attach tooltip/menu to.
39
+ // ! \ This computed uses the DOM - NO SSR (only trigger from beforeMount and later).
40
+ appendToTarget () {
41
+ const defaultTarget = '.w-app'
42
+
43
+ // Convert deprecated prop to renamed one.
44
+ if (this.detachTo && !this.appendTo) {
45
+ consoleWarn(`The ${this.$options.name} prop \`detach-to\` is deprecated. You can replace it with \`append-to\`.`, this)
46
+ }
47
+
48
+ let target = this.appendTo || this.detachTo || defaultTarget
49
+ if (target === true) target = defaultTarget
50
+ else if (this.appendTo === 'activator') target = this.$el.previousElementSibling
51
+ else if (target && !['object', 'string'].includes(typeof target)) target = defaultTarget
52
+ else if (typeof target === 'object' && !target.nodeType) {
53
+ target = defaultTarget
54
+ consoleWarn(`Invalid node provided in ${this.$options.name} \`append-to\`. Falling back to .w-app.`, this)
55
+ }
56
+ if (typeof target === 'string') target = document.querySelector(target)
57
+
58
+ if (!target) {
59
+ consoleWarn(`Unable to locate ${this.appendTo ? `target ${this.appendTo}` : defaultTarget}`, this)
60
+ target = document.querySelector(defaultTarget)
61
+ }
62
+
63
+ return target
64
+ },
65
+
66
+ // DOM element that will receive the tooltip/menu.
67
+ // ! \ This computed uses the DOM - NO SSR (only trigger from beforeMount and later).
68
+ detachableParentEl () {
69
+ return this.appendToTarget
70
+ },
71
+
72
+ hasSeparateActivator () {
73
+ return !this.$slots.activator && typeof this.activator === 'string'
74
+ },
75
+
76
+ activatorEl: {
77
+ get () {
78
+ if (this.hasSeparateActivator) return document.querySelector(this.activator)
79
+ return this.$el.firstElementChild
80
+ },
81
+ set () {
82
+
83
+ }
84
+ },
85
+
86
+ position () {
87
+ return (
88
+ (this.top && 'top') ||
89
+ (this.bottom && 'bottom') ||
90
+ (this.left && 'left') ||
91
+ (this.right && 'right') ||
92
+ 'bottom'
93
+ )
94
+ },
95
+
96
+ alignment () {
97
+ return (
98
+ (['top', 'bottom'].includes(this.position) && this.alignLeft && 'left') ||
99
+ (['top', 'bottom'].includes(this.position) && this.alignRight && 'right') ||
100
+ (['left', 'right'].includes(this.position) && this.alignTop && 'top') ||
101
+ (['left', 'right'].includes(this.position) && this.alignBottom && 'bottom') ||
102
+ ''
103
+ )
104
+ }
105
+ },
106
+
107
+ methods: {
108
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
109
+ getActivatorCoordinates (e) {
110
+ // Get the activator coordinates relative to window.
111
+ const { top, left, width, height } = (e ? e.target : this.activatorEl).getBoundingClientRect()
112
+ let coords = { top, left, width, height }
113
+
114
+ // If absolute position, adjust top & left.
115
+ if (!this.fixed) {
116
+ const { top: targetTop, left: targetLeft } = this.detachableParentEl.getBoundingClientRect()
117
+ const computedStyles = window.getComputedStyle(this.detachableParentEl, null)
118
+ coords = {
119
+ ...coords,
120
+ top: top - targetTop + this.detachableParentEl.scrollTop - parseInt(computedStyles.getPropertyValue('border-top-width')),
121
+ left: left - targetLeft + this.detachableParentEl.scrollLeft - parseInt(computedStyles.getPropertyValue('border-left-width'))
122
+ }
123
+ }
124
+
125
+ return coords
126
+ },
127
+
128
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
129
+ computeDetachableCoords (e) {
130
+ // Get the activator coordinates.
131
+ let { top, left, width, height } = this.getActivatorCoordinates(e)
132
+
133
+ // 1. First display the menu but hide it (So we can get its dimension).
134
+ // --------------------------------------------------
135
+ this.detachableEl.style.visibility = 'hidden'
136
+ this.detachableEl.style.display = 'flex'
137
+ const computedStyles = window.getComputedStyle(this.detachableEl, null)
138
+
139
+ // 2. Position the menu top, left, right, bottom and apply chosen alignment.
140
+ // --------------------------------------------------
141
+ // Subtract half or full activator width or height and menu width or height according to the
142
+ // menu alignment.
143
+ // Note: the menu position relies on transform translate, the custom animation may override the
144
+ // css transform property so do without it i.e. no translateX(-50%), and recalculate top & left
145
+ // manually.
146
+ switch (this.position) {
147
+ case 'top': {
148
+ top -= this.detachableEl.offsetHeight
149
+ if (this.alignRight) {
150
+ // left: 100% of activator.
151
+ left += width - this.detachableEl.offsetWidth +
152
+ parseInt(computedStyles.getPropertyValue('border-right-width'))
153
+ }
154
+ else if (!this.alignLeft) left += (width - this.detachableEl.offsetWidth) / 2 // left: 50% of activator - half menu width.
155
+ break
156
+ }
157
+ case 'bottom': {
158
+ top += height
159
+ if (this.alignRight) {
160
+ // left: 100% of activator.
161
+ left += width - this.detachableEl.offsetWidth +
162
+ parseInt(computedStyles.getPropertyValue('border-right-width'))
163
+ }
164
+ else if (!this.alignLeft) left += (width - this.detachableEl.offsetWidth) / 2 // left: 50% of activator - half menu width.
165
+ break
166
+ }
167
+ case 'left': {
168
+ left -= this.detachableEl.offsetWidth
169
+ if (this.alignBottom) top += height - this.detachableEl.offsetHeight
170
+ else if (!this.alignTop) top += (height - this.detachableEl.offsetHeight) / 2 // top: 50% of activator - half menu height.
171
+ break
172
+ }
173
+ case 'right': {
174
+ left += width
175
+ if (this.alignBottom) {
176
+ top += height - this.detachableEl.offsetHeight +
177
+ parseInt(computedStyles.getPropertyValue('margin-top'))
178
+ }
179
+ else if (!this.alignTop) {
180
+ top += (height - this.detachableEl.offsetHeight) / 2 + // top: 50% of activator - half menu height.
181
+ parseInt(computedStyles.getPropertyValue('margin-top'))
182
+ }
183
+ break
184
+ }
185
+ }
186
+
187
+ // 3. Keep fully in viewport.
188
+ // @todo: do this.
189
+ // --------------------------------------------------
190
+ // if (this.position === 'top' && ((top - this.detachableEl.offsetHeight) < 0)) {
191
+ // const margin = - parseInt(computedStyles.getPropertyValue('margin-top'))
192
+ // top -= top - this.detachableEl.offsetHeight - margin - marginFromWindowSide
193
+ // }
194
+ // else if (this.position === 'left' && left - this.detachableEl.offsetWidth < 0) {
195
+ // const margin = - parseInt(computedStyles.getPropertyValue('margin-left'))
196
+ // left -= left - this.detachableEl.offsetWidth - margin - marginFromWindowSide
197
+ // }
198
+ // else if (this.position === 'right' && left + width + this.detachableEl.offsetWidth > window.innerWidth) {
199
+ // const margin = parseInt(computedStyles.getPropertyValue('margin-left'))
200
+ // left -= left + width + this.detachableEl.offsetWidth - window.innerWidth + margin + marginFromWindowSide
201
+ // }
202
+ // else if (this.position === 'bottom' && top + height + this.detachableEl.offsetHeight > window.innerHeight) {
203
+ // const margin = parseInt(computedStyles.getPropertyValue('margin-top'))
204
+ // top -= top + height + this.detachableEl.offsetHeight - window.innerHeight + margin + marginFromWindowSide
205
+ // }
206
+
207
+ // 4. Hide the menu again so the transition happens correctly.
208
+ // --------------------------------------------------
209
+ this.detachableEl.style.visibility = null
210
+
211
+ // The menu coordinates are also recalculated while resizing window with open menu: keep the menu visible.
212
+ if (!this.detachableVisible) this.detachableEl.style.display = 'none'
213
+
214
+ this.detachableCoords = { top, left }
215
+ },
216
+
217
+ onResize () {
218
+ if (this.minWidth === 'activator') this.activatorWidth = this.activatorEl.offsetWidth
219
+ this.computeDetachableCoords()
220
+ },
221
+
222
+ // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
223
+ onOutsideMousedown (e) {
224
+ if (!this.detachableEl.contains(e.target) && !this.activatorEl.contains(e.target)) {
225
+ this.$emit('update:modelValue', (this.detachableVisible = false))
226
+ this.$emit('input', false)
227
+ this.$emit('close')
228
+ document.removeEventListener('mousedown', this.onOutsideMousedown)
229
+ window.removeEventListener('resize', this.onResize)
230
+ }
231
+ },
232
+
233
+ insertInDOM () {
234
+ return new Promise(resolve => {
235
+ this.$nextTick(() => {
236
+ this.detachableEl = this.$refs.detachable?.$el || this.$refs.detachable
237
+
238
+ // Move the tooltip/menu elsewhere in the DOM.
239
+ // wrapper.parentNode.insertBefore(this.detachableEl, wrapper)
240
+ if (this.detachableEl) this.appendToTarget.appendChild(this.detachableEl)
241
+ resolve()
242
+ })
243
+ })
244
+ },
245
+
246
+ removeFromDOM () {
247
+ document.removeEventListener('mousedown', this.onOutsideMousedown)
248
+ window.removeEventListener('resize', this.onResize)
249
+ if (this.detachableEl && this.detachableEl.parentNode) {
250
+ this.detachableVisible = false
251
+ this.detachableEl.remove()
252
+ this.detachableEl = null
253
+ }
254
+ }
255
+ },
256
+
257
+ mounted () {
258
+ const wrapper = this.$el
259
+
260
+ // Unwrap the activator element if the activator is in the activator slot.
261
+ if (this.$slots.activator) wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
262
+
263
+ // If the activator is external, add event listeners to the document and check the target is
264
+ // the activator when toggling.
265
+ // This way, the activator can be a future DOM element, that is not yet in the DOM.
266
+ else if (this.activator) {
267
+ Object.entries(this.activatorEventHandlers).forEach(([eventName, handler]) => {
268
+ // Convert mouseenter to mouseover & mouseleave to mouseout because we are attaching
269
+ // event to the document, so it can accept future nodes.
270
+ eventName = eventName.replace('mouseenter', 'mouseover').replace('mouseleave', 'mouseout')
271
+ const handlerWrap = e => {
272
+ if (e.target?.matches && e.target.matches(this.activator)) handler(e)
273
+ }
274
+ document.addEventListener(eventName, handlerWrap)
275
+ // The event listeners handlers have to be removed the exact same way they have been attached.
276
+ // Since the handler functions have variables that change after hot-reload, keep them exactly
277
+ // as is in an array so we can delete them on destroy.
278
+ this.docAEventListenersHandlers.push({ eventName, handler: handlerWrap })
279
+ })
280
+ }
281
+
282
+ // Unwrap the overlay if any.
283
+ if (this.overlay) {
284
+ this.overlayEl = this.$refs.overlay?.$el
285
+ wrapper.parentNode.insertBefore(this.overlayEl, wrapper)
286
+ }
287
+
288
+ if (this.modelValue) this.toggleMenu({ type: 'click', target: this.activatorEl })
289
+ },
290
+
291
+ beforeUnmount () {
292
+ this.close()
293
+
294
+ this.removeFromDOM()
295
+
296
+ // Remove the event listeners the exact same way they have been defined.
297
+ // Fixes issues on hot-reloading.
298
+ if (this.docAEventListenersHandlers.length) {
299
+ this.docAEventListenersHandlers.forEach(({ eventName, handler }) => {
300
+ document.removeEventListener(eventName, handler)
301
+ })
302
+ }
303
+
304
+ if (this.overlay && this.overlayEl.parentNode) this.overlayEl.remove()
305
+ if (this.activatorEl?.parentNode && this.$slots.activator) this.activatorEl.remove()
306
+ },
307
+
308
+ watch: {
309
+ modelValue (bool) {
310
+ if (!!bool !== this.detachableVisible) this.toggle({ type: 'click', target: this.activatorEl })
311
+ },
312
+ detachTo () {
313
+ this.removeFromDOM()
314
+ this.insertInDOM()
315
+ },
316
+ appendTo () {
317
+ this.removeFromDOM()
318
+ this.insertInDOM()
319
+ }
320
+ }
321
+ }
@@ -6,14 +6,11 @@
6
6
  transition: $duration $delay cubic-bezier(0.18, 0.89, 0.32, 1.28);
7
7
  }
8
8
 
9
- /**
10
- * Generates a triangle arrow on the edge of an element.
11
- *
12
- * @param $color: the color to apply to the triangle.
13
- * @param $selector: the element selector that receives the modifiers (--top, --left, etc.).
14
- * @param $size: the triangle size at the base.
15
- * @param $thickness: the border thickness, 0 to remove the border.
16
- */
9
+ // Generates a triangle arrow on the edge of an element.
10
+ // @param $color: the color to apply to the triangle.
11
+ // @param $selector: the element selector that receives the modifiers (--top, --left, etc.).
12
+ // @param $size: the triangle size at the base.
13
+ // @param $thickness: the border thickness, 0 to remove the border.
17
14
  @mixin triangle($color: white, $selector: '', $size: 7px, $thickness: 1px) {
18
15
  @if ($thickness > 0) {
19
16
  // The underneath border triangle.
@@ -54,10 +51,24 @@
54
51
  margin-right: 0;
55
52
  }
56
53
 
57
- &#{$selector}--align-top:before {transform: none;top: (2 * $base-increment) - 1px;}
58
- &#{$selector}--align-bottom:before {transform: none;top: auto;bottom: (2 * $base-increment) - 1px;}
59
- &#{$selector}--align-left:before {transform: none;left: (2 * $base-increment) - 1px;}
60
- &#{$selector}--align-right:before {transform: none;left: auto;right: (2 * $base-increment) - 1px;}
54
+ &#{$selector}--align-top:before {
55
+ transform: none;
56
+ top: (2 * $base-increment) - 1px;
57
+ }
58
+ &#{$selector}--align-bottom:before {
59
+ transform: none;
60
+ top: auto;
61
+ bottom: (2 * $base-increment) - 1px;
62
+ }
63
+ &#{$selector}--align-left:before {
64
+ transform: none;
65
+ left: (2 * $base-increment) - 1px;
66
+ }
67
+ &#{$selector}--align-right:before {
68
+ transform: none;
69
+ left: auto;
70
+ right: (2 * $base-increment) - 1px;
71
+ }
61
72
  }
62
73
 
63
74
  // The colored triangle on top of `:before`.
@@ -11,7 +11,7 @@ $css-scope: '.w-app' !default; // Allows control on CSS rules priority.
11
11
  // True by default. False allows you to use an external CSS library (like Tailwind).
12
12
  $use-layout-classes: true !default;
13
13
 
14
- $base-font-size: 14px !default;
14
+ $base-font-size: 14px !default; // Must be a px unit.
15
15
  $base-increment: round(divide($base-font-size, 4)) !default;
16
16
  $layout-padding: $base-increment * 4 !default; // Applied on the .content-wrap tag.
17
17
  $border-radius: 3px !default;