0
Fork 0
mirror of https://github.com/penpot/penpot-plugins.git synced 2025-01-21 22:22:45 -05:00

feat(plugins-runtime): add new events 'contentsave' and 'shapechange', changed on/off signatures

This commit is contained in:
alonso.torres 2024-07-11 13:37:12 +02:00 committed by Alonso Torres
parent a1174c99a6
commit 2b8a76b2b0
5 changed files with 86 additions and 93 deletions

View file

@ -121,6 +121,16 @@ function createRect() {
const center = penpot.viewport.center; const center = penpot.viewport.center;
shape.x = center.x; shape.x = center.x;
shape.y = center.y; shape.y = center.y;
penpot.on(
'shapechange',
(s) => {
console.log('change', s.name, s.x, s.y);
},
{
shapeId: shape.id,
}
);
} }
function moveX(data: { id: string }) { function moveX(data: { id: string }) {

View file

@ -78,17 +78,19 @@ export interface Penpot
* @param type The event type to listen for. * @param type The event type to listen for.
* @param callback The callback function to execute when the event is triggered. * @param callback The callback function to execute when the event is triggered.
* @param props The properties for the current event handler. Only makes sense for specific events. * @param props The properties for the current event handler. Only makes sense for specific events.
* @return the listener id that can be used to call `off` and cancel the listener
* *
* @example * @example
* ```js * ```js
* penpot.on('pagechange', () => {...do something}). * penpot.on('pagechange', () => {...do something}).
* ``` * ```
*/ */
on: <T extends keyof EventsMap>( on<T extends keyof EventsMap>(
type: T, type: T,
callback: (event: EventsMap[T]) => void, callback: (event: EventsMap[T]) => void,
props?: Map<string, unknown> props?: { [key: string]: unknown }
) => void; ): symbol;
/** /**
* Removes an event listener for the specified event type. * Removes an event listener for the specified event type.
* *
@ -99,11 +101,25 @@ export interface Penpot
* ```js * ```js
* penpot.off('pagechange', () => {...do something}). * penpot.off('pagechange', () => {...do something}).
* ``` * ```
* @deprecated this method should not be used. Use instead off sending the `listenerId` (return value from `on` method)
*/ */
off: <T extends keyof EventsMap>( off<T extends keyof EventsMap>(
type: T, type: T,
callback: (event: EventsMap[T]) => void callback?: (event: EventsMap[T]) => void
) => void; ): void;
/**
* Removes an event listener for the specified event type.
*
* @param listenerId the id returned by the `on` method when the callback was set
*
* @example
* ```js
* const listenerId = penpot.on('contentsave', () => console.log("Changed"));
* penpot.off(listenerId);
* ```
*/
off(listenerId: symbol): void;
} }
/** /**
@ -1948,6 +1964,17 @@ export interface EventsMap {
* The `finish` event is triggered when some operation is finished. * The `finish` event is triggered when some operation is finished.
*/ */
finish: string; finish: string;
/**
* This event will triger whenever the shape in the props change. It's mandatory to send
* with the props an object like `{ shapeId: '<id>' }`
*/
shapechange: PenpotShape;
/**
* The `contentsave` event will trigger when the content file changes.
*/
contentsave: void;
} }
/** /**
@ -2739,7 +2766,7 @@ export interface PenpotContext {
addListener<T extends keyof EventsMap>( addListener<T extends keyof EventsMap>(
type: T, type: T,
callback: (event: EventsMap[T]) => void, callback: (event: EventsMap[T]) => void,
props?: Map<string, unknown> props?: { [key: string]: unknown }
): symbol; ): symbol;
/** /**

View file

@ -39,13 +39,16 @@ export const validEvents = [
'filechange', 'filechange',
'selectionchange', 'selectionchange',
'themechange', 'themechange',
'shapechange',
'contentsave',
] as const; ] as const;
export let uiMessagesCallbacks: Callback<unknown>[] = []; export let uiMessagesCallbacks: Callback<unknown>[] = [];
let modals = new Set<PluginModalElement>([]); let modals = new Set<PluginModalElement>([]);
const eventListeners: Map<string, Callback<unknown>[]> = new Map(); // TODO: Remove when deprecating method `off`
let listeners: { [key: string]: Map<object, symbol> } = {};
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
try { try {
@ -57,17 +60,10 @@ window.addEventListener('message', (event) => {
} }
}); });
export function triggerEvent( export function themeChange(theme: PenpotTheme) {
type: keyof EventsMap,
message: EventsMap[keyof EventsMap]
) {
if (type === 'themechange') {
modals.forEach((modal) => { modals.forEach((modal) => {
modal.setTheme(message as PenpotTheme); modal.setTheme(theme);
}); });
}
const listeners = eventListeners.get(type) || [];
listeners.forEach((listener) => listener(message));
} }
export function createApi(context: PenpotContext, manifest: Manifest): Penpot { export function createApi(context: PenpotContext, manifest: Manifest): Penpot {
@ -166,8 +162,9 @@ export function createApi(context: PenpotContext, manifest: Manifest): Penpot {
on<T extends keyof EventsMap>( on<T extends keyof EventsMap>(
type: T, type: T,
callback: (event: EventsMap[T]) => void callback: (event: EventsMap[T]) => void,
): void { props?: { [key: string]: unknown }
): symbol {
// z.function alter fn, so can't use it here // z.function alter fn, so can't use it here
z.enum(validEvents).parse(type); z.enum(validEvents).parse(type);
z.function().parse(callback); z.function().parse(callback);
@ -175,24 +172,30 @@ export function createApi(context: PenpotContext, manifest: Manifest): Penpot {
// To suscribe to events needs the read permission // To suscribe to events needs the read permission
checkPermission('content:read'); checkPermission('content:read');
const listeners = eventListeners.get(type) || []; const id = context.addListener(type, callback, props);
listeners.push(callback as Callback<unknown>);
eventListeners.set(type, listeners); if (!listeners[type]) {
listeners[type] = new Map<object, symbol>();
}
listeners[type].set(callback, id);
return id;
}, },
off<T extends keyof EventsMap>( off<T extends keyof EventsMap>(
type: T, idtype: symbol | T,
callback: (event: EventsMap[T]) => void callback?: (event: EventsMap[T]) => void
): void { ): void {
z.enum(validEvents).parse(type); let listenerId: symbol | undefined;
z.function().parse(callback);
const listeners = eventListeners.get(type) || []; if (typeof idtype === 'symbol') {
listenerId = idtype;
} else if (callback) {
listenerId = listeners[idtype as T].get(callback);
}
eventListeners.set( if (listenerId) {
type, context.removeListener(listenerId);
listeners.filter((listener) => listener !== callback) }
);
}, },
// Penpot State API // Penpot State API

View file

@ -1,5 +1,5 @@
import { expect, describe, vi } from 'vitest'; import { expect, describe, vi } from 'vitest';
import { createApi, triggerEvent, uiMessagesCallbacks } from './index.js'; import { createApi, themeChange, uiMessagesCallbacks } from './index.js';
import openUIApi from './openUI.api.js'; import openUIApi from './openUI.api.js';
import type { PenpotFile } from '@penpot/plugin-types'; import type { PenpotFile } from '@penpot/plugin-types';
@ -28,6 +28,8 @@ describe('Plugin api', () => {
getSelected: vi.fn(), getSelected: vi.fn(),
getSelectedShapes: vi.fn(), getSelectedShapes: vi.fn(),
getTheme: vi.fn(() => 'dark'), getTheme: vi.fn(() => 'dark'),
addListener: vi.fn(() => Symbol()),
removeListener: vi.fn(),
}; };
const api = createApi(mockContext as any, { const api = createApi(mockContext as any, {
@ -116,62 +118,15 @@ describe('Plugin api', () => {
it('pagechange', () => { it('pagechange', () => {
const callback = vi.fn(); const callback = vi.fn();
api.on('pagechange', callback); const id = api.on('pagechange', callback);
expect(mockContext.addListener).toHaveBeenCalled();
expect(mockContext.addListener.mock.calls[0][0]).toBe('pagechange');
expect(mockContext.addListener.mock.calls[0][1]).toBe(callback);
triggerEvent('pagechange', 'test' as any); api.off(id);
expect(mockContext.removeListener).toHaveBeenCalled();
api.off('pagechange', callback); expect(mockContext.removeListener.mock.calls[0][0]).toBe(id);
triggerEvent('pagechange', 'test' as any);
expect(callback).toHaveBeenCalledWith('test');
expect(callback).toHaveBeenCalledTimes(1);
}); });
it('filechange', () => {
const callback = vi.fn();
api.on('filechange', callback);
triggerEvent('filechange', 'test' as any);
api.off('filechange', callback);
triggerEvent('filechange', 'test' as any);
expect(callback).toHaveBeenCalledWith('test');
expect(callback).toHaveBeenCalledTimes(1);
});
});
it('selectionchange', () => {
const callback = vi.fn();
api.on('selectionchange', callback);
triggerEvent('selectionchange', 'test' as any);
api.off('selectionchange', callback);
triggerEvent('selectionchange', 'test' as any);
expect(callback).toHaveBeenCalledWith('test');
expect(callback).toHaveBeenCalledTimes(1);
});
it('themechange', () => {
const callback = vi.fn();
api.on('themechange', callback);
triggerEvent('themechange', 'light');
api.off('themechange', callback);
triggerEvent('themechange', 'light');
expect(callback).toHaveBeenCalledWith('light');
expect(callback).toHaveBeenCalledTimes(1);
}); });
describe.concurrent('permissions', () => { describe.concurrent('permissions', () => {
@ -266,7 +221,7 @@ describe('Plugin api', () => {
expect(modalMock.setTheme).toHaveBeenCalledWith('light'); expect(modalMock.setTheme).toHaveBeenCalledWith('light');
expect(api.getTheme()).toBe('light'); expect(api.getTheme()).toBe('light');
triggerEvent('themechange', 'dark' as any); themeChange('dark');
expect(modalMock.setTheme).toHaveBeenCalledWith('dark'); expect(modalMock.setTheme).toHaveBeenCalledWith('dark');
}); });

View file

@ -1,4 +1,4 @@
import type { PenpotContext } from '@penpot/plugin-types'; import type { PenpotContext, PenpotTheme } from '@penpot/plugin-types';
import { createApi } from './api/index.js'; import { createApi } from './api/index.js';
import { loadManifest, loadManifestCode } from './parse-manifest.js'; import { loadManifest, loadManifestCode } from './parse-manifest.js';
@ -25,9 +25,7 @@ export const ɵloadPlugin = async function (manifest: Manifest) {
return; return;
} }
for (const event of api.validEvents) { context.addListener('themechange', (e: PenpotTheme) => api.themeChange(e));
context.addListener(event, api.triggerEvent.bind(null, event));
}
const code = await loadManifestCode(manifest); const code = await loadManifestCode(manifest);