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
This commit is contained in:
WithoutPants 2024-08-20 12:36:45 +10:00 committed by GitHub
parent a94bf29b34
commit 49060e6686
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 8 deletions

View File

@ -33,4 +33,15 @@
.scene-performer-popover .image-thumbnail {
margin: 1em;
}
.example-react-component-custom-overlay {
display: block;
font-weight: 900;
height: 100%;
opacity: 0.25;
position: absolute;
text-align: center;
top: 0;
width: 100%;
z-index: 8;
}

View File

@ -132,10 +132,18 @@ interface IPluginApi {
);
}
function Overlays() {
return <span className="example-react-component-custom-overlay">Custom overlay</span>;
}
PluginApi.patch.instead("SceneCard.Details", function (props: any, _: any, original: any) {
return <SceneDetails {...props} />;
});
PluginApi.patch.instead("SceneCard.Overlays", function (props: any, _: any, original: (props: any) => any) {
return <><Overlays />{original({...props})}</>;
});
const TestPage: React.FC = () => {
const componentsLoading = PluginApi.hooks.useLoadComponents([PluginApi.loadableComponents.SceneCard]);

View File

@ -117,12 +117,12 @@ Returns `void`.
#### `PluginApi.patch.instead`
Registers a replacement function for a component. The provided function will be called with the arguments passed to the original render function, plus the original render function as the last argument. An error will be thrown if the component already has a replacement function registered.
Registers a replacement function for a component. The provided function will be called with the arguments passed to the original render function, plus the next render function as the last argument. Replacement functions will be called in the order that they are registered. If a replacement function does not call the next render function then the following replacement functions will not be called or applied.
| Parameter | Type | Description |
|-----------|------|-------------|
| `component` | `string` | The name of the component to patch. |
| `fn` | `Function` | The replacement function. It accepts the same arguments as the original render function, plus the original render function, and is expected to return the replacement component. |
| `fn` | `Function` | The replacement function. It accepts the same arguments as the original render function, plus the next render function, and is expected to return the replacement component. |
Returns `void`.

View File

@ -10,7 +10,7 @@ export const components: Record<string, Function> = {
};
const beforeFns: Record<string, Function[]> = {};
const insteadFns: Record<string, Function> = {};
const insteadFns: Record<string, Function[]> = {};
const afterFns: Record<string, Function[]> = {};
// patch functions
@ -23,11 +23,14 @@ export function before(component: string, fn: Function) {
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]) {
throw new Error("instead has already been called for " + component);
if (!insteadFns[component]) {
insteadFns[component] = [];
}
insteadFns[component] = fn;
insteadFns[component].push(fn);
}
export function after(component: string, fn: Function) {
@ -51,6 +54,35 @@ export function RegisterComponent<T extends Function>(
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, {
@ -61,7 +93,7 @@ export function PatchFunction<T extends Function>(name: string, fn: T) {
args = beforeFn.apply(ctx, args);
}
if (insteadFns[name]) {
result = insteadFns[name].apply(ctx, args.concat(target));
result = runInstead(insteadFns[name], target, ctx, args);
} else {
result = target.apply(ctx, args);
}