Files
thetool/public/plugins/vue/tt-core/composables/useInfiniteScroll.js
2025-12-09 05:34:24 +00:00

151 lines
4.2 KiB
JavaScript

/**
* TT-Core Infinite Scroll Composable (Vue 3)
* Provides infinite scrolling functionality
*/
/**
* Create an infinite scroll composable
* @param {Ref} items - Reactive reference to items array
* @param {Object} options - Scroll options
* @returns {Object} - Composable with visible items and methods
*/
export function useInfiniteScroll(items, options = {}) {
const { ref, computed, onMounted, onBeforeUnmount, onUpdated } = Vue;
const visibleCount = ref(options.initialCount || 50);
const incrementBy = options.incrementBy || 50;
const sentinelRef = ref(null);
let scrollObserver = null;
const visibleItems = computed(() => {
return items.value.slice(0, visibleCount.value);
});
const hasMore = computed(() => {
return visibleCount.value < items.value.length;
});
const loadMore = () => {
if (hasMore.value) {
visibleCount.value += incrementBy;
}
};
const resetVisibleCount = () => {
visibleCount.value = options.initialCount || 50;
};
const setupScrollObserver = () => {
scrollObserver = new IntersectionObserver(
([entry]) => {
if (entry && entry.isIntersecting) {
loadMore();
}
},
{
root: options.root || null,
threshold: 0.1
}
);
if (sentinelRef.value) {
scrollObserver.observe(sentinelRef.value);
}
};
onMounted(() => {
setupScrollObserver();
});
onBeforeUnmount(() => {
if (scrollObserver) {
scrollObserver.disconnect();
scrollObserver = null;
}
});
onUpdated(() => {
// Reconnect observer when DOM updates
if (scrollObserver && sentinelRef.value) {
scrollObserver.disconnect();
scrollObserver.observe(sentinelRef.value);
}
});
return {
sentinelRef,
visibleItems,
visibleCount,
hasMore,
loadMore,
resetVisibleCount
};
}
/**
* Create an infinite scroll mixin (backward compatibility)
* @param {Object} options - Scroll options
* @returns {Object} - Vue mixin
*/
export function createInfiniteScrollMixin(options = {}) {
return {
data() {
return {
visibleCount: options.initialCount || 50,
incrementBy: options.incrementBy || 50,
scrollObserver: null
};
},
computed: {
visibleItems() {
const items = this[options.itemsKey || 'items'] || [];
return items.slice(0, this.visibleCount);
}
},
mounted() {
this.setupScrollObserver();
},
beforeUnmount() {
if (this.scrollObserver) {
this.scrollObserver.disconnect();
this.scrollObserver = null;
}
},
updated() {
// Reconnect observer when DOM updates
if (this.scrollObserver && this.$refs.sentinel) {
this.scrollObserver.disconnect();
this.scrollObserver.observe(this.$refs.sentinel);
}
},
methods: {
setupScrollObserver() {
this.scrollObserver = new IntersectionObserver(
([entry]) => {
if (entry && entry.isIntersecting) {
this.loadMore();
}
},
{
root: this.$refs.tableWrap || null,
threshold: 0.1
}
);
if (this.$refs.sentinel) {
this.scrollObserver.observe(this.$refs.sentinel);
}
},
loadMore() {
const items = this[options.itemsKey || 'items'] || [];
if (this.visibleCount < items.length) {
this.visibleCount += this.incrementBy;
}
},
resetVisibleCount() {
this.visibleCount = options.initialCount || 50;
}
}
};
}