UNPKG

@oruga-ui/oruga-next

Version:

UI components for Vue.js and CSS framework agnostic

286 lines (250 loc) 9 kB
import { computed, getCurrentInstance, inject, onUnmounted, provide, ref, watch, type Component, type ComputedRef, type MaybeRefOrGetter, type Ref, } from "vue"; import { unrefElement } from "./unrefElement"; import { useIndexer } from "./useIndexer"; export type ProviderItem<T = unknown> = { /** The root element of the item component */ el: MaybeRefOrGetter<HTMLElement | null>; /** The index of the item component in the parent */ index: number; /** A unique identifier for the item component */ identifier: string; /** Item component data */ data: T; }; type PovidedData<P, I = unknown> = { registerItem: ( el: MaybeRefOrGetter<HTMLElement | null>, data: MaybeRefOrGetter<I>, ) => ProviderItem<I>; unregisterItem: (item: ProviderItem) => void; total: ComputedRef<number>; data?: ComputedRef<P>; }; type ProviderParentOptions<T = unknown> = { /** * Root element of the provider component */ rootRef?: MaybeRefOrGetter<HTMLElement | Component | null | undefined>; /** * Override the provide/inject key. * Default is the component configField attribute */ key?: string | symbol; /** * Additional data provided for the child to the item */ data?: ComputedRef<T>; }; /** * Provide functionalities and data to child components * @param options parent provider options */ export function useProviderParent<ItemData = undefined, ParentData = unknown>( options?: ProviderParentOptions<ParentData>, ): { childItems: Readonly<Ref<ProviderItem<ItemData>[]>>; itemsCount: ComputedRef<number>; } { // getting a hold of the internal instance in setup() const vm = getCurrentInstance(); if (!vm) throw new Error( "useProviderChild must be called within a component setup function.", ); const configField = String(vm.proxy?.$options.configField); const key = (typeof options?.key === "symbol" ? options.key?.toString() : options?.key) || configField; const childItems = ref<ProviderItem<ItemData>[]>([]); const total = computed<number>(() => childItems.value.length); if (options?.rootRef) { /** Sort child items according to their DOM position */ function sortItems(items: typeof childItems.value): void { const parent = unrefElement(options?.rootRef); if (!parent) return; // create a list of child item ids const ids = items .map((item) => `[data-id="${key}-${item.identifier}"]`) .join(","); if (!ids) return; // query all child items in the order of the DOM appearance const children = parent.querySelectorAll(ids); // create a list of ids ordered after the elements in DOM const sortedIds = Array.from(children).map((el) => el.getAttribute("data-id")?.replace(`${key}-`, ""), ); // update the index attribute of the child items items.forEach( (item) => (item.index = sortedIds.indexOf(`${item.identifier}`)), ); // sort items according to their index position items.sort((a, b) => a.index - b.index); } // sort items when child items list get updated (no deep change - only list update) // use flush: "post" to ensure DOM is updated and batch updates by a Vue tick watch(childItems, sortItems, { flush: "post" }); } const { nextIndex } = useIndexer(1); /** register a child item on the parent */ function registerItem( el: MaybeRefOrGetter<HTMLElement | null>, data: MaybeRefOrGetter<ItemData>, ): ProviderItem<ItemData> { const index = childItems.value.length; const identifier = nextIndex(); const item = { el, index, identifier, data }; // add new item to the child list // this unwraps all inner refs childItems.value = [ ...childItems.value, item, ] as ProviderItem<ItemData>[]; return item as ProviderItem<ItemData>; } /** unregister a child item on the parent */ function unregisterItem(item: ProviderItem): void { childItems.value = childItems.value.filter( (i) => i.identifier !== item.identifier, ); } /** Provide functionality for child components via dependency injection. */ provide<PovidedData<ParentData, ItemData>>("$o-" + key, { registerItem, unregisterItem, total: total, data: options?.data, }); return { childItems: childItems as Ref<ProviderItem<ItemData>[]>, itemsCount: total, }; } type ProviderChildOptions<T = unknown> = { /** * Override the provide/inject key. * Default is the component configField attribute */ key?: string | symbol; /** * Does the child need the be below the parent? * @default true */ needParent?: boolean; /** * Additional data appended to the item */ data?: ComputedRef<T>; /** * Register child on parent * @default true */ register?: boolean; }; export function useProviderChild<ParentData = undefined, ItemData = unknown>( el: MaybeRefOrGetter<HTMLElement | null>, options: Omit<ProviderChildOptions<ItemData>, "needParent"> & { needParent: true; }, ): { parent: ComputedRef<ParentData>; item: Readonly<Ref<ProviderItem<ItemData> | undefined>>; itemsCount: ComputedRef<number>; }; export function useProviderChild<ParentData = undefined, ItemData = unknown>( el: MaybeRefOrGetter<HTMLElement | null>, options: Omit<ProviderChildOptions<ItemData>, "needParent"> & { needParent: false; }, ): { parent: ComputedRef<ParentData | undefined>; item: Readonly<Ref<ProviderItem<ItemData> | undefined>>; itemsCount: ComputedRef<number>; }; export function useProviderChild<ParentData = undefined, ItemData = unknown>( el: MaybeRefOrGetter<HTMLElement | null>, options: Omit<ProviderChildOptions<ItemData>, "needParent"> & { register: false; }, ): { parent: ComputedRef<ParentData>; item: Readonly<Ref<undefined>>; itemsCount: ComputedRef<number>; }; export function useProviderChild<ParentData = undefined, ItemData = unknown>( el: MaybeRefOrGetter<HTMLElement | null>, options: Omit<ProviderChildOptions<ItemData>, "needParent" | "register"> & { needParent: true; register: true; }, ): { parent: ComputedRef<ParentData>; item: Readonly<Ref<ProviderItem<ItemData>>>; itemsCount: ComputedRef<number>; }; export function useProviderChild<ParentData = undefined, ItemData = undefined>( el: MaybeRefOrGetter<HTMLElement | null>, options?: Omit<ProviderChildOptions<ItemData>, "needParent" | "register">, ): { parent: ComputedRef<ParentData>; item: Readonly<Ref<ProviderItem<ItemData>>>; itemsCount: ComputedRef<number>; }; /** * Inject functionalities and data from parent components * @param options additional options */ export function useProviderChild<ParentData = undefined, ItemData = unknown>( el: MaybeRefOrGetter<HTMLElement | null>, options?: ProviderChildOptions<ItemData>, ): { parent: ComputedRef<ParentData | undefined>; item: Readonly<Ref<ProviderItem | undefined>>; itemsCount: ComputedRef<number>; } { options = Object.assign({ needParent: true, register: true }, options); // getting a hold of the internal instance in setup() const vm = getCurrentInstance(); if (!vm) throw new Error( "useProviderChild must be called within a component setup function.", ); const configField = String(vm.proxy?.$options.configField); const key = (typeof options?.key === "symbol" ? options.key.toString() : options?.key) || configField; /** Inject parent component functionality if used inside one **/ const parent = inject<PovidedData<ParentData, ItemData> | undefined>( "$o-" + key, undefined, ); if (options.needParent && !parent) throw new Error( `You should wrap ${vm.proxy?.$options.name} in a ${key} component`, ); const data = options.data ?? computed(() => undefined as ItemData); const parentData = parent?.data ?? computed(() => undefined); const total = parent?.total ?? computed(() => 0); const item = ref<ProviderItem<ItemData>>(); // register item at parent if (parent && options.register) item.value = parent.registerItem(el, data); onUnmounted(() => { // unregister item at parent on item unmount if (parent && item.value) parent.unregisterItem(item.value); }); return { parent: parentData, item: item, itemsCount: total }; }