0
# Directives
1
2
Vue directives for adding interactive behaviors and functionality to DOM elements without requiring component wrappers.
3
4
## Capabilities
5
6
### ClickOutside
7
8
Directive for handling clicks outside of an element, commonly used for closing dropdowns and modals.
9
10
```typescript { .api }
11
/**
12
* Detects clicks outside the element and calls handler
13
*/
14
const ClickOutside: Directive;
15
16
interface ClickOutsideBinding {
17
/** Handler function called when click occurs outside */
18
handler?: (event: Event) => void;
19
/** Additional options */
20
options?: {
21
/** Elements to exclude from outside detection */
22
exclude?: (string | Element | ComponentPublicInstance)[];
23
/** Include the element itself in outside detection */
24
closeConditional?: (event: Event) => boolean;
25
};
26
}
27
28
// Usage syntax variations
29
type ClickOutsideValue =
30
| ((event: Event) => void)
31
| {
32
handler: (event: Event) => void;
33
exclude?: (string | Element)[];
34
closeConditional?: (event: Event) => boolean;
35
};
36
```
37
38
**Usage Examples:**
39
40
```vue
41
<template>
42
<!-- Basic usage -->
43
<div v-click-outside="closeMenu" class="menu">
44
<button @click="menuOpen = !menuOpen">Toggle Menu</button>
45
<div v-show="menuOpen" class="menu-content">
46
Menu items here...
47
</div>
48
</div>
49
50
<!-- With options -->
51
<div
52
v-click-outside="{
53
handler: closeDropdown,
54
exclude: ['.ignore-outside', $refs.trigger]
55
}"
56
class="dropdown"
57
>
58
<button ref="trigger" @click="dropdownOpen = true">
59
Open Dropdown
60
</button>
61
<div v-show="dropdownOpen" class="dropdown-content">
62
<button class="ignore-outside">Won't trigger close</button>
63
</div>
64
</div>
65
66
<!-- With conditional close -->
67
<div
68
v-click-outside="{
69
handler: closeModal,
70
closeConditional: (e) => !e.target.closest('.modal-persistent')
71
}"
72
class="modal"
73
>
74
<div class="modal-content">
75
<button class="modal-persistent">Safe button</button>
76
<button @click="closeModal">Close</button>
77
</div>
78
</div>
79
</template>
80
81
<script setup>
82
const menuOpen = ref(false);
83
const dropdownOpen = ref(false);
84
85
const closeMenu = () => {
86
menuOpen.value = false;
87
};
88
89
const closeDropdown = () => {
90
dropdownOpen.value = false;
91
};
92
93
const closeModal = () => {
94
console.log('Modal closed');
95
};
96
</script>
97
```
98
99
### Intersect
100
101
Directive using Intersection Observer API to detect when elements enter or leave the viewport.
102
103
```typescript { .api }
104
/**
105
* Observes element intersection with viewport or ancestor
106
*/
107
const Intersect: Directive;
108
109
interface IntersectBinding {
110
/** Handler function called on intersection changes */
111
handler?: (
112
entries: IntersectionObserverEntry[],
113
observer: IntersectionObserver,
114
isIntersecting: boolean
115
) => void;
116
/** Intersection observer options */
117
options?: IntersectionObserverInit & {
118
/** Only trigger once when element enters */
119
once?: boolean;
120
/** Quiet mode - don't trigger on initial observation */
121
quiet?: boolean;
122
};
123
}
124
125
interface IntersectionObserverInit {
126
/** Element to use as viewport */
127
root?: Element | null;
128
/** Margin around root */
129
rootMargin?: string;
130
/** Threshold percentages for triggering */
131
threshold?: number | number[];
132
}
133
134
// Usage syntax variations
135
type IntersectValue =
136
| ((entries: IntersectionObserverEntry[], observer: IntersectionObserver, isIntersecting: boolean) => void)
137
| {
138
handler: (entries: IntersectionObserverEntry[], observer: IntersectionObserver, isIntersecting: boolean) => void;
139
options?: IntersectionObserverInit & { once?: boolean; quiet?: boolean };
140
};
141
```
142
143
**Usage Examples:**
144
145
```vue
146
<template>
147
<!-- Basic intersection detection -->
148
<div v-intersect="onIntersect" class="observe-me">
149
This element is being watched
150
</div>
151
152
<!-- Lazy loading images -->
153
<img
154
v-for="image in images"
155
:key="image.id"
156
v-intersect="{
157
handler: loadImage,
158
options: { once: true, threshold: 0.1 }
159
}"
160
:data-src="image.src"
161
:alt="image.alt"
162
class="lazy-image"
163
/>
164
165
<!-- Infinite scroll trigger -->
166
<div class="content-list">
167
<div v-for="item in items" :key="item.id">
168
{{ item.title }}
169
</div>
170
<div
171
v-intersect="{
172
handler: loadMore,
173
options: { rootMargin: '100px' }
174
}"
175
class="scroll-trigger"
176
>
177
Loading more...
178
</div>
179
</div>
180
181
<!-- Animation on scroll -->
182
<div
183
v-intersect="{
184
handler: animateElement,
185
options: { threshold: 0.5, once: true }
186
}"
187
class="animate-on-scroll"
188
:class="{ 'is-visible': elementVisible }"
189
>
190
Animate when 50% visible
191
</div>
192
</template>
193
194
<script setup>
195
const images = ref([]);
196
const items = ref([]);
197
const elementVisible = ref(false);
198
199
const onIntersect = (entries, observer, isIntersecting) => {
200
console.log('Element intersecting:', isIntersecting);
201
};
202
203
const loadImage = (entries, observer, isIntersecting) => {
204
if (isIntersecting) {
205
const img = entries[0].target;
206
img.src = img.dataset.src;
207
img.classList.add('loaded');
208
}
209
};
210
211
const loadMore = async (entries, observer, isIntersecting) => {
212
if (isIntersecting) {
213
const newItems = await fetchMoreItems();
214
items.value.push(...newItems);
215
}
216
};
217
218
const animateElement = (entries, observer, isIntersecting) => {
219
elementVisible.value = isIntersecting;
220
};
221
</script>
222
223
<style>
224
.lazy-image {
225
opacity: 0;
226
transition: opacity 0.3s;
227
}
228
229
.lazy-image.loaded {
230
opacity: 1;
231
}
232
233
.animate-on-scroll {
234
transform: translateY(50px);
235
opacity: 0;
236
transition: all 0.6s ease;
237
}
238
239
.animate-on-scroll.is-visible {
240
transform: translateY(0);
241
opacity: 1;
242
}
243
</style>
244
```
245
246
### Mutate
247
248
Directive using MutationObserver to watch for DOM changes within an element.
249
250
```typescript { .api }
251
/**
252
* Observes DOM mutations within element
253
*/
254
const Mutate: Directive;
255
256
interface MutateBinding {
257
/** Handler function called when mutations occur */
258
handler?: (mutations: MutationRecord[], observer: MutationObserver) => void;
259
/** Mutation observer configuration */
260
options?: MutationObserverInit;
261
}
262
263
interface MutationObserverInit {
264
/** Watch for child node changes */
265
childList?: boolean;
266
/** Watch for attribute changes */
267
attributes?: boolean;
268
/** Watch for character data changes */
269
characterData?: boolean;
270
/** Watch changes in descendant nodes */
271
subtree?: boolean;
272
/** Report previous attribute values */
273
attributeOldValue?: boolean;
274
/** Report previous character data */
275
characterDataOldValue?: boolean;
276
/** Filter specific attributes to watch */
277
attributeFilter?: string[];
278
}
279
280
// Usage syntax variations
281
type MutateValue =
282
| ((mutations: MutationRecord[], observer: MutationObserver) => void)
283
| {
284
handler: (mutations: MutationRecord[], observer: MutationObserver) => void;
285
options?: MutationObserverInit;
286
};
287
```
288
289
**Usage Examples:**
290
291
```vue
292
<template>
293
<!-- Watch for content changes -->
294
<div
295
v-mutate="onContentChange"
296
class="dynamic-content"
297
v-html="dynamicHtml"
298
/>
299
300
<!-- Watch for attribute changes -->
301
<div
302
v-mutate="{
303
handler: onAttributeChange,
304
options: {
305
attributes: true,
306
attributeFilter: ['class', 'style']
307
}
308
}"
309
:class="dynamicClass"
310
:style="dynamicStyle"
311
>
312
Watch my attributes
313
</div>
314
315
<!-- Watch for child additions -->
316
<ul
317
v-mutate="{
318
handler: onListChange,
319
options: {
320
childList: true,
321
subtree: true
322
}
323
}"
324
class="dynamic-list"
325
>
326
<li v-for="item in listItems" :key="item.id">
327
{{ item.name }}
328
<button @click="removeItem(item.id)">Remove</button>
329
</li>
330
</ul>
331
332
<!-- Monitor form changes -->
333
<form
334
v-mutate="{
335
handler: onFormChange,
336
options: {
337
attributes: true,
338
childList: true,
339
subtree: true,
340
attributeFilter: ['disabled', 'required']
341
}
342
}"
343
>
344
<input v-model="formData.name" :disabled="formDisabled" />
345
<input v-model="formData.email" :required="emailRequired" />
346
</form>
347
</template>
348
349
<script setup>
350
const dynamicHtml = ref('<p>Initial content</p>');
351
const dynamicClass = ref('initial-class');
352
const listItems = ref([
353
{ id: 1, name: 'Item 1' },
354
{ id: 2, name: 'Item 2' }
355
]);
356
357
const onContentChange = (mutations) => {
358
mutations.forEach(mutation => {
359
if (mutation.type === 'childList') {
360
console.log('Content changed:', mutation.addedNodes, mutation.removedNodes);
361
}
362
});
363
};
364
365
const onAttributeChange = (mutations) => {
366
mutations.forEach(mutation => {
367
if (mutation.type === 'attributes') {
368
console.log(`Attribute ${mutation.attributeName} changed from`,
369
mutation.oldValue, 'to', mutation.target[mutation.attributeName]);
370
}
371
});
372
};
373
374
const onListChange = (mutations) => {
375
console.log('List structure changed:', mutations.length, 'mutations');
376
};
377
378
const onFormChange = (mutations) => {
379
console.log('Form changed:', mutations);
380
// Handle form validation or auto-save
381
};
382
</script>
383
```
384
385
### Resize
386
387
Directive using ResizeObserver to detect element size changes.
388
389
```typescript { .api }
390
/**
391
* Observes element resize events
392
*/
393
const Resize: Directive;
394
395
interface ResizeBinding {
396
/** Handler function called when element resizes */
397
handler?: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
398
/** Resize observer options */
399
options?: {
400
/** Debounce resize events (ms) */
401
debounce?: number;
402
/** Only trigger on width changes */
403
watchWidth?: boolean;
404
/** Only trigger on height changes */
405
watchHeight?: boolean;
406
};
407
}
408
409
// Usage syntax variations
410
type ResizeValue =
411
| ((entries: ResizeObserverEntry[], observer: ResizeObserver) => void)
412
| {
413
handler: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
414
options?: { debounce?: number; watchWidth?: boolean; watchHeight?: boolean };
415
};
416
```
417
418
**Usage Examples:**
419
420
```vue
421
<template>
422
<!-- Basic resize detection -->
423
<div
424
v-resize="onResize"
425
class="resizable-container"
426
:style="{ width: containerWidth + 'px', height: containerHeight + 'px' }"
427
>
428
<p>Size: {{ currentSize.width }} x {{ currentSize.height }}</p>
429
<button @click="resizeContainer">Change Size</button>
430
</div>
431
432
<!-- Debounced resize handling -->
433
<textarea
434
v-resize="{
435
handler: onTextareaResize,
436
options: { debounce: 250 }
437
}"
438
v-model="textContent"
439
class="auto-resize-textarea"
440
/>
441
442
<!-- Chart responsive to container -->
443
<div
444
v-resize="{
445
handler: updateChart,
446
options: { debounce: 100 }
447
}"
448
class="chart-container"
449
>
450
<canvas ref="chartCanvas" />
451
</div>
452
453
<!-- Responsive grid -->
454
<div
455
v-resize="{
456
handler: updateGridColumns,
457
options: { watchWidth: true, debounce: 150 }
458
}"
459
class="responsive-grid"
460
:style="{ gridTemplateColumns: gridColumns }"
461
>
462
<div v-for="item in gridItems" :key="item.id" class="grid-item">
463
{{ item.name }}
464
</div>
465
</div>
466
</template>
467
468
<script setup>
469
const containerWidth = ref(400);
470
const containerHeight = ref(300);
471
const currentSize = ref({ width: 0, height: 0 });
472
const textContent = ref('');
473
const gridColumns = ref('1fr 1fr 1fr');
474
const chartCanvas = ref();
475
476
const onResize = (entries) => {
477
const { width, height } = entries[0].contentRect;
478
currentSize.value = { width: Math.round(width), height: Math.round(height) };
479
};
480
481
const onTextareaResize = (entries) => {
482
const { width, height } = entries[0].contentRect;
483
console.log(`Textarea resized to ${width}x${height}`);
484
// Auto-save or adjust UI based on size
485
};
486
487
const updateChart = (entries) => {
488
const { width, height } = entries[0].contentRect;
489
if (chartCanvas.value) {
490
chartCanvas.value.width = width;
491
chartCanvas.value.height = height;
492
// Redraw chart with new dimensions
493
redrawChart();
494
}
495
};
496
497
const updateGridColumns = (entries) => {
498
const { width } = entries[0].contentRect;
499
if (width < 600) {
500
gridColumns.value = '1fr';
501
} else if (width < 900) {
502
gridColumns.value = '1fr 1fr';
503
} else {
504
gridColumns.value = '1fr 1fr 1fr';
505
}
506
};
507
508
const resizeContainer = () => {
509
containerWidth.value = Math.random() * 400 + 200;
510
containerHeight.value = Math.random() * 300 + 150;
511
};
512
</script>
513
```
514
515
### Ripple
516
517
Directive for adding Material Design ripple effect to elements on interaction.
518
519
```typescript { .api }
520
/**
521
* Adds Material Design ripple effect
522
*/
523
const Ripple: Directive;
524
525
interface RippleBinding {
526
/** Ripple configuration */
527
value?: boolean | {
528
/** Ripple color */
529
color?: string;
530
/** Center the ripple effect */
531
center?: boolean;
532
/** Custom CSS class */
533
class?: string;
534
};
535
}
536
537
// Usage syntax variations
538
type RippleValue =
539
| boolean
540
| {
541
color?: string;
542
center?: boolean;
543
class?: string;
544
};
545
```
546
547
**Usage Examples:**
548
549
```vue
550
<template>
551
<!-- Basic ripple effect -->
552
<button v-ripple class="custom-button">
553
Click for ripple
554
</button>
555
556
<!-- Disabled ripple -->
557
<button v-ripple="false" class="no-ripple-button">
558
No ripple effect
559
</button>
560
561
<!-- Custom color ripple -->
562
<div
563
v-ripple="{ color: 'red' }"
564
class="ripple-card"
565
@click="handleClick"
566
>
567
Custom red ripple
568
</div>
569
570
<!-- Centered ripple -->
571
<v-icon
572
v-ripple="{ center: true }"
573
size="large"
574
@click="toggleFavorite"
575
>
576
{{ isFavorite ? 'mdi-heart' : 'mdi-heart-outline' }}
577
</v-icon>
578
579
<!-- Conditional ripple -->
580
<button
581
v-ripple="!disabled"
582
:disabled="disabled"
583
class="conditional-ripple"
584
>
585
{{ disabled ? 'Disabled' : 'Enabled' }}
586
</button>
587
588
<!-- Custom ripple class -->
589
<div
590
v-ripple="{ class: 'custom-ripple-class', color: 'purple' }"
591
class="fancy-card"
592
>
593
Custom styled ripple
594
</div>
595
</template>
596
597
<script setup>
598
const isFavorite = ref(false);
599
const disabled = ref(false);
600
601
const handleClick = () => {
602
console.log('Card clicked with ripple');
603
};
604
605
const toggleFavorite = () => {
606
isFavorite.value = !isFavorite.value;
607
};
608
</script>
609
610
<style>
611
.custom-button {
612
padding: 12px 24px;
613
background: #1976d2;
614
color: white;
615
border: none;
616
border-radius: 4px;
617
cursor: pointer;
618
position: relative;
619
overflow: hidden;
620
}
621
622
.ripple-card {
623
padding: 20px;
624
background: #f5f5f5;
625
border-radius: 8px;
626
cursor: pointer;
627
position: relative;
628
overflow: hidden;
629
}
630
631
.custom-ripple-class {
632
animation-duration: 0.8s !important;
633
opacity: 0.3 !important;
634
}
635
</style>
636
```
637
638
### Scroll
639
640
Directive for handling scroll events with customizable options and performance optimizations.
641
642
```typescript { .api }
643
/**
644
* Handles scroll events with options
645
*/
646
const Scroll: Directive;
647
648
interface ScrollBinding {
649
/** Scroll handler function */
650
handler?: (event: Event) => void;
651
/** Scroll configuration options */
652
options?: {
653
/** Throttle scroll events (ms) */
654
throttle?: number;
655
/** Target element to watch for scroll */
656
target?: string | Element | Window;
657
/** Passive event listener */
658
passive?: boolean;
659
};
660
}
661
662
// Usage syntax variations
663
type ScrollValue =
664
| ((event: Event) => void)
665
| {
666
handler: (event: Event) => void;
667
options?: {
668
throttle?: number;
669
target?: string | Element | Window;
670
passive?: boolean;
671
};
672
};
673
```
674
675
**Usage Examples:**
676
677
```vue
678
<template>
679
<!-- Basic scroll handling -->
680
<div
681
v-scroll="onScroll"
682
class="scrollable-content"
683
style="height: 400px; overflow-y: auto;"
684
>
685
<div v-for="n in 100" :key="n" class="scroll-item">
686
Item {{ n }}
687
</div>
688
</div>
689
690
<!-- Throttled scroll -->
691
<div
692
v-scroll="{
693
handler: onThrottledScroll,
694
options: { throttle: 100 }
695
}"
696
class="performance-scroll"
697
style="height: 300px; overflow-y: auto;"
698
>
699
<div style="height: 2000px; background: linear-gradient(to bottom, #f0f0f0, #d0d0d0);">
700
Large scrollable content
701
</div>
702
</div>
703
704
<!-- Window scroll (global) -->
705
<div
706
v-scroll="{
707
handler: onWindowScroll,
708
options: {
709
target: 'window',
710
throttle: 50,
711
passive: true
712
}
713
}"
714
>
715
<!-- This element watches window scroll -->
716
<div class="scroll-indicator" :style="{ width: scrollProgress + '%' }"></div>
717
</div>
718
719
<!-- Scroll to top button -->
720
<div class="page-content" style="height: 200vh; padding: 20px;">
721
<div
722
v-scroll="{
723
handler: updateScrollTop,
724
options: { target: 'window', throttle: 100 }
725
}"
726
>
727
Long page content...
728
</div>
729
730
<v-fab
731
v-show="showScrollTop"
732
location="bottom right"
733
icon="mdi-chevron-up"
734
@click="scrollToTop"
735
/>
736
</div>
737
738
<!-- Infinite scroll implementation -->
739
<div
740
v-scroll="{
741
handler: checkInfiniteScroll,
742
options: { throttle: 200 }
743
}"
744
class="infinite-list"
745
style="height: 400px; overflow-y: auto;"
746
>
747
<div v-for="item in infiniteItems" :key="item.id" class="list-item">
748
{{ item.name }}
749
</div>
750
<div v-if="loading" class="loading">Loading more...</div>
751
</div>
752
</template>
753
754
<script setup>
755
const scrollProgress = ref(0);
756
const showScrollTop = ref(false);
757
const infiniteItems = ref([]);
758
const loading = ref(false);
759
760
const onScroll = (event) => {
761
const target = event.target;
762
console.log('Scroll position:', target.scrollTop);
763
};
764
765
const onThrottledScroll = (event) => {
766
const target = event.target;
767
const scrollPercent = (target.scrollTop / (target.scrollHeight - target.clientHeight)) * 100;
768
console.log('Scroll percent:', scrollPercent.toFixed(1));
769
};
770
771
const onWindowScroll = () => {
772
const scrolled = window.scrollY;
773
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
774
scrollProgress.value = (scrolled / maxScroll) * 100;
775
};
776
777
const updateScrollTop = () => {
778
showScrollTop.value = window.scrollY > 300;
779
};
780
781
const scrollToTop = () => {
782
window.scrollTo({
783
top: 0,
784
behavior: 'smooth'
785
});
786
};
787
788
const checkInfiniteScroll = (event) => {
789
const target = event.target;
790
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
791
792
if (scrollBottom < 100 && !loading.value) {
793
loadMoreItems();
794
}
795
};
796
797
const loadMoreItems = async () => {
798
loading.value = true;
799
// Simulate API call
800
await new Promise(resolve => setTimeout(resolve, 1000));
801
802
const newItems = Array.from({ length: 20 }, (_, i) => ({
803
id: infiniteItems.value.length + i + 1,
804
name: `Item ${infiniteItems.value.length + i + 1}`
805
}));
806
807
infiniteItems.value.push(...newItems);
808
loading.value = false;
809
};
810
</script>
811
812
<style>
813
.scroll-indicator {
814
height: 4px;
815
background: linear-gradient(to right, #2196f3, #21cbf3);
816
transition: width 0.1s ease;
817
position: fixed;
818
top: 0;
819
left: 0;
820
z-index: 9999;
821
}
822
823
.scroll-item,
824
.list-item {
825
padding: 16px;
826
border-bottom: 1px solid #eee;
827
}
828
829
.loading {
830
padding: 20px;
831
text-align: center;
832
color: #666;
833
}
834
</style>
835
```
836
837
### Touch
838
839
Directive for handling touch gestures and swipe interactions on mobile devices.
840
841
```typescript { .api }
842
/**
843
* Handles touch gestures and swipes
844
*/
845
const Touch: Directive;
846
847
interface TouchBinding {
848
/** Touch handler functions */
849
handlers?: {
850
/** Start touch handler */
851
start?: (wrapper: TouchWrapper) => void;
852
/** End touch handler */
853
end?: (wrapper: TouchWrapper) => void;
854
/** Move touch handler */
855
move?: (wrapper: TouchWrapper) => void;
856
/** Left swipe handler */
857
left?: (wrapper: TouchWrapper) => void;
858
/** Right swipe handler */
859
right?: (wrapper: TouchWrapper) => void;
860
/** Up swipe handler */
861
up?: (wrapper: TouchWrapper) => void;
862
/** Down swipe handler */
863
down?: (wrapper: TouchWrapper) => void;
864
};
865
/** Touch configuration options */
866
options?: {
867
/** Minimum distance for swipe */
868
threshold?: number;
869
/** Prevent default touch behavior */
870
passive?: boolean;
871
};
872
}
873
874
interface TouchWrapper {
875
/** Original touch event */
876
touchstartX: number;
877
touchstartY: number;
878
touchmoveX: number;
879
touchmoveY: number;
880
touchendX: number;
881
touchendY: number;
882
offsetX: number;
883
offsetY: number;
884
}
885
886
// Usage syntax variations
887
type TouchValue = {
888
[K in 'start' | 'end' | 'move' | 'left' | 'right' | 'up' | 'down']?: (wrapper: TouchWrapper) => void;
889
} & {
890
options?: {
891
threshold?: number;
892
passive?: boolean;
893
};
894
};
895
```
896
897
**Usage Examples:**
898
899
```vue
900
<template>
901
<!-- Basic swipe detection -->
902
<div
903
v-touch="{
904
left: () => nextSlide(),
905
right: () => previousSlide()
906
}"
907
class="swipe-container"
908
>
909
<div class="slide" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">
910
<div v-for="(slide, index) in slides" :key="index" class="slide-item">
911
Slide {{ index + 1 }}
912
</div>
913
</div>
914
</div>
915
916
<!-- Full gesture handling -->
917
<div
918
v-touch="{
919
start: onTouchStart,
920
move: onTouchMove,
921
end: onTouchEnd,
922
left: onSwipeLeft,
923
right: onSwipeRight,
924
up: onSwipeUp,
925
down: onSwipeDown,
926
options: { threshold: 50 }
927
}"
928
class="gesture-area"
929
:style="{
930
transform: `translate(${position.x}px, ${position.y}px)`,
931
background: touchActive ? '#e3f2fd' : '#f5f5f5'
932
}"
933
>
934
<p>Touch and swipe me!</p>
935
<p>Position: {{ position.x }}, {{ position.y }}</p>
936
<p>Last gesture: {{ lastGesture }}</p>
937
</div>
938
939
<!-- Swipeable cards -->
940
<div class="card-stack">
941
<div
942
v-for="(card, index) in cards"
943
v-show="index >= currentCardIndex"
944
:key="card.id"
945
v-touch="{
946
left: () => swipeCard('left', index),
947
right: () => swipeCard('right', index),
948
options: { threshold: 100 }
949
}"
950
class="swipe-card"
951
:style="{
952
zIndex: cards.length - index,
953
transform: `scale(${1 - (index - currentCardIndex) * 0.05})`
954
}"
955
>
956
<h3>{{ card.title }}</h3>
957
<p>{{ card.content }}</p>
958
</div>
959
</div>
960
961
<!-- Mobile drawer -->
962
<div
963
v-touch="{
964
right: openDrawer,
965
options: { threshold: 30 }
966
}"
967
class="drawer-edge"
968
>
969
<!-- Edge area for drawer gesture -->
970
</div>
971
972
<div
973
v-show="drawerOpen"
974
v-touch="{
975
left: closeDrawer,
976
options: { threshold: 50 }
977
}"
978
class="drawer"
979
:class="{ open: drawerOpen }"
980
>
981
<div class="drawer-content">
982
<h3>Mobile Drawer</h3>
983
<p>Swipe left to close</p>
984
</div>
985
</div>
986
</template>
987
988
<script setup>
989
const currentSlide = ref(0);
990
const slides = ref(['Slide 1', 'Slide 2', 'Slide 3']);
991
const position = ref({ x: 0, y: 0 });
992
const touchActive = ref(false);
993
const lastGesture = ref('none');
994
const currentCardIndex = ref(0);
995
const drawerOpen = ref(false);
996
997
const cards = ref([
998
{ id: 1, title: 'Card 1', content: 'Swipe left or right' },
999
{ id: 2, title: 'Card 2', content: 'Another card to swipe' },
1000
{ id: 3, title: 'Card 3', content: 'Last card in stack' },
1001
]);
1002
1003
const nextSlide = () => {
1004
if (currentSlide.value < slides.value.length - 1) {
1005
currentSlide.value++;
1006
}
1007
};
1008
1009
const previousSlide = () => {
1010
if (currentSlide.value > 0) {
1011
currentSlide.value--;
1012
}
1013
};
1014
1015
const onTouchStart = (wrapper) => {
1016
touchActive.value = true;
1017
console.log('Touch started at:', wrapper.touchstartX, wrapper.touchstartY);
1018
};
1019
1020
const onTouchMove = (wrapper) => {
1021
position.value = {
1022
x: wrapper.touchmoveX - wrapper.touchstartX,
1023
y: wrapper.touchmoveY - wrapper.touchstartY
1024
};
1025
};
1026
1027
const onTouchEnd = (wrapper) => {
1028
touchActive.value = false;
1029
// Reset position with animation
1030
setTimeout(() => {
1031
position.value = { x: 0, y: 0 };
1032
}, 100);
1033
};
1034
1035
const onSwipeLeft = () => {
1036
lastGesture.value = 'swipe left';
1037
};
1038
1039
const onSwipeRight = () => {
1040
lastGesture.value = 'swipe right';
1041
};
1042
1043
const onSwipeUp = () => {
1044
lastGesture.value = 'swipe up';
1045
};
1046
1047
const onSwipeDown = () => {
1048
lastGesture.value = 'swipe down';
1049
};
1050
1051
const swipeCard = (direction, index) => {
1052
if (index === currentCardIndex.value) {
1053
console.log(`Card swiped ${direction}`);
1054
currentCardIndex.value++;
1055
}
1056
};
1057
1058
const openDrawer = () => {
1059
drawerOpen.value = true;
1060
};
1061
1062
const closeDrawer = () => {
1063
drawerOpen.value = false;
1064
};
1065
</script>
1066
1067
<style>
1068
.swipe-container {
1069
width: 300px;
1070
height: 200px;
1071
overflow: hidden;
1072
border-radius: 8px;
1073
background: #f0f0f0;
1074
}
1075
1076
.slide {
1077
display: flex;
1078
transition: transform 0.3s ease;
1079
}
1080
1081
.slide-item {
1082
min-width: 300px;
1083
height: 200px;
1084
display: flex;
1085
align-items: center;
1086
justify-content: center;
1087
font-size: 24px;
1088
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1089
color: white;
1090
}
1091
1092
.gesture-area {
1093
width: 300px;
1094
height: 200px;
1095
border-radius: 12px;
1096
display: flex;
1097
flex-direction: column;
1098
align-items: center;
1099
justify-content: center;
1100
transition: transform 0.2s ease, background 0.2s ease;
1101
cursor: grab;
1102
user-select: none;
1103
}
1104
1105
.card-stack {
1106
position: relative;
1107
width: 280px;
1108
height: 180px;
1109
}
1110
1111
.swipe-card {
1112
position: absolute;
1113
top: 0;
1114
left: 0;
1115
width: 100%;
1116
height: 100%;
1117
background: white;
1118
border-radius: 12px;
1119
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
1120
padding: 20px;
1121
transition: transform 0.2s ease;
1122
cursor: grab;
1123
}
1124
1125
.drawer-edge {
1126
position: fixed;
1127
left: 0;
1128
top: 0;
1129
width: 20px;
1130
height: 100vh;
1131
z-index: 1000;
1132
}
1133
1134
.drawer {
1135
position: fixed;
1136
left: -300px;
1137
top: 0;
1138
width: 300px;
1139
height: 100vh;
1140
background: white;
1141
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
1142
transition: left 0.3s ease;
1143
z-index: 999;
1144
}
1145
1146
.drawer.open {
1147
left: 0;
1148
}
1149
1150
.drawer-content {
1151
padding: 20px;
1152
}
1153
</style>
1154
```
1155
1156
### Tooltip (Directive)
1157
1158
Directive version of tooltip for simple text tooltips without component overhead.
1159
1160
```typescript { .api }
1161
/**
1162
* Simple tooltip directive
1163
*/
1164
const Tooltip: Directive;
1165
1166
interface TooltipBinding {
1167
/** Tooltip text or configuration */
1168
value?: string | {
1169
/** Tooltip text */
1170
text: string;
1171
/** Tooltip position */
1172
location?: 'top' | 'bottom' | 'left' | 'right';
1173
/** Show delay in ms */
1174
delay?: number;
1175
/** Disabled state */
1176
disabled?: boolean;
1177
};
1178
}
1179
1180
// Usage syntax variations
1181
type TooltipValue =
1182
| string
1183
| {
1184
text: string;
1185
location?: 'top' | 'bottom' | 'left' | 'right';
1186
delay?: number;
1187
disabled?: boolean;
1188
};
1189
```
1190
1191
**Usage Examples:**
1192
1193
```vue
1194
<template>
1195
<!-- Simple text tooltip -->
1196
<button v-tooltip="'This is a simple tooltip'">
1197
Hover for tooltip
1198
</button>
1199
1200
<!-- Positioned tooltip -->
1201
<v-icon
1202
v-tooltip="{
1203
text: 'Save your work',
1204
location: 'bottom'
1205
}"
1206
@click="saveWork"
1207
>
1208
mdi-content-save
1209
</v-icon>
1210
1211
<!-- Tooltip with delay -->
1212
<v-chip
1213
v-tooltip="{
1214
text: 'Click to remove',
1215
location: 'top',
1216
delay: 1000
1217
}"
1218
closable
1219
@click:close="removeChip"
1220
>
1221
Delayed tooltip
1222
</v-chip>
1223
1224
<!-- Conditional tooltip -->
1225
<v-btn
1226
v-tooltip="{
1227
text: 'Please fill all required fields',
1228
disabled: formValid
1229
}"
1230
:disabled="!formValid"
1231
@click="submitForm"
1232
>
1233
Submit
1234
</v-btn>
1235
1236
<!-- Dynamic tooltip content -->
1237
<div
1238
v-for="item in items"
1239
:key="item.id"
1240
v-tooltip="getTooltipText(item)"
1241
class="item"
1242
>
1243
{{ item.name }}
1244
</div>
1245
</template>
1246
1247
<script setup>
1248
const formValid = ref(false);
1249
const items = ref([
1250
{ id: 1, name: 'Item 1', description: 'First item description' },
1251
{ id: 2, name: 'Item 2', description: 'Second item description' },
1252
]);
1253
1254
const saveWork = () => {
1255
console.log('Work saved');
1256
};
1257
1258
const removeChip = () => {
1259
console.log('Chip removed');
1260
};
1261
1262
const submitForm = () => {
1263
if (formValid.value) {
1264
console.log('Form submitted');
1265
}
1266
};
1267
1268
const getTooltipText = (item) => {
1269
return {
1270
text: item.description,
1271
location: 'right',
1272
delay: 500
1273
};
1274
};
1275
</script>
1276
1277
<style>
1278
.item {
1279
padding: 8px 16px;
1280
margin: 4px;
1281
background: #f5f5f5;
1282
border-radius: 4px;
1283
cursor: pointer;
1284
}
1285
1286
.item:hover {
1287
background: #e0e0e0;
1288
}
1289
</style>
1290
```
1291
1292
## Types
1293
1294
```typescript { .api }
1295
// Common directive binding interface
1296
interface DirectiveBinding<T = any> {
1297
value: T;
1298
oldValue: T;
1299
arg?: string;
1300
modifiers: Record<string, boolean>;
1301
instance: ComponentPublicInstance | null;
1302
dir: ObjectDirective<any, T>;
1303
}
1304
1305
// Event types
1306
interface TouchWrapper {
1307
touchstartX: number;
1308
touchstartY: number;
1309
touchmoveX: number;
1310
touchmoveY: number;
1311
touchendX: number;
1312
touchendY: number;
1313
offsetX: number;
1314
offsetY: number;
1315
}
1316
1317
// Observer types
1318
interface IntersectionObserverEntry {
1319
boundingClientRect: DOMRectReadOnly;
1320
intersectionRatio: number;
1321
intersectionRect: DOMRectReadOnly;
1322
isIntersecting: boolean;
1323
rootBounds: DOMRectReadOnly | null;
1324
target: Element;
1325
time: number;
1326
}
1327
1328
interface ResizeObserverEntry {
1329
borderBoxSize: ResizeObserverSize[];
1330
contentBoxSize: ResizeObserverSize[];
1331
contentRect: DOMRectReadOnly;
1332
target: Element;
1333
}
1334
1335
interface MutationRecord {
1336
type: 'childList' | 'attributes' | 'characterData';
1337
target: Node;
1338
addedNodes: NodeList;
1339
removedNodes: NodeList;
1340
previousSibling: Node | null;
1341
nextSibling: Node | null;
1342
attributeName: string | null;
1343
attributeNamespace: string | null;
1344
oldValue: string | null;
1345
}
1346
1347
// Location types for tooltips and positioning
1348
type DirectiveLocation = 'top' | 'bottom' | 'left' | 'right';
1349
```