diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 448bef97..150b14d7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -625,9 +625,9 @@ export function createDraggable({ instance.effects.add(func); }, - propose: (x: number | null, y: number | null) => { - instance.ctx.proposed.x = x; - instance.ctx.proposed.y = y; + propose(x: number | null, y: number | null) { + this.proposed.x = x; + this.proposed.y = y; }, cancel() { @@ -637,6 +637,17 @@ export function createDraggable({ preventStart() { instance.dragstart_prevented = true; }, + + setForcedPosition(x, y) { + this.offset.x = x; + this.offset.y = y; + // Only sync initial with offset when not dragging + // This maintains the drag calculations during active drags + if (!this.isDragging) { + this.initial.x = x; + this.initial.y = y; + } + }, }; // Initial setup diff --git a/packages/core/src/plugins.ts b/packages/core/src/plugins.ts index 3f6d0c5a..eef6e705 100644 --- a/packages/core/src/plugins.ts +++ b/packages/core/src/plugins.ts @@ -21,6 +21,7 @@ export interface PluginContext { propose: (x: number | null, y: number | null) => void; cancel: () => void; preventStart: () => void; + setForcedPosition: (x: number, y: number) => void; } export interface Plugin { @@ -842,44 +843,30 @@ export const controls = unstable_definePlugin< }, }); -export const position = unstable_definePlugin( +export const position = unstable_definePlugin< + [ + options?: { + current?: { x: number; y: number } | null; + default?: { x: number; y: number } | null; + } | null, + ] +>( { name: 'neodrag:position', priority: 1000, liveUpdate: true, setup([options], ctx) { - if (options?.default) { - ctx.offset.x = options.default.x ?? ctx.offset.x; - ctx.offset.y = options.default.y ?? ctx.offset.y; - ctx.initial.x = options.default.x ?? ctx.initial.x; - ctx.initial.y = options.default.y ?? ctx.initial.y; + if (options?.default && !options.current) { + ctx.setForcedPosition(options.default.x ?? ctx.offset.x, options.default.y ?? ctx.offset.y); } if (options?.current) { - ctx.offset.x = options.current.x ?? ctx.offset.x; - ctx.offset.y = options.current.y ?? ctx.offset.y; - } - }, - - drag([options], ctx) { - // Only intervene if position has changed externally - if ( - ctx.isDragging && - options?.current && - (options.current.x !== ctx.offset.x || options.current.y !== ctx.offset.y) - ) { - ctx.propose(options.current.x - ctx.offset.x, options.current.y - ctx.offset.y); - ctx.cancel(); + ctx.setForcedPosition(options.current.x, options.current.y); } }, }, - [{}] as [ - options?: { - current?: { x: number; y: number }; - default?: { x: number; y: number }; - } | null, - ], + [null], ); type TouchActionMode = diff --git a/packages/core/test-app/e2e/plugins/controls.test.ts b/packages/core/test-app/e2e/plugins/controls.test.ts index e5d59a90..f9732383 100644 --- a/packages/core/test-app/e2e/plugins/controls.test.ts +++ b/packages/core/test-app/e2e/plugins/controls.test.ts @@ -104,48 +104,6 @@ test('block-only', async ({ page }) => { await expect(div).toHaveCSS('translate', '20px 20px'); }); -test('block-only', async ({ page }) => { - await setup(page, 'plugins/controls', SCHEMAS.PLUGINS.CONTROLS, { - type: 'allow-block', - }); - - const div = page.getByTestId('draggable'); - const handle = div.locator('.handle'); - const cancel = div.locator('.cancel'); - - await handle.hover(); - let { x, y } = await get_mouse_position(page); - - await page.mouse.down(); - await page.mouse.move(x + 10, y + 10); - await page.mouse.up(); - - // This should have moved. - await expect(div).toHaveCSS('translate', '10px 10px'); - - // Now move to some point within the box, near bottom right and attempt to drag - await page.mouse.move(100, 100); // 10x10px from the corner - - ({ x, y } = await get_mouse_position(page)); - - await page.mouse.down(); - await page.mouse.move(x + 10, y + 10); - await page.mouse.up(); - - // Should have moved - await expect(div).toHaveCSS('translate', '20px 20px'); - - await cancel.hover(); - ({ x, y } = await get_mouse_position(page)); - - await page.mouse.down(); - await page.mouse.move(x + 10, y + 10); - await page.mouse.up(); - - // Should not have moved - await expect(div).toHaveCSS('translate', '20px 20px'); -}); - test('allow-block', async ({ page }) => { await setup(page, 'plugins/controls', SCHEMAS.PLUGINS.CONTROLS, { type: 'allow-block', diff --git a/packages/core/test-app/e2e/plugins/position.test.ts b/packages/core/test-app/e2e/plugins/position.test.ts new file mode 100644 index 00000000..1d8f4b8a --- /dev/null +++ b/packages/core/test-app/e2e/plugins/position.test.ts @@ -0,0 +1,141 @@ +import test, { expect } from '@playwright/test'; +import { get_mouse_position, setup } from '../test-utils'; +import { SCHEMAS } from '../../src/lib/schemas'; + +test('undefined disables the plugin', async ({ page }) => { + await setup(page, 'plugins/position', SCHEMAS.PLUGINS.POSITION, undefined); + + const div = page.getByTestId('draggable'); + + await div.hover(); + const { x, y } = await get_mouse_position(page); + await page.mouse.down(); + await page.mouse.move(x + 100, y + 100); + await page.mouse.up(); + + // This will drag as usual + await expect(div).toHaveCSS('translate', '100px 100px'); +}); + +test('null disables the plugin', async ({ page }) => { + await setup(page, 'plugins/position', SCHEMAS.PLUGINS.POSITION, null); + + const div = page.getByTestId('draggable'); + + await div.hover(); + const { x, y } = await get_mouse_position(page); + await page.mouse.down(); + await page.mouse.move(x + 100, y + 100); + await page.mouse.up(); + + // This will drag as usual + await expect(div).toHaveCSS('translate', '100px 100px'); +}); + +test('default only', async ({ page }) => { + await setup(page, 'plugins/position', SCHEMAS.PLUGINS.POSITION, { + default: { + x: 20, + y: 80, + }, + }); + + const div = page.getByTestId('draggable'); + + // Should be translated by `default` + await expect(div).toHaveCSS('translate', '20px 80px'); + + await div.hover(); + const { x, y } = await get_mouse_position(page); + await page.mouse.down(); + await page.mouse.move(x + 100, y + 100); + await page.mouse.up(); + + // This will drag as usual + await expect(div).toHaveCSS('translate', '120px 180px'); +}); + +test('current only', async ({ page }) => { + await setup(page, 'plugins/position', SCHEMAS.PLUGINS.POSITION, { + current: { + x: 20, + y: 80, + }, + }); + + const div = page.getByTestId('draggable'); + + // Should be translated by `default` + await expect(div).toHaveCSS('translate', '20px 80px'); + + await div.hover(); + const { x, y } = await get_mouse_position(page); + await page.mouse.down(); + await page.mouse.move(x + 100, y + 100); + await page.mouse.up(); + + // This will drag too + await expect(div).toHaveCSS('translate', '120px 180px'); +}); + +test('current-default', async ({ page }) => { + await setup(page, 'plugins/position', SCHEMAS.PLUGINS.POSITION, { + default: { + x: 20, + y: 80, + }, + current: { + x: 100, + y: 180, + }, + }); + + const div = page.getByTestId('draggable'); + + // current ahould be prioritized over default + await expect(div).toHaveCSS('translate', '100px 180px'); + + await div.hover(); + const { x, y } = await get_mouse_position(page); + await page.mouse.down(); + await page.mouse.move(x + 100, y + 100); + await page.mouse.up(); + + // This will drag too + await expect(div).toHaveCSS('translate', '200px 280px'); +}); + +test('two-way-binding', async ({ page }) => { + await setup(page, 'plugins/position', SCHEMAS.PLUGINS.POSITION, { + default: { + x: 20, + y: 80, + }, + current: { + x: 100, + y: 180, + }, + two_way_binding: true, + }); + + const div = page.getByTestId('draggable'); + + // current ahould be prioritized over default + await expect(div).toHaveCSS('translate', '100px 180px'); + + await div.hover(); + const { x, y } = await get_mouse_position(page); + await page.mouse.down(); + await page.mouse.move(x + 100, y + 100); + await page.mouse.up(); + + // This will drag too + await expect(div).toHaveCSS('translate', '200px 280px'); + + const xSlider = page.getByTestId('x-slider'); + const ySlider = page.getByTestId('y-slider'); + + // Check their values, make sure theyre the same as drag translatye + await expect(xSlider).toHaveValue('200'); + await expect(ySlider).toHaveValue('280'); +}); diff --git a/packages/core/test-app/src/lib/ROUTES.ts b/packages/core/test-app/src/lib/ROUTES.ts index 47aaf39c..4064f2e4 100644 --- a/packages/core/test-app/src/lib/ROUTES.ts +++ b/packages/core/test-app/src/lib/ROUTES.ts @@ -16,6 +16,7 @@ const PAGES = { "/plugins/bounds": `/plugins/bounds`, "/plugins/controls": `/plugins/controls`, "/plugins/grid": `/plugins/grid`, + "/plugins/position": `/plugins/position`, "/plugins/threshold": `/plugins/threshold`, "/plugins/transform": `/plugins/transform` } @@ -138,7 +139,7 @@ export function route(key: T, ...params: any[]): strin * ``` */ export type KIT_ROUTES = { - PAGES: { '/': never, '/defaults': never, '/plugins/applyUserSelectHack': never, '/plugins/axis': never, '/plugins/bounds': never, '/plugins/controls': never, '/plugins/grid': never, '/plugins/threshold': never, '/plugins/transform': never } + PAGES: { '/': never, '/defaults': never, '/plugins/applyUserSelectHack': never, '/plugins/axis': never, '/plugins/bounds': never, '/plugins/controls': never, '/plugins/grid': never, '/plugins/position': never, '/plugins/threshold': never, '/plugins/transform': never } SERVERS: Record ACTIONS: Record LINKS: Record diff --git a/packages/core/test-app/src/lib/schemas.ts b/packages/core/test-app/src/lib/schemas.ts index 332a57d0..2c43e197 100644 --- a/packages/core/test-app/src/lib/schemas.ts +++ b/packages/core/test-app/src/lib/schemas.ts @@ -46,5 +46,27 @@ export const SCHEMAS = { 'block-allow-block', ]), }), + + POSITION: z + .object({ + two_way_binding: z.boolean().optional(), + + default: z + .object({ + x: z.number(), + y: z.number(), + }) + .optional() + .nullable(), + + current: z + .object({ + x: z.number(), + y: z.number(), + }) + .optional() + .nullable(), + }) + .optional(), }, }; diff --git a/packages/core/test-app/src/routes/(test)/plugins/position/+page.server.ts b/packages/core/test-app/src/routes/(test)/plugins/position/+page.server.ts new file mode 100644 index 00000000..7c68347f --- /dev/null +++ b/packages/core/test-app/src/routes/(test)/plugins/position/+page.server.ts @@ -0,0 +1,6 @@ +import { extract_options_from_url } from '$lib/helpers.js'; +import { SCHEMAS } from '$lib/schemas.js'; + +export function load({ request }) { + return extract_options_from_url(request, SCHEMAS.PLUGINS.POSITION); +} diff --git a/packages/core/test-app/src/routes/(test)/plugins/position/+page.svelte b/packages/core/test-app/src/routes/(test)/plugins/position/+page.svelte new file mode 100644 index 00000000..96767b18 --- /dev/null +++ b/packages/core/test-app/src/routes/(test)/plugins/position/+page.svelte @@ -0,0 +1,44 @@ + + + + + + + diff --git a/playground/svelte/src/routes/modular/position/+page.svelte b/playground/svelte/src/routes/modular/position/+page.svelte index d264959e..6f159c35 100644 --- a/playground/svelte/src/routes/modular/position/+page.svelte +++ b/playground/svelte/src/routes/modular/position/+page.svelte @@ -1,5 +1,5 @@