mark-3 0.0.6 → 0.0.10
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.
- package/CHANGELOG.md +51 -0
- package/components/index.js +1 -0
- package/components/virtual-scroll.vue +275 -0
- package/composables/index.js +6 -2
- package/composables/use-css-metrics.ts +115 -0
- package/composables/use-event-listener.js +17 -8
- package/composables/use-media-query.ts +51 -0
- package/composables/use-outside-clicks.js +7 -1
- package/composables/use-pointer-swipe.js +62 -25
- package/composables/use-resize-observer.js +93 -0
- package/composables/use-responsiveness.js +5 -3
- package/composables/use-scroll-event.js +25 -21
- package/composables/use-scroll-position.js +75 -0
- package/composables/use-swipeable-drawer.ts +139 -0
- package/composables/use-timeout.ts +71 -0
- package/helpers/array/chunk.ts +24 -0
- package/helpers/array/index.js +1 -0
- package/helpers/array/tests/chunk.test.js +41 -0
- package/helpers/date/index.js +2 -1
- package/helpers/date/is-on-same-day.js +1 -1
- package/helpers/date/seconds-remaining.ts +20 -0
- package/helpers/date/tests/seconds-remaining.test.js +49 -0
- package/helpers/number/compact.js +40 -0
- package/helpers/number/index.js +2 -0
- package/helpers/number/reduce.js +32 -0
- package/helpers/number/tests/compact.test.js +44 -0
- package/helpers/number/tests/reduce.test.js +30 -0
- package/helpers/time/debounce.ts +33 -0
- package/helpers/time/index.js +1 -0
- package/helpers/time/tests/throttle.test.js +81 -0
- package/helpers/time/throttle.js +63 -0
- package/package.json +1 -1
- package/composables/use-media-query.js +0 -34
- package/composables/use-swipeable-drawer.js +0 -161
- package/helpers/time/debounce.js +0 -29
- /package/helpers/date/{format-timestamp.js → from-timestamp.js} +0 -0
- /package/helpers/date/tests/{format-timestamp.test.js → from-timestamp.test.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.0.10](https://github.com/ismailceylan/mark-3/compare/v0.0.9...v0.0.10) (2025-09-19)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **composables:** add new use-timeout composable ([e55c99b](https://github.com/ismailceylan/mark-3/commit/e55c99becb211c006b853682d699b6df543c3bf0))
|
|
11
|
+
* **composables:** add persistent option for the use-event-listener composable ([3361183](https://github.com/ismailceylan/mark-3/commit/3361183e77ac2a3842834efabfe2ea057d890ddf))
|
|
12
|
+
* **composables:** add persistent option for the use-media-query composable ([1cbff8f](https://github.com/ismailceylan/mark-3/commit/1cbff8f6d92fe9a08a73840789261552eecfdd03))
|
|
13
|
+
* **helpers:** add new date/seconds-remaining helper ([0404d06](https://github.com/ismailceylan/mark-3/commit/0404d066f8240152a3a4aa25ee49129549636983))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **composables:** reattach the events when ref-type elements are reassigned ([dde2c85](https://github.com/ismailceylan/mark-3/commit/dde2c85039b69bb6ca75f88334c9d5d81664c08f))
|
|
19
|
+
* **helpers:** correct from-timestamp filename without affecting exports ([15273cb](https://github.com/ismailceylan/mark-3/commit/15273cb7ade8be17a5dda74d8402dbd1f1371b5c))
|
|
20
|
+
* **helpers:** correct is-on-same-day filename without affecting exports ([015b92a](https://github.com/ismailceylan/mark-3/commit/015b92ada54f67fb7d529b92c084f0e82d744f0a))
|
|
21
|
+
|
|
22
|
+
### [0.0.9](https://github.com/ismailceylan/mark-3/compare/v0.0.8...v0.0.9) (2025-08-30)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* **components:** add an option to ignore window size changes in virtual-scroll component ([2e27806](https://github.com/ismailceylan/mark-3/commit/2e2780680718824df973d050b109cf81ef657dc5))
|
|
28
|
+
* **components:** add support for two-way top scroll determination using the v-model method for virtual-scroll component ([123c89e](https://github.com/ismailceylan/mark-3/commit/123c89e6cc216305ac7d66e2c8d961d50cc7aaad))
|
|
29
|
+
* **helpers:** add new chunk method for arrays ([9150623](https://github.com/ismailceylan/mark-3/commit/9150623b9194830b5699c0863e8313fcde21c3d3))
|
|
30
|
+
* **helpers:** add new number/compact helper ([6d40850](https://github.com/ismailceylan/mark-3/commit/6d40850dcb174d32232e70e9139fe71f43a05d96))
|
|
31
|
+
* **helpers:** add new number/reduce helper ([1881c99](https://github.com/ismailceylan/mark-3/commit/1881c992547a42eb6b23aa60298d5abfc2ef0c56))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Bug Fixes
|
|
35
|
+
|
|
36
|
+
* **components:** add a watch getter to watch some ref elements in virtual-scroll component ([a74b0b7](https://github.com/ismailceylan/mark-3/commit/a74b0b7061a3fef9da8acc1177dda03f54edd9ae))
|
|
37
|
+
* **components:** add the immediate flag to process the already full item store immediately in virtual-scroll component ([a45a4da](https://github.com/ismailceylan/mark-3/commit/a45a4da63c3921714b0df8f86e33601956ad9a69))
|
|
38
|
+
* **components:** use a more secure value to ensure that the key value of the virtual scroll element is unique ([1db4081](https://github.com/ismailceylan/mark-3/commit/1db40812aadbbc186bcae6a7169a298060aa9ced))
|
|
39
|
+
* **components:** use the getter watch pattern to watch elements properly when they are sent as refs in virtual-scroll component ([6556ae8](https://github.com/ismailceylan/mark-3/commit/6556ae8abec1c2b5715fd914503d539bb1dd8e8a))
|
|
40
|
+
* **composables:** fix virtual-scroll artifact problems and add relative position to wrapper ([a7566c3](https://github.com/ismailceylan/mark-3/commit/a7566c352f0f83f8ab0df910d3e7d6517d0e5c3a))
|
|
41
|
+
* **composables:** virtual-scroll recalculate properly item heights when the screen size changed ([0749124](https://github.com/ismailceylan/mark-3/commit/07491247c28461c9cc65b49c98d992dfab07aeea))
|
|
42
|
+
|
|
43
|
+
### [0.0.8](https://github.com/ismailceylan/mark-3/compare/v0.0.7...v0.0.8) (2025-08-13)
|
|
44
|
+
|
|
45
|
+
### [0.0.7](https://github.com/ismailceylan/mark-3/compare/v0.0.6...v0.0.7) (2025-08-13)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Features
|
|
49
|
+
|
|
50
|
+
* **components:** add new virtual-scroll component ([c73a31d](https://github.com/ismailceylan/mark-3/commit/c73a31d6dc2ec37d8e70a15160bda495425712ca))
|
|
51
|
+
* **composables:** add new use-css-metrics composable ([8500859](https://github.com/ismailceylan/mark-3/commit/85008595788348956cd2f40cc0fff65d038672b0))
|
|
52
|
+
* **composables:** add new use-resize-observer composable ([6d110b5](https://github.com/ismailceylan/mark-3/commit/6d110b5c17facd4918a8a02a49d2f7644ada8f96))
|
|
53
|
+
* **composables:** add new use-scroll-position composable ([f847cb0](https://github.com/ismailceylan/mark-3/commit/f847cb0a9f19f09f82ab55d447227a7aa21516d5))
|
|
54
|
+
* **helpers:** add new time/throttle method ([60b8472](https://github.com/ismailceylan/mark-3/commit/60b8472fdb56119888c90446fe237b1fc9573f2b))
|
|
55
|
+
|
|
5
56
|
### [0.0.6](https://github.com/ismailceylan/mark-3/compare/v0.0.5...v0.0.6) (2025-08-09)
|
|
6
57
|
|
|
7
58
|
|
package/components/index.js
CHANGED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Mark-3/VirtualScroll -->
|
|
3
|
+
<div ref="container">
|
|
4
|
+
<div :style="{ transform: 'translateZ(0)', position: 'relative', minHeight: totalHeight + 'px' }">
|
|
5
|
+
<slot
|
|
6
|
+
v-for="{ item, index } of visibleItems"
|
|
7
|
+
:key="index"
|
|
8
|
+
:item
|
|
9
|
+
:index
|
|
10
|
+
:observe
|
|
11
|
+
:has-scrollbar
|
|
12
|
+
:style="{ position: 'absolute', transform: 'translateY(' + offsets[ index ] + 'px)' }"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<slot name="bottom" />
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script lang="ts" setup>
|
|
21
|
+
import { ref, watch, computed, reactive, nextTick } from "vue";
|
|
22
|
+
import { useCssMetrics, useEventListener, useResizeObserver, useScrollPosition } from "../composables";
|
|
23
|
+
import { debounce } from "../helpers/time";
|
|
24
|
+
import { clamp } from "../helpers/number";
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits([ "threshold-reached" ]);
|
|
27
|
+
|
|
28
|
+
const scrollTop = defineModel( "scrollTop", { default: 0 });
|
|
29
|
+
|
|
30
|
+
const props = defineProps(
|
|
31
|
+
{
|
|
32
|
+
pageMode: Boolean,
|
|
33
|
+
dontWatchResizing: Boolean,
|
|
34
|
+
|
|
35
|
+
items: {
|
|
36
|
+
type: Array,
|
|
37
|
+
required: true
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
buffer: {
|
|
41
|
+
type: Number,
|
|
42
|
+
default: 5
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
minHeight: {
|
|
46
|
+
type: Number,
|
|
47
|
+
default: 20
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
itemGapClasses: {
|
|
51
|
+
type: String
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
threshold: {
|
|
55
|
+
type: Number,
|
|
56
|
+
default: 3
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const { max, abs, floor } = Math;
|
|
61
|
+
const heights = ref<number[]>([]);
|
|
62
|
+
const identifiedItems = reactive<{ item: any, index: number }[]>([]);
|
|
63
|
+
const container = ref<HTMLDivElement>( null );
|
|
64
|
+
const hasScrollbar = ref( false );
|
|
65
|
+
const metrics = useCssMetrics( props.itemGapClasses, [ "gap" ] as const, { throttle: 100 });
|
|
66
|
+
const isHeightsDirty = ref( false );
|
|
67
|
+
const dirtyItems = {}
|
|
68
|
+
|
|
69
|
+
const scrollableElement = computed(() =>
|
|
70
|
+
props.pageMode
|
|
71
|
+
? window
|
|
72
|
+
: container.value
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const scrollPos = useScrollPosition( scrollableElement, { throttle: 150 });
|
|
76
|
+
|
|
77
|
+
const offsets = computed<number[]>(() =>
|
|
78
|
+
{
|
|
79
|
+
const arr = new Array( heights.value.length );
|
|
80
|
+
let sum = 0;
|
|
81
|
+
|
|
82
|
+
for( let i = 0; i < heights.value.length; i++ )
|
|
83
|
+
{
|
|
84
|
+
arr[ i ] = sum;
|
|
85
|
+
sum += heights.value[ i ] || 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return arr;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const totalHeight = computed(() =>
|
|
92
|
+
{
|
|
93
|
+
if( identifiedItems.length === 0 )
|
|
94
|
+
{
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const latestOffset = offsets.value[ offsets.value.length - 1 ] || 0;
|
|
99
|
+
const latestHeight = heights.value[ heights.value.length - 1 ] || 0;
|
|
100
|
+
const h = latestOffset + latestHeight;
|
|
101
|
+
|
|
102
|
+
if( h === metrics.value.gap * 2 )
|
|
103
|
+
{
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return h + 1;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const startIndex = computed(() =>
|
|
111
|
+
{
|
|
112
|
+
let low = 0;
|
|
113
|
+
let high = offsets.value.length - 1;
|
|
114
|
+
let mid: number;
|
|
115
|
+
|
|
116
|
+
while( low <= high )
|
|
117
|
+
{
|
|
118
|
+
mid = floor(( low + high ) / 2 );
|
|
119
|
+
|
|
120
|
+
if( offsets.value[ mid ] === scrollPos.y.value )
|
|
121
|
+
{
|
|
122
|
+
return mid;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if( offsets.value[ mid ] < scrollPos.y.value )
|
|
126
|
+
{
|
|
127
|
+
low = mid + 1;
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
{
|
|
131
|
+
high = mid - 1;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return low - 1 < 0
|
|
136
|
+
? 0
|
|
137
|
+
: low - 1;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const endIndex = computed(() =>
|
|
141
|
+
{
|
|
142
|
+
let total = 0;
|
|
143
|
+
let idx = startIndex.value;
|
|
144
|
+
|
|
145
|
+
while( idx < heights.value.length && total < window.innerHeight )
|
|
146
|
+
{
|
|
147
|
+
total += heights.value[ idx ];
|
|
148
|
+
idx++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return max( 0, idx - 1 );
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const startIndexWithMargin = computed(() =>
|
|
155
|
+
max( 0, startIndex.value - props.buffer )
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const endIndexWithMargin = computed(() =>
|
|
159
|
+
clamp( endIndex.value + props.buffer, 0, heights.value.length )
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const visibleItems = computed(() =>
|
|
163
|
+
identifiedItems.slice( startIndexWithMargin.value, endIndexWithMargin.value )
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const { observe } = useResizeObserver(( entries ) =>
|
|
167
|
+
{
|
|
168
|
+
entries.forEach( entry =>
|
|
169
|
+
{
|
|
170
|
+
const index = parseInt(( entry.target as HTMLElement ).dataset.index );
|
|
171
|
+
const newHeight = entry.contentRect.height;
|
|
172
|
+
const oldHeight = heights.value[ index ] || 0;
|
|
173
|
+
|
|
174
|
+
heights.value[ index ] = index in dirtyItems
|
|
175
|
+
// replace old height
|
|
176
|
+
? newHeight + metrics.value.gap
|
|
177
|
+
: max( newHeight + metrics.value.gap, oldHeight );
|
|
178
|
+
|
|
179
|
+
delete dirtyItems[ index ];
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
watch( scrollPos.y, () => scrollTop.value = scrollPos.y.value );
|
|
184
|
+
|
|
185
|
+
watch( scrollTop, async () =>
|
|
186
|
+
{
|
|
187
|
+
await nextTick();
|
|
188
|
+
|
|
189
|
+
if( scrollableElement.value instanceof HTMLElement )
|
|
190
|
+
{
|
|
191
|
+
scrollableElement.value.scrollTop = scrollTop.value;
|
|
192
|
+
}
|
|
193
|
+
else if( scrollableElement.value === window )
|
|
194
|
+
{
|
|
195
|
+
window.scrollTo( 0, scrollTop.value );
|
|
196
|
+
}
|
|
197
|
+
}, { immediate: true, once: true });
|
|
198
|
+
|
|
199
|
+
watch( endIndex, () =>
|
|
200
|
+
{
|
|
201
|
+
if(( identifiedItems.length - 1 ) - endIndex.value <= props.threshold )
|
|
202
|
+
{
|
|
203
|
+
emit( "threshold-reached" );
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
watch(() => props.items, items =>
|
|
208
|
+
{
|
|
209
|
+
const normalizedItems = items.map(( item, i ) =>
|
|
210
|
+
({
|
|
211
|
+
item,
|
|
212
|
+
index: i
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
identifiedItems.length = 0;
|
|
216
|
+
identifiedItems.push( ...normalizedItems );
|
|
217
|
+
|
|
218
|
+
heights.value = items.map(( _, i ) =>
|
|
219
|
+
heights.value[ i ] ?? props.minHeight
|
|
220
|
+
);
|
|
221
|
+
}, { immediate: true, deep: true });
|
|
222
|
+
|
|
223
|
+
watch( isHeightsDirty, () =>
|
|
224
|
+
{
|
|
225
|
+
if( isHeightsDirty.value === false )
|
|
226
|
+
{
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
visibleItems.value.forEach(({ index }) =>
|
|
231
|
+
{
|
|
232
|
+
const itemEl = container.value.querySelector( "[data-index='" + index + "']" );
|
|
233
|
+
|
|
234
|
+
if( itemEl === null )
|
|
235
|
+
{
|
|
236
|
+
return dirtyItems[ index ] = true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
heights.value[ index ] = itemEl.getBoundingClientRect().height + metrics.value.gap;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
isHeightsDirty.value = false;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
watch( container, () =>
|
|
246
|
+
{
|
|
247
|
+
setTimeout(() =>
|
|
248
|
+
hasScrollbar.value = container.value.scrollHeight > container.value.clientHeight
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if( props.dontWatchResizing === false )
|
|
253
|
+
{
|
|
254
|
+
let latestWidth = 0;
|
|
255
|
+
const debouncedResetHeights = debounce( resetHeights, 500 );
|
|
256
|
+
|
|
257
|
+
useEventListener( window, "resize", () =>
|
|
258
|
+
{
|
|
259
|
+
const newWidth = window.innerWidth;
|
|
260
|
+
|
|
261
|
+
if( newWidth !== latestWidth )
|
|
262
|
+
{
|
|
263
|
+
latestWidth = newWidth;
|
|
264
|
+
debouncedResetHeights();
|
|
265
|
+
}
|
|
266
|
+
}, { passive: true });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resetHeights()
|
|
270
|
+
{
|
|
271
|
+
heights.value = identifiedItems.map(() => props.minHeight );
|
|
272
|
+
isHeightsDirty.value = true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
</script>
|
package/composables/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
export { default as
|
|
1
|
+
export { default as useTimeout } from "./use-timeout.ts";
|
|
2
|
+
export { default as useMediaQuery } from "./use-media-query.ts";
|
|
3
|
+
export { default as useCssMetrics } from "./use-css-metrics.ts";
|
|
2
4
|
export { default as useScrollEvent } from "./use-scroll-event.js";
|
|
3
5
|
export { default as usePointerSwipe } from "./use-pointer-swipe.js";
|
|
4
6
|
export { default as useOutsideClicks } from "./use-outside-clicks.js";
|
|
5
7
|
export { default as useEventListener } from "./use-event-listener.js";
|
|
6
8
|
export { default as useResponsiveness } from "./use-responsiveness.js";
|
|
7
|
-
export { default as
|
|
9
|
+
export { default as useResizeObserver } from "./use-resize-observer.js";
|
|
10
|
+
export { default as useScrollPosition } from "./use-scroll-position.js";
|
|
11
|
+
export { default as useSwipeableDrawer } from "./use-swipeable-drawer.ts";
|
|
8
12
|
export { default as useIntersectionObserver } from "./use-intersection-observer.js";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ref, onMounted, onUnmounted, Ref } from "vue";
|
|
2
|
+
import { useResizeObserver } from ".";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a reactive reference containing numeric values of specified CSS properties
|
|
6
|
+
* for elements with given classes. It observes size changes and updates the metrics
|
|
7
|
+
* accordingly.
|
|
8
|
+
*
|
|
9
|
+
* @template P
|
|
10
|
+
* @param classes - CSS classes to apply to the ghost element used for measurement.
|
|
11
|
+
* @param properties - Array of CSS property names to track.
|
|
12
|
+
* @param options - Optional configuration object.
|
|
13
|
+
* @returns A reactive reference with the current values of the specified CSS properties.
|
|
14
|
+
*/
|
|
15
|
+
export default function useCssMetrics<P extends readonly string[]>(
|
|
16
|
+
classes: string|string[],
|
|
17
|
+
properties: P, { throttle = 0 } = {}
|
|
18
|
+
): Ref<{[ K in typeof properties[ number ]]: number }>
|
|
19
|
+
{
|
|
20
|
+
const metrics = ref({});
|
|
21
|
+
let ghostEl = null;
|
|
22
|
+
let widthEl = null;
|
|
23
|
+
let observer = null;
|
|
24
|
+
const { observe, unobserve, disconnect } = useResizeObserver( updateMetrics, { throttle });
|
|
25
|
+
|
|
26
|
+
metrics.value = Object.fromEntries( properties.map( prop =>
|
|
27
|
+
[ prop, 0 ]
|
|
28
|
+
));
|
|
29
|
+
|
|
30
|
+
if( classes === undefined || ( Array.isArray( classes ) && classes.length === 0 ))
|
|
31
|
+
{
|
|
32
|
+
return metrics;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMounted( init );
|
|
36
|
+
onUnmounted( destroy );
|
|
37
|
+
|
|
38
|
+
function init()
|
|
39
|
+
{
|
|
40
|
+
if( ghostEl )
|
|
41
|
+
{
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ghostEl = document.createElement( "div" );
|
|
46
|
+
widthEl = document.createElement( "div" );
|
|
47
|
+
|
|
48
|
+
const ghostStyle = ghostEl.style;
|
|
49
|
+
const widthStyle = widthEl.style;
|
|
50
|
+
|
|
51
|
+
ghostStyle.position = "absolute";
|
|
52
|
+
ghostStyle.top = "0";
|
|
53
|
+
ghostStyle.left = "0";
|
|
54
|
+
ghostStyle.width = "100%";
|
|
55
|
+
ghostStyle.height = "100%";
|
|
56
|
+
ghostStyle.visibility = "hidden";
|
|
57
|
+
ghostStyle.pointerEvents = "none";
|
|
58
|
+
|
|
59
|
+
widthStyle.position = "absolute";
|
|
60
|
+
widthStyle.visibility = "hidden";
|
|
61
|
+
widthStyle.pointerEvents = "none";
|
|
62
|
+
|
|
63
|
+
ghostEl.appendChild( widthEl );
|
|
64
|
+
|
|
65
|
+
const cls = Array.isArray( classes )
|
|
66
|
+
? classes
|
|
67
|
+
: classes.split( " " );
|
|
68
|
+
|
|
69
|
+
ghostEl.classList.add( ...cls );
|
|
70
|
+
widthEl.classList.add( ...cls );
|
|
71
|
+
|
|
72
|
+
document.body.appendChild( ghostEl );
|
|
73
|
+
|
|
74
|
+
observe( ghostEl );
|
|
75
|
+
updateMetrics();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateMetrics()
|
|
79
|
+
{
|
|
80
|
+
if( ! ghostEl )
|
|
81
|
+
{
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const computed = getComputedStyle( ghostEl );
|
|
86
|
+
const computed2 = getComputedStyle( widthEl );
|
|
87
|
+
|
|
88
|
+
properties.forEach( prop =>
|
|
89
|
+
{
|
|
90
|
+
const source = [ "width", "height" ].includes( prop )
|
|
91
|
+
? computed2
|
|
92
|
+
: computed;
|
|
93
|
+
|
|
94
|
+
metrics.value[ prop ] = parseFloat( source.getPropertyValue( prop )) || 0;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function destroy()
|
|
99
|
+
{
|
|
100
|
+
if( observer && ghostEl )
|
|
101
|
+
{
|
|
102
|
+
unobserve( ghostEl );
|
|
103
|
+
disconnect();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if( ghostEl && ghostEl.parentElement )
|
|
107
|
+
{
|
|
108
|
+
ghostEl.parentElement.removeChild( ghostEl );
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ghostEl = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return metrics;
|
|
115
|
+
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unref, onMounted, onUnmounted, getCurrentInstance } from "vue";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* @template T
|
|
5
|
-
* @typedef {import("vue").Ref<T>} Ref
|
|
6
|
-
*/
|
|
7
3
|
/**
|
|
8
4
|
* A composition function that adds a DOM event listener to the given target with the
|
|
9
5
|
* given event name and callback. The options object is optional. If the composition
|
|
@@ -15,12 +11,12 @@ import { isRef, unref, onMounted, onUnmounted, getCurrentInstance } from "vue";
|
|
|
15
11
|
* @param {EventTarget|Ref<EventTarget>} maybeRefTarget - The target element to add the event listener to.
|
|
16
12
|
* @param {string} eventName - The name of the event to add a listener for.
|
|
17
13
|
* @param {function} callBack - The callback function to call when the event happens.
|
|
18
|
-
* @param {
|
|
14
|
+
* @param {EventListenerOptions} [options] - The options object to pass to addEventListener.
|
|
19
15
|
* @returns {function} - A function that can be called to remove the event listener.
|
|
20
16
|
*/
|
|
21
|
-
export default function useEventListener( maybeRefTarget, eventName, callBack, options )
|
|
17
|
+
export default function useEventListener( maybeRefTarget, eventName, callBack, options = {})
|
|
22
18
|
{
|
|
23
|
-
if( getCurrentInstance() &&
|
|
19
|
+
if( getCurrentInstance() && ! options.persistent )
|
|
24
20
|
{
|
|
25
21
|
onMounted( listen );
|
|
26
22
|
onUnmounted( stop );
|
|
@@ -52,3 +48,16 @@ export default function useEventListener( maybeRefTarget, eventName, callBack, o
|
|
|
52
48
|
|
|
53
49
|
return stop;
|
|
54
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @template T
|
|
54
|
+
* @typedef {import("vue").Ref<T>} Ref
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* @typedef EventListenerOptions
|
|
58
|
+
* @type {object}
|
|
59
|
+
* @property {boolean} [capture=false] true for capturing, false for bubbling
|
|
60
|
+
* @property {boolean} [once=false] true for run the event once or false for keep it persistent
|
|
61
|
+
* @property {boolean} [passive=false] true for passive, false for not passive
|
|
62
|
+
* @property {boolean} [persistent=false] true for persistent, false for not persistent
|
|
63
|
+
*/
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Ref, ref } from "vue";
|
|
2
|
+
import { useEventListener } from ".";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a reactive reference that tracks whether the given media query matches the current viewport.
|
|
6
|
+
*
|
|
7
|
+
* @param query - A valid media query string.
|
|
8
|
+
* @returns A reactive reference to the match status.
|
|
9
|
+
* @example
|
|
10
|
+
* const isMobileRef = useMediaQuery( "(max-width: 767px)",
|
|
11
|
+
* {
|
|
12
|
+
* onChange: matches => console.log( matches )
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
export default function useMediaQuery(
|
|
16
|
+
query: string,
|
|
17
|
+
{ persistent = false, onChange }: UseMediaQueryOptions = {}
|
|
18
|
+
): Ref<boolean>
|
|
19
|
+
{
|
|
20
|
+
const match = matchMedia( query );
|
|
21
|
+
const matches = ref( match.matches );
|
|
22
|
+
|
|
23
|
+
triggerWatcher();
|
|
24
|
+
|
|
25
|
+
useEventListener(
|
|
26
|
+
match, "change",
|
|
27
|
+
( e: MediaQueryListEvent ) => triggerWatcher( matches.value = e.matches ),
|
|
28
|
+
{ persistent }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function triggerWatcher( _?: boolean )
|
|
32
|
+
{
|
|
33
|
+
onChange && onChange( matches.value );
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return matches;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ChangeListener = ( matches: boolean ) => void;
|
|
40
|
+
|
|
41
|
+
interface UseMediaQueryOptions
|
|
42
|
+
{
|
|
43
|
+
/** Whether to persist the match status across component mounts and unmounts */
|
|
44
|
+
persistent?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* A function that will be called whenever the match status
|
|
47
|
+
* changes. The function will receive the current match status
|
|
48
|
+
* as a boolean argument.
|
|
49
|
+
*/
|
|
50
|
+
onChange?: ChangeListener;
|
|
51
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEventListener } from ".";
|
|
2
|
+
import { isArray } from "../helpers/types";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Watches for clicks outside of the given element and calls the
|
|
@@ -16,7 +17,7 @@ import { useEventListener } from ".";
|
|
|
16
17
|
* listener when the extraordinary circumstances arise.
|
|
17
18
|
*
|
|
18
19
|
* @typedef {import('vue').Ref<Element>} ElementRef
|
|
19
|
-
* @param {ElementRef[]} elRefs - The elements to watch for clicks outside of.
|
|
20
|
+
* @param {ElementRef|ElementRef[]} elRefs - The elements to watch for clicks outside of.
|
|
20
21
|
* @param {function} callback - The callback to call when a click is outside of the element.
|
|
21
22
|
* @param {object} options - The options object.
|
|
22
23
|
* @property {string} options.on - The event to listen for.
|
|
@@ -40,6 +41,11 @@ import { useEventListener } from ".";
|
|
|
40
41
|
*/
|
|
41
42
|
export default function useOutsideClick( elRefs, callback, { on = "click" } = {})
|
|
42
43
|
{
|
|
44
|
+
if( ! isArray( elRefs ))
|
|
45
|
+
{
|
|
46
|
+
elRefs = [ elRefs ];
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
return useEventListener( document, on, e =>
|
|
44
50
|
{
|
|
45
51
|
if( elRefs.length === 0 )
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, reactive, computed, onMounted, onUnmounted } from "vue";
|
|
1
|
+
import { ref, isRef, watch, reactive, computed, onMounted, onUnmounted } from "vue";
|
|
2
2
|
import { useEventListener } from ".";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -61,30 +61,6 @@ export default function usePointerSwipe( maybeRefEl, { threshold = 50, disableTe
|
|
|
61
61
|
return abs( distance / timePassed.value );
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
useEventListener( maybeRefEl, "pointerdown", e =>
|
|
65
|
-
{
|
|
66
|
-
isSwiping.value = true;
|
|
67
|
-
posStart.x = e.clientX;
|
|
68
|
-
posStart.y = e.clientY;
|
|
69
|
-
timeStart.value = performance.now();
|
|
70
|
-
|
|
71
|
-
const stopListeningMove = useEventListener( maybeRefEl, "pointermove", e =>
|
|
72
|
-
{
|
|
73
|
-
posEnd.x = e.clientX;
|
|
74
|
-
posEnd.y = e.clientY;
|
|
75
|
-
distanceX.value = posEnd.x - posStart.x;
|
|
76
|
-
distanceY.value = posEnd.y - posStart.y;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
useEventListener( maybeRefEl, "pointerup", () =>
|
|
80
|
-
{
|
|
81
|
-
isSwiping.value = false;
|
|
82
|
-
timeEnd.value = performance.now();
|
|
83
|
-
|
|
84
|
-
stopListeningMove();
|
|
85
|
-
},{ once: true });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
64
|
onMounted(() =>
|
|
89
65
|
{
|
|
90
66
|
if( disableTextSelect )
|
|
@@ -101,6 +77,67 @@ export default function usePointerSwipe( maybeRefEl, { threshold = 50, disableTe
|
|
|
101
77
|
}
|
|
102
78
|
});
|
|
103
79
|
|
|
80
|
+
if( isRef( maybeRefEl ))
|
|
81
|
+
{
|
|
82
|
+
watch( maybeRefEl, listen, { immediate: true });
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
{
|
|
86
|
+
listen();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function listen()
|
|
90
|
+
{
|
|
91
|
+
useEventListener(
|
|
92
|
+
maybeRefEl,
|
|
93
|
+
"pointerdown",
|
|
94
|
+
onDown
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function onDown( e )
|
|
99
|
+
{
|
|
100
|
+
onStart( e );
|
|
101
|
+
|
|
102
|
+
const stopMoving = useEventListener(
|
|
103
|
+
maybeRefEl,
|
|
104
|
+
"pointermove",
|
|
105
|
+
onMove,
|
|
106
|
+
{ passive: true, capture: false }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
useEventListener(
|
|
110
|
+
maybeRefEl,
|
|
111
|
+
"pointerup",
|
|
112
|
+
() => onUp( stopMoving ),
|
|
113
|
+
{ once: true }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function onStart( e )
|
|
118
|
+
{
|
|
119
|
+
isSwiping.value = true;
|
|
120
|
+
posStart.x = e.clientX;
|
|
121
|
+
posStart.y = e.clientY;
|
|
122
|
+
timeStart.value = performance.now();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function onMove( e )
|
|
126
|
+
{
|
|
127
|
+
posEnd.x = e.clientX;
|
|
128
|
+
posEnd.y = e.clientY;
|
|
129
|
+
distanceX.value = posEnd.x - posStart.x;
|
|
130
|
+
distanceY.value = posEnd.y - posStart.y;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function onUp( stopMoving )
|
|
134
|
+
{
|
|
135
|
+
isSwiping.value = false;
|
|
136
|
+
timeEnd.value = performance.now();
|
|
137
|
+
|
|
138
|
+
stopMoving();
|
|
139
|
+
}
|
|
140
|
+
|
|
104
141
|
return {
|
|
105
142
|
isSwiping,
|
|
106
143
|
posStart,
|