0
Fork 0
mirror of https://github.com/penpot/penpot-plugins.git synced 2025-01-21 06:02:34 -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;
shape.x = center.x;
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 }) {

View file

@ -78,17 +78,19 @@ export interface Penpot
* @param type The event type to listen for.
* @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.
* @return the listener id that can be used to call `off` and cancel the listener
*
* @example
* ```js
* penpot.on('pagechange', () => {...do something}).
* ```
*/
on: <T extends keyof EventsMap>(
on<T extends keyof EventsMap>(
type: T,
callback: (event: EventsMap[T]) => void,
props?: Map<string, unknown>
) => void;
props?: { [key: string]: unknown }
): symbol;
/**
* Removes an event listener for the specified event type.
*
@ -99,11 +101,25 @@ export interface Penpot
* ```js
* 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,
callback: (event: EventsMap[T]) => void
) => void;
callback?: (event: EventsMap[T]) => 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.
*/
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>(
type: T,
callback: (event: EventsMap[T]) => void,
props?: Map<string, unknown>
props?: { [key: string]: unknown }
): symbol;
/**

View file

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

View file

@ -1,5 +1,5 @@
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 type { PenpotFile } from '@penpot/plugin-types';
@ -28,6 +28,8 @@ describe('Plugin api', () => {
getSelected: vi.fn(),
getSelectedShapes: vi.fn(),
getTheme: vi.fn(() => 'dark'),
addListener: vi.fn(() => Symbol()),
removeListener: vi.fn(),
};
const api = createApi(mockContext as any, {
@ -116,62 +118,15 @@ describe('Plugin api', () => {
it('pagechange', () => {
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('pagechange', callback);
triggerEvent('pagechange', 'test' as any);
expect(callback).toHaveBeenCalledWith('test');
expect(callback).toHaveBeenCalledTimes(1);
api.off(id);
expect(mockContext.removeListener).toHaveBeenCalled();
expect(mockContext.removeListener.mock.calls[0][0]).toBe(id);
});
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', () => {
@ -266,7 +221,7 @@ describe('Plugin api', () => {
expect(modalMock.setTheme).toHaveBeenCalledWith('light');
expect(api.getTheme()).toBe('light');
triggerEvent('themechange', 'dark' as any);
themeChange('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 { loadManifest, loadManifestCode } from './parse-manifest.js';
@ -25,9 +25,7 @@ export const ɵloadPlugin = async function (manifest: Manifest) {
return;
}
for (const event of api.validEvents) {
context.addListener(event, api.triggerEvent.bind(null, event));
}
context.addListener('themechange', (e: PenpotTheme) => api.themeChange(e));
const code = await loadManifestCode(manifest);