Radius/add network structure
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* TT-Core Async Data Composable (Vue 3)
|
||||
* Provides async data fetching with loading states
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an async data composable
|
||||
* @returns {Object} - Composable with state and methods
|
||||
*/
|
||||
export function useAsyncData() {
|
||||
const { ref } = Vue;
|
||||
|
||||
const isLoading = ref(false);
|
||||
const hasError = ref(false);
|
||||
const errorMessage = ref(null);
|
||||
const data = ref(null);
|
||||
|
||||
/**
|
||||
* Execute async operation with loading state
|
||||
* @param {Function} asyncFn - Async function to execute
|
||||
* @param {Object} options - Options
|
||||
* @returns {Promise<any>} - Result
|
||||
*/
|
||||
const executeAsync = async (asyncFn, options = {}) => {
|
||||
isLoading.value = true;
|
||||
hasError.value = false;
|
||||
errorMessage.value = null;
|
||||
|
||||
try {
|
||||
const result = await asyncFn();
|
||||
data.value = result;
|
||||
return result;
|
||||
} catch (error) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = error.message || 'Ein Fehler ist aufgetreten';
|
||||
|
||||
if (options.onError) {
|
||||
options.onError(error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch data from API
|
||||
* @param {string} url - API URL
|
||||
* @param {Object} options - Fetch options
|
||||
* @returns {Promise<any>} - Response data
|
||||
*/
|
||||
const fetchData = async (url, options = {}) => {
|
||||
return executeAsync(async () => {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset state
|
||||
*/
|
||||
const reset = () => {
|
||||
isLoading.value = false;
|
||||
hasError.value = false;
|
||||
errorMessage.value = null;
|
||||
data.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
hasError,
|
||||
errorMessage,
|
||||
data,
|
||||
executeAsync,
|
||||
fetchData,
|
||||
reset
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an async data mixin (backward compatibility)
|
||||
* @returns {Object} - Vue mixin
|
||||
*/
|
||||
export function createAsyncDataMixin() {
|
||||
return {
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
hasError: false,
|
||||
errorMessage: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Execute async operation with loading state
|
||||
* @param {Function} asyncFn - Async function to execute
|
||||
* @param {Object} options - Options
|
||||
* @returns {Promise<any>} - Result
|
||||
*/
|
||||
async executeAsync(asyncFn, options = {}) {
|
||||
this.isLoading = true;
|
||||
this.hasError = false;
|
||||
this.errorMessage = null;
|
||||
|
||||
try {
|
||||
const result = await asyncFn();
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.hasError = true;
|
||||
this.errorMessage = error.message || 'Ein Fehler ist aufgetreten';
|
||||
|
||||
if (options.onError) {
|
||||
options.onError(error);
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch data from API
|
||||
* @param {string} url - API URL
|
||||
* @param {Object} options - Fetch options
|
||||
* @returns {Promise<any>} - Response data
|
||||
*/
|
||||
async fetchData(url, options = {}) {
|
||||
return this.executeAsync(async () => {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* TT-Core Intersection Observer Composable (Vue 3)
|
||||
* Provides lazy-loading and visibility detection
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an intersection observer composable
|
||||
* @param {Function} callback - Callback when element becomes visible
|
||||
* @param {Object} options - Observer options
|
||||
* @returns {Object} - Composable with ref and cleanup
|
||||
*/
|
||||
export function useIntersectionObserver(callback, options = {}) {
|
||||
const { ref, onMounted, onBeforeUnmount } = Vue;
|
||||
|
||||
const targetRef = ref(null);
|
||||
let observer = null;
|
||||
|
||||
onMounted(() => {
|
||||
const threshold = options.threshold || 0.1;
|
||||
const rootMargin = options.rootMargin || '0px';
|
||||
const once = options.once !== undefined ? options.once : true;
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
callback(entry);
|
||||
if (once && observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold, rootMargin, root: options.root || null }
|
||||
);
|
||||
|
||||
if (targetRef.value) {
|
||||
observer.observe(targetRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
targetRef
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an intersection observer mixin (backward compatibility)
|
||||
* @param {Object} options - Observer options
|
||||
* @returns {Object} - Vue mixin
|
||||
*/
|
||||
export function createIntersectionObserverMixin(options = {}) {
|
||||
return {
|
||||
data() {
|
||||
return {
|
||||
isVisible: false,
|
||||
hasBeenVisible: false,
|
||||
observer: null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.setupObserver();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setupObserver() {
|
||||
const threshold = options.threshold || 0.1;
|
||||
const rootMargin = options.rootMargin || '0px';
|
||||
|
||||
this.observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
this.isVisible = entry.isIntersecting;
|
||||
if (entry.isIntersecting && !this.hasBeenVisible) {
|
||||
this.hasBeenVisible = true;
|
||||
if (this.onFirstVisible) {
|
||||
this.onFirstVisible();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold, rootMargin }
|
||||
);
|
||||
|
||||
if (this.$refs.root) {
|
||||
this.observer.observe(this.$refs.root);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user