Profectus-Demo/src/util/vue.tsx

187 lines
5.9 KiB
TypeScript

/* eslint-disable vue/multi-word-component-names */
// ^ I have no idea why that's necessary; the rule is disabled, and this file isn't a vue component?
// I'm _guessing_ it's related to us using DefineComponent, but I figured that eslint rule should
// only apply to SFCs
import Col from "components/layout/Column.vue";
import Row from "components/layout/Row.vue";
import { getUniqueID, Visibility } from "features/feature";
import VueFeatureComponent from "features/VueFeature.vue";
import { MaybeGetter, processGetter } from "util/computed";
import type { CSSProperties, MaybeRef, MaybeRefOrGetter, Ref } from "vue";
import { isRef, onUnmounted, ref, toValue } from "vue";
import { JSX } from "vue/jsx-runtime";
import { camelToKebab } from "./common";
export const VueFeature = Symbol("VueFeature");
export type Renderable = JSX.Element | string;
export type Wrapper = (el: () => Renderable) => Renderable;
export interface VueFeatureOptions {
/** Whether this feature should be visible. */
visibility?: MaybeRefOrGetter<Visibility | boolean>;
/** Dictionary of CSS classes to apply to this feature. */
classes?: MaybeRefOrGetter<Record<string, boolean>>;
/** CSS to apply to this feature. */
style?: MaybeRefOrGetter<CSSProperties>;
}
export interface VueFeature {
/** An auto-generated ID for identifying features that appear in the DOM. Will not persist between refreshes or updates. */
id: string;
/** Whether this feature should be visible. */
visibility?: MaybeRef<Visibility | boolean>;
/** Dictionary of CSS classes to apply to this feature. */
classes?: MaybeRef<Record<string, boolean>>;
/** CSS to apply to this feature. */
style?: MaybeRef<CSSProperties>;
/** The components to render inside the vue feature */
components: MaybeGetter<Renderable>[];
/** The components to render wrapped around the vue feature */
wrappers: Wrapper[];
/** Used to identify Vue Features */
[VueFeature]: true;
}
export function vueFeatureMixin(
featureName: string,
options: VueFeatureOptions,
component?: MaybeGetter<Renderable>
) {
return {
id: getUniqueID(featureName),
visibility: processGetter(options.visibility),
classes: processGetter(options.classes),
style: processGetter(options.style),
components: component == null ? [] : [component],
wrappers: [] as Wrapper[],
[VueFeature]: true
} satisfies VueFeature;
}
export function render(object: VueFeature, wrapper?: (el: Renderable) => Renderable): JSX.Element;
export function render<T extends Renderable>(
object: MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => T
): T;
export function render(
object: VueFeature | MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => Renderable
): Renderable;
export function render(
object: VueFeature | MaybeGetter<Renderable>,
wrapper?: (el: Renderable) => Renderable
) {
if (typeof object === "object" && VueFeature in object) {
const { id, visibility, style, classes, components, wrappers } = object;
return (
<VueFeatureComponent
id={id}
visibility={visibility}
style={style}
classes={classes}
components={components}
wrappers={wrappers}
/>
);
}
object = toValue(object);
return wrapper?.(object) ?? object;
}
export function renderRow(...objects: (VueFeature | MaybeGetter<Renderable>)[]): JSX.Element {
return <Row>{objects.map(obj => render(obj))}</Row>;
}
export function renderCol(...objects: (VueFeature | MaybeGetter<Renderable>)[]): JSX.Element {
return <Col>{objects.map(obj => render(obj))}</Col>;
}
export function joinJSX(
objects: (VueFeature | MaybeGetter<Renderable>)[],
joiner: JSX.Element
): JSX.Element {
return objects.reduce<JSX.Element>(
(acc, curr) => (
<>
{acc}
{joiner}
{render(curr)}
</>
),
<></>
);
}
export function isJSXElement(element: unknown): element is JSX.Element {
return (
element != null && typeof element === "object" && "type" in element && "children" in element
);
}
export function setupHoldToClick(callback: (e?: MouseEvent | TouchEvent) => void): {
start: (e: MouseEvent | TouchEvent) => void;
stop: VoidFunction;
handleHolding: VoidFunction;
} {
const interval = ref<NodeJS.Timeout | null>(null);
const event = ref<MouseEvent | TouchEvent | undefined>(undefined);
function start(e: MouseEvent | TouchEvent) {
if (interval.value == null) {
interval.value = setInterval(handleHolding, 250);
}
event.value = e;
}
function stop() {
if (interval.value != null) {
clearInterval(interval.value);
interval.value = null;
}
}
function handleHolding() {
callback(event.value);
}
onUnmounted(stop);
return { start, stop, handleHolding };
}
export function setRefValue<T>(ref: Ref<T | Ref<T>>, value: T) {
if (isRef(ref.value)) {
ref.value.value = value;
} else {
ref.value = value;
}
}
export type PropTypes =
| typeof Boolean
| typeof String
| typeof Number
| typeof Function
| typeof Object
| typeof Array;
export function trackHover(element: VueFeature): Ref<boolean> {
const isHovered = ref(false);
(element as unknown as { onPointerenter: VoidFunction }).onPointerenter = () =>
(isHovered.value = true);
(element as unknown as { onPointerleave: VoidFunction }).onPointerleave = () =>
(isHovered.value = true);
return isHovered;
}
export function kebabifyObject(object: Record<string, unknown>) {
return Object.keys(object).reduce(
(acc, curr) => {
acc[camelToKebab(curr)] = object[curr];
return acc;
},
{} as Record<string, unknown>
);
}