A comprehensive Notion-like editor for Laravel using Editor.js with advanced features
rafaelogic/writr is a Laravel package for a comprehensive notion-like editor for laravel using editor.js with advanced features.
It currently has 0 GitHub stars and 1 downloads on Packagist (latest version v1.1.0).
Install it with composer require rafaelogic/writr.
Discover more Laravel packages by rafaelogic
or browse all Laravel packages to compare alternatives.
Last updated
A production-ready, comprehensive Notion-like editor for Laravel applications using Editor.js. Features a complete block-based editing experience with advanced tools, file handling, and seamless Laravel integration.
Experience all features of Writr Editor including:
<x-writr-editor /> component with self-contained renderingcomposer require rafaelogic/writr
The package will auto-register via Laravel's package discovery.
# Publish configuration file (recommended for customization)
php artisan vendor:publish --tag=writr-config
# Publish all Blade views (rarely needed)
php artisan vendor:publish --tag=writr-views
# Publish compiled assets (CSS/JS) to public directory
php artisan vendor:publish --tag=writr-assets
# Publish all package files
php artisan vendor:publish --provider="Rafaelogic\Writr\WritrServiceProvider"
Note: The Writr component is self-contained and renders directly from PHP for maximum compatibility. Publishing is typically only needed for configuration changes.
Include required dependencies in your layout:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Writr Styles -->
@stack('writr-styles')
</head>
<body>
<!-- Your content -->
<!-- Writr Scripts -->
@stack('writr-scripts')
</body>
</html>
The simplest way to get started:
{{-- In your Blade template --}}
<form action="/save-document" method="POST">
@csrf
<x-writr-editor
name="content"
:value="old('content', $document->content ?? '')"
placeholder="Start writing your amazing content..."
/>
<button type="submit" class="btn btn-primary">Save Document</button>
</form>
// In your Controller
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|json' // Writr content is stored as JSON
]);
// Optionally validate the structure
$contentData = json_decode($validated['content'], true);
if (!isset($contentData['blocks']) || empty($contentData['blocks'])) {
return back()->withErrors(['content' => 'Content cannot be empty.']);
}
Document::create($validated);
return redirect()->route('documents.index')
->with('success', 'Document saved successfully!');
}
<x-writr-editor
name="content"
id="advanced-editor"
:value="$document->content"
:config="[
'placeholder' => 'Start your masterpiece...',
'autofocus' => true,
'features' => [
'darkMode' => true,
'wordCount' => true,
'tableOfContents' => true,
'autoSave' => [
'enabled' => true,
'interval' => 10000, // 10 seconds
'endpoint' => route('documents.autosave', $document)
]
],
'tools' => [
'embed' => false, // Disable embeds
'raw' => false, // Disable raw HTML
'warning' => true, // Enable warning blocks
]
]"
class="min-h-96 border rounded-lg"
required
/>
The Writr Blade component uses self-contained PHP rendering for maximum compatibility and reliability. Unlike traditional Blade templates, the component renders its HTML, CSS, and JavaScript directly from the PHP class to ensure:
This approach ensures the editor works out-of-the-box without requiring publishing or customization for most use cases.
The configuration file provides extensive customization options. You can manage settings in two ways:
Access the visual settings interface at /writr/settings for an intuitive configuration experience:
# Visit in your browser
https://yourapp.com/writr/settings
For programmatic or advanced configuration, publish the config file:
php artisan vendor:publish --tag=writr-config
// config/writr.php
'editor' => [
'placeholder' => 'Start writing your story...',
'autofocus' => false,
'readonly' => false,
'min_height' => 300,
'max_height' => null,
'spellcheck' => true,
],
Each tool can be enabled/disabled and configured:
'tools' => [
'header' => [
'enabled' => true,
'config' => [
'levels' => [1, 2, 3, 4, 5, 6],
'defaultLevel' => 2,
'allowAnchor' => true,
],
],
'image' => [
'enabled' => true,
'config' => [
'endpoints' => [
'byFile' => route('writr.upload.image'),
'byUrl' => route('writr.upload.url'),
],
'additionalRequestData' => [
'_token' => csrf_token(),
],
'field' => 'image',
'types' => 'image/*',
'captionPlaceholder' => 'Enter caption...',
],
],
'table' => [
'enabled' => true,
'config' => [
'rows' => 2,
'cols' => 3,
'withHeadings' => true,
],
],
// ... more tools
],
'features' => [
'drag_drop' => true,
'undo_redo' => true,
'dark_mode' => [
'enabled' => true,
'default' => 'auto', // 'light', 'dark', 'auto'
'storage_key' => 'writr-theme',
],
'live_preview' => true,
'table_of_contents' => [
'enabled' => true,
'levels' => [1, 2, 3, 4, 5, 6],
'position' => 'right', // 'left', 'right', 'top'
],
'word_count' => [
'enabled' => true,
'position' => 'bottom', // 'top', 'bottom'
'show_characters' => true,
'show_words' => true,
'show_reading_time' => true,
],
'auto_save' => [
'enabled' => false,
'interval' => 30000, // 30 seconds
'endpoint' => null,
'debounce' => 1000,
],
],
// config/writr.php
'uploads' => [
'disk' => 'public',
'path' => 'writr/uploads',
'max_file_size' => 10 * 1024 * 1024, // 10MB
'allowed_extensions' => [
'images' => ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
'documents' => ['pdf', 'doc', 'docx', 'txt', 'rtf'],
'archives' => ['zip', 'rar', '7z'],
],
'image_optimization' => [
'enabled' => true,
'quality' => 85,
'max_width' => 1920,
'max_height' => 1080,
],
],
Create your own upload endpoint:
// routes/web.php
Route::post('/custom-upload', [CustomUploadController::class, 'handle'])
->name('custom.upload')
->middleware(['auth', 'throttle:60,1']);
// CustomUploadController.php
public function handle(Request $request)
{
$request->validate([
'image' => 'required|image|max:10240', // 10MB
]);
$file = $request->file('image');
// Custom processing (resize, watermark, etc.)
$processedImage = $this->processImage($file);
$path = $processedImage->store('custom-uploads', 'public');
return response()->json([
'success' => 1,
'file' => [
'url' => Storage::url($path),
'name' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'caption' => '',
]
]);
}
<x-writr-editor
name="content"
:config="[
'uploads' => [
'progress_callback' => 'uploadProgress',
'validation' => [
'max_size' => '5MB',
'allowed_types' => ['image/jpeg', 'image/png'],
'required_dimensions' => ['min_width' => 300, 'min_height' => 200]
]
]
]"
/>
<script>
function uploadProgress(percentComplete) {
console.log('Upload progress:', percentComplete + '%');
// Update progress bar, etc.
}
</script>
Writr stores content as JSON. Use the included service for processing:
use Rafaelogic\Writr\Services\WritrService;
class DocumentController extends Controller
{
public function __construct(
private WritrService $writr
) {}
public function show(Document $document)
{
$content = json_decode($document->content, true);
// Convert to HTML for display
$html = $this->writr->toHtml($content);
// Convert to Markdown
$markdown = $this->writr->toMarkdown($content);
// Extract plain text (for search, etc.)
$plainText = $this->writr->toText($content);
// Get word count and reading time
$stats = $this->writr->getContentStats($content);
// Returns: ['words' => 245, 'characters' => 1428, 'reading_time' => 1.2]
// Generate table of contents
$toc = $this->writr->generateToc($content);
return view('documents.show', [
'document' => $document,
'html' => $html,
'stats' => $stats,
'toc' => $toc,
]);
}
}
// Custom validation rule
$request->validate([
'content' => [
'required',
'json',
function ($attribute, $value, $fail) {
$data = json_decode($value, true);
// Validate structure
if (!isset($data['blocks']) || !is_array($data['blocks'])) {
$fail('Invalid content structure.');
}
// Validate content is not empty
if (empty($data['blocks'])) {
$fail('Content cannot be empty.');
}
// Validate each block
foreach ($data['blocks'] as $block) {
if (!isset($block['type']) || !isset($block['data'])) {
$fail('Invalid block structure.');
}
}
}
]
]);
// Make content searchable
class Document extends Model
{
protected $casts = [
'content' => 'array',
];
// Accessor for search indexing
public function getSearchableContentAttribute()
{
return app(WritrService::class)->toText($this->content);
}
// Scope for searching
public function scopeSearch($query, $term)
{
return $query->whereRaw(
"JSON_UNQUOTE(JSON_EXTRACT(content, '$.blocks[*].data.text')) LIKE ?",
["%{$term}%"]
);
}
}
<x-writr-editor
name="content"
:config="[
'autoSave' => [
'enabled' => true,
'interval' => 5000, // 5 seconds
'endpoint' => '/custom-auto-save'
]
]"
/>
<x-writr-editor
name="content"
:config="[
'theme' => [
'default' => 'dark',
'allowToggle' => true
]
]"
/>
// config/writr.php
'theme' => [
'css_variables' => [
'--writr-primary-color' => '#your-color',
'--writr-bg-color' => '#your-bg-color',
],
],
The package provides several API endpoints for editor functionality:
POST /writr/upload-image - Upload imagesPOST /writr/upload-file - Upload filesPOST /writr/fetch-url - Fetch URL metadataPOST /writr/preview - Generate content previewPOST /writr/export - Export contentPOST /writr/auto-save - Auto-save contentFor advanced customization, use the JavaScript API directly:
import WritrEditor from './vendor/writr/js/writr.esm.js';
// Initialize editor
const editor = new WritrEditor({
holder: 'editor-container',
data: {
blocks: []
},
tools: {
// Custom tool configuration
},
features: {
darkMode: true,
autoSave: {
enabled: true,
interval: 15000,
endpoint: '/api/auto-save'
}
},
onChange: (data) => {
console.log('Content changed:', data);
// Handle changes
},
onReady: () => {
console.log('Editor is ready');
}
});
// Editor methods
editor.save().then(data => {
console.log('Saved data:', data);
});
editor.clear();
editor.render(newData);
editor.focus();
Create your own Editor.js tools:
// CustomAlertTool.js
class CustomAlertTool {
static get toolbox() {
return {
title: 'Alert',
icon: '<svg>...</svg>'
};
}
constructor({ data, api }) {
this.data = data;
this.api = api;
this.wrapper = undefined;
}
render() {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('custom-alert');
const input = document.createElement('input');
input.placeholder = 'Enter alert message...';
input.value = this.data.text || '';
input.addEventListener('input', (e) => {
this.data.text = e.target.value;
});
this.wrapper.appendChild(input);
return this.wrapper;
}
save() {
return {
text: this.data.text || '',
level: this.data.level || 'info'
};
}
validate(savedData) {
return savedData.text && savedData.text.trim() !== '';
}
}
// Register tool
const editor = new WritrEditor({
tools: {
alert: CustomAlertTool
}
});
// Listen to editor events
editor.on('change', (data) => {
// Content changed
localStorage.setItem('draft', JSON.stringify(data));
});
editor.on('focus', () => {
// Editor focused
document.body.classList.add('editor-focused');
});
editor.on('blur', () => {
// Editor lost focus
document.body.classList.remove('editor-focused');
});
editor.on('tool-change', (toolName) => {
// Active tool changed
console.log('Active tool:', toolName);
});
# Clone or install the package
composer require rafaelogic/writr
# Install frontend dependencies
cd vendor/rafaelogic/writr # or your package directory
npm install
# Start development server
npm run dev
# Watch for changes
npm run watch
# Development build (with source maps)
npm run development
# Production build (optimized, minified)
npm run production
# Bundle build (standalone distribution)
npm run bundle
# Run PHP tests
composer test
# Run with coverage
composer test-coverage
# Run JavaScript tests
npm test
# Watch mode for JS tests
npm run test:watch
# Lint code
npm run lint
npm run lint:fix
writr/
βββ config/writr.php # Configuration file
βββ src/ # PHP source code
β βββ WritrServiceProvider.php # Service provider
β βββ Components/ # Blade components
β βββ Http/Controllers/ # Controllers
β βββ Services/ # Services
βββ resources/
β βββ js/ # JavaScript source
β β βββ writr.js # Main entry point
β β βββ editor/ # Editor modules
β β βββ tools/ # Custom tools
β β βββ utils/ # Utilities
β βββ css/writr.css # Compiled CSS
β βββ sass/writr.scss # Sass source
β βββ views/ # Blade templates
βββ public/ # Compiled assets
β βββ js/writr.js # Main bundle
β βββ js/writr.min.js # Minified bundle
β βββ css/writr.css # Compiled CSS
β βββ css/writr.min.css # Minified CSS
βββ tests/ # Test files
βββ Feature/ # Feature tests
βββ Unit/ # Unit tests
βββ frontend/ # JS tests
// config/writr.php - Production settings
'performance' => [
'lazy_load_tools' => true,
'cache_assets' => true,
'minify_output' => true,
'debounce_delay' => 300,
'memory_monitoring' => env('APP_ENV') === 'local',
],
// Image optimization
'uploads' => [
'image_optimization' => [
'enabled' => true,
'quality' => 85,
'progressive' => true,
'strip_metadata' => true,
],
],
// Automatic memory cleanup
const editor = new WritrEditor({
features: {
memoryMonitoring: true, // Monitor memory usage
autoCleanup: true, // Automatic cleanup
maxMemoryUsage: 50 // Max memory in MB
}
});
// Manual cleanup
editor.destroy(); // Clean up when done
# Analyze bundle size
npm run bundle -- --analyze
# Check performance
npm run lighthouse
| Browser | Version | Status | |---------|---------|--------| | Chrome | 70+ | β Full Support | | Firefox | 65+ | β Full Support | | Safari | 12+ | β Full Support | | Edge | 79+ | β Full Support | | Opera | 57+ | β Full Support | | Mobile Safari | 12+ | β Full Support | | Chrome Mobile | 70+ | β Full Support |
For older browsers, include polyfills:
<!-- For IE11 and older browsers -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
We welcome contributions! Here's how to get started:
Fork and Clone
git clone https://github.com/yourusername/writr.git
cd writr
Install Dependencies
composer install
npm install
Set Up Testing Environment
cd tests/TestApp
composer install
npm install
Run Tests
# PHP tests
composer test
# JavaScript tests
npm test
# Full test suite
npm run test:all
develop branchdevelopThis package is open-sourced software licensed under the MIT license.
Made with β€οΈ for the Laravel community
Website β’ Documentation β’ Demo β’ GitHub