0
# Utilities
1
2
@tiptap/core provides a comprehensive set of utility functions for type checking, object manipulation, string processing, platform detection, and DOM operations. These utilities help with common tasks in editor development.
3
4
## Capabilities
5
6
### Type Guards
7
8
Functions for runtime type checking and validation.
9
10
```typescript { .api }
11
/**
12
* Check if value is a function
13
* @param value - Value to check
14
* @returns Whether value is a function
15
*/
16
function isFunction(value: unknown): value is Function;
17
18
/**
19
* Check if value is a string
20
* @param value - Value to check
21
* @returns Whether value is a string
22
*/
23
function isString(value: unknown): value is string;
24
25
/**
26
* Check if value is a number
27
* @param value - Value to check
28
* @returns Whether value is a number
29
*/
30
function isNumber(value: unknown): value is number;
31
32
/**
33
* Check if value is a regular expression
34
* @param value - Value to check
35
* @returns Whether value is a RegExp
36
*/
37
function isRegExp(value: unknown): value is RegExp;
38
39
/**
40
* Check if value is a plain object (not array, function, etc.)
41
* @param value - Value to check
42
* @returns Whether value is a plain object
43
*/
44
function isPlainObject(value: unknown): value is Record<string, any>;
45
46
/**
47
* Check if object has no own properties
48
* @param value - Object to check
49
* @returns Whether object is empty
50
*/
51
function isEmptyObject(value: Record<string, any>): boolean;
52
```
53
54
**Usage Examples:**
55
56
```typescript
57
import {
58
isFunction,
59
isString,
60
isNumber,
61
isPlainObject
62
} from '@tiptap/core';
63
64
// Type checking in extension configuration
65
function processExtensionConfig(config: any) {
66
if (isString(config.name)) {
67
console.log('Extension name:', config.name);
68
}
69
70
if (isFunction(config.addCommands)) {
71
const commands = config.addCommands();
72
// Process commands
73
}
74
75
if (isPlainObject(config.defaultOptions)) {
76
// Merge with existing options
77
}
78
}
79
80
// Validate node attributes
81
function validateAttributes(attrs: unknown): Record<string, any> {
82
if (!isPlainObject(attrs)) {
83
return {};
84
}
85
86
const validated: Record<string, any> = {};
87
88
for (const [key, value] of Object.entries(attrs)) {
89
if (isString(value) || isNumber(value)) {
90
validated[key] = value;
91
}
92
}
93
94
return validated;
95
}
96
97
// Safe function calling
98
function safeCall(fn: unknown, ...args: any[]): any {
99
if (isFunction(fn)) {
100
return fn(...args);
101
}
102
return null;
103
}
104
105
// Dynamic attribute handling
106
function processAttribute(value: unknown): string {
107
if (isString(value)) {
108
return value;
109
}
110
if (isNumber(value)) {
111
return value.toString();
112
}
113
if (isPlainObject(value)) {
114
return JSON.stringify(value);
115
}
116
return '';
117
}
118
```
119
120
### Object Utilities
121
122
Functions for manipulating and working with objects and arrays.
123
124
```typescript { .api }
125
/**
126
* Merge multiple attribute objects
127
* @param attributes - Objects to merge
128
* @returns Merged attributes object
129
*/
130
function mergeAttributes(...attributes: Record<string, any>[]): Record<string, any>;
131
132
/**
133
* Deep merge two objects
134
* @param target - Target object to merge into
135
* @param source - Source object to merge from
136
* @returns Merged object
137
*/
138
function mergeDeep(
139
target: Record<string, any>,
140
source: Record<string, any>
141
): Record<string, any>;
142
143
/**
144
* Delete properties from an object
145
* @param obj - Object to modify
146
* @param propOrProps - Property name or array of property names to delete
147
* @returns Modified object
148
*/
149
function deleteProps(
150
obj: Record<string, any>,
151
propOrProps: string | string[]
152
): Record<string, any>;
153
154
/**
155
* Call function or return value
156
* @param value - Function to call or value to return
157
* @param context - Context for function call
158
* @param props - Arguments for function call
159
* @returns Function result or original value
160
*/
161
function callOrReturn<T>(
162
value: T | ((...args: any[]) => T),
163
context?: any,
164
...props: any[]
165
): T;
166
167
/**
168
* Check if object includes specified values
169
* @param object - Object to check
170
* @param values - Values to look for
171
* @param options - Comparison options
172
* @returns Whether object includes the values
173
*/
174
function objectIncludes(
175
object: Record<string, any>,
176
values: Record<string, any>,
177
options?: { strict?: boolean }
178
): boolean;
179
180
/**
181
* Remove duplicate items from array
182
* @param array - Array to deduplicate
183
* @param by - Optional key function for comparison
184
* @returns Array without duplicates
185
*/
186
function removeDuplicates<T>(
187
array: T[],
188
by?: (item: T) => any
189
): T[];
190
191
/**
192
* Find duplicate items in array
193
* @param items - Array to check for duplicates
194
* @returns Array of duplicate items
195
*/
196
function findDuplicates<T>(items: T[]): T[];
197
```
198
199
**Usage Examples:**
200
201
```typescript
202
import {
203
mergeAttributes,
204
mergeDeep,
205
deleteProps,
206
callOrReturn,
207
removeDuplicates
208
} from '@tiptap/core';
209
210
// Merge HTML attributes
211
const baseAttrs = { class: 'editor', id: 'main' };
212
const userAttrs = { class: 'custom', 'data-test': 'true' };
213
const finalAttrs = mergeAttributes(baseAttrs, userAttrs);
214
// { class: 'editor custom', id: 'main', 'data-test': 'true' }
215
216
// Deep merge configuration objects
217
const defaultConfig = {
218
ui: { theme: 'light', colors: { primary: 'blue' } },
219
features: { spellcheck: true }
220
};
221
const userConfig = {
222
ui: { colors: { secondary: 'green' } },
223
features: { autosave: true }
224
};
225
const finalConfig = mergeDeep(defaultConfig, userConfig);
226
// {
227
// ui: { theme: 'light', colors: { primary: 'blue', secondary: 'green' } },
228
// features: { spellcheck: true, autosave: true }
229
// }
230
231
// Clean up object properties
232
const rawData = {
233
name: 'test',
234
password: 'secret',
235
temp: 'remove-me',
236
internal: 'also-remove'
237
};
238
const cleanData = deleteProps(rawData, ['password', 'temp', 'internal']);
239
// { name: 'test' }
240
241
// Dynamic value resolution
242
const dynamicValue = callOrReturn(
243
() => new Date().toISOString(),
244
null
245
); // Returns current timestamp
246
247
const staticValue = callOrReturn('static-string'); // Returns 'static-string'
248
249
// Extension configuration
250
function configureExtension(config: any) {
251
return {
252
name: callOrReturn(config.name),
253
priority: callOrReturn(config.priority, config),
254
options: callOrReturn(config.defaultOptions, config)
255
};
256
}
257
258
// Remove duplicate extensions
259
const extensions = [ext1, ext2, ext1, ext3, ext2];
260
const uniqueExtensions = removeDuplicates(extensions, ext => ext.name);
261
262
// Deduplicate by complex key
263
const items = [
264
{ id: 1, name: 'Item 1', category: 'A' },
265
{ id: 2, name: 'Item 2', category: 'B' },
266
{ id: 1, name: 'Item 1 Updated', category: 'A' }
267
];
268
const uniqueItems = removeDuplicates(items, item => `${item.id}-${item.category}`);
269
```
270
271
### Platform Detection
272
273
Functions for detecting the current platform and environment.
274
275
```typescript { .api }
276
/**
277
* Check if running on Android
278
* @returns Whether current platform is Android
279
*/
280
function isAndroid(): boolean;
281
282
/**
283
* Check if running on iOS
284
* @returns Whether current platform is iOS
285
*/
286
function isiOS(): boolean;
287
288
/**
289
* Check if running on macOS
290
* @returns Whether current platform is macOS
291
*/
292
function isMacOS(): boolean;
293
```
294
295
**Usage Examples:**
296
297
```typescript
298
import { isAndroid, isiOS, isMacOS } from '@tiptap/core';
299
300
// Platform-specific keyboard shortcuts
301
function getKeyboardShortcuts() {
302
const isMac = isMacOS();
303
304
return {
305
bold: isMac ? 'Cmd+B' : 'Ctrl+B',
306
italic: isMac ? 'Cmd+I' : 'Ctrl+I',
307
undo: isMac ? 'Cmd+Z' : 'Ctrl+Z',
308
redo: isMac ? 'Cmd+Shift+Z' : 'Ctrl+Y'
309
};
310
}
311
312
// Platform-specific behavior
313
function setupEditor() {
314
const isMobile = isAndroid() || isiOS();
315
316
return new Editor({
317
// Different options for mobile vs desktop
318
autofocus: !isMobile,
319
editable: true,
320
extensions: [
321
// Platform-specific extensions
322
...(isMobile ? [TouchExtension] : [DesktopExtension])
323
]
324
});
325
}
326
327
// Touch-friendly UI on mobile
328
function EditorToolbar() {
329
const isMobile = isAndroid() || isiOS();
330
331
return (
332
<div className={`toolbar ${isMobile ? 'toolbar-mobile' : 'toolbar-desktop'}`}>
333
{/* Larger buttons on mobile */}
334
</div>
335
);
336
}
337
338
// Handle paste behavior
339
function handlePaste(event: ClipboardEvent) {
340
const isIOS = isiOS();
341
342
if (isIOS) {
343
// iOS-specific paste handling
344
// iOS has different clipboard API behavior
345
} else {
346
// Standard paste handling
347
}
348
}
349
```
350
351
### DOM Utilities
352
353
Functions for working with DOM elements and HTML.
354
355
```typescript { .api }
356
/**
357
* Create DOM element from HTML string
358
* @param html - HTML string to parse
359
* @returns DOM element
360
*/
361
function elementFromString(html: string): Element;
362
363
/**
364
* Create style tag with CSS content
365
* @param css - CSS content for the style tag
366
* @param nonce - Optional nonce for Content Security Policy
367
* @returns HTMLStyleElement
368
*/
369
function createStyleTag(css: string, nonce?: string): HTMLStyleElement;
370
```
371
372
**Usage Examples:**
373
374
```typescript
375
import { elementFromString, createStyleTag } from '@tiptap/core';
376
377
// Create DOM elements from HTML
378
const element = elementFromString('<div class="custom">Content</div>');
379
document.body.appendChild(element);
380
381
// Create complex elements
382
const complexElement = elementFromString(`
383
<div class="editor-widget">
384
<h3>Widget Title</h3>
385
<p>Widget content with <strong>formatting</strong></p>
386
<button onclick="handleClick()">Action</button>
387
</div>
388
`);
389
390
// Add custom styles
391
const customCSS = `
392
.tiptap-editor {
393
border: 1px solid #ccc;
394
border-radius: 4px;
395
padding: 1rem;
396
}
397
398
.tiptap-editor h1 {
399
margin-top: 0;
400
}
401
`;
402
403
const styleTag = createStyleTag(customCSS);
404
document.head.appendChild(styleTag);
405
406
// Add styles with CSP nonce
407
const secureStyleTag = createStyleTag(customCSS, 'random-nonce-value');
408
document.head.appendChild(secureStyleTag);
409
410
// Dynamic element creation in node views
411
function createNodeViewElement(node: ProseMirrorNode): Element {
412
const html = `
413
<div class="custom-node" data-type="${node.type.name}">
414
<div class="node-controls">
415
<button class="edit-btn">Edit</button>
416
<button class="delete-btn">Delete</button>
417
</div>
418
<div class="node-content"></div>
419
</div>
420
`;
421
422
return elementFromString(html);
423
}
424
```
425
426
### String Utilities
427
428
Functions for string processing and manipulation.
429
430
```typescript { .api }
431
/**
432
* Escape string for use in regular expressions
433
* @param string - String to escape
434
* @returns Escaped string safe for RegExp
435
*/
436
function escapeForRegEx(string: string): string;
437
438
/**
439
* Parse value from string with type coercion
440
* @param value - String value to parse
441
* @returns Parsed value with appropriate type
442
*/
443
function fromString(value: string): any;
444
```
445
446
**Usage Examples:**
447
448
```typescript
449
import { escapeForRegEx, fromString } from '@tiptap/core';
450
451
// Escape user input for regex
452
function createSearchPattern(userInput: string): RegExp {
453
const escaped = escapeForRegEx(userInput);
454
return new RegExp(escaped, 'gi');
455
}
456
457
// Safe regex creation
458
const userSearch = 'search (with) special [chars]';
459
const pattern = createSearchPattern(userSearch);
460
// Creates regex that matches literal string, not regex pattern
461
462
// Parse string values with type coercion
463
const stringValues = ['true', 'false', '42', '3.14', 'null', 'undefined', 'text'];
464
465
stringValues.forEach(str => {
466
const parsed = fromString(str);
467
console.log(`"${str}" → ${parsed} (${typeof parsed})`);
468
});
469
// "true" → true (boolean)
470
// "false" → false (boolean)
471
// "42" → 42 (number)
472
// "3.14" → 3.14 (number)
473
// "null" → null (object)
474
// "undefined" → undefined (undefined)
475
// "text" → "text" (string)
476
477
// Attribute parsing
478
function parseNodeAttributes(rawAttrs: Record<string, string>): Record<string, any> {
479
const parsed: Record<string, any> = {};
480
481
for (const [key, value] of Object.entries(rawAttrs)) {
482
parsed[key] = fromString(value);
483
}
484
485
return parsed;
486
}
487
488
// HTML attribute handling
489
const htmlAttrs = {
490
'data-level': '2',
491
'data-active': 'true',
492
'data-count': '42',
493
'data-name': 'heading'
494
};
495
496
const typedAttrs = parseNodeAttributes(htmlAttrs);
497
// {
498
// 'data-level': 2,
499
// 'data-active': true,
500
// 'data-count': 42,
501
// 'data-name': 'heading'
502
// }
503
```
504
505
### Math Utilities
506
507
Mathematical helper functions.
508
509
```typescript { .api }
510
/**
511
* Clamp value between minimum and maximum
512
* @param value - Value to clamp
513
* @param min - Minimum allowed value
514
* @param max - Maximum allowed value
515
* @returns Clamped value
516
*/
517
function minMax(value: number, min: number, max: number): number;
518
```
519
520
**Usage Examples:**
521
522
```typescript
523
import { minMax } from '@tiptap/core';
524
525
// Clamp user input values
526
function setFontSize(size: number): void {
527
const clampedSize = minMax(size, 8, 72);
528
editor.commands.updateAttributes('textStyle', { fontSize: `${clampedSize}px` });
529
}
530
531
// Clamp table dimensions
532
function createTable(rows: number, cols: number): void {
533
const safeRows = minMax(rows, 1, 20);
534
const safeCols = minMax(cols, 1, 10);
535
536
editor.commands.insertTable({ rows: safeRows, cols: safeCols });
537
}
538
539
// Clamp scroll position
540
function scrollToPosition(pos: number): void {
541
const docSize = editor.state.doc.content.size;
542
const safePos = minMax(pos, 0, docSize);
543
544
editor.commands.focus(safePos);
545
}
546
547
// UI range controls
548
function HeadingLevelControl() {
549
const [level, setLevel] = useState(1);
550
551
const handleLevelChange = (newLevel: number) => {
552
const clampedLevel = minMax(newLevel, 1, 6);
553
setLevel(clampedLevel);
554
editor.commands.setNode('heading', { level: clampedLevel });
555
};
556
557
return (
558
<input
559
type="range"
560
min={1}
561
max={6}
562
value={level}
563
onChange={e => handleLevelChange(parseInt(e.target.value))}
564
/>
565
);
566
}
567
```
568
569
### Utility Composition
570
571
Examples of combining utilities for complex operations.
572
573
```typescript { .api }
574
// Complex utility compositions for real-world use cases
575
576
// Safe configuration merger
577
function createExtensionConfig<T>(
578
defaults: T,
579
userConfig?: Partial<T>,
580
validator?: (config: T) => boolean
581
): T {
582
let config = defaults;
583
584
if (isPlainObject(userConfig)) {
585
config = mergeDeep(config, userConfig as Record<string, any>) as T;
586
}
587
588
if (isFunction(validator) && !validator(config)) {
589
console.warn('Invalid configuration, using defaults');
590
return defaults;
591
}
592
593
return config;
594
}
595
596
// Platform-aware DOM manipulation
597
function createPlatformOptimizedElement(html: string): Element {
598
const element = elementFromString(html);
599
const isMobile = isAndroid() || isiOS();
600
601
if (isMobile) {
602
element.classList.add('mobile-optimized');
603
// Add touch-friendly attributes
604
element.setAttribute('touch-action', 'manipulation');
605
}
606
607
return element;
608
}
609
610
// Attribute sanitization pipeline
611
function sanitizeAttributes(
612
attrs: unknown,
613
allowedKeys: string[],
614
typeValidators: Record<string, (value: any) => boolean> = {}
615
): Record<string, any> {
616
if (!isPlainObject(attrs)) {
617
return {};
618
}
619
620
const sanitized: Record<string, any> = {};
621
622
for (const key of allowedKeys) {
623
const value = attrs[key];
624
const validator = typeValidators[key];
625
626
if (value !== undefined) {
627
if (!validator || validator(value)) {
628
sanitized[key] = value;
629
}
630
}
631
}
632
633
return sanitized;
634
}
635
636
// Usage examples
637
const extension = createExtensionConfig(
638
{ enabled: true, count: 0 },
639
{ count: 5 },
640
config => isNumber(config.count) && config.count >= 0
641
);
642
643
const mobileElement = createPlatformOptimizedElement(`
644
<button class="editor-button">Click me</button>
645
`);
646
647
const cleanAttrs = sanitizeAttributes(
648
{ level: 2, color: 'red', invalid: {} },
649
['level', 'color', 'size'],
650
{
651
level: isNumber,
652
color: isString,
653
size: isNumber
654
}
655
);
656
// Result: { level: 2, color: 'red' }
657
```
658
659
### Transaction Position Tracking
660
661
The Tracker class provides position tracking during document transactions, allowing you to track how positions change as the document is modified.
662
663
```typescript { .api }
664
/**
665
* Tracks position changes during document transactions
666
*/
667
class Tracker {
668
/** The transaction being tracked */
669
readonly transaction: Transaction;
670
671
/** Current step index in the transaction */
672
readonly currentStep: number;
673
674
/**
675
* Create a new position tracker
676
* @param transaction - Transaction to track positions in
677
*/
678
constructor(transaction: Transaction);
679
680
/**
681
* Map a position through transaction steps to get current position
682
* @param position - Original position to map
683
* @returns TrackerResult with new position and deletion status
684
*/
685
map(position: number): TrackerResult;
686
}
687
688
interface TrackerResult {
689
/** New position after mapping through transaction steps */
690
position: number;
691
/** Whether the content at this position was deleted */
692
deleted: boolean;
693
}
694
```
695
696
**Usage Examples:**
697
698
```typescript
699
import { Tracker } from '@tiptap/core';
700
701
// Track position changes during complex operations
702
function trackPositionDuringEdit(editor: Editor, initialPos: number) {
703
const { tr } = editor.state;
704
const tracker = new Tracker(tr);
705
706
// Perform some operations
707
tr.insertText('New text', 10);
708
tr.delete(20, 30);
709
tr.setSelection(TextSelection.create(tr.doc, 15));
710
711
// Check where our original position ended up
712
const result = tracker.map(initialPos);
713
714
if (result.deleted) {
715
console.log(`Content at position ${initialPos} was deleted`);
716
} else {
717
console.log(`Position ${initialPos} is now at ${result.position}`);
718
}
719
720
return result;
721
}
722
723
// Track multiple positions simultaneously
724
function trackMultiplePositions(
725
transaction: Transaction,
726
positions: number[]
727
): Map<number, TrackerResult> {
728
const tracker = new Tracker(transaction);
729
const results = new Map<number, TrackerResult>();
730
731
positions.forEach(pos => {
732
results.set(pos, tracker.map(pos));
733
});
734
735
return results;
736
}
737
738
// Use in custom command to maintain cursor position
739
function insertContentAndMaintainPosition(content: string) {
740
return ({ tr, state }: CommandProps) => {
741
const { selection } = state;
742
const tracker = new Tracker(tr);
743
744
// Insert content at current position
745
tr.insertText(content, selection.from);
746
747
// Track where the cursor should be after insertion
748
const newCursorResult = tracker.map(selection.from + content.length);
749
750
if (!newCursorResult.deleted) {
751
tr.setSelection(TextSelection.create(tr.doc, newCursorResult.position));
752
}
753
754
return true;
755
};
756
}
757
758
// Advanced usage: Track complex document changes
759
function performComplexEdit(editor: Editor) {
760
const { tr } = editor.state;
761
const tracker = new Tracker(tr);
762
763
// Store important positions before making changes
764
const bookmarks = [50, 100, 150, 200];
765
766
// Perform multiple operations
767
tr.delete(10, 20); // Delete range
768
tr.insertText('Replacement', 10); // Insert replacement
769
tr.setMark(40, 60, schema.marks.bold.create()); // Add formatting
770
771
// Check what happened to our bookmarked positions
772
const updatedBookmarks = bookmarks.map(pos => {
773
const result = tracker.map(pos);
774
return {
775
original: pos,
776
current: result.position,
777
deleted: result.deleted
778
};
779
});
780
781
console.log('Position changes:', updatedBookmarks);
782
783
// Apply the transaction
784
editor.view.dispatch(tr);
785
786
return updatedBookmarks;
787
}
788
789
// Use with undo/redo to maintain selection
790
function smartUndo(editor: Editor) {
791
const { selection } = editor.state;
792
const tracker = new Tracker(editor.state.tr);
793
794
// Perform undo
795
editor.commands.undo();
796
797
// Try to maintain a similar selection position
798
const mappedResult = tracker.map(selection.from);
799
800
if (!mappedResult.deleted) {
801
editor.commands.setTextSelection(mappedResult.position);
802
}
803
}
804
```