Profectus/src/features/feature.ts
thepaperpilot 67ca253f5c
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s
Run Tests / test (push) Successful in 1m13s
Add docs and otherwise improve how docs will generate
2024-12-31 13:04:20 +00:00

137 lines
5.8 KiB
TypeScript

import Decimal from "util/bignum";
import { Renderable, renderCol, VueFeature } from "util/vue";
import { computed, isRef, MaybeRef, Ref, unref } from "vue";
let id = 0;
/**
* Gets a unique ID to give to each feature, used for any sort of system that needs to identify
* elements in the DOM rather than references to the feature itself. (For example, branches)
* IDs are guaranteed unique, but _NOT_ persistent - they likely will change between updates.
* @param prefix A string to prepend to the id to make it more readable in the inspector tools
*/
export function getUniqueID(prefix = "feature-"): string {
return prefix + id++;
}
/** Enum for what the visibility of a feature or component should be */
export enum Visibility {
/** The feature or component should be visible */
Visible,
/** The feature or component should not appear but still take up space */
Hidden,
/** The feature or component should not appear not take up space */
None
}
/**
* Utility function for determining if a visibility value is anything but Visibility.None.
Booleans are allowed and false will be considered to be Visibility.None.
* @param visibility The ref to either a visibility value or boolean
* @returns True if the visibility is either true, Visibility.Visible, or Visibility.Hidden
*/
export function isVisible(visibility: MaybeRef<Visibility | boolean>) {
const currVisibility = unref(visibility);
return currVisibility !== Visibility.None && currVisibility !== false;
}
/**
* Utility function for determining if a visibility value is Visibility.Hidden.
Booleans are allowed but will never be considered to be Visible.Hidden.
* @param visibility The ref to either a visibility value or boolean
* @returns True if the visibility is Visibility.Hidden
*/
export function isHidden(visibility: MaybeRef<Visibility | boolean>) {
const currVisibility = unref(visibility);
return currVisibility === Visibility.Hidden;
}
/**
* Utility function for narrowing something that may or may not be a specified type of feature.
* Works off the principle that all features have a unique symbol to identify themselves with.
* @param object The object to determine whether or not is of the specified type
* @param type The symbol to look for in the object's "type" property
* @returns Whether or not the object is the specified type
*/
export function isType<T extends symbol>(object: unknown, type: T): object is { type: T } {
return object != null && typeof object === "object" && "type" in object && object.type === type;
}
/**
* Traverses an object and returns all features of the given type(s)
* @param obj The object to traverse
* @param types The feature types that will be searched for
*/
export function findFeatures(obj: object, ...types: symbol[]): unknown[] {
const objects: unknown[] = [];
const handleObject = (obj: object) => {
Object.keys(obj).forEach(key => {
const value: unknown = obj[key as keyof typeof obj];
if (
value != null &&
typeof value === "object" &&
(value as Record<string, unknown>).__v_isVNode !== true
) {
if (types.includes((value as Record<string, unknown>).type as symbol)) {
objects.push(value);
} else if (!(value instanceof Decimal) && !isRef(value)) {
handleObject(value as Record<string, unknown>);
}
}
});
};
handleObject(obj);
return objects;
}
/**
* Utility function for taking a list of features and filtering them out, but keeping a reference to the first filtered out feature. Used for having a collapsible of the filtered out content, with the first filtered out item remaining outside the collapsible for easy reference.
* @param features The list of features to search through
* @param filter The filter to use to determine features that shouldn't be collapsible
* @returns An object containing a ref to the first filtered _out_ feature, a render function for the collapsed content, and a ref for whether or not there is any collapsed content to show
*/
export function getFirstFeature<T extends VueFeature>(
features: T[],
filter: (feature: T) => boolean
): {
firstFeature: Ref<T | undefined>;
collapsedContent: () => Renderable;
hasCollapsedContent: Ref<boolean>;
} {
const filteredFeatures = computed(() =>
features.filter(feature => isVisible(feature.visibility ?? true) && filter(feature))
);
return {
firstFeature: computed(() => filteredFeatures.value[0]),
collapsedContent: () => renderCol(...filteredFeatures.value.slice(1)),
hasCollapsedContent: computed(() => filteredFeatures.value.length > 1)
};
}
/**
* Traverses an object and returns all features that are _not_ any of the given types.
* Features are any object with a "type" property that has a symbol value.
* @param obj The object to traverse
* @param types The feature types that will be skipped over
*/
export function excludeFeatures(obj: Record<string, unknown>, ...types: symbol[]): unknown[] {
const objects: unknown[] = [];
const handleObject = (obj: Record<string, unknown>) => {
Object.keys(obj).forEach(key => {
const value = obj[key];
if (
value != null &&
typeof value === "object" &&
(value as Record<string, unknown>).__v_isVNode !== true
) {
const type = (value as Record<string, unknown>).type;
if (typeof type === "symbol" && !types.includes(type)) {
objects.push(value);
} else if (!(value instanceof Decimal) && !isRef(value)) {
handleObject(value as Record<string, unknown>);
}
}
});
};
handleObject(obj);
return objects;
}