@tft/interact 21.0.7 → 21.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -22
- package/fesm2022/tft-interact.mjs +244 -390
- package/fesm2022/tft-interact.mjs.map +1 -1
- package/package.json +1 -1
- package/types/tft-interact.d.ts +112 -116
package/README.md
CHANGED
|
@@ -1,42 +1,337 @@
|
|
|
1
|
+
# @tft/interact
|
|
1
2
|
|
|
2
|
-
|
|
3
|
+
An Angular directive library wrapping [interactjs](https://interactjs.io/) for drag-and-drop, resize, gesture, auto-scroll, and lasso selection.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
We aim to build a versatile API that exposes all the features of interactjs, but also simplifies implementing common behaviors.
|
|
5
|
+
---
|
|
7
6
|
|
|
8
7
|
## Installation
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install @tft/interact interactjs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
All directives are **standalone** — import them individually or use `InteractModule` for NgModule-based apps.
|
|
14
|
+
|
|
15
|
+
**Standalone (recommended):**
|
|
16
|
+
```typescript
|
|
17
|
+
import { DraggableDirective, DropzoneDirective, ResizableDirective } from '@tft/interact';
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
imports: [DraggableDirective, DropzoneDirective, ResizableDirective],
|
|
21
|
+
})
|
|
22
|
+
export class MyComponent {}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**NgModule (legacy):**
|
|
26
|
+
```typescript
|
|
27
|
+
import { InteractModule } from '@tft/interact';
|
|
11
28
|
|
|
12
|
-
|
|
29
|
+
@NgModule({
|
|
30
|
+
imports: [InteractModule.forRoot({ cssDimensionUnit: 'px' })],
|
|
31
|
+
})
|
|
32
|
+
export class AppModule {}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> `InteractModule` is deprecated and will be removed in the next major version. Prefer importing directives directly.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Directives
|
|
13
40
|
|
|
14
|
-
|
|
41
|
+
| Directive | Selector | Purpose |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `DraggableDirective` | `[tftDraggable]` | Makes an element draggable |
|
|
44
|
+
| `DropzoneDirective` | `[tftDropzone]` | Defines a drop target |
|
|
45
|
+
| `ResizableDirective` | `[tftResizable]` | Makes an element resizable |
|
|
46
|
+
| `GesturableDirective` | `[tftGesturable]` | Pinch/rotate gesture support |
|
|
47
|
+
| `DragRootDirective` | `[tftDragRoot]` | Sets where the drag clone is appended |
|
|
48
|
+
| `AccountForScaleDirective` | `[tftAccountForScale]` | Compensates coordinates for CSS `scale()` |
|
|
49
|
+
| `ApplyScaleDirective` | `[tftApplyScale]` | Applies a scale value as `font-size` |
|
|
50
|
+
| `AutoScrollDirective` | `[tftAutoScroll]` | Auto-scrolls a container during drag |
|
|
51
|
+
| `LassoDirective` | `[tftLasso]` | Drag-to-select rectangle on a canvas |
|
|
52
|
+
| `DragPreviewDirective` | `ng-template[tftDragPreview]` | Custom template for the drag clone |
|
|
53
|
+
| `ArrayOfNPipe` | `arrayOfN` | Creates an array of N items for `@for` |
|
|
15
54
|
|
|
16
|
-
|
|
17
|
-
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Draggable
|
|
18
58
|
|
|
19
59
|
```html
|
|
20
60
|
<div tftDraggable
|
|
21
|
-
|
|
22
|
-
[
|
|
23
|
-
[
|
|
61
|
+
[x]="item.x" [y]="item.y"
|
|
62
|
+
[dragData]="item"
|
|
63
|
+
[dragDisabled]="isLocked"
|
|
64
|
+
(dragEnd)="onDragEnd($event)">
|
|
65
|
+
</div>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Key inputs:**
|
|
69
|
+
|
|
70
|
+
| Input | Type | Default | Description |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `x` | `number` | — | Initial x position |
|
|
73
|
+
| `y` | `number` | — | Initial y position |
|
|
74
|
+
| `dragData` | `D` | — | Payload attached to drag events |
|
|
75
|
+
| `dragDisabled` | `boolean` | `false` | Disables drag |
|
|
76
|
+
| `dragZIndex` | `number` | `10000` | z-index while dragging |
|
|
77
|
+
| `dragConfig` | `DraggableOptions` | — | Raw interactjs config (inertia, modifiers, etc.) |
|
|
78
|
+
| `showPlaceholder` | `boolean` | `false` | Keep original element visible while dragging |
|
|
79
|
+
| `dragRoot` | `DragRootDirective` | — | Override drag root element |
|
|
80
|
+
| `enableDragDefault` | `boolean` | `true` | Use built-in clone+position behaviour |
|
|
81
|
+
|
|
82
|
+
**Outputs:** `dragStart`, `dragMove`, `dragInertiaStart`, `dragEnd` — all emit `TftDragEvent`.
|
|
83
|
+
|
|
84
|
+
**Inertia + restrict example:**
|
|
85
|
+
```typescript
|
|
86
|
+
dragConfig: DraggableOptions = {
|
|
87
|
+
inertia: { resistance: 2, allowResume: true, endSpeed: 10, smoothEndDuration: 2000 },
|
|
88
|
+
modifiers: [
|
|
89
|
+
interact.modifiers.restrict({
|
|
90
|
+
restriction: '.board',
|
|
91
|
+
endOnly: false,
|
|
92
|
+
elementRect: { top: 0, left: 0, bottom: 1, right: 1 },
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Dropzone
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<div tftDropzone
|
|
104
|
+
[dropzoneId]="'yard'"
|
|
105
|
+
[dropzoneConfig]="{ overlap: 0.5 }"
|
|
106
|
+
(dragDrop)="onDrop($event)"
|
|
107
|
+
(dragEnter)="onEnter($event)"
|
|
108
|
+
(dragLeave)="onLeave($event)">
|
|
24
109
|
</div>
|
|
25
110
|
```
|
|
26
111
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
112
|
+
**Key inputs:** `dropzoneId`, `dropzoneConfig` (`DropzoneOptions`), `dropzoneData`, `position` (CSS position, default `'relative'`).
|
|
113
|
+
|
|
114
|
+
**Outputs:** `dragDrop`, `dragEnter`, `dragLeave`, `dropActivate` — all emit `TftDropEvent`.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
onDrop(event: TftDropEvent) {
|
|
118
|
+
if (event.dropTarget?.dropzoneId() === 'yard') { ... }
|
|
119
|
+
const data = event.dragRef.dragData();
|
|
32
120
|
}
|
|
33
121
|
```
|
|
34
|
-
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Resizable
|
|
35
126
|
|
|
36
127
|
```html
|
|
37
|
-
<div tftResizable
|
|
128
|
+
<div tftResizable
|
|
129
|
+
[width]="w" [height]="h"
|
|
130
|
+
[resizeConfig]="{ edges: { right: true, bottom: true } }"
|
|
131
|
+
(resizeEnd)="onResizeEnd($event)">
|
|
132
|
+
</div>
|
|
38
133
|
```
|
|
39
134
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
135
|
+
**Key inputs:** `width`, `height`, `resizeConfig` (`ResizableOptions`), `resizeDisabled`, `position`, `enableResizeDefault`.
|
|
136
|
+
|
|
137
|
+
**Outputs:** `resizeStart`, `resizeMove`, `resizeInertiaStart`, `resizeEnd` — all emit `TftResizeEvent`.
|
|
138
|
+
|
|
139
|
+
`TftResizeEvent` includes `size: { width, height }` and `positionInDropTarget`.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Scale Support
|
|
144
|
+
|
|
145
|
+
Use `tftAccountForScale` on the drag root when CSS `scale()` is applied:
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<div tftDragRoot tftAccountForScale [scale]="scale">
|
|
149
|
+
<div tftDraggable tftResizable [width]="w" [height]="h"></div>
|
|
150
|
+
</div>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For cross-dropzone dragging where the draggable is not a child of the drag root:
|
|
154
|
+
```html
|
|
155
|
+
<div tftDragRoot tftAccountForScale [scale]="scale" #root="tftDragRoot">
|
|
156
|
+
<div tftDropzone>
|
|
157
|
+
<div tftDraggable [dragRoot]="root"></div>
|
|
158
|
+
</div>
|
|
159
|
+
<div tftDropzone (dragDrop)="onDrop($event)"></div>
|
|
160
|
+
</div>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Gesture (pinch-to-zoom)
|
|
166
|
+
|
|
167
|
+
```html
|
|
168
|
+
<div tftDragRoot tftGesturable tftAccountForScale
|
|
169
|
+
[scale]="scale"
|
|
170
|
+
[style.transform]="'scale(' + scale + ')'"
|
|
171
|
+
[style.transform-origin]="transformOrigin"
|
|
172
|
+
(gestureMove)="onGestureMove($event)">
|
|
173
|
+
</div>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
onGestureMove(event: TftGestureEvent) {
|
|
178
|
+
this.scale += event.interactEvent?.ds || 0; // ds = delta scale
|
|
179
|
+
const { x0, y0 } = event.interactEvent;
|
|
180
|
+
this.transformOrigin = `${x0}px ${y0}px`;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Auto-Scroll
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<div class="scroll-container" tftAutoScroll>
|
|
190
|
+
<div tftDragRoot>
|
|
191
|
+
<div tftDraggable></div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Configure scroll sensitivity via `[autoScrollConfig]`:
|
|
197
|
+
```typescript
|
|
198
|
+
autoScrollConfig = { margin: 50, distance: 3, interval: 50, speed: 300, enabled: true };
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Drag Preview Template
|
|
204
|
+
|
|
205
|
+
```html
|
|
206
|
+
<div tftDraggable [dragData]="item">
|
|
207
|
+
<ng-template tftDragPreview>
|
|
208
|
+
<div class="drag-ghost">{{ item.name }}</div>
|
|
209
|
+
</ng-template>
|
|
210
|
+
Original content
|
|
211
|
+
</div>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Lasso (Drag-to-Select)
|
|
217
|
+
|
|
218
|
+
`tftLasso` tracks a pointer drag on a background canvas and emits the selection rectangle. Hit-testing against child elements is the consumer's responsibility.
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<div tftLasso #lasso="tftLasso" [scale]="scale"
|
|
222
|
+
(lassoStart)="clearSelection()"
|
|
223
|
+
(lassoCommit)="onLassoCommit($event)">
|
|
224
|
+
|
|
225
|
+
@if (lasso.lassoDisplayRect(); as r) {
|
|
226
|
+
<div class="lasso-rect"
|
|
227
|
+
[style.left.px]="r.x" [style.top.px]="r.y"
|
|
228
|
+
[style.width.px]="r.w" [style.height.px]="r.h">
|
|
229
|
+
</div>
|
|
230
|
+
}
|
|
231
|
+
</div>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
onLassoCommit(rect: LassoRect) {
|
|
236
|
+
// rect is in viewport (clientX/Y) space
|
|
237
|
+
this.selected = this.items.filter(item => {
|
|
238
|
+
const b = this.itemEl(item).getBoundingClientRect();
|
|
239
|
+
return b.left < rect.right && b.right > rect.left &&
|
|
240
|
+
b.top < rect.bottom && b.bottom > rect.top;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Inputs:** `scale` (default `1`), `lassoDisabled` (default `false`).
|
|
246
|
+
**Outputs:** `lassoStart` (`void`), `lassoCommit` (`LassoRect`).
|
|
247
|
+
**Exported signals:** `isLassoing`, `lassoDisplayRect` (null when not dragging).
|
|
248
|
+
|
|
249
|
+
Uses Pointer Events — works with mouse, touch, and stylus.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Event Types
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
TftDragEvent {
|
|
257
|
+
interactEvent: NgDragEvent;
|
|
258
|
+
dragRef: DraggableDirective; // dragRef.dragData() to get payload
|
|
259
|
+
dragOrigin?: DropzoneDirective;
|
|
260
|
+
dropTarget?: DropzoneDirective;
|
|
261
|
+
positionInDropTarget: { x, y } | null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
TftDropEvent // same shape as TftDragEvent
|
|
265
|
+
|
|
266
|
+
TftResizeEvent {
|
|
267
|
+
interactEvent: NgResizeEvent;
|
|
268
|
+
resizeRef: ResizableDirective;
|
|
269
|
+
dragRef?: DraggableDirective;
|
|
270
|
+
dragOrigin?: DropzoneDirective;
|
|
271
|
+
dropTarget?: DropzoneDirective;
|
|
272
|
+
positionInDropTarget: { x, y } | null;
|
|
273
|
+
size: { width: number; height: number };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
TftGestureEvent {
|
|
277
|
+
gestureRef: GesturableDirective;
|
|
278
|
+
interactEvent: GestureEvent; // .ds = delta scale, .x0/.y0 = touch origin
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
LassoRect {
|
|
282
|
+
left: number; right: number;
|
|
283
|
+
top: number; bottom: number; // all in viewport (clientX/Y) space
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Migration to v21.1.0
|
|
290
|
+
|
|
291
|
+
Version 21.1.0 converted all directives to **standalone** and replaced `@Input()`/`@Output()` with signal-based `input()`/`output()`.
|
|
292
|
+
|
|
293
|
+
### Import directives directly
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// BEFORE
|
|
297
|
+
@NgModule({ imports: [InteractModule.forRoot({ cssDimensionUnit: 'px' })] })
|
|
298
|
+
|
|
299
|
+
// AFTER
|
|
300
|
+
@Component({ imports: [DraggableDirective, DropzoneDirective, ResizableDirective, ...] })
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`InteractModule` still works but is deprecated. `InteractService` is `providedIn: 'root'`.
|
|
304
|
+
|
|
305
|
+
### `dragData` and `dropzoneData` are now signals
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// BEFORE
|
|
309
|
+
const team = event.dragRef.dragData.team;
|
|
310
|
+
const points = event.dropTarget.dropzoneData.points;
|
|
311
|
+
|
|
312
|
+
// AFTER
|
|
313
|
+
const team = event.dragRef.dragData().team;
|
|
314
|
+
const points = event.dropTarget.dropzoneData().points;
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### `dropzoneId` is now a signal
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// BEFORE
|
|
321
|
+
if (event.dropTarget?.dropzoneId === 'yard') { ... }
|
|
322
|
+
|
|
323
|
+
// AFTER
|
|
324
|
+
if (event.dropTarget?.dropzoneId() === 'yard') { ... }
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### `autoScrollConfig` and `scale` are now signals
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// BEFORE
|
|
331
|
+
const config = this.autoScrollDir.autoScrollConfig;
|
|
332
|
+
const scale = this.accountForScaleDir.scale;
|
|
333
|
+
|
|
334
|
+
// AFTER
|
|
335
|
+
const config = this.autoScrollDir.autoScrollConfig();
|
|
336
|
+
const scale = this.accountForScaleDir.scale();
|
|
337
|
+
```
|