Skip to content

Commit

Permalink
Multi-drag, some
Browse files Browse the repository at this point in the history
  • Loading branch information
PuruVJ committed Dec 21, 2024
1 parent 9ce6d48 commit 5613c08
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 154 deletions.
10 changes: 5 additions & 5 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": {
"production": "./dist/index.js",
"development": "./dist/index.js"
},
"default": "./dist/index.js"
"import": "./dist/index.js"
},
"./plugins": {
"types": "./dist/plugins.d.ts",
"import": "./dist/plugins.js"
},
"./package.json": "./package.json"
},
Expand Down
79 changes: 59 additions & 20 deletions packages/core/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import type { Plugin, PluginContext } from './plugins.ts';
import {
applyUserSelectHack,
ignoreMultitouch,
stateMarker,
threshold,
touchAction,
transform,
type Plugin,
type PluginContext,
} from './plugins.ts';
import { is_svg_element, is_svg_svg_element, listen } from './utils.ts';

type DeepMutable<T> = T extends object
Expand Down Expand Up @@ -48,7 +57,9 @@ export function createDraggable({
} = {}) {
const instances = new WeakMap<HTMLElement | SVGElement, DraggableInstance>();
let listeners_initialized = false;
let active_node: HTMLElement | SVGElement | null = null;

/** track multiple active nodes by pointerId */
const active_nodes = new Map<number, HTMLElement | SVGElement>();

function resultify<T>(fn: () => T, errorInfo: Omit<ErrorInfo, 'error'>): Result<T> {
try {
Expand Down Expand Up @@ -125,12 +136,13 @@ export function createDraggable({
instance.effects.clear();
}

function cleanup_active_node() {
function cleanup_active_node(pointer_id: number) {
// If no node is currently being dragged, nothing to clean up
if (!active_node) return;
const node = active_nodes.get(pointer_id);
if (!node) return;

// Get the instance associated with the active node
const instance = instances.get(active_node);
const instance = instances.get(node);
if (!instance) return;

// If we have captured pointer events, release them
Expand All @@ -145,7 +157,7 @@ export function createDraggable({
},
{
phase: 'dragEnd',
node: active_node,
node,
},
);
}
Expand All @@ -155,7 +167,7 @@ export function createDraggable({
instance.ctx.isDragging = false; // No longer dragging
instance.dragstart_prevented = false; // Reset prevention flag
instance.pointer_captured_id = null; // Clear pointer ID
active_node = null; // Clear active node reference
active_nodes.delete(pointer_id); // Clear active node reference
clear_effects(instance); // Clear any pending effects
}

Expand All @@ -174,7 +186,7 @@ export function createDraggable({
if (!should_drag) return;

instance.ctx.isInteracting = true;
active_node = draggable_node;
active_nodes.set(e.pointerId, draggable_node);

const capture_result = resultify(
() => {
Expand All @@ -188,7 +200,7 @@ export function createDraggable({
);

if (!capture_result.ok) {
cleanup_active_node();
cleanup_active_node(e.pointerId);
return;
}

Expand Down Expand Up @@ -217,9 +229,10 @@ export function createDraggable({
}

function handle_pointer_move(e: PointerEvent) {
if (!active_node) return;
const draggable_node = active_nodes.get(e.pointerId);
if (!draggable_node) return;

const instance = instances.get(active_node)!;
const instance = instances.get(draggable_node)!;
if (!instance.ctx.isInteracting) return;

instance.ctx.lastEvent = e;
Expand Down Expand Up @@ -260,13 +273,14 @@ export function createDraggable({
}

function handle_pointer_up(e: PointerEvent) {
if (!active_node) return;
const draggable_node = active_nodes.get(e.pointerId);
if (!draggable_node) return;

const instance = instances.get(active_node)!;
const instance = instances.get(draggable_node)!;
if (!instance.ctx.isInteracting) return;

if (instance.ctx.isDragging) {
listen(active_node as HTMLElement, 'click', (e) => e.stopPropagation(), {
listen(draggable_node as HTMLElement, 'click', (e) => e.stopPropagation(), {
once: true,
signal: instance.controller.signal,
capture: true,
Expand Down Expand Up @@ -353,8 +367,11 @@ export function createDraggable({
const new_plugin_list = initialize_plugins(new_plugins);
let has_changes = false;

// Check if this instance is currently involved in any drag operation
const is_active = Array.from(active_nodes.values()).includes(instance.root_node);

// During drag, only update plugins that opted into live updates
if (instance.ctx.isDragging || instance.ctx.isInteracting) {
if (is_active && (instance.ctx.isDragging || instance.ctx.isInteracting)) {
const updated_plugins = new_plugin_list.filter((plugin) => plugin.liveUpdate);

for (const plugin of updated_plugins) {
Expand All @@ -365,8 +382,13 @@ export function createDraggable({
}

// If we made changes and we're the active node, re-run drag
if (has_changes && active_node === instance.root_node && instance.ctx.lastEvent) {
handle_pointer_move(instance.ctx.lastEvent);
if (has_changes) {
for (const node of active_nodes.values()) {
if (node === instance.root_node && instance.ctx.lastEvent) {
handle_pointer_move(instance.ctx.lastEvent);
break;
}
}
}

return;
Expand Down Expand Up @@ -481,10 +503,12 @@ export function createDraggable({
instances.set(node, instance);

return {
update: () => update(instance),
update: (new_opts: Plugin[]) => update(instance, new_opts),
destroy() {
if (active_node === node) {
active_node = null;
for (const [pointer_id, active_node] of active_nodes) {
if (active_node === node) {
cleanup_active_node(pointer_id);
}
}

for (const plugin of instance.plugins) {
Expand All @@ -497,3 +521,18 @@ export function createDraggable({
},
};
}

export const DEFAULT_plugins = [
ignoreMultitouch(),
stateMarker(),
applyUserSelectHack(),
transform(),
threshold(),
touchAction(),
];

export const DEFAULT_onError = (error: ErrorInfo) => {
console.error(error);
};

export const DEFAULT_delegate = () => document.body;
25 changes: 0 additions & 25 deletions packages/core/src/index.ts

This file was deleted.

15 changes: 13 additions & 2 deletions packages/core/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import { core_config } from '../config';
import { defineConfig } from 'tsup';

export default core_config({ modularAsIndex: false });
export default defineConfig([
{
entry: {
index: `./src/core.ts`,
plugins: `./src/plugins.ts`,
},
format: 'esm',
dts: { resolve: true },
clean: true,
treeshake: 'smallest',
},
]);
2 changes: 1 addition & 1 deletion packages/svelte/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"dependencies": {
"@neodrag/core": "workspace:*"
"@neodrag/svelte": "workspace:*"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/demo/src/routes/modular/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
position,
threshold,
scrollLock,
} from '@neodrag/core';
} from '@neodrag/svelte';
let element = $state<HTMLElement>();
Expand Down
9 changes: 3 additions & 6 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": {
"production": "./dist/min/index.js",
"development": "./dist/index.js"
},
"default": "./dist/min/index.js"
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
Expand Down Expand Up @@ -49,7 +46,7 @@
"bugs": {
"url": "https://github.com/PuruVJ/neodrag/issues"
},
"devDependencies": {
"dependencies": {
"@neodrag/core": "workspace:*"
},
"homepage": "https://neodrag.dev/docs/svelte"
Expand Down
102 changes: 13 additions & 89 deletions packages/svelte/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,13 @@
import {
draggable as core_draggable,
Plugin,
type DragEventData,
} from '@neodrag/core';

//!THIS IS HACK, WE WANNA IMPORT THE TYPE WHEN THE ISSUE IS FIXED LATER
/**
* Actions can return an object containing the two properties defined in this interface. Both are optional.
* - update: An action can have a parameter. This method will be called whenever that parameter changes,
* immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn<undefined>` both
* mean that the action accepts no parameters.
* - destroy: Method that is called after the element is unmounted
*
* Additionally, you can specify which additional attributes and events the action enables on the applied element.
* This applies to TypeScript typings only and has no effect at runtime.
*
* Example usage:
* ```ts
* interface Attributes {
* newprop?: string;
* 'on:event': (e: CustomEvent<boolean>) => void;
* }
*
* export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn<Parameter, Attributes> {
* // ...
* return {
* update: (updatedParameter) => {...},
* destroy: () => {...}
* };
* }
* ```
*
* Docs: https://svelte.dev/docs/svelte-action
*/
export interface ActionReturn<
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>,
> {
update?: (parameter: Parameter) => void;
destroy?: () => void;
/**
* ### DO NOT USE THIS
* This exists solely for type-checking and has no effect at runtime.
* Set this through the `Attributes` generic instead.
*/
$$_attributes?: Attributes;
}

/**
* Actions are functions that are called when an element is created.
* You can use this interface to type such actions.
* The following example defines an action that only works on `<div>` elements
* and optionally accepts a parameter which it has a default value for:
* ```ts
* export const myAction: Action<HTMLDivElement, { someProperty: boolean } | undefined> = (node, param = { someProperty: true }) => {
* // ...
* }
* ```
* `Action<HTMLDivElement>` and `Action<HTMLDivElement, undefined>` both signal that the action accepts no parameters.
*
* You can return an object with methods `update` and `destroy` from the function and type which additional attributes and events it has.
* See interface `ActionReturn` for more details.
*
* Docs: https://svelte.dev/docs/svelte-action
*/
export interface Action<
Element = HTMLElement,
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>,
> {
<Node extends Element>(
...args: undefined extends Parameter
? [node: Node, parameter?: Parameter]
: [node: Node, parameter: Parameter]
): void | ActionReturn<Parameter, Attributes>;
}

export const draggable = core_draggable as Action<
HTMLElement,
Plugin[] | undefined,
{
onneodrag_start: (e: CustomEvent<DragEventData>) => void;
onneodrag: (e: CustomEvent<DragEventData>) => void;
onneodrag_end: (e: CustomEvent<DragEventData>) => void;
}
>;

export * from '@neodrag/core';
import { createDraggable, DEFAULT_delegate, DEFAULT_onError, DEFAULT_plugins } from '@neodrag/core';
import type { Plugin } from '@neodrag/core/plugins';
import type { Action } from 'svelte/action';

const { draggable: core, instances } = createDraggable({
delegate: DEFAULT_delegate,
plugins: DEFAULT_plugins,
onError: DEFAULT_onError,
});

export const draggable = core as Action<HTMLElement | SVGElement, Plugin[] | undefined>;
export { instances };
export * from '@neodrag/core/plugins';
13 changes: 11 additions & 2 deletions packages/svelte/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { core_config } from '../config';
import { defineConfig } from 'tsup';

export default core_config({});
export default defineConfig([
{
entry: [`./src/index.ts`],
format: 'esm',
dts: { resolve: true },
external: ['svelte/action', '@neodrag/core'],
clean: true,
treeshake: 'smallest',
},
]);
Loading

0 comments on commit 5613c08

Please sign in to comment.