stash/ui/v2.5/src/patch.tsx
WithoutPants 49060e6686
UI nested instead (#5125)
* Support multiple calls to PluginApi.patch.instead for a component.

Allow calling the original/chained function from the hook function.

* Add example of new usage of instead
* Update documentation
2024-08-20 12:36:45 +10:00

129 lines
3.4 KiB
TypeScript

import React from "react";
import { HoverPopover } from "./components/Shared/HoverPopover";
import { TagLink } from "./components/Shared/TagLink";
import { LoadingIndicator } from "./components/Shared/LoadingIndicator";
export const components: Record<string, Function> = {
HoverPopover,
TagLink,
LoadingIndicator,
};
const beforeFns: Record<string, Function[]> = {};
const insteadFns: Record<string, Function[]> = {};
const afterFns: Record<string, Function[]> = {};
// patch functions
// registers a patch to a function. Before functions are expected to return the
// new arguments to be passed to the function.
export function before(component: string, fn: Function) {
if (!beforeFns[component]) {
beforeFns[component] = [];
}
beforeFns[component].push(fn);
}
// registers a patch to a function. Instead functions receive the original arguments,
// plus the next function to call. In order for all instead functions to be called,
// it is expected that the provided next() function will be called.
export function instead(component: string, fn: Function) {
if (!insteadFns[component]) {
insteadFns[component] = [];
}
insteadFns[component].push(fn);
}
export function after(component: string, fn: Function) {
if (!afterFns[component]) {
afterFns[component] = [];
}
afterFns[component].push(fn);
}
export function RegisterComponent<T extends Function>(
component: string,
fn: T
) {
// register with the plugin api
if (components[component]) {
throw new Error("Component " + component + " has already been registered");
}
components[component] = fn;
return fn;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
function runInstead(
fns: Function[],
targetFn: Function,
thisArg: any,
argArray: any[]
) {
if (!fns.length) {
return targetFn.apply(thisArg, argArray);
}
let i = 1;
function next(): any {
if (i >= fns.length) {
return targetFn;
}
const thisTarget = fns[i++];
return new Proxy(thisTarget, {
apply: function (target, ctx, args) {
return target.apply(ctx, args.concat(next()));
},
});
}
return fns[0].apply(thisArg, argArray.concat(next()));
}
/* eslint-enable @typescript-eslint/no-explicit-any */
// patches a function to implement the before/instead/after functionality
export function PatchFunction<T extends Function>(name: string, fn: T) {
return new Proxy(fn, {
apply(target, ctx, args) {
let result;
for (const beforeFn of beforeFns[name] || []) {
args = beforeFn.apply(ctx, args);
}
if (insteadFns[name]) {
result = runInstead(insteadFns[name], target, ctx, args);
} else {
result = target.apply(ctx, args);
}
for (const afterFn of afterFns[name] || []) {
result = afterFn.apply(ctx, args.concat(result));
}
return result;
},
});
}
// patches a component and registers it in the pluginapi components object
export function PatchComponent<T>(
component: string,
fn: React.FC<T>
): React.FC<T> {
const ret = PatchFunction(component, fn);
// register with the plugin api
RegisterComponent(component, ret);
return ret as React.FC<T>;
}
// patches a component and registers it in the pluginapi components object
export function PatchContainerComponent(
component: string
): React.FC<React.PropsWithChildren<{}>> {
const fn = (props: React.PropsWithChildren<{}>) => {
return <>{props.children}</>;
};
return PatchComponent(component, fn);
}