@tiptap/extension-drag-handle 3.22.4 → 3.23.1

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/dist/index.d.ts CHANGED
@@ -5,42 +5,76 @@ import { EditorView } from '@tiptap/pm/view';
5
5
  import { PluginKey, Plugin } from '@tiptap/pm/state';
6
6
 
7
7
  /**
8
- * Context provided to each rule for evaluation.
9
- * Contains all information needed to make a decision.
8
+ * Context provided to each rule evaluation function.
9
+ *
10
+ * Contains information about the node being evaluated and its position in the
11
+ * ProseMirror document tree. This is the full context available for making
12
+ * scoring decisions in custom `DragHandleRule` implementations.
13
+ *
14
+ * @example
15
+ * // Typical usage in a custom rule
16
+ * evaluate: ({ node, parent, depth, isFirst }) => {
17
+ * if (parent?.type.name === 'listItem' && isFirst) {
18
+ * return 1000 // exclude first child of list items
19
+ * }
20
+ * if (depth > 3) {
21
+ * return depth * 200 // deprioritize deep nesting
22
+ * }
23
+ * return 0
24
+ * }
10
25
  */
11
26
  interface RuleContext {
12
- /** The node being evaluated */
27
+ /** The ProseMirror node being evaluated as a potential drag target */
13
28
  node: Node;
14
29
  /** Absolute position of the node in the document */
15
30
  pos: number;
16
- /** Depth in the document tree (0 = doc root) */
31
+ /**
32
+ * Depth in the document tree (0 = document root).
33
+ * A paragraph inside a listItem inside a bulletList has depth 3.
34
+ */
17
35
  depth: number;
18
- /** Parent node (null if this is the doc) */
36
+ /**
37
+ * Parent node of the node being evaluated.
38
+ * `null` if the node is the document root (depth 0).
39
+ */
19
40
  parent: Node | null;
20
- /** This node's index among siblings (0-based) */
41
+ /** This node's index among its parent's children (0-based) */
21
42
  index: number;
22
- /** Convenience: true if index === 0 */
43
+ /** Convenience: `true` when this node is the first child of its parent (index === 0) */
23
44
  isFirst: boolean;
24
- /** Convenience: true if this is the last child */
45
+ /** Convenience: `true` when this node is the last child of its parent */
25
46
  isLast: boolean;
26
- /** The resolved position for advanced queries */
47
+ /**
48
+ * The resolved position for advanced ProseMirror queries.
49
+ * Allows access to ancestor nodes, child nodes, and document structure
50
+ * beyond the current node.
51
+ */
27
52
  $pos: ResolvedPos;
28
- /** Editor view for DOM access if needed */
53
+ /**
54
+ * The editor view for DOM access if needed in custom rules.
55
+ * Can be used to access the editor DOM element, measure dimensions, etc.
56
+ */
29
57
  view: EditorView;
30
58
  }
31
59
  /**
32
60
  * A rule that determines whether a node should be a drag target.
61
+ *
62
+ * Each rule receives a `RuleContext` and returns a numeric deduction.
63
+ * Multiple rules are evaluated in sequence; the total deduction is subtracted
64
+ * from the node's base score (1000). If the score drops to 0 or below,
65
+ * the node is excluded as a drag target.
33
66
  */
34
67
  interface DragHandleRule {
35
68
  /**
36
69
  * Unique identifier for debugging and rule management.
70
+ * Choose a descriptive name that explains what the rule does.
37
71
  */
38
72
  id: string;
39
73
  /**
40
74
  * Evaluate the node and return a score deduction.
41
75
  *
42
- * The return value is subtracted from the node's score (which starts at 1000).
43
- * Higher deductions make the node less likely to be selected as the drag target.
76
+ * The return value is subtracted from the node's base score (1000).
77
+ * Higher deductions make the node less likely to be selected.
44
78
  *
45
79
  * @returns A number representing the score deduction:
46
80
  * - `0` - No deduction, node remains fully eligible
@@ -59,7 +93,6 @@ interface DragHandleRule {
59
93
  * @example
60
94
  * // Prefer shallower nodes with partial deduction
61
95
  * evaluate: ({ depth }) => {
62
- * // Deeper nodes get small deductions, making shallower nodes win ties
63
96
  * return depth * 50
64
97
  * }
65
98
  *
@@ -67,7 +100,6 @@ interface DragHandleRule {
67
100
  * // Context-based partial deductions
68
101
  * evaluate: ({ node, parent }) => {
69
102
  * if (parent?.type.name === 'tableCell') {
70
- * // Inside table cells, slightly prefer the cell over its content
71
103
  * return node.type.name === 'paragraph' ? 100 : 0
72
104
  * }
73
105
  * return 0
@@ -78,39 +110,216 @@ interface DragHandleRule {
78
110
 
79
111
  /**
80
112
  * Edge detection presets for common use cases.
113
+ *
114
+ * Edge detection helps you grab parent containers (lists, blockquotes, etc.)
115
+ * by moving the cursor near the edge of a nested element. When the cursor is
116
+ * within the `threshold` zone of a configured edge, the scoring system deducts
117
+ * `strength * depth` from deeper nodes, making the outer container the easier
118
+ * target.
119
+ *
120
+ * In short: cursor near edge prefers parent; cursor centered prefers child.
121
+ *
122
+ * @example
123
+ * // Left/top edges, natural for LTR layouts (default)
124
+ * DragHandle.configure({
125
+ * nested: {
126
+ * edgeDetection: 'left',
127
+ * },
128
+ * })
129
+ *
130
+ * @example
131
+ * // Right/top edges, for RTL layouts
132
+ * DragHandle.configure({
133
+ * nested: {
134
+ * edgeDetection: 'right',
135
+ * },
136
+ * })
137
+ *
138
+ * @example
139
+ * // No edge detection, cursor position does not affect scoring
140
+ * DragHandle.configure({
141
+ * nested: {
142
+ * edgeDetection: 'none',
143
+ * },
144
+ * })
81
145
  */
82
146
  type EdgeDetectionPreset = 'left' | 'right' | 'both' | 'none';
83
147
  /**
84
- * Advanced edge detection configuration.
85
- * Most users should use presets instead.
148
+ * Advanced edge detection configuration for fine-grained control.
149
+ *
150
+ * Use this interface when the preset strings (\`'left'\`, \`'right'\`, etc.) aren't
151
+ * enough and you need to customize **which edges**, **how wide the zone is**,
152
+ * or **how aggressive** the parent preference should be.
153
+ *
154
+ * Most users should use \`EdgeDetectionPreset\` strings instead of this interface.
155
+ * Only reach for this when you need precise control.
156
+ *
157
+ * @example
158
+ * // Wider edge zone, gentler deduction, top/bottom edges only
159
+ * DragHandle.configure({
160
+ * nested: {
161
+ * edgeDetection: {
162
+ * edges: ['top', 'bottom'],
163
+ * threshold: 24,
164
+ * strength: 300,
165
+ * },
166
+ * },
167
+ * })
168
+ *
169
+ * @example
170
+ * // Aggressive left-edge only: narrow zone, strong deduction
171
+ * DragHandle.configure({
172
+ * nested: {
173
+ * edgeDetection: {
174
+ * edges: ['left'],
175
+ * threshold: 8,
176
+ * strength: 800,
177
+ * },
178
+ * },
179
+ * })
86
180
  */
87
181
  interface EdgeDetectionConfig {
88
182
  /**
89
183
  * Which edges trigger parent preference.
184
+ * - `'left'`: Cursor within threshold pixels of the element's left edge
185
+ * - `'right'`: Cursor within threshold pixels of the element's right edge
186
+ * - `'top'`: Cursor within threshold pixels of the element's top edge
187
+ * - `'bottom'`: Cursor within threshold pixels of the element's bottom edge
188
+ *
90
189
  * @default ['left', 'top']
91
190
  */
92
191
  edges: Array<'left' | 'right' | 'top' | 'bottom'>;
93
192
  /**
94
- * Distance in pixels from edge to trigger.
193
+ * Distance in pixels from the element edge that triggers the deduction.
194
+ *
195
+ * Think of this as the size of an invisible "edge zone" around the element.
196
+ * When the cursor is inside this zone, `strength * depth` is deducted from
197
+ * deeper nodes, making parent containers easier to grab.
198
+ *
199
+ * - **Higher value** (e.g., 24): The zone is wider, edge detection triggers
200
+ * even when the cursor is relatively far from the element's edge. Parent
201
+ * selection feels more "eager."
202
+ * - **Lower value** (e.g., 6): The zone is narrower, the cursor must be
203
+ * very close to the edge before parent preference kicks in. You need to be
204
+ * more deliberate to grab a parent container.
205
+ *
206
+ * @example
207
+ * // threshold: 12 means the cursor must be within 12px of the edge
208
+ * // threshold: 24 doubles the trigger zone
209
+ *
95
210
  * @default 12
96
211
  */
97
212
  threshold: number;
98
213
  /**
99
- * How strongly to prefer parent (higher = stronger preference).
100
- * This is multiplied by depth, so deeper nodes are affected more.
214
+ * How strongly to prefer parent nodes near edges (higher = stronger preference).
215
+ *
216
+ * The deduction formula is: `strength * depth`. This means the penalty grows
217
+ * linearly with nesting depth, making deeply nested children less attractive
218
+ * targets when you're near an edge, exactly what you want when trying to
219
+ * grab the outer list rather than the inner paragraph.
220
+ *
221
+ * **Visual guide, default strength (500):**
222
+ * ```
223
+ * Depth | Deduction | Eligible?
224
+ * ──────┼───────────┼──────────
225
+ * 1 | 500 │ Yes, still a valid target
226
+ * 2 | 1000 │ No, penalty matches base score
227
+ * 3 | 1500 │ No, penalty exceeds base score
228
+ * 4 | 2000 │ No, deeply buried
229
+ * ```
230
+ *
231
+ * **Lower strength (200):**
232
+ * ```
233
+ * Depth | Deduction | Eligible?
234
+ * ──────┼───────────┼──────────
235
+ * 1 | 200 │ Yes
236
+ * 2 | 400 │ Yes
237
+ * 3 | 600 │ Yes
238
+ * 4 | 800 │ Yes (but parent still preferred)
239
+ * 5 | 1000 │ No, excluded at threshold
240
+ * ```
241
+ * Good when you want edge detection to nudge toward parents without
242
+ * excluding typical nesting depths.
243
+ *
244
+ * **Higher strength (1000):**
245
+ * ```
246
+ * Depth | Deduction | Eligible?
247
+ * ──────┼───────────┼──────────
248
+ * 1 | 1000 │ No, excluded at threshold
249
+ * ```
250
+ * Every non-doc candidate near the edge is excluded from being a drag
251
+ * target. Use when you want edge detection to completely disable nested
252
+ * dragging near the edges and force root-level handles.
253
+ *
101
254
  * @default 500
102
255
  */
103
256
  strength: number;
104
257
  }
105
258
  /**
106
259
  * Configuration for nested drag handle behavior.
260
+ *
261
+ * When enabled, the drag handle can target nodes at any depth in the document
262
+ * tree (not just top-level blocks). A rule-based scoring system evaluates all
263
+ * ancestor nodes at the cursor position and selects the best drag target.
264
+ *
265
+ * **How the scoring works:**
266
+ * 1. Each ancestor node at the cursor position starts with a base score of 1000
267
+ * 2. Default rules are applied first (subtracting deductions for lists, tables, etc.)
268
+ * 3. Your custom rules are applied next (for app-specific logic)
269
+ * 4. Edge detection adds a final deduction (`strength * depth`) when near element edges
270
+ * 5. The highest-scoring node wins; ties are broken by depth (deeper nodes win)
271
+ * 6. Any node with a score of 0 or below is excluded as a drag target
272
+ *
273
+ * @example
274
+ * // Simple enable with sensible defaults
275
+ * DragHandle.configure({
276
+ * nested: true,
277
+ * })
278
+ *
279
+ * @example
280
+ * // Full custom configuration
281
+ * DragHandle.configure({
282
+ * nested: {
283
+ * defaultRules: true,
284
+ * allowedContainers: ['bulletList', 'orderedList', 'blockquote'],
285
+ * edgeDetection: 'left',
286
+ * rules: [
287
+ * {
288
+ * id: 'myCustomRule',
289
+ * evaluate: ({ node }) =>
290
+ * node.type.name === 'myCustomBlock' ? 1000 : 0,
291
+ * },
292
+ * ],
293
+ * },
294
+ * })
107
295
  */
108
296
  interface NestedOptions {
109
297
  /**
110
- * Additional rules to determine which nodes are draggable.
111
- * These run AFTER the default rules.
298
+ * Custom rules that determine which nodes are draggable.
299
+ *
300
+ * Rules are evaluated AFTER the default rules. Each rule receives a
301
+ * `RuleContext` and returns a score deduction:
302
+ * - `0`: No effect, node remains fully eligible
303
+ * - `1-999`: Partial deduction, node is less preferred but still eligible
304
+ * - `>= 1000`: Node is **excluded** from being a drag target
305
+ *
306
+ * Common use cases for custom rules:
307
+ * - Exclude specific node types from being draggable
308
+ * - Deprioritize certain nodes with partial deductions
309
+ * - Scope dragging to specific document structures
112
310
  *
113
311
  * @example
312
+ * // Exclude code blocks from being draggable
313
+ * rules: [
314
+ * {
315
+ * id: 'excludeCodeBlocks',
316
+ * evaluate: ({ node }) =>
317
+ * node.type.name === 'codeBlock' ? 1000 : 0,
318
+ * },
319
+ * ]
320
+ *
321
+ * @example
322
+ * // Inside a custom "question" block, only allow dragging "alternative" children
114
323
  * rules: [
115
324
  * {
116
325
  * id: 'onlyAlternatives',
@@ -122,57 +331,128 @@ interface NestedOptions {
122
331
  * },
123
332
  * },
124
333
  * ]
334
+ *
335
+ * @example
336
+ * // Deprioritize deeper nodes with partial deduction
337
+ * rules: [
338
+ * {
339
+ * id: 'preferShallow',
340
+ * evaluate: ({ depth }) => depth * 100,
341
+ * },
342
+ * ]
125
343
  */
126
344
  rules?: DragHandleRule[];
127
345
  /**
128
- * Set to `false` to disable default rules and use only your custom rules.
129
- * Default rules handle common cases like list items and inline content.
346
+ * Whether to include the built-in default rules before your custom rules.
347
+ *
348
+ * The default rules handle common editor patterns:
349
+ * - \`listItemFirstChild\` -- Excludes the first child of listItem/taskItem
350
+ * (the content paragraph), so the list item itself is the drag target
351
+ * - \`listWrapperDeprioritize\` -- Excludes bulletList/orderedList wrappers,
352
+ * so individual list items are the default drag target
353
+ * - \`tableStructure\` -- Excludes tableRow, tableCell, tableHeader from dragging
354
+ * (table extensions handle their own drag behavior)
355
+ * - \`inlineContent\` -- Excludes inline nodes and text from being drag targets
356
+ *
357
+ * Set to `false` to disable all default rules and use only your custom `rules`.
358
+ * This is useful when the default behavior conflicts with your custom setup.
130
359
  *
131
360
  * @default true
361
+ *
362
+ * @example
363
+ * // Use only your own rule, no defaults
364
+ * nested: {
365
+ * defaultRules: false,
366
+ * rules: [{
367
+ * id: 'onlyParagraphs',
368
+ * evaluate: ({ node }) =>
369
+ * node.type.name === 'paragraph' ? 0 : 1000,
370
+ * }],
371
+ * }
132
372
  */
133
373
  defaultRules?: boolean;
134
374
  /**
135
- * Restrict nested drag handles to specific container types.
136
- * If set, nested dragging only works inside these node types.
375
+ * Restrict nested drag handles to specific container node types.
376
+ *
377
+ * When set, nested dragging only activates when the cursor is inside one of
378
+ * the specified node types (at any ancestor level). When the cursor is
379
+ * outside these containers, the drag handle hides entirely for nested
380
+ * content positioned inside those regions.
381
+ *
382
+ * This is useful for scoping nested drag handles to specific editor regions
383
+ * (e.g., lists and blockquotes) while keeping simpler blocks (headings,
384
+ * paragraphs) working with only top-level handles.
137
385
  *
138
386
  * @example
139
- * // Only enable nested dragging in lists and custom question blocks
140
- * allowedContainers: ['bulletList', 'orderedList', 'questionBlock']
387
+ * // Only enable nested dragging inside lists
388
+ * allowedContainers: ['bulletList', 'orderedList']
389
+ *
390
+ * @example
391
+ * // Enable nested dragging inside lists and blockquotes
392
+ * allowedContainers: ['bulletList', 'orderedList', 'blockquote']
141
393
  */
142
394
  allowedContainers?: string[];
143
395
  /**
144
- * Edge detection behavior. Controls when to prefer parent over nested node.
396
+ * Controls when the drag handle prefers a parent node over a deeply nested
397
+ * child node, based on cursor proximity to element edges.
398
+ *
399
+ * When the cursor is near a configured edge of a nested element, the scoring
400
+ * system deducts \`strength * depth\` from deeper nodes, making the parent
401
+ * container (like an entire list) easier to grab.
145
402
  *
146
- * Presets:
147
- * - `'left'` (default) - Prefer parent near left/top edges
148
- * - `'right'` - Prefer parent near right/top edges (for RTL)
149
- * - `'both'` - Prefer parent near any horizontal edge
150
- * - `'none'` - Disable edge detection
403
+ * **Presets (quick and simple):**
404
+ * - `'left'` (default): Cursor near left or top edge → prefer parent (LTR)
405
+ * - `'right'`: Cursor near right or top edge → prefer parent (RTL)
406
+ * - `'both'`: Cursor near left, right, or top edge → prefer parent
407
+ * - \`'none'\`: Disabled, cursor position does not affect scoring at all
151
408
  *
152
- * Or pass a partial/full config object for fine-tuned control.
153
- * Partial configs are merged with defaults.
409
+ * **Fine-tuned object (full control):**
410
+ * Pass a partial `EdgeDetectionConfig` to override only what you need:
411
+ * - `edges`: Which element edges trigger parent preference (default: `['left', 'top']`)
412
+ * - `threshold`: Width of the edge zone in pixels (default: `12`). Higher = easier to trigger.
413
+ * - `strength`: Deduction multiplier per depth level (default: `500`). Higher = stronger parent preference.
414
+ *
415
+ * The effective deduction when near an edge is `strength * depth`, so deeper
416
+ * nesting always gets penalized more, you naturally grab the outer wrapper.
154
417
  *
155
418
  * @default 'left'
156
419
  *
157
420
  * @example
158
- * // Only override threshold, keep default edges and strength
159
- * edgeDetection: { threshold: 20 }
421
+ * // Just widen the trigger zone to 24px
422
+ * edgeDetection: { threshold: 24 }
423
+ *
424
+ * @example
425
+ * // Top/bottom edges only, very aggressive parent preference
426
+ * edgeDetection: {
427
+ * edges: ['top', 'bottom'],
428
+ * threshold: 30,
429
+ * strength: 1000,
430
+ * }
431
+ *
432
+ * @example
433
+ * // Gentle edge detection, nudges toward parents without blocking typical depths
434
+ * edgeDetection: {
435
+ * threshold: 6,
436
+ * strength: 200,
437
+ * }
160
438
  */
161
439
  edgeDetection?: EdgeDetectionPreset | Partial<EdgeDetectionConfig>;
162
440
  }
163
441
  /**
164
- * Normalized nested options with all properties resolved.
442
+ * Fully resolved nested drag handle options after normalization.
443
+ * Produced by `normalizeNestedOptions()` from user-provided `NestedOptions`
444
+ * or a boolean flag. This is the internal representation consumed by the plugin.
165
445
  */
166
446
  interface NormalizedNestedOptions {
167
447
  /** Whether nested drag handles are enabled */
168
448
  enabled: boolean;
169
- /** Custom rules to apply */
449
+ /** Custom rules to apply (combined with default rules if `defaultRules` is true) */
170
450
  rules: DragHandleRule[];
171
- /** Whether to include default rules */
451
+ /** Whether the built-in default rules are included alongside custom rules */
172
452
  defaultRules: boolean;
173
- /** Allowed container node types (undefined means all) */
453
+ /** Allowed container node types, or `undefined` to allow all containers */
174
454
  allowedContainers: string[] | undefined;
175
- /** Resolved edge detection configuration */
455
+ /** Fully resolved edge detection configuration with all defaults applied */
176
456
  edgeDetection: EdgeDetectionConfig;
177
457
  }
178
458
 
@@ -214,24 +494,14 @@ interface DragHandleOptions {
214
494
  /**
215
495
  * Enable drag handles for nested content (list items, blockquotes, etc.).
216
496
  *
217
- * When enabled, the drag handle will appear for nested blocks, not just
218
- * top-level blocks. A rule-based scoring system determines which node
219
- * to target based on cursor position and configured rules.
497
+ * When enabled, the drag handle appears for block nodes at any depth, not just
498
+ * top-level blocks. A rule-based scoring system evaluates all ancestor nodes
499
+ * at the cursor position and selects the best drag target.
220
500
  *
221
501
  * **Values:**
222
502
  * - `false` (default): Only root-level blocks show drag handles
223
503
  * - `true`: Enable with sensible defaults (left edge detection, default rules)
224
- * - `NestedOptions`: Enable with custom configuration
225
- *
226
- * **Configuration options:**
227
- * - `rules`: Custom rules to determine which nodes are draggable
228
- * - `defaultRules`: Whether to include default rules (default: true)
229
- * - `allowedContainers`: Restrict nested dragging to specific container types
230
- * - `edgeDetection`: Control when to prefer parent over nested node
231
- * - `'left'` (default): Prefer parent near left/top edges
232
- * - `'right'`: Prefer parent near right/top edges (for RTL)
233
- * - `'both'`: Prefer parent near any horizontal edge
234
- * - `'none'`: Disable edge detection
504
+ * - `NestedOptions`: Enable with full custom configuration
235
505
  *
236
506
  * @default false
237
507
  *
@@ -250,7 +520,7 @@ interface DragHandleOptions {
250
520
  * })
251
521
  *
252
522
  * @example
253
- * // With custom rules
523
+ * // With custom rules and edge detection disabled
254
524
  * DragHandle.configure({
255
525
  * nested: {
256
526
  * rules: [{
@@ -260,6 +530,20 @@ interface DragHandleOptions {
260
530
  * edgeDetection: 'none',
261
531
  * },
262
532
  * })
533
+ *
534
+ * @example
535
+ * // Full configuration
536
+ * DragHandle.configure({
537
+ * nested: {
538
+ * defaultRules: true,
539
+ * allowedContainers: ['bulletList', 'orderedList', 'blockquote'],
540
+ * edgeDetection: { threshold: 20 },
541
+ * rules: [{
542
+ * id: 'preferShallow',
543
+ * evaluate: ({ depth }) => depth * 200,
544
+ * }],
545
+ * },
546
+ * })
263
547
  */
264
548
  nested?: boolean | NestedOptions;
265
549
  }