# MediumEditor
## Introduction
MediumEditor is a vanilla JavaScript WYSIWYG editor that clones the inline text editing experience from Medium.com. Built without any framework dependencies, it provides a clean, intuitive interface for rich text editing in web applications. The library transforms HTML elements into contenteditable regions with a floating toolbar that appears on text selection, supporting standard formatting operations like bold, italic, headings, and links.
The editor is designed for flexibility and extensibility, offering comprehensive options for customizing toolbar behavior, paste handling, keyboard shortcuts, and visual appearance through themes. It supports multiple simultaneous editable elements, textarea integration with automatic synchronization, and a robust event system for integrating with application logic. With built-in extensions for anchor previews, placeholders, auto-linking, and image handling, MediumEditor provides a complete solution for adding Medium-style editing capabilities to any web application.
## API Reference
### Basic Initialization
Initialize the editor on HTML elements with contenteditable support.
```javascript
// Initialize on elements with class selector
var editor = new MediumEditor('.editable');
// Initialize on specific DOM elements
var elements = document.querySelectorAll('.editable');
var editor = new MediumEditor(elements);
// Initialize with custom options
var editor = new MediumEditor('.editable', {
toolbar: {
buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote']
},
placeholder: {
text: 'Type your text here...',
hideOnClick: true
}
});
// Initialize on textarea (automatically creates contenteditable div)
//
var editor = new MediumEditor('textarea.editable');
// Creates hidden textarea with synced contenteditable div
```
### Toolbar Configuration
Customize the floating toolbar appearance, positioning, and button set.
```javascript
var editor = new MediumEditor('.editable', {
toolbar: {
allowMultiParagraphSelection: true,
buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote',
'orderedlist', 'unorderedlist', 'pre', 'strikethrough'],
diffLeft: 0,
diffTop: -10,
firstButtonClass: 'medium-editor-button-first',
lastButtonClass: 'medium-editor-button-last',
standardizeSelectionStart: false,
static: false,
align: 'center',
sticky: false,
updateOnEmptySelection: false
}
});
// Custom button configuration with overrides
var editor = new MediumEditor('.editable', {
toolbar: {
buttons: [
'bold',
'italic',
{
name: 'h1',
action: 'append-h2',
aria: 'header type 1',
tagNames: ['h2'],
contentDefault: 'H1',
classList: ['custom-class-h1'],
attrs: {
'data-custom-attr': 'attr-value-h1'
}
},
{
name: 'customButton',
contentDefault: '✓',
classList: ['custom-button'],
action: 'bold',
aria: 'custom bold button'
}
]
}
});
// Static toolbar (always visible)
var editor = new MediumEditor('.editable', {
toolbar: {
static: true,
sticky: true,
align: 'left',
updateOnEmptySelection: true
}
});
// Disable toolbar completely
var editor = new MediumEditor('.editable', {
toolbar: false
});
```
### Content Management
Retrieve, set, and manage editor content programmatically.
```javascript
var editor = new MediumEditor('.editable');
// Get content from first element
var content = editor.getContent();
console.log(content); // "
Hello world
"
// Get content from specific element by index
var content = editor.getContent(1);
// Set content for first element
editor.setContent('New content here
');
// Set content for specific element
editor.setContent('Title
Paragraph
', 1);
// Serialize all editor elements to JSON
var data = editor.serialize();
console.log(data);
// {"element-0": {"value": "Content 1
"}, "element-1": {"value": "Content 2
"}}
// Reset content to initial state
editor.resetContent(); // Resets all elements
editor.resetContent(document.querySelector('.editable')); // Reset specific element
// Check if content has changed
editor.checkContentChanged(); // Check active element
editor.checkContentChanged(document.querySelector('.editable')); // Check specific element
```
### Event System
Subscribe to custom events for tracking editor state and user interactions.
```javascript
var editor = new MediumEditor('.editable');
// Listen to content changes
editor.subscribe('editableInput', function (event, editable) {
console.log('Content changed in:', editable);
console.log('New content:', editable.innerHTML);
// Send to server, update preview, etc.
});
// Listen to focus events
editor.subscribe('focus', function (event, editable) {
console.log('Editor focused:', editable);
editable.style.border = '2px solid blue';
});
// Listen to blur events
editor.subscribe('blur', function (event, editable) {
console.log('Editor blurred:', editable);
editable.style.border = '1px solid #ccc';
// Save content when user leaves editor
saveContent(editable.innerHTML);
});
// Listen to toolbar events
editor.subscribe('showToolbar', function (event, editable) {
console.log('Toolbar shown');
});
editor.subscribe('hideToolbar', function (event, editable) {
console.log('Toolbar hidden');
});
// Listen to element addition/removal
editor.subscribe('addElement', function (data, editable) {
console.log('Element added:', editable);
});
editor.subscribe('removeElement', function (data, editable) {
console.log('Element removed:', editable);
});
// Unsubscribe from events
var inputHandler = function(event, editable) {
console.log('Input detected');
};
editor.subscribe('editableInput', inputHandler);
editor.unsubscribe('editableInput', inputHandler);
// Manually trigger custom events
editor.trigger('customEvent', { data: 'value' }, document.querySelector('.editable'));
```
### Selection Management
Control and manipulate text selection within the editor.
```javascript
var editor = new MediumEditor('.editable');
// Save current selection
editor.saveSelection();
// Do some work...
someAsyncOperation().then(function() {
// Restore previously saved selection
editor.restoreSelection();
});
// Export selection state (for persistence)
var selectionState = editor.exportSelection();
console.log(selectionState);
// {start: 5, end: 10, editableElementIndex: 0}
localStorage.setItem('selection', JSON.stringify(selectionState));
// Import selection state
var savedState = JSON.parse(localStorage.getItem('selection'));
editor.importSelection(savedState);
// Get focused element
var focusedElement = editor.getFocusedElement();
if (focusedElement) {
console.log('Currently editing:', focusedElement);
}
// Get parent element of selection
var parentElement = editor.getSelectedParentElement();
console.log('Selection is within:', parentElement);
// Select all content in focused element
editor.selectAllContents();
// Select specific element
var elementToSelect = document.querySelector('.highlight');
editor.selectElement(elementToSelect);
// Stop toolbar updates during bulk operations
editor.stopSelectionUpdates();
performBulkEdits();
editor.startSelectionUpdates();
// Manually trigger toolbar update
editor.checkSelection();
```
### Editor Actions
Execute formatting commands and content manipulation.
```javascript
var editor = new MediumEditor('.editable');
// Execute built-in commands
editor.execAction('bold');
editor.execAction('italic');
editor.execAction('underline');
editor.execAction('strikethrough');
editor.execAction('append-h2'); // Create H2 heading
editor.execAction('append-h3'); // Create H3 heading
editor.execAction('append-blockquote');
// Create links programmatically
editor.createLink({
value: 'https://github.com/yabwe/medium-editor',
target: '_blank',
buttonClass: 'custom-link-class'
});
// Paste HTML at cursor
editor.pasteHTML('Inserted HTML content
');
// Paste HTML with cleaning options
editor.pasteHTML('Clean me
', {
cleanAttrs: ['class', 'style'],
cleanTags: ['script', 'style'],
unwrapTags: ['span']
});
// Clean paste (convert to plain text)
editor.cleanPaste('Plain text to insert');
// Query command state
var isBold = editor.queryCommandState('bold');
console.log('Is text bold?', isBold);
var isItalic = editor.queryCommandState('italic');
console.log('Is text italic?', isItalic);
```
### Dynamic Element Management
Add and remove editable elements dynamically after initialization.
```javascript
var editor = new MediumEditor('.editable');
// Subscribe to events before adding elements
editor.subscribe('editableInput', function(event, editable) {
console.log('Content changed');
});
// Add new elements dynamically
// HTML: New content area
editor.addElements('.new-editable');
// Add specific DOM elements
var newDiv = document.createElement('div');
newDiv.className = 'dynamic-editor';
newDiv.innerHTML = 'Dynamic content';
document.body.appendChild(newDiv);
editor.addElements(newDiv);
// Add multiple elements at once
var newElements = document.querySelectorAll('.dynamic');
editor.addElements(newElements);
// Remove elements dynamically
editor.removeElements('.old-editable');
editor.removeElements(document.querySelector('#remove-me'));
// Clean up removed elements (useful in SPAs)
var removedElements = [];
editor.elements.forEach(function(element) {
if (!document.body.contains(element)) {
removedElements.push(element);
}
});
editor.removeElements(removedElements);
```
### Lifecycle Management
Initialize, destroy, and reinitialize the editor.
```javascript
// Create editor instance
var editor = new MediumEditor('.editable', {
toolbar: {
buttons: ['bold', 'italic', 'underline']
}
});
// Destroy editor (removes event listeners and toolbar)
editor.destroy();
// Elements remain contenteditable but MediumEditor is torn down
// Reinitialize with same configuration
editor.setup();
// Get editor instance from element
var element = document.querySelector('.editable');
var editor = MediumEditor.getEditorFromElement(element);
if (editor) {
console.log('Editor found:', editor);
console.log('Version:', editor.constructor.version.toString());
}
// Check version
console.log('MediumEditor version:', MediumEditor.version.toString());
// "5.23.3"
```
### Anchor and Link Management
Configure link creation and preview behavior.
```javascript
var editor = new MediumEditor('.editable', {
toolbar: {
buttons: ['bold', 'italic', 'anchor']
},
anchor: {
customClassOption: 'btn',
customClassOptionText: 'Button Style',
linkValidation: true,
placeholderText: 'Paste or type a link',
targetCheckbox: true,
targetCheckboxText: 'Open in new window'
},
anchorPreview: {
hideDelay: 500,
previewValueSelector: 'a',
showWhenToolbarIsVisible: false,
showOnEmptyLinks: true
}
});
// Disable anchor preview
var editor = new MediumEditor('.editable', {
anchorPreview: false
});
```
### Paste Handling
Control how pasted content is processed and cleaned.
```javascript
var editor = new MediumEditor('.editable', {
paste: {
// Force plain text pasting
forcePlainText: true,
// Clean pasted HTML from external sources
cleanPastedHTML: false,
// Remove specific attributes
cleanAttrs: ['class', 'style', 'dir'],
// Remove specific tags entirely
cleanTags: ['meta', 'script', 'style'],
// Unwrap tags (keep content, remove wrapper)
unwrapTags: ['span', 'div'],
// Custom replacements before built-in cleaning
preCleanReplacements: [
[/\u00A0/g, ' '] // Replace non-breaking spaces
],
// Custom replacements after built-in cleaning
cleanReplacements: [
[/
/gi, '\n'] // Convert BR to newlines
]
}
});
// Advanced paste cleaning
var editor = new MediumEditor('.editable', {
paste: {
forcePlainText: false,
cleanPastedHTML: true,
cleanAttrs: ['style', 'class', 'id', 'name'],
cleanTags: ['meta', 'script', 'style', 'iframe'],
unwrapTags: ['font', 'span']
}
});
```
### Keyboard Commands
Customize keyboard shortcuts and commands.
```javascript
var editor = new MediumEditor('.editable', {
keyboardCommands: {
commands: [
{
command: 'bold',
key: 'B',
meta: true,
shift: false,
alt: false
},
{
command: 'italic',
key: 'I',
meta: true,
shift: false,
alt: false
},
{
command: 'underline',
key: 'U',
meta: true,
shift: false,
alt: false
},
{
command: 'append-h2',
key: '1',
meta: true,
shift: true,
alt: false
},
{
command: 'append-h3',
key: '2',
meta: true,
shift: true,
alt: false
}
]
}
});
// Disable keyboard commands
var editor = new MediumEditor('.editable', {
keyboardCommands: false
});
// Disable specific keyboard command
var editor = new MediumEditor('.editable', {
keyboardCommands: {
commands: [
{
command: false, // Disable this shortcut
key: 'B',
meta: true,
shift: false,
alt: false
}
]
}
});
```
### Auto-Linking and Image Handling
Enable automatic URL detection and configure image drag-and-drop.
```javascript
// Enable auto-linking (URLs become clickable links automatically)
var editor = new MediumEditor('.editable', {
autoLink: true
});
// Disable image dragging
var editor = new MediumEditor('.editable', {
imageDragging: false
});
// Completely disable file dragging (no drag/drop prevention)
var editor = new MediumEditor('.editable', {
extensions: {
'imageDragging': {} // Empty extension disables default behavior
}
});
```
### Core Editor Options
Configure fundamental editor behavior and constraints.
```javascript
var editor = new MediumEditor('.editable', {
// Delay before showing toolbar (milliseconds)
delay: 1000,
// Disable return key
disableReturn: false,
// Disable double return (prevent multiple blank lines)
disableDoubleReturn: false,
// Disable extra spaces (trim spaces, prevent multiple consecutive spaces)
disableExtraSpaces: false,
// Disable editing (useful for read-only with toolbar actions)
disableEditing: false,
// Enable spellcheck
spellcheck: true,
// Add target="_blank" to all links
targetBlank: true,
// CSS class for active toolbar buttons
activeButtonClass: 'medium-editor-button-active',
// Button label type: false or 'fontawesome'
buttonLabels: false,
// Container for toolbar elements (default: document.body)
elementsContainer: document.body
});
// Per-element options via data attributes
//
//
```
### Extensions and Custom Functionality
Extend editor functionality with custom extensions.
```javascript
// Custom button extension
var MyCustomButton = MediumEditor.Extension.extend({
name: 'mybutton',
init: function() {
this.button = this.document.createElement('button');
this.button.classList.add('medium-editor-action');
this.button.innerHTML = 'M';
this.button.title = 'My Custom Action';
this.on(this.button, 'click', this.handleClick.bind(this));
},
getButton: function() {
return this.button;
},
handleClick: function(event) {
event.preventDefault();
event.stopPropagation();
this.base.execAction('bold');
// Custom logic here
}
});
// Use custom extension
var editor = new MediumEditor('.editable', {
toolbar: {
buttons: ['bold', 'italic', 'mybutton']
},
extensions: {
'mybutton': new MyCustomButton()
}
});
// Access extensions by name
var toolbar = editor.getExtensionByName('toolbar');
var anchorPreview = editor.getExtensionByName('anchor-preview');
console.log('Toolbar extension:', toolbar);
```
### Placeholder Configuration
Customize empty editor placeholder text and behavior.
```javascript
var editor = new MediumEditor('.editable', {
placeholder: {
text: 'Start typing your article...',
hideOnClick: true // Hide immediately on focus
}
});
// Show placeholder while empty (hide only when typing)
var editor = new MediumEditor('.editable', {
placeholder: {
text: 'Enter content here',
hideOnClick: false // Only hide when content is added
}
});
// Disable placeholder
var editor = new MediumEditor('.editable', {
placeholder: false
});
// Per-element placeholder via data attribute
//
```
### Helper Methods
Utility functions for working with the editor.
```javascript
var editor = new MediumEditor('.editable');
// Delay function execution
editor.delay(function() {
console.log('Executed after delay option timeout');
editor.checkSelection();
});
// Direct DOM event handling (auto-cleanup on destroy)
editor.on(document.querySelector('.custom-button'), 'click', function(event) {
console.log('Button clicked');
editor.execAction('bold');
}, false);
// Remove event listener
var handler = function(event) {
console.log('Clicked');
};
editor.on(element, 'click', handler, false);
editor.off(element, 'click', handler, false);
```
## Use Cases and Integration
MediumEditor is ideal for implementing rich text editing in content management systems, blogging platforms, commenting systems, and collaborative writing tools. Its lightweight footprint and framework-agnostic design make it suitable for integration into any web application without introducing heavy dependencies. Common use cases include article editors, note-taking applications, email composition interfaces, and inline content editing for CMS platforms.
The editor integrates seamlessly with modern JavaScript frameworks through community-maintained wrappers for React, Angular, Vue, and other popular frameworks. Its event-driven architecture allows for easy integration with autosave functionality, version control systems, and real-time collaboration features. The extensive configuration options enable developers to tailor the editing experience to match their application's requirements, from minimal formatting toolbars for simple comments to full-featured editors for long-form content. With built-in support for custom extensions, keyboard shortcuts, and paste handling, MediumEditor provides a solid foundation for building sophisticated content editing experiences while maintaining the clean, distraction-free aesthetic that made Medium.com's editor popular.