Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .better-commits.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"custom_scope": true,
"initial_value": "",
"options": [
{
"value": "di",
"label": "di"
},
{
"value": "dom",
"label": "dom"
Expand All @@ -20,10 +24,18 @@
"value": "react",
"label": "react"
},
{
"value": "router",
"label": "router"
},
{
"value": "rspack",
"label": "rspack"
},
{
"value": "store",
"label": "store"
},
{
"value": "testing",
"label": "testing"
Expand All @@ -32,10 +44,6 @@
"value": "types",
"label": "types"
},
{
"value": "di",
"label": "di"
},
{
"value": "",
"label": "none"
Expand Down
92 changes: 92 additions & 0 deletions docs/docs/react/use-drag-and-drop.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,95 @@ function App() {
return <div ref={ref}>This block is draggable</div>;
}
```

### Controlling drag start

By default hook start dragging immediately on `pointerdown` event.

To disable or change this behavior you can call `preventDefault` on grab event.

For example you can start drag only if some child element is pressed:

```ts
const needStartDrag = event => {
return event.nativeEvent.target.classList.contains('handle');
};

useDragAndDrop(ref, {
onGrab(event) {
if (needStartDrag(event)) {
return event.preventDefault();
}

// ... your state logic
},
});
```

Or you can implement dragging activation only after some shift threshold reached:

```ts
const threshold = 8;

const needStartDrag = event => {
return Math.abs(event.offset.x - event.startOffset.x) >= threshold;
};
```

### Plugins

Hook provides ability to define plugins to extend basic drag-and-drop behavior.

```ts
useDragAndDrop(ref, {
plugins: [myPlugin, myOtherPlugin],
// ...other options
});
```

Plugin can add hooks for some events:

- `init` - here you can add some listeners
- `grab` - calls hook right after dispatching `grab` event
- `move`- calls hook right after dispatching `move` event
- `drop`- calls hook right after dispatching `drop` event
- `destroy` - here you can remove some listeners

By default hook uses some builtin plugins.

You can disable this plugins by passing array:

```ts
useDragAndDrop(ref, {
// no plugins and no builtins
plugins: [],
});

useDragAndDrop(ref, {
// only your plugin and no builtins
plugins: [myPlugin],
});
```

#### Builtin plugin `cleanSelection`

Plugin that cleans selection during drag.

#### Builtin plugin `preventClick`

Plugin that prevents click if target element was moved by drag-and-drop.

#### Builtin plugin `touchScroll`

Plugin that prevents page scroll and "pull-to-refresh" for draggable element.

#### Using builtins selectively

```ts
import { DragAndDropBuiltinPlugins, useDragAndDrop } from '@krutoo/utils/react';

useDragAndDrop(ref, {
// Using only `touchScroll` plugin from builtins
plugins: [DragAndDropBuiltinPlugins.touchScroll],
});
```
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions src/react/drag-and-drop/builtin-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { DragAndDropPlugin } from './types.ts';

export const DragAndDropBuiltinPlugins = {
/**
* Returns plugin that cleans selection during drag-and-drop.
* @returns Plugin.
*/
cleanSelection(): DragAndDropPlugin {
return api => {
const cleanSelection = () => {
document.getSelection()?.removeAllRanges();
};

api.hooks.grab.add(cleanSelection);
api.hooks.move.add(cleanSelection);
};
},

/**
* Returns plugin that prevents click if target element was moved by drag-and-drop.
* @returns Plugin.
*/
preventClick(): DragAndDropPlugin {
return api => {
const onClickCapture = (event: MouseEvent) => {
const state = api.getState();
const distance = state.offset.getDistance(state.startOffset);

if (distance > 0) {
event.preventDefault();
event.stopPropagation();
}
};

api.hooks.init.add(ctx => {
ctx.element.addEventListener('click', onClickCapture, { capture: true });
});
api.hooks.destroy.add(ctx => {
ctx.element.addEventListener('click', onClickCapture, { capture: true });
});
};
},

/**
* Returns plugin that prevents page scroll and "pull-to-refresh" on draggable element.
* @returns Plugin.
*/
touchScroll(): DragAndDropPlugin {
return api => {
let onTouchMove: (event: TouchEvent) => void;

api.hooks.init.add(ctx => {
onTouchMove = (event: TouchEvent) => {
// IMPORTANT: check only inside target
if (!(event.target instanceof Element) || !ctx.element.contains(event.target)) {
return false;
}

if (api.getState().grabbed) {
event.preventDefault();
}
};

window.addEventListener('touchmove', onTouchMove, { passive: false });
});
api.hooks.destroy.add(() => {
window.removeEventListener('touchmove', onTouchMove);
});
};
},
};
47 changes: 47 additions & 0 deletions src/react/drag-and-drop/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Point2d } from '../../mod.ts';
import type { DragAndDropEvent, DragAndDropState } from './types.ts';

export class DnDEvent<E extends Event> implements DragAndDropEvent<E> {
readonly target: HTMLElement;

readonly nativeEvent: E;

readonly clientPosition: Point2d;

readonly grabbed: boolean;

readonly pressed: boolean;

readonly offset: Point2d;

readonly startOffset: Point2d;

readonly innerOffset: Point2d;

protected _defaultPrevented: boolean;

constructor(
target: HTMLElement,
state: DragAndDropState,
nativeEvent: E,
clientPosition: Point2d,
) {
this.target = target;
this.nativeEvent = nativeEvent;
this.clientPosition = clientPosition;
this.grabbed = state.grabbed;
this.pressed = state.pressed;
this.offset = state.offset;
this.startOffset = state.startOffset;
this.innerOffset = state.innerOffset;
this._defaultPrevented = false;
}

get defaultPrevented(): boolean {
return this._defaultPrevented;
}

preventDefault(): void {
this._defaultPrevented = true;
}
}
Loading
Loading