Profectus/src/util/vue.tsx

254 lines
7.9 KiB
TypeScript
Raw Normal View History

2022-03-04 03:39:48 +00:00
import Col from "components/layout/Column.vue";
import Row from "components/layout/Row.vue";
2022-06-27 00:17:22 +00:00
import type { CoercableComponent, GenericComponent, JSXFunction } from "features/feature";
import { Component as ComponentKey, GatherProps, jsx, Visibility } from "features/feature";
import type { ProcessedComputable } from "util/computed";
import { DoNotCache } from "util/computed";
import type { Component, ComputedRef, DefineComponent, PropType, Ref, ShallowRef } from "vue";
2022-01-14 04:25:47 +00:00
import {
2022-01-25 04:25:34 +00:00
computed,
defineComponent,
isRef,
2022-03-11 23:01:22 +00:00
onUnmounted,
2022-01-25 04:25:34 +00:00
ref,
shallowRef,
toRef,
2022-01-25 04:25:34 +00:00
unref,
watchEffect
2022-01-25 04:25:34 +00:00
} from "vue";
2022-01-14 04:25:47 +00:00
export function coerceComponent(
component: CoercableComponent,
defaultWrapper = "span"
): DefineComponent {
if (typeof component === "function") {
return defineComponent({ render: component });
}
2022-01-14 04:25:47 +00:00
if (typeof component === "string") {
if (component.length > 0) {
component = component.trim();
if (component.charAt(0) !== "<") {
component = `<${defaultWrapper}>${component}</${defaultWrapper}>`;
}
2022-01-14 04:25:47 +00:00
return defineComponent({ template: component });
}
return defineComponent({ render: () => ({}) });
2022-01-14 04:25:47 +00:00
}
return component;
}
export type VueFeature = {
[ComponentKey]: GenericComponent;
[GatherProps]: () => Record<string, unknown>;
};
export function render(object: VueFeature | CoercableComponent): JSX.Element | DefineComponent {
if (isCoercableComponent(object)) {
if (typeof object === "function") {
return (object as JSXFunction)();
2022-01-14 04:25:47 +00:00
}
return coerceComponent(object);
}
const Component = object[ComponentKey];
return <Component {...object[GatherProps]()} />;
2022-01-14 04:25:47 +00:00
}
export function renderRow(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
return <Row>{objects.map(render)}</Row>;
2022-01-14 04:25:47 +00:00
}
export function renderCol(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
return <Col>{objects.map(render)}</Col>;
2022-01-14 04:25:47 +00:00
}
export function renderJSX(object: VueFeature | CoercableComponent): JSX.Element {
if (isCoercableComponent(object)) {
if (typeof object === "function") {
return (object as JSXFunction)();
}
if (typeof object === "string") {
return <>{object}</>;
}
// TODO why is object typed as never?
const Comp = object as DefineComponent;
return <Comp />;
}
const Component = object[ComponentKey];
return <Component {...object[GatherProps]()} />;
}
export function renderRowJSX(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
return <Row>{objects.map(renderJSX)}</Row>;
}
export function renderColJSX(...objects: (VueFeature | CoercableComponent)[]): JSX.Element {
return <Col>{objects.map(renderJSX)}</Col>;
}
2022-01-14 04:25:47 +00:00
export function isCoercableComponent(component: unknown): component is CoercableComponent {
if (typeof component === "string") {
return true;
} else if (typeof component === "object") {
if (component == null) {
return false;
}
return "render" in component || "component" in component;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} else if (typeof component === "function" && (component as any)[DoNotCache] === true) {
return true;
2022-01-14 04:25:47 +00:00
}
return false;
}
export function setupHoldToClick(
2022-03-27 05:14:35 +00:00
onClick?: Ref<((e?: MouseEvent | TouchEvent) => void) | undefined>,
2022-01-14 04:25:47 +00:00
onHold?: Ref<VoidFunction | undefined>
): {
2022-03-27 05:14:35 +00:00
start: (e: MouseEvent | TouchEvent) => void;
2022-01-14 04:25:47 +00:00
stop: VoidFunction;
handleHolding: VoidFunction;
} {
const interval = ref<NodeJS.Timer | null>(null);
2022-03-27 05:14:35 +00:00
const event = ref<MouseEvent | TouchEvent | undefined>(undefined);
2022-01-14 04:25:47 +00:00
2022-03-27 05:14:35 +00:00
function start(e: MouseEvent | TouchEvent) {
2022-01-25 04:25:34 +00:00
if (!interval.value) {
interval.value = setInterval(handleHolding, 250);
2022-01-14 04:25:47 +00:00
}
2022-03-27 05:14:35 +00:00
event.value = e;
2022-01-14 04:25:47 +00:00
}
function stop() {
2022-01-25 04:25:34 +00:00
if (interval.value) {
clearInterval(interval.value);
interval.value = null;
2022-01-14 04:25:47 +00:00
}
}
function handleHolding() {
if (onHold && onHold.value) {
onHold.value();
} else if (onClick && onClick.value) {
2022-03-27 05:14:35 +00:00
onClick.value(event.value);
2022-01-14 04:25:47 +00:00
}
}
2022-03-11 23:01:22 +00:00
onUnmounted(stop);
2022-01-14 04:25:47 +00:00
return { start, stop, handleHolding };
}
2022-01-25 04:25:34 +00:00
export function getFirstFeature<
T extends VueFeature & { visibility: ProcessedComputable<Visibility> }
>(
2022-03-27 18:47:36 +00:00
features: T[],
filter: (feature: T) => boolean
): {
firstFeature: Ref<T | undefined>;
collapsedContent: JSXFunction;
hasCollapsedContent: Ref<boolean>;
} {
2022-03-27 18:47:36 +00:00
const filteredFeatures = computed(() =>
features.filter(
feature => unref(feature.visibility) === Visibility.Visible && filter(feature)
)
);
return {
firstFeature: computed(() => filteredFeatures.value[0]),
collapsedContent: jsx(() => renderCol(...filteredFeatures.value.slice(1))),
hasCollapsedContent: computed(() => filteredFeatures.value.length > 1)
2022-03-27 18:47:36 +00:00
};
}
2022-01-25 04:25:34 +00:00
export function computeComponent(
component: Ref<ProcessedComputable<CoercableComponent>>,
defaultWrapper = "div"
2022-04-23 20:23:38 +00:00
): ShallowRef<Component | ""> {
const comp = shallowRef<Component | "">();
watchEffect(() => {
comp.value = coerceComponent(unwrapRef(component), defaultWrapper);
2022-01-25 04:25:34 +00:00
});
2022-04-23 19:19:39 +00:00
return comp as ShallowRef<Component | "">;
2022-01-25 04:25:34 +00:00
}
export function computeOptionalComponent(
component: Ref<ProcessedComputable<CoercableComponent | undefined> | undefined>,
defaultWrapper = "div"
2022-04-23 20:23:38 +00:00
): ShallowRef<Component | "" | null> {
const comp = shallowRef<Component | "" | null>(null);
watchEffect(() => {
const currComponent = unwrapRef(component);
comp.value = currComponent == null ? null : coerceComponent(currComponent, defaultWrapper);
2022-01-25 04:25:34 +00:00
});
return comp;
2022-01-25 04:25:34 +00:00
}
export function wrapRef<T>(ref: Ref<ProcessedComputable<T>>): ComputedRef<T> {
return computed(() => unwrapRef(ref));
}
export function unwrapRef<T>(ref: Ref<ProcessedComputable<T>>): T {
return unref<T>(unref(ref));
}
export function setRefValue<T>(ref: Ref<T | Ref<T>>, value: T) {
if (isRef(ref.value)) {
ref.value.value = value;
} else {
ref.value = value;
}
2022-01-25 04:25:34 +00:00
}
2022-03-09 01:40:51 +00:00
export type PropTypes =
2022-01-25 04:25:34 +00:00
| typeof Boolean
| typeof String
| typeof Number
| typeof Function
| typeof Object
| typeof Array;
// TODO Unfortunately, the typescript engine gives up on typing completely when you use this method,
// Even though it has the same typing as when doing it manually
export function processedPropType<T>(...types: PropTypes[]): PropType<ProcessedComputable<T>> {
if (!types.includes(Object)) {
types.push(Object);
}
return types as PropType<ProcessedComputable<T>>;
}
export function trackHover(element: VueFeature): Ref<boolean> {
const elementComponent = element[ComponentKey];
const isHovered = ref(false);
element[ComponentKey] = defineComponent({
props: {
element: {
type: Object as PropType<VueFeature>,
required: true
}
},
setup: function (props) {
const element = toRef(props, "element");
onUnmounted(() => (isHovered.value = false));
return () => (
<div
style="display: inline-block"
onPointerenter={() => (isHovered.value = true)}
onPointerleave={() => (isHovered.value = false)}
>
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
{unwrapRef(element) == null ? "" : renderJSX(unwrapRef(element)!)}
</div>
);
}
}) as GenericComponent;
const elementGatherProps = element[GatherProps].bind(element);
element[GatherProps] = () => ({
element: {
[ComponentKey]: elementComponent,
[GatherProps]: elementGatherProps
}
});
return isHovered;
}