mirror of
https://github.com/penpot/penpot-plugins.git
synced 2025-03-06 04:45:44 -05:00
feat: add ui.resize & ui.size api
This commit is contained in:
parent
32de075099
commit
815181d20a
6 changed files with 134 additions and 77 deletions
41
libs/plugin-types/index.d.ts
vendored
41
libs/plugin-types/index.d.ts
vendored
|
@ -21,8 +21,33 @@ export interface Penpot
|
|||
open: (
|
||||
name: string,
|
||||
url: string,
|
||||
options?: { width: number; height: number }
|
||||
options?: { width: number; height: number },
|
||||
) => void;
|
||||
|
||||
size: {
|
||||
/**
|
||||
* Returns the size of the modal.
|
||||
* @example
|
||||
* ```js
|
||||
* const size = penpot.ui.size;
|
||||
* console.log(size);
|
||||
* ```
|
||||
*/
|
||||
width: number;
|
||||
height: number;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* Resizes the plugin UI.
|
||||
* @param width The width of the modal.
|
||||
* @param height The height of the modal.
|
||||
* @example
|
||||
* ```js
|
||||
* penpot.ui.resize(300, 400);
|
||||
* ```
|
||||
* */
|
||||
resize: (width: number, height: number) => void;
|
||||
|
||||
/**
|
||||
* Sends a message to the plugin UI.
|
||||
*
|
||||
|
@ -89,7 +114,7 @@ export interface Penpot
|
|||
on<T extends keyof EventsMap>(
|
||||
type: T,
|
||||
callback: (event: EventsMap[T]) => void,
|
||||
props?: { [key: string]: unknown }
|
||||
props?: { [key: string]: unknown },
|
||||
): symbol;
|
||||
|
||||
/**
|
||||
|
@ -858,7 +883,7 @@ export interface Context {
|
|||
uploadMediaData(
|
||||
name: string,
|
||||
data: Uint8Array,
|
||||
mimeType: string
|
||||
mimeType: string,
|
||||
): Promise<ImageData>;
|
||||
|
||||
/**
|
||||
|
@ -1095,7 +1120,7 @@ export interface Context {
|
|||
type?: 'css';
|
||||
withPrelude?: boolean;
|
||||
includeChildren?: boolean;
|
||||
}
|
||||
},
|
||||
): string;
|
||||
|
||||
/**
|
||||
|
@ -1111,7 +1136,7 @@ export interface Context {
|
|||
addListener<T extends keyof EventsMap>(
|
||||
type: T,
|
||||
callback: (event: EventsMap[T]) => void,
|
||||
props?: { [key: string]: unknown }
|
||||
props?: { [key: string]: unknown },
|
||||
): symbol;
|
||||
|
||||
/**
|
||||
|
@ -1153,7 +1178,7 @@ export interface Context {
|
|||
*/
|
||||
alignHorizontal(
|
||||
shapes: Shape[],
|
||||
direction: 'left' | 'center' | 'right'
|
||||
direction: 'left' | 'center' | 'right',
|
||||
): void;
|
||||
|
||||
/**
|
||||
|
@ -1454,7 +1479,7 @@ export interface File extends PluginData {
|
|||
*/
|
||||
export(
|
||||
exportType: 'penpot' | 'zip',
|
||||
libraryExportType?: 'all' | 'merge' | 'detach'
|
||||
libraryExportType?: 'all' | 'merge' | 'detach',
|
||||
): Promise<Uint8Array>;
|
||||
|
||||
/**
|
||||
|
@ -2862,7 +2887,7 @@ export interface Page extends PluginData {
|
|||
addRulerGuide(
|
||||
orientation: RulerGuideOrientation,
|
||||
value: number,
|
||||
board?: Board
|
||||
board?: Board,
|
||||
): RulerGuide;
|
||||
|
||||
/**
|
||||
|
|
|
@ -40,7 +40,7 @@ export const validEvents = [
|
|||
] as const;
|
||||
|
||||
export function createApi(
|
||||
plugin: Awaited<ReturnType<typeof createPluginManager>>
|
||||
plugin: Awaited<ReturnType<typeof createPluginManager>>,
|
||||
) {
|
||||
const checkPermission = (permission: Permissions) => {
|
||||
if (!plugin.manifest.permissions.includes(permission)) {
|
||||
|
@ -54,6 +54,14 @@ export function createApi(
|
|||
plugin.openModal(name, url, options);
|
||||
},
|
||||
|
||||
get size() {
|
||||
return plugin.getModal()?.size() || null;
|
||||
},
|
||||
|
||||
resize: (width: number, height: number) => {
|
||||
return plugin.resizeModal(width, height);
|
||||
},
|
||||
|
||||
sendMessage(message: unknown) {
|
||||
const event = new CustomEvent('message', {
|
||||
detail: message,
|
||||
|
@ -112,7 +120,7 @@ export function createApi(
|
|||
on<T extends keyof EventsMap>(
|
||||
type: T,
|
||||
callback: (event: EventsMap[T]) => void,
|
||||
props?: { [key: string]: unknown }
|
||||
props?: { [key: string]: unknown },
|
||||
): symbol {
|
||||
// z.function alter fn, so can't use it here
|
||||
z.enum(validEvents).parse(type);
|
||||
|
@ -254,7 +262,7 @@ export function createApi(
|
|||
|
||||
generateMarkup(
|
||||
shapes: Shape[],
|
||||
options?: { type?: 'html' | 'svg' }
|
||||
options?: { type?: 'html' | 'svg' },
|
||||
): string {
|
||||
checkPermission('content:read');
|
||||
return plugin.context.generateMarkup(shapes, options);
|
||||
|
@ -266,7 +274,7 @@ export function createApi(
|
|||
type?: 'css';
|
||||
withPrelude?: boolean;
|
||||
includeChildren?: boolean;
|
||||
}
|
||||
},
|
||||
): string {
|
||||
checkPermission('content:read');
|
||||
return plugin.context.generateStyle(shapes, options);
|
||||
|
@ -289,7 +297,7 @@ export function createApi(
|
|||
|
||||
alignHorizontal(
|
||||
shapes: Shape[],
|
||||
direction: 'left' | 'center' | 'right'
|
||||
direction: 'left' | 'center' | 'right',
|
||||
): void {
|
||||
checkPermission('content:write');
|
||||
plugin.context.alignHorizontal(shapes, direction);
|
||||
|
@ -297,7 +305,7 @@ export function createApi(
|
|||
|
||||
alignVertical(
|
||||
shapes: Shape[],
|
||||
direction: 'top' | 'center' | 'bottom'
|
||||
direction: 'top' | 'center' | 'bottom',
|
||||
): void {
|
||||
checkPermission('content:write');
|
||||
plugin.context.alignVertical(shapes, direction);
|
||||
|
|
|
@ -9,6 +9,10 @@ describe('createModal', () => {
|
|||
setTheme: vi.fn(),
|
||||
style: {
|
||||
setProperty: vi.fn(),
|
||||
getPropertyValue: vi.fn(),
|
||||
},
|
||||
wrapper: {
|
||||
style: {},
|
||||
},
|
||||
setAttribute: vi.fn(),
|
||||
} as unknown as PluginModalElement;
|
||||
|
@ -34,7 +38,7 @@ describe('createModal', () => {
|
|||
'Test Modal',
|
||||
'https://example.com',
|
||||
theme,
|
||||
options
|
||||
options,
|
||||
);
|
||||
|
||||
expect(createElementSpy).toHaveBeenCalledWith('plugin-modal');
|
||||
|
@ -42,16 +46,16 @@ describe('createModal', () => {
|
|||
|
||||
expect(modal.style.setProperty).toHaveBeenCalledWith(
|
||||
'--modal-block-start',
|
||||
'40px'
|
||||
'40px',
|
||||
);
|
||||
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith('title', 'Test Modal');
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith(
|
||||
'iframe-src',
|
||||
'https://example.com'
|
||||
'https://example.com',
|
||||
);
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith('width', '400');
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith('height', '600');
|
||||
expect(modal.wrapper.style.width).toEqual('400px');
|
||||
expect(modal.wrapper.style.height).toEqual('600px');
|
||||
|
||||
expect(appendChildSpy).toHaveBeenCalledWith(modal);
|
||||
});
|
||||
|
@ -61,8 +65,8 @@ describe('createModal', () => {
|
|||
|
||||
const modal = createModal('Test Modal', 'https://example.com', theme);
|
||||
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith('width', '335');
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith('height', '590');
|
||||
expect(modal.wrapper.style.width).toEqual('335px');
|
||||
expect(modal.wrapper.style.height).toEqual('590px');
|
||||
});
|
||||
|
||||
it('should limit modal dimensions to the window size', () => {
|
||||
|
@ -76,20 +80,14 @@ describe('createModal', () => {
|
|||
'Test Modal',
|
||||
'https://example.com',
|
||||
theme,
|
||||
options
|
||||
options,
|
||||
);
|
||||
|
||||
const expectedWidth = 710; // 1000 - 270 (initialPosition.inlineEnd)
|
||||
const expectedHeight = 760; // 800 - 40 (initialPosition.blockStart)
|
||||
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith(
|
||||
'width',
|
||||
String(expectedWidth)
|
||||
);
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith(
|
||||
'height',
|
||||
String(expectedHeight)
|
||||
);
|
||||
expect(modal.wrapper.style.width).toEqual(`${expectedWidth}px`);
|
||||
expect(modal.wrapper.style.height).toEqual(`${expectedHeight}px`);
|
||||
});
|
||||
|
||||
it('should apply minimum dimensions to the modal', () => {
|
||||
|
@ -100,10 +98,10 @@ describe('createModal', () => {
|
|||
'Test Modal',
|
||||
'https://example.com',
|
||||
theme,
|
||||
options
|
||||
options,
|
||||
);
|
||||
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith('width', '200');
|
||||
expect(modal.setAttribute).toHaveBeenCalledWith('height', '200');
|
||||
expect(modal.wrapper.style.width).toEqual('200px');
|
||||
expect(modal.wrapper.style.height).toEqual('200px');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,49 +7,32 @@ export function createModal(
|
|||
url: string,
|
||||
theme: Theme,
|
||||
options?: OpenUIOptions,
|
||||
allowDownloads?: boolean
|
||||
allowDownloads?: boolean,
|
||||
) {
|
||||
const modal = document.createElement('plugin-modal') as PluginModalElement;
|
||||
|
||||
modal.setTheme(theme);
|
||||
const minPluginWidth = 200;
|
||||
const minPluginHeight = 200;
|
||||
|
||||
const defaultWidth = 335;
|
||||
const defaultHeight = 590;
|
||||
|
||||
const maxWidth =
|
||||
(options?.width ?? defaultWidth) > window.innerWidth
|
||||
? window.innerWidth - 290
|
||||
: options?.width ?? defaultWidth;
|
||||
const { width } = resizeModal(modal, options?.width, options?.height);
|
||||
|
||||
const initialPosition = {
|
||||
blockStart: 40,
|
||||
// To be able to resize the element as expected the position must be absolute from the right.
|
||||
// This value is the length of the window minus the width of the element plus the width of the design tab.
|
||||
inlineStart: window.innerWidth - maxWidth - 290,
|
||||
inlineStart: window.innerWidth - width - 290,
|
||||
};
|
||||
|
||||
modal.style.setProperty(
|
||||
'--modal-block-start',
|
||||
`${initialPosition.blockStart}px`
|
||||
`${initialPosition.blockStart}px`,
|
||||
);
|
||||
modal.style.setProperty(
|
||||
'--modal-inline-start',
|
||||
`${initialPosition.inlineStart}px`
|
||||
`${initialPosition.inlineStart}px`,
|
||||
);
|
||||
|
||||
const maxHeight = window.innerHeight - initialPosition.blockStart;
|
||||
let width = Math.min(options?.width || defaultWidth, maxWidth);
|
||||
let height = Math.min(options?.height || defaultHeight, maxHeight);
|
||||
|
||||
width = Math.max(width, minPluginWidth);
|
||||
height = Math.max(height, minPluginHeight);
|
||||
|
||||
modal.setAttribute('title', name);
|
||||
modal.setAttribute('iframe-src', url);
|
||||
modal.setAttribute('width', String(width));
|
||||
modal.setAttribute('height', String(height));
|
||||
|
||||
if (allowDownloads) {
|
||||
modal.setAttribute('allow-downloads', 'true');
|
||||
|
@ -59,3 +42,33 @@ export function createModal(
|
|||
|
||||
return modal;
|
||||
}
|
||||
|
||||
export function resizeModal(
|
||||
modal: PluginModalElement,
|
||||
width: number = 335,
|
||||
height: number = 590,
|
||||
) {
|
||||
const minPluginWidth = 200;
|
||||
const minPluginHeight = 200;
|
||||
|
||||
const maxWidth = width > window.innerWidth ? window.innerWidth - 290 : width;
|
||||
|
||||
const blockStart = parseInt(
|
||||
modal.style.getPropertyValue('--modal-block-start') || '40',
|
||||
10,
|
||||
);
|
||||
|
||||
const maxHeight = window.innerHeight - blockStart;
|
||||
width = Math.min(width, maxWidth);
|
||||
height = Math.min(height, maxHeight);
|
||||
|
||||
width = Math.max(width, minPluginWidth);
|
||||
height = Math.max(height, minPluginHeight);
|
||||
|
||||
modal.wrapper.style.width = `${width}px`;
|
||||
modal.wrapper.style.minWidth = `${width}px`;
|
||||
modal.wrapper.style.height = `${height}px`;
|
||||
modal.wrapper.style.minHeight = `${height}px`;
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ const closeSvg = `
|
|||
import type { Theme } from '@penpot/plugin-types';
|
||||
import { dragHandler } from '../drag-handler.js';
|
||||
import modalCss from './plugin.modal.css?inline';
|
||||
import { resizeModal } from '../create-modal.js';
|
||||
|
||||
export class PluginModalElement extends HTMLElement {
|
||||
constructor() {
|
||||
|
@ -11,13 +12,19 @@ export class PluginModalElement extends HTMLElement {
|
|||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
#wrapper: HTMLElement | null = null;
|
||||
#inner: HTMLElement | null = null;
|
||||
wrapper = document.createElement('div');
|
||||
#inner = document.createElement('div');
|
||||
#dragEvents: ReturnType<typeof dragHandler> | null = null;
|
||||
|
||||
setTheme(theme: Theme) {
|
||||
if (this.#wrapper) {
|
||||
this.#wrapper.setAttribute('data-theme', theme);
|
||||
if (this.wrapper) {
|
||||
this.wrapper.setAttribute('data-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
resize(width: number, height: number) {
|
||||
if (this.wrapper) {
|
||||
resizeModal(this, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,8 +49,6 @@ export class PluginModalElement extends HTMLElement {
|
|||
connectedCallback() {
|
||||
const title = this.getAttribute('title');
|
||||
const iframeSrc = this.getAttribute('iframe-src');
|
||||
const width = Number(this.getAttribute('width') || '300');
|
||||
const height = Number(this.getAttribute('height') || '400');
|
||||
const allowDownloads = this.getAttribute('allow-downloads') || false;
|
||||
|
||||
if (!title || !iframeSrc) {
|
||||
|
@ -54,21 +59,14 @@ export class PluginModalElement extends HTMLElement {
|
|||
throw new Error('Error creating shadow root');
|
||||
}
|
||||
|
||||
this.#wrapper = document.createElement('div');
|
||||
this.#inner = document.createElement('div');
|
||||
|
||||
this.#inner.classList.add('inner');
|
||||
|
||||
this.#wrapper.classList.add('wrapper');
|
||||
this.#wrapper.style.inlineSize = `${width}px`;
|
||||
this.#wrapper.style.minInlineSize = `${width}px`;
|
||||
this.#wrapper.style.blockSize = `${height}px`;
|
||||
this.#wrapper.style.minBlockSize = `${height}px`;
|
||||
this.#wrapper.style.maxInlineSize = '90vw';
|
||||
this.#wrapper.style.maxBlockSize = '90vh';
|
||||
this.wrapper.classList.add('wrapper');
|
||||
this.wrapper.style.maxInlineSize = '90vw';
|
||||
this.wrapper.style.maxBlockSize = '90vh';
|
||||
|
||||
// move modal to the top
|
||||
this.#dragEvents = dragHandler(this.#inner, this.#wrapper, () => {
|
||||
this.#dragEvents = dragHandler(this.#inner, this.wrapper, () => {
|
||||
this.calculateZIndex();
|
||||
});
|
||||
|
||||
|
@ -92,7 +90,7 @@ export class PluginModalElement extends HTMLElement {
|
|||
new CustomEvent('close', {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -107,7 +105,7 @@ export class PluginModalElement extends HTMLElement {
|
|||
'allow-modals',
|
||||
'allow-popups',
|
||||
'allow-popups-to-escape-sandbox',
|
||||
'allow-storage-access-by-user-activation'
|
||||
'allow-storage-access-by-user-activation',
|
||||
);
|
||||
|
||||
if (allowDownloads) {
|
||||
|
@ -119,7 +117,7 @@ export class PluginModalElement extends HTMLElement {
|
|||
new CustomEvent('load', {
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -131,9 +129,9 @@ export class PluginModalElement extends HTMLElement {
|
|||
iframe.contentWindow.postMessage((e as CustomEvent).detail, '*');
|
||||
});
|
||||
|
||||
this.shadowRoot.appendChild(this.#wrapper);
|
||||
this.shadowRoot.appendChild(this.wrapper);
|
||||
|
||||
this.#wrapper.appendChild(this.#inner);
|
||||
this.wrapper.appendChild(this.#inner);
|
||||
this.#inner.appendChild(header);
|
||||
this.#inner.appendChild(iframe);
|
||||
|
||||
|
@ -144,6 +142,13 @@ export class PluginModalElement extends HTMLElement {
|
|||
|
||||
this.calculateZIndex();
|
||||
}
|
||||
|
||||
size() {
|
||||
const width = Number(this.wrapper.style.width.replace('px', '') || '300');
|
||||
const height = Number(this.wrapper.style.height.replace('px', '') || '400');
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('plugin-modal', PluginModalElement);
|
||||
|
|
|
@ -6,6 +6,7 @@ import { PluginModalElement } from './modal/plugin-modal.js';
|
|||
import { openUIApi } from './api/openUI.api.js';
|
||||
import { OpenUIOptions } from './models/open-ui-options.model.js';
|
||||
import { RegisterListener } from './models/plugin.model.js';
|
||||
import { openUISchema } from './models/open-ui-options.schema.js';
|
||||
|
||||
export async function createPluginManager(
|
||||
context: Context,
|
||||
|
@ -129,6 +130,13 @@ export async function createPluginManager(
|
|||
close: closePlugin,
|
||||
destroyListener,
|
||||
openModal,
|
||||
resizeModal: (width: number, height: number) => {
|
||||
openUISchema.parse({ width, height });
|
||||
|
||||
if (modal) {
|
||||
modal.resize(width, height);
|
||||
}
|
||||
},
|
||||
getModal: () => modal,
|
||||
registerListener,
|
||||
registerMessageCallback,
|
||||
|
|
Loading…
Add table
Reference in a new issue