The Markdown Toolbar is a comprehensive component that provides a user-friendly interface for editing Markdown content. It includes formatting options, special presentation features, and interactive popups for inserting links, images, and tables.
Answer: We didn't find any good, up-to-date, flexible, and bug-free solution. The strongest options were Milkdown and MDXEditor, both of which had significant bugs. Major issues included lack of flexibility, features like undo not working correctly, and inability to properly control themes in dark and light modes.
- Architecture
- Toolbar Sections
- Formatting Features
- Block-Level Formatting
- Interactive Popups
- Special Features
- API Reference
- Keyboard Shortcuts
- Component Structure
- Responsive Design
The toolbar is built using React functional components and hooks. It's organized into multiple sections, each serving a specific purpose:
MarkdownToolbar.tsx: Main toolbar componentImagePicker.tsx: Reusable image picker component (upload, URL, Unsplash)ToolbarButton.tsx: Reusable button componentToolbarDivider.tsx: Visual separator componentPopupForm.tsx: Generic popup form component with file upload support
src/features/editor/components/
├── MarkdownToolbar.tsx # Main toolbar component
├── ImagePicker.tsx # Reusable image picker component
└── toolbar/
├── ToolbarButton.tsx # Reusable button component
├── ToolbarDivider.tsx # Visual divider
├── PopupForm.tsx # Popup form component
└── index.ts # Export file
The toolbar consists of two main sections:
This section contains special presentation features and content generation tools:
- AI Generate: Opens AI content generation modal
- Confetti: Inserts confetti HTML comment (
<!-- confetti -->) - New Slide: Inserts slide separator (
---) - QR Code: Opens QR code feature (requires authentication)
- Live Polling: Opens live polling feature (requires authentication)
- Live Quiz: Opens live quiz feature (requires authentication)
- Q&A: Opens Q&A feature (requires authentication)
This section contains all Markdown formatting options:
Order (left to right):
- Undo/Redo: History management buttons
- Title Dropdown: Paragraph, Heading levels (1-6), and Quote
- Text Formatting: Bold, Italic, Underline, Strikethrough
- Code: Inline code and Code block
- Links & Media: Link, Image
- Table: Table generator
- Lists: Unordered and Ordered lists
Note: File operations (New, Open, Save) are handled via props (onOpenNewFileConfirmation, onOpenFile, onOpenSaveModal) but their UI buttons are not rendered in this toolbar component. They are handled by the parent ContentEditor component through modals.
- Syntax:
**text** - Behavior:
- Toggles formatting on/off
- Works with selected text or word at cursor
- Maintains selection after formatting
- Syntax:
_text_ - Behavior: Same as Bold
- Syntax:
<u>text</u> - Behavior: HTML-style underline formatting
- Syntax:
~~text~~ - Behavior: Same toggle behavior as Bold/Italic
- Syntax:
`code` - Behavior:
- Wraps selected text in backticks
- Toggles off if already formatted
- Works on single-line selections
- Converts selected text to plain paragraph
- Adds two empty lines before for spacing
- Removes existing formatting (headings, quotes)
- Syntax:
> text - Behavior:
- Converts selected text to quote
- Toggles off if already a quote
- Converts from heading if applicable
- Syntax:
# Heading 1through###### Heading 6 - Behavior:
- Converts selected text to heading
- Changes heading level if already a heading
- Toggles off if same level clicked again
- Converts from quote if applicable
- Syntax:
- itemor* item - Behavior:
- Converts selected lines to list items
- Toggles off if already a list
- Converts from ordered list if applicable
- Maintains selection after formatting
- Syntax:
1. itemor2. item - Behavior:
- Same as unordered list
- Converts from unordered list if applicable
- Syntax:
```\ncode\n``` - Behavior:
- Wraps multi-line selections in code block
- Wraps single-line selections in inline code
- Toggles off if inside code block
- Inserts markers with cursor between if no selection
Opens when clicking the Link button:
Fields:
- Link Text: The visible text for the link
- URL: The target URL
Behavior:
- Pre-fills "Link Text" if text is selected
- Closes on outside click
- Clears values when canceled
- Inserts:
[Link Text](URL)
Validation:
- Both fields required
- Submit button disabled if fields are empty
Opens when clicking the Image button:
Fields:
- Alt Text: Alternative text for the image
- Upload Image: File upload with drag-and-drop support
- Enter Image URL: Manual URL input
- Search Images from Unsplash: Opens Unsplash image search modal
Image Upload Features:
- Drag & Drop: Drag images directly into the upload area
- File Selection: Click to browse and select image files
- Paste Support: Paste images from clipboard (handled in ContentEditor)
- File Validation:
- Supported formats: JPG, PNG, GIF, WebP
- Maximum file size: 2MB
- Upload Progress: Real-time progress bar during upload
- Image Preview: Preview before upload
- Error Handling: Clear error messages for validation failures
Behavior:
- Pre-fills "Alt Text" if text is selected
- Three options available: Upload, URL, or Unsplash search
- Options are separated with dividers ("or")
- When file is selected, URL field is hidden
- When URL is entered, upload field is hidden
- Closes on outside click
- Clears values when canceled
- Inserts:

Validation:
- Either file upload or URL required
- Alt text required when uploading file
- Submit button disabled if no image source is provided
Opens when clicking the Table button:
Fields:
- Number of Columns: 1-10 (number input)
- Number of Rows: 1-20 (number input)
Behavior:
- Generates aligned Markdown table
- All cells have consistent width (12 characters)
- Includes header row with aligned separators
- Closes on outside click
Table Format:
| Header 1 | Header 2 | Header 3 |
| -------- | -------- | -------- |
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |A dropdown menu containing:
- Paragraph: Plain paragraph formatting
- --- (Separator)
- Title Level 1 through Title Level 6: Heading levels
- --- (Separator)
- Quote: Blockquote formatting
Order: Paragraph → Separator → Title Levels 1-6 → Separator → Quote
The toolbar includes comprehensive undo/redo functionality powered by the useUndoRedo hook.
- Undo: Reverts the last change made to the content
- Redo: Reapplies the last undone change
- History Management: Maintains up to 500 history states
- Smart Tracking: Distinguishes between programmatic changes (toolbar actions) and user typing
- Immediate Updates: Each character typed creates a new history entry (no debouncing)
- Visual Feedback: Buttons show disabled state when no history is available
- Undo:
Ctrl+Z(Windows/Linux) orCmd+Z(Mac) - Redo:
Ctrl+Y(Windows standard)Ctrl+Shift+Z(Linux/Mac standard)Cmd+Shift+Z(Mac)
Note: Shortcuts use e.code for language-independent detection, ensuring they work correctly with Persian, English, and other keyboard layouts.
History Storage:
- Maximum history size: 500 entries (configurable)
- Each entry stores a complete copy of the content
- Memory usage:
maxHistorySize × contentSize - For small/medium content (<100KB): 500 entries is safe (~50MB max)
- For large content (>500KB): Consider reducing to 100-200 entries
State Management:
- Uses
useReducerfor atomic state updates - Prevents stale closure issues
- Eliminates race conditions between history and index updates
Behavior:
- All changes (toolbar actions and typing) are added to history immediately
- Each character typed creates a separate history entry
- Undo/redo operations maintain cursor position when possible
- History is reset when new content is loaded from file
The undo/redo functionality is implemented in:
- Hook:
src/features/editor/hooks/useUndoRedo.ts - Integration:
src/features/editor/components/ContentEditor.tsx
The hook provides:
executeCommand(): For programmatic changes (toolbar)handleChange(): For user typingundo(): Revert to previous stateredo(): Reapply undone changereset(): Clear history (used when loading new file)
interface MarkdownToolbarProps {
// Text insertion
onInsert: (before: string, after?: string, placeholder?: string) => void;
// Formatting toggles
onToggleFormatting?: (marker: string, closingMarker?: string) => void;
onToggleList?: (listType: "unordered" | "ordered") => void;
// Block-level formatting
onApplyHeading?: (level: number) => void;
onApplyQuote?: () => void;
onApplyParagraph?: () => void;
onApplyCodeBlock?: () => void;
// File operations
onOpenNewFileConfirmation: () => void;
onOpenFile: () => void;
onOpenSaveModal: () => void;
// Modals
onOpenAIModal: () => void;
onOpenAuthModal: () => void;
// Utilities
getSelectedText?: () => string;
// Undo/Redo
onUndo?: () => void;
onRedo?: () => void;
canUndo?: boolean;
canRedo?: boolean;
// Styling
className?: string;
}// Table generation limits
const MAX_TABLE_COLUMNS = 10;
const MAX_TABLE_ROWS = 20;
const DEFAULT_TABLE_COLUMNS = 3;
const DEFAULT_TABLE_ROWS = 2;
const TABLE_CELL_WIDTH = 12;The toolbar supports standard keyboard shortcuts for undo/redo operations:
| Action | Windows/Linux | Mac |
|---|---|---|
| Undo | Ctrl+Z |
Cmd+Z |
| Redo | Ctrl+Y or Ctrl+Shift+Z |
Cmd+Shift+Z |
Implementation Notes:
- Uses
e.code(physical key) instead ofe.key(character) for language-independent detection - Works correctly with Persian, English, Arabic, and other keyboard layouts
- Physical key detection ensures consistency regardless of keyboard language setting
- Shortcuts are handled in
ContentEditor.tsxviaonKeyDownevent handler
The toolbar uses React hooks for state management:
// Dropdown states
const [showTitleDropdown, setShowTitleDropdown] = useState(false);
// Popup states
const [showLinkPopup, setShowLinkPopup] = useState(false);
const [showImagePopup, setShowImagePopup] = useState(false);
const [showTablePopup, setShowTablePopup] = useState(false);
// Form states
const [linkText, setLinkText] = useState("");
const [linkUrl, setLinkUrl] = useState("");
// ... similar for image and tableUsed for click-outside detection and storing selected text:
const titleDropdownRef = useRef<HTMLDivElement>(null);
const linkPopupRef = useRef<HTMLDivElement>(null);
const imagePopupRef = useRef<HTMLDivElement>(null);
const tablePopupRef = useRef<HTMLDivElement>(null);
// Store selected text for popups
const linkSelectedTextRef = useRef<string>("");
const imageSelectedTextRef = useRef<string>("");Implemented using useEffect hook with an array of refs and their corresponding close functions:
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
const refsWithClosers: Array<{
ref: React.RefObject<HTMLDivElement | null>;
closer: () => void;
}> = [
{ ref: titleDropdownRef, closer: closeTitleDropdown },
{ ref: linkPopupRef, closer: closeLinkPopup },
{ ref: imagePopupRef, closer: closeImagePopup },
{ ref: tablePopupRef, closer: closeTablePopup },
];
refsWithClosers.forEach(({ ref, closer }) => {
if (ref.current && !ref.current.contains(target)) {
closer();
}
});
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);- Popups positioned absolutely next to buttons
- Dropdowns positioned below buttons
- No backdrop overlay
- Standard z-index (
z-[100]for dropdowns,z-[9997]for popups)
- Popups centered on screen using
fixedpositioning - Backdrop overlay (dark background) for focus
- Clicking backdrop closes popup
- Full-width popups with margins (
w-[calc(100vw-2rem)]) - Larger touch targets for buttons and inputs
- z-index:
z-[9997]for popups,z-[9996]for backdrop
const POPUP_CLASSES =
"fixed sm:absolute sm:top-full sm:left-0 sm:mt-1 " +
"top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 " +
"sm:translate-x-0 sm:translate-y-0 " +
"bg-white dark:bg-gray-800 border border-input rounded-md " +
"shadow-xl z-[9997] p-4 " +
"min-w-[280px] max-w-[calc(100vw-2rem)] " +
"w-[calc(100vw-2rem)] sm:w-auto sm:shadow-lg";
const POPUP_BACKDROP_CLASSES = "fixed inset-0 bg-black/50 z-[9996] sm:hidden";The toolbar includes a generateMarkdownTable function that creates aligned Markdown tables:
function generateMarkdownTable(columns: number, rows: number): string {
// Generates aligned Markdown table with:
// - Header row with numbered headers (Header 1, Header 2, ...)
// - Separator row with dashes
// - Data rows with numbered cells (Cell 1, Cell 2, ...)
// - All cells padded to consistent width (12 characters)
// Uses padCell utility for consistent column widths
}Example Output:
| Header 1 | Header 2 | Header 3 |
| -------- | -------- | -------- |
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |The toolbar uses a factory function to create formatting handlers with toggle behavior:
const createToggleFormatter =
(marker: string, closingMarker?: string, fallbackPlaceholder?: string) =>
() => {
// If onToggleFormatting is provided, use it
// Otherwise, fallback to onInsert with markers
// Creates a formatting function with toggle behavior
// Handles selected text and empty selection cases
};This factory is used to create all inline formatting functions:
formatBold:createToggleFormatter("**", undefined, "bold text")formatItalic:createToggleFormatter("_", undefined, "italic text")formatUnderline:createToggleFormatter("<u>", "</u>", "underlined text")formatStrikethrough:createToggleFormatter("~~", undefined, "strikethrough text")formatCode:createToggleFormatter("\", undefined, "code")`
The toolbar is integrated into ContentEditor.tsx:
<MarkdownToolbar
onInsert={insertText}
onToggleFormatting={toggleFormatting}
onToggleList={toggleList}
onApplyHeading={applyHeading}
onApplyQuote={applyQuote}
onApplyParagraph={applyParagraph}
onApplyCodeBlock={applyCodeBlock}
// ... other props
/>- Selection Preservation: All formatting functions maintain text selection after applying changes
- Toggle Behavior: Formatting buttons toggle on/off based on current state
- Smart Defaults: Pre-fills popup forms with selected text when available
- Accessibility: All buttons have descriptive titles and tooltips
- Responsive: Works seamlessly on mobile and desktop
- Language Independence: Keyboard shortcuts work regardless of keyboard layout
The image upload feature uses a dedicated service that handles:
- File Validation: Type and size validation (max 2MB)
- Base64 Encoding: Converts image to base64 for API transmission
- Progress Tracking: Real-time upload progress updates
- Error Handling: Comprehensive error messages
- S3 Integration: Uploads to AWS S3 via Lambda function
- Authentication: Requires user authentication (AWS Cognito)
Service Location: src/features/editor/services/imageUploadService.ts
Backend Integration:
- Lambda function:
backend/src/lambda/images/upload.ts - API endpoint:
POST /images/upload - Storage: AWS S3 bucket with public read access
- CORS: Configured for cross-origin requests
Potential improvements for future versions:
- Custom table styling options
- Link validation
- Formatting presets/templates
- Undo/redo for individual formatting actions
- Emoji picker integration
- Image editing (crop, resize)
- Multiple image upload