@oruga-ui/oruga-next
Version:
UI components for Vue.js and CSS framework agnostic
261 lines (231 loc) • 8.14 kB
text/typescript
import { toValue, type MaybeRefOrGetter } from "vue";
import { isEqual } from "@/utils/helpers";
import { type Indexer } from "./useIndexer";
// --- PUBLIC API ---
/**
* An real option item object which get passed by an options property.
*
* @public
*/
export type Option<T extends object> = {} & T & Record<string, any>;
/**
* Simplified types of options that can be passed to the options prop.
*
* @public
*/
export type SimpleOptions =
| (string | number)[]
| Record<string | number, string>;
/**
* The types of options that can be passed to the options prop.
*
* @public
*/
export type OptionsProp<T extends object = object> =
| SimpleOptions
| Option<T>[];
/**
* Option groups should always be formatted as an array of group objects with nested options.
*
* @public
*/
export type OptionsGroupsProp<T extends object = object> = (Option<T> & {
/** list of options in this group */
options: OptionsProp<T>;
})[];
/**
* A list of options that can either be a list of option items or a list of group items.
*
* @public
*/
export type OptionsOrGroupsProp<T extends object = object> =
| OptionsProp<T>
| OptionsGroupsProp<T>;
// --- INTERNAL API ---
/**
* Internal option item representation wrapper with additional information.
* @internal
*/
export type OptionItem<T extends object> = {
/** internal genereated uniqe option key */
key: string;
/** the option item object */
item: Option<T>;
};
/**
* Internal option group item representation wrapper with additional information.
* @internal
*/
export type OptionGroupItem<T extends object> = {
/** internal genereated uniqe option key */
key: string;
/** the option item object */
item: Option<T>;
/** list of options */
options: OptionItem<T>[];
};
// ------------------
type SimpleOption = { label: string; value: string | number };
type NormalizedOption<T extends object> = OptionItem<T>;
type NormalizedGroup<T extends object> = Omit<OptionGroupItem<T>, "options"> & {
options: NormalizedOption<T>[];
};
type NormalizedItem<T extends object = any> =
| NormalizedOption<T>
| NormalizedGroup<T>;
/**
* A function to normalize an array of objects, array of strings, or object of
* key-value pairs to an array of internal option items.
*
* @param options - An un-normalized array of options.
* @param indexer - An indexer to generate unique keys for each option.
* @returns A list of normalized option items.
*/
export function normalizeOptions<T extends object>(
options: OptionsProp<T> | undefined,
indexer: Indexer,
groupable?: false,
): NormalizedOption<T>[];
export function normalizeOptions<T extends object>(
options: OptionsOrGroupsProp<T> | undefined,
indexer: Indexer,
groupable: boolean,
): NormalizedItem<T>[];
export function normalizeOptions<T extends object>(
options: OptionsProp<T> | OptionsGroupsProp<T> | undefined,
indexer: Indexer,
groupable: boolean = false,
): NormalizedItem<T>[] {
if (!options) return [] as NormalizedOption<T>[];
if (Array.isArray(options)) {
return options.map(
(
option:
| (string | number)
| Option<T>
| OptionsGroupsProp<T>[number],
) => {
if (typeof option === "string" || typeof option === "number")
// create options item from primitive
return {
item: {
label: String(option),
value: option,
},
key: indexer.nextIndex(),
} satisfies NormalizedOption<SimpleOption>;
if (groupable && "options" in option) {
const key = indexer.nextIndex();
const options = normalizeOptions(
option.options,
indexer,
) as NormalizedOption<T>[];
const item = { ...toValue(option) };
delete item.options; // delete options from item to prevent loop
// create group options item
return {
item,
options,
key,
} satisfies NormalizedGroup<T>;
}
// create options item
return {
item: toValue(option),
key: indexer.nextIndex(),
};
},
) as NormalizedItem<T>[];
}
// options are from type SimpleOption and is an object
return Object.keys(options).map(
(value: string | number) =>
({
// create option from object key/value
item: {
label: options[value],
value,
},
key: indexer.nextIndex(),
}) satisfies NormalizedOption<SimpleOption>,
) as unknown as NormalizedOption<T>[];
}
/**
* Checks if the given normalized option item is an option group or not.
*
* @param options - An option item to check.
* @returns True if the option is a group; otherwise, false.
*/
export function isGroupOption<T extends object>(
option: NormalizedItem<T>,
): option is NormalizedGroup<T> {
return "options" in option;
}
/**
* Determines if a normalized options list contains groups or not.
*
* @param options - An array of individual options or grouped options.
* @returns True if the options are grouped; otherwise, false.
*/
export function areOptionsGrouped(
options: MaybeRefOrGetter<NormalizedItem[]>,
): boolean {
const _options = toValue(options);
if (!_options?.length) return false;
return isGroupOption(_options[0]);
}
/**
* Recursively finds the index of a specific option within a flat or grouped options array.
* It traverses the structure and returns the index of the target option as if all options were flattened.
*
* @param options - A list of options, which may be a flat array or an array of grouped options.
* @param option - The option to find within the options list.
* @returns The index of the option if found; otherwise, -1.
*/
export function findOptionIndex<T extends object>(
options: MaybeRefOrGetter<NormalizedItem<T>[]>,
option: MaybeRefOrGetter<NormalizedOption<T>>,
): number {
if (!Array.isArray(toValue(options))) return -1;
const optionItem = toValue(option).item;
let idx = 0;
for (const item of toValue(options)) {
if (typeof item !== "object" && item) continue;
// check if the first item has an options propery which defines it as group
if (isGroupOption(item)) {
// check options in group options
const groupIdx = findOptionIndex(item.options, option);
// increase full group options length when not found
if (groupIdx === -1) idx += item.options.length;
else {
// increase found index of group options
idx += groupIdx;
return idx;
}
}
// check if option is searched option
else if (isEqual(item.item, optionItem)) return idx;
// else increase search indx
else idx += 1;
}
// not matching option found
return -1;
}
/**
* Recursively calculates the total number of options from a list of options or grouped options.
*
* @param options - An array of individual options or grouped options.
* @returns The total count of individual options, including those nested within groups.
*/
export function getOptionsLength<T extends object>(
options: MaybeRefOrGetter<NormalizedItem<T>[]>,
): number {
if (!Array.isArray(toValue(options))) return 0;
return toValue(options).reduce((length, item) => {
if (item && typeof item !== "object") return length;
if (isGroupOption(item)) {
return length + getOptionsLength(item.options);
}
return length + 1;
}, 0);
}