/* 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 interface VueFeatureOptions { /** Whether this feature should be visible. */ visibility?: MaybeRefOrGetter; /** Dictionary of CSS classes to apply to this feature. */ classes?: MaybeRefOrGetter>; /** CSS to apply to this feature. */ style?: MaybeRefOrGetter; } 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; /** Dictionary of CSS classes to apply to this feature. */ classes?: MaybeRef>; /** CSS to apply to this feature. */ style?: MaybeRef; /** The components to render inside the vue feature */ components: MaybeGetter[]; /** The components to render wrapped around the vue feature */ wrappers: ((el: () => Renderable) => Renderable)[]; /** Used to identify Vue Features */ [VueFeature]: true; } export function vueFeatureMixin( featureName: string, options: VueFeatureOptions, component?: MaybeGetter ) { return { id: getUniqueID(featureName), visibility: processGetter(options.visibility), classes: processGetter(options.classes), style: processGetter(options.style), components: component == null ? [] : [component], wrappers: [] as ((el: () => Renderable) => Renderable)[], [VueFeature]: true } satisfies VueFeature; } export function render(object: VueFeature, wrapper?: (el: Renderable) => Renderable): JSX.Element; export function render( object: MaybeGetter, wrapper?: (el: Renderable) => T ): T; export function render( object: VueFeature | MaybeGetter, wrapper?: (el: Renderable) => Renderable ): Renderable; export function render( object: VueFeature | MaybeGetter, wrapper?: (el: Renderable) => Renderable ) { if (typeof object === "object" && VueFeature in object) { const { id, visibility, style, classes, components, wrappers } = object; return ( ); } object = toValue(object); return wrapper?.(object) ?? object; } export function renderRow(...objects: (VueFeature | MaybeGetter)[]): JSX.Element { return {objects.map(obj => render(obj))}; } export function renderCol(...objects: (VueFeature | MaybeGetter)[]): JSX.Element { return {objects.map(obj => render(obj))}; } export function joinJSX( objects: (VueFeature | MaybeGetter)[], joiner: JSX.Element ): JSX.Element { return objects.reduce( (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( onClick?: Ref<((e?: MouseEvent | TouchEvent) => void) | undefined>, onHold?: Ref ): { start: (e: MouseEvent | TouchEvent) => void; stop: VoidFunction; handleHolding: VoidFunction; } { const interval = ref(null); const event = ref(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() { if (onHold && onHold.value) { onHold.value(); } else if (onClick && onClick.value) { onClick.value(event.value); } } onUnmounted(stop); return { start, stop, handleHolding }; } export function setRefValue(ref: Ref>, 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 { 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) { return Object.keys(object).reduce( (acc, curr) => { acc[camelToKebab(curr)] = object[curr]; return acc; }, {} as Record ); }