import debounce from 'lodash/debounce' import { ref, watch, type Ref } from 'vue' import { asyncDebounce } from '../helpers' import { type QuerySerializationStrategy } from '../types/QuerySerializationStrategies/QuerySerializationStrategy' import type ApiInterface from '../types/api-interface' import type DataProviderInterface from '../types/data-provider-interface' import { checkEndpointOptionAndCreateQueryString, createQueryCommaString } from '../utils' import { ArkUiConstants } from './use-ark-ui' export interface OnLoadEventInterface { allItems: any[] loadedItems: any[] unloadedItems: any[] } export interface PaginatedApiOptions { endpoint: string countEndpoint?: string useAsyncCountLoad?: boolean params: Ref> api?: ApiInterface dataProvider?: DataProviderInterface inMemorySize?: number sortField?: Ref paginationType?: string requestSize?: Ref paramsSerialization?: paramsSerializationOptions usePrevNextFlags?: boolean prevPageFlagKey?: string nextPageFlagKey?: string sortFieldKey?: string requestPageKey?: string responseItemsKey?: string responseTotalKey?: string requestPerPageKey?: string itemConverter?: (item: any) => T autoReload?: boolean queryParamsSerializationStrategy?: QuerySerializationStrategy onLoadPrev?: Array<(options: OnLoadEventInterface) => void> onLoadNext?: Array<(options: OnLoadEventInterface) => void> onInitialLoad?: Array<(options: OnLoadEventInterface) => void> debug?: boolean } export type paramsSerializationOptions = 'default' | 'custom' | 'comma' export default function useEndlessScrollApi(options: PaginatedApiOptions) { const defaultItemConverter = (item: any) => item const api = options.api || ArkUiConstants.api const sortField: Ref = options.sortField || ref('id') const requestSize = options.requestSize || ref(10) const inMemorySize = options.inMemorySize || 3 * requestSize.value const paginationType = options.paginationType || ArkUiConstants.paginationType const autoReload = typeof options.autoReload === 'undefined' ? true : options.autoReload const sortFieldKey = options.sortFieldKey || ArkUiConstants.sortFieldKey const requestPageKey = options.requestPageKey || ArkUiConstants.requestPageKey const responseItemsKey = options.responseItemsKey || ArkUiConstants.responseItemsKey const responseTotalKey = options.responseTotalKey || ArkUiConstants.responseTotalKey const requestPerPageKey = options.requestPerPageKey || ArkUiConstants.requestPerPageKey const paramsSerialization = options.paramsSerialization || 'default' const itemConverter = options.itemConverter || defaultItemConverter const usePrevNextFlags = options.usePrevNextFlags || ArkUiConstants.usePrevNextFlags const prevPageFlagKey = options.prevPageFlagKey || ArkUiConstants.prevPageFlagKey const nextPageFlagKey = options.nextPageFlagKey || ArkUiConstants.nextPageFlagKey if (!api && !options.dataProvider) throw new Error('API is not defined') const offset = ref(0) const count = ref(0) const isLoading = ref(false) const isCountLoading = ref(false) const items: Ref = ref([]) const actualRequestId = ref(0) const actualRequestCountId = ref(0) const startOffset = ref(0) const endOffset = ref(requestSize.value) const hasPrevPage = ref(false) const hasNextPage = ref(false) const clear = () => { items.value = [] offset.value = 0 startOffset.value = 0 endOffset.value = requestSize.value } const getQueryParams = (limit: number, offset: number) => { const queryParams: { [code: string]: string | string[] | number } = { ...options.params.value, [sortFieldKey]: sortField.value, } if (paginationType === 'page') { queryParams[requestPageKey] = Math.floor(offset / requestSize.value) + 1 queryParams[requestPerPageKey] = requestSize.value } else { queryParams.limit = limit queryParams.offset = offset } return queryParams } const serializeParams = (endpoint: string, limit: number, offset: number): [string, { params?: any }] => { const queryParams = getQueryParams(limit, offset) if (options.queryParamsSerializationStrategy) { return [ endpoint + options.queryParamsSerializationStrategy.serialize(queryParams), {}, ] } switch (paramsSerialization) { case 'default': return [endpoint, { params: queryParams }] case 'custom': return [checkEndpointOptionAndCreateQueryString(endpoint, queryParams), {}] case 'comma': return [endpoint + createQueryCommaString(queryParams), {}] default: break } throw new Error('Не задана стратегия сериализации параметров запроса') } const getCount = async () => { if (!options.countEndpoint && !options.dataProvider?.getCount) return const doRequest = async () => { if (options.dataProvider?.getCount) { return Number(await options.dataProvider.getCount({ params: getQueryParams(0, 0) })) } if (api) { return Number((await api.get(...serializeParams(options.countEndpoint || '', 0, 0))).data) } throw new Error('Для метода getCount не задан ни api, ни dataProvider.getCount') } const requestId = Date.now() actualRequestCountId.value = requestId isCountLoading.value = true const response = await doRequest() if (actualRequestCountId.value === requestId) { count.value = response isCountLoading.value = false } } const load = async (offset: number, limit: number): Promise => { const requestId = Date.now() actualRequestId.value = requestId isLoading.value = true let response: any if (options.dataProvider) response = await options.dataProvider.get({ params: getQueryParams(limit, offset) }) else { if (!api) throw new Error('API is not defined') response = (await api.get(...serializeParams(options.endpoint, limit, offset))).data } if (requestId !== actualRequestId.value) { return undefined } isLoading.value = false // пересчитываем количество элементов только при первом запросе if (!usePrevNextFlags && !options.countEndpoint && offset === 0) count.value = response[responseTotalKey] if (usePrevNextFlags) { hasNextPage.value = response[nextPageFlagKey] hasPrevPage.value = response[prevPageFlagKey] } return response[responseItemsKey].map((i: any) => itemConverter(i)) } const loadNext = async () => { if (isLoading.value) return false if (!usePrevNextFlags && (endOffset.value >= count.value || offset.value > count.value)) return false if (usePrevNextFlags && !hasNextPage.value) return false const loadedItems = await load(offset.value + requestSize.value, requestSize.value) if (!loadedItems) return false const processedItems = [...items.value, ...loadedItems] let unloadedItems: any[] = [] if (endOffset.value >= inMemorySize) { unloadedItems = processedItems.splice(0, loadedItems.length) startOffset.value += loadedItems.length hasPrevPage.value = true } else { hasPrevPage.value = false } if (options.onLoadNext) { options.onLoadNext.forEach((f) => f({ allItems: items.value, loadedItems, unloadedItems })) } items.value = processedItems offset.value += paginationType === 'limit' ? loadedItems.length : requestSize.value endOffset.value += loadedItems.length return true } const loadPrev = async () => { if (isLoading.value) return false if (!usePrevNextFlags && startOffset.value < requestSize.value) return false if (usePrevNextFlags && !hasPrevPage.value) return false const overloadedItems = endOffset.value % requestSize.value const loadedItems = await load(startOffset.value - requestSize.value - overloadedItems, requestSize.value + overloadedItems) if (!loadedItems) return false const processedItems = [...loadedItems, ...items.value] const unloadedItems = processedItems.splice(processedItems.length - loadedItems.length, loadedItems.length + overloadedItems) if (options.onLoadPrev) { options.onLoadPrev.forEach((f) => f({ allItems: items.value, loadedItems, unloadedItems })) } items.value = processedItems offset.value -= unloadedItems.length % requestSize.value ? requestSize.value * 2 : requestSize.value startOffset.value -= unloadedItems.length endOffset.value -= unloadedItems.length hasNextPage.value = true return true } const initialLoad = async () => { if (!options.useAsyncCountLoad) await getCount() const loadedItems = await load(0, requestSize.value) if (loadedItems) { items.value = loadedItems if (options.onInitialLoad) { options.onInitialLoad.forEach((f) => f({ allItems: items.value, loadedItems, unloadedItems: [] })) } } } const reloadFunc = async () => { await clear() if (!options.useAsyncCountLoad) { await getCount() } else { getCount() } await initialLoad() } const reload = asyncDebounce(reloadFunc, 500) as unknown as () => Promise const refresh = debounce(async () => { const loadedItems = await load(offset.value, inMemorySize) if (loadedItems) { items.value = loadedItems } }, 500) if (autoReload) { watch([options.params, sortField], reload) } if (options.countEndpoint && options.useAsyncCountLoad) getCount() return { loadNext, loadPrev, reload, refresh, load, initialLoad, clear, items, isLoading, isCountLoading, count, offset, startOffset, endOffset, getCount, hasNextPage, hasPrevPage, } }