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