Created
July 6, 2023 18:27
-
-
Save yagudaev/f8ba07012ef6ce8360b7543c56242eb1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { emit, on, once } from "@create-figma-plugin/utilities" | |
export type AsyncActionType<F extends (...args: any) => any> = F | |
export type SyncActionType<F extends (...args: any) => any> = ( | |
...args: Parameters<F> | |
) => Promise<ReturnType<F>> | |
let lastCallerId = 0 | |
let lastSubscriptionId = 0 | |
const subscriptions = new Map<string, Function[]>() | |
export function callMainWithoutUnsubscribe(fnName: string, ...args: any[]) { | |
return call({ fnName, args, autoCleanupSubscriptions: false }) | |
} | |
export function callMain(fnName: string, ...args: any[]) { | |
return call({ fnName, args }) | |
} | |
function call({ | |
fnName, | |
args, | |
autoCleanupSubscriptions = true | |
}: { | |
fnName: string | |
args: any[] | |
autoCleanupSubscriptions?: boolean | |
}) { | |
lastCallerId += 1 | |
const callerId = lastCallerId | |
args = args.map((arg) => checkForCallbacks(fnName, callerId, arg)) | |
function cleanUpSubscriptions() { | |
const unsubscribes = subscriptions.get(getSubscriptionStringPrefix(fnName, callerId)) | |
unsubscribes?.forEach((unsubscribe) => unsubscribe()) | |
subscriptions.set(getSubscriptionStringPrefix(fnName, callerId), []) | |
} | |
return new Promise<any>(function (resolve, reject) { | |
once(`RES_${fnName}_${callerId}`, (returnValue) => { | |
resolve(returnValue) | |
autoCleanupSubscriptions && cleanUpSubscriptions() | |
}) | |
once(`ERR_${fnName}_${callerId}`, (error) => { | |
autoCleanupSubscriptions && cleanUpSubscriptions() | |
let errorObj | |
try { | |
const errorJson = JSON.parse(error) | |
if (typeof errorJson === "string") { | |
errorObj = new Error(errorJson) | |
} else { | |
errorObj = new Error(errorJson.message ?? "Unknown error") | |
} | |
errorObj.stack += `\n${errorJson.stack}` | |
} catch (error) { | |
errorObj = new Error("Unknown error") | |
} | |
reject(errorObj) | |
}) | |
emit(`REQ_${fnName}`, callerId, ...args) | |
}) | |
} | |
export function exposeToUI(fn: (...args: any[]) => any) { | |
const name = (fn as any).actionName as string | |
on(`REQ_${name}`, async (callerId: number, ...reqArgs: any[]) => { | |
reqArgs = reqArgs.map((arg) => checkForSubscriptions(name, callerId, arg)) | |
try { | |
const returnValue = await fn(...reqArgs) | |
emit(`RES_${name}_${callerId}`, returnValue) | |
} catch (error) { | |
let errorStr | |
if (typeof error === "string") { | |
const errorObj = new Error(error) | |
errorStr = JSON.stringify(errorObj, Object.getOwnPropertyNames(errorObj)) | |
} else { | |
errorStr = JSON.stringify(error, Object.getOwnPropertyNames(error)) | |
} | |
emit(`ERR_${name}_${callerId}`, errorStr) | |
} | |
}) | |
} | |
export function exposeAllToUI(actions: { [key: string]: (...args: any[]) => any }) { | |
Object.keys(actions).map((actionName: string) => { | |
const fn = actions[actionName] as any | |
fn.actionName = actionName // protect against minification | |
exposeToUI(fn) | |
}) | |
} | |
// helper functions | |
function checkForCallbacks(fnName: string, callerId: number, arg: any) { | |
if (typeof arg === "object") { | |
return { | |
...Object.keys(arg).reduce((acc, key) => { | |
acc[key] = checkForCallbacks(fnName, callerId, arg[key]) | |
return acc | |
}, {} as any) | |
} | |
} | |
if (typeof arg !== "function") return arg | |
const callback = arg as Function | |
lastSubscriptionId += 1 | |
const subscriptionId = lastSubscriptionId | |
const unsubscribe = on( | |
getSubscriptionString(fnName, callerId, subscriptionId), | |
(subscriptionId: number, ...args: any[]) => { | |
// call original callback | |
callback(...args) | |
} | |
) | |
const prevSubscriptions = subscriptions.get(getSubscriptionStringPrefix(fnName, callerId)) || [] | |
subscriptions.set(getSubscriptionStringPrefix(fnName, callerId), [ | |
...prevSubscriptions, | |
unsubscribe | |
]) | |
return { subscriptionId, fnName: arg.name, __SUBSCRIPTION__: true } | |
} | |
function getSubscriptionString(fnName: string, callerId: number, subscriptionId: number): string { | |
return `${getSubscriptionStringPrefix(fnName, callerId)}_${subscriptionId}` | |
} | |
function getSubscriptionStringPrefix(fnName: string, callerId: number): string { | |
return `SUB_${fnName}_${callerId}` | |
} | |
function checkForSubscriptions(fnName: string, callerId: number, arg: any) { | |
if (typeof arg === "object" && !arg.__SUBSCRIPTION__) { | |
return { | |
...Object.keys(arg).reduce((acc, key) => { | |
acc[key] = checkForSubscriptions(fnName, callerId, arg[key]) | |
return acc | |
}, {} as any) | |
} | |
} | |
if (typeof arg !== "object" || !arg.__SUBSCRIPTION__) return arg | |
const { subscriptionId } = arg | |
return (...args: any[]) => { | |
emit(getSubscriptionString(fnName, callerId, subscriptionId), subscriptionId, ...args) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const subscribeToSelectionChange = function (callback: (nodeIds: string[]) => void) { | |
figma.on("selectionchange", () => { | |
callback(figma.currentPage.selection.map((n) => n.id)) | |
}) | |
} | |
export const getSelection = () => { | |
return figma.currentPage.selection.map((n) => n.id) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { exposeAllToUI } from "../shared/figmaRPC" | |
import * as actions from "./actions" | |
export default function () { | |
exposeAllToUI(actions) | |
showUI({ width: 400, height: 380, themeColors: true }) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { getSelection, subscribeToSelectionChange } from "./mainAPI" | |
function Plugin() { | |
useEffect(() => { | |
getSelection().then(handleSelectionChange) | |
subscribeToSelectionChange(handleSelectionChange) | |
}, []) | |
async function handleSelectionChange(selectedNodeIds: string[]) { | |
console.log("selectedNodeIds", selectedNodeIds) | |
} | |
return <div>Plugin Content here</div> | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
AsyncActionType, | |
SyncActionType, | |
callMain, | |
callMainWithoutUnsubscribe | |
} from "../shared/figmaRPC" | |
import type * as Actions from "../main/actions" | |
export const subscribeToSelectionChange: SyncActionType<typeof Actions.subscribeToSelectionChange> = | |
async function (...args) { | |
return callMainWithoutUnsubscribe("subscribeToSelectionChange", ...args) | |
} | |
export const getSelection: SyncActionType<typeof Actions.getSelection> = function () { | |
return callMain("getSelection") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quick and dirty starter docs for keeping track of selection using the figma rest API