diff --git a/libs/plugins-runtime/src/lib/drag-handler.spec.ts b/libs/plugins-runtime/src/lib/drag-handler.spec.ts
new file mode 100644
index 0000000..6684179
--- /dev/null
+++ b/libs/plugins-runtime/src/lib/drag-handler.spec.ts
@@ -0,0 +1,79 @@
+import { expect, describe, vi } from 'vitest';
+import { dragHandler } from './drag-handler.js';
+
+describe('dragHandler', () => {
+ let element: HTMLElement;
+
+ beforeEach(() => {
+ element = document.createElement('div');
+ document.body.appendChild(element);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(element);
+ vi.clearAllMocks();
+ });
+
+ it('should attach mousedown event listener to the element', () => {
+ const addEventListenerMock = vi.spyOn(element, 'addEventListener');
+
+ dragHandler(element);
+
+ expect(addEventListenerMock).toHaveBeenCalledWith(
+ 'mousedown',
+ expect.any(Function)
+ );
+ });
+
+ it('should update element transform on mousemove', () => {
+ const mouseDownEvent = new MouseEvent('mousedown', {
+ clientX: 100,
+ clientY: 100,
+ });
+
+ dragHandler(element);
+
+ element.dispatchEvent(mouseDownEvent);
+
+ const mouseMoveEvent = new MouseEvent('mousemove', {
+ clientX: 150,
+ clientY: 150,
+ });
+ document.dispatchEvent(mouseMoveEvent);
+
+ expect(element.style.transform).toBe('translate(50px, 50px)');
+
+ const mouseMoveEvent2 = new MouseEvent('mousemove', {
+ clientX: 200,
+ clientY: 200,
+ });
+ document.dispatchEvent(mouseMoveEvent2);
+
+ expect(element.style.transform).toBe('translate(100px, 100px)');
+ });
+
+ it('should remove event listeners on mouseup', () => {
+ const removeEventListenerMock = vi.spyOn(document, 'removeEventListener');
+
+ const mouseDownEvent = new MouseEvent('mousedown', {
+ clientX: 100,
+ clientY: 100,
+ });
+
+ dragHandler(element);
+
+ element.dispatchEvent(mouseDownEvent);
+
+ const mouseUpEvent = new MouseEvent('mouseup');
+ document.dispatchEvent(mouseUpEvent);
+
+ expect(removeEventListenerMock).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function)
+ );
+ expect(removeEventListenerMock).toHaveBeenCalledWith(
+ 'mouseup',
+ expect.any(Function)
+ );
+ });
+});
diff --git a/libs/plugins-runtime/src/lib/drag-handler.ts b/libs/plugins-runtime/src/lib/drag-handler.ts
new file mode 100644
index 0000000..b2d4993
--- /dev/null
+++ b/libs/plugins-runtime/src/lib/drag-handler.ts
@@ -0,0 +1,32 @@
+export const dragHandler = (el: HTMLElement) => {
+ let currentTranslate = { x: 0, y: 0 };
+ let initialTranslate = { x: 0, y: 0 };
+ let initialClientPosition = { x: 0, y: 0 };
+
+ const handleMouseMove = (moveEvent: MouseEvent) => {
+ const { clientX: moveX, clientY: moveY } = moveEvent;
+ const deltaX = moveX - initialClientPosition.x + initialTranslate.x;
+ const deltaY = moveY - initialClientPosition.y + initialTranslate.y;
+
+ currentTranslate = { x: deltaX, y: deltaY };
+
+ el.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
+ };
+
+ const handleMouseUp = () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ };
+
+ const handleMouseDown = (e: MouseEvent) => {
+ initialClientPosition = { x: e.clientX, y: e.clientY };
+ initialTranslate = { x: currentTranslate.x, y: currentTranslate.y };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ };
+
+ el.addEventListener('mousedown', handleMouseDown);
+
+ return handleMouseUp;
+};
diff --git a/libs/plugins-runtime/src/lib/plugin-modal.ts b/libs/plugins-runtime/src/lib/plugin-modal.ts
index 17e4a3f..b477635 100644
--- a/libs/plugins-runtime/src/lib/plugin-modal.ts
+++ b/libs/plugins-runtime/src/lib/plugin-modal.ts
@@ -2,6 +2,7 @@ const closeSvg = `
`;
import type { PenpotTheme } from '@penpot/plugin-types';
+import { dragHandler } from './drag-handler.js';
export class PluginModalElement extends HTMLElement {
constructor() {
@@ -10,6 +11,7 @@ export class PluginModalElement extends HTMLElement {
}
#wrapper: HTMLElement | null = null;
+ #dragEvents: ReturnType | null = null;
setTheme(theme: PenpotTheme) {
if (this.#wrapper) {
@@ -17,6 +19,10 @@ export class PluginModalElement extends HTMLElement {
}
}
+ disconnectedCallback() {
+ this.#dragEvents?.();
+ }
+
connectedCallback() {
const title = this.getAttribute('title');
const iframeSrc = this.getAttribute('iframe-src');
@@ -33,6 +39,7 @@ export class PluginModalElement extends HTMLElement {
this.#wrapper = document.createElement('div');
this.#wrapper.classList.add('wrapper');
+ this.#dragEvents = dragHandler(this.#wrapper);
const header = document.createElement('div');
header.classList.add('header');
@@ -149,6 +156,7 @@ export class PluginModalElement extends HTMLElement {
font-weight: var(--font-weight-bold);
margin: 0;
margin-inline-end: var(--spacing-4);
+ user-select: none;
}
iframe {