@tiptap/core 3.12.0 → 3.13.0
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.cjs +57 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -4
- package/dist/index.d.ts +100 -4
- package/dist/index.js +57 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/lib/ResizableNodeView.ts +122 -4
- package/src/utilities/markdown/createInlineMarkdownSpec.ts +51 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/core",
|
|
3
3
|
"description": "headless rich text editor",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.13.0",
|
|
5
5
|
"homepage": "https://tiptap.dev",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tiptap",
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"jsx-dev-runtime"
|
|
53
53
|
],
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@tiptap/pm": "^3.
|
|
55
|
+
"@tiptap/pm": "^3.13.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@tiptap/pm": "^3.
|
|
58
|
+
"@tiptap/pm": "^3.13.0"
|
|
59
59
|
},
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Node as PMNode } from '@tiptap/pm/model'
|
|
2
2
|
import type { Decoration, DecorationSource, NodeView } from '@tiptap/pm/view'
|
|
3
3
|
|
|
4
|
+
import type { Editor } from '../Editor.js'
|
|
5
|
+
|
|
4
6
|
const isTouchEvent = (e: MouseEvent | TouchEvent): e is TouchEvent => {
|
|
5
7
|
return 'touches' in e
|
|
6
8
|
}
|
|
@@ -73,6 +75,11 @@ export type ResizableNodeViewOptions = {
|
|
|
73
75
|
*/
|
|
74
76
|
node: PMNode
|
|
75
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The Tiptap editor instance
|
|
80
|
+
*/
|
|
81
|
+
editor: Editor
|
|
82
|
+
|
|
76
83
|
/**
|
|
77
84
|
* Function that returns the current position of the node in the document
|
|
78
85
|
*/
|
|
@@ -204,6 +211,51 @@ export type ResizableNodeViewOptions = {
|
|
|
204
211
|
/** Class added to container while actively resizing */
|
|
205
212
|
resizing?: string
|
|
206
213
|
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Optional callback for creating custom resize handle elements.
|
|
217
|
+
*
|
|
218
|
+
* This function allows developers to define their own handle element
|
|
219
|
+
* (e.g., custom icons, classes, or styles) for a given resize direction.
|
|
220
|
+
* It is called internally for each handle direction.
|
|
221
|
+
*
|
|
222
|
+
* @param direction - The direction of the handle being created (e.g., 'top', 'bottom-right').
|
|
223
|
+
* @returns The custom handle HTMLElement.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* createCustomHandle: (direction) => {
|
|
228
|
+
* const handle = document.createElement('div')
|
|
229
|
+
* handle.dataset.resizeHandle = direction
|
|
230
|
+
* handle.style.position = 'absolute'
|
|
231
|
+
* handle.className = 'tiptap-custom-handle'
|
|
232
|
+
*
|
|
233
|
+
* const isTop = direction.includes('top')
|
|
234
|
+
* const isBottom = direction.includes('bottom')
|
|
235
|
+
* const isLeft = direction.includes('left')
|
|
236
|
+
* const isRight = direction.includes('right')
|
|
237
|
+
*
|
|
238
|
+
* if (isTop) handle.style.top = '0'
|
|
239
|
+
* if (isBottom) handle.style.bottom = '0'
|
|
240
|
+
* if (isLeft) handle.style.left = '0'
|
|
241
|
+
* if (isRight) handle.style.right = '0'
|
|
242
|
+
*
|
|
243
|
+
* // Edge handles span the full width or height
|
|
244
|
+
* if (direction === 'top' || direction === 'bottom') {
|
|
245
|
+
* handle.style.left = '0'
|
|
246
|
+
* handle.style.right = '0'
|
|
247
|
+
* }
|
|
248
|
+
*
|
|
249
|
+
* if (direction === 'left' || direction === 'right') {
|
|
250
|
+
* handle.style.top = '0'
|
|
251
|
+
* handle.style.bottom = '0'
|
|
252
|
+
* }
|
|
253
|
+
*
|
|
254
|
+
* return handle
|
|
255
|
+
* }
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement
|
|
207
259
|
}
|
|
208
260
|
}
|
|
209
261
|
|
|
@@ -247,6 +299,9 @@ export class ResizableNodeView {
|
|
|
247
299
|
/** The ProseMirror node instance */
|
|
248
300
|
node: PMNode
|
|
249
301
|
|
|
302
|
+
/** The Tiptap editor instance */
|
|
303
|
+
editor: Editor
|
|
304
|
+
|
|
250
305
|
/** The DOM element being made resizable */
|
|
251
306
|
element: HTMLElement
|
|
252
307
|
|
|
@@ -294,6 +349,9 @@ export class ResizableNodeView {
|
|
|
294
349
|
resizing: '',
|
|
295
350
|
}
|
|
296
351
|
|
|
352
|
+
/** Optional callback for creating custom resize handles */
|
|
353
|
+
createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement
|
|
354
|
+
|
|
297
355
|
/** Initial width of the element (for aspect ratio calculation) */
|
|
298
356
|
private initialWidth: number = 0
|
|
299
357
|
|
|
@@ -324,6 +382,12 @@ export class ResizableNodeView {
|
|
|
324
382
|
/** Whether Shift key is currently pressed (for temporary aspect ratio lock) */
|
|
325
383
|
private isShiftKeyPressed: boolean = false
|
|
326
384
|
|
|
385
|
+
/** Last known editable state of the editor */
|
|
386
|
+
private lastEditableState: boolean | undefined = undefined
|
|
387
|
+
|
|
388
|
+
/** Map of handle elements by direction */
|
|
389
|
+
private handleMap = new Map<ResizableNodeViewDirection, HTMLElement>()
|
|
390
|
+
|
|
327
391
|
/**
|
|
328
392
|
* Creates a new ResizableNodeView instance.
|
|
329
393
|
*
|
|
@@ -334,6 +398,7 @@ export class ResizableNodeView {
|
|
|
334
398
|
*/
|
|
335
399
|
constructor(options: ResizableNodeViewOptions) {
|
|
336
400
|
this.node = options.node
|
|
401
|
+
this.editor = options.editor
|
|
337
402
|
this.element = options.element
|
|
338
403
|
this.contentElement = options.contentElement
|
|
339
404
|
|
|
@@ -371,11 +436,17 @@ export class ResizableNodeView {
|
|
|
371
436
|
}
|
|
372
437
|
}
|
|
373
438
|
|
|
439
|
+
if (options.options?.createCustomHandle) {
|
|
440
|
+
this.createCustomHandle = options.options.createCustomHandle
|
|
441
|
+
}
|
|
442
|
+
|
|
374
443
|
this.wrapper = this.createWrapper()
|
|
375
444
|
this.container = this.createContainer()
|
|
376
445
|
|
|
377
446
|
this.applyInitialSize()
|
|
378
447
|
this.attachHandles()
|
|
448
|
+
|
|
449
|
+
this.editor.on('update', this.handleEditorUpdate.bind(this))
|
|
379
450
|
}
|
|
380
451
|
|
|
381
452
|
/**
|
|
@@ -394,6 +465,23 @@ export class ResizableNodeView {
|
|
|
394
465
|
return this.contentElement
|
|
395
466
|
}
|
|
396
467
|
|
|
468
|
+
private handleEditorUpdate() {
|
|
469
|
+
const isEditable = this.editor.isEditable
|
|
470
|
+
|
|
471
|
+
// Only if state actually changed
|
|
472
|
+
if (isEditable === this.lastEditableState) {
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
this.lastEditableState = isEditable
|
|
477
|
+
|
|
478
|
+
if (!isEditable) {
|
|
479
|
+
this.removeHandles()
|
|
480
|
+
} else if (isEditable && this.handleMap.size === 0) {
|
|
481
|
+
this.attachHandles()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
397
485
|
/**
|
|
398
486
|
* Called when the node's content or attributes change.
|
|
399
487
|
*
|
|
@@ -442,6 +530,8 @@ export class ResizableNodeView {
|
|
|
442
530
|
this.activeHandle = null
|
|
443
531
|
}
|
|
444
532
|
|
|
533
|
+
this.editor.off('update', this.handleEditorUpdate.bind(this))
|
|
534
|
+
|
|
445
535
|
this.container.remove()
|
|
446
536
|
}
|
|
447
537
|
|
|
@@ -459,8 +549,6 @@ export class ResizableNodeView {
|
|
|
459
549
|
element.dataset.resizeContainer = ''
|
|
460
550
|
element.dataset.node = this.node.type.name
|
|
461
551
|
element.style.display = 'flex'
|
|
462
|
-
element.style.justifyContent = 'flex-start'
|
|
463
|
-
element.style.alignItems = 'flex-start'
|
|
464
552
|
|
|
465
553
|
if (this.classNames.container) {
|
|
466
554
|
element.className = this.classNames.container
|
|
@@ -567,14 +655,44 @@ export class ResizableNodeView {
|
|
|
567
655
|
*/
|
|
568
656
|
private attachHandles(): void {
|
|
569
657
|
this.directions.forEach(direction => {
|
|
570
|
-
|
|
571
|
-
|
|
658
|
+
let handle: HTMLElement
|
|
659
|
+
|
|
660
|
+
if (this.createCustomHandle) {
|
|
661
|
+
handle = this.createCustomHandle(direction)
|
|
662
|
+
} else {
|
|
663
|
+
handle = this.createHandle(direction)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!(handle instanceof HTMLElement)) {
|
|
667
|
+
console.warn(
|
|
668
|
+
`[ResizableNodeView] createCustomHandle("${direction}") did not return an HTMLElement. Falling back to default handle.`,
|
|
669
|
+
)
|
|
670
|
+
handle = this.createHandle(direction)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!this.createCustomHandle) {
|
|
674
|
+
this.positionHandle(handle, direction)
|
|
675
|
+
}
|
|
676
|
+
|
|
572
677
|
handle.addEventListener('mousedown', event => this.handleResizeStart(event, direction))
|
|
573
678
|
handle.addEventListener('touchstart', event => this.handleResizeStart(event as unknown as MouseEvent, direction))
|
|
679
|
+
|
|
680
|
+
this.handleMap.set(direction, handle)
|
|
681
|
+
|
|
574
682
|
this.wrapper.appendChild(handle)
|
|
575
683
|
})
|
|
576
684
|
}
|
|
577
685
|
|
|
686
|
+
/**
|
|
687
|
+
* Removes all resize handles from the wrapper.
|
|
688
|
+
*
|
|
689
|
+
* Cleans up the handle map and removes each handle element from the DOM.
|
|
690
|
+
*/
|
|
691
|
+
private removeHandles(): void {
|
|
692
|
+
this.handleMap.forEach(el => el.remove())
|
|
693
|
+
this.handleMap.clear()
|
|
694
|
+
}
|
|
695
|
+
|
|
578
696
|
/**
|
|
579
697
|
* Applies initial sizing from node attributes to the element.
|
|
580
698
|
*
|
|
@@ -40,6 +40,23 @@ function serializeShortcodeAttributes(attrs: Record<string, any>): string {
|
|
|
40
40
|
.join(' ')
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Configuration for an allowed attribute in markdown serialization.
|
|
45
|
+
* Can be a simple string (attribute name) or an object with additional options.
|
|
46
|
+
*/
|
|
47
|
+
export type AllowedAttribute =
|
|
48
|
+
| string
|
|
49
|
+
| {
|
|
50
|
+
/** The attribute name */
|
|
51
|
+
name: string
|
|
52
|
+
/**
|
|
53
|
+
* If provided, the attribute will be skipped during serialization when its value
|
|
54
|
+
* equals this default value. This keeps markdown output clean by omitting
|
|
55
|
+
* attributes that have their default values.
|
|
56
|
+
*/
|
|
57
|
+
skipIfDefault?: any
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
export interface InlineMarkdownSpecOptions {
|
|
44
61
|
/** The Tiptap node name this spec is for */
|
|
45
62
|
nodeName: string
|
|
@@ -55,8 +72,26 @@ export interface InlineMarkdownSpecOptions {
|
|
|
55
72
|
defaultAttributes?: Record<string, any>
|
|
56
73
|
/** Whether this is a self-closing shortcode (no content, like [emoji name=party]) */
|
|
57
74
|
selfClosing?: boolean
|
|
58
|
-
/**
|
|
59
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Allowlist of attributes to include in markdown serialization.
|
|
77
|
+
* If not provided, all attributes are included.
|
|
78
|
+
*
|
|
79
|
+
* Each item can be either:
|
|
80
|
+
* - A string: the attribute name (always included if present)
|
|
81
|
+
* - An object: `{ name: string, skipIfDefault?: any }` for conditional inclusion
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // Simple string attributes (backward compatible)
|
|
85
|
+
* allowedAttributes: ['id', 'label']
|
|
86
|
+
*
|
|
87
|
+
* // Mixed with conditional attributes
|
|
88
|
+
* allowedAttributes: [
|
|
89
|
+
* 'id',
|
|
90
|
+
* 'label',
|
|
91
|
+
* { name: 'mentionSuggestionChar', skipIfDefault: '@' }
|
|
92
|
+
* ]
|
|
93
|
+
*/
|
|
94
|
+
allowedAttributes?: AllowedAttribute[]
|
|
60
95
|
}
|
|
61
96
|
|
|
62
97
|
/**
|
|
@@ -130,9 +165,20 @@ export function createInlineMarkdownSpec(options: InlineMarkdownSpecOptions): {
|
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
const filtered: Record<string, any> = {}
|
|
133
|
-
allowedAttributes.forEach(
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
allowedAttributes.forEach(attr => {
|
|
169
|
+
// Handle both string and object formats for backward compatibility
|
|
170
|
+
const attrName = typeof attr === 'string' ? attr : attr.name
|
|
171
|
+
const skipIfDefault = typeof attr === 'string' ? undefined : attr.skipIfDefault
|
|
172
|
+
|
|
173
|
+
if (attrName in attrs) {
|
|
174
|
+
const value = attrs[attrName]
|
|
175
|
+
|
|
176
|
+
// Skip if value equals the default (when skipIfDefault is specified)
|
|
177
|
+
if (skipIfDefault !== undefined && value === skipIfDefault) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
filtered[attrName] = value
|
|
136
182
|
}
|
|
137
183
|
})
|
|
138
184
|
return filtered
|