Utilities for key binding management, selection handling, and miscellaneous helper functions that support the Draft.js editor.
Utility functions for detecting keyboard modifiers and command keys across different platforms.
/**
* Key binding utilities for cross-platform keyboard handling
*/
const KeyBindingUtil: {
/**
* Check if event has Ctrl key (or Cmd on Mac)
* @param e - Keyboard event
* @returns True if Ctrl/Cmd is pressed
*/
isCtrlKeyCommand(e: SyntheticKeyboardEvent): boolean;
/**
* Check if event has Option key (Mac only)
* @param e - Keyboard event
* @returns True if Option is pressed on Mac
*/
isOptionKeyCommand(e: SyntheticKeyboardEvent): boolean;
/**
* Check if event has the platform's command modifier
* @param e - Keyboard event
* @returns True if command modifier is pressed
*/
hasCommandModifier(e: SyntheticKeyboardEvent): boolean;
};Usage Examples:
import { KeyBindingUtil } from "draft-js";
// Custom key binding function
function myKeyBindingFn(e) {
// Handle Ctrl+S / Cmd+S for save
if (e.keyCode === 83 && KeyBindingUtil.hasCommandModifier(e)) {
return 'save-content';
}
// Handle Ctrl+K / Cmd+K for link
if (e.keyCode === 75 && KeyBindingUtil.hasCommandModifier(e)) {
return 'add-link';
}
// Handle Alt+Arrow keys for word navigation
if (KeyBindingUtil.isOptionKeyCommand(e)) {
if (e.keyCode === 37) return 'move-word-left'; // Alt+Left
if (e.keyCode === 39) return 'move-word-right'; // Alt+Right
}
// Fall back to default key bindings
return getDefaultKeyBinding(e);
}
// Custom key command handler
function handleKeyCommand(command, editorState) {
switch (command) {
case 'save-content':
saveContent(editorState);
return 'handled';
case 'add-link':
addLink(editorState);
return 'handled';
case 'move-word-left':
// Custom word navigation logic
return 'handled';
default:
return RichUtils.handleKeyCommand(editorState, command);
}
}Default key binding function that maps keyboard events to Draft.js commands.
/**
* Default key binding function for Draft.js editor
* @param e - Keyboard event
* @returns Command string or null
*/
function getDefaultKeyBinding(e: SyntheticKeyboardEvent): ?string;Usage Examples:
import { getDefaultKeyBinding } from "draft-js";
// Extend default key bindings
function customKeyBindingFn(e) {
// Add custom bindings
if (e.keyCode === 9) { // Tab key
return 'indent';
}
if (e.keyCode === 13 && e.shiftKey) { // Shift+Enter
return 'soft-newline';
}
// Fall back to default bindings for standard commands
return getDefaultKeyBinding(e);
}
// Use in editor
<Editor
editorState={editorState}
onChange={setEditorState}
keyBindingFn={customKeyBindingFn}
handleKeyCommand={handleKeyCommand}
/>Utility function for generating unique keys for blocks and entities.
/**
* Generate unique key for blocks and entities
* @returns Random key string
*/
function genKey(): string;Usage Examples:
import { genKey, ContentBlock } from "draft-js";
// Create new content block with unique key
function createNewBlock(text, type = 'paragraph') {
return new ContentBlock({
key: genKey(),
type: type,
text: text,
characterList: List(Repeat(CharacterMetadata.EMPTY, text.length))
});
}
// Generate keys for custom entities
function createCustomEntity(type, mutability, data) {
const entityKey = genKey();
// Store entity with custom key...
return entityKey;
}Utility for getting the visible selection rectangle in the viewport.
/**
* Get visible selection rectangle
* @param window - Window object
* @returns ClientRect object or null if no visible selection
*/
function getVisibleSelectionRect(window: DOMWindow): ?ClientRect;Usage Examples:
import { getVisibleSelectionRect } from "draft-js";
// Position popup relative to selection
function showSelectionPopup() {
const selectionRect = getVisibleSelectionRect(window);
if (selectionRect) {
const popup = document.getElementById('selection-popup');
popup.style.display = 'block';
popup.style.top = `${selectionRect.top - 40}px`;
popup.style.left = `${selectionRect.left}px`;
}
}
// Hide popup when selection changes
function handleSelectionChange(editorState) {
const selection = editorState.getSelection();
if (selection.isCollapsed()) {
const popup = document.getElementById('selection-popup');
popup.style.display = 'none';
} else {
// Small delay to ensure DOM is updated
setTimeout(showSelectionPopup, 10);
}
}Utility for building BlockMap structures from arrays of ContentBlocks.
/**
* Utilities for building BlockMap structures
*/
const BlockMapBuilder: {
/**
* Create BlockMap from array of ContentBlocks
* @param blocks - Array of ContentBlock instances
* @returns OrderedMap of blocks keyed by block key
*/
createFromArray(blocks: Array<ContentBlock>): BlockMap;
};Usage Examples:
import { BlockMapBuilder, ContentBlock, CharacterMetadata } from "draft-js";
// Create blocks and build map
function createContentFromBlocks(blockData) {
const blocks = blockData.map(({ text, type = 'paragraph' }) => {
return new ContentBlock({
key: genKey(),
type: type,
text: text,
characterList: List(Repeat(CharacterMetadata.EMPTY, text.length))
});
});
return BlockMapBuilder.createFromArray(blocks);
}
// Use with ContentState
function createContentState(blockData) {
const blockMap = createContentFromBlocks(blockData);
return ContentState.createFromBlockArray(
blockMap.toArray(),
{} // entity map
);
}
// Example usage
const content = createContentState([
{ text: 'Welcome to Draft.js', type: 'header-one' },
{ text: 'This is a paragraph with some text.' },
{ text: 'This is another paragraph.' }
]);Additional utility functions for working with Draft.js content and editor state.
/**
* Advanced utility functions for Draft.js
*/
// Get all text content with formatting preserved
function getFormattedText(contentState: ContentState): Array<{
text: string,
style: DraftInlineStyle,
entityKey: ?string,
blockType: DraftBlockType
}> {
const result = [];
const blocks = contentState.getBlocksAsArray();
blocks.forEach(block => {
const text = block.getText();
const characterList = block.getCharacterList();
const blockType = block.getType();
for (let i = 0; i < text.length; i++) {
const char = text[i];
const metadata = characterList.get(i);
result.push({
text: char,
style: metadata.getStyle(),
entityKey: metadata.getEntity(),
blockType: blockType
});
}
});
return result;
}
// Find text occurrences with position info
function findText(contentState: ContentState, searchText: string): Array<{
blockKey: string,
start: number,
end: number,
text: string
}> {
const results = [];
const blocks = contentState.getBlocksAsArray();
blocks.forEach(block => {
const text = block.getText();
const blockKey = block.getKey();
let index = 0;
while ((index = text.indexOf(searchText, index)) !== -1) {
results.push({
blockKey,
start: index,
end: index + searchText.length,
text: searchText
});
index += searchText.length;
}
});
return results;
}
// Get word boundaries for selection
function getWordBoundaries(contentState: ContentState, blockKey: string, offset: number): {
start: number,
end: number
} {
const block = contentState.getBlockForKey(blockKey);
const text = block.getText();
// Find word start
let start = offset;
while (start > 0 && /\w/.test(text[start - 1])) {
start--;
}
// Find word end
let end = offset;
while (end < text.length && /\w/.test(text[end])) {
end++;
}
return { start, end };
}
// Select word at position
function selectWordAt(editorState: EditorState, blockKey: string, offset: number): EditorState {
const contentState = editorState.getCurrentContent();
const { start, end } = getWordBoundaries(contentState, blockKey, offset);
const selection = SelectionState.createEmpty(blockKey).merge({
anchorOffset: start,
focusOffset: end
});
return EditorState.forceSelection(editorState, selection);
}Complete Utility Example:
import React, { useState, useRef } from "react";
import {
Editor,
EditorState,
KeyBindingUtil,
getDefaultKeyBinding,
getVisibleSelectionRect,
RichUtils
} from "draft-js";
function AdvancedEditor() {
const [editorState, setEditorState] = useState(EditorState.createEmpty());
const [showPopup, setShowPopup] = useState(false);
const editorRef = useRef(null);
// Custom key binding with utilities
const keyBindingFn = (e) => {
// Save with Ctrl+S / Cmd+S
if (e.keyCode === 83 && KeyBindingUtil.hasCommandModifier(e)) {
return 'save';
}
// Show popup with Ctrl+/ / Cmd+/
if (e.keyCode === 191 && KeyBindingUtil.hasCommandModifier(e)) {
return 'show-popup';
}
return getDefaultKeyBinding(e);
};
// Handle key commands
const handleKeyCommand = (command, editorState) => {
if (command === 'save') {
console.log('Saving content...');
return 'handled';
}
if (command === 'show-popup') {
const rect = getVisibleSelectionRect(window);
if (rect) {
setShowPopup(true);
}
return 'handled';
}
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
setEditorState(newState);
return 'handled';
}
return 'not-handled';
};
// Handle selection change
const handleChange = (newEditorState) => {
setEditorState(newEditorState);
// Hide popup when selection changes
const selection = newEditorState.getSelection();
if (selection.isCollapsed()) {
setShowPopup(false);
}
};
return (
<div style={{ position: 'relative' }}>
<Editor
ref={editorRef}
editorState={editorState}
onChange={handleChange}
keyBindingFn={keyBindingFn}
handleKeyCommand={handleKeyCommand}
placeholder="Type here... (Ctrl+S to save, Ctrl+/ for popup)"
/>
{showPopup && (
<div
style={{
position: 'absolute',
backgroundColor: 'black',
color: 'white',
padding: '8px',
borderRadius: '4px',
fontSize: '12px',
zIndex: 1000
}}
>
Selection popup!
</div>
)}
</div>
);
}// Keyboard event type
type SyntheticKeyboardEvent = Event & {
keyCode: number,
which: number,
key: string,
ctrlKey: boolean,
metaKey: boolean,
altKey: boolean,
shiftKey: boolean,
preventDefault: () => void,
stopPropagation: () => void
};
// DOM Window type
type DOMWindow = Window;
// Client rectangle
interface ClientRect {
bottom: number;
height: number;
left: number;
right: number;
top: number;
width: number;
}
// Key binding function type
type KeyBindingFunction = (e: SyntheticKeyboardEvent) => ?string;
// Block map type
type BlockMap = OrderedMap<string, ContentBlock>;
// Search result type
interface SearchResult {
blockKey: string;
start: number;
end: number;
text: string;
}
// Word boundary type
interface WordBoundary {
start: number;
end: number;
}